diff --git a/cmd/syncthing/cli/main.go b/cmd/syncthing/cli/main.go index 82367cc67..b0dceff57 100644 --- a/cmd/syncthing/cli/main.go +++ b/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"` diff --git a/cmd/syncthing/cmdutil/diroptions.go b/cmd/syncthing/cmdutil/diroptions.go new file mode 100644 index 000000000..b51126f74 --- /dev/null +++ b/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"` +} diff --git a/cmd/syncthing/cmdutil/options_common.go b/cmd/syncthing/cmdutil/options_common.go deleted file mode 100644 index 1a7a30999..000000000 --- a/cmd/syncthing/cmdutil/options_common.go +++ /dev/null @@ -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"` -} diff --git a/cmd/syncthing/generate/generate.go b/cmd/syncthing/generate/generate.go index 5446cb3ba..237aad2f3 100644 --- a/cmd/syncthing/generate/generate.go +++ b/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 diff --git a/cmd/syncthing/cmdutil/options_others.go b/cmd/syncthing/hideconsole_others.go similarity index 86% rename from cmd/syncthing/cmdutil/options_others.go rename to cmd/syncthing/hideconsole_others.go index e299ba48c..0688dac1c 100644 --- a/cmd/syncthing/cmdutil/options_others.go +++ b/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:""` } diff --git a/cmd/syncthing/cmdutil/options_windows.go b/cmd/syncthing/hideconsole_windows.go similarity index 65% rename from cmd/syncthing/cmdutil/options_windows.go rename to cmd/syncthing/hideconsole_windows.go index 99fb50d5b..f51e8acc3 100644 --- a/cmd/syncthing/cmdutil/options_windows.go +++ b/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"` } diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 740ac7c5b..7df390c71 100644 --- a/cmd/syncthing/main.go +++ b/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 +}