Skip to content

Commit e5c5760

Browse files
committed
fixes
1 parent 80ec7da commit e5c5760

File tree

6 files changed

+626
-0
lines changed

6 files changed

+626
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
10+
"github.com/spf13/cobra"
11+
12+
"github.com/pushchain/push-validator-cli/internal/config"
13+
"github.com/pushchain/push-validator-cli/internal/node"
14+
"github.com/pushchain/push-validator-cli/internal/process"
15+
)
16+
17+
var internalRefreshHome string
18+
19+
// internalRefreshCmd is a hidden command used by the cron job to refresh peers.
20+
// It is not shown in help output.
21+
var internalRefreshCmd = &cobra.Command{
22+
Use: "_internal-refresh-peers",
23+
Short: "Internal: refresh peers (used by cron)",
24+
Hidden: true, // Not shown in help
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
homeDir := internalRefreshHome
27+
if homeDir == "" {
28+
homeDir = os.ExpandEnv("$HOME/.pchain")
29+
}
30+
31+
// Setup logging
32+
logPath := filepath.Join(homeDir, "logs", "peer-refresh.log")
33+
_ = os.MkdirAll(filepath.Dir(logPath), 0o755)
34+
35+
logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
36+
if err == nil {
37+
defer logFile.Close()
38+
}
39+
40+
log := func(format string, args ...interface{}) {
41+
msg := fmt.Sprintf("[%s] %s\n", time.Now().Format("2006-01-02 15:04:05"), fmt.Sprintf(format, args...))
42+
if logFile != nil {
43+
_, _ = logFile.WriteString(msg)
44+
}
45+
}
46+
47+
cfg := loadCfgFrom(homeDir)
48+
remoteURL := cfg.RemoteRPCURL()
49+
if remoteURL == "" {
50+
log("ERROR: no remote RPC URL configured")
51+
return nil // Don't fail cron job
52+
}
53+
54+
// Get current peers before refresh
55+
oldPeers, _ := node.GetCurrentPeers(homeDir)
56+
57+
// Fetch and update peers
58+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
59+
defer cancel()
60+
61+
count, err := node.RefreshPeersFromRemote(ctx, remoteURL, homeDir, 10)
62+
if err != nil {
63+
log("ERROR: failed to refresh peers: %v", err)
64+
return nil // Don't fail cron job
65+
}
66+
67+
// Get new peers after refresh
68+
newPeers, _ := node.GetCurrentPeers(homeDir)
69+
70+
// Check if peers changed
71+
if peersEqual(oldPeers, newPeers) {
72+
log("peers unchanged (%d peers)", count)
73+
return nil
74+
}
75+
76+
log("peers updated: %d -> %d peers", len(oldPeers), len(newPeers))
77+
78+
// Restart node if running
79+
sup := process.New(homeDir)
80+
if !sup.IsRunning() {
81+
log("node not running, skip restart")
82+
return nil
83+
}
84+
85+
log("restarting node to apply new peers...")
86+
if err := sup.Stop(); err != nil {
87+
log("ERROR: failed to stop node: %v", err)
88+
return nil
89+
}
90+
91+
// Brief pause before restart
92+
time.Sleep(2 * time.Second)
93+
94+
_, err = sup.Start(process.StartOpts{
95+
HomeDir: homeDir,
96+
BinPath: findPchaind(),
97+
})
98+
if err != nil {
99+
log("ERROR: failed to start node: %v", err)
100+
return nil
101+
}
102+
103+
log("node restarted successfully")
104+
return nil
105+
},
106+
}
107+
108+
func init() {
109+
internalRefreshCmd.Flags().StringVar(&internalRefreshHome, "home", "", "Node home directory")
110+
rootCmd.AddCommand(internalRefreshCmd)
111+
}
112+
113+
// peersEqual compares two peer slices for equality (order-independent).
114+
func peersEqual(a, b []string) bool {
115+
if len(a) != len(b) {
116+
return false
117+
}
118+
aSet := make(map[string]bool, len(a))
119+
for _, p := range a {
120+
aSet[p] = true
121+
}
122+
for _, p := range b {
123+
if !aSet[p] {
124+
return false
125+
}
126+
}
127+
return true
128+
}
129+
130+
// loadCfgFrom loads config from a specific home directory.
131+
func loadCfgFrom(homeDir string) config.Config {
132+
cfg := config.Load()
133+
cfg.HomeDir = homeDir
134+
return cfg
135+
}

