diff --git a/model/blocks.go b/blocks.go similarity index 99% rename from model/blocks.go rename to blocks.go index 57b96e4f..7f2c9081 100644 --- a/model/blocks.go +++ b/blocks.go @@ -1,4 +1,4 @@ -package model +package main import ( "bytes" diff --git a/model/blocks_test.go b/blocks_test.go similarity index 99% rename from model/blocks_test.go rename to blocks_test.go index a3fcd437..be791493 100644 --- a/model/blocks_test.go +++ b/blocks_test.go @@ -1,4 +1,4 @@ -package model +package main import ( "bytes" diff --git a/model/filemonitor.go b/filemonitor.go similarity index 99% rename from model/filemonitor.go rename to filemonitor.go index 76ac2cab..2aa7df8e 100644 --- a/model/filemonitor.go +++ b/filemonitor.go @@ -1,4 +1,4 @@ -package model +package main import ( "bytes" diff --git a/model/filequeue.go b/filequeue.go similarity index 99% rename from model/filequeue.go rename to filequeue.go index 77a217d5..5835de32 100644 --- a/model/filequeue.go +++ b/filequeue.go @@ -1,4 +1,4 @@ -package model +package main import ( "log" diff --git a/model/filequeue_test.go b/filequeue_test.go similarity index 99% rename from model/filequeue_test.go rename to filequeue_test.go index 5f867029..289c69a9 100644 --- a/model/filequeue_test.go +++ b/filequeue_test.go @@ -1,4 +1,4 @@ -package model +package main import ( "reflect" diff --git a/gui.go b/gui.go index f7fc6751..a584f886 100644 --- a/gui.go +++ b/gui.go @@ -2,18 +2,28 @@ package main import ( "encoding/json" + "io/ioutil" "log" "net/http" "runtime" "sync" + "time" - "github.com/calmh/syncthing/model" "github.com/codegangsta/martini" ) -var configInSync = true +type guiError struct { + Time time.Time + Error string +} -func startGUI(addr string, m *model.Model) { +var ( + configInSync = true + guiErrors = []guiError{} + guiErrorsMut sync.Mutex +) + +func startGUI(addr string, m *Model) { router := martini.NewRouter() router.Get("/", getRoot) router.Get("/rest/version", restGetVersion) @@ -23,9 +33,11 @@ func startGUI(addr string, m *model.Model) { router.Get("/rest/config/sync", restGetConfigInSync) router.Get("/rest/need", restGetNeed) router.Get("/rest/system", restGetSystem) + router.Get("/rest/errors", restGetErrors) router.Post("/rest/config", restPostConfig) router.Post("/rest/restart", restPostRestart) + router.Post("/rest/error", restPostError) go func() { mr := martini.New() @@ -48,7 +60,7 @@ func restGetVersion() string { return Version } -func restGetModel(m *model.Model, w http.ResponseWriter) { +func restGetModel(m *Model, w http.ResponseWriter) { var res = make(map[string]interface{}) globalFiles, globalDeleted, globalBytes := m.GlobalSize() @@ -67,7 +79,7 @@ func restGetModel(m *model.Model, w http.ResponseWriter) { json.NewEncoder(w).Encode(res) } -func restGetConnections(m *model.Model, w http.ResponseWriter) { +func restGetConnections(m *Model, w http.ResponseWriter) { var res = m.ConnectionStats() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(res) @@ -95,7 +107,7 @@ func restPostRestart(req *http.Request) { restart() } -type guiFile model.File +type guiFile File func (f guiFile) MarshalJSON() ([]byte, error) { type t struct { @@ -104,11 +116,11 @@ func (f guiFile) MarshalJSON() ([]byte, error) { } return json.Marshal(t{ Name: f.Name, - Size: model.File(f).Size(), + Size: File(f).Size(), }) } -func restGetNeed(m *model.Model, w http.ResponseWriter) { +func restGetNeed(m *Model, w http.ResponseWriter) { files, _ := m.NeedFiles() gfs := make([]guiFile, len(files)) for i, f := range files { @@ -137,3 +149,24 @@ func restGetSystem(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(res) } + +func restGetErrors(w http.ResponseWriter) { + guiErrorsMut.Lock() + json.NewEncoder(w).Encode(guiErrors) + guiErrorsMut.Unlock() +} + +func restPostError(req *http.Request) { + bs, _ := ioutil.ReadAll(req.Body) + req.Body.Close() + showGuiError(string(bs)) +} + +func showGuiError(err string) { + guiErrorsMut.Lock() + guiErrors = append(guiErrors, guiError{time.Now(), err}) + if len(guiErrors) > 5 { + guiErrors = guiErrors[len(guiErrors)-5:] + } + guiErrorsMut.Unlock() +} diff --git a/gui/app.js b/gui/app.js index 12795ac8..7cab3a08 100644 --- a/gui/app.js +++ b/gui/app.js @@ -14,6 +14,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) { $scope.myID = ''; $scope.nodes = []; $scope.configInSync = true; + $scope.errors = []; + $scope.seenError = ''; // Strings before bools look better $scope.settings = [ @@ -131,6 +133,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) { }); $scope.need = data; }); + $http.get('/rest/errors').success(function (data) { + $scope.errors = data; + }); }; $scope.nodeIcon = function (nodeCfg) { @@ -234,6 +239,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) { $scope.nodes = newNodes; $scope.config.Repositories[0].Nodes = newNodes; + $scope.configInSync = false; $http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}}); }; @@ -287,6 +293,29 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) { } }; + $scope.errorList = function () { + var errors = []; + for (var i = 0; i < $scope.errors.length; i++) { + var e = $scope.errors[i]; + if (e.Time > $scope.seenError) { + errors.push(e); + } + } + return errors; + }; + + $scope.clearErrors = function () { + $scope.seenError = $scope.errors[$scope.errors.length - 1].Time; + }; + + $scope.friendlyNodes = function (str) { + for (var i = 0; i < $scope.nodes.length; i++) { + var cfg = $scope.nodes[i]; + str = str.replace(cfg.NodeID, $scope.nodeName(cfg)); + } + return str; + }; + $scope.refresh(); setInterval($scope.refresh, 10000); }); diff --git a/gui/index.html b/gui/index.html index 4dac829b..350d8c45 100644 --- a/gui/index.html +++ b/gui/index.html @@ -55,6 +55,12 @@ thead tr th {
+
+

{{err.Time | date:"hh:mm:ss.sss"}}: {{friendlyNodes(err.Error)}}

+ +
+
+

Cluster

diff --git a/logger.go b/logger.go index 1608b3bc..ee61bb5b 100644 --- a/logger.go +++ b/logger.go @@ -41,11 +41,13 @@ func okf(format string, vals ...interface{}) { func warnln(vals ...interface{}) { s := fmt.Sprintln(vals...) + showGuiError(s) logger.Output(2, "WARNING: "+s) } func warnf(format string, vals ...interface{}) { s := fmt.Sprintf(format, vals...) + showGuiError(s) logger.Output(2, "WARNING: "+s) } diff --git a/main.go b/main.go index fdf6de44..876d023f 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,6 @@ import ( "github.com/calmh/ini" "github.com/calmh/syncthing/discover" - "github.com/calmh/syncthing/model" "github.com/calmh/syncthing/protocol" ) @@ -181,7 +180,7 @@ func main() { } ensureDir(dir, -1) - m := model.NewModel(dir, cfg.Options.MaxChangeKbps*1000) + m := NewModel(dir, cfg.Options.MaxChangeKbps*1000) for _, t := range strings.Split(trace, ",") { m.Trace(t) } @@ -250,7 +249,7 @@ func main() { okln("Ready to synchronize (read only; no external updates accepted)") } - // Periodically scan the repository and update the local model. + // Periodically scan the repository and update the local // XXX: Should use some fsnotify mechanism. go func() { td := time.Duration(cfg.Options.RescanIntervalS) * time.Second @@ -328,9 +327,9 @@ func saveConfig() { saveConfigCh <- struct{}{} } -func printStatsLoop(m *model.Model) { +func printStatsLoop(m *Model) { var lastUpdated int64 - var lastStats = make(map[string]model.ConnectionInfo) + var lastStats = make(map[string]ConnectionInfo) for { time.Sleep(60 * time.Second) @@ -359,7 +358,7 @@ func printStatsLoop(m *model.Model) { } } -func listen(myID string, addr string, m *model.Model, tlsCfg *tls.Config, connOpts map[string]string) { +func listen(myID string, addr string, m *Model, tlsCfg *tls.Config, connOpts map[string]string) { if strings.Contains(trace, "connect") { debugln("NET: Listening on", addr) } @@ -435,7 +434,7 @@ func discovery(addr string) *discover.Discoverer { return disc } -func connect(myID string, disc *discover.Discoverer, m *model.Model, tlsCfg *tls.Config, connOpts map[string]string) { +func connect(myID string, disc *discover.Discoverer, m *Model, tlsCfg *tls.Config, connOpts map[string]string) { for { nextNode: for _, nodeCfg := range cfg.Repositories[0].Nodes { @@ -484,13 +483,13 @@ func connect(myID string, disc *discover.Discoverer, m *model.Model, tlsCfg *tls } } -func updateLocalModel(m *model.Model) { +func updateLocalModel(m *Model) { files, _ := m.Walk(cfg.Options.FollowSymlinks) m.ReplaceLocal(files) saveIndex(m) } -func saveIndex(m *model.Model) { +func saveIndex(m *Model) { name := m.RepoID() + ".idx.gz" fullName := path.Join(confDir, name) idxf, err := os.Create(fullName + ".tmp") @@ -506,7 +505,7 @@ func saveIndex(m *model.Model) { os.Rename(fullName+".tmp", fullName) } -func loadIndex(m *model.Model) { +func loadIndex(m *Model) { name := m.RepoID() + ".idx.gz" idxf, err := os.Open(path.Join(confDir, name)) if err != nil { diff --git a/model/model.go b/model.go similarity index 92% rename from model/model.go rename to model.go index 65bbb7c1..059200eb 100644 --- a/model/model.go +++ b/model.go @@ -1,11 +1,10 @@ -package model +package main import ( "crypto/sha1" "errors" "fmt" "io" - "log" "net" "os" "path" @@ -268,7 +267,7 @@ func (m *Model) Index(nodeID string, fs []protocol.FileInfo) { defer m.imut.Unlock() if m.trace["net"] { - log.Printf("DEBUG: NET IDX(in): %s: %d files", nodeID, len(fs)) + debugf("NET IDX(in): %s: %d files", nodeID, len(fs)) } repo := make(map[string]File) @@ -296,13 +295,13 @@ func (m *Model) IndexUpdate(nodeID string, fs []protocol.FileInfo) { defer m.imut.Unlock() if m.trace["net"] { - log.Printf("DEBUG: NET IDXUP(in): %s: %d files", nodeID, len(files)) + debugf("NET IDXUP(in): %s: %d files", nodeID, len(files)) } m.rmut.Lock() repo, ok := m.remote[nodeID] if !ok { - log.Printf("WARNING: Index update from node %s that does not have an index", nodeID) + warnf("Index update from node %s that does not have an index", nodeID) m.rmut.Unlock() return } @@ -322,11 +321,11 @@ func (m *Model) indexUpdate(repo map[string]File, f File) { if f.Flags&protocol.FlagDeleted != 0 { flagComment = " (deleted)" } - log.Printf("DEBUG: IDX(in): %q m=%d f=%o%s v=%d (%d blocks)", f.Name, f.Modified, f.Flags, flagComment, f.Version, len(f.Blocks)) + debugf("IDX(in): %q m=%d f=%o%s v=%d (%d blocks)", f.Name, f.Modified, f.Flags, flagComment, f.Version, len(f.Blocks)) } if extraFlags := f.Flags &^ (protocol.FlagInvalid | protocol.FlagDeleted | 0xfff); extraFlags != 0 { - log.Printf("WARNING: IDX(in): Unknown flags 0x%x in index record %+v", extraFlags, f) + warnf("IDX(in): Unknown flags 0x%x in index record %+v", extraFlags, f) return } @@ -337,10 +336,10 @@ func (m *Model) indexUpdate(repo map[string]File, f File) { // Implements the protocol.Model interface. func (m *Model) Close(node string, err error) { if m.trace["net"] { - log.Printf("DEBUG: NET: %s: %v", node, err) + debugf("NET: %s: %v", node, err) } if err == protocol.ErrClusterHash { - log.Printf("WARNING: Connection to %s closed due to mismatched cluster hash. Ensure that the configured cluster members are identical on both nodes.", node) + warnf("Connection to %s closed due to mismatched cluster hash. Ensure that the configured cluster members are identical on both nodes.", node) } m.fq.RemoveAvailable(node) @@ -377,7 +376,7 @@ func (m *Model) Request(nodeID, name string, offset int64, size uint32, hash []b m.gmut.RUnlock() if !localOk || !globalOk { - log.Printf("SECURITY (nonexistent file) REQ(in): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash) + warnf("SECURITY (nonexistent file) REQ(in): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash) return nil, ErrNoSuchFile } if lf.Flags&protocol.FlagInvalid != 0 { @@ -385,7 +384,7 @@ func (m *Model) Request(nodeID, name string, offset int64, size uint32, hash []b } if m.trace["net"] && nodeID != "" { - log.Printf("DEBUG: NET REQ(in): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash) + debugf("NET REQ(in): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash) } fn := path.Join(m.dir, name) fd, err := os.Open(fn) // XXX: Inefficient, should cache fd? @@ -502,13 +501,13 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn Connection) { i := i go func() { if m.trace["pull"] { - log.Println("DEBUG: PULL: Starting", nodeID, i) + debugln("PULL: Starting", nodeID, i) } for { m.pmut.RLock() if _, ok := m.protoConn[nodeID]; !ok { if m.trace["pull"] { - log.Println("DEBUG: PULL: Exiting", nodeID, i) + debugln("PULL: Exiting", nodeID, i) } m.pmut.RUnlock() return @@ -518,7 +517,7 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn Connection) { qb, ok := m.fq.Get(nodeID) if ok { if m.trace["pull"] { - log.Println("DEBUG: PULL: Request", nodeID, i, qb.name, qb.block.Offset) + debugln("PULL: Request", nodeID, i, qb.name, qb.block.Offset) } data, _ := protoConn.Request(qb.name, qb.block.Offset, qb.block.Size, qb.block.Hash) m.fq.Done(qb.name, qb.block.Offset, data) @@ -544,7 +543,7 @@ func (m *Model) ProtocolIndex() []protocol.FileInfo { if mf.Flags&protocol.FlagDeleted != 0 { flagComment = " (deleted)" } - log.Printf("DEBUG: IDX(out): %q m=%d f=%o%s v=%d (%d blocks)", mf.Name, mf.Modified, mf.Flags, flagComment, mf.Version, len(mf.Blocks)) + debugf("IDX(out): %q m=%d f=%o%s v=%d (%d blocks)", mf.Name, mf.Modified, mf.Flags, flagComment, mf.Version, len(mf.Blocks)) } index = append(index, mf) } @@ -563,7 +562,7 @@ func (m *Model) requestGlobal(nodeID, name string, offset int64, size uint32, ha } if m.trace["net"] { - log.Printf("DEBUG: NET REQ(out): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash) + debugf("NET REQ(out): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash) } return nc.Request(name, offset, size, hash) @@ -591,7 +590,7 @@ func (m *Model) broadcastIndexLoop() { for _, node := range m.protoConn { node := node if m.trace["net"] { - log.Printf("DEBUG: NET IDX(out/loop): %s: %d files", node.ID(), len(idx)) + debugf("NET IDX(out/loop): %s: %d files", node.ID(), len(idx)) } go func() { node.Index(idx) @@ -803,7 +802,7 @@ func (m *Model) recomputeNeedForFile(gf File, toAdd []addOrder, toDelete []File) return toAdd, toDelete } if m.trace["need"] { - log.Printf("DEBUG: NEED: lf:%v gf:%v", lf, gf) + debugf("NEED: lf:%v gf:%v", lf, gf) } if gf.Flags&protocol.FlagDeleted != 0 { @@ -845,12 +844,12 @@ func (m *Model) WhoHas(name string) []string { func (m *Model) deleteLoop() { for file := range m.dq { if m.trace["file"] { - log.Println("DEBUG: FILE: Delete", file.Name) + debugln("FILE: Delete", file.Name) } path := path.Clean(path.Join(m.dir, file.Name)) err := os.Remove(path) if err != nil { - log.Printf("WARNING: %s: %v", file.Name, err) + warnf("%s: %v", file.Name, err) } m.updateLocal(file) diff --git a/model/model_test.go b/model_test.go similarity index 99% rename from model/model_test.go rename to model_test.go index a61f3ec5..ef30b59d 100644 --- a/model/model_test.go +++ b/model_test.go @@ -1,4 +1,4 @@ -package model +package main import ( "bytes" diff --git a/model/suppressor.go b/suppressor.go similarity index 98% rename from model/suppressor.go rename to suppressor.go index 0eb43963..5690207f 100644 --- a/model/suppressor.go +++ b/suppressor.go @@ -1,4 +1,4 @@ -package model +package main import ( "sync" diff --git a/model/suppressor_test.go b/suppressor_test.go similarity index 99% rename from model/suppressor_test.go rename to suppressor_test.go index 58fc370f..26cc5e3b 100644 --- a/model/suppressor_test.go +++ b/suppressor_test.go @@ -1,4 +1,4 @@ -package model +package main import ( "testing" diff --git a/model/testdata/.stignore b/testdata/.stignore similarity index 100% rename from model/testdata/.stignore rename to testdata/.stignore diff --git a/model/testdata/bar b/testdata/bar similarity index 100% rename from model/testdata/bar rename to testdata/bar diff --git a/model/testdata/baz/quux b/testdata/baz/quux similarity index 100% rename from model/testdata/baz/quux rename to testdata/baz/quux diff --git a/model/testdata/empty b/testdata/empty similarity index 100% rename from model/testdata/empty rename to testdata/empty diff --git a/model/testdata/foo b/testdata/foo similarity index 100% rename from model/testdata/foo rename to testdata/foo diff --git a/model/walk.go b/walk.go similarity index 99% rename from model/walk.go rename to walk.go index 1255bf40..23cd985c 100644 --- a/model/walk.go +++ b/walk.go @@ -1,4 +1,4 @@ -package model +package main import ( "bytes" diff --git a/model/walk_test.go b/walk_test.go similarity index 99% rename from model/walk_test.go rename to walk_test.go index 26353946..43707e50 100644 --- a/model/walk_test.go +++ b/walk_test.go @@ -1,4 +1,4 @@ -package model +package main import ( "fmt"