diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index cbff1132..c992edae 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -112,6 +112,7 @@ type modelIntf interface { State(folder string) (string, time.Time, error) UsageReportingStats(version int, preview bool) map[string]interface{} PullErrors(folder string) ([]model.FileError, error) + WatchError(folder string) error } type configIntf interface { @@ -733,6 +734,11 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]inter } } + err = m.WatchError(folder) + if err != nil { + res["watchError"] = err.Error() + } + return res, nil } diff --git a/cmd/syncthing/mocked_model_test.go b/cmd/syncthing/mocked_model_test.go index b9c507a1..13aed6db 100644 --- a/cmd/syncthing/mocked_model_test.go +++ b/cmd/syncthing/mocked_model_test.go @@ -136,3 +136,7 @@ func (m *mockedModel) UsageReportingStats(version int, preview bool) map[string] func (m *mockedModel) PullErrors(folder string) ([]model.FileError, error) { return nil, nil } + +func (m *mockedModel) WatchError(folder string) error { + return nil +} diff --git a/gui/default/assets/lang/lang-en.json b/gui/default/assets/lang/lang-en.json index 6fd9e415..27839293 100644 --- a/gui/default/assets/lang/lang-en.json +++ b/gui/default/assets/lang/lang-en.json @@ -65,6 +65,7 @@ "Device that last modified the item": "Device that last modified the item", "Devices": "Devices", "Disabled": "Disabled", + "Disabled periodic scanning": "Disabled periodic scanning", "Disconnected": "Disconnected", "Discovered": "Discovered", "Discovery": "Discovery", @@ -89,6 +90,7 @@ "Error": "Error", "External File Versioning": "External File Versioning", "Failed Items": "Failed Items", + "Failed to setup, retrying": "Failed to setup, retrying", "Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.", "File Pull Order": "File Pull Order", "File Versioning": "File Versioning", @@ -183,6 +185,7 @@ "Pause": "Pause", "Pause All": "Pause All", "Paused": "Paused", + "Periodic scan every": "Periodic scan every", "Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.", "Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.", "Please wait": "Please wait", @@ -205,6 +208,7 @@ "Rescan": "Rescan", "Rescan All": "Rescan All", "Rescan Interval": "Rescan Interval", + "Rescans": "Rescans", "Restart": "Restart", "Restart Needed": "Restart Needed", "Restarting": "Restarting", @@ -213,6 +217,7 @@ "Resume": "Resume", "Resume All": "Resume All", "Reused": "Reused", + "Running": "Running", "Save": "Save", "Scan Time Remaining": "Scan Time Remaining", "Scanning": "Scanning", diff --git a/gui/default/index.html b/gui/default/index.html index 67035bfa..803d9303 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -371,16 +371,35 @@ Yes - -  Filesystem Notifications + +  Rescans - Yes + +  {{folder.rescanIntervalS | duration}}  + Disabled + + + {{folder.rescanIntervalS | duration}}  +  Running + + + {{folder.rescanIntervalS | duration}}  +  Failed to setup, retrying + + +  Disabled  +  Disabled + + +  Disabled  +  Running + + +  Disabled  +  Failed to setup, retrying + - -  Rescan Interval - {{folder.rescanIntervalS}} s -  File Pull Order diff --git a/lib/model/folder.go b/lib/model/folder.go index 4206b649..52c0d0d2 100644 --- a/lib/model/folder.go +++ b/lib/model/folder.go @@ -8,12 +8,17 @@ package model import ( "context" + "errors" "time" "github.com/syncthing/syncthing/lib/config" + "github.com/syncthing/syncthing/lib/ignore" + "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/watchaggregator" ) +var errWatchNotStarted error = errors.New("not started") + type folder struct { stateTracker config.FolderConfiguration @@ -26,6 +31,8 @@ type folder struct { watchCancel context.CancelFunc watchChan chan []string restartWatchChan chan struct{} + watchErr error + watchErrMut sync.Mutex } func newFolder(model *Model, cfg config.FolderConfiguration) folder { @@ -41,6 +48,8 @@ func newFolder(model *Model, cfg config.FolderConfiguration) folder { model: model, initialScanFinished: make(chan struct{}), watchCancel: func() {}, + watchErr: errWatchNotStarted, + watchErrMut: sync.NewMutex(), } } @@ -127,28 +136,22 @@ func (f *folder) scanTimerFired() { f.scan.Reschedule() } -func (f *folder) startWatch() { - ctx, cancel := context.WithCancel(f.ctx) - f.model.fmut.RLock() - ignores := f.model.folderIgnores[f.folderID] - f.model.fmut.RUnlock() - eventChan, err := f.Filesystem().Watch(".", ignores, ctx, f.IgnorePerms) - if err != nil { - l.Warnf("Failed to start filesystem watcher for folder %s: %v", f.Description(), err) - } else { - f.watchChan = make(chan []string) - f.watchCancel = cancel - watchaggregator.Aggregate(eventChan, f.watchChan, f.FolderConfiguration, f.model.cfg, ctx) - l.Infoln("Started filesystem watcher for folder", f.Description()) - } +func (f *folder) WatchError() error { + f.watchErrMut.Lock() + defer f.watchErrMut.Unlock() + return f.watchErr } -func (f *folder) restartWatch() { +// stopWatch immediately aborts watching and may be called asynchronously +func (f *folder) stopWatch() { f.watchCancel() - f.startWatch() - f.Scan(nil) + f.watchErrMut.Lock() + f.watchErr = errWatchNotStarted + f.watchErrMut.Unlock() } +// scheduleWatchRestart makes sure watching is restarted from the main for loop +// in a folder's Serve and thus may be called asynchronously (e.g. when ignores change). func (f *folder) scheduleWatchRestart() { select { case f.restartWatchChan <- struct{}{}: @@ -159,6 +162,56 @@ func (f *folder) scheduleWatchRestart() { } } +// restartWatch should only ever be called synchronously. If you want to use +// this asynchronously, you should probably use scheduleWatchRestart instead. +func (f *folder) restartWatch() { + f.stopWatch() + f.startWatch() + f.Scan(nil) +} + +// startWatch should only ever be called synchronously. If you want to use +// this asynchronously, you should probably use scheduleWatchRestart instead. +func (f *folder) startWatch() { + ctx, cancel := context.WithCancel(f.ctx) + f.model.fmut.RLock() + ignores := f.model.folderIgnores[f.folderID] + f.model.fmut.RUnlock() + f.watchChan = make(chan []string) + f.watchCancel = cancel + go f.startWatchAsync(ctx, ignores) +} + +// startWatchAsync tries to start the filesystem watching and retries every minute on failure. +// It is a convenience function that should not be used except in startWatch. +func (f *folder) startWatchAsync(ctx context.Context, ignores *ignore.Matcher) { + timer := time.NewTimer(0) + for { + select { + case <-timer.C: + eventChan, err := f.Filesystem().Watch(".", ignores, ctx, f.IgnorePerms) + f.watchErrMut.Lock() + prevErr := f.watchErr + f.watchErr = err + f.watchErrMut.Unlock() + if err != nil { + if prevErr == errWatchNotStarted { + l.Warnf("Failed to start filesystem watcher for folder %s: %v", f.Description(), err) + } else { + l.Debugf("Failed to start filesystem watcher for folder %s again: %v", f.Description(), err) + } + timer.Reset(time.Minute) + continue + } + watchaggregator.Aggregate(eventChan, f.watchChan, f.FolderConfiguration, f.model.cfg, ctx) + l.Debugln("Started filesystem watcher for folder", f.Description()) + return + case <-ctx.Done(): + return + } + } +} + func (f *folder) setError(err error) { _, _, oldErr := f.getState() if (err != nil && oldErr != nil && oldErr.Error() == err.Error()) || (err == nil && oldErr == nil) { @@ -177,7 +230,7 @@ func (f *folder) setError(err error) { if f.FSWatcherEnabled { if err != nil { - f.watchCancel() + f.stopWatch() } else { f.scheduleWatchRestart() } diff --git a/lib/model/model.go b/lib/model/model.go index 47696937..304f33e4 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -65,6 +65,7 @@ type service interface { Stop() CheckHealth() error PullErrors() []FileError + WatchError() error getState() (folderState, time.Time, error) setState(state folderState) @@ -2213,6 +2214,15 @@ func (m *Model) PullErrors(folder string) ([]FileError, error) { return m.folderRunners[folder].PullErrors(), nil } +func (m *Model) WatchError(folder string) error { + m.fmut.RLock() + defer m.fmut.RUnlock() + if err := m.checkFolderRunningLocked(folder); err != nil { + return err + } + return m.folderRunners[folder].WatchError() +} + func (m *Model) Override(folder string) { m.fmut.RLock() fs, ok := m.folderFiles[folder]