cmd/push-validator/cmd_start.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/pushchain/push-validator-cli/internal/cosmovisor"
1818
"github.com/pushchain/push-validator-cli/internal/dashboard"
1919
"github.com/pushchain/push-validator-cli/internal/metrics"
20+
"github.com/pushchain/push-validator-cli/internal/node"
2021
"github.com/pushchain/push-validator-cli/internal/process"
2122
"github.com/pushchain/push-validator-cli/internal/snapshot"
2223
syncmon "github.com/pushchain/push-validator-cli/internal/sync"
@@ -142,6 +143,25 @@ var startCmd = &cobra.Command{
142143
if flagOutput != "json" && !detection.SetupComplete {
143144
p.Info("Initializing Cosmovisor...")
144145
}
146+
147+
// Auto-refresh peers from remote RPC before starting
148+
if remoteURL := cfg.RemoteRPCURL(); remoteURL != "" {
149+
if flagOutput != "json" {
150+
fmt.Println("→ Refreshing peers from network...")
151+
}
152+
refreshCtx, refreshCancel := context.WithTimeout(cmd.Context(), 15*time.Second)
153+
peerCount, err := node.RefreshPeersFromRemote(refreshCtx, remoteURL, cfg.HomeDir, 10)
154+
refreshCancel()
155+
if err != nil {
156+
if flagOutput != "json" {
157+
fmt.Printf(" ⚠ Could not refresh peers: %v\n", err)
158+
fmt.Println(" Continuing with existing peers...")
159+
}
160+
} else if flagOutput != "json" {
161+
fmt.Printf(" ✓ Updated with %d peers\n", peerCount)
162+
}
163+
}
164+
145165
sup := newSupervisor(cfg.HomeDir)
146166

147167
// Check if node is already running
@@ -187,6 +207,14 @@ var startCmd = &cobra.Command{
187207
p.Success("Node started with Cosmovisor")
188208
}
189209

210+
// Install peer refresh cron job (silent, idempotent)
211+
if err := node.InstallPeerRefreshCron(cfg.HomeDir); err != nil {
212+
// Non-fatal - just log if verbose
213+
if flagVerbose {
214+
fmt.Printf(" [DEBUG] Could not install peer refresh cron: %v\n", err)
215+
}
216+
}
217+
190218
// Check validator status and show appropriate next steps (skip if --no-prompt)
191219
if !startNoPrompt {
192220
fmt.Println()

internal/node/cron.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package node
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"runtime"
9+
"strings"
10+
)
11+
12+
const (
13+
cronMarker = "# push-validator-peer-refresh"
14+
cronInterval = "* * * * *" // Every 1 minute
15+
launchdPlistName = "com.push.validator.peerrefresh.plist"
16+
)
17+
18+
// InstallPeerRefreshCron installs a cron job (or launchd on macOS) that
19+
// runs every minute to refresh peers and restart the node if needed.
20+
// This is idempotent - safe to call multiple times.
21+
func InstallPeerRefreshCron(homeDir string) error {
22+
// Find the push-validator binary path
23+
binPath, err := os.Executable()
24+
if err != nil {
25+
binPath = "push-validator" // fallback
26+
}
27+
28+
if runtime.GOOS == "darwin" {
29+
return installLaunchd(binPath, homeDir)
30+
}
31+
return installCron(binPath, homeDir)
32+
}
33+
34+
// UninstallPeerRefreshCron removes the peer refresh cron job.
35+
func UninstallPeerRefreshCron(homeDir string) error {
36+
if runtime.GOOS == "darwin" {
37+
return uninstallLaunchd()
38+
}
39+
return uninstallCron()
40+
}
41+
42+
// IsPeerRefreshCronInstalled checks if the cron job is installed.
43+
func IsPeerRefreshCronInstalled(homeDir string) bool {
44+
if runtime.GOOS == "darwin" {
45+
return isLaunchdInstalled()
46+
}
47+
return isCronInstalled()
48+
}
49+
50+
// --- Cron (Linux) ---
51+
52+
func installCron(binPath, homeDir string) error {
53+
// Check if already installed
54+
if isCronInstalled() {
55+
return nil
56+
}
57+
58+
// Get current crontab
59+
cmd := exec.Command("crontab", "-l")
60+
existing, _ := cmd.Output() // ignore error (no crontab yet)
61+
62+
// Build new cron entry
63+
cronLine := fmt.Sprintf("%s %s _internal-refresh-peers --home %s %s",
64+
cronInterval, binPath, homeDir, cronMarker)
65+
66+
// Append to existing crontab
67+
newCrontab := string(existing)
68+
if !strings.HasSuffix(newCrontab, "\n") && len(newCrontab) > 0 {
69+
newCrontab += "\n"
70+
}
71+
newCrontab += cronLine + "\n"
72+
73+
// Install new crontab
74+
cmd = exec.Command("crontab", "-")
75+
cmd.Stdin = strings.NewReader(newCrontab)
76+
return cmd.Run()
77+
}
78+
79+
func uninstallCron() error {
80+
// Get current crontab
81+
cmd := exec.Command("crontab", "-l")
82+
existing, err := cmd.Output()
83+
if err != nil {
84+
return nil // no crontab
85+
}
86+
87+
// Filter out our marker line
88+
lines := strings.Split(string(existing), "\n")
89+
var filtered []string
90+
for _, line := range lines {
91+
if !strings.Contains(line, cronMarker) {
92+
filtered = append(filtered, line)
93+
}
94+
}
95+
96+
newCrontab := strings.Join(filtered, "\n")
97+
98+
// Install filtered crontab
99+
cmd = exec.Command("crontab", "-")
100+
cmd.Stdin = strings.NewReader(newCrontab)
101+
return cmd.Run()
102+
}
103+
104+
func isCronInstalled() bool {
105+
cmd := exec.Command("crontab", "-l")
106+
output, err := cmd.Output()
107+
if err != nil {
108+
return false
109+
}
110+
return strings.Contains(string(output), cronMarker)
111+
}
112+
113+
// --- Launchd (macOS) ---
114+
115+
func launchdPlistPath() string {
116+
home, _ := os.UserHomeDir()
117+
return filepath.Join(home, "Library", "LaunchAgents", launchdPlistName)
118+
}
119+
120+
func installLaunchd(binPath, homeDir string) error {
121+
// Check if already installed
122+
if isLaunchdInstalled() {
123+
return nil
124+
}
125+
126+
plistPath := launchdPlistPath()
127+
128+
// Ensure LaunchAgents directory exists
129+
if err := os.MkdirAll(filepath.Dir(plistPath), 0o755); err != nil {
130+
return err
131+
}
132+
133+
// Create plist content - runs every 60 seconds
134+
plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
135+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
136+
<plist version="1.0">
137+
<dict>
138+
<key>Label</key>
139+
<string>com.push.validator.peerrefresh</string>
140+
<key>ProgramArguments</key>
141+
<array>
142+
<string>%s</string>
143+
<string>_internal-refresh-peers</string>
144+
<string>--home</string>
145+
<string>%s</string>
146+
</array>
147+
<key>StartInterval</key>
148+
<integer>60</integer>
149+
<key>RunAtLoad</key>
150+
<true/>
151+
<key>StandardOutPath</key>
152+
<string>%s/logs/peer-refresh.log</string>
153+
<key>StandardErrorPath</key>
154+
<string>%s/logs/peer-refresh.log</string>
155+
</dict>
156+
</plist>
157+
`, binPath, homeDir, homeDir, homeDir)
158+
159+
// Write plist file
160+
if err := os.WriteFile(plistPath, []byte(plist), 0o644); err != nil {
161+
return err
162+
}
163+
164+
// Load the agent
165+
cmd := exec.Command("launchctl", "load", plistPath)
166+
return cmd.Run()
167+
}
168+
169+
func uninstallLaunchd() error {
170+
plistPath := launchdPlistPath()
171+
172+
// Unload the agent (ignore error if not loaded)
173+
cmd := exec.Command("launchctl", "unload", plistPath)
174+
_ = cmd.Run()
175+
176+
// Remove the plist file
177+
return os.Remove(plistPath)
178+
}
179+
180+
func isLaunchdInstalled() bool {
181+
_, err := os.Stat(launchdPlistPath())
182+
return err == nil
183+
}

0 commit comments

Comments
 (0)