mirror of
https://github.com/octoleo/syncthing.git
synced 2025-02-02 11:58:28 +00:00
cmd/syncthing: Make API serve loop more robust (fixes #3136)
This sacrifices the ability to return an error when creating the service for being more persistent in keeping it running. GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3270 LGTM: AudriusButkevicius, canton7
This commit is contained in:
parent
06e63aedea
commit
5aacfd1639
@ -64,10 +64,8 @@ type apiService struct {
|
|||||||
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
|
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
|
||||||
stop chan struct{} // signals intentional stop
|
stop chan struct{} // signals intentional stop
|
||||||
configChanged chan struct{} // signals intentional listener close due to config change
|
configChanged chan struct{} // signals intentional listener close due to config change
|
||||||
started chan struct{} // signals startup complete, for testing only
|
started chan string // signals startup complete by sending the listener address, for testing only
|
||||||
|
startedOnce bool // the service has started successfully at least once
|
||||||
listener net.Listener
|
|
||||||
listenerMut sync.Mutex
|
|
||||||
|
|
||||||
guiErrors logger.Recorder
|
guiErrors logger.Recorder
|
||||||
systemLog logger.Recorder
|
systemLog logger.Recorder
|
||||||
@ -119,7 +117,7 @@ type connectionsIntf interface {
|
|||||||
Status() map[string]interface{}
|
Status() map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder) (*apiService, error) {
|
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder) *apiService {
|
||||||
service := &apiService{
|
service := &apiService{
|
||||||
id: id,
|
id: id,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@ -133,7 +131,6 @@ func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKey
|
|||||||
systemConfigMut: sync.NewMutex(),
|
systemConfigMut: sync.NewMutex(),
|
||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
configChanged: make(chan struct{}),
|
configChanged: make(chan struct{}),
|
||||||
listenerMut: sync.NewMutex(),
|
|
||||||
guiErrors: errors,
|
guiErrors: errors,
|
||||||
systemLog: systemLog,
|
systemLog: systemLog,
|
||||||
}
|
}
|
||||||
@ -157,9 +154,7 @@ func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKey
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
return service
|
||||||
service.listener, err = service.getListener(cfg.GUI())
|
|
||||||
return service, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
|
func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
|
||||||
@ -226,9 +221,22 @@ func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiService) Serve() {
|
func (s *apiService) Serve() {
|
||||||
s.listenerMut.Lock()
|
listener, err := s.getListener(s.cfg.GUI())
|
||||||
listener := s.listener
|
if err != nil {
|
||||||
s.listenerMut.Unlock()
|
if !s.startedOnce {
|
||||||
|
// This is during initialization. A failure here should be fatal
|
||||||
|
// as there will be no way for the user to communicate with us
|
||||||
|
// otherwise anyway.
|
||||||
|
l.Fatalln("Starting API/GUI:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We let this be a loud user-visible warning as it may be the only
|
||||||
|
// indication they get that the GUI won't be available on startup.
|
||||||
|
l.Warnln("Starting API/GUI:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.startedOnce = true
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
if listener == nil {
|
if listener == nil {
|
||||||
// Not much we can do here other than exit quickly. The supervisor
|
// Not much we can do here other than exit quickly. The supervisor
|
||||||
@ -348,33 +356,33 @@ func (s *apiService) Serve() {
|
|||||||
l.Infoln("Access the GUI via the following URL:", guiCfg.URL())
|
l.Infoln("Access the GUI via the following URL:", guiCfg.URL())
|
||||||
if s.started != nil {
|
if s.started != nil {
|
||||||
// only set when run by the tests
|
// only set when run by the tests
|
||||||
close(s.started)
|
s.started <- listener.Addr().String()
|
||||||
}
|
}
|
||||||
err := srv.Serve(listener)
|
|
||||||
|
|
||||||
// The return could be due to an intentional close. Wait for the stop
|
// Serve in the background
|
||||||
// signal before returning. IF there is no stop signal within a second, we
|
|
||||||
// assume it was unintentional and log the error before retrying.
|
serveError := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
serveError <- srv.Serve(listener)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for stop, restart or error signals
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-s.stop:
|
case <-s.stop:
|
||||||
|
// Shutting down permanently
|
||||||
|
l.Debugln("shutting down (stop)")
|
||||||
case <-s.configChanged:
|
case <-s.configChanged:
|
||||||
case <-time.After(time.Second):
|
// Soft restart due to configuration change
|
||||||
l.Warnln("API:", err)
|
l.Debugln("restarting (config changed)")
|
||||||
|
case <-serveError:
|
||||||
|
// Restart due to listen/serve failure
|
||||||
|
l.Warnln("GUI/API:", err, "(restarting)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiService) Stop() {
|
func (s *apiService) Stop() {
|
||||||
s.listenerMut.Lock()
|
|
||||||
listener := s.listener
|
|
||||||
s.listenerMut.Unlock()
|
|
||||||
|
|
||||||
close(s.stop)
|
close(s.stop)
|
||||||
|
|
||||||
// listener may be nil here if we've had a config change to a broken
|
|
||||||
// configuration, in which case we shouldn't try to close it.
|
|
||||||
if listener != nil {
|
|
||||||
listener.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiService) String() string {
|
func (s *apiService) String() string {
|
||||||
@ -382,6 +390,9 @@ func (s *apiService) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
|
func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
|
||||||
|
if _, err := net.ResolveTCPAddr("tcp", to.GUI.Address()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,27 +401,7 @@ func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order here is important. We must close the listener to stop Serve(). We
|
// Tell the serve loop to restart
|
||||||
// must create a new listener before Serve() starts again. We can't create
|
|
||||||
// a new listener on the same port before the previous listener is closed.
|
|
||||||
// To assist in this little dance the Serve() method will wait for a
|
|
||||||
// signal on the configChanged channel after the listener has closed.
|
|
||||||
|
|
||||||
s.listenerMut.Lock()
|
|
||||||
defer s.listenerMut.Unlock()
|
|
||||||
|
|
||||||
s.listener.Close()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
s.listener, err = s.getListener(to.GUI)
|
|
||||||
if err != nil {
|
|
||||||
// Ideally this should be a verification error, but we check it by
|
|
||||||
// creating a new listener which requires shutting down the previous
|
|
||||||
// one first, which is too destructive for the VerifyConfiguration
|
|
||||||
// method.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
s.configChanged <- struct{}{}
|
s.configChanged <- struct{}{}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -68,11 +68,8 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
w := config.Wrap("/dev/null", cfg)
|
w := config.Wrap("/dev/null", cfg)
|
||||||
|
|
||||||
srv, err := newAPIService(protocol.LocalDeviceID, w, "../../test/h1/https-cert.pem", "../../test/h1/https-key.pem", "", nil, nil, nil, nil, nil, nil)
|
srv := newAPIService(protocol.LocalDeviceID, w, "../../test/h1/https-cert.pem", "../../test/h1/https-key.pem", "", nil, nil, nil, nil, nil, nil)
|
||||||
if err != nil {
|
srv.started = make(chan string)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
srv.started = make(chan struct{})
|
|
||||||
|
|
||||||
sup := suture.NewSimple("test")
|
sup := suture.NewSimple("test")
|
||||||
sup.Add(srv)
|
sup.Add(srv)
|
||||||
@ -90,8 +87,8 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
|||||||
RawUseTLS: false,
|
RawUseTLS: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if srv.CommitConfiguration(cfg, newCfg) {
|
if err := srv.VerifyConfiguration(cfg, newCfg); err == nil {
|
||||||
t.Fatal("Config commit should have failed")
|
t.Fatal("Verify config should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nonetheless, it should be fine to Stop() it without panic.
|
// Nonetheless, it should be fine to Stop() it without panic.
|
||||||
@ -475,30 +472,26 @@ func startHTTP(cfg *mockedConfig) (string, error) {
|
|||||||
connections := new(mockedConnections)
|
connections := new(mockedConnections)
|
||||||
errorLog := new(mockedLoggerRecorder)
|
errorLog := new(mockedLoggerRecorder)
|
||||||
systemLog := new(mockedLoggerRecorder)
|
systemLog := new(mockedLoggerRecorder)
|
||||||
|
addrChan := make(chan string)
|
||||||
|
|
||||||
// Instantiate the API service
|
// Instantiate the API service
|
||||||
svc, err := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
|
svc := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
|
||||||
eventSub, discoverer, connections, errorLog, systemLog)
|
eventSub, discoverer, connections, errorLog, systemLog)
|
||||||
if err != nil {
|
svc.started = addrChan
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the API service is listening, and get the URL to use.
|
|
||||||
addr := svc.listener.Addr()
|
|
||||||
if addr == nil {
|
|
||||||
return "", fmt.Errorf("Nil listening address from API service")
|
|
||||||
}
|
|
||||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr.String())
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("Weird address from API service: %v", err)
|
|
||||||
}
|
|
||||||
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
|
|
||||||
|
|
||||||
// Actually start the API service
|
// Actually start the API service
|
||||||
supervisor := suture.NewSimple("API test")
|
supervisor := suture.NewSimple("API test")
|
||||||
supervisor.Add(svc)
|
supervisor.Add(svc)
|
||||||
supervisor.ServeBackground()
|
supervisor.ServeBackground()
|
||||||
|
|
||||||
|
// Make sure the API service is listening, and get the URL to use.
|
||||||
|
addr := <-addrChan
|
||||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Weird address from API service: %v", err)
|
||||||
|
}
|
||||||
|
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
|
||||||
|
|
||||||
return baseURL, nil
|
return baseURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -933,10 +933,7 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
|
|||||||
l.Warnln("Insecure admin access is enabled.")
|
l.Warnln("Insecure admin access is enabled.")
|
||||||
}
|
}
|
||||||
|
|
||||||
api, err := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, connectionsService, errors, systemLog)
|
api := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, connectionsService, errors, systemLog)
|
||||||
if err != nil {
|
|
||||||
l.Fatalln("Cannot start GUI:", err)
|
|
||||||
}
|
|
||||||
cfg.Subscribe(api)
|
cfg.Subscribe(api)
|
||||||
mainService.Add(api)
|
mainService.Add(api)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user