cmd/syncthing: Refactor command line parsing (#7330)

This commit is contained in:
Jakob Borg 2021-02-10 20:35:37 +01:00 committed by GitHub
parent 0471daf771
commit 4f20c900d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 278 additions and 321 deletions

View File

@ -15,19 +15,17 @@ import (
"time" "time"
) )
func init() { func startBlockProfiler() {
if innerProcess && os.Getenv("STBLOCKPROFILE") != "" { profiler := pprof.Lookup("block")
profiler := pprof.Lookup("block") if profiler == nil {
if profiler == nil { panic("Couldn't find block profiler")
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")
}()
} }
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 { func saveBlockingProfiles(profiler *pprof.Profile) error {

View File

@ -11,24 +11,17 @@ import (
"os" "os"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
"strconv"
"syscall" "syscall"
"time" "time"
) )
func init() { func startHeapProfiler() {
if innerProcess && os.Getenv("STHEAPPROFILE") != "" { l.Debugln("Starting heap profiling")
rate := 1 go func() {
if i, err := strconv.Atoi(os.Getenv("STHEAPPROFILE")); err == nil { err := saveHeapProfiles(1) // Only returns on error
rate = i l.Warnln("Heap profiler failed:", err)
} panic("Heap profiler failed")
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 saveHeapProfiles(rate int) error { func saveHeapProfiles(rate int) error {

View File

@ -10,7 +10,6 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"flag"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -22,13 +21,16 @@ import (
"os/signal" "os/signal"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
"sort" "sort"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"time" "time"
"github.com/alecthomas/kong"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/db"
@ -55,9 +57,8 @@ const (
) )
const ( const (
usage = "syncthing [options]"
extraUsage = ` extraUsage = `
The -logflags value is a sum of the following: The --logflags value is a sum of the following:
1 Date 1 Date
2 Time 2 Time
@ -80,31 +81,11 @@ Development Settings
-------------------- --------------------
The following environment variables modify Syncthing's behavior in ways that The following environment variables modify Syncthing's behavior in ways that
are mostly useful for developers. Use with care. are mostly useful for developers. Use with care. See also the --debug-* options
above.
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.
STTRACE A comma separated string of facilities to trace. The valid STTRACE A comma separated string of facilities to trace. The valid
facility strings listed below. facility strings are 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.
STDEADLOCKTIMEOUT Used for debugging internal deadlocks; sets debug STDEADLOCKTIMEOUT Used for debugging internal deadlocks; sets debug
sensitivity. Use only under direction of a developer. 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 STLOCKTHRESHOLD Used for debugging internal deadlocks; sets debug
sensitivity. Use only under direction of a developer. 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 STHASHING Select the SHA256 hashing package to use. Possible values
are "standard" for the Go standard library implementation, are "standard" for the Go standard library implementation,
"minio" for the github.com/minio/sha256-simd implementation, "minio" for the github.com/minio/sha256-simd implementation,
and blank (the default) for auto detection. 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 GOMAXPROCS Set the maximum number of CPU cores to use. Defaults to all
available CPU cores. available CPU cores.
@ -143,75 +111,83 @@ Debugging Facilities
The following are valid values for the STTRACE variable: The following are valid values for the STTRACE variable:
%s` %s
`
) )
var ( var (
// Environment options
innerProcess = os.Getenv("STMONITORED") != ""
noDefaultFolder = os.Getenv("STNODEFAULTFOLDER") != ""
upgradeCheckInterval = 5 * time.Minute upgradeCheckInterval = 5 * time.Minute
upgradeRetryInterval = time.Hour upgradeRetryInterval = time.Hour
upgradeCheckKey = "lastUpgradeCheck" upgradeCheckKey = "lastUpgradeCheck"
upgradeTimeKey = "lastUpgradeTime" upgradeTimeKey = "lastUpgradeTime"
upgradeVersionKey = "lastUpgradeVersion" 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) 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) errTooEarlyUpgrade = fmt.Errorf("last upgrade happened less than %v ago, skipping", upgradeRetryInterval)
) )
type RuntimeOptions struct { // The cli struct is the main entry point for the command line parser. The
syncthing.Options // commands and options here are top level commands to syncthing.
homeDir string var cli struct {
confDir string Serve serveOptions `cmd:"" help:"Run Syncthing"`
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
} }
func defaultRuntimeOptions() RuntimeOptions { // serveOptions are the options for the `syncthing serve` command.
options := RuntimeOptions{ type serveOptions struct {
Options: syncthing.Options{ AllowNewerConfig bool `help:"Allow loading newer than current config version"`
AssetDir: os.Getenv("STGUIASSETS"), Audit bool `help:"Write events to audit file"`
NoUpgrade: os.Getenv("STNOUPGRADE") != "", AuditFile string `name:"auditfile" placeholder:"PATH" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)"`
ProfilerURL: os.Getenv("STPROFILER"), BrowserOnly bool `help:"Open GUI in browser"`
}, ConfDir string `name:"conf" placeholder:"PATH" help:"Set configuration directory (config and keys)"`
noRestart: os.Getenv("STNORESTART") != "", DataDir string `name:"data" placeholder:"PATH" help:"Set data directory (database and logs)"`
cpuProfile: os.Getenv("STCPUPROFILE") != "", DeviceID bool `help:"Show the device ID"`
stRestarting: os.Getenv("STRESTART") != "", GenerateDir string `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"`
logFlags: log.Ltime, GUIAddress string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"`
logMaxSize: 10 << 20, // 10 MiB GUIAPIKey string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"`
logMaxFiles: 3, // plus the current one 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") != "" { if os.Getenv("STTRACE") != "" {
options.logFlags = logger.DebugFlags options.LogFlags = logger.DebugFlags
} }
// On non-Windows, we explicitly default to "-" which means stdout. On // 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 // default path, unless the user has manually specified "-" or
// something else. // something else.
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
options.logFile = "default" options.LogFile = "default"
} else { } 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() { func main() {
options := parseCommandLineOptions() // First some massaging of the raw command line to fit the new model.
l.SetFlags(options.logFlags) // Basically this means adding the default command at the front, and
// converting -options to --options.
if options.guiAddress != "" { args := os.Args[1:]
// The config picks this up from the environment. switch {
os.Setenv("STGUIADDRESS", options.guiAddress) case len(args) == 0:
} // Empty command line is equivalent to just calling serve
if options.guiAPIKey != "" { args = []string{"serve"}
// The config picks this up from the environment. case args[0] == "-help":
os.Setenv("STGUIAPIKEY", options.guiAPIKey) // 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() osutil.HideConsole()
} }
// Not set as default above because the strings can be really long. // Not set as default above because the strings can be really long.
var err error var err error
homeSet := options.homeDir != "" homeSet := options.HomeDir != ""
confSet := options.confDir != "" confSet := options.ConfDir != ""
dataSet := options.dataDir != "" dataSet := options.DataDir != ""
switch { switch {
case dataSet != confSet: case dataSet != confSet:
err = errors.New("either both or none of -conf and -data must be given, use -home to set both at once") err = errors.New("either both or none of -conf and -data must be given, use -home to set both at once")
case homeSet && dataSet: case homeSet && dataSet:
err = errors.New("-home must not be used together with -conf and -data") err = errors.New("-home must not be used together with -conf and -data")
case homeSet: case homeSet:
if err = setLocation(locations.ConfigBaseDir, options.homeDir); err == nil { if err = setLocation(locations.ConfigBaseDir, options.HomeDir); err == nil {
err = setLocation(locations.DataBaseDir, options.homeDir) err = setLocation(locations.DataBaseDir, options.HomeDir)
} }
case dataSet: case dataSet:
if err = setLocation(locations.ConfigBaseDir, options.confDir); err == nil { if err = setLocation(locations.ConfigBaseDir, options.ConfDir); err == nil {
err = setLocation(locations.DataBaseDir, options.dataDir) err = setLocation(locations.DataBaseDir, options.DataDir)
} }
} }
if err != nil { if err != nil {
@ -327,34 +293,29 @@ func main() {
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
if options.logFile == "default" || options.logFile == "" { if options.LogFile == "default" || options.LogFile == "" {
// We must set this *after* expandLocations above. // We must set this *after* expandLocations above.
// Handling an empty value is for backwards compatibility (<1.4.1). // 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 // The asset dir is blank if STGUIASSETS wasn't set, in which case we
// should look for extra assets in the default place. // 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) fmt.Println(build.LongVersion)
return return nil
} }
if options.showHelp { if options.Paths {
flag.Usage()
return
}
if options.showPaths {
showPaths(options) showPaths(options)
return return nil
} }
if options.showDeviceId { if options.DeviceID {
cert, err := tls.LoadX509KeyPair( cert, err := tls.LoadX509KeyPair(
locations.Get(locations.CertFile), locations.Get(locations.CertFile),
locations.Get(locations.KeyFile), locations.Get(locations.KeyFile),
@ -365,23 +326,23 @@ func main() {
} }
fmt.Println(protocol.NewDeviceID(cert.Certificate[0])) fmt.Println(protocol.NewDeviceID(cert.Certificate[0]))
return return nil
} }
if options.browserOnly { if options.BrowserOnly {
if err := openGUI(protocol.EmptyDeviceID); err != nil { if err := openGUI(protocol.EmptyDeviceID); err != nil {
l.Warnln("Failed to open web UI:", err) l.Warnln("Failed to open web UI:", err)
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
return return nil
} }
if options.generateDir != "" { if options.GenerateDir != "" {
if err := generate(options.generateDir); err != nil { if err := generate(options.GenerateDir, options.NoDefaultFolder); err != nil {
l.Warnln("Failed to generate config and keys:", err) l.Warnln("Failed to generate config and keys:", err)
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
return return nil
} }
// Ensure that our home directory exists. // Ensure that our home directory exists.
@ -390,25 +351,25 @@ func main() {
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
if options.upgradeTo != "" { if options.UpgradeTo != "" {
err := upgrade.ToURL(options.upgradeTo) err := upgrade.ToURL(options.UpgradeTo)
if err != nil { if err != nil {
l.Warnln("Error while Upgrading:", err) l.Warnln("Error while Upgrading:", err)
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
l.Infoln("Upgraded from", options.upgradeTo) l.Infoln("Upgraded from", options.UpgradeTo)
return return nil
} }
if options.doUpgradeCheck { if options.UpgradeCheck {
if _, err := checkUpgrade(); err != nil { if _, err := checkUpgrade(); err != nil {
l.Warnln("Checking for upgrade:", err) l.Warnln("Checking for upgrade:", err)
os.Exit(exitCodeForUpgrade(err)) os.Exit(exitCodeForUpgrade(err))
} }
return return nil
} }
if options.doUpgrade { if options.Upgrade {
release, err := checkUpgrade() release, err := checkUpgrade()
if err == nil { if err == nil {
// Use leveldb database locks to protect against concurrent upgrades // Use leveldb database locks to protect against concurrent upgrades
@ -428,24 +389,25 @@ func main() {
os.Exit(svcutil.ExitUpgrade.AsInt()) os.Exit(svcutil.ExitUpgrade.AsInt())
} }
if options.resetDatabase { if options.DebugResetDatabase {
if err := resetDB(); err != nil { if err := resetDB(); err != nil {
l.Warnln("Resetting database:", err) l.Warnln("Resetting database:", err)
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
l.Infoln("Successfully reset database - it will be rebuilt after next start.") l.Infoln("Successfully reset database - it will be rebuilt after next start.")
return return nil
} }
if innerProcess { if options.InternalInnerProcess {
syncthingMain(options) syncthingMain(options)
} else { } else {
monitorMain(options) monitorMain(options)
} }
return nil
} }
func openGUI(myID protocol.DeviceID) error { func openGUI(myID protocol.DeviceID) error {
cfg, err := loadOrDefaultConfig(myID, events.NoopLogger) cfg, err := loadOrDefaultConfig(myID, events.NoopLogger, true)
if err != nil { if err != nil {
return err return err
} }
@ -459,7 +421,7 @@ func openGUI(myID protocol.DeviceID) error {
return nil return nil
} }
func generate(generateDir string) error { func generate(generateDir string, noDefaultFolder bool) error {
dir, err := fs.ExpandTilde(generateDir) dir, err := fs.ExpandTilde(generateDir)
if err != nil { if err != nil {
return err return err
@ -530,7 +492,7 @@ func (e *errNoUpgrade) Error() string {
} }
func checkUpgrade() (upgrade.Release, error) { func checkUpgrade() (upgrade.Release, error) {
cfg, err := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger) cfg, err := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger, true)
if err != nil { if err != nil {
return upgrade.Release{}, err return upgrade.Release{}, err
} }
@ -549,7 +511,7 @@ func checkUpgrade() (upgrade.Release, error) {
} }
func upgradeViaRest() error { func upgradeViaRest() error {
cfg, _ := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger) cfg, _ := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger, true)
u, err := url.Parse(cfg.GUI().URL()) u, err := url.Parse(cfg.GUI().URL())
if err != nil { if err != nil {
return err return err
@ -584,7 +546,17 @@ func upgradeViaRest() error {
return err 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 // Set a log prefix similar to the ID we will have later on, or early log
// lines look ugly. // lines look ugly.
l.SetPrefix("[start] ") l.SetPrefix("[start] ")
@ -615,7 +587,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
evLogger := events.NewLogger() evLogger := events.NewLogger()
earlyService.Add(evLogger) 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 { if err != nil {
l.Warnln("Failed to initialize config:", err) l.Warnln("Failed to initialize config:", err)
os.Exit(svcutil.ExitError.AsInt()) 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 // unless we are in a build where it's disabled or the STNOUPGRADE
// environment variable is set. // environment variable is set.
if build.IsCandidate && !upgrade.DisabledByCompilation && !runtimeOptions.NoUpgrade { if build.IsCandidate && !upgrade.DisabledByCompilation && !options.NoUpgrade {
cfgWrapper.Modify(func(cfg *config.Configuration) { cfgWrapper.Modify(func(cfg *config.Configuration) {
l.Infoln("Automatic upgrade is always enabled for candidate releases.") l.Infoln("Automatic upgrade is always enabled for candidate releases.")
if cfg.Options.AutoUpgradeIntervalH == 0 || cfg.Options.AutoUpgradeIntervalH > 24 { 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 // upgrade immedately. The auto-upgrade routine can only be started
// later after App is initialised. // later after App is initialised.
autoUpgradePossible := autoUpgradePossible(runtimeOptions) autoUpgradePossible := autoUpgradePossible(options)
if autoUpgradePossible && cfgWrapper.Options().AutoUpgradeEnabled() { if autoUpgradePossible && cfgWrapper.Options().AutoUpgradeEnabled() {
// try to do upgrade directly and log the error if relevant. // try to do upgrade directly and log the error if relevant.
release, err := initialAutoUpgradeCheck(db.NewMiscDataNamespace(ldb)) release, err := initialAutoUpgradeCheck(db.NewMiscDataNamespace(ldb))
@ -671,15 +643,24 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
} }
} }
if runtimeOptions.unpaused { if options.Unpaused {
setPauseState(cfgWrapper, false) setPauseState(cfgWrapper, false)
} else if runtimeOptions.paused { } else if options.Paused {
setPauseState(cfgWrapper, true) setPauseState(cfgWrapper, true)
} }
appOpts := runtimeOptions.Options appOpts := syncthing.Options{
if runtimeOptions.auditEnabled { AssetDir: options.DebugGUIAssetsDir,
appOpts.AuditWriter = auditWriter(runtimeOptions.auditFile) 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 != "" { if t := os.Getenv("STDEADLOCKTIMEOUT"); t != "" {
secs, _ := strconv.Atoi(t) secs, _ := strconv.Atoi(t)
@ -708,7 +689,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())
} }
if runtimeOptions.cpuProfile { if options.DebugProfileCPU {
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid())) f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
if err != nil { if err != nil {
l.Warnln("Creating profile:", err) l.Warnln("Creating profile:", err)
@ -728,7 +709,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
cleanConfigDirectory() 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 // Can potentially block if the utility we are invoking doesn't
// fork, and just execs, hence keep it in its own routine. // fork, and just execs, hence keep it in its own routine.
go func() { _ = openURL(cfgWrapper.GUI().URL()) }() go func() { _ = openURL(cfgWrapper.GUI().URL()) }()
@ -740,7 +721,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
l.Warnln("Syncthing stopped with error:", app.Error()) l.Warnln("Syncthing stopped with error:", app.Error())
} }
if runtimeOptions.cpuProfile { if options.DebugProfileCPU {
pprof.StopCPUProfile() 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) cfgFile := locations.Get(locations.ConfigFile)
cfg, _, err := config.Load(cfgFile, myID, evLogger) 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 { if upgrade.DisabledByCompilation {
return false return false
} }
if runtimeOptions.NoUpgrade { if options.NoUpgrade {
l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.") l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
return false 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("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("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("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("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("Log file:\n\t%s\n\n", options.LogFile)
fmt.Printf("GUI override directory:\n\t%s\n\n", options.AssetDir) 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)) 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() 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
}

View File

@ -44,12 +44,12 @@ const (
panicUploadNoticeWait = 10 * time.Second panicUploadNoticeWait = 10 * time.Second
) )
func monitorMain(runtimeOptions RuntimeOptions) { func monitorMain(options serveOptions) {
l.SetPrefix("[monitor] ") l.SetPrefix("[monitor] ")
var dst io.Writer = os.Stdout var dst io.Writer = os.Stdout
logFile := runtimeOptions.logFile logFile := options.LogFile
if logFile != "-" { if logFile != "-" {
if expanded, err := fs.ExpandTilde(logFile); err == nil { if expanded, err := fs.ExpandTilde(logFile); err == nil {
logFile = expanded logFile = expanded
@ -59,8 +59,8 @@ func monitorMain(runtimeOptions RuntimeOptions) {
open := func(name string) (io.WriteCloser, error) { open := func(name string) (io.WriteCloser, error) {
return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime) return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime)
} }
if runtimeOptions.logMaxSize > 0 { if options.LogMaxSize > 0 {
fileDst, err = newRotatedFile(logFile, open, int64(runtimeOptions.logMaxSize), runtimeOptions.logMaxFiles) fileDst, err = newRotatedFile(logFile, open, int64(options.LogMaxSize), options.LogMaxFiles)
} else { } else {
fileDst, err = open(logFile) fileDst, err = open(logFile)
} }
@ -174,7 +174,7 @@ func monitorMain(runtimeOptions RuntimeOptions) {
if exiterr, ok := err.(*exec.ExitError); ok { if exiterr, ok := err.(*exec.ExitError); ok {
exitCode := exiterr.ExitCode() exitCode := exiterr.ExitCode()
if stopped || runtimeOptions.noRestart { if stopped || options.NoRestart {
os.Exit(exitCode) os.Exit(exitCode)
} }
if exitCode == svcutil.ExitUpgrade.AsInt() { if exitCode == svcutil.ExitUpgrade.AsInt() {
@ -188,7 +188,7 @@ func monitorMain(runtimeOptions RuntimeOptions) {
} }
} }
if runtimeOptions.noRestart { if options.NoRestart {
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
@ -563,7 +563,7 @@ func childEnv() []string {
// panicUploadMaxWait uploading panics... // panicUploadMaxWait uploading panics...
func maybeReportPanics() { func maybeReportPanics() {
// Try to get a config to see if/where panics should be reported. // 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 { if err != nil {
l.Warnln("Couldn't load config; not reporting crash") l.Warnln("Couldn't load config; not reporting crash")
return return

View File

@ -18,10 +18,8 @@ import (
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
) )
func init() { func startPerfStats() {
if innerProcess && os.Getenv("STPERFSTATS") != "" { go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
}
} }
func savePerfStats(file string) { func savePerfStats(file string) {

View File

@ -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() {
}

View File

@ -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)
}
}
}

1
go.mod
View File

@ -3,6 +3,7 @@ module github.com/syncthing/syncthing
require ( require (
github.com/AudriusButkevicius/pfilter v0.0.0-20190627213056-c55ef6137fc6 github.com/AudriusButkevicius/pfilter v0.0.0-20190627213056-c55ef6137fc6
github.com/AudriusButkevicius/recli v0.0.5 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/bkaradzic/go-lz4 v0.0.0-20160924222819-7224d8d8f27e
github.com/calmh/xdr v1.1.0 github.com/calmh/xdr v1.1.0
github.com/ccding/go-stun v0.1.2 github.com/ccding/go-stun v0.1.2

2
go.sum
View File

@ -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/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/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/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-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/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= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=

View File

@ -57,7 +57,7 @@ type Options struct {
AuditWriter io.Writer AuditWriter io.Writer
DeadlockTimeoutS int DeadlockTimeoutS int
NoUpgrade bool NoUpgrade bool
ProfilerURL string ProfilerAddr string
ResetDeltaIdxs bool ResetDeltaIdxs bool
Verbose bool Verbose bool
// null duration means use default value // null duration means use default value
@ -169,11 +169,11 @@ func (a *App) startup() error {
return err return err
} }
if len(a.opts.ProfilerURL) > 0 { if len(a.opts.ProfilerAddr) > 0 {
go func() { go func() {
l.Debugln("Starting profiler on", a.opts.ProfilerURL) l.Debugln("Starting profiler on", a.opts.ProfilerAddr)
runtime.SetBlockProfileRate(1) runtime.SetBlockProfileRate(1)
err := http.ListenAndServe(a.opts.ProfilerURL, nil) err := http.ListenAndServe(a.opts.ProfilerAddr, nil)
if err != nil { if err != nil {
l.Warnln(err) l.Warnln(err)
return return