all: Implement suture v4-api (#6947)
This commit is contained in:
+38
-21
@@ -33,7 +33,7 @@ import (
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
metrics "github.com/rcrowley/go-metrics"
|
||||
"github.com/thejerf/suture"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/vitrun/qart/qr"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
@@ -82,7 +82,6 @@ type service struct {
|
||||
connectionsService connections.Service
|
||||
fss model.FolderSummaryService
|
||||
urService *ur.Service
|
||||
contr Controller
|
||||
noUpgrade bool
|
||||
tlsDefaultCommonName string
|
||||
configChanged chan struct{} // signals intentional listener close due to config change
|
||||
@@ -90,25 +89,20 @@ type service struct {
|
||||
startedOnce chan struct{} // the service has started successfully at least once
|
||||
startupErr error
|
||||
listenerAddr net.Addr
|
||||
exitChan chan *util.FatalErr
|
||||
|
||||
guiErrors logger.Recorder
|
||||
systemLog logger.Recorder
|
||||
}
|
||||
|
||||
type Controller interface {
|
||||
ExitUpgrading()
|
||||
Restart()
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
suture.Service
|
||||
config.Committer
|
||||
WaitForStart() error
|
||||
}
|
||||
|
||||
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, contr Controller, noUpgrade bool) Service {
|
||||
s := &service{
|
||||
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool) Service {
|
||||
return &service{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
statics: newStaticsServer(cfg.GUI().Theme, assetDir, cfg.Options().FeatureFlag(featureFlagUntrusted)),
|
||||
@@ -125,14 +119,12 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
|
||||
urService: urService,
|
||||
guiErrors: errors,
|
||||
systemLog: systemLog,
|
||||
contr: contr,
|
||||
noUpgrade: noUpgrade,
|
||||
tlsDefaultCommonName: tlsDefaultCommonName,
|
||||
configChanged: make(chan struct{}),
|
||||
startedOnce: make(chan struct{}),
|
||||
exitChan: make(chan *util.FatalErr, 1),
|
||||
}
|
||||
s.Service = util.AsService(s.serve, s.String())
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *service) WaitForStart() error {
|
||||
@@ -211,7 +203,7 @@ func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
|
||||
fmt.Fprintf(w, "%s\n", bs)
|
||||
}
|
||||
|
||||
func (s *service) serve(ctx context.Context) {
|
||||
func (s *service) Serve(ctx context.Context) error {
|
||||
listener, err := s.getListener(s.cfg.GUI())
|
||||
if err != nil {
|
||||
select {
|
||||
@@ -227,13 +219,13 @@ func (s *service) serve(ctx context.Context) {
|
||||
s.startupErr = err
|
||||
close(s.startedOnce)
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if listener == nil {
|
||||
// Not much we can do here other than exit quickly. The supervisor
|
||||
// will log an error at some point.
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
s.listenerAddr = listener.Addr()
|
||||
@@ -410,6 +402,7 @@ func (s *service) serve(ctx context.Context) {
|
||||
|
||||
// Wait for stop, restart or error signals
|
||||
|
||||
err = nil
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Shutting down permanently
|
||||
@@ -417,11 +410,14 @@ func (s *service) serve(ctx context.Context) {
|
||||
case <-s.configChanged:
|
||||
// Soft restart due to configuration change
|
||||
l.Debugln("restarting (config changed)")
|
||||
case <-serveError:
|
||||
case err = <-s.exitChan:
|
||||
case err = <-serveError:
|
||||
// Restart due to listen/serve failure
|
||||
l.Warnln("GUI/API:", err, "(restarting)")
|
||||
}
|
||||
srv.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Complete implements suture.IsCompletable, which signifies to the supervisor
|
||||
@@ -470,6 +466,14 @@ func (s *service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *service) fatal(err *util.FatalErr) {
|
||||
// s.exitChan is 1-buffered and whoever is first gets handled.
|
||||
select {
|
||||
case s.exitChan <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func debugMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t0 := time.Now()
|
||||
@@ -874,7 +878,11 @@ 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)
|
||||
go s.contr.Restart()
|
||||
|
||||
s.fatal(&util.FatalErr{
|
||||
Err: errors.New("restart initiated by rest API"),
|
||||
Status: util.ExitRestart,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -900,12 +908,18 @@ func (s *service) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "resetting folder `+folder+`"}`, w)
|
||||
}
|
||||
|
||||
go s.contr.Restart()
|
||||
s.fatal(&util.FatalErr{
|
||||
Err: errors.New("restart after db reset initiated by rest API"),
|
||||
Status: util.ExitRestart,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "shutting down"}`, w)
|
||||
go s.contr.Shutdown()
|
||||
s.fatal(&util.FatalErr{
|
||||
Err: errors.New("shutdown initiated by rest API"),
|
||||
Status: util.ExitSuccess,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) flushResponse(resp string, w http.ResponseWriter) {
|
||||
@@ -1340,7 +1354,10 @@ func (s *service) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
s.flushResponse(`{"ok": "restarting"}`, w)
|
||||
s.contr.ExitUpgrading()
|
||||
s.fatal(&util.FatalErr{
|
||||
Err: errors.New("exit after upgrade initiated by rest API"),
|
||||
Status: util.ExitUpgrade,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-40
@@ -9,6 +9,7 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -34,7 +35,8 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/ur"
|
||||
"github.com/thejerf/suture"
|
||||
"github.com/syncthing/syncthing/lib/util"
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -113,15 +115,14 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
||||
}
|
||||
w := config.Wrap("/dev/null", cfg, events.NoopLogger)
|
||||
|
||||
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, false).(*service)
|
||||
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, false).(*service)
|
||||
defer os.Remove(token)
|
||||
srv.started = make(chan string)
|
||||
|
||||
sup := suture.New("test", suture.Spec{
|
||||
PassThroughPanics: true,
|
||||
})
|
||||
sup := suture.New("test", util.Spec())
|
||||
sup.Add(srv)
|
||||
sup.ServeBackground()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sup.ServeBackground(ctx)
|
||||
|
||||
<-srv.started
|
||||
|
||||
@@ -139,9 +140,7 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
||||
t.Fatal("Verify config should have failed")
|
||||
}
|
||||
|
||||
// Nonetheless, it should be fine to Stop() it without panic.
|
||||
|
||||
sup.Stop()
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestAssetsDir(t *testing.T) {
|
||||
@@ -250,11 +249,11 @@ func TestAPIServiceRequests(t *testing.T) {
|
||||
const testAPIKey = "foobarbaz"
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.APIKey = testAPIKey
|
||||
baseURL, sup, err := startHTTP(cfg)
|
||||
baseURL, cancel, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
|
||||
cases := []httpTestCase{
|
||||
// /rest/db
|
||||
@@ -519,11 +518,11 @@ func TestHTTPLogin(t *testing.T) {
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.User = "üser"
|
||||
cfg.gui.Password = "$2a$10$IdIZTxTg/dCNuNEGlmLynOjqg4B1FvDKuIV5e0BB3pnWVHNb8.GSq" // bcrypt of "räksmörgås" in UTF-8
|
||||
baseURL, sup, err := startHTTP(cfg)
|
||||
baseURL, cancel, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
|
||||
// Verify rejection when not using authorization
|
||||
|
||||
@@ -581,7 +580,7 @@ func TestHTTPLogin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
|
||||
func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
|
||||
m := new(mockedModel)
|
||||
assetDir := "../../gui"
|
||||
eventSub := new(mockedEventSub)
|
||||
@@ -594,7 +593,7 @@ func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
|
||||
|
||||
// Instantiate the API service
|
||||
urService := ur.New(cfg, m, connections, false)
|
||||
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, &mockedFolderSummaryService{}, errorLog, systemLog, nil, false).(*service)
|
||||
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, &mockedFolderSummaryService{}, errorLog, systemLog, false).(*service)
|
||||
defer os.Remove(token)
|
||||
svc.started = addrChan
|
||||
|
||||
@@ -603,14 +602,15 @@ func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
|
||||
PassThroughPanics: true,
|
||||
})
|
||||
supervisor.Add(svc)
|
||||
supervisor.ServeBackground()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
supervisor.ServeBackground(ctx)
|
||||
|
||||
// Make sure the API service is listening, and get the URL to use.
|
||||
addr := <-addrChan
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
supervisor.Stop()
|
||||
return "", nil, fmt.Errorf("weird address from API service: %w", err)
|
||||
cancel()
|
||||
return "", cancel, fmt.Errorf("weird address from API service: %w", err)
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(cfg.GUI().RawAddress)
|
||||
@@ -619,7 +619,7 @@ func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
|
||||
}
|
||||
baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
|
||||
|
||||
return baseURL, supervisor, nil
|
||||
return baseURL, cancel, nil
|
||||
}
|
||||
|
||||
func TestCSRFRequired(t *testing.T) {
|
||||
@@ -628,11 +628,11 @@ func TestCSRFRequired(t *testing.T) {
|
||||
const testAPIKey = "foobarbaz"
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.APIKey = testAPIKey
|
||||
baseURL, sup, err := startHTTP(cfg)
|
||||
baseURL, cancel, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error from getting base URL:", err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
|
||||
cli := &http.Client{
|
||||
Timeout: time.Minute,
|
||||
@@ -704,11 +704,11 @@ func TestRandomString(t *testing.T) {
|
||||
const testAPIKey = "foobarbaz"
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.APIKey = testAPIKey
|
||||
baseURL, sup, err := startHTTP(cfg)
|
||||
baseURL, cancel, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
cli := &http.Client{
|
||||
Timeout: time.Second,
|
||||
}
|
||||
@@ -797,11 +797,11 @@ func testConfigPost(data io.Reader) (*http.Response, error) {
|
||||
const testAPIKey = "foobarbaz"
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.APIKey = testAPIKey
|
||||
baseURL, sup, err := startHTTP(cfg)
|
||||
baseURL, cancel, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
cli := &http.Client{
|
||||
Timeout: time.Second,
|
||||
}
|
||||
@@ -818,11 +818,11 @@ func TestHostCheck(t *testing.T) {
|
||||
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.RawAddress = "127.0.0.1:0"
|
||||
baseURL, sup, err := startHTTP(cfg)
|
||||
baseURL, cancel, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
|
||||
// A normal HTTP get to the localhost-bound service should succeed
|
||||
|
||||
@@ -879,11 +879,11 @@ func TestHostCheck(t *testing.T) {
|
||||
cfg = new(mockedConfig)
|
||||
cfg.gui.RawAddress = "127.0.0.1:0"
|
||||
cfg.gui.InsecureSkipHostCheck = true
|
||||
baseURL, sup, err = startHTTP(cfg)
|
||||
baseURL, cancel, err = startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
|
||||
// A request with a suspicious Host header should be allowed
|
||||
|
||||
@@ -903,11 +903,11 @@ func TestHostCheck(t *testing.T) {
|
||||
cfg = new(mockedConfig)
|
||||
cfg.gui.RawAddress = "0.0.0.0:0"
|
||||
cfg.gui.InsecureSkipHostCheck = true
|
||||
baseURL, sup, err = startHTTP(cfg)
|
||||
baseURL, cancel, err = startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
|
||||
// A request with a suspicious Host header should be allowed
|
||||
|
||||
@@ -931,11 +931,11 @@ func TestHostCheck(t *testing.T) {
|
||||
|
||||
cfg = new(mockedConfig)
|
||||
cfg.gui.RawAddress = "[::1]:0"
|
||||
baseURL, sup, err = startHTTP(cfg)
|
||||
baseURL, cancel, err = startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
|
||||
// A normal HTTP get to the localhost-bound service should succeed
|
||||
|
||||
@@ -1026,11 +1026,11 @@ func TestAccessControlAllowOriginHeader(t *testing.T) {
|
||||
const testAPIKey = "foobarbaz"
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.APIKey = testAPIKey
|
||||
baseURL, sup, err := startHTTP(cfg)
|
||||
baseURL, cancel, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
cli := &http.Client{
|
||||
Timeout: time.Second,
|
||||
}
|
||||
@@ -1057,11 +1057,11 @@ func TestOptionsRequest(t *testing.T) {
|
||||
const testAPIKey = "foobarbaz"
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.APIKey = testAPIKey
|
||||
baseURL, sup, err := startHTTP(cfg)
|
||||
baseURL, cancel, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
cli := &http.Client{
|
||||
Timeout: time.Second,
|
||||
}
|
||||
@@ -1093,7 +1093,7 @@ func TestEventMasks(t *testing.T) {
|
||||
cfg := new(mockedConfig)
|
||||
defSub := new(mockedEventSub)
|
||||
diskSub := new(mockedEventSub)
|
||||
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, false).(*service)
|
||||
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, false).(*service)
|
||||
defer os.Remove(token)
|
||||
|
||||
if mask := svc.getEventMask(""); mask != DefaultEventMask {
|
||||
@@ -1253,11 +1253,11 @@ func TestConfigChanges(t *testing.T) {
|
||||
defer os.Remove(tmpFile.Name())
|
||||
w := config.Wrap(tmpFile.Name(), cfg, events.NoopLogger)
|
||||
tmpFile.Close()
|
||||
baseURL, sup, err := startHTTP(w)
|
||||
baseURL, cancel, err := startHTTP(w)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error from getting base URL:", err)
|
||||
}
|
||||
defer sup.Stop()
|
||||
defer cancel()
|
||||
|
||||
cli := &http.Client{
|
||||
Timeout: time.Second,
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/connections"
|
||||
)
|
||||
|
||||
@@ -24,9 +26,7 @@ func (m *mockedConnections) NATType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockedConnections) Serve() {}
|
||||
|
||||
func (m *mockedConnections) Stop() {}
|
||||
func (m *mockedConnections) Serve(ctx context.Context) error { return nil }
|
||||
|
||||
func (m *mockedConnections) ExternalAddresses() []string { return nil }
|
||||
|
||||
|
||||
@@ -17,13 +17,10 @@ type mockedCachingMux struct{}
|
||||
|
||||
// from suture.Service
|
||||
|
||||
func (m *mockedCachingMux) Serve() {
|
||||
func (m *mockedCachingMux) Serve(ctx context.Context) error {
|
||||
select {}
|
||||
}
|
||||
|
||||
func (m *mockedCachingMux) Stop() {
|
||||
}
|
||||
|
||||
// from events.Finder
|
||||
|
||||
func (m *mockedCachingMux) Lookup(ctx context.Context, deviceID protocol.DeviceID) (direct []string, err error) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -124,8 +125,7 @@ func (m *mockedModel) WatchError(folder string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) Serve() {}
|
||||
func (m *mockedModel) Stop() {}
|
||||
func (m *mockedModel) Serve(ctx context.Context) error { return nil }
|
||||
|
||||
func (m *mockedModel) Index(deviceID protocol.DeviceID, folder string, files []protocol.FileInfo) error {
|
||||
return nil
|
||||
@@ -167,9 +167,7 @@ func (m *mockedModel) DBSnapshot(_ string) (*db.Snapshot, error) {
|
||||
|
||||
type mockedFolderSummaryService struct{}
|
||||
|
||||
func (m *mockedFolderSummaryService) Serve() {}
|
||||
|
||||
func (m *mockedFolderSummaryService) Stop() {}
|
||||
func (m *mockedFolderSummaryService) Serve(context.Context) error { return nil }
|
||||
|
||||
func (m *mockedFolderSummaryService) Summary(folder string) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{"mocked": true}, nil
|
||||
|
||||
Reference in New Issue
Block a user