diff --git a/cmd/syncthing/blockprof.go b/cmd/syncthing/blockprof.go index 159da9b7d..0dbee8893 100644 --- a/cmd/syncthing/blockprof.go +++ b/cmd/syncthing/blockprof.go @@ -15,19 +15,17 @@ import ( "time" ) -func init() { - if innerProcess && os.Getenv("STBLOCKPROFILE") != "" { - profiler := pprof.Lookup("block") - if profiler == nil { - panic("Couldn't find block profiler") - } - l.Debugln("Starting block profiling") - go func() { - err := saveBlockingProfiles(profiler) // Only returns on error - l.Warnln("Block profiler failed:", err) - panic("Block profiler failed") - }() +func startBlockProfiler() { + profiler := pprof.Lookup("block") + if profiler == nil { + panic("Couldn't find block profiler") } + l.Debugln("Starting block profiling") + go func() { + err := saveBlockingProfiles(profiler) // Only returns on error + l.Warnln("Block profiler failed:", err) + panic("Block profiler failed") + }() } func saveBlockingProfiles(profiler *pprof.Profile) error { diff --git a/cmd/syncthing/heapprof.go b/cmd/syncthing/heapprof.go index 29b71a811..ca552d6a4 100644 --- a/cmd/syncthing/heapprof.go +++ b/cmd/syncthing/heapprof.go @@ -11,24 +11,17 @@ import ( "os" "runtime" "runtime/pprof" - "strconv" "syscall" "time" ) -func init() { - if innerProcess && os.Getenv("STHEAPPROFILE") != "" { - rate := 1 - if i, err := strconv.Atoi(os.Getenv("STHEAPPROFILE")); err == nil { - rate = i - } - l.Debugln("Starting heap profiling") - go func() { - err := saveHeapProfiles(rate) // Only returns on error - l.Warnln("Heap profiler failed:", err) - panic("Heap profiler failed") - }() - } +func startHeapProfiler() { + l.Debugln("Starting heap profiling") + go func() { + err := saveHeapProfiles(1) // Only returns on error + l.Warnln("Heap profiler failed:", err) + panic("Heap profiler failed") + }() } func saveHeapProfiles(rate int) error { diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 2ec4c6d1f..ecb6aedf5 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -10,7 +10,6 @@ import ( "bytes" "context" "crypto/tls" - "flag" "fmt" "io" "io/ioutil" @@ -22,13 +21,16 @@ import ( "os/signal" "path" "path/filepath" + "regexp" "runtime" "runtime/pprof" "sort" "strconv" + "strings" "syscall" "time" + "github.com/alecthomas/kong" "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/db" @@ -55,9 +57,8 @@ const ( ) const ( - usage = "syncthing [options]" extraUsage = ` -The -logflags value is a sum of the following: +The --logflags value is a sum of the following: 1 Date 2 Time @@ -80,31 +81,11 @@ Development Settings -------------------- The following environment variables modify Syncthing's behavior in ways that -are mostly useful for developers. Use with care. - - STNODEFAULTFOLDER Don't create a default folder when starting for the first - time. This variable will be ignored anytime after the first - run. - - STGUIASSETS Directory to load GUI assets from. Overrides compiled in - assets. +are mostly useful for developers. Use with care. See also the --debug-* options +above. STTRACE A comma separated string of facilities to trace. The valid - facility strings listed below. - - STPROFILER Set to a listen address such as "127.0.0.1:9090" to start - the profiler with HTTP access. - - STCPUPROFILE Write a CPU profile to cpu-$pid.pprof on exit. - - STHEAPPROFILE Write heap profiles to heap-$pid-$timestamp.pprof each time - heap usage increases. - - STBLOCKPROFILE Write block profiles to block-$pid-$timestamp.pprof every 20 - seconds. - - STPERFSTATS Write running performance statistics to perf-$pid.csv. Not - supported on Windows. + facility strings are listed below. STDEADLOCKTIMEOUT Used for debugging internal deadlocks; sets debug sensitivity. Use only under direction of a developer. @@ -112,24 +93,11 @@ are mostly useful for developers. Use with care. STLOCKTHRESHOLD Used for debugging internal deadlocks; sets debug sensitivity. Use only under direction of a developer. - STNORESTART Equivalent to the -no-restart argument. - - STNOUPGRADE Disable automatic upgrades. - STHASHING Select the SHA256 hashing package to use. Possible values are "standard" for the Go standard library implementation, "minio" for the github.com/minio/sha256-simd implementation, and blank (the default) for auto detection. - STRECHECKDBEVERY Set to a time interval to override the default database - check interval of 30 days (720h). The interval understands - "h", "m" and "s" abbreviations for hours minutes and seconds. - Valid values are like "720h", "30s", etc. - - STGCINDIRECTEVERY Set to a time interval to override the default database - indirection GC interval of 13 hours. Same format as the - STRECHECKDBEVERY variable. - GOMAXPROCS Set the maximum number of CPU cores to use. Defaults to all available CPU cores. @@ -143,75 +111,83 @@ Debugging Facilities The following are valid values for the STTRACE variable: -%s` +%s +` ) var ( - // Environment options - innerProcess = os.Getenv("STMONITORED") != "" - noDefaultFolder = os.Getenv("STNODEFAULTFOLDER") != "" - upgradeCheckInterval = 5 * time.Minute upgradeRetryInterval = time.Hour upgradeCheckKey = "lastUpgradeCheck" upgradeTimeKey = "lastUpgradeTime" upgradeVersionKey = "lastUpgradeVersion" - errConcurrentUpgrade = errors.New("upgrade prevented by other running Syncthing instance") errTooEarlyUpgradeCheck = fmt.Errorf("last upgrade check happened less than %v ago, skipping", upgradeCheckInterval) errTooEarlyUpgrade = fmt.Errorf("last upgrade happened less than %v ago, skipping", upgradeRetryInterval) ) -type RuntimeOptions struct { - syncthing.Options - homeDir string - confDir string - dataDir string - resetDatabase bool - showVersion bool - showPaths bool - showDeviceId bool - doUpgrade bool - doUpgradeCheck bool - upgradeTo string - noBrowser bool - browserOnly bool - hideConsole bool - logFile string - logMaxSize int - logMaxFiles int - auditEnabled bool - auditFile string - paused bool - unpaused bool - guiAddress string - guiAPIKey string - generateDir string - noRestart bool - cpuProfile bool - stRestarting bool - logFlags int - showHelp bool - allowNewerConfig bool +// The cli struct is the main entry point for the command line parser. The +// commands and options here are top level commands to syncthing. +var cli struct { + Serve serveOptions `cmd:"" help:"Run Syncthing"` } -func defaultRuntimeOptions() RuntimeOptions { - options := RuntimeOptions{ - Options: syncthing.Options{ - AssetDir: os.Getenv("STGUIASSETS"), - NoUpgrade: os.Getenv("STNOUPGRADE") != "", - ProfilerURL: os.Getenv("STPROFILER"), - }, - noRestart: os.Getenv("STNORESTART") != "", - cpuProfile: os.Getenv("STCPUPROFILE") != "", - stRestarting: os.Getenv("STRESTART") != "", - logFlags: log.Ltime, - logMaxSize: 10 << 20, // 10 MiB - logMaxFiles: 3, // plus the current one - } +// serveOptions are the options for the `syncthing serve` command. +type serveOptions struct { + 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"` + ConfDir string `name:"conf" placeholder:"PATH" help:"Set configuration directory (config and keys)"` + DataDir string `name:"data" placeholder:"PATH" 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"` + 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"` + HideConsole bool `help:"Hide console window (Windows only)"` + HomeDir string `name:"home" placeholder:"PATH" help:"Set configuration and data directory"` + LogFile string `placeholder:"PATH" help:"Log file name (see below)"` + LogFlags int `placeholder:"BITS" help:"Select information in log line prefix (see below)"` + LogMaxFiles int `placeholder:"N" name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)"` + LogMaxSize int `placeholder:"BYTES" 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"` + NoDefaultFolder bool `env:"STNODEFAULTFOLDER" help:"Don't create the \"default\" folder on first startup"` + 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"` + + // Debug options below + DebugDBIndirectGCInterval time.Duration `env:"STGCINDIRECTEVERY" help:"Database indirection GC interval"` + DebugDBRecheckInterval time.Duration `env:"STRECHECKDBEVERY" help:"Database metadata recalculation interval"` + DebugDeadlockTimeout int `placeholder:"SECONDS" env:"STDEADLOCKTIMEOUT" help:"Used for debugging internal deadlocks"` + 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"` + DebugProfileCPU bool `help:"Write a CPU profile to cpu-$pid.pprof on exit" env:"CPUPROFILE"` + 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"` + + // Internal options, not shown to users + InternalRestarting bool `env:"STRESTART" hidden:"1"` + InternalInnerProcess bool `env:"STMONITORED" hidden:"1"` +} + +func (options *serveOptions) setDefaults() { + options.LogFlags = log.Ltime + options.LogMaxSize = 10 << 20 // 10 MiB + options.LogMaxFiles = 3 // plus the current one if os.Getenv("STTRACE") != "" { - options.logFlags = logger.DebugFlags + options.LogFlags = logger.DebugFlags } // On non-Windows, we explicitly default to "-" which means stdout. On @@ -219,107 +195,97 @@ func defaultRuntimeOptions() RuntimeOptions { // default path, unless the user has manually specified "-" or // something else. if runtime.GOOS == "windows" { - options.logFile = "default" + options.LogFile = "default" } else { - options.logFile = "-" + options.LogFile = "-" } - - return options -} - -func parseCommandLineOptions() RuntimeOptions { - options := defaultRuntimeOptions() - - flag.StringVar(&options.generateDir, "generate", "", "Generate key and config in specified dir, then exit") - flag.StringVar(&options.guiAddress, "gui-address", options.guiAddress, "Override GUI address (e.g. \"http://192.0.2.42:8443\")") - flag.StringVar(&options.guiAPIKey, "gui-apikey", options.guiAPIKey, "Override GUI API key") - flag.StringVar(&options.homeDir, "home", "", "Set configuration and data directory") - flag.StringVar(&options.confDir, "config", "", "Set configuration directory (config and keys)") - flag.StringVar(&options.dataDir, "data", "", "Set data directory (database and logs)") - flag.IntVar(&options.logFlags, "logflags", options.logFlags, "Select information in log line prefix (see below)") - flag.BoolVar(&options.noBrowser, "no-browser", false, "Do not start browser") - flag.BoolVar(&options.browserOnly, "browser-only", false, "Open GUI in browser") - flag.BoolVar(&options.noRestart, "no-restart", options.noRestart, "Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash") - flag.BoolVar(&options.resetDatabase, "reset-database", false, "Reset the database, forcing a full rescan and resync") - flag.BoolVar(&options.ResetDeltaIdxs, "reset-deltas", false, "Reset delta index IDs, forcing a full index exchange") - flag.BoolVar(&options.doUpgrade, "upgrade", false, "Perform upgrade") - flag.BoolVar(&options.doUpgradeCheck, "upgrade-check", false, "Check for available upgrade") - flag.BoolVar(&options.showVersion, "version", false, "Show version") - flag.BoolVar(&options.showHelp, "help", false, "Show this help") - flag.BoolVar(&options.showPaths, "paths", false, "Show configuration paths") - flag.BoolVar(&options.showDeviceId, "device-id", false, "Show the device ID") - flag.StringVar(&options.upgradeTo, "upgrade-to", options.upgradeTo, "Force upgrade directly from specified URL") - flag.BoolVar(&options.auditEnabled, "audit", false, "Write events to audit file") - flag.BoolVar(&options.Verbose, "verbose", false, "Print verbose log output") - flag.BoolVar(&options.paused, "paused", false, "Start with all devices and folders paused") - flag.BoolVar(&options.unpaused, "unpaused", false, "Start with all devices and folders unpaused") - flag.StringVar(&options.logFile, "logfile", options.logFile, "Log file name (see below).") - flag.IntVar(&options.logMaxSize, "log-max-size", options.logMaxSize, "Maximum size of any file (zero to disable log rotation).") - flag.IntVar(&options.logMaxFiles, "log-max-old-files", options.logMaxFiles, "Number of old files to keep (zero to keep only current).") - flag.StringVar(&options.auditFile, "auditfile", options.auditFile, "Specify audit file (use \"-\" for stdout, \"--\" for stderr)") - flag.BoolVar(&options.allowNewerConfig, "allow-newer-config", false, "Allow loading newer than current config version") - if runtime.GOOS == "windows" { - // Allow user to hide the console window - flag.BoolVar(&options.hideConsole, "no-console", false, "Hide console window") - } - - longUsage := fmt.Sprintf(extraUsage, debugFacilities()) - flag.Usage = usageFor(flag.CommandLine, usage, longUsage) - flag.Parse() - - if len(flag.Args()) > 0 { - flag.Usage() - os.Exit(2) - } - - return options -} - -func setLocation(enum locations.BaseDirEnum, loc string) error { - if !filepath.IsAbs(loc) { - var err error - loc, err = filepath.Abs(loc) - if err != nil { - return err - } - } - return locations.SetBaseDir(enum, loc) } func main() { - options := parseCommandLineOptions() - l.SetFlags(options.logFlags) + // 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. - if options.guiAddress != "" { - // The config picks this up from the environment. - os.Setenv("STGUIADDRESS", options.guiAddress) - } - if options.guiAPIKey != "" { - // The config picks this up from the environment. - os.Setenv("STGUIAPIKEY", options.guiAPIKey) + 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)...) } - if options.hideConsole { + cli.Serve.setDefaults() + + // Create a parser with an overridden help function to print our extra + // help info. + parser, err := kong.New(&cli, kong.Help(extraHelpPrinter)) + if err != nil { + log.Fatal(err) + } + + ctx, err := parser.Parse(args) + parser.FatalIfErrorf(err) + err = ctx.Run() + parser.FatalIfErrorf(err) +} + +func extraHelpPrinter(options kong.HelpOptions, ctx *kong.Context) error { + if err := kong.DefaultHelpPrinter(options, ctx); err != nil { + return err + } + if ctx.Command() == "serve" { + // Help was requested for `syncthing serve`, so we add our extra + // usage info afte the normal options output. + fmt.Printf(extraUsage, debugFacilities()) + } + return nil +} + +// serveOptions.Run() is the entrypoint for `syncthing serve` +func (options serveOptions) Run() error { + l.SetFlags(options.LogFlags) + + if options.GUIAddress != "" { + // The config picks this up from the environment. + os.Setenv("STGUIADDRESS", options.GUIAddress) + } + if options.GUIAPIKey != "" { + // The config picks this up from the environment. + os.Setenv("STGUIAPIKEY", options.GUIAPIKey) + } + + if options.HideConsole { osutil.HideConsole() } // Not set as default above because the strings can be really long. var err error - homeSet := options.homeDir != "" - confSet := options.confDir != "" - dataSet := options.dataDir != "" + homeSet := options.HomeDir != "" + confSet := options.ConfDir != "" + dataSet := options.DataDir != "" switch { case dataSet != confSet: err = errors.New("either both or none of -conf and -data must be given, use -home to set both at once") case homeSet && dataSet: err = errors.New("-home must not be used together with -conf and -data") case homeSet: - if err = setLocation(locations.ConfigBaseDir, options.homeDir); err == nil { - err = setLocation(locations.DataBaseDir, options.homeDir) + if err = setLocation(locations.ConfigBaseDir, options.HomeDir); err == nil { + err = setLocation(locations.DataBaseDir, options.HomeDir) } case dataSet: - if err = setLocation(locations.ConfigBaseDir, options.confDir); err == nil { - err = setLocation(locations.DataBaseDir, options.dataDir) + if err = setLocation(locations.ConfigBaseDir, options.ConfDir); err == nil { + err = setLocation(locations.DataBaseDir, options.DataDir) } } if err != nil { @@ -327,34 +293,29 @@ func main() { os.Exit(svcutil.ExitError.AsInt()) } - if options.logFile == "default" || options.logFile == "" { + if options.LogFile == "default" || options.LogFile == "" { // We must set this *after* expandLocations above. // Handling an empty value is for backwards compatibility (<1.4.1). - options.logFile = locations.Get(locations.LogFile) + options.LogFile = locations.Get(locations.LogFile) } - if options.AssetDir == "" { + if options.DebugGUIAssetsDir == "" { // The asset dir is blank if STGUIASSETS wasn't set, in which case we // should look for extra assets in the default place. - options.AssetDir = locations.Get(locations.GUIAssets) + options.DebugGUIAssetsDir = locations.Get(locations.GUIAssets) } - if options.showVersion { + if options.Version { fmt.Println(build.LongVersion) - return + return nil } - if options.showHelp { - flag.Usage() - return - } - - if options.showPaths { + if options.Paths { showPaths(options) - return + return nil } - if options.showDeviceId { + if options.DeviceID { cert, err := tls.LoadX509KeyPair( locations.Get(locations.CertFile), locations.Get(locations.KeyFile), @@ -365,23 +326,23 @@ func main() { } fmt.Println(protocol.NewDeviceID(cert.Certificate[0])) - return + return nil } - if options.browserOnly { + if options.BrowserOnly { if err := openGUI(protocol.EmptyDeviceID); err != nil { l.Warnln("Failed to open web UI:", err) os.Exit(svcutil.ExitError.AsInt()) } - return + return nil } - if options.generateDir != "" { - if err := generate(options.generateDir); err != nil { + if options.GenerateDir != "" { + if err := generate(options.GenerateDir, options.NoDefaultFolder); err != nil { l.Warnln("Failed to generate config and keys:", err) os.Exit(svcutil.ExitError.AsInt()) } - return + return nil } // Ensure that our home directory exists. @@ -390,25 +351,25 @@ func main() { os.Exit(svcutil.ExitError.AsInt()) } - if options.upgradeTo != "" { - err := upgrade.ToURL(options.upgradeTo) + 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 + l.Infoln("Upgraded from", options.UpgradeTo) + return nil } - if options.doUpgradeCheck { + if options.UpgradeCheck { if _, err := checkUpgrade(); err != nil { l.Warnln("Checking for upgrade:", err) os.Exit(exitCodeForUpgrade(err)) } - return + return nil } - if options.doUpgrade { + if options.Upgrade { release, err := checkUpgrade() if err == nil { // Use leveldb database locks to protect against concurrent upgrades @@ -428,24 +389,25 @@ func main() { os.Exit(svcutil.ExitUpgrade.AsInt()) } - if options.resetDatabase { + 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 + return nil } - if innerProcess { + if options.InternalInnerProcess { syncthingMain(options) } else { monitorMain(options) } + return nil } func openGUI(myID protocol.DeviceID) error { - cfg, err := loadOrDefaultConfig(myID, events.NoopLogger) + cfg, err := loadOrDefaultConfig(myID, events.NoopLogger, true) if err != nil { return err } @@ -459,7 +421,7 @@ func openGUI(myID protocol.DeviceID) error { return nil } -func generate(generateDir string) error { +func generate(generateDir string, noDefaultFolder bool) error { dir, err := fs.ExpandTilde(generateDir) if err != nil { return err @@ -530,7 +492,7 @@ func (e *errNoUpgrade) Error() string { } func checkUpgrade() (upgrade.Release, error) { - cfg, err := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger) + cfg, err := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger, true) if err != nil { return upgrade.Release{}, err } @@ -549,7 +511,7 @@ func checkUpgrade() (upgrade.Release, error) { } func upgradeViaRest() error { - cfg, _ := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger) + cfg, _ := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger, true) u, err := url.Parse(cfg.GUI().URL()) if err != nil { return err @@ -584,7 +546,17 @@ func upgradeViaRest() error { return err } -func syncthingMain(runtimeOptions RuntimeOptions) { +func syncthingMain(options serveOptions) { + if options.DebugProfileBlock { + startBlockProfiler() + } + if options.DebugProfileHeap { + startHeapProfiler() + } + if options.DebugPerfStats { + startPerfStats() + } + // Set a log prefix similar to the ID we will have later on, or early log // lines look ugly. l.SetPrefix("[start] ") @@ -615,7 +587,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { evLogger := events.NewLogger() earlyService.Add(evLogger) - cfgWrapper, err := syncthing.LoadConfigAtStartup(locations.Get(locations.ConfigFile), cert, evLogger, runtimeOptions.allowNewerConfig, noDefaultFolder) + cfgWrapper, err := syncthing.LoadConfigAtStartup(locations.Get(locations.ConfigFile), cert, evLogger, options.AllowNewerConfig, options.NoDefaultFolder) if err != nil { l.Warnln("Failed to initialize config:", err) os.Exit(svcutil.ExitError.AsInt()) @@ -628,7 +600,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // unless we are in a build where it's disabled or the STNOUPGRADE // environment variable is set. - if build.IsCandidate && !upgrade.DisabledByCompilation && !runtimeOptions.NoUpgrade { + if build.IsCandidate && !upgrade.DisabledByCompilation && !options.NoUpgrade { cfgWrapper.Modify(func(cfg *config.Configuration) { l.Infoln("Automatic upgrade is always enabled for candidate releases.") if cfg.Options.AutoUpgradeIntervalH == 0 || cfg.Options.AutoUpgradeIntervalH > 24 { @@ -652,7 +624,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // upgrade immedately. The auto-upgrade routine can only be started // later after App is initialised. - autoUpgradePossible := autoUpgradePossible(runtimeOptions) + autoUpgradePossible := autoUpgradePossible(options) if autoUpgradePossible && cfgWrapper.Options().AutoUpgradeEnabled() { // try to do upgrade directly and log the error if relevant. release, err := initialAutoUpgradeCheck(db.NewMiscDataNamespace(ldb)) @@ -671,15 +643,24 @@ func syncthingMain(runtimeOptions RuntimeOptions) { } } - if runtimeOptions.unpaused { + if options.Unpaused { setPauseState(cfgWrapper, false) - } else if runtimeOptions.paused { + } else if options.Paused { setPauseState(cfgWrapper, true) } - appOpts := runtimeOptions.Options - if runtimeOptions.auditEnabled { - appOpts.AuditWriter = auditWriter(runtimeOptions.auditFile) + appOpts := syncthing.Options{ + AssetDir: options.DebugGUIAssetsDir, + DeadlockTimeoutS: options.DebugDeadlockTimeout, + NoUpgrade: options.NoUpgrade, + ProfilerAddr: options.DebugProfilerListen, + ResetDeltaIdxs: options.DebugResetDeltaIdxs, + Verbose: options.Verbose, + DBRecheckInterval: options.DebugDBRecheckInterval, + DBIndirectGCInterval: options.DebugDBIndirectGCInterval, + } + if options.Audit { + appOpts.AuditWriter = auditWriter(options.AuditFile) } if t := os.Getenv("STDEADLOCKTIMEOUT"); t != "" { secs, _ := strconv.Atoi(t) @@ -708,7 +689,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { runtime.GOMAXPROCS(runtime.NumCPU()) } - if runtimeOptions.cpuProfile { + if options.DebugProfileCPU { f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid())) if err != nil { l.Warnln("Creating profile:", err) @@ -728,7 +709,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { cleanConfigDirectory() - if cfgWrapper.Options().StartBrowser && !runtimeOptions.noBrowser && !runtimeOptions.stRestarting { + if cfgWrapper.Options().StartBrowser && !options.NoBrowser && !options.InternalRestarting { // Can potentially block if the utility we are invoking doesn't // fork, and just execs, hence keep it in its own routine. go func() { _ = openURL(cfgWrapper.GUI().URL()) }() @@ -740,7 +721,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { l.Warnln("Syncthing stopped with error:", app.Error()) } - if runtimeOptions.cpuProfile { + if options.DebugProfileCPU { pprof.StopCPUProfile() } @@ -768,7 +749,7 @@ func setupSignalHandling(app *syncthing.App) { }() } -func loadOrDefaultConfig(myID protocol.DeviceID, evLogger events.Logger) (config.Wrapper, error) { +func loadOrDefaultConfig(myID protocol.DeviceID, evLogger events.Logger, noDefaultFolder bool) (config.Wrapper, error) { cfgFile := locations.Get(locations.ConfigFile) cfg, _, err := config.Load(cfgFile, myID, evLogger) @@ -858,11 +839,11 @@ func standbyMonitor(app *syncthing.App, cfg config.Wrapper) { } } -func autoUpgradePossible(runtimeOptions RuntimeOptions) bool { +func autoUpgradePossible(options serveOptions) bool { if upgrade.DisabledByCompilation { return false } - if runtimeOptions.NoUpgrade { + if options.NoUpgrade { l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.") return false } @@ -989,13 +970,13 @@ func cleanConfigDirectory() { } } -func showPaths(options RuntimeOptions) { +func showPaths(options serveOptions) { fmt.Printf("Configuration file:\n\t%s\n\n", locations.Get(locations.ConfigFile)) fmt.Printf("Database directory:\n\t%s\n\n", locations.Get(locations.Database)) fmt.Printf("Device private key & certificate files:\n\t%s\n\t%s\n\n", locations.Get(locations.KeyFile), locations.Get(locations.CertFile)) fmt.Printf("HTTPS private key & certificate files:\n\t%s\n\t%s\n\n", locations.Get(locations.HTTPSKeyFile), locations.Get(locations.HTTPSCertFile)) - fmt.Printf("Log file:\n\t%s\n\n", options.logFile) - fmt.Printf("GUI override directory:\n\t%s\n\n", options.AssetDir) + fmt.Printf("Log file:\n\t%s\n\n", options.LogFile) + fmt.Printf("GUI override directory:\n\t%s\n\n", options.DebugGUIAssetsDir) fmt.Printf("Default sync folder directory:\n\t%s\n\n", locations.Get(locations.DefFolder)) } @@ -1020,3 +1001,32 @@ func exitCodeForUpgrade(err error) int { } return svcutil.ExitError.AsInt() } + +func setLocation(enum locations.BaseDirEnum, loc string) error { + if !filepath.IsAbs(loc) { + var err error + loc, err = filepath.Abs(loc) + if err != nil { + return err + } + } + return locations.SetBaseDir(enum, loc) +} + +// convertLegacyArgs returns the slice of arguments with single dash long +// flags converted to double dash long flags. +func convertLegacyArgs(args []string) []string { + // Legacy args begin with a single dash, followed by two or more characters. + legacyExp := regexp.MustCompile(`^-\w{2,}`) + + res := make([]string, len(args)) + for i, arg := range args { + if legacyExp.MatchString(arg) { + res[i] = "-" + arg + } else { + res[i] = arg + } + } + + return res +} diff --git a/cmd/syncthing/monitor.go b/cmd/syncthing/monitor.go index 66113bed6..829daec8e 100644 --- a/cmd/syncthing/monitor.go +++ b/cmd/syncthing/monitor.go @@ -44,12 +44,12 @@ const ( panicUploadNoticeWait = 10 * time.Second ) -func monitorMain(runtimeOptions RuntimeOptions) { +func monitorMain(options serveOptions) { l.SetPrefix("[monitor] ") var dst io.Writer = os.Stdout - logFile := runtimeOptions.logFile + logFile := options.LogFile if logFile != "-" { if expanded, err := fs.ExpandTilde(logFile); err == nil { logFile = expanded @@ -59,8 +59,8 @@ func monitorMain(runtimeOptions RuntimeOptions) { open := func(name string) (io.WriteCloser, error) { return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime) } - if runtimeOptions.logMaxSize > 0 { - fileDst, err = newRotatedFile(logFile, open, int64(runtimeOptions.logMaxSize), runtimeOptions.logMaxFiles) + if options.LogMaxSize > 0 { + fileDst, err = newRotatedFile(logFile, open, int64(options.LogMaxSize), options.LogMaxFiles) } else { fileDst, err = open(logFile) } @@ -174,7 +174,7 @@ func monitorMain(runtimeOptions RuntimeOptions) { if exiterr, ok := err.(*exec.ExitError); ok { exitCode := exiterr.ExitCode() - if stopped || runtimeOptions.noRestart { + if stopped || options.NoRestart { os.Exit(exitCode) } if exitCode == svcutil.ExitUpgrade.AsInt() { @@ -188,7 +188,7 @@ func monitorMain(runtimeOptions RuntimeOptions) { } } - if runtimeOptions.noRestart { + if options.NoRestart { os.Exit(svcutil.ExitError.AsInt()) } @@ -563,7 +563,7 @@ func childEnv() []string { // panicUploadMaxWait uploading panics... func maybeReportPanics() { // Try to get a config to see if/where panics should be reported. - cfg, err := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger) + cfg, err := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger, true) if err != nil { l.Warnln("Couldn't load config; not reporting crash") return diff --git a/cmd/syncthing/perfstats_unix.go b/cmd/syncthing/perfstats_unix.go index 94fd6de0c..4ec20b3ef 100644 --- a/cmd/syncthing/perfstats_unix.go +++ b/cmd/syncthing/perfstats_unix.go @@ -18,10 +18,8 @@ import ( "github.com/syncthing/syncthing/lib/protocol" ) -func init() { - if innerProcess && os.Getenv("STPERFSTATS") != "" { - go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid())) - } +func startPerfStats() { + go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid())) } func savePerfStats(file string) { diff --git a/cmd/syncthing/perfstats_unsupported.go b/cmd/syncthing/perfstats_unsupported.go new file mode 100644 index 000000000..7621fe2d3 --- /dev/null +++ b/cmd/syncthing/perfstats_unsupported.go @@ -0,0 +1,12 @@ +// 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/. + +// +build solaris windows + +package main + +func startPerfStats() { +} diff --git a/cmd/syncthing/usage.go b/cmd/syncthing/usage.go deleted file mode 100644 index 1284ed2d4..000000000 --- a/cmd/syncthing/usage.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2014 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 main - -import ( - "bytes" - "flag" - "fmt" - "io" - "text/tabwriter" -) - -func optionTable(w io.Writer, rows [][]string) { - tw := tabwriter.NewWriter(w, 2, 4, 2, ' ', 0) - for _, row := range rows { - for i, cell := range row { - if i > 0 { - tw.Write([]byte("\t")) - } - tw.Write([]byte(cell)) - } - tw.Write([]byte("\n")) - } - tw.Flush() -} - -func usageFor(fs *flag.FlagSet, usage string, extra string) func() { - return func() { - var b bytes.Buffer - b.WriteString("Usage:\n " + usage + "\n") - - var options [][]string - fs.VisitAll(func(f *flag.Flag) { - var opt = " -" + f.Name - - if f.DefValue != "false" { - opt += "=" + fmt.Sprintf(`"%s"`, f.DefValue) - } - options = append(options, []string{opt, f.Usage}) - }) - - if len(options) > 0 { - b.WriteString("\nOptions:\n") - optionTable(&b, options) - } - - fmt.Println(b.String()) - - if len(extra) > 0 { - fmt.Println(extra) - } - } -} diff --git a/go.mod b/go.mod index 0ba2e1227..e89c8c520 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/syncthing/syncthing require ( github.com/AudriusButkevicius/pfilter v0.0.0-20190627213056-c55ef6137fc6 github.com/AudriusButkevicius/recli v0.0.5 + github.com/alecthomas/kong v0.2.12 github.com/bkaradzic/go-lz4 v0.0.0-20160924222819-7224d8d8f27e github.com/calmh/xdr v1.1.0 github.com/ccding/go-stun v0.1.2 diff --git a/go.sum b/go.sum index 4ad4c4794..959d4d981 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUW github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/kong v0.2.12 h1:X3kkCOXGUNzLmiu+nQtoxWqj4U2a39MpSJR3QdQXOwI= +github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= diff --git a/lib/syncthing/syncthing.go b/lib/syncthing/syncthing.go index 23226c9aa..7e2a96df2 100644 --- a/lib/syncthing/syncthing.go +++ b/lib/syncthing/syncthing.go @@ -57,7 +57,7 @@ type Options struct { AuditWriter io.Writer DeadlockTimeoutS int NoUpgrade bool - ProfilerURL string + ProfilerAddr string ResetDeltaIdxs bool Verbose bool // null duration means use default value @@ -169,11 +169,11 @@ func (a *App) startup() error { return err } - if len(a.opts.ProfilerURL) > 0 { + if len(a.opts.ProfilerAddr) > 0 { go func() { - l.Debugln("Starting profiler on", a.opts.ProfilerURL) + l.Debugln("Starting profiler on", a.opts.ProfilerAddr) runtime.SetBlockProfileRate(1) - err := http.ListenAndServe(a.opts.ProfilerURL, nil) + err := http.ListenAndServe(a.opts.ProfilerAddr, nil) if err != nil { l.Warnln(err) return