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