Auto start Syncthing and Syncthing-inotify.
authorSebastien Douheret <sebastien.douheret@iot.bzh>
Tue, 16 May 2017 20:51:32 +0000 (22:51 +0200)
committerSebastien Douheret <sebastien.douheret@iot.bzh>
Wed, 17 May 2017 12:04:32 +0000 (14:04 +0200)
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
17 files changed:
.vscode/settings.json
Makefile
README.md
config.json.in
lib/apiv1/apiv1.go
lib/apiv1/config.go
lib/apiv1/exec.go
lib/apiv1/folders.go
lib/apiv1/make.go
lib/model/folder.go [new file with mode: 0644]
lib/session/session.go
lib/syncthing/st.go
lib/webserver/server.go [moved from lib/xdsserver/server.go with 93% similarity]
lib/xdsconfig/config.go
lib/xdsconfig/fileconfig.go
lib/xdsconfig/folderconfig.go
main.go

index a90ab0d..a873478 100644 (file)
@@ -17,6 +17,6 @@
     "cSpell.words": [
         "apiv", "gonic", "devel", "csrffound", "Syncthing", "STID",
         "ISTCONFIG", "socketio", "ldflags", "SThg", "Intf", "dismissible",
-        "rpath", "WSID", "sess", "IXDS", "xdsconfig", "xdsserver"
+        "rpath", "WSID", "sess", "IXDS", "xdsconfig", "xdsserver", "mfolder"
     ]
 }
\ No newline at end of file
index 247d454..e3cc99b 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -33,9 +33,11 @@ mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
 ROOT_SRCDIR := $(patsubst %/,%,$(dir $(mkfile_path)))
 ROOT_GOPRJ := $(abspath $(ROOT_SRCDIR)/../../../..)
 LOCAL_BINDIR := $(ROOT_SRCDIR)/bin
+LOCAL_TOOLSDIR := $(ROOT_SRCDIR)/tools
+
 
 export GOPATH := $(shell go env GOPATH):$(ROOT_GOPRJ)
-export PATH := $(PATH):$(ROOT_SRCDIR)/tools
+export PATH := $(PATH):$(LOCAL_TOOLSDIR)
 
 VERBOSE_1 := -v
 VERBOSE_2 := -v -x
@@ -51,13 +53,13 @@ xds:vendor scripts
        @cd $(ROOT_SRCDIR); $(BUILD_ENV_FLAGS) go build $(VERBOSE_$(V)) -i -o $(LOCAL_BINDIR)/xds-server -ldflags "-X main.AppVersion=$(VERSION) -X main.AppSubVersion=$(SUB_VERSION)" .
 
 test: tools/glide
-       go test --race $(shell ./tools/glide novendor)
+       go test --race $(shell $(LOCAL_TOOLSDIR)/glide novendor)
 
 vet: tools/glide
-       go vet $(shell ./tools/glide novendor)
+       go vet $(shell $(LOCAL_TOOLSDIR)/glide novendor)
 
 fmt: tools/glide
-       go fmt $(shell ./tools/glide novendor)
+       go fmt $(shell $(LOCAL_TOOLSDIR)/glide novendor)
 
 run: build/xds tools/syncthing
        $(LOCAL_BINDIR)/xds-server --log info -c config.json.in
@@ -71,7 +73,7 @@ clean:
 
 .PHONY: distclean
 distclean: clean
-       rm -rf $(LOCAL_BINDIR) tools glide.lock vendor webapp/node_modules webapp/dist
+       rm -rf $(LOCAL_BINDIR) $(LOCAL_TOOLSDIR) glide.lock vendor webapp/node_modules webapp/dist
 
 webapp: webapp/install
        (cd webapp && gulp build)
@@ -88,21 +90,24 @@ scripts:
 
 .PHONY: install
 install: all scripts tools/syncthing
