Skip to content
Permalink
Browse files

start an FsWatcher

  • Loading branch information...
plouj committed Feb 22, 2016
1 parent ed30ea4 commit 2da1aefd526ed5f6efdd3abe0a0ac54d20083037
@@ -945,6 +945,7 @@ func defaultConfig(myName string) config.Configuration {
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
defaultFolder.Label = "Default Folder (" + folderID + ")"
defaultFolder.RescanIntervalS = 60
defaultFolder.LongRescanIntervalS = 60 * 60
defaultFolder.MinDiskFreePct = 1
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}
defaultFolder.AutoNormalize = true
@@ -360,6 +360,10 @@ <h4 class="panel-title">
<th><span class="fa fa-fw fa-refresh"></span>&nbsp;<span translate>Rescan Interval</span></th>
<td class="text-right">{{folder.rescanIntervalS}} s</td>
</tr>
<tr ng-if="folder.longRescanIntervalS != 3600">
<th><span class="fa fa-fw fa-refresh"></span>&nbsp;<span translate>LongRescan Interval</span></th>
<td class="text-right">{{folder.longRescanIntervalS}} s</td>
</tr>
<tr ng-if="folder.order != 'random'">
<th><span class="fa fa-fw fa-sort"></span>&nbsp;<span translate>File Pull Order</span></th>
<td class="text-right" ng-switch="folder.order">
@@ -1303,6 +1303,7 @@ angular.module('syncthing.core')
selectedDevices: {},
type: "readwrite",
rescanIntervalS: 60,
longRescanIntervalS: 3600,
minDiskFreePct: 1,
maxConflicts: 10,
order: "random",
@@ -1330,6 +1331,7 @@ angular.module('syncthing.core')
label: folderLabel,
selectedDevices: {},
rescanIntervalS: 60,
longRescanIntervalS: 3600,
minDiskFreePct: 1,
maxConflicts: 10,
order: "random",
@@ -70,6 +70,13 @@
<span translate ng-if="!folderEditor.rescanIntervalS.$valid && folderEditor.rescanIntervalS.$dirty">The rescan interval must be a non-negative number of seconds.</span>
</p>
</div>
<div class="form-group" ng-class="{'has-error': folderEditor.longRescanIntervalS.$invalid && folderEditor.longRescanIntervalS.$dirty}">
<label for="longRescanIntervalS"><span translate>Long Rescan Interval</span> (s)</label>
<input name="longRescanIntervalS" id="longRescanIntervalS" class="form-control" type="number" ng-model="currentFolder.longRescanIntervalS" required min="0">
<p class="help-block">
<span translate ng-if="!folderEditor.longRescanIntervalS.$valid && folderEditor.longRescanIntervalS.$dirty">The rescan interval must be a non-negative number of seconds.</span>
</p>
</div>
<div class="form-group" ng-class="{'has-error': folderEditor.minDiskFreePct.$invalid && folderEditor.minDiskFreePct.$dirty}">
<label for="minDiskFreePct"><span translate>Minimum Free Disk Space</span> (0.0 - 100.0%)</label>
<input name="minDiskFreePct" id="minDiskFreePct" class="form-control" type="number" ng-model="currentFolder.minDiskFreePct" required min="0.0" max="100.0">
@@ -23,6 +23,7 @@ type FolderConfiguration struct {
Type FolderType `xml:"type,attr" json:"type"`
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
LongRescanIntervalS int `xml:"longRescanIntervalS,attr" json:"longRescanIntervalS"`
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
MinDiskFreePct float64 `xml:"minDiskFreePct" json:"minDiskFreePct"`
@@ -131,6 +132,12 @@ func (f *FolderConfiguration) prepare() {
f.RescanIntervalS = 0
}

if f.LongRescanIntervalS > MaxRescanIntervalS {
f.LongRescanIntervalS = MaxRescanIntervalS
} else if f.LongRescanIntervalS < 0 {
f.LongRescanIntervalS = 0
}

if f.Versioning.Params == nil {
f.Versioning.Params = make(map[string]string)
}
@@ -0,0 +1,24 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

package fswatcher

import (
"os"
"strings"

"github.com/syncthing/syncthing/lib/logger"
)

var facilityName = "fswatcher"

var (
l = logger.DefaultLogger.NewFacility(facilityName, "Filesystem event watcher")
)

func init() {
l.SetDebug(facilityName, strings.Contains(os.Getenv("STTRACE"), facilityName) || os.Getenv("STTRACE") == "all")
}
@@ -0,0 +1,19 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

package fswatcher

import (
"syscall"
)

func interpretNotifyWatchError(err error, folder string) error {
if errno, converted := err.(syscall.Errno); converted &&
errno == 24 || errno == 28 {
return WatchesLimitTooLowError(folder)
}
return err
}
@@ -0,0 +1,13 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

// +build !linux

package fswatcher

func interpretNotifyWatchError(err error, folder string) error {
return err
}
@@ -0,0 +1,213 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

package fswatcher

import (
"errors"
"github.com/zillode/notify"
"os"
"path/filepath"
"strings"
"time"

"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/scanner"
)

type FsEvent struct {
path string
}

var Tempnamer scanner.TempNamer

type FsEventsBatch map[string]*FsEvent

type FsWatcher struct {
folderPath string
notifyModelChan chan<- FsEventsBatch
fsEvents FsEventsBatch
fsEventChan <-chan notify.EventInfo
WatchingFs bool
notifyDelay time.Duration
notifyTimer *time.Timer
notifyTimerNeedsReset bool
inProgress map[string]struct{}
}

const (
slowNotifyDelay = time.Duration(60) * time.Second
fastNotifyDelay = time.Duration(500) * time.Millisecond
)

func NewFsWatcher(folderPath string) *FsWatcher {
return &FsWatcher{
folderPath: folderPath,
notifyModelChan: nil,
fsEvents: make(FsEventsBatch),
fsEventChan: nil,
WatchingFs: false,
notifyDelay: fastNotifyDelay,
notifyTimerNeedsReset: false,
inProgress: make(map[string]struct{}),
}
}

func (watcher *FsWatcher) StartWatchingFilesystem() (<-chan FsEventsBatch, error) {
fsEventChan, err := setupNotifications(watcher.folderPath)
if err == nil {
watcher.WatchingFs = true
watcher.fsEventChan = fsEventChan
go watcher.watchFilesystem()
}
notifyModelChan := make(chan FsEventsBatch)
watcher.notifyModelChan = notifyModelChan
return notifyModelChan, err
}

var maxFiles = 512

func setupNotifications(path string) (chan notify.EventInfo, error) {
c := make(chan notify.EventInfo, maxFiles)
if err := notify.Watch(path, c, notify.All); err != nil {
notify.Stop(c)
close(c)
return nil, interpretNotifyWatchError(err, path)
}
l.Debugf("Setup filesystem notification for %s", path)
return c, nil
}

func (watcher *FsWatcher) watchFilesystem() {
watcher.notifyTimer = time.NewTimer(watcher.notifyDelay)
defer watcher.notifyTimer.Stop()
inProgressItemSubscription := events.Default.Subscribe(
events.ItemStarted | events.ItemFinished)
for {
watcher.resetNotifyTimerIfNeeded()
select {
case event, _ := <-watcher.fsEventChan:
watcher.speedUpNotifyTimer()
watcher.storeFsEvent(event)
case <-watcher.notifyTimer.C:
watcher.actOnTimer()
case event := <-inProgressItemSubscription.C():
watcher.updateInProgressSet(event)
}
}
}

func (watcher *FsWatcher) newFsEvent(eventPath string) *FsEvent {
if isSubpath(eventPath, watcher.folderPath) {
path, _ := filepath.Rel(watcher.folderPath, eventPath)
if !shouldIgnore(path) {
return &FsEvent{path}
}
}
return nil
}

func isSubpath(path string, folderPath string) bool {
if len(path) > 1 && os.IsPathSeparator(path[len(path)-1]) {
path = path[0 : len(path)-1]
}
if len(folderPath) > 1 && os.IsPathSeparator(folderPath[len(folderPath)-1]) {
folderPath = folderPath[0 : len(folderPath)-1]
}
return strings.HasPrefix(path, folderPath)
}

func (watcher *FsWatcher) resetNotifyTimerIfNeeded() {
if watcher.notifyTimerNeedsReset {
l.Debugf("Resetting notifyTimer to %#v\n", watcher.notifyDelay)
watcher.notifyTimer.Reset(watcher.notifyDelay)
watcher.notifyTimerNeedsReset = false
}
}

func (watcher *FsWatcher) speedUpNotifyTimer() {
if watcher.notifyDelay != fastNotifyDelay {
watcher.notifyDelay = fastNotifyDelay
l.Debugf("Speeding up notifyTimer to %#v\n", fastNotifyDelay)
watcher.notifyTimerNeedsReset = true
}
}

func (watcher *FsWatcher) slowDownNotifyTimer() {
if watcher.notifyDelay != slowNotifyDelay {
watcher.notifyDelay = slowNotifyDelay
l.Debugf("Slowing down notifyTimer to %#v\n", watcher.notifyDelay)
watcher.notifyTimerNeedsReset = true
}
}

func (watcher *FsWatcher) storeFsEvent(event notify.EventInfo) {
newEvent := watcher.newFsEvent(event.Path())
if newEvent != nil {
if watcher.pathInProgress(newEvent.path) {
l.Debugf("Skipping notification for finished path: %s\n",
newEvent.path)
} else {
watcher.fsEvents[newEvent.path] = newEvent
}
}
}

func (watcher *FsWatcher) actOnTimer() {
watcher.notifyTimerNeedsReset = true
if len(watcher.fsEvents) > 0 {
l.Debugf("Notifying about %d fs events\n", len(watcher.fsEvents))
watcher.notifyModelChan <- watcher.fsEvents
} else {
watcher.slowDownNotifyTimer()
}
watcher.fsEvents = make(FsEventsBatch)
}

func (watcher *FsWatcher) events() []*FsEvent {
list := make([]*FsEvent, 0, len(watcher.fsEvents))
for _, event := range watcher.fsEvents {
list = append(list, event)
}
return list
}

func (watcher *FsWatcher) updateInProgressSet(event events.Event) {
if event.Type == events.ItemStarted {
path := event.Data.(map[string]string)["item"]
watcher.inProgress[path] = struct{}{}
} else if event.Type == events.ItemFinished {
path := event.Data.(map[string]interface{})["item"].(string)
delete(watcher.inProgress, path)
}
}

func shouldIgnore(path string) bool {
return strings.Contains(path, ".syncthing.") &&
strings.HasSuffix(path, ".tmp") ||
scanner.IsIgnoredPath(path, nil) ||
Tempnamer.IsTemporary(path)
}

func (watcher *FsWatcher) pathInProgress(path string) bool {
_, exists := watcher.inProgress[path]
return exists
}

func (batch FsEventsBatch) GetPaths() []string {
var paths []string
for _, event := range batch {
paths = append(paths, event.path)
}
return paths
}

func WatchesLimitTooLowError(folder string) error {
return errors.New("Failed to install inotify handler for " +
folder +
". Please increase inotify limits," +
" see http://bit.ly/1PxkdUC for more information.")
}
@@ -0,0 +1,36 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

package fswatcher

import (
"errors"
"syscall"
"testing"
)

func TestErrorInotifyInterpretation(t *testing.T) {
msg := "Failed to install inotify handler for test-folder." +
" Please increase inotify limits," +
" see http://bit.ly/1PxkdUC for more information."
var errTooManyFiles syscall.Errno = 24
var errNoSpace syscall.Errno = 28
err := interpretNotifyWatchError(errTooManyFiles, "test-folder")
if err.Error() != msg {
t.Errorf("Expected error about inotify limits, but got: %#v",
err)
}
err = interpretNotifyWatchError(errNoSpace, "test-folder")
if err.Error() != msg {
t.Errorf("Expected error about inotify limits, but got: %#v",
err)
}
err = interpretNotifyWatchError(
errors.New("Another error"), "test-folder")
if err.Error() != "Another error" {
t.Errorf("Unexpected error: %#v", err)
}
}

0 comments on commit 2da1aef

Please sign in to comment.
You can’t perform that action at this time.