diff --git a/cmd/auto-update/coordinator.go b/cmd/auto-update/coordinator.go index a2a35908c..a1916418a 100644 --- a/cmd/auto-update/coordinator.go +++ b/cmd/auto-update/coordinator.go @@ -276,6 +276,9 @@ func (c *Coordinator) UpdateLoop(cancelSignal chan os.Signal) error { return err // periodic check for updates case <-timer.C: + if !c.updater.Enabled { + continue + } // wrap it on a goroutine so it doesn't block the main loop go func() { c.log.Infof("checking for updates") diff --git a/cmd/auto-update/main.go b/cmd/auto-update/main.go index 6a13f61c6..98d0fd7eb 100644 --- a/cmd/auto-update/main.go +++ b/cmd/auto-update/main.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "os/signal" + "path/filepath" "syscall" "time" @@ -91,32 +92,32 @@ func main() { "This message appears because the program was started directly instead of using 'start'.") return } - // do not run the auto-update process if its disabled - if !configs.Coordinator.Canopy.AutoUpdate { - logger.Info("auto-update disabled, starting CLI directly") - cli.Start() - return + // ensure the binary exists before proceeding + if !isExecutable(configs.Coordinator.BinPath) { + logger.Fatalf("canopy binary not found or not executable: %s", configs.Coordinator.BinPath) + } + if configs.Coordinator.Canopy.AutoUpdate { + logger.Infof("auto-update enabled, starting coordinator on version %s", rpc.SoftwareVersion) + } else { + logger.Infof("auto-update disabled, starting binary: %s", configs.Coordinator.BinPath) } - logger.Infof("auto-update enabled, starting coordinator on version %s", rpc.SoftwareVersion) // handle external shutdown signals sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) // setup the dependencies - updater := NewReleaseManager(configs.Updater, rpc.SoftwareVersion) + updater := NewReleaseManager(configs.Updater, rpc.SoftwareVersion, configs.Coordinator.Canopy.AutoUpdate) snapshot := NewSnapshotManager(configs.Snapshot) - // setup plugin updater and config if configured var pluginUpdater *ReleaseManager var pluginConfig *PluginReleaseConfig if configs.PluginUpdater != nil { - pluginUpdater = NewReleaseManager(configs.PluginUpdater, "v0.0.0") + pluginUpdater = NewReleaseManager(configs.PluginUpdater, "v0.0.0", true) pluginConfig = configs.PluginUpdater.PluginConfig logger.Infof("plugin auto-update enabled from %s/%s", configs.PluginUpdater.RepoOwner, configs.PluginUpdater.RepoName) } supervisor := NewSupervisor(logger, pluginConfig) - coordinator := NewCoordinator(configs.Coordinator, updater, pluginUpdater, supervisor, snapshot, logger) // start the update loop err := coordinator.UpdateLoop(sigChan) @@ -155,10 +156,20 @@ func getConfigs() (*Configs, lib.LoggerI) { binPath := envOrDefault("BIN_PATH", defaultBinPath) githubToken := envOrDefault("CANOPY_GITHUB_API_TOKEN", "") + // core auto-update repo: config.json or defaults + repoOwner := canopyConfig.AutoUpdateRepoOwner + if repoOwner == "" { + repoOwner = defaultRepoOwner + } + repoName := canopyConfig.AutoUpdateRepoName + if repoName == "" { + repoName = defaultRepoName + } + updater := &ReleaseManagerConfig{ Type: ReleaseTypeCLI, - RepoName: envOrDefault("REPO_NAME", defaultRepoName), - RepoOwner: envOrDefault("REPO_OWNER", defaultRepoOwner), + RepoName: repoName, + RepoOwner: repoOwner, GithubApiToken: githubToken, BinPath: binPath, SnapshotKey: snapshotMetadataKey, @@ -213,6 +224,26 @@ func getConfigs() (*Configs, lib.LoggerI) { }, l } +// isExecutable returns true if path exists, is a regular file, and has execute permission. +func isExecutable(path string) bool { + // resolve to absolute path to avoid relative path ambiguity + absPath, err := filepath.Abs(path) + if err != nil { + return false + } + // check if the file exists and is accessible + info, err := os.Stat(absPath) + if err != nil { + return false + } + // directories are not executable binaries + if info.IsDir() { + return false + } + // check for any execute bit (owner, group, or other) + return info.Mode()&0111 != 0 +} + // envOrDefault returns the value of the environment variable with the given key, // or the default value if the variable is not set. func envOrDefault(key, defaultValue string) string { diff --git a/cmd/auto-update/releaser.go b/cmd/auto-update/releaser.go index 925555684..00f333095 100644 --- a/cmd/auto-update/releaser.go +++ b/cmd/auto-update/releaser.go @@ -75,14 +75,16 @@ type ReleaseManager struct { config *ReleaseManagerConfig httpClient *http.Client Version string // current version + Enabled bool // whether this updater should actively check for updates } // NewReleaseManager creates a new ReleaseManager instance -func NewReleaseManager(config *ReleaseManagerConfig, version string) *ReleaseManager { +func NewReleaseManager(config *ReleaseManagerConfig, version string, enabled bool) *ReleaseManager { return &ReleaseManager{ config: config, httpClient: &http.Client{Timeout: httpReleaseClientTimeout}, Version: version, + Enabled: enabled, } } diff --git a/lib/config.go b/lib/config.go index ea9f8b86d..309f1c087 100644 --- a/lib/config.go +++ b/lib/config.go @@ -66,8 +66,10 @@ type MainConfig struct { RootChain []RootChain `json:"rootChain"` // a list of the root chain(s) a node could connect to as dictated by the governance parameter 'RootChainId' RunVDF bool `json:"runVDF"` // whether the node should run a Verifiable Delay Function to help secure the network against Long-Range-Attacks Headless bool `json:"headless"` // turn off the web wallet and block explorer 'web' front ends - AutoUpdate bool `json:"autoUpdate"` // check for new versions of software each X time - Plugin string `json:"plugin"` // the configured plugin to use + AutoUpdate bool `json:"autoUpdate"` // check for new versions of software each X time + AutoUpdateRepoOwner string `json:"autoUpdateRepoOwner"` // GitHub repo owner for core auto-updates (e.g., "canopy-network") + AutoUpdateRepoName string `json:"autoUpdateRepoName"` // GitHub repo name for core auto-updates (e.g., "canopy") + Plugin string `json:"plugin"` // the configured plugin to use PluginTimeoutMS int `json:"pluginTimeoutMS"` // plugin request timeout in milliseconds PluginAutoUpdate PluginAutoUpdateConfig `json:"pluginAutoUpdate"` // plugin auto-update configuration }