-       mkdir -p $(INSTALL_DIR) && cp $(LOCAL_BINDIR)/* $(INSTALL_DIR)
-       mkdir -p $(INSTALL_WEBAPP_DIR) && cp -a webapp/dist/* $(INSTALL_WEBAPP_DIR)
+       mkdir -p $(INSTALL_DIR) \
+               && cp $(LOCAL_BINDIR)/* $(INSTALL_DIR) \
+               && cp $(LOCAL_TOOLSDIR)/syncthing* $(INSTALL_DIR)
+       mkdir -p $(INSTALL_WEBAPP_DIR) \
+               && cp -a webapp/dist/* $(INSTALL_WEBAPP_DIR)
 
 vendor: tools/glide glide.yaml
-       ./tools/glide install --strip-vendor
+       $(LOCAL_TOOLSDIR)/glide install --strip-vendor
 
 tools/glide:
        @echo "Downloading glide"
-       mkdir -p tools
-       curl --silent -L https://glide.sh/get | GOBIN=./tools  sh
+       mkdir -p $(LOCAL_TOOLSDIR)
+       curl --silent -L https://glide.sh/get | GOBIN=$(LOCAL_TOOLSDIR)  sh
 
 .PHONY: tools/syncthing
 tools/syncthing:
-       @(test -s $(LOCAL_BINDIR)/syncthing || \
-       DESTDIR=$(LOCAL_BINDIR) \
+       @(test -s $(LOCAL_TOOLSDIR)/syncthing || \
+       DESTDIR=$(LOCAL_TOOLSDIR) \
        SYNCTHING_VERSION=$(SYNCTHING_VERSION) \
        SYNCTHING_INOTIFY_VERSION=$(SYNCTHING_INOTIFY_VERSION) \
        ./scripts/get-syncthing.sh)
index 9615041..677ada2 100644 (file)
--- a/README.md
+++ b/README.md
@@ -64,7 +64,9 @@ Supported fields in configuration file are:
 {
     "webAppDir": "location of client dashboard (default: webapp/dist)",
     "shareRootDir": "root directory where projects will be copied",
+    "logsDir": "directory to store logs (eg. syncthing output)",
     "syncthing": {
+        "binDir": "syncthing binaries directory (default: executable directory)",
         "home": "syncthing home directory (usually .../syncthing-config)",
         "gui-address": "syncthing gui url (default http://localhost:8384)"
     }
index a4dcf33..dd34579 100644 (file)
@@ -1,7 +1,9 @@
 {
     "webAppDir": "webapp/dist",
     "shareRootDir": "${ROOT_DIR}/tmp/builder_dev_host/share",
+    "logsDir": "/tmp/xds-server/logs",
     "syncthing": {
+        "binDir": "./bin",
         "home": "${ROOT_DIR}/tmp/local_dev/syncthing-config",
         "gui-address": "http://localhost:8384"
     }
index 56c7503..c94849d 100644 (file)
@@ -4,6 +4,7 @@ import (
        "github.com/Sirupsen/logrus"
        "github.com/gin-gonic/gin"
 
+       "github.com/iotbzh/xds-server/lib/model"
        "github.com/iotbzh/xds-server/lib/session"
        "github.com/iotbzh/xds-server/lib/xdsconfig"
 )
@@ -13,17 +14,19 @@ type APIService struct {
        router    *gin.Engine
        apiRouter *gin.RouterGroup
        sessions  *session.Sessions
-       cfg       xdsconfig.Config
+       cfg       *xdsconfig.Config
+       mfolder   *model.Folder
        log       *logrus.Logger
 }
 
 // New creates a new instance of API service
-func New(sess *session.Sessions, cfg xdsconfig.Config, r *gin.Engine) *APIService {
+func New(sess *session.Sessions, cfg *xdsconfig.Config, mfolder *model.Folder, r *gin.Engine) *APIService {
        s := &APIService{
                router:    r,
                sessions:  sess,
                apiRouter: r.Group("/api/v1"),
                cfg:       cfg,
+               mfolder:   mfolder,
                log:       cfg.Log,
        }
 
index a2817a0..326b6fa 100644 (file)
@@ -36,7 +36,7 @@ func (s *APIService) setConfig(c *gin.Context) {
 
        s.log.Debugln("SET config: ", cfgArg)
 
-       if err := s.cfg.UpdateAll(cfgArg); err != nil {
+       if err := s.mfolder.UpdateAll(cfgArg); err != nil {
                common.APIError(c, err.Error())
                return
        }
index b0bfd41..18fdc7e 100644 (file)
@@ -75,7 +75,7 @@ func (s *APIService) execCmd(c *gin.Context) {
                return
        }
 
-       prj := s.cfg.GetFolderFromID(id)
+       prj := s.mfolder.GetFolderFromID(id)
        if prj == nil {
                common.APIError(c, "Unknown id")
                return
index b1864a2..b4d2ac0 100644 (file)
@@ -44,7 +44,7 @@ func (s *APIService) addFolder(c *gin.Context) {
 
        s.log.Debugln("Add folder config: ", cfgArg)
 
-       newFld, err := s.cfg.UpdateFolder(cfgArg)
+       newFld, err := s.mfolder.UpdateFolder(cfgArg)
        if err != nil {
                common.APIError(c, err.Error())
                return
@@ -68,7 +68,7 @@ func (s *APIService) delFolder(c *gin.Context) {
 
        var delEntry xdsconfig.FolderConfig
        var err error
-       if delEntry, err = s.cfg.DeleteFolder(id); err != nil {
+       if delEntry, err = s.mfolder.DeleteFolder(id); err != nil {
                common.APIError(c, err.Error())
                return
        }
index 9596e13..0f7561f 100644 (file)
@@ -72,7 +72,7 @@ func (s *APIService) buildMake(c *gin.Context) {
                return
        }
 
-       prj := s.cfg.GetFolderFromID(id)
+       prj := s.mfolder.GetFolderFromID(id)
        if prj == nil {
                common.APIError(c, "Unknown id")
                return
diff --git a/lib/model/folder.go b/lib/model/folder.go
new file mode 100644 (file)
index 0000000..6687b68
--- /dev/null
@@ -0,0 +1,99 @@
+package model
+
+import (
+       "fmt"
+
+       "github.com/iotbzh/xds-server/lib/syncthing"
+       "github.com/iotbzh/xds-server/lib/xdsconfig"
+)
+
+// Folder Represent a an XDS folder
+type Folder struct {
+       Conf *xdsconfig.Config
+       SThg *st.SyncThing
+}
+
+// NewFolder Create a new instance of Model Folder
+func NewFolder(cfg *xdsconfig.Config, st *st.SyncThing) *Folder {
+       return &Folder{
+               Conf: cfg,
+               SThg: st,
+       }
+}
+
+// GetFolderFromID retrieves the Folder config from id
+func (c *Folder) GetFolderFromID(id string) *xdsconfig.FolderConfig {
+       if idx := c.Conf.Folders.GetIdx(id); idx != -1 {
+               return &c.Conf.Folders[idx]
+       }
+       return nil
+}
+
+// UpdateAll updates all the current configuration
+func (c *Folder) UpdateAll(newCfg xdsconfig.Config) error {
+       return fmt.Errorf("Not Supported")
+       /*
+               if err := VerifyConfig(newCfg); err != nil {
+                       return err
+               }
+
+               // TODO: c.Builder = c.Builder.Update(newCfg.Builder)
+               c.Folders = c.Folders.Update(newCfg.Folders)
+
+               // FIXME To be tested & improved error handling
+               for _, f := range c.Folders {
+                       if err := c.SThg.FolderChange(st.FolderChangeArg{
+                               ID:           f.ID,
+                               Label:        f.Label,
+                               RelativePath: f.RelativePath,
+                               SyncThingID:  f.SyncThingID,
+                               ShareRootDir: c.ShareRootDir,
+                       }); err != nil {
+                               return err
+                       }
+               }
+
+               return nil
+       */
+}
+
+// UpdateFolder updates a specific folder into the current configuration
+func (c *Folder) UpdateFolder(newFolder xdsconfig.FolderConfig) (xdsconfig.FolderConfig, error) {
+       // rootPath should not be empty
+       if newFolder.RootPath == "" {
+               newFolder.RootPath = c.Conf.ShareRootDir
+       }
+
+       // Sanity check of folder settings
+       if err := newFolder.Verify(); err != nil {
+               return xdsconfig.FolderConfig{}, err
+       }
+
+       c.Conf.Folders = c.Conf.Folders.Update(xdsconfig.FoldersConfig{newFolder})
+
+       err := c.SThg.FolderChange(st.FolderChangeArg{
+               ID:           newFolder.ID,
+               Label:        newFolder.Label,
+               RelativePath: newFolder.RelativePath,
+               SyncThingID:  newFolder.SyncThingID,
+               ShareRootDir: c.Conf.ShareRootDir,
+       })
+       newFolder.BuilderSThgID = c.Conf.Builder.SyncThingID // FIXME - should be removed after local ST config rework
+       newFolder.Status = xdsconfig.FolderStatusEnable
+
+       return newFolder, err
+}
+
+// DeleteFolder deletes a specific folder
+func (c *Folder) DeleteFolder(id string) (xdsconfig.FolderConfig, error) {
+       var fld xdsconfig.FolderConfig
+       var err error
+
+       if err = c.SThg.FolderDelete(id); err != nil {
+               return fld, err
+       }
+
+       c.Conf.Folders, fld, err = c.Conf.Folders.Delete(id)
+
+       return fld, err
+}
index 35dfdc6..d4e1ad3 100644 (file)
@@ -205,8 +205,8 @@ func (s *Sessions) monitorSessMap() {
                        s.log.Debugln("Stop monitorSessMap")
                        return
                case <-time.After(sessionMonitorTime * time.Second):
-                       s.log.Debugf("Sessions Map size: %d", len(s.sessMap))
                        if dbgFullTrace {
+                               s.log.Debugf("Sessions Map size: %d", len(s.sessMap))
                                s.log.Debugf("Sessions Map : %v", s.sessMap)
                        }
 
index 7d07b70..15cab0d 100644 (file)
@@ -2,26 +2,207 @@ package st
 
 import (
        "encoding/json"
+       "os"
+       "os/exec"
+       "path"
+       "path/filepath"
+       "syscall"
+       "time"
 
        "strings"
 
        "fmt"
 
+       "io"
+
        "github.com/Sirupsen/logrus"
        "github.com/iotbzh/xds-server/lib/common"
+       "github.com/iotbzh/xds-server/lib/xdsconfig"
        "github.com/syncthing/syncthing/lib/config"
 )
 
 // SyncThing .
 type SyncThing struct {
        BaseURL string
-       client  *common.HTTPClient
-       log     *logrus.Logger
+       APIKey  string
+       Home    string
+       STCmd   *exec.Cmd
+
+       // Private fields
+       binDir     string
+       logsDir    string
+       exitSTChan chan ExitChan
+       client     *common.HTTPClient
+       log        *logrus.Logger
+}
+
+// ExitChan Channel used for process exit
+type ExitChan struct {
+       status int
+       err    error
 }
 
 // NewSyncThing creates a new instance of Syncthing
-func NewSyncThing(url string, apikey string, log *logrus.Logger) *SyncThing {
-       cl, err := common.HTTPNewClient(url,
+func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
+       var url, apiKey, home, binDir string
+       var err error
+
+       stCfg := conf.FileConf.SThgConf
+       if stCfg != nil {
+               url = stCfg.GuiAddress
+               apiKey = stCfg.GuiAPIKey
+               home = stCfg.Home
+               binDir = stCfg.BinDir
+       }
+
+       if url == "" {
+               url = "http://localhost:8384"
+       }
+       if url[0:7] != "http://" {
+               url = "http://" + url
+       }
+
+       if home == "" {
+               home = "/mnt/share"
+       }
+
+       if binDir == "" {
+               if binDir, err = filepath.Abs(filepath.Dir(os.Args[0])); err != nil {
+                       binDir = "/usr/local/bin"
+               }
+       }
+
+       s := SyncThing{
+               BaseURL: url,
+               APIKey:  apiKey,
+               Home:    home,
+               binDir:  binDir,
+               logsDir: conf.FileConf.LogsDir,
+               log:     log,
+       }
+
+       return &s
+}
+
+// Start Starts syncthing process
+func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
+
+       // Kill existing process (useful for debug ;-) )
+       if os.Getenv("DEBUG_MODE") != "" {
+               exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
+       }
+
+       path, err := exec.LookPath(path.Join(s.binDir, exeName))
+       if err != nil {
+               return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, s.binDir)
+       }
+       cmd := exec.Command(path, args...)
+       cmd.Env = os.Environ()
+       for _, ev := range env {
+               cmd.Env = append(cmd.Env, ev)
+       }
+
+       // open log file
+       var outfile *os.File
+       logFilename := filepath.Join(s.logsDir, exeName+".log")
+       if s.logsDir != "" {
+               outfile, err := os.Create(logFilename)
+               if err != nil {
+                       return nil, fmt.Errorf("Cannot create log file %s", logFilename)
+               }
+
+               cmdOut, err := cmd.StdoutPipe()
+               if err != nil {
+                       return nil, fmt.Errorf("Pipe stdout error for : %s", err)
+               }
+
+               go io.Copy(outfile, cmdOut)
+       }
+
+       err = cmd.Start()
+       if err != nil {
+               return nil, err
+       }
+
+       *eChan = make(chan ExitChan, 1)
+       go func(c *exec.Cmd, oF *os.File) {
+               status := 0
+               sts, err := c.Process.Wait()
+               if !sts.Success() {
+                       s := sts.Sys().(syscall.WaitStatus)
+                       status = s.ExitStatus()
+               }
+               if oF != nil {
+                       oF.Close()
+               }
+               s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
+
+               *eChan <- ExitChan{status, err}
+       }(cmd, outfile)
+
+       return cmd, nil
+}
+
+// Start Starts syncthing process
+func (s *SyncThing) Start() (*exec.Cmd, error) {
+       var err error
+
+       s.log.Infof(" ST home=%s", s.Home)
+       s.log.Infof(" ST  url=%s", s.BaseURL)
+
+       args := []string{
+               "--home=" + s.Home,
+               "-no-browser",
+               "--gui-address=" + s.BaseURL,
+       }
+
+       if s.APIKey != "" {
+               args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
+               s.log.Infof(" ST apikey=%s", s.APIKey)
+       }
+       if s.log.Level == logrus.DebugLevel {
+               args = append(args, "-verbose")
+       }
+
+       env := []string{
+               "STNODEFAULTFOLDER=1",
+       }
+
+       s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
+
+       return s.STCmd, err
+}
+
+func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
+       if err := proc.Signal(os.Interrupt); err != nil {
+               s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
+
+               select {
+               case <-exit:
+               case <-time.After(time.Second):
+                       // A bigger bonk on the head.
+                       if err := proc.Signal(os.Kill); err != nil {
+                               s.log.Infof("Proc term %s error: %s", pname, err.Error())
+                       }
+                       <-exit
+               }
+       }
+       s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
+}
+
+// Stop Stops syncthing process
+func (s *SyncThing) Stop() {
+       if s.STCmd == nil {
+               return
+       }
+       s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
+       s.STCmd = nil
+}
+
+// Connect Establish HTTP connection with Syncthing
+func (s *SyncThing) Connect() error {
+       var err error
+       s.client, err = common.HTTPNewClient(s.BaseURL,
                common.HTTPClientConfig{
                        URLPrefix:           "/rest",
                        HeaderClientKeyName: "X-Syncthing-ID",
@@ -29,19 +210,14 @@ func NewSyncThing(url string, apikey string, log *logrus.Logger) *SyncThing {
        if err != nil {
                msg := ": " + err.Error()
                if strings.Contains(err.Error(), "connection refused") {
-                       msg = fmt.Sprintf("(url: %s)", url)
+                       msg = fmt.Sprintf("(url: %s)", s.BaseURL)
                }
-               log.Debugf("ERROR: cannot connect to Syncthing %s", msg)
-               return nil
+               return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
        }
-
-       s := SyncThing{
-               BaseURL: url,
-               client:  cl,
-               log:     log,
+       if s.client == nil {
+               return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
        }
-
-       return &s
+       return nil
 }
 
 // IDGet returns the Syncthing ID of Syncthing instance running locally
similarity index 93%
rename from lib/xdsserver/server.go
rename to lib/webserver/server.go
index 90d0f38..7be157a 100644 (file)
@@ -1,4 +1,4 @@
-package xdsserver
+package webserver
 
 import (
        "net/http"
@@ -11,6 +11,7 @@ import (
        "github.com/gin-gonic/gin"
        "github.com/googollee/go-socket.io"
        "github.com/iotbzh/xds-server/lib/apiv1"
+       "github.com/iotbzh/xds-server/lib/model"
        "github.com/iotbzh/xds-server/lib/session"
        "github.com/iotbzh/xds-server/lib/xdsconfig"
 )
@@ -21,8 +22,9 @@ type ServerService struct {
        api       *apiv1.APIService
        sIOServer *socketio.Server
        webApp    *gin.RouterGroup
-       cfg       xdsconfig.Config
+       cfg       *xdsconfig.Config
        sessions  *session.Sessions
+       mfolder   *model.Folder
        log       *logrus.Logger
        stop      chan struct{} // signals intentional stop
 }
@@ -31,7 +33,7 @@ const indexFilename = "index.html"
 const cookieMaxAge = "3600"
 
 // NewServer creates an instance of ServerService
-func NewServer(cfg xdsconfig.Config) *ServerService {
+func NewServer(cfg *xdsconfig.Config, mfolder *model.Folder, log *logrus.Logger) *ServerService {
 
        // Setup logging for gin router
        if cfg.Log.Level == logrus.DebugLevel {
@@ -55,8 +57,9 @@ func NewServer(cfg xdsconfig.Config) *ServerService {
                sIOServer: nil,
                webApp:    nil,
                cfg:       cfg,
-               log:       cfg.Log,
+               log:       log,
                sessions:  nil,
+               mfolder:   mfolder,
                stop:      make(chan struct{}),
        }
 
@@ -77,7 +80,7 @@ func (s *ServerService) Serve() error {
        s.sessions = session.NewClientSessions(s.router, s.log, cookieMaxAge)
 
        // Create REST API
-       s.api = apiv1.New(s.sessions, s.cfg, s.router)
+       s.api = apiv1.New(s.sessions, s.cfg, s.mfolder, s.router)
 
        // Websocket routes
        s.sIOServer, err = socketio.NewServer(nil)
index 801891b..3f8a91d 100644 (file)
@@ -2,15 +2,11 @@ package xdsconfig
 
 import (
        "fmt"
-       "strings"
 
        "os"
 
-       "time"
-
        "github.com/Sirupsen/logrus"
        "github.com/codegangsta/cli"
-       "github.com/iotbzh/xds-server/lib/syncthing"
 )
 
 // Config parameters (json format) of /config command
@@ -21,14 +17,12 @@ type Config struct {
        Builder       BuilderConfig `json:"builder"`
        Folders       FoldersConfig `json:"folders"`
 
-       // Private / un-exported fields
-       progName     string
-       fileConf     FileConfig
+       // Private (un-exported fields in REST GET /config route)
+       FileConf     FileConfig     `json:"-"`
        WebAppDir    string         `json:"-"`
        HTTPPort     string         `json:"-"`
        ShareRootDir string         `json:"-"`
        Log          *logrus.Logger `json:"-"`
-       SThg         *st.SyncThing  `json:"-"`
 }
 
 // Config default values
@@ -36,196 +30,48 @@ const (
        DefaultAPIVersion = "1"
        DefaultPort       = "8000"
        DefaultShareDir   = "/mnt/share"
-       DefaultLogLevel   = "error"
 )
 
 // Init loads the configuration on start-up
-func Init(ctx *cli.Context) (Config, error) {
+func Init(cliCtx *cli.Context, log *logrus.Logger) (*Config, error) {
        var err error
 
-       // Set logger level and formatter
-       log := ctx.App.Metadata["logger"].(*logrus.Logger)
-
-       logLevel := ctx.GlobalString("log")
-       if logLevel == "" {
-               logLevel = DefaultLogLevel
-       }
-       if log.Level, err = logrus.ParseLevel(logLevel); err != nil {
-               fmt.Printf("Invalid log level : \"%v\"\n", logLevel)
-               os.Exit(1)
-       }
-       log.Formatter = &logrus.TextFormatter{}
-
        // Define default configuration
        c := Config{
-               Version:       ctx.App.Metadata["version"].(string),
+               Version:       cliCtx.App.Metadata["version"].(string),
                APIVersion:    DefaultAPIVersion,
-               VersionGitTag: ctx.App.Metadata["git-tag"].(string),
+               VersionGitTag: cliCtx.App.Metadata["git-tag"].(string),
                Builder:       BuilderConfig{},
                Folders:       FoldersConfig{},
 
-               progName:     ctx.App.Name,
                WebAppDir:    "webapp/dist",
                HTTPPort:     DefaultPort,
                ShareRootDir: DefaultShareDir,
                Log:          log,
-               SThg:         nil,
        }
 
        // config file settings overwrite default config
-       err = updateConfigFromFile(&c, ctx.GlobalString("config"))
+       err = updateConfigFromFile(&c, cliCtx.GlobalString("config"))
        if err != nil {
-               return Config{}, err
+               return nil, err
        }
 
        // Update location of shared dir if needed
        if !dirExists(c.ShareRootDir) {
                if err := os.MkdirAll(c.ShareRootDir, 0770); err != nil {
-                       c.Log.Fatalf("No valid shared directory found (err=%v)", err)
+                       return nil, fmt.Errorf("No valid shared directory found: %v", err)
                }
        }
        c.Log.Infoln("Share root directory: ", c.ShareRootDir)
 
-       // FIXME - add a builder interface and support other builder type (eg. native)
-       builderType := "syncthing"
-
-       switch builderType {
-       case "syncthing":
-               // Syncthing settings only configurable from config.json file
-               stGuiAddr := c.fileConf.SThgConf.GuiAddress
-               stGuiApikey := c.fileConf.SThgConf.GuiAPIKey
-               if stGuiAddr == "" {
-                       stGuiAddr = "http://localhost:8384"
-               }
-               if stGuiAddr[0:7] != "http://" {
-                       stGuiAddr = "http://" + stGuiAddr
-               }
-
-               // Retry if connection fail
-               retry := 5
-               for retry > 0 {
-                       c.SThg = st.NewSyncThing(stGuiAddr, stGuiApikey, c.Log)
-                       if c.SThg != nil {
-                               break
-                       }
-                       c.Log.Warningf("Establishing connection to Syncthing (retry %d/5)", retry)
-                       time.Sleep(time.Second)
-                       retry--
-               }
-               if c.SThg == nil {
-                       c.Log.Fatalf("ERROR: cannot connect to Syncthing (url: %s)", stGuiAddr)
-               }
-
-               // Retrieve Syncthing config
-               id, err := c.SThg.IDGet()
-               if err != nil {
-                       return Config{}, err
-               }
-
-               if c.Builder, err = NewBuilderConfig(id); err != nil {
-                       c.Log.Fatalln(err)
-               }
-
-               // Retrieve initial Syncthing config
-               stCfg, err := c.SThg.ConfigGet()
-               if err != nil {
-                       return Config{}, err
-               }
-               for _, stFld := range stCfg.Folders {
-                       relativePath := strings.TrimPrefix(stFld.RawPath, c.ShareRootDir)
-                       if relativePath == "" {
-                               relativePath = stFld.RawPath
-                       }
-                       newFld := NewFolderConfig(stFld.ID, stFld.Label, c.ShareRootDir, strings.Trim(relativePath, "/"))
-                       c.Folders = c.Folders.Update(FoldersConfig{newFld})
-               }
-
-       default:
-               log.Fatalln("Unsupported builder type")
-       }
-
-       return c, nil
-}
-
-// GetFolderFromID retrieves the Folder config from id
-func (c *Config) GetFolderFromID(id string) *FolderConfig {
-       if idx := c.Folders.GetIdx(id); idx != -1 {
-               return &c.Folders[idx]
-       }
-       return nil
-}
-
-// UpdateAll updates all the current configuration
-func (c *Config) UpdateAll(newCfg Config) error {
-       return fmt.Errorf("Not Supported")
-       /*
-               if err := VerifyConfig(newCfg); err != nil {
-                       return err
+       if c.FileConf.LogsDir != "" && !dirExists(c.FileConf.LogsDir) {
+               if err := os.MkdirAll(c.FileConf.LogsDir, 0770); err != nil {
+                       return nil, fmt.Errorf("Cannot create logs dir: %v", err)
                }
-
-               // TODO: c.Builder = c.Builder.Update(newCfg.Builder)
-               c.Folders = c.Folders.Update(newCfg.Folders)
-
-               // SEB A SUP model.NotifyListeners(c, NotifyFoldersChange, FolderConfig{})
-               // FIXME To be tested & improved error handling
-               for _, f := range c.Folders {
-                       if err := c.SThg.FolderChange(st.FolderChangeArg{
-                               ID:           f.ID,
-                               Label:        f.Label,
-                               RelativePath: f.RelativePath,
-                               SyncThingID:  f.SyncThingID,
-                               ShareRootDir: c.ShareRootDir,
-                       }); err != nil {
-                               return err
-                       }
-               }
-
-               return nil
-       */
-}
-
-// UpdateFolder updates a specific folder into the current configuration
-func (c *Config) UpdateFolder(newFolder FolderConfig) (FolderConfig, error) {
-       // rootPath should not be empty
-       if newFolder.rootPath == "" {
-               newFolder.rootPath = c.ShareRootDir
-       }
-
-       // Sanity check of folder settings
-       if err := FolderVerify(newFolder); err != nil {
-               return FolderConfig{}, err
        }
+       c.Log.Infoln("Logs directory: ", c.FileConf.LogsDir)
 
-       c.Folders = c.Folders.Update(FoldersConfig{newFolder})
-
-       // SEB A SUP model.NotifyListeners(c, NotifyFolderAdd, newFolder)
-       err := c.SThg.FolderChange(st.FolderChangeArg{
-               ID:           newFolder.ID,
-               Label:        newFolder.Label,
-               RelativePath: newFolder.RelativePath,
-               SyncThingID:  newFolder.SyncThingID,
-               ShareRootDir: c.ShareRootDir,
-       })
-
-       newFolder.BuilderSThgID = c.Builder.SyncThingID // FIXME - should be removed after local ST config rework
-       newFolder.Status = FolderStatusEnable
-
-       return newFolder, err
-}
-
-// DeleteFolder deletes a specific folder
-func (c *Config) DeleteFolder(id string) (FolderConfig, error) {
-       var fld FolderConfig
-       var err error
-
-       //SEB A SUP model.NotifyListeners(c, NotifyFolderDelete, fld)
-       if err = c.SThg.FolderDelete(id); err != nil {
-               return fld, err
-       }
-
-       c.Folders, fld, err = c.Folders.Delete(id)
-
-       return fld, err
+       return &c, nil
 }
 
 func dirExists(path string) bool {
index 7370ed0..3daf77c 100644 (file)
@@ -12,23 +12,25 @@ import (
 )
 
 type SyncThingConf struct {
+       BinDir     string `json:"binDir"`
        Home       string `json:"home"`
        GuiAddress string `json:"gui-address"`
        GuiAPIKey  string `json:"gui-apikey"`
 }
 
 type FileConfig struct {
-       WebAppDir    string        `json:"webAppDir"`
-       ShareRootDir string        `json:"shareRootDir"`
-       HTTPPort     string        `json:"httpPort"`
-       SThgConf     SyncThingConf `json:"syncthing"`
+       WebAppDir    string         `json:"webAppDir"`
+       ShareRootDir string         `json:"shareRootDir"`
+       HTTPPort     string         `json:"httpPort"`
+       SThgConf     *SyncThingConf `json:"syncthing"`
+       LogsDir      string         `json:"logsDir"`
 }
 
 // getConfigFromFile reads configuration from a config file.
 // Order to determine which config file is used:
 //  1/ from command line option: "--config myConfig.json"
 //  2/ $HOME/.xds/config.json file
-//  3/ <current_dir>/agent-config.json file
+//  3/ <current_dir>/config.json file
 //  4/ <xds-server executable dir>/config.json file
 
 func updateConfigFromFile(c *Config, confFile string) error {
@@ -73,7 +75,7 @@ func updateConfigFromFile(c *Config, confFile string) error {
        if err := json.NewDecoder(fd).Decode(&fCfg); err != nil {
                return err
        }
-       c.fileConf = fCfg
+       c.FileConf = fCfg
 
        // Support environment variables (IOW ${MY_ENV_VAR} syntax) in config.json
        // TODO: better to use reflect package to iterate on fields and be more generic
index f22e76f..e32f46a 100644 (file)
@@ -30,8 +30,8 @@ type FolderConfig struct {
        BuilderSThgID string     `json:"builderSThgID"`
        Status        string     `json:"status"`
 
-       // Private fields
-       rootPath string
+       // Not exported fields
+       RootPath string `json:"-"`
 }
 
 // NewFolderConfig creates a new folder object
@@ -43,7 +43,7 @@ func NewFolderConfig(id, label, rootDir, path string) FolderConfig {
                Type:         FolderTypeCloudSync,
                SyncThingID:  "",
                Status:       FolderStatusDisable,
-               rootPath:     rootDir,
+               RootPath:     rootDir,
        }
 }
 
@@ -53,30 +53,30 @@ func (c *FolderConfig) GetFullPath(dir string) string {
                dir = ""
        }
        if filepath.IsAbs(dir) {
-               return filepath.Join(c.rootPath, dir)
+               return filepath.Join(c.RootPath, dir)
        }
-       return filepath.Join(c.rootPath, c.RelativePath, dir)
+       return filepath.Join(c.RootPath, c.RelativePath, dir)
 }
 
-// FolderVerify is called to verify that a configuration is valid
-func FolderVerify(fCfg FolderConfig) error {
+// Verify is called to verify that a configuration is valid
+func (c *FolderConfig) Verify() error {
        var err error
 
-       if fCfg.Type != FolderTypeCloudSync {
+       if c.Type != FolderTypeCloudSync {
                err = fmt.Errorf("Unsupported folder type")
        }
 
-       if fCfg.SyncThingID == "" {
+       if c.SyncThingID == "" {
                err = fmt.Errorf("device id not set (SyncThingID field)")
        }
 
-       if fCfg.rootPath == "" {
-               err = fmt.Errorf("rootPath must not be empty")
+       if c.RootPath == "" {
+               err = fmt.Errorf("RootPath must not be empty")
        }
 
        if err != nil {
-               fCfg.Status = FolderStatusErrorConfig
-               log.Printf("ERROR FolderVerify: %v\n", err)
+               c.Status = FolderStatusErrorConfig
+               log.Printf("ERROR Verify: %v\n", err)
        }
 
        return err
diff --git a/main.go b/main.go
index 40617d1..ba445f5 100644 (file)
--- a/main.go
+++ b/main.go
@@ -3,13 +3,20 @@
 package main
 
 import (
-       "log"
+       "fmt"
        "os"
+       "os/exec"
+       "os/signal"
+       "strings"
+       "syscall"
+       "time"
 
        "github.com/Sirupsen/logrus"
        "github.com/codegangsta/cli"
+       "github.com/iotbzh/xds-server/lib/model"
+       "github.com/iotbzh/xds-server/lib/syncthing"
+       "github.com/iotbzh/xds-server/lib/webserver"
        "github.com/iotbzh/xds-server/lib/xdsconfig"
-       "github.com/iotbzh/xds-server/lib/xdsserver"
 )
 
 const (
@@ -30,19 +37,150 @@ var AppVersion = "?.?.?"
 // Should be set by compilation -ldflags "-X main.AppSubVersion=xxx"
 var AppSubVersion = "unknown-dev"
 
-// Web server main routine
-func webServer(ctx *cli.Context) error {
+// Context holds the XDS server context
+type Context struct {
+       ProgName  string
+       Cli       *cli.Context
+       Config    *xdsconfig.Config
+       Log       *logrus.Logger
+       SThg      *st.SyncThing
+       SThgCmd   *exec.Cmd
+       MFolder   *model.Folder
+       WWWServer *webserver.ServerService
+       Exit      chan os.Signal
+}
+
+// NewContext Create a new instance of XDS server
+func NewContext(cliCtx *cli.Context) *Context {
+       var err error
+
+       // Set logger level and formatter
+       log := cliCtx.App.Metadata["logger"].(*logrus.Logger)
+
+       logLevel := cliCtx.GlobalString("log")
+       if logLevel == "" {
+               logLevel = "error" // FIXME get from Config DefaultLogLevel
+       }
+       if log.Level, err = logrus.ParseLevel(logLevel); err != nil {
+               fmt.Printf("Invalid log level : \"%v\"\n", logLevel)
+               os.Exit(1)
+       }
+       log.Formatter = &logrus.TextFormatter{}
+
+       // Define default configuration
+       ctx := Context{
+               ProgName: cliCtx.App.Name,
+               Cli:      cliCtx,
+               Log:      log,
+               Exit:     make(chan os.Signal, 1),
+       }
+
+       // register handler on SIGTERM / exit
+       signal.Notify(ctx.Exit, os.Interrupt, syscall.SIGTERM)
+       go handlerSigTerm(&ctx)
+
+       return &ctx
+}
+
+// Handle exit and properly stop/close all stuff
+func handlerSigTerm(ctx *Context) {
+       <-ctx.Exit
+       if ctx.SThg != nil {
+               ctx.Log.Infof("Stopping Syncthing... (PID %d)",
+                       ctx.SThgCmd.Process.Pid)
+               ctx.SThg.Stop()
+       }
+       if ctx.WWWServer != nil {
+               ctx.Log.Infof("Stoping Web server...")
+               ctx.WWWServer.Stop()
+       }
+       os.Exit(1)
+}
 
-       // Init config
-       cfg, err := xdsconfig.Init(ctx)
+// xdsServer main routine
+func xdsApp(cliCtx *cli.Context) error {
+       var err error
+
+       // Create XDS server context
+       ctx := NewContext(cliCtx)
+
+       // Load config
+       cfg, err := xdsconfig.Init(ctx.Cli, ctx.Log)
        if err != nil {
                return cli.NewExitError(err, 2)
        }
+       ctx.Config = cfg
+
+       // TODO allow to redirect stdout/sterr into logs file
+       //logFilename := filepath.Join(ctx.Config.FileConf.LogsDir + "xds-server.log")
+
+       // FIXME - add a builder interface and support other builder type (eg. native)
+       builderType := "syncthing"
+
+       switch builderType {
+       case "syncthing":
+
+               // Start local instance of Syncthing and Syncthing-notify
+               ctx.SThg = st.NewSyncThing(ctx.Config, ctx.Log)
+
+               ctx.Log.Infof("Starting Syncthing...")
+               ctx.SThgCmd, err = ctx.SThg.Start()
+               if err != nil {
+                       return cli.NewExitError(err, 2)
+               }
+               ctx.Log.Infof("Syncthing started (PID %d)", ctx.SThgCmd.Process.Pid)
+
+               // Establish connection with local Syncthing (retry if connection fail)
+               retry := 10
+               err = nil
+               for retry > 0 {
+                       if err = ctx.SThg.Connect(); err == nil {
+                               break
+                       }
+                       ctx.Log.Warningf("Establishing connection to Syncthing (retry %d/10)", retry)
+                       time.Sleep(time.Second)
+                       retry--
+               }
+               if err != nil || retry == 0 {
+                       return cli.NewExitError(err, 2)
+               }
+
+               // Retrieve Syncthing config
+               id, err := ctx.SThg.IDGet()
+               if err != nil {
+                       return cli.NewExitError(err, 2)
+               }
+
+               if ctx.Config.Builder, err = xdsconfig.NewBuilderConfig(id); err != nil {
+                       return cli.NewExitError(err, 2)
+               }
+
+               // Retrieve initial Syncthing config
+               stCfg, err := ctx.SThg.ConfigGet()
+               if err != nil {
+                       return cli.NewExitError(err, 2)
+               }
+               for _, stFld := range stCfg.Folders {
+                       relativePath := strings.TrimPrefix(stFld.RawPath, ctx.Config.ShareRootDir)
+                       if relativePath == "" {
+                               relativePath = stFld.RawPath
+                       }
+                       newFld := xdsconfig.NewFolderConfig(stFld.ID, stFld.Label, ctx.Config.ShareRootDir, strings.Trim(relativePath, "/"))
+                       ctx.Config.Folders = ctx.Config.Folders.Update(xdsconfig.FoldersConfig{newFld})
+               }
+
+               // Init model folder
+               ctx.MFolder = model.NewFolder(ctx.Config, ctx.SThg)
+
+       default:
+               err = fmt.Errorf("Unsupported builder type")
+               return cli.NewExitError(err, 3)
+       }
 
        // Create and start Web Server
-       svr := xdsserver.NewServer(cfg)
-       if err = svr.Serve(); err != nil {
-               log.Println(err)
+       ctx.WWWServer = webserver.NewServer(ctx.Config, ctx.MFolder, ctx.Log)
+       if err = ctx.WWWServer.Serve(); err != nil {
+               ctx.Log.Println(err)
                return cli.NewExitError(err, 3)
        }
 
@@ -83,7 +221,7 @@ func main() {
        }
 
        // only one action: Web Server
-       app.Action = webServer
+       app.Action = xdsApp
 
        app.Run(os.Args)
 }