diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index c3e3e0b82..8714b6fa4 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -313,21 +313,6 @@ func (c *serveCmd) Run() error { return nil } -func openGUI() error { - cfg, err := loadOrDefaultConfig() - if err != nil { - return err - } - if guiCfg := cfg.GUI(); guiCfg.Enabled { - if err := openURL(guiCfg.URL()); err != nil { - return err - } - } else { - slog.Error("Browser: GUI is currently disabled") - } - return nil -} - func logPackages() string { packages := slogutil.PackageDescrs() @@ -418,6 +403,28 @@ func upgradeViaRest() error { } func (c *serveCmd) syncthingMain() { + // Ensure we are the only running instance + lf := flock.New(locations.Get(locations.LockFile)) + locked, err := lf.TryLock() + switch { + case err != nil: + slog.Error("Failed to acquire lock", slogutil.Error(err)) + os.Exit(svcutil.ExitError.AsInt()) + + case !locked && c.NoBrowser: + slog.Error("Failed to acquire lock: is another Syncthing instance already running?") + os.Exit(svcutil.ExitError.AsInt()) + + case !locked: + slog.Info("Seems to already be running, launching GUI instead (use --no-browser to prevent)") + cmd := browserCmd{Verify: true} + if err := cmd.Run(); err != nil { + slog.Error("Failed to open browser", slogutil.Error(err)) + os.Exit(svcutil.ExitNoRestart.AsInt()) + } + return + } + if c.DebugProfileBlock { startBlockProfiler() } @@ -436,18 +443,7 @@ func (c *serveCmd) syncthingMain() { ) if err != nil { slog.Error("Failed to load/generate certificate", slogutil.Error(err)) - os.Exit(1) - } - - // Ensure we are the only running instance - lf := flock.New(locations.Get(locations.LockFile)) - locked, err := lf.TryLock() - if err != nil { - slog.Error("Failed to acquire lock", slogutil.Error(err)) - os.Exit(1) - } else if !locked { - slog.Error("Failed to acquire lock: is another Syncthing instance already running?") - os.Exit(1) + os.Exit(svcutil.ExitError.AsInt()) } ctx, cancel := context.WithCancel(context.Background()) @@ -502,13 +498,13 @@ func (c *serveCmd) syncthingMain() { if err := syncthing.TryMigrateDatabase(ctx, c.DBDeleteRetentionInterval); err != nil { slog.Error("Failed to migrate old-style database", slogutil.Error(err)) - os.Exit(1) + os.Exit(svcutil.ExitError.AsInt()) } sdb, err := syncthing.OpenDatabase(locations.Get(locations.Database), c.DBDeleteRetentionInterval) if err != nil { slog.Error("Error opening database", slogutil.Error(err)) - os.Exit(1) + os.Exit(svcutil.ExitError.AsInt()) } if c.DebugPerfStats { @@ -915,7 +911,7 @@ func (u upgradeCmd) Run() error { switch { case err != nil && !os.IsNotExist(err): slog.Error("Failed to lock for upgrade", slogutil.Error(err)) - os.Exit(1) + os.Exit(svcutil.ExitError.AsInt()) case locked || os.IsNotExist(err): // We got the lock, or the config directory didn't exist, so we // can do a direct upgrade @@ -935,14 +931,38 @@ func (u upgradeCmd) Run() error { return nil } -type browserCmd struct{} +type browserCmd struct { + Verify bool `help:"Verify that the GUI is reachable before launching browser"` +} -func (browserCmd) Run() error { - if err := openGUI(); err != nil { - slog.Error("Failed to open web UI", slogutil.Error(err)) +func (c browserCmd) Run() error { + cfg, err := loadOrDefaultConfig() + if err != nil { + return err + } + guiCfg := cfg.GUI() + if !guiCfg.Enabled { + slog.Error("Browser: GUI is currently disabled") os.Exit(svcutil.ExitError.AsInt()) } - return nil + url := guiCfg.URL() + + if c.Verify { + // Do an HTTP request to verify the GUI/API is up and available + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + _, err = http.DefaultClient.Do(req) //nolint:bodyclose // we're exiting in a millisecond + if err != nil { + slog.Error("GUI not available", slogutil.Error(err)) + os.Exit(svcutil.ExitError.AsInt()) //nolint:gocritic // deferred cancel + } + } + + return openURL(url) } type debugCmd struct { diff --git a/cmd/syncthing/monitor.go b/cmd/syncthing/monitor.go index 7b0cf2186..ff7a72b9c 100644 --- a/cmd/syncthing/monitor.go +++ b/cmd/syncthing/monitor.go @@ -171,10 +171,11 @@ func (c *serveCmd) monitorMain() { exiterr := &exec.ExitError{} if errors.As(err, &exiterr) { exitCode := exiterr.ExitCode() - if stopped || c.NoRestart { + switch { + case stopped || c.NoRestart: os.Exit(exitCode) - } - if exitCode == svcutil.ExitUpgrade.AsInt() { + + case exitCode == svcutil.ExitUpgrade.AsInt(): // Restart the monitor process to release the .old // binary as part of the upgrade process. slog.Info("Restarting monitor...") @@ -182,6 +183,10 @@ func (c *serveCmd) monitorMain() { slog.Error("Failed to restart monitor", slogutil.Error(err)) } os.Exit(exitCode) + + case exitCode == svcutil.ExitNoRestart.AsInt(): + // Requested to not restart the child + os.Exit(exitCode) } } diff --git a/lib/svcutil/svcutil.go b/lib/svcutil/svcutil.go index 72d1825be..aadcfc53b 100644 --- a/lib/svcutil/svcutil.go +++ b/lib/svcutil/svcutil.go @@ -88,6 +88,7 @@ const ( ExitNoUpgradeAvailable ExitStatus = 2 ExitRestart ExitStatus = 3 ExitUpgrade ExitStatus = 4 + ExitNoRestart ExitStatus = 5 // do not get restarted by monitor ) func (s ExitStatus) AsInt() int {