Browse Source

chore: harmonise command line flags (#10007)

(v2 change)

This cleans up the command line parsing a little:
- Remove the hack for supporting legacy single-dash long options (e.g.
`-home`), thus enabling actual short options
- Move legacy imperative flags from under the serve command into
separate commands, e.g. `syncthing serve --paths` to see the paths list
is now `syncthing paths`, `syncthing --upgrade-check` is now `syncthing
upgrade --check`
- Add environment variable support for all remaining flags for the
`serve` command (with one exception, left for the reader to discover),
as these are now all modifiers and not imperative

```
% syncthing --help
Usage: syncthing <command>

Flags:
  -h, --help    Show context-sensitive help.

Commands:
  serve                  Run Syncthing (default)
  cli                    Command line interface for Syncthing
  browser                Open GUI in browser, then exit
  decrypt                Decrypt or verify an encrypted folder
  device-id              Show device ID, then exit
  generate               Generate key and config, then exit
  paths                  Show configuration paths, then exit
  upgrade                Perform or check for upgrade, then exit
  version                Show current version, then exit
  debug                  Various debugging commands
  install-completions    Print commands to install shell completions

Run "syncthing <command> --help" for more information on a command.
```

```
% syncthing serve --help
Usage: syncthing serve [flags]

Run Syncthing (default)

Flags:
  -h, --help                          Show context-sensitive help.

  -C, --config=PATH                   Set configuration directory (config and keys) ($STCONFDIR)
  -D, --data=PATH                     Set data directory (database and logs) ($STDATADIR)
  -H, --home=PATH                     Set configuration and data directory ($STHOMEDIR)
      --allow-newer-config            Allow loading newer than current config version ($STALLOWNEWERCONFIG)
      --audit                         Write events to audit file ($STAUDIT)
      --auditfile=PATH                Specify audit file (use "-" for stdout, "--" for stderr) ($STAUDITFILE)
      --db-maintenance-interval=8h    Database maintenance interval ($STDBMAINTINTERVAL)
      --gui-address=URL               Override GUI address (e.g. "http://192.0.2.42:8443") ($STGUIADDRESS)
      --gui-apikey=API-KEY            Override GUI API key ($STGUIAPIKEY)
      --no-console                    Hide console window ($STHIDECONSOLE)
      --logfile=PATH                  Log file name (see below) ($STLOGFILE)
      --logflags=BITS                 Select information in log line prefix (see below) ($STLOGFLAGS)
      --log-max-old-files=N           Number of old files to keep (zero to keep only current) ($STNUMLOGFILES)
      --log-max-size=BYTES            Maximum size of any file (zero to disable log rotation) ($STLOGMAXSIZE)
      --no-browser                    Do not start browser ($STNOBROWSER)
      --no-default-folder             Don't create the "default" folder on first startup ($STNODEFAULTFOLDER)
      --no-port-probing               Don't try to find free ports for GUI and listen addresses on first startup ($STNOPORTPROBING)
      --no-restart                    Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash ($STNORESTART)
      --no-upgrade                    Disable automatic upgrades ($STNOUPGRADE)
      --paused                        Start with all devices and folders paused ($STPAUSED)
      --unpaused                      Start with all devices and folders unpaused ($STUNPAUSED)
      --verbose                       Print verbose log output ($STVERBOSE)
      --debug-gui-assets-dir=PATH     Directory to load GUI assets from ($STGUIASSETS)
      --debug-perf-stats              Write running performance statistics to perf-$pid.csv (Unix only) ($STPERFSTATS)
      --debug-profile-block           Write block profiles to block-$pid-$timestamp.pprof every 20 seconds ($STBLOCKPROFILE)
      --debug-profile-cpu             Write a CPU profile to cpu-$pid.pprof on exit ($STCPUPROFILE)
      --debug-profile-heap            Write heap profiles to heap-$pid-$timestamp.pprof each time heap usage increases ($STHEAPPROFILE)
      --debug-profiler-listen=ADDR    Network profiler listen address ($STPROFILER)
      --debug-reset-delta-idxs        Reset delta index IDs, forcing a full index exchange
...
```
Jakob Borg 9 months ago
parent
commit
a99e670ebb

+ 4 - 4
cmd/syncthing/cli/main.go

@@ -19,10 +19,10 @@ import (
 )
 
 type CLI struct {
-	cmdutil.CommonOptions
-	DataDir    string `name:"data" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
-	GUIAddress string `name:"gui-address"`
-	GUIAPIKey  string `name:"gui-apikey"`
+	cmdutil.DirOptions
+
+	GUIAddress string `name:"gui-address" env:"STGUIADDRESS"`
+	GUIAPIKey  string `name:"gui-apikey" env:"STGUIAPIKEY"`
 
 	Show       showCommand      `cmd:"" help:"Show command group"`
 	Debug      debugCommand     `cmd:"" help:"Debug command group"`

+ 14 - 0
cmd/syncthing/cmdutil/diroptions.go

@@ -0,0 +1,14 @@
+// Copyright (C) 2021 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package cmdutil
+
+// DirOptions are reused among several subcommands
+type DirOptions struct {
+	ConfDir string `name:"config" short:"C" placeholder:"PATH" env:"STCONFDIR" help:"Set configuration directory (config and keys)"`
+	DataDir string `name:"data" short:"D" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
+	HomeDir string `name:"home" short:"H" placeholder:"PATH" env:"STHOMEDIR" help:"Set configuration and data directory"`
+}

+ 0 - 16
cmd/syncthing/cmdutil/options_common.go

@@ -1,16 +0,0 @@
-// Copyright (C) 2021 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package cmdutil
-
-// CommonOptions are reused among several subcommands
-type CommonOptions struct {
-	buildCommonOptions
-	ConfDir         string `name:"config" placeholder:"PATH" env:"STCONFDIR" help:"Set configuration directory (config and keys)"`
-	HomeDir         string `name:"home" placeholder:"PATH" env:"STHOMEDIR" help:"Set configuration and data directory"`
-	NoDefaultFolder bool   `env:"STNODEFAULTFOLDER" help:"Don't create the \"default\" folder on first startup"`
-	SkipPortProbing bool   `help:"Don't try to find free ports for GUI and listen addresses on first startup"`
-}

+ 6 - 9
cmd/syncthing/generate/generate.go

@@ -21,22 +21,19 @@ import (
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/locations"
 	"github.com/syncthing/syncthing/lib/logger"
-	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/syncthing"
 )
 
 type CLI struct {
-	cmdutil.CommonOptions
-	GUIUser     string `placeholder:"STRING" help:"Specify new GUI authentication user name"`
-	GUIPassword string `placeholder:"STRING" help:"Specify new GUI authentication password (use - to read from standard input)"`
+	cmdutil.DirOptions
+	GUIUser         string `placeholder:"STRING" help:"Specify new GUI authentication user name"`
+	GUIPassword     string `placeholder:"STRING" help:"Specify new GUI authentication password (use - to read from standard input)"`
+	NoDefaultFolder bool   `help:"Don't create the \"default\" folder on first startup" env:"STNODEFAULTFOLDER"`
+	NoPortProbing   bool   `help:"Don't try to find free ports for GUI and listen addresses on first startup" env:"STNOPORTPROBING"`
 }
 
 func (c *CLI) Run(l logger.Logger) error {
-	if c.HideConsole {
-		osutil.HideConsole()
-	}
-
 	if c.HomeDir != "" {
 		if c.ConfDir != "" {
 			return errors.New("--home must not be used together with --config")
@@ -57,7 +54,7 @@ func (c *CLI) Run(l logger.Logger) error {
 		c.GUIPassword = string(password)
 	}
 
-	if err := Generate(l, c.ConfDir, c.GUIUser, c.GUIPassword, c.NoDefaultFolder, c.SkipPortProbing); err != nil {
+	if err := Generate(l, c.ConfDir, c.GUIUser, c.GUIPassword, c.NoDefaultFolder, c.NoPortProbing); err != nil {
 		return fmt.Errorf("failed to generate config and keys: %w", err)
 	}
 	return nil

+ 2 - 2
cmd/syncthing/cmdutil/options_others.go → cmd/syncthing/hideconsole_others.go

@@ -7,8 +7,8 @@
 //go:build !windows
 // +build !windows
 
-package cmdutil
+package main
 
-type buildCommonOptions struct {
+type buildSpecificOptions struct {
 	HideConsole bool `hidden:""`
 }

+ 3 - 3
cmd/syncthing/cmdutil/options_windows.go → cmd/syncthing/hideconsole_windows.go

@@ -4,8 +4,8 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package cmdutil
+package main
 
-type buildCommonOptions struct {
-	HideConsole bool `name:"no-console" help:"Hide console window"`
+type buildSpecificOptions struct {
+	HideConsole bool `name:"no-console" help:"Hide console window" env:"STHIDECONSOLE"`
 }

+ 143 - 152
cmd/syncthing/main.go

@@ -26,7 +26,6 @@ import (
 	"runtime/pprof"
 	"sort"
 	"strconv"
-	"strings"
 	"syscall"
 	"time"
 
@@ -130,51 +129,54 @@ var (
 // commands and options here are top level commands to syncthing.
 // Cli is just a placeholder for the help text (see main).
 var entrypoint struct {
-	Serve              serveOptions                 `cmd:"" help:"Run Syncthing"`
-	Generate           generate.CLI                 `cmd:"" help:"Generate key and config, then exit"`
-	Decrypt            decrypt.CLI                  `cmd:"" help:"Decrypt or verify an encrypted folder"`
-	Cli                cli.CLI                      `cmd:"" help:"Command line interface for Syncthing"`
+	Serve serveOptions `cmd:"" help:"Run Syncthing (default)" default:"withargs"`
+	CLI   cli.CLI      `cmd:"" help:"Command line interface for Syncthing"`
+
+	Browser  browserCmd   `cmd:"" help:"Open GUI in browser, then exit"`
+	Decrypt  decrypt.CLI  `cmd:"" help:"Decrypt or verify an encrypted folder"`
+	DeviceID deviceIDCmd  `cmd:"" help:"Show device ID, then exit"`
+	Generate generate.CLI `cmd:"" help:"Generate key and config, then exit"`
+	Paths    pathsCmd     `cmd:"" help:"Show configuration paths, then exit"`
+	Upgrade  upgradeCmd   `cmd:"" help:"Perform or check for upgrade, then exit"`
+	Version  versionCmd   `cmd:"" help:"Show current version, then exit"`
+	Debug    debugCmd     `cmd:"" help:"Various debugging commands"`
+
 	InstallCompletions kongplete.InstallCompletions `cmd:"" help:"Print commands to install shell completions"`
 }
 
 // serveOptions are the options for the `syncthing serve` command.
 type serveOptions struct {
-	cmdutil.CommonOptions
-	AllowNewerConfig      bool          `help:"Allow loading newer than current config version"`
-	Audit                 bool          `help:"Write events to audit file"`
-	AuditFile             string        `name:"auditfile" placeholder:"PATH" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)"`
-	BrowserOnly           bool          `help:"Open GUI in browser"`
-	DataDir               string        `name:"data" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
-	DeviceID              bool          `help:"Show the device ID"`
-	GenerateDir           string        `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"` // DEPRECATED: replaced by subcommand!
-	GUIAddress            string        `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"`
-	GUIAPIKey             string        `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"`
-	LogFile               string        `name:"logfile" default:"${logFile}" placeholder:"PATH" help:"Log file name (see below)"`
-	LogFlags              int           `name:"logflags" default:"${logFlags}" placeholder:"BITS" help:"Select information in log line prefix (see below)"`
-	LogMaxFiles           int           `placeholder:"N" default:"${logMaxFiles}" name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)"`
-	LogMaxSize            int           `placeholder:"BYTES" default:"${logMaxSize}" help:"Maximum size of any file (zero to disable log rotation)"`
-	NoBrowser             bool          `help:"Do not start browser"`
-	NoRestart             bool          `env:"STNORESTART" help:"Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash"`
-	NoUpgrade             bool          `env:"STNOUPGRADE" help:"Disable automatic upgrades"`
-	Paths                 bool          `help:"Show configuration paths"`
-	Paused                bool          `help:"Start with all devices and folders paused"`
-	Unpaused              bool          `help:"Start with all devices and folders unpaused"`
-	Upgrade               bool          `help:"Perform upgrade"`
-	UpgradeCheck          bool          `help:"Check for available upgrade"`
-	UpgradeTo             string        `placeholder:"URL" help:"Force upgrade directly from specified URL"`
-	Verbose               bool          `help:"Print verbose log output"`
-	Version               bool          `help:"Show version"`
-	DBMaintenanceInterval time.Duration `env:"STDBMAINTINTERVAL" help:"Database maintenance interval" default:"8h"`
+	cmdutil.DirOptions
+	buildSpecificOptions
+
+	AllowNewerConfig      bool          `help:"Allow loading newer than current config version" env:"STALLOWNEWERCONFIG"`
+	Audit                 bool          `help:"Write events to audit file" env:"STAUDIT"`
+	AuditFile             string        `name:"auditfile" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)" placeholder:"PATH" env:"STAUDITFILE"`
+	DBMaintenanceInterval time.Duration `help:"Database maintenance interval" default:"8h" env:"STDBMAINTINTERVAL"`
+	GUIAddress            string        `name:"gui-address" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")" placeholder:"URL" env:"STGUIADDRESS"`
+	GUIAPIKey             string        `name:"gui-apikey" help:"Override GUI API key" placeholder:"API-KEY" env:"STGUIAPIKEY"`
+	HideConsole           bool          `name:"no-console" help:"Hide console window" env:"STHIDECONSOLE"`
+	LogFile               string        `name:"logfile" help:"Log file name (see below)" default:"${logFile}" placeholder:"PATH" env:"STLOGFILE"`
+	LogFlags              int           `name:"logflags" help:"Select information in log line prefix (see below)" default:"${logFlags}" placeholder:"BITS" env:"STLOGFLAGS"`
+	LogMaxFiles           int           `name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)" default:"${logMaxFiles}" placeholder:"N" env:"STNUMLOGFILES"`
+	LogMaxSize            int           `help:"Maximum size of any file (zero to disable log rotation)" default:"${logMaxSize}" placeholder:"BYTES" env:"STLOGMAXSIZE"`
+	NoBrowser             bool          `help:"Do not start browser" env:"STNOBROWSER"`
+	NoDefaultFolder       bool          `help:"Don't create the \"default\" folder on first startup" env:"STNODEFAULTFOLDER"`
+	NoPortProbing         bool          `help:"Don't try to find free ports for GUI and listen addresses on first startup" env:"STNOPORTPROBING"`
+	NoRestart             bool          `help:"Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash" env:"STNORESTART"`
+	NoUpgrade             bool          `help:"Disable automatic upgrades" env:"STNOUPGRADE"`
+	Paused                bool          `help:"Start with all devices and folders paused" env:"STPAUSED"`
+	Unpaused              bool          `help:"Start with all devices and folders unpaused" env:"STUNPAUSED"`
+	Verbose               bool          `help:"Print verbose log output" env:"STVERBOSE"`
 
 	// Debug options below
-	DebugGUIAssetsDir   string `placeholder:"PATH" help:"Directory to load GUI assets from" env:"STGUIASSETS"`
-	DebugPerfStats      bool   `env:"STPERFSTATS" help:"Write running performance statistics to perf-$pid.csv (Unix only)"`
-	DebugProfileBlock   bool   `env:"STBLOCKPROFILE" help:"Write block profiles to block-$pid-$timestamp.pprof every 20 seconds"`
+	DebugGUIAssetsDir   string `help:"Directory to load GUI assets from" placeholder:"PATH" env:"STGUIASSETS"`
+	DebugPerfStats      bool   `help:"Write running performance statistics to perf-$pid.csv (Unix only)" env:"STPERFSTATS"`
+	DebugProfileBlock   bool   `help:"Write block profiles to block-$pid-$timestamp.pprof every 20 seconds" env:"STBLOCKPROFILE"`
 	DebugProfileCPU     bool   `help:"Write a CPU profile to cpu-$pid.pprof on exit" env:"STCPUPROFILE"`
-	DebugProfileHeap    bool   `env:"STHEAPPROFILE" help:"Write heap profiles to heap-$pid-$timestamp.pprof each time heap usage increases"`
-	DebugProfilerListen string `placeholder:"ADDR" env:"STPROFILER" help:"Network profiler listen address"`
-	DebugResetDatabase  bool   `name:"reset-database" help:"Reset the database, forcing a full rescan and resync"`
-	DebugResetDeltaIdxs bool   `name:"reset-deltas" help:"Reset delta index IDs, forcing a full index exchange"`
+	DebugProfileHeap    bool   `help:"Write heap profiles to heap-$pid-$timestamp.pprof each time heap usage increases" env:"STHEAPPROFILE"`
+	DebugProfilerListen string `help:"Network profiler listen address" placeholder:"ADDR" env:"STPROFILER" `
+	DebugResetDeltaIdxs bool   `help:"Reset delta index IDs, forcing a full index exchange"`
 
 	// Internal options, not shown to users
 	InternalRestarting   bool `env:"STRESTART" hidden:"1"`
@@ -206,29 +208,6 @@ func defaultVars() kong.Vars {
 }
 
 func main() {
-	// First some massaging of the raw command line to fit the new model.
-	// Basically this means adding the default command at the front, and
-	// converting -options to --options.
-
-	args := os.Args[1:]
-	switch {
-	case len(args) == 0:
-		// Empty command line is equivalent to just calling serve
-		args = []string{"serve"}
-	case args[0] == "-help":
-		// For consistency, we consider this equivalent with --help even
-		// though kong would otherwise consider it a bad flag.
-		args[0] = "--help"
-	case args[0] == "-h", args[0] == "--help":
-		// Top level request for help, let it pass as-is to be handled by
-		// kong to list commands.
-	case strings.HasPrefix(args[0], "-"):
-		// There are flags not preceded by a command, so we tack on the
-		// "serve" command and convert the old style arguments (single dash)
-		// to new style (double dash).
-		args = append([]string{"serve"}, convertLegacyArgs(args)...)
-	}
-
 	// Create a parser with an overridden help function to print our extra
 	// help info.
 	parser, err := kong.New(
@@ -245,7 +224,7 @@ func main() {
 	}
 
 	kongplete.Complete(parser)
-	ctx, err := parser.Parse(args)
+	ctx, err := parser.Parse(os.Args[1:])
 	parser.FatalIfErrorf(err)
 	ctx.BindTo(l, (*logger.Logger)(nil)) // main logger available to subcommands
 	err = ctx.Run()
@@ -309,101 +288,12 @@ func (options serveOptions) Run() error {
 		}
 	}
 
-	if options.Version {
-		fmt.Println(build.LongVersion)
-		return nil
-	}
-
-	if options.Paths {
-		fmt.Print(locations.PrettyPaths())
-		return nil
-	}
-
-	if options.DeviceID {
-		cert, err := tls.LoadX509KeyPair(
-			locations.Get(locations.CertFile),
-			locations.Get(locations.KeyFile),
-		)
-		if err != nil {
-			l.Warnln("Error reading device ID:", err)
-			os.Exit(svcutil.ExitError.AsInt())
-		}
-
-		fmt.Println(protocol.NewDeviceID(cert.Certificate[0]))
-		return nil
-	}
-
-	if options.BrowserOnly {
-		if err := openGUI(); err != nil {
-			l.Warnln("Failed to open web UI:", err)
-			os.Exit(svcutil.ExitError.AsInt())
-		}
-		return nil
-	}
-
-	if options.GenerateDir != "" {
-		if err := generate.Generate(l, options.GenerateDir, "", "", options.NoDefaultFolder, options.SkipPortProbing); err != nil {
-			l.Warnln("Failed to generate config and keys:", err)
-			os.Exit(svcutil.ExitError.AsInt())
-		}
-		return nil
-	}
-
 	// Ensure that our home directory exists.
 	if err := syncthing.EnsureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0o700); err != nil {
 		l.Warnln("Failure on home directory:", err)
 		os.Exit(svcutil.ExitError.AsInt())
 	}
 
-	if options.UpgradeTo != "" {
-		err := upgrade.ToURL(options.UpgradeTo)
-		if err != nil {
-			l.Warnln("Error while Upgrading:", err)
-			os.Exit(svcutil.ExitError.AsInt())
-		}
-		l.Infoln("Upgraded from", options.UpgradeTo)
-		return nil
-	}
-
-	if options.UpgradeCheck {
-		if _, err := checkUpgrade(); err != nil {
-			l.Warnln("Checking for upgrade:", err)
-			os.Exit(exitCodeForUpgrade(err))
-		}
-		return nil
-	}
-
-	if options.Upgrade {
-		release, err := checkUpgrade()
-		if err == nil {
-			lf := flock.New(locations.Get(locations.CertFile))
-			locked, err := lf.TryLock()
-			if err != nil {
-				l.Warnln("Upgrade:", err)
-				os.Exit(1)
-			} else if locked {
-				err = upgradeViaRest()
-			} else {
-				err = upgrade.To(release)
-			}
-		}
-		if err != nil {
-			l.Warnln("Upgrade:", err)
-			os.Exit(exitCodeForUpgrade(err))
-		}
-		l.Infof("Upgraded to %q", release.Tag)
-		os.Exit(svcutil.ExitUpgrade.AsInt())
-	}
-
-	if options.DebugResetDatabase {
-		if err := resetDB(); err != nil {
-			l.Warnln("Resetting database:", err)
-			os.Exit(svcutil.ExitError.AsInt())
-		}
-		l.Infoln("Successfully reset database - it will be rebuilt after next start.")
-		return nil
-	}
-
 	if options.InternalInnerProcess {
 		syncthingMain(options)
 	} else {
@@ -568,7 +458,7 @@ func syncthingMain(options serveOptions) {
 	evLogger := events.NewLogger()
 	earlyService.Add(evLogger)
 
-	cfgWrapper, err := syncthing.LoadConfigAtStartup(locations.Get(locations.ConfigFile), cert, evLogger, options.AllowNewerConfig, options.NoDefaultFolder, options.SkipPortProbing)
+	cfgWrapper, err := syncthing.LoadConfigAtStartup(locations.Get(locations.ConfigFile), cert, evLogger, options.AllowNewerConfig, options.NoDefaultFolder, options.NoPortProbing)
 	if err != nil {
 		l.Warnln("Failed to initialize config:", err)
 		os.Exit(svcutil.ExitError.AsInt())
@@ -938,3 +828,104 @@ func convertLegacyArgs(args []string) []string {
 
 	return res
 }
+
+type versionCmd struct{}
+
+func (versionCmd) Run() error {
+	fmt.Println(build.LongVersion)
+	return nil
+}
+
+type deviceIDCmd struct{}
+
+func (deviceIDCmd) Run() error {
+	cert, err := tls.LoadX509KeyPair(
+		locations.Get(locations.CertFile),
+		locations.Get(locations.KeyFile),
+	)
+	if err != nil {
+		l.Warnln("Error reading device ID:", err)
+		os.Exit(svcutil.ExitError.AsInt())
+	}
+
+	fmt.Println(protocol.NewDeviceID(cert.Certificate[0]))
+	return nil
+}
+
+type pathsCmd struct{}
+
+func (pathsCmd) Run() error {
+	fmt.Print(locations.PrettyPaths())
+	return nil
+}
+
+type upgradeCmd struct {
+	CheckOnly bool   `short:"c" help:"Check for available upgrade, then exit"`
+	From      string `short:"u" placeholder:"URL" help:"Force upgrade directly from specified URL"`
+}
+
+func (u upgradeCmd) Run() error {
+	if u.CheckOnly {
+		if _, err := checkUpgrade(); err != nil {
+			l.Warnln("Checking for upgrade:", err)
+			os.Exit(exitCodeForUpgrade(err))
+		}
+		return nil
+	}
+
+	if u.From != "" {
+		err := upgrade.ToURL(u.From)
+		if err != nil {
+			l.Warnln("Error while Upgrading:", err)
+			os.Exit(svcutil.ExitError.AsInt())
+		}
+		l.Infoln("Upgraded from", u.From)
+		return nil
+	}
+
+	release, err := checkUpgrade()
+	if err == nil {
+		lf := flock.New(locations.Get(locations.CertFile))
+		locked, err := lf.TryLock()
+		if err != nil {
+			l.Warnln("Upgrade:", err)
+			os.Exit(1)
+		} else if locked {
+			err = upgradeViaRest()
+		} else {
+			err = upgrade.To(release)
+		}
+	}
+	if err != nil {
+		l.Warnln("Upgrade:", err)
+		os.Exit(exitCodeForUpgrade(err))
+	}
+	l.Infof("Upgraded to %q", release.Tag)
+	os.Exit(svcutil.ExitUpgrade.AsInt())
+	return nil
+}
+
+type browserCmd struct{}
+
+func (browserCmd) Run() error {
+	if err := openGUI(); err != nil {
+		l.Warnln("Failed to open web UI:", err)
+		os.Exit(svcutil.ExitError.AsInt())
+	}
+	return nil
+}
+
+type debugCmd struct {
+	ResetDatabase resetDatabaseCmd `cmd:"" help:"Reset the database, forcing a full rescan and resync"`
+}
+
+type resetDatabaseCmd struct{}
+
+func (resetDatabaseCmd) Run() error {
+	if err := resetDB(); err != nil {
+		l.Warnln("Resetting database:", err)
+		os.Exit(svcutil.ExitError.AsInt())
+	}
+	l.Infoln("Successfully reset database - it will be rebuilt after next start.")
+	return nil
+}