1// Copyright (C) 2016 The Syncthing Authors. 2// 3// This Source Code Form is subject to the terms of the Mozilla Public 4// License, v. 2.0. If a copy of the MPL was not distributed with this file, 5// You can obtain one at https://mozilla.org/MPL/2.0/. 6 7package svcutil 8 9import ( 10 "context" 11 "errors" 12 "fmt" 13 "time" 14 15 "github.com/syncthing/syncthing/lib/logger" 16 "github.com/syncthing/syncthing/lib/sync" 17 18 "github.com/thejerf/suture/v4" 19) 20 21const ServiceTimeout = 10 * time.Second 22 23type FatalErr struct { 24 Err error 25 Status ExitStatus 26} 27 28// AsFatalErr wraps the given error creating a FatalErr. If the given error 29// already is of type FatalErr, it is not wrapped again. 30func AsFatalErr(err error, status ExitStatus) *FatalErr { 31 var ferr *FatalErr 32 if errors.As(err, &ferr) { 33 return ferr 34 } 35 return &FatalErr{ 36 Err: err, 37 Status: status, 38 } 39} 40 41func IsFatal(err error) bool { 42 ferr := &FatalErr{} 43 return errors.As(err, &ferr) 44} 45 46func (e *FatalErr) Error() string { 47 return e.Err.Error() 48} 49 50func (e *FatalErr) Unwrap() error { 51 return e.Err 52} 53 54func (e *FatalErr) Is(target error) bool { 55 return target == suture.ErrTerminateSupervisorTree 56} 57 58// NoRestartErr wraps the given error err (which may be nil) to make sure that 59// `errors.Is(err, suture.ErrDoNotRestart) == true`. 60func NoRestartErr(err error) error { 61 if err == nil { 62 return suture.ErrDoNotRestart 63 } 64 return &noRestartErr{err} 65} 66 67type noRestartErr struct { 68 err error 69} 70 71func (e *noRestartErr) Error() string { 72 return e.err.Error() 73} 74 75func (e *noRestartErr) Unwrap() error { 76 return e.err 77} 78 79func (e *noRestartErr) Is(target error) bool { 80 return target == suture.ErrDoNotRestart 81} 82 83type ExitStatus int 84 85const ( 86 ExitSuccess ExitStatus = 0 87 ExitError ExitStatus = 1 88 ExitNoUpgradeAvailable ExitStatus = 2 89 ExitRestart ExitStatus = 3 90 ExitUpgrade ExitStatus = 4 91) 92 93func (s ExitStatus) AsInt() int { 94 return int(s) 95} 96 97type ServiceWithError interface { 98 suture.Service 99 fmt.Stringer 100 Error() error 101} 102 103// AsService wraps the given function to implement suture.Service. In addition 104// it keeps track of the returned error and allows querying that error. 105func AsService(fn func(ctx context.Context) error, creator string) ServiceWithError { 106 return &service{ 107 creator: creator, 108 serve: fn, 109 mut: sync.NewMutex(), 110 } 111} 112 113type service struct { 114 creator string 115 serve func(ctx context.Context) error 116 err error 117 mut sync.Mutex 118} 119 120func (s *service) Serve(ctx context.Context) error { 121 s.mut.Lock() 122 s.err = nil 123 s.mut.Unlock() 124 125 err := s.serve(ctx) 126 127 s.mut.Lock() 128 s.err = err 129 s.mut.Unlock() 130 131 return err 132} 133 134func (s *service) Error() error { 135 s.mut.Lock() 136 defer s.mut.Unlock() 137 return s.err 138} 139 140func (s *service) String() string { 141 return fmt.Sprintf("Service@%p created by %v", s, s.creator) 142 143} 144 145type doneService func() 146 147func (fn doneService) Serve(ctx context.Context) error { 148 <-ctx.Done() 149 fn() 150 return nil 151} 152 153// OnSupervisorDone calls fn when sup is done. 154func OnSupervisorDone(sup *suture.Supervisor, fn func()) { 155 sup.Add(doneService(fn)) 156} 157 158func SpecWithDebugLogger(l logger.Logger) suture.Spec { 159 return spec(func(e suture.Event) { l.Debugln(e) }) 160} 161 162func SpecWithInfoLogger(l logger.Logger) suture.Spec { 163 return spec(infoEventHook(l)) 164} 165 166func spec(eventHook suture.EventHook) suture.Spec { 167 return suture.Spec{ 168 EventHook: eventHook, 169 Timeout: ServiceTimeout, 170 PassThroughPanics: true, 171 DontPropagateTermination: false, 172 } 173} 174 175// infoEventHook prints service failures and failures to stop services at level 176// info. All other events and identical, consecutive failures are logged at 177// debug only. 178func infoEventHook(l logger.Logger) suture.EventHook { 179 var prevTerminate suture.EventServiceTerminate 180 return func(ei suture.Event) { 181 switch e := ei.(type) { 182 case suture.EventStopTimeout: 183 l.Infof("%s: Service %s failed to terminate in a timely manner", e.SupervisorName, e.ServiceName) 184 case suture.EventServicePanic: 185 l.Warnln("Caught a service panic, which shouldn't happen") 186 l.Infoln(e) 187 case suture.EventServiceTerminate: 188 msg := fmt.Sprintf("%s: service %s failed: %s", e.SupervisorName, e.ServiceName, e.Err) 189 if e.ServiceName == prevTerminate.ServiceName && e.Err == prevTerminate.Err { 190 l.Debugln(msg) 191 } else { 192 l.Infoln(msg) 193 } 194 prevTerminate = e 195 l.Debugln(e) // Contains some backoff statistics 196 case suture.EventBackoff: 197 l.Debugf("%s: exiting the backoff state.", e.SupervisorName) 198 case suture.EventResume: 199 l.Debugf("%s: too many service failures - entering the backoff state.", e.SupervisorName) 200 default: 201 l.Warnln("Unknown suture supervisor event type", e.Type()) 202 l.Infoln(e) 203 } 204 } 205} 206