浏览代码

lib/ur: Send unreported failures on shutdown (#7164)

Simon Frei 4 年之前
父节点
当前提交
a20a5f61f0

+ 24 - 24
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()
 }

+ 5 - 5
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)

+ 12 - 12
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,
 		})
 	}
 }

+ 2 - 2
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)

+ 7 - 7
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 {

+ 3 - 3
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
 }

+ 3 - 3
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
 }
 

+ 5 - 4
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)
 	})

+ 3 - 3
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
 }
 

+ 3 - 2
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")

+ 4 - 4
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
 }

+ 3 - 2
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
 }
 

+ 4 - 4
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
 }

+ 2 - 2
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)
 	}()
 

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

+ 3 - 3
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)

+ 177 - 0
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,
+	}
+}

+ 9 - 9
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

+ 4 - 4
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 {

+ 18 - 5
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,
+	}
+}

+ 0 - 103
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{})