From a20a5f61f0556f60c5598544a829189138eddec0 Mon Sep 17 00:00:00 2001 From: Simon Frei Date: Tue, 22 Dec 2020 20:17:14 +0100 Subject: [PATCH] lib/ur: Send unreported failures on shutdown (#7164) --- cmd/syncthing/main.go | 48 ++++----- cmd/syncthing/monitor.go | 10 +- lib/api/api.go | 24 ++--- lib/api/api_test.go | 4 +- lib/beacon/beacon.go | 14 +-- lib/connections/quic_listen.go | 6 +- lib/connections/relay_listen.go | 6 +- lib/connections/service.go | 9 +- lib/connections/tcp_listen.go | 6 +- lib/db/lowlevel.go | 5 +- lib/discover/local.go | 8 +- lib/discover/manager.go | 5 +- lib/model/folder_summary.go | 8 +- lib/model/indexsender.go | 4 +- lib/model/model.go | 10 +- lib/relay/client/client.go | 6 +- lib/svcutil/svcutil.go | 177 ++++++++++++++++++++++++++++++++ lib/syncthing/syncthing.go | 18 ++-- lib/syncthing/syncthing_test.go | 8 +- lib/ur/failurereporting.go | 23 ++++- lib/util/utils.go | 103 ------------------- 21 files changed, 296 insertions(+), 206 deletions(-) create mode 100644 lib/svcutil/svcutil.go diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index ccac50472..385b29d29 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -39,10 +39,10 @@ import ( "github.com/syncthing/syncthing/lib/logger" "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/syncthing" "github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/upgrade" - "github.com/syncthing/syncthing/lib/util" "github.com/pkg/errors" ) @@ -323,7 +323,7 @@ func main() { } if err != nil { l.Warnln("Command line options:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } if options.logFile == "default" || options.logFile == "" { @@ -360,7 +360,7 @@ func main() { ) if err != nil { l.Warnln("Error reading device ID:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } fmt.Println(protocol.NewDeviceID(cert.Certificate[0])) @@ -370,7 +370,7 @@ func main() { if options.browserOnly { if err := openGUI(protocol.EmptyDeviceID); err != nil { l.Warnln("Failed to open web UI:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } return } @@ -378,7 +378,7 @@ func main() { if options.generateDir != "" { if err := generate(options.generateDir); err != nil { l.Warnln("Failed to generate config and keys:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } return } @@ -386,14 +386,14 @@ func main() { // 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(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } if options.upgradeTo != "" { err := upgrade.ToURL(options.upgradeTo) if err != nil { l.Warnln("Error while Upgrading:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } l.Infoln("Upgraded from", options.upgradeTo) return @@ -424,13 +424,13 @@ func main() { os.Exit(exitCodeForUpgrade(err)) } l.Infof("Upgraded to %q", release.Tag) - os.Exit(util.ExitUpgrade.AsInt()) + os.Exit(svcutil.ExitUpgrade.AsInt()) } if options.resetDatabase { if err := resetDB(); err != nil { l.Warnln("Resetting database:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } l.Infoln("Successfully reset database - it will be rebuilt after next start.") return @@ -610,7 +610,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { 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(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } // Candidate builds should auto upgrade. Make sure the option is set, @@ -656,7 +656,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { } } else { l.Infof("Upgraded to %q, exiting now.", release.Tag) - os.Exit(util.ExitUpgrade.AsInt()) + os.Exit(svcutil.ExitUpgrade.AsInt()) } } @@ -684,7 +684,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { app, err := syncthing.New(cfg, ldb, evLogger, cert, appOpts) if err != nil { l.Warnln("Failed to start Syncthing:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } if autoUpgradePossible { @@ -701,18 +701,18 @@ func syncthingMain(runtimeOptions RuntimeOptions) { f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid())) if err != nil { l.Warnln("Creating profile:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } if err := pprof.StartCPUProfile(f); err != nil { l.Warnln("Starting profile:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } } go standbyMonitor(app, cfg) if err := app.Start(); err != nil { - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } cleanConfigDirectory() @@ -725,7 +725,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { status := app.Wait() - if status == util.ExitError { + if status == svcutil.ExitError { l.Warnln("Syncthing stopped with error:", app.Error()) } @@ -744,7 +744,7 @@ func setupSignalHandling(app *syncthing.App) { signal.Notify(restartSign, sigHup) go func() { <-restartSign - app.Stop(util.ExitRestart) + app.Stop(svcutil.ExitRestart) }() // Exit with "success" code (no restart) on INT/TERM @@ -753,7 +753,7 @@ func setupSignalHandling(app *syncthing.App) { signal.Notify(stopSign, os.Interrupt, sigTerm) go func() { <-stopSign - app.Stop(util.ExitSuccess) + app.Stop(svcutil.ExitSuccess) }() } @@ -790,7 +790,7 @@ func auditWriter(auditFile string) io.Writer { fd, err = os.OpenFile(auditFile, auditFlags, 0600) if err != nil { l.Warnln("Audit:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } auditDest = auditFile } @@ -840,7 +840,7 @@ func standbyMonitor(app *syncthing.App, cfg config.Wrapper) { // things a moment to stabilize. time.Sleep(restartDelay) - app.Stop(util.ExitRestart) + app.Stop(svcutil.ExitRestart) return } now = time.Now() @@ -910,7 +910,7 @@ func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger) sub.Unsubscribe() l.Warnf("Automatically upgraded to version %q. Restarting in 1 minute.", rel.Tag) time.Sleep(time.Minute) - app.Stop(util.ExitUpgrade) + app.Stop(svcutil.ExitUpgrade) return } } @@ -998,13 +998,13 @@ func setPauseState(cfg config.Wrapper, paused bool) { } if _, err := cfg.Replace(raw); err != nil { l.Warnln("Cannot adjust paused state:", err) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } } func exitCodeForUpgrade(err error) int { if _, ok := err.(*errNoUpgrade); ok { - return util.ExitNoUpgradeAvailable.AsInt() + return svcutil.ExitNoUpgradeAvailable.AsInt() } - return util.ExitError.AsInt() + return svcutil.ExitError.AsInt() } diff --git a/cmd/syncthing/monitor.go b/cmd/syncthing/monitor.go index 93e8f6f07..66113bed6 100644 --- a/cmd/syncthing/monitor.go +++ b/cmd/syncthing/monitor.go @@ -25,8 +25,8 @@ import ( "github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" - "github.com/syncthing/syncthing/lib/util" ) var ( @@ -99,7 +99,7 @@ func monitorMain(runtimeOptions RuntimeOptions) { if t := time.Since(restarts[0]); t < loopThreshold { l.Warnf("%d restarts in %v; not retrying further", countRestarts, t) - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } copy(restarts[0:], restarts[1:]) @@ -169,7 +169,7 @@ func monitorMain(runtimeOptions RuntimeOptions) { if err == nil { // Successful exit indicates an intentional shutdown - os.Exit(util.ExitSuccess.AsInt()) + os.Exit(svcutil.ExitSuccess.AsInt()) } if exiterr, ok := err.(*exec.ExitError); ok { @@ -177,7 +177,7 @@ func monitorMain(runtimeOptions RuntimeOptions) { if stopped || runtimeOptions.noRestart { os.Exit(exitCode) } - if exitCode == util.ExitUpgrade.AsInt() { + if exitCode == svcutil.ExitUpgrade.AsInt() { // Restart the monitor process to release the .old // binary as part of the upgrade process. l.Infoln("Restarting monitor...") @@ -189,7 +189,7 @@ func monitorMain(runtimeOptions RuntimeOptions) { } if runtimeOptions.noRestart { - os.Exit(util.ExitError.AsInt()) + os.Exit(svcutil.ExitError.AsInt()) } l.Infoln("Syncthing exited:", err) diff --git a/lib/api/api.go b/lib/api/api.go index cafefc33b..ec8100d08 100644 --- a/lib/api/api.go +++ b/lib/api/api.go @@ -49,11 +49,11 @@ import ( "github.com/syncthing/syncthing/lib/model" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/rand" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/ur" - "github.com/syncthing/syncthing/lib/util" ) // matches a bcrypt hash and not too much else @@ -89,7 +89,7 @@ type service struct { startedOnce chan struct{} // the service has started successfully at least once startupErr error listenerAddr net.Addr - exitChan chan *util.FatalErr + exitChan chan *svcutil.FatalErr guiErrors logger.Recorder systemLog logger.Recorder @@ -123,7 +123,7 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam tlsDefaultCommonName: tlsDefaultCommonName, configChanged: make(chan struct{}), startedOnce: make(chan struct{}), - exitChan: make(chan *util.FatalErr, 1), + exitChan: make(chan *svcutil.FatalErr, 1), } } @@ -474,7 +474,7 @@ func (s *service) CommitConfiguration(from, to config.Configuration) bool { return true } -func (s *service) fatal(err *util.FatalErr) { +func (s *service) fatal(err *svcutil.FatalErr) { // s.exitChan is 1-buffered and whoever is first gets handled. select { case s.exitChan <- err: @@ -915,9 +915,9 @@ func (s *service) getDebugFile(w http.ResponseWriter, r *http.Request) { func (s *service) postSystemRestart(w http.ResponseWriter, r *http.Request) { s.flushResponse(`{"ok": "restarting"}`, w) - s.fatal(&util.FatalErr{ + s.fatal(&svcutil.FatalErr{ Err: errors.New("restart initiated by rest API"), - Status: util.ExitRestart, + Status: svcutil.ExitRestart, }) } @@ -944,17 +944,17 @@ func (s *service) postSystemReset(w http.ResponseWriter, r *http.Request) { s.flushResponse(`{"ok": "resetting folder `+folder+`"}`, w) } - s.fatal(&util.FatalErr{ + s.fatal(&svcutil.FatalErr{ Err: errors.New("restart after db reset initiated by rest API"), - Status: util.ExitRestart, + Status: svcutil.ExitRestart, }) } func (s *service) postSystemShutdown(w http.ResponseWriter, r *http.Request) { s.flushResponse(`{"ok": "shutting down"}`, w) - s.fatal(&util.FatalErr{ + s.fatal(&svcutil.FatalErr{ Err: errors.New("shutdown initiated by rest API"), - Status: util.ExitSuccess, + Status: svcutil.ExitSuccess, }) } @@ -1390,9 +1390,9 @@ func (s *service) postSystemUpgrade(w http.ResponseWriter, r *http.Request) { } s.flushResponse(`{"ok": "restarting"}`, w) - s.fatal(&util.FatalErr{ + s.fatal(&svcutil.FatalErr{ Err: errors.New("exit after upgrade initiated by rest API"), - Status: util.ExitUpgrade, + Status: svcutil.ExitUpgrade, }) } } diff --git a/lib/api/api_test.go b/lib/api/api_test.go index 323f3443d..fcc4c00c4 100644 --- a/lib/api/api_test.go +++ b/lib/api/api_test.go @@ -32,10 +32,10 @@ import ( "github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/ur" - "github.com/syncthing/syncthing/lib/util" "github.com/thejerf/suture/v4" ) @@ -119,7 +119,7 @@ func TestStopAfterBrokenConfig(t *testing.T) { defer os.Remove(token) srv.started = make(chan string) - sup := suture.New("test", util.SpecWithDebugLogger(l)) + sup := suture.New("test", svcutil.SpecWithDebugLogger(l)) sup.Add(srv) ctx, cancel := context.WithCancel(context.Background()) sup.ServeBackground(ctx) diff --git a/lib/beacon/beacon.go b/lib/beacon/beacon.go index e937d5d1a..f5182709d 100644 --- a/lib/beacon/beacon.go +++ b/lib/beacon/beacon.go @@ -14,7 +14,7 @@ import ( "github.com/thejerf/suture/v4" - "github.com/syncthing/syncthing/lib/util" + "github.com/syncthing/syncthing/lib/svcutil" ) type recv struct { @@ -33,8 +33,8 @@ type Interface interface { type cast struct { *suture.Supervisor name string - reader util.ServiceWithError - writer util.ServiceWithError + reader svcutil.ServiceWithError + writer svcutil.ServiceWithError outbox chan recv inbox chan []byte stopped chan struct{} @@ -45,7 +45,7 @@ type cast struct { // methods to get a functional implementation of Interface. func newCast(name string) *cast { // Only log restarts in debug mode. - spec := util.SpecWithDebugLogger(l) + spec := svcutil.SpecWithDebugLogger(l) // Don't retry too frenetically: an error to open a socket or // whatever is usually something that is either permanent or takes // a while to get solved... @@ -58,7 +58,7 @@ func newCast(name string) *cast { outbox: make(chan recv, 16), stopped: make(chan struct{}), } - util.OnSupervisorDone(c.Supervisor, func() { close(c.stopped) }) + svcutil.OnSupervisorDone(c.Supervisor, func() { close(c.stopped) }) return c } @@ -72,8 +72,8 @@ func (c *cast) addWriter(svc func(ctx context.Context) error) { c.Add(c.writer) } -func (c *cast) createService(svc func(context.Context) error, suffix string) util.ServiceWithError { - return util.AsService(svc, fmt.Sprintf("%s/%s", c, suffix)) +func (c *cast) createService(svc func(context.Context) error, suffix string) svcutil.ServiceWithError { + return svcutil.AsService(svc, fmt.Sprintf("%s/%s", c, suffix)) } func (c *cast) String() string { diff --git a/lib/connections/quic_listen.go b/lib/connections/quic_listen.go index 8a8db5e98..6673bdfe2 100644 --- a/lib/connections/quic_listen.go +++ b/lib/connections/quic_listen.go @@ -24,7 +24,7 @@ import ( "github.com/syncthing/syncthing/lib/connections/registry" "github.com/syncthing/syncthing/lib/nat" "github.com/syncthing/syncthing/lib/stun" - "github.com/syncthing/syncthing/lib/util" + "github.com/syncthing/syncthing/lib/svcutil" ) func init() { @@ -35,7 +35,7 @@ func init() { } type quicListener struct { - util.ServiceWithError + svcutil.ServiceWithError nat atomic.Value onAddressesChangedNotifier @@ -205,7 +205,7 @@ func (f *quicListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls. conns: conns, factory: f, } - l.ServiceWithError = util.AsService(l.serve, l.String()) + l.ServiceWithError = svcutil.AsService(l.serve, l.String()) l.nat.Store(stun.NATUnknown) return l } diff --git a/lib/connections/relay_listen.go b/lib/connections/relay_listen.go index 09e2a852a..a388aedcf 100644 --- a/lib/connections/relay_listen.go +++ b/lib/connections/relay_listen.go @@ -19,7 +19,7 @@ import ( "github.com/syncthing/syncthing/lib/dialer" "github.com/syncthing/syncthing/lib/nat" "github.com/syncthing/syncthing/lib/relay/client" - "github.com/syncthing/syncthing/lib/util" + "github.com/syncthing/syncthing/lib/svcutil" ) func init() { @@ -30,7 +30,7 @@ func init() { } type relayListener struct { - util.ServiceWithError + svcutil.ServiceWithError onAddressesChangedNotifier uri *url.URL @@ -184,7 +184,7 @@ func (f *relayListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls conns: conns, factory: f, } - t.ServiceWithError = util.AsService(t.serve, t.String()) + t.ServiceWithError = svcutil.AsService(t.serve, t.String()) return t } diff --git a/lib/connections/service.go b/lib/connections/service.go index 9269d36f9..a1bec950b 100644 --- a/lib/connections/service.go +++ b/lib/connections/service.go @@ -24,6 +24,7 @@ import ( "github.com/syncthing/syncthing/lib/nat" "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/util" @@ -143,7 +144,7 @@ type service struct { } func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, bepProtocolName string, tlsDefaultCommonName string, evLogger events.Logger) Service { - spec := util.SpecWithInfoLogger(l) + spec := svcutil.SpecWithInfoLogger(l) service := &service{ Supervisor: suture.New("connections.Service", spec), connectionStatusHandler: newConnectionStatusHandler(), @@ -191,12 +192,12 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t // the common handling regardless of whether the connection was // incoming or outgoing. - service.Add(util.AsService(service.connect, fmt.Sprintf("%s/connect", service))) - service.Add(util.AsService(service.handle, fmt.Sprintf("%s/handle", service))) + service.Add(svcutil.AsService(service.connect, fmt.Sprintf("%s/connect", service))) + service.Add(svcutil.AsService(service.handle, fmt.Sprintf("%s/handle", service))) service.Add(service.listenerSupervisor) service.Add(service.natService) - util.OnSupervisorDone(service.Supervisor, func() { + svcutil.OnSupervisorDone(service.Supervisor, func() { service.cfg.Unsubscribe(service.limiter) service.cfg.Unsubscribe(service) }) diff --git a/lib/connections/tcp_listen.go b/lib/connections/tcp_listen.go index 912708563..24bf9c53c 100644 --- a/lib/connections/tcp_listen.go +++ b/lib/connections/tcp_listen.go @@ -18,7 +18,7 @@ import ( "github.com/syncthing/syncthing/lib/connections/registry" "github.com/syncthing/syncthing/lib/dialer" "github.com/syncthing/syncthing/lib/nat" - "github.com/syncthing/syncthing/lib/util" + "github.com/syncthing/syncthing/lib/svcutil" ) func init() { @@ -29,7 +29,7 @@ func init() { } type tcpListener struct { - util.ServiceWithError + svcutil.ServiceWithError onAddressesChangedNotifier uri *url.URL @@ -207,7 +207,7 @@ func (f *tcpListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.C natService: natService, factory: f, } - l.ServiceWithError = util.AsService(l.serve, l.String()) + l.ServiceWithError = svcutil.AsService(l.serve, l.String()) return l } diff --git a/lib/db/lowlevel.go b/lib/db/lowlevel.go index 1bec3aeb7..6dda13e82 100644 --- a/lib/db/lowlevel.go +++ b/lib/db/lowlevel.go @@ -25,6 +25,7 @@ import ( "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/rand" "github.com/syncthing/syncthing/lib/sha256" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/util" "github.com/thejerf/suture/v4" @@ -73,7 +74,7 @@ type Lowlevel struct { func NewLowlevel(backend backend.Backend, evLogger events.Logger, opts ...Option) (*Lowlevel, error) { // Only log restarts in debug mode. - spec := util.SpecWithDebugLogger(l) + spec := svcutil.SpecWithDebugLogger(l) db := &Lowlevel{ Supervisor: suture.New("db.Lowlevel", spec), Backend: backend, @@ -89,7 +90,7 @@ func NewLowlevel(backend backend.Backend, evLogger events.Logger, opts ...Option opt(db) } db.keyer = newDefaultKeyer(db.folderIdx, db.deviceIdx) - db.Add(util.AsService(db.gcRunner, "db.Lowlevel/gcRunner")) + db.Add(svcutil.AsService(db.gcRunner, "db.Lowlevel/gcRunner")) if path := db.needsRepairPath(); path != "" { if _, err := os.Lstat(path); err == nil { l.Infoln("Database was marked for repair - this may take a while") diff --git a/lib/discover/local.go b/lib/discover/local.go index 9dfe17559..a15e073aa 100644 --- a/lib/discover/local.go +++ b/lib/discover/local.go @@ -24,7 +24,7 @@ import ( "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/rand" - "github.com/syncthing/syncthing/lib/util" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/thejerf/suture/v4" ) @@ -52,7 +52,7 @@ const ( func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, evLogger events.Logger) (FinderService, error) { c := &localClient{ - Supervisor: suture.New("local", util.SpecWithDebugLogger(l)), + Supervisor: suture.New("local", svcutil.SpecWithDebugLogger(l)), myID: id, addrList: addrList, evLogger: evLogger, @@ -81,9 +81,9 @@ func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, evLogge c.beacon = beacon.NewMulticast(addr) } c.Add(c.beacon) - c.Add(util.AsService(c.recvAnnouncements, fmt.Sprintf("%s/recv", c))) + c.Add(svcutil.AsService(c.recvAnnouncements, fmt.Sprintf("%s/recv", c))) - c.Add(util.AsService(c.sendLocalAnnouncements, fmt.Sprintf("%s/sendLocal", c))) + c.Add(svcutil.AsService(c.sendLocalAnnouncements, fmt.Sprintf("%s/sendLocal", c))) return c, nil } diff --git a/lib/discover/manager.go b/lib/discover/manager.go index d3d0cf626..f9f1cd064 100644 --- a/lib/discover/manager.go +++ b/lib/discover/manager.go @@ -18,6 +18,7 @@ import ( "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/util" ) @@ -47,7 +48,7 @@ type manager struct { func NewManager(myID protocol.DeviceID, cfg config.Wrapper, cert tls.Certificate, evLogger events.Logger, lister AddressLister) Manager { m := &manager{ - Supervisor: suture.New("discover.Manager", util.SpecWithDebugLogger(l)), + Supervisor: suture.New("discover.Manager", svcutil.SpecWithDebugLogger(l)), myID: myID, cfg: cfg, cert: cert, @@ -57,7 +58,7 @@ func NewManager(myID protocol.DeviceID, cfg config.Wrapper, cert tls.Certificate finders: make(map[string]cachedFinder), mut: sync.NewRWMutex(), } - m.Add(util.AsService(m.serve, m.String())) + m.Add(svcutil.AsService(m.serve, m.String())) return m } diff --git a/lib/model/folder_summary.go b/lib/model/folder_summary.go index 2b1234513..d5253758a 100644 --- a/lib/model/folder_summary.go +++ b/lib/model/folder_summary.go @@ -18,8 +18,8 @@ import ( "github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" - "github.com/syncthing/syncthing/lib/util" ) const maxDurationSinceLastEventReq = time.Minute @@ -52,7 +52,7 @@ type folderSummaryService struct { func NewFolderSummaryService(cfg config.Wrapper, m Model, id protocol.DeviceID, evLogger events.Logger) FolderSummaryService { service := &folderSummaryService{ - Supervisor: suture.New("folderSummaryService", util.SpecWithDebugLogger(l)), + Supervisor: suture.New("folderSummaryService", svcutil.SpecWithDebugLogger(l)), cfg: cfg, model: m, id: id, @@ -63,8 +63,8 @@ func NewFolderSummaryService(cfg config.Wrapper, m Model, id protocol.DeviceID, lastEventReqMut: sync.NewMutex(), } - service.Add(util.AsService(service.listenForUpdates, fmt.Sprintf("%s/listenForUpdates", service))) - service.Add(util.AsService(service.calculateSummaries, fmt.Sprintf("%s/calculateSummaries", service))) + service.Add(svcutil.AsService(service.listenForUpdates, fmt.Sprintf("%s/listenForUpdates", service))) + service.Add(svcutil.AsService(service.calculateSummaries, fmt.Sprintf("%s/calculateSummaries", service))) return service } diff --git a/lib/model/indexsender.go b/lib/model/indexsender.go index 7bca77447..948019856 100644 --- a/lib/model/indexsender.go +++ b/lib/model/indexsender.go @@ -18,7 +18,7 @@ import ( "github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/protocol" - "github.com/syncthing/syncthing/lib/util" + "github.com/syncthing/syncthing/lib/svcutil" ) type indexSender struct { @@ -38,7 +38,7 @@ type indexSender struct { func (s *indexSender) Serve(ctx context.Context) (err error) { l.Debugf("Starting indexSender for %s to %s at %s (slv=%d)", s.folder, s.conn.ID(), s.conn, s.prevSequence) defer func() { - err = util.NoRestartErr(err) + err = svcutil.NoRestartErr(err) l.Debugf("Exiting indexSender for %s to %s at %s: %v", s.folder, s.conn.ID(), s.conn, err) }() diff --git a/lib/model/model.go b/lib/model/model.go index 72f8c849e..5f725ec86 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -33,9 +33,9 @@ import ( "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/scanner" "github.com/syncthing/syncthing/lib/stats" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/ur/contract" - "github.com/syncthing/syncthing/lib/util" "github.com/syncthing/syncthing/lib/versioner" ) @@ -199,7 +199,7 @@ var ( // where it sends index information to connected peers and responds to requests // for file data without altering the local folder in any way. func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string, evLogger events.Logger) Model { - spec := util.SpecWithDebugLogger(l) + spec := svcutil.SpecWithDebugLogger(l) m := &model{ Supervisor: suture.New("model", spec), @@ -249,7 +249,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(m.db, devID.String()) } m.Add(m.progressEmitter) - m.Add(util.AsService(m.serve, m.String())) + m.Add(svcutil.AsService(m.serve, m.String())) return m } @@ -262,7 +262,7 @@ func (m *model) serve(ctx context.Context) error { if err := m.initFolders(); err != nil { close(m.started) - return util.AsFatalErr(err, util.ExitError) + return svcutil.AsFatalErr(err, svcutil.ExitError) } close(m.started) @@ -271,7 +271,7 @@ func (m *model) serve(ctx context.Context) error { case <-ctx.Done(): return ctx.Err() case err := <-m.fatalChan: - return util.AsFatalErr(err, util.ExitError) + return svcutil.AsFatalErr(err, svcutil.ExitError) } } diff --git a/lib/relay/client/client.go b/lib/relay/client/client.go index 2da735062..50d38a962 100644 --- a/lib/relay/client/client.go +++ b/lib/relay/client/client.go @@ -10,8 +10,8 @@ import ( "time" "github.com/syncthing/syncthing/lib/relay/protocol" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/sync" - "github.com/syncthing/syncthing/lib/util" "github.com/thejerf/suture/v4" ) @@ -45,7 +45,7 @@ func NewClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol. } type commonClient struct { - util.ServiceWithError + svcutil.ServiceWithError invitations chan protocol.SessionInvitation closeInvitationsOnFinish bool @@ -61,7 +61,7 @@ func newCommonClient(invitations chan protocol.SessionInvitation, serve func(con defer c.cleanup() return serve(ctx) } - c.ServiceWithError = util.AsService(newServe, creator) + c.ServiceWithError = svcutil.AsService(newServe, creator) if c.invitations == nil { c.closeInvitationsOnFinish = true c.invitations = make(chan protocol.SessionInvitation) diff --git a/lib/svcutil/svcutil.go b/lib/svcutil/svcutil.go new file mode 100644 index 000000000..eee33352e --- /dev/null +++ b/lib/svcutil/svcutil.go @@ -0,0 +1,177 @@ +// Copyright (C) 2016 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 svcutil + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/syncthing/syncthing/lib/logger" + "github.com/syncthing/syncthing/lib/sync" + + "github.com/thejerf/suture/v4" +) + +const ServiceTimeout = 10 * time.Second + +type FatalErr struct { + Err error + Status ExitStatus +} + +// AsFatalErr wraps the given error creating a FatalErr. If the given error +// already is of type FatalErr, it is not wrapped again. +func AsFatalErr(err error, status ExitStatus) *FatalErr { + var ferr *FatalErr + if errors.As(err, &ferr) { + return ferr + } + return &FatalErr{ + Err: err, + Status: status, + } +} + +func (e *FatalErr) Error() string { + return e.Err.Error() +} + +func (e *FatalErr) Unwrap() error { + return e.Err +} + +func (e *FatalErr) Is(target error) bool { + return target == suture.ErrTerminateSupervisorTree +} + +// NoRestartErr wraps the given error err (which may be nil) to make sure that +// `errors.Is(err, suture.ErrDoNotRestart) == true`. +func NoRestartErr(err error) error { + if err == nil { + return suture.ErrDoNotRestart + } + return &noRestartErr{err} +} + +type noRestartErr struct { + err error +} + +func (e *noRestartErr) Error() string { + return e.err.Error() +} + +func (e *noRestartErr) Unwrap() error { + return e.err +} + +func (e *noRestartErr) Is(target error) bool { + return target == suture.ErrDoNotRestart +} + +type ExitStatus int + +const ( + ExitSuccess ExitStatus = 0 + ExitError ExitStatus = 1 + ExitNoUpgradeAvailable ExitStatus = 2 + ExitRestart ExitStatus = 3 + ExitUpgrade ExitStatus = 4 +) + +func (s ExitStatus) AsInt() int { + return int(s) +} + +type ServiceWithError interface { + suture.Service + fmt.Stringer + Error() error + SetError(error) +} + +// AsService wraps the given function to implement suture.Service. In addition +// it keeps track of the returned error and allows querying and setting that error. +func AsService(fn func(ctx context.Context) error, creator string) ServiceWithError { + return &service{ + creator: creator, + serve: fn, + mut: sync.NewMutex(), + } +} + +type service struct { + creator string + serve func(ctx context.Context) error + err error + mut sync.Mutex +} + +func (s *service) Serve(ctx context.Context) error { + s.mut.Lock() + s.err = nil + s.mut.Unlock() + + err := s.serve(ctx) + + s.mut.Lock() + s.err = err + s.mut.Unlock() + + return err +} + +func (s *service) Error() error { + s.mut.Lock() + defer s.mut.Unlock() + return s.err +} + +func (s *service) SetError(err error) { + s.mut.Lock() + s.err = err + s.mut.Unlock() +} + +func (s *service) String() string { + return fmt.Sprintf("Service@%p created by %v", s, s.creator) + +} + +type doneService struct { + fn func() +} + +func (s *doneService) Serve(ctx context.Context) error { + <-ctx.Done() + s.fn() + return nil +} + +// OnSupervisorDone calls fn when sup is done. +func OnSupervisorDone(sup *suture.Supervisor, fn func()) { + sup.Add(&doneService{fn}) +} + +func SpecWithDebugLogger(l logger.Logger) suture.Spec { + return spec(func(e suture.Event) { l.Debugln(e) }) +} + +func SpecWithInfoLogger(l logger.Logger) suture.Spec { + return spec(func(e suture.Event) { l.Infoln(e) }) +} + +func spec(eventHook suture.EventHook) suture.Spec { + return suture.Spec{ + EventHook: eventHook, + Timeout: ServiceTimeout, + PassThroughPanics: true, + DontPropagateTermination: false, + } +} diff --git a/lib/syncthing/syncthing.go b/lib/syncthing/syncthing.go index 9a3778a07..c4ed0b5ef 100644 --- a/lib/syncthing/syncthing.go +++ b/lib/syncthing/syncthing.go @@ -37,10 +37,10 @@ import ( "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/rand" "github.com/syncthing/syncthing/lib/sha256" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/ur" - "github.com/syncthing/syncthing/lib/util" ) const ( @@ -73,7 +73,7 @@ type App struct { evLogger events.Logger cert tls.Certificate opts Options - exitStatus util.ExitStatus + exitStatus svcutil.ExitStatus err error stopOnce sync.Once mainServiceCancel context.CancelFunc @@ -103,7 +103,7 @@ func New(cfg config.Wrapper, dbBackend backend.Backend, evLogger events.Logger, func (a *App) Start() error { // Create a main service manager. We'll add things to this as we go along. // We want any logging it does to go through our log system. - spec := util.SpecWithDebugLogger(l) + spec := svcutil.SpecWithDebugLogger(l) a.mainService = suture.New("main", spec) // Start the supervisor and wait for it to stop to handle cleanup. @@ -113,7 +113,7 @@ func (a *App) Start() error { go a.run(ctx) if err := a.startup(); err != nil { - a.stopWithErr(util.ExitError, err) + a.stopWithErr(svcutil.ExitError, err) return err } @@ -355,19 +355,19 @@ func (a *App) handleMainServiceError(err error) { if err == nil || errors.Is(err, context.Canceled) { return } - var fatalErr *util.FatalErr + var fatalErr *svcutil.FatalErr if errors.As(err, &fatalErr) { a.exitStatus = fatalErr.Status a.err = fatalErr.Err return } a.err = err - a.exitStatus = util.ExitError + a.exitStatus = svcutil.ExitError } // Wait blocks until the app stops running. Also returns if the app hasn't been // started yet. -func (a *App) Wait() util.ExitStatus { +func (a *App) Wait() svcutil.ExitStatus { <-a.stopped return a.exitStatus } @@ -385,11 +385,11 @@ func (a *App) Error() error { // Stop stops the app and sets its exit status to given reason, unless the app // was already stopped before. In any case it returns the effective exit status. -func (a *App) Stop(stopReason util.ExitStatus) util.ExitStatus { +func (a *App) Stop(stopReason svcutil.ExitStatus) svcutil.ExitStatus { return a.stopWithErr(stopReason, nil) } -func (a *App) stopWithErr(stopReason util.ExitStatus, err error) util.ExitStatus { +func (a *App) stopWithErr(stopReason svcutil.ExitStatus, err error) svcutil.ExitStatus { a.stopOnce.Do(func() { a.exitStatus = stopReason a.err = err diff --git a/lib/syncthing/syncthing_test.go b/lib/syncthing/syncthing_test.go index 17bd2ab56..5c6d6278c 100644 --- a/lib/syncthing/syncthing_test.go +++ b/lib/syncthing/syncthing_test.go @@ -17,8 +17,8 @@ import ( "github.com/syncthing/syncthing/lib/db/backend" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/tlsutil" - "github.com/syncthing/syncthing/lib/util" ) func tempCfgFilename(t *testing.T) string { @@ -90,7 +90,7 @@ func TestStartupFail(t *testing.T) { } done := make(chan struct{}) - var waitE util.ExitStatus + var waitE svcutil.ExitStatus go func() { waitE = app.Wait() close(done) @@ -102,8 +102,8 @@ func TestStartupFail(t *testing.T) { case <-done: } - if waitE != util.ExitError { - t.Errorf("Got exit status %v, expected %v", waitE, util.ExitError) + if waitE != svcutil.ExitError { + t.Errorf("Got exit status %v, expected %v", waitE, svcutil.ExitError) } if err = app.Error(); err != startErr { diff --git a/lib/ur/failurereporting.go b/lib/ur/failurereporting.go index 942a9f601..4e28ffc9b 100644 --- a/lib/ur/failurereporting.go +++ b/lib/ur/failurereporting.go @@ -17,6 +17,7 @@ import ( "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/dialer" "github.com/syncthing/syncthing/lib/events" + "github.com/syncthing/syncthing/lib/svcutil" "github.com/thejerf/suture/v4" ) @@ -28,6 +29,7 @@ var ( minDelay = 10 * time.Second maxDelay = time.Minute sendTimeout = time.Minute + finalSendTimeout = svcutil.ServiceTimeout / 2 evChanClosed = "failure event channel closed" invalidEventDataType = "failure event data is not a string" ) @@ -116,11 +118,7 @@ outer: now := time.Now() for descr, stat := range h.buf { if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay { - reports = append(reports, FailureReport{ - Description: descr, - Count: stat.count, - Version: build.LongVersion, - }) + reports = append(reports, newFailureReport(descr, stat.count)) delete(h.buf, descr) } } @@ -145,6 +143,13 @@ outer: if sub != nil { sub.Unsubscribe() + reports := make([]FailureReport, 0, len(h.buf)) + for descr, stat := range h.buf { + reports = append(reports, newFailureReport(descr, stat.count)) + } + timeout, cancel := context.WithTimeout(context.Background(), finalSendTimeout) + defer cancel() + sendFailureReports(timeout, reports, url) } return err } @@ -207,3 +212,11 @@ func sendFailureReports(ctx context.Context, reports []FailureReport, url string resp.Body.Close() return } + +func newFailureReport(descr string, count int) FailureReport { + return FailureReport{ + Description: descr, + Count: count, + Version: build.LongVersion, + } +} diff --git a/lib/util/utils.go b/lib/util/utils.go index 124fcea10..67bd9d2fe 100644 --- a/lib/util/utils.go +++ b/lib/util/utils.go @@ -8,7 +8,6 @@ package util import ( "context" - "errors" "fmt" "net" "net/url" @@ -17,9 +16,6 @@ import ( "strings" "time" - "github.com/syncthing/syncthing/lib/logger" - "github.com/syncthing/syncthing/lib/sync" - "github.com/thejerf/suture/v4" ) @@ -263,19 +259,6 @@ type FatalErr struct { Status ExitStatus } -// AsFatalErr wraps the given error creating a FatalErr. If the given error -// already is of type FatalErr, it is not wrapped again. -func AsFatalErr(err error, status ExitStatus) *FatalErr { - var ferr *FatalErr - if errors.As(err, &ferr) { - return ferr - } - return &FatalErr{ - Err: err, - Status: status, - } -} - func (e *FatalErr) Error() string { return e.Err.Error() } @@ -327,61 +310,6 @@ func (s ExitStatus) AsInt() int { return int(s) } -type ServiceWithError interface { - suture.Service - fmt.Stringer - Error() error - SetError(error) -} - -// AsService wraps the given function to implement suture.Service. In addition -// it keeps track of the returned error and allows querying and setting that error. -func AsService(fn func(ctx context.Context) error, creator string) ServiceWithError { - return &service{ - creator: creator, - serve: fn, - mut: sync.NewMutex(), - } -} - -type service struct { - creator string - serve func(ctx context.Context) error - err error - mut sync.Mutex -} - -func (s *service) Serve(ctx context.Context) error { - s.mut.Lock() - s.err = nil - s.mut.Unlock() - - err := s.serve(ctx) - - s.mut.Lock() - s.err = err - s.mut.Unlock() - - return err -} - -func (s *service) Error() error { - s.mut.Lock() - defer s.mut.Unlock() - return s.err -} - -func (s *service) SetError(err error) { - s.mut.Lock() - s.err = err - s.mut.Unlock() -} - -func (s *service) String() string { - return fmt.Sprintf("Service@%p created by %v", s, s.creator) - -} - // OnDone calls fn when ctx is cancelled. func OnDone(ctx context.Context, fn func()) { go func() { @@ -390,37 +318,6 @@ func OnDone(ctx context.Context, fn func()) { }() } -type doneService struct { - fn func() -} - -func (s *doneService) Serve(ctx context.Context) error { - <-ctx.Done() - s.fn() - return nil -} - -// OnSupervisorDone calls fn when sup is done. -func OnSupervisorDone(sup *suture.Supervisor, fn func()) { - sup.Add(&doneService{fn}) -} - -func SpecWithDebugLogger(l logger.Logger) suture.Spec { - return spec(func(e suture.Event) { l.Debugln(e) }) -} - -func SpecWithInfoLogger(l logger.Logger) suture.Spec { - return spec(func(e suture.Event) { l.Infoln(e) }) -} - -func spec(eventHook suture.EventHook) suture.Spec { - return suture.Spec{ - EventHook: eventHook, - PassThroughPanics: true, - DontPropagateTermination: false, - } -} - func CallWithContext(ctx context.Context, fn func() error) error { var err error done := make(chan struct{})