Browse Source

cmd/syncthing: Do auto-upgrade before startup (fixes #6384) (#6385)

Simon Frei 5 years ago
parent
commit
c101a04179
1 changed files with 106 additions and 53 deletions
  1. 106 53
      cmd/syncthing/main.go

+ 106 - 53
cmd/syncthing/main.go

@@ -139,10 +139,12 @@ The following are valid values for the STTRACE variable:
 %s`
 )
 
-// Environment options
 var (
+	// Environment options
 	innerProcess    = os.Getenv("STNORESTART") != "" || os.Getenv("STMONITORED") != ""
 	noDefaultFolder = os.Getenv("STNODEFAULTFOLDER") != ""
+
+	errConcurrentUpgrade = errors.New("upgrade prevented by other running Syncthing instance")
 )
 
 type RuntimeOptions struct {
@@ -359,14 +361,24 @@ func main() {
 	}
 
 	if options.doUpgradeCheck {
-		checkUpgrade()
+		if _, err := checkUpgrade(); err != nil {
+			l.Warnln("Checking for upgrade:", err)
+			os.Exit(exitCodeForUpgrade(err))
+		}
 		return
 	}
 
 	if options.doUpgrade {
-		release := checkUpgrade()
-		performUpgrade(release)
-		return
+		release, err := checkUpgrade()
+		if err == nil {
+			err = performUpgrade(release)
+		}
+		if err != nil {
+			l.Warnln("Upgrade:", err)
+			os.Exit(exitCodeForUpgrade(err))
+		}
+		l.Infof("Upgraded to %q", release.Tag)
+		os.Exit(syncthing.ExitUpgrade.AsInt())
 	}
 
 	if options.resetDatabase {
@@ -462,45 +474,47 @@ func debugFacilities() string {
 	return b.String()
 }
 
-func checkUpgrade() upgrade.Release {
+type errNoUpgrade struct {
+	current, latest string
+}
+
+func (e errNoUpgrade) Error() string {
+	return fmt.Sprintf("no upgrade available (current %q >= latest %q).", e.current, e.latest)
+}
+
+func checkUpgrade() (upgrade.Release, error) {
 	cfg, _ := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger)
 	opts := cfg.Options()
 	release, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases)
 	if err != nil {
-		l.Warnln("Upgrade:", err)
-		os.Exit(syncthing.ExitError.AsInt())
+		return upgrade.Release{}, err
 	}
 
 	if upgrade.CompareVersions(release.Tag, build.Version) <= 0 {
-		noUpgradeMessage := "No upgrade available (current %q >= latest %q)."
-		l.Infof(noUpgradeMessage, build.Version, release.Tag)
-		os.Exit(syncthing.ExitNoUpgradeAvailable.AsInt())
+		return upgrade.Release{}, errNoUpgrade{build.Version, release.Tag}
 	}
 
 	l.Infof("Upgrade available (current %q < latest %q)", build.Version, release.Tag)
-	return release
+	return release, nil
 }
 
-func performUpgrade(release upgrade.Release) {
+func performUpgradeDirect(release upgrade.Release) error {
 	// Use leveldb database locks to protect against concurrent upgrades
-	_, err := syncthing.OpenDBBackend(locations.Get(locations.Database), config.TuningAuto)
-	if err == nil {
-		err = upgrade.To(release)
-		if err != nil {
-			l.Warnln("Upgrade:", err)
-			os.Exit(syncthing.ExitError.AsInt())
+	if _, err := syncthing.OpenDBBackend(locations.Get(locations.Database), config.TuningAuto); err != nil {
+		return errConcurrentUpgrade
+	}
+	return upgrade.To(release)
+}
+
+func performUpgrade(release upgrade.Release) error {
+	if err := performUpgradeDirect(release); err != nil {
+		if err != errConcurrentUpgrade {
+			return err
 		}
-		l.Infof("Upgraded to %q", release.Tag)
-	} else {
 		l.Infoln("Attempting upgrade through running Syncthing...")
-		err = upgradeViaRest()
-		if err != nil {
-			l.Warnln("Upgrade:", err)
-			os.Exit(syncthing.ExitError.AsInt())
-		}
-		l.Infoln("Syncthing upgrading")
-		os.Exit(syncthing.ExitUpgrade.AsInt())
+		return upgradeViaRest()
 	}
+	return nil
 }
 
 func upgradeViaRest() error {
@@ -568,6 +582,45 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 		os.Exit(syncthing.ExitError.AsInt())
 	}
 
+	// Candidate builds should auto upgrade. Make sure the option is set,
+	// unless we are in a build where it's disabled or the STNOUPGRADE
+	// environment variable is set.
+
+	if build.IsCandidate && !upgrade.DisabledByCompilation && !runtimeOptions.NoUpgrade {
+		l.Infoln("Automatic upgrade is always enabled for candidate releases.")
+		if opts := cfg.Options(); opts.AutoUpgradeIntervalH == 0 || opts.AutoUpgradeIntervalH > 24 {
+			opts.AutoUpgradeIntervalH = 12
+			// Set the option into the config as well, as the auto upgrade
+			// loop expects to read a valid interval from there.
+			cfg.SetOptions(opts)
+			cfg.Save()
+		}
+		// We don't tweak the user's choice of upgrading to pre-releases or
+		// not, as otherwise they cannot step off the candidate channel.
+	}
+
+	// Check if auto-upgrades should be done and if yes, do an initial
+	// upgrade immedately. The auto-upgrade routine can only be started
+	// later after App is initialised.
+
+	shouldAutoUpgrade := shouldUpgrade(cfg, runtimeOptions)
+	if shouldAutoUpgrade {
+		// Try to do upgrade directly
+		release, err := checkUpgrade()
+		if err == nil {
+			if err = performUpgradeDirect(release); err == nil {
+				l.Infof("Upgraded to %q, exiting now.", release.Tag)
+				os.Exit(syncthing.ExitUpgrade.AsInt())
+			}
+		}
+		// Log the error if relevant.
+		if err != nil {
+			if _, ok := err.(errNoUpgrade); !ok {
+				l.Infoln("Initial automatic upgrade:", err)
+			}
+		}
+	}
+
 	if runtimeOptions.unpaused {
 		setPauseState(cfg, false)
 	} else if runtimeOptions.paused {
@@ -592,6 +645,10 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 
 	app := syncthing.New(cfg, ldb, evLogger, cert, appOpts)
 
+	if shouldAutoUpgrade {
+		go autoUpgrade(cfg, app, evLogger)
+	}
+
 	setupSignalHandling(app)
 
 	if len(os.Getenv("GOMAXPROCS")) == 0 {
@@ -614,31 +671,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 		go standbyMonitor(app)
 	}
 
-	// Candidate builds should auto upgrade. Make sure the option is set,
-	// unless we are in a build where it's disabled or the STNOUPGRADE
-	// environment variable is set.
-
-	if build.IsCandidate && !upgrade.DisabledByCompilation && !runtimeOptions.NoUpgrade {
-		l.Infoln("Automatic upgrade is always enabled for candidate releases.")
-		if opts := cfg.Options(); opts.AutoUpgradeIntervalH == 0 || opts.AutoUpgradeIntervalH > 24 {
-			opts.AutoUpgradeIntervalH = 12
-			// Set the option into the config as well, as the auto upgrade
-			// loop expects to read a valid interval from there.
-			cfg.SetOptions(opts)
-			cfg.Save()
-		}
-		// We don't tweak the user's choice of upgrading to pre-releases or
-		// not, as otherwise they cannot step off the candidate channel.
-	}
-
-	if opts := cfg.Options(); opts.AutoUpgradeIntervalH > 0 {
-		if runtimeOptions.NoUpgrade {
-			l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
-		} else {
-			go autoUpgrade(cfg, app, evLogger)
-		}
-	}
-
 	if err := app.Start(); err != nil {
 		os.Exit(syncthing.ExitError.AsInt())
 	}
@@ -771,6 +803,20 @@ func standbyMonitor(app *syncthing.App) {
 	}
 }
 
+func shouldUpgrade(cfg config.Wrapper, runtimeOptions RuntimeOptions) bool {
+	if upgrade.DisabledByCompilation {
+		return false
+	}
+	if opts := cfg.Options(); opts.AutoUpgradeIntervalH < 0 {
+		return false
+	}
+	if runtimeOptions.NoUpgrade {
+		l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
+		return false
+	}
+	return true
+}
+
 func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger) {
 	timer := time.NewTimer(0)
 	sub := evLogger.Subscribe(events.DeviceConnected)
@@ -893,3 +939,10 @@ func setPauseState(cfg config.Wrapper, paused bool) {
 		os.Exit(syncthing.ExitError.AsInt())
 	}
 }
+
+func exitCodeForUpgrade(err error) int {
+	if _, ok := err.(errNoUpgrade); ok {
+		return syncthing.ExitNoUpgradeAvailable.AsInt()
+	}
+	return syncthing.ExitError.AsInt()
+}