// 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"
	"crypto/tls"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	_ "net/http/pprof" // Need to import this to support STPROFILER.
	"net/url"
	"os"
	"os/signal"
	"path"
	"path/filepath"
	"runtime"
	"runtime/pprof"
	"sort"
	"strconv"
	"syscall"
	"time"

	"github.com/syncthing/syncthing/lib/build"
	"github.com/syncthing/syncthing/lib/config"
	"github.com/syncthing/syncthing/lib/db"
	"github.com/syncthing/syncthing/lib/dialer"
	"github.com/syncthing/syncthing/lib/events"
	"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"
	"github.com/syncthing/syncthing/lib/tlsutil"
	"github.com/syncthing/syncthing/lib/upgrade"

	"github.com/pkg/errors"
)

const (
	tlsDefaultCommonName   = "syncthing"
	deviceCertLifetimeDays = 20 * 365
	sigTerm                = syscall.Signal(15)
)

const (
	usage      = "syncthing [options]"
	extraUsage = `
The -logflags value is a sum of the following:

   1  Date
   2  Time
   4  Microsecond time
   8  Long filename
  16  Short filename

I.e. to prefix each log line with date and time, set -logflags=3 (1 + 2 from
above). The value 0 is used to disable all of the above. The default is to
show time only (2).

Logging always happens to the command line (stdout) and optionally to the
file at the path specified by -logfile=path. In addition to an path, the special
values "default" and "-" may be used. The former logs to DATADIR/syncthing.log
(see -data-dir), which is the default on Windows, and the latter only to stdout,
no file, which is the default anywhere else.


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.

 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.

 STDEADLOCKTIMEOUT Used for debugging internal deadlocks; sets debug
                   sensitivity. Use only under direction of a developer.

 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.

 GOGC              Percentage of heap growth at which to trigger GC. Default is
                   100. Lower numbers keep peak memory usage down, at the price
                   of CPU usage (i.e. performance).


Debugging Facilities
--------------------

The following are valid values for the STTRACE variable:

%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
}

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
	}

	if os.Getenv("STTRACE") != "" {
		options.logFlags = logger.DebugFlags
	}

	// On non-Windows, we explicitly default to "-" which means stdout. On
	// Windows, the "default" options.logFile will later be replaced with the
	// default path, unless the user has manually specified "-" or
	// something else.
	if runtime.GOOS == "windows" {
		options.logFile = "default"
	} else {
		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)

	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 != ""
	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)
		}
	case dataSet:
		if err = setLocation(locations.ConfigBaseDir, options.confDir); err == nil {
			err = setLocation(locations.DataBaseDir, options.dataDir)
		}
	}
	if err != nil {
		l.Warnln("Command line options:", err)
		os.Exit(syncthing.ExitError.AsInt())
	}

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

	if options.AssetDir == "" {
		// 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)
	}

	if options.showVersion {
		fmt.Println(build.LongVersion)
		return
	}

	if options.showHelp {
		flag.Usage()
		return
	}

	if options.showPaths {
		showPaths(options)
		return
	}

	if options.showDeviceId {
		cert, err := tls.LoadX509KeyPair(
			locations.Get(locations.CertFile),
			locations.Get(locations.KeyFile),
		)
		if err != nil {
			l.Warnln("Error reading device ID:", err)
			os.Exit(syncthing.ExitError.AsInt())
		}

		fmt.Println(protocol.NewDeviceID(cert.Certificate[0]))
		return
	}

	if options.browserOnly {
		if err := openGUI(protocol.EmptyDeviceID); err != nil {
			l.Warnln("Failed to open web UI:", err)
			os.Exit(syncthing.ExitError.AsInt())
		}
		return
	}

	if options.generateDir != "" {
		if err := generate(options.generateDir); err != nil {
			l.Warnln("Failed to generate config and keys:", err)
			os.Exit(syncthing.ExitError.AsInt())
		}
		return
	}

	// Ensure that our home directory exists.
	if err := ensureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0700); err != nil {
		l.Warnln("Failure on home directory:", err)
		os.Exit(syncthing.ExitError.AsInt())
	}

	if options.upgradeTo != "" {
		err := upgrade.ToURL(options.upgradeTo)
		if err != nil {
			l.Warnln("Error while Upgrading:", err)
			os.Exit(syncthing.ExitError.AsInt())
		}
		l.Infoln("Upgraded from", options.upgradeTo)
		return
	}

	if options.doUpgradeCheck {
		if _, err := checkUpgrade(); err != nil {
			l.Warnln("Checking for upgrade:", err)
			os.Exit(exitCodeForUpgrade(err))
		}
		return
	}

	if options.doUpgrade {
		release, err := checkUpgrade()
		if err == nil {
			// Use leveldb database locks to protect against concurrent upgrades
			ldb, err := syncthing.OpenDBBackend(locations.Get(locations.Database), config.TuningAuto)
			if err != nil {
				err = upgradeViaRest()
			} else {
				_ = ldb.Close()
				err = upgrade.To(release)
			}
		}
		if err != nil {
			l.Warnln("Upgrade:", err)
			os.Exit(exitCodeForUpgrade(err))
		}
		l.Infof("Upgraded to %q", release.Tag)
		os.Exit(syncthing.ExitUpgrade.AsInt())
	}

	if options.resetDatabase {
		if err := resetDB(); err != nil {
			l.Warnln("Resetting database:", err)
			os.Exit(syncthing.ExitError.AsInt())
		}
		l.Infoln("Successfully reset database - it will be rebuilt after next start.")
		return
	}

	if innerProcess {
		syncthingMain(options)
	} else {
		monitorMain(options)
	}
}

func openGUI(myID protocol.DeviceID) error {
	cfg, err := loadOrDefaultConfig(myID, events.NoopLogger)
	if err != nil {
		return err
	}
	if cfg.GUI().Enabled {
		if err := openURL(cfg.GUI().URL()); err != nil {
			return err
		}
	} else {
		l.Warnln("Browser: GUI is currently disabled")
	}
	return nil
}

func generate(generateDir string) error {
	dir, err := fs.ExpandTilde(generateDir)
	if err != nil {
		return err
	}

	if err := ensureDir(dir, 0700); err != nil {
		return err
	}

	var myID protocol.DeviceID
	certFile, keyFile := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err == nil {
		l.Warnln("Key exists; will not overwrite.")
	} else {
		cert, err = tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName, deviceCertLifetimeDays)
		if err != nil {
			return errors.Wrap(err, "create certificate")
		}
	}
	myID = protocol.NewDeviceID(cert.Certificate[0])
	l.Infoln("Device ID:", myID)

	cfgFile := filepath.Join(dir, "config.xml")
	if _, err := os.Stat(cfgFile); err == nil {
		l.Warnln("Config exists; will not overwrite.")
		return nil
	}
	cfg, err := syncthing.DefaultConfig(cfgFile, myID, events.NoopLogger, noDefaultFolder)
	if err != nil {
		return err
	}
	err = cfg.Save()
	if err != nil {
		return errors.Wrap(err, "save config")
	}
	return nil
}

func debugFacilities() string {
	facilities := l.Facilities()

	// Get a sorted list of names
	var names []string
	maxLen := 0
	for name := range facilities {
		names = append(names, name)
		if len(name) > maxLen {
			maxLen = len(name)
		}
	}
	sort.Strings(names)

	// Format the choices
	b := new(bytes.Buffer)
	for _, name := range names {
		fmt.Fprintf(b, " %-*s - %s\n", maxLen, name, facilities[name])
	}
	return b.String()
}

type errNoUpgrade struct {
	current, latest string
}

func (e *errNoUpgrade) Error() string {
	return fmt.Sprintf("no upgrade available (current %q >= latest %q).", e.current, e.latest)
}

func checkUpgrade() (upgrade.Release, error) {
	cfg, err := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger)
	if err != nil {
		return upgrade.Release{}, err
	}
	opts := cfg.Options()
	release, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases)
	if err != nil {
		return upgrade.Release{}, err
	}

	if upgrade.CompareVersions(release.Tag, build.Version) <= 0 {
		return upgrade.Release{}, &errNoUpgrade{build.Version, release.Tag}
	}

	l.Infof("Upgrade available (current %q < latest %q)", build.Version, release.Tag)
	return release, nil
}

func upgradeViaRest() error {
	cfg, _ := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger)
	u, err := url.Parse(cfg.GUI().URL())
	if err != nil {
		return err
	}
	u.Path = path.Join(u.Path, "rest/system/upgrade")
	target := u.String()
	r, _ := http.NewRequest("POST", target, nil)
	r.Header.Set("X-API-Key", cfg.GUI().APIKey)

	tr := &http.Transport{
		DialContext:     dialer.DialContext,
		Proxy:           http.ProxyFromEnvironment,
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{
		Transport: tr,
		Timeout:   60 * time.Second,
	}
	resp, err := client.Do(r)
	if err != nil {
		return err
	}
	if resp.StatusCode != 200 {
		bs, err := ioutil.ReadAll(resp.Body)
		defer resp.Body.Close()
		if err != nil {
			return err
		}
		return errors.New(string(bs))
	}

	return err
}

func syncthingMain(runtimeOptions RuntimeOptions) {
	// Set a log prefix similar to the ID we will have later on, or early log
	// lines look ugly.
	l.SetPrefix("[start] ")

	// Print our version information up front, so any crash that happens
	// early etc. will have it available.
	l.Infoln(build.LongVersion)

	// Ensure that we have a certificate and key.
	cert, err := syncthing.LoadOrGenerateCertificate(
		locations.Get(locations.CertFile),
		locations.Get(locations.KeyFile),
	)
	if err != nil {
		l.Warnln("Failed to load/generate certificate:", err)
		os.Exit(1)
	}

	evLogger := events.NewLogger()
	go evLogger.Serve()
	defer evLogger.Stop()

	cfg, err := syncthing.LoadConfigAtStartup(locations.Get(locations.ConfigFile), cert, evLogger, runtimeOptions.allowNewerConfig, noDefaultFolder)
	if err != nil {
		l.Warnln("Failed to initialize config:", err)
		os.Exit(syncthing.ExitError.AsInt())
	}

	// Candidate builds should auto upgrade. Make sure the option is set,
	// unless we are in a build where it's disabled or the STNOUPGRADE
	// environment variable is set.

	if build.IsCandidate && !upgrade.DisabledByCompilation && !runtimeOptions.NoUpgrade {
		l.Infoln("Automatic upgrade is always enabled for candidate releases.")
		if opts := cfg.Options(); opts.AutoUpgradeIntervalH == 0 || opts.AutoUpgradeIntervalH > 24 {
			opts.AutoUpgradeIntervalH = 12
			// Set the option into the config as well, as the auto upgrade
			// loop expects to read a valid interval from there.
			cfg.SetOptions(opts)
			cfg.Save()
		}
		// We don't tweak the user's choice of upgrading to pre-releases or
		// not, as otherwise they cannot step off the candidate channel.
	}

	dbFile := locations.Get(locations.Database)
	ldb, err := syncthing.OpenDBBackend(dbFile, cfg.Options().DatabaseTuning)
	if err != nil {
		l.Warnln("Error opening database:", err)
		os.Exit(1)
	}

	// Check if auto-upgrades is possible, and if yes, and it's enabled do an initial
	// upgrade immedately. The auto-upgrade routine can only be started
	// later after App is initialised.

	autoUpgradePossible := autoUpgradePossible(runtimeOptions)
	if autoUpgradePossible && cfg.Options().AutoUpgradeEnabled() {
		// try to do upgrade directly and log the error if relevant.
		release, err := initialAutoUpgradeCheck(db.NewMiscDataNamespace(ldb))
		if err == nil {
			err = upgrade.To(release)
		}
		if err != nil {
			if _, ok := err.(*errNoUpgrade); ok || err == errTooEarlyUpgradeCheck || err == errTooEarlyUpgrade {
				l.Debugln("Initial automatic upgrade:", err)
			} else {
				l.Infoln("Initial automatic upgrade:", err)
			}
		} else {
			l.Infof("Upgraded to %q, exiting now.", release.Tag)
			os.Exit(syncthing.ExitUpgrade.AsInt())
		}
	}

	if runtimeOptions.unpaused {
		setPauseState(cfg, false)
	} else if runtimeOptions.paused {
		setPauseState(cfg, true)
	}

	appOpts := runtimeOptions.Options
	if runtimeOptions.auditEnabled {
		appOpts.AuditWriter = auditWriter(runtimeOptions.auditFile)
	}
	if t := os.Getenv("STDEADLOCKTIMEOUT"); t != "" {
		secs, _ := strconv.Atoi(t)
		appOpts.DeadlockTimeoutS = secs
	}
	if dur, err := time.ParseDuration(os.Getenv("STRECHECKDBEVERY")); err == nil {
		appOpts.DBRecheckInterval = dur
	}
	if dur, err := time.ParseDuration(os.Getenv("STGCINDIRECTEVERY")); err == nil {
		appOpts.DBIndirectGCInterval = dur
	}

	app := syncthing.New(cfg, ldb, evLogger, cert, appOpts)

	if autoUpgradePossible {
		go autoUpgrade(cfg, app, evLogger)
	}

	setupSignalHandling(app)

	if len(os.Getenv("GOMAXPROCS")) == 0 {
		runtime.GOMAXPROCS(runtime.NumCPU())
	}

	if runtimeOptions.cpuProfile {
		f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
		if err != nil {
			l.Warnln("Creating profile:", err)
			os.Exit(syncthing.ExitError.AsInt())
		}
		if err := pprof.StartCPUProfile(f); err != nil {
			l.Warnln("Starting profile:", err)
			os.Exit(syncthing.ExitError.AsInt())
		}
	}

	go standbyMonitor(app, cfg)

	if err := app.Start(); err != nil {
		os.Exit(syncthing.ExitError.AsInt())
	}

	cleanConfigDirectory()

	if cfg.Options().StartBrowser && !runtimeOptions.noBrowser && !runtimeOptions.stRestarting {
		// 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(cfg.GUI().URL()) }()
	}

	status := app.Wait()

	if runtimeOptions.cpuProfile {
		pprof.StopCPUProfile()
	}

	os.Exit(int(status))
}

func setupSignalHandling(app *syncthing.App) {
	// Exit cleanly with "restarting" code on SIGHUP.

	restartSign := make(chan os.Signal, 1)
	sigHup := syscall.Signal(1)
	signal.Notify(restartSign, sigHup)
	go func() {
		<-restartSign
		app.Stop(syncthing.ExitRestart)
	}()

	// Exit with "success" code (no restart) on INT/TERM

	stopSign := make(chan os.Signal, 1)
	signal.Notify(stopSign, os.Interrupt, sigTerm)
	go func() {
		<-stopSign
		app.Stop(syncthing.ExitSuccess)
	}()
}

func loadOrDefaultConfig(myID protocol.DeviceID, evLogger events.Logger) (config.Wrapper, error) {
	cfgFile := locations.Get(locations.ConfigFile)
	cfg, _, err := config.Load(cfgFile, myID, evLogger)

	if err != nil {
		cfg, err = syncthing.DefaultConfig(cfgFile, myID, evLogger, noDefaultFolder)
	}

	return cfg, err
}

func auditWriter(auditFile string) io.Writer {
	var fd io.Writer
	var err error
	var auditDest string
	var auditFlags int

	if auditFile == "-" {
		fd = os.Stdout
		auditDest = "stdout"
	} else if auditFile == "--" {
		fd = os.Stderr
		auditDest = "stderr"
	} else {
		if auditFile == "" {
			auditFile = locations.GetTimestamped(locations.AuditLog)
			auditFlags = os.O_WRONLY | os.O_CREATE | os.O_EXCL
		} else {
			auditFlags = os.O_WRONLY | os.O_CREATE | os.O_APPEND
		}
		fd, err = os.OpenFile(auditFile, auditFlags, 0600)
		if err != nil {
			l.Warnln("Audit:", err)
			os.Exit(syncthing.ExitError.AsInt())
		}
		auditDest = auditFile
	}

	l.Infoln("Audit log in", auditDest)

	return fd
}

func resetDB() error {
	return os.RemoveAll(locations.Get(locations.Database))
}

func ensureDir(dir string, mode fs.FileMode) error {
	fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
	err := fs.MkdirAll(".", mode)
	if err != nil {
		return err
	}

	if fi, err := fs.Stat("."); err == nil {
		// Apprently the stat may fail even though the mkdirall passed. If it
		// does, we'll just assume things are in order and let other things
		// fail (like loading or creating the config...).
		currentMode := fi.Mode() & 0777
		if currentMode != mode {
			err := fs.Chmod(".", mode)
			// This can fail on crappy filesystems, nothing we can do about it.
			if err != nil {
				l.Warnln(err)
			}
		}
	}
	return nil
}

func standbyMonitor(app *syncthing.App, cfg config.Wrapper) {
	restartDelay := 60 * time.Second
	now := time.Now()
	for {
		time.Sleep(10 * time.Second)
		if time.Since(now) > 2*time.Minute && cfg.Options().RestartOnWakeup {
			l.Infof("Paused state detected, possibly woke up from standby. Restarting in %v.", restartDelay)

			// We most likely just woke from standby. If we restart
			// immediately chances are we won't have networking ready. Give
			// things a moment to stabilize.
			time.Sleep(restartDelay)

			app.Stop(syncthing.ExitRestart)
			return
		}
		now = time.Now()
	}
}

func autoUpgradePossible(runtimeOptions RuntimeOptions) bool {
	if upgrade.DisabledByCompilation {
		return false
	}
	if runtimeOptions.NoUpgrade {
		l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
		return false
	}
	return true
}

func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger) {
	timer := time.NewTimer(upgradeCheckInterval)
	sub := evLogger.Subscribe(events.DeviceConnected)
	for {
		select {
		case event := <-sub.C():
			data, ok := event.Data.(map[string]string)
			if !ok || data["clientName"] != "syncthing" || upgrade.CompareVersions(data["clientVersion"], build.Version) != upgrade.Newer {
				continue
			}
			if cfg.Options().AutoUpgradeEnabled() {
				l.Infof("Connected to device %s with a newer version (current %q < remote %q). Checking for upgrades.", data["id"], build.Version, data["clientVersion"])
			}
		case <-timer.C:
		}

		opts := cfg.Options()
		if !opts.AutoUpgradeEnabled() {
			timer.Reset(upgradeCheckInterval)
			continue
		}

		checkInterval := time.Duration(opts.AutoUpgradeIntervalH) * time.Hour
		rel, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases)
		if err == upgrade.ErrUpgradeUnsupported {
			sub.Unsubscribe()
			return
		}
		if err != nil {
			// Don't complain too loudly here; we might simply not have
			// internet connectivity, or the upgrade server might be down.
			l.Infoln("Automatic upgrade:", err)
			timer.Reset(checkInterval)
			continue
		}

		if upgrade.CompareVersions(rel.Tag, build.Version) != upgrade.Newer {
			// Skip equal, older or majorly newer (incompatible) versions
			timer.Reset(checkInterval)
			continue
		}

		l.Infof("Automatic upgrade (current %q < latest %q)", build.Version, rel.Tag)
		err = upgrade.To(rel)
		if err != nil {
			l.Warnln("Automatic upgrade:", err)
			timer.Reset(checkInterval)
			continue
		}
		sub.Unsubscribe()
		l.Warnf("Automatically upgraded to version %q. Restarting in 1 minute.", rel.Tag)
		time.Sleep(time.Minute)
		app.Stop(syncthing.ExitUpgrade)
		return
	}
}

func initialAutoUpgradeCheck(misc *db.NamespacedKV) (upgrade.Release, error) {
	if last, ok, err := misc.Time(upgradeCheckKey); err == nil && ok && time.Since(last) < upgradeCheckInterval {
		return upgrade.Release{}, errTooEarlyUpgradeCheck
	}
	_ = misc.PutTime(upgradeCheckKey, time.Now())
	release, err := checkUpgrade()
	if err != nil {
		return upgrade.Release{}, err
	}
	if lastVersion, ok, err := misc.String(upgradeVersionKey); err == nil && ok && lastVersion == release.Tag {
		// Only check time if we try to upgrade to the same release.
		if lastTime, ok, err := misc.Time(upgradeTimeKey); err == nil && ok && time.Since(lastTime) < upgradeRetryInterval {
			return upgrade.Release{}, errTooEarlyUpgrade
		}
	}
	_ = misc.PutString(upgradeVersionKey, release.Tag)
	_ = misc.PutTime(upgradeTimeKey, time.Now())
	return release, nil
}

// cleanConfigDirectory removes old, unused configuration and index formats, a
// suitable time after they have gone out of fashion.
func cleanConfigDirectory() {
	patterns := map[string]time.Duration{
		"panic-*.log":        7 * 24 * time.Hour,  // keep panic logs for a week
		"audit-*.log":        7 * 24 * time.Hour,  // keep audit logs for a week
		"index":              14 * 24 * time.Hour, // keep old index format for two weeks
		"index-v0.11.0.db":   14 * 24 * time.Hour, // keep old index format for two weeks
		"index-v0.13.0.db":   14 * 24 * time.Hour, // keep old index format for two weeks
		"index*.converted":   14 * 24 * time.Hour, // keep old converted indexes for two weeks
		"config.xml.v*":      30 * 24 * time.Hour, // old config versions for a month
		"*.idx.gz":           30 * 24 * time.Hour, // these should for sure no longer exist
		"backup-of-v0.8":     30 * 24 * time.Hour, // these neither
		"tmp-index-sorter.*": time.Minute,         // these should never exist on startup
		"support-bundle-*":   30 * 24 * time.Hour, // keep old support bundle zip or folder for a month
	}

	for pat, dur := range patterns {
		fs := fs.NewFilesystem(fs.FilesystemTypeBasic, locations.GetBaseDir(locations.ConfigBaseDir))
		files, err := fs.Glob(pat)
		if err != nil {
			l.Infoln("Cleaning:", err)
			continue
		}

		for _, file := range files {
			info, err := fs.Lstat(file)
			if err != nil {
				l.Infoln("Cleaning:", err)
				continue
			}

			if time.Since(info.ModTime()) > dur {
				if err = fs.RemoveAll(file); err != nil {
					l.Infoln("Cleaning:", err)
				} else {
					l.Infoln("Cleaned away old file", filepath.Base(file))
				}
			}
		}
	}
}

func showPaths(options RuntimeOptions) {
	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("Default sync folder directory:\n\t%s\n\n", locations.Get(locations.DefFolder))
}

func setPauseState(cfg config.Wrapper, paused bool) {
	raw := cfg.RawCopy()
	for i := range raw.Devices {
		raw.Devices[i].Paused = paused
	}
	for i := range raw.Folders {
		raw.Folders[i].Paused = paused
	}
	if _, err := cfg.Replace(raw); err != nil {
		l.Warnln("Cannot adjust paused state:", err)
		os.Exit(syncthing.ExitError.AsInt())
	}
}

func exitCodeForUpgrade(err error) int {
	if _, ok := err.(*errNoUpgrade); ok {
		return syncthing.ExitNoUpgradeAvailable.AsInt()
	}
	return syncthing.ExitError.AsInt()
}
