1
0
Эх сурвалжийг харах

Show warnings in GUI (fixes #66)

Jakob Borg 11 жил өмнө
parent
commit
91d5c4a1ae
21 өөрчлөгдсөн 116 нэмэгдсэн , 48 устгасан
  1. 1 1
      blocks.go
  2. 1 1
      blocks_test.go
  3. 1 1
      filemonitor.go
  4. 1 1
      filequeue.go
  5. 1 1
      filequeue_test.go
  6. 41 8
      gui.go
  7. 29 0
      gui/app.js
  8. 6 0
      gui/index.html
  9. 2 0
      logger.go
  10. 9 10
      main.go
  11. 19 20
      model.go
  12. 1 1
      model_test.go
  13. 1 1
      suppressor.go
  14. 1 1
      suppressor_test.go
  15. 0 0
      testdata/.stignore
  16. 0 0
      testdata/bar
  17. 0 0
      testdata/baz/quux
  18. 0 0
      testdata/empty
  19. 0 0
      testdata/foo
  20. 1 1
      walk.go
  21. 1 1
      walk_test.go

+ 1 - 1
model/blocks.go → blocks.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"bytes"

+ 1 - 1
model/blocks_test.go → blocks_test.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"bytes"

+ 1 - 1
model/filemonitor.go → filemonitor.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"bytes"

+ 1 - 1
model/filequeue.go → filequeue.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"log"

+ 1 - 1
model/filequeue_test.go → filequeue_test.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"reflect"

+ 41 - 8
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
+}
+
+var (
+	configInSync = true
+	guiErrors    = []guiError{}
+	guiErrorsMut sync.Mutex
+)
 
-func startGUI(addr string, m *model.Model) {
+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()
+}

+ 29 - 0
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);
 });

+ 6 - 0
gui/index.html

@@ -55,6 +55,12 @@ thead tr th {
 <div class="container">
     <div class="row">
         <div class="col-md-12">
+            <div ng-if="errorList().length > 0" class="alert alert-warning">
+                <p ng-repeat="err in errorList()"><small>{{err.Time | date:"hh:mm:ss.sss"}}:</small> {{friendlyNodes(err.Error)}}</p>
+                    <button type="button" class="pull-right btn btn-warning" ng-click="clearErrors()">OK</button>
+            <div class="clearfix"></div>
+            </div>
+
             <div class="panel panel-info">
                 <div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
                 <table class="table table-condensed">

+ 2 - 0
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)
 }
 

+ 9 - 10
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 {

+ 19 - 20
model/model.go → 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 != "<local>" {
-		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)

+ 1 - 1
model/model_test.go → model_test.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"bytes"

+ 1 - 1
model/suppressor.go → suppressor.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"sync"

+ 1 - 1
model/suppressor_test.go → suppressor_test.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"testing"

+ 0 - 0
model/testdata/.stignore → testdata/.stignore


+ 0 - 0
model/testdata/bar → testdata/bar


+ 0 - 0
model/testdata/baz/quux → testdata/baz/quux


+ 0 - 0
model/testdata/empty → testdata/empty


+ 0 - 0
model/testdata/foo → testdata/foo


+ 1 - 1
model/walk.go → walk.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"bytes"

+ 1 - 1
model/walk_test.go → walk_test.go

@@ -1,4 +1,4 @@
-package model
+package main
 
 import (
 	"fmt"