From: Sebastien Douheret Date: Tue, 16 May 2017 20:51:32 +0000 (+0200) Subject: Auto start Syncthing and Syncthing-inotify. X-Git-Tag: v0.0.1-alpha~34 X-Git-Url: https://gerrit.automotivelinux.org/gerrit/gitweb?p=src%2Fxds%2Fxds-server.git;a=commitdiff_plain;h=c07adb807c41a1545a9a0f5bbf40080d86946538 Auto start Syncthing and Syncthing-inotify. Signed-off-by: Sebastien Douheret --- diff --git a/.vscode/settings.json b/.vscode/settings.json index a90ab0d..a873478 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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 diff --git a/Makefile b/Makefile index 247d454..e3cc99b 100644 --- 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) diff --git a/README.md b/README.md index 9615041..677ada2 100644 --- 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)" } diff --git a/config.json.in b/config.json.in index a4dcf33..dd34579 100644 --- a/config.json.in +++ b/config.json.in @@ -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" } diff --git a/lib/apiv1/apiv1.go b/lib/apiv1/apiv1.go index 56c7503..c94849d 100644 --- a/lib/apiv1/apiv1.go +++ b/lib/apiv1/apiv1.go @@ -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, } diff --git a/lib/apiv1/config.go b/lib/apiv1/config.go index a2817a0..326b6fa 100644 --- a/lib/apiv1/config.go +++ b/lib/apiv1/config.go @@ -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 } diff --git a/lib/apiv1/exec.go b/lib/apiv1/exec.go index b0bfd41..18fdc7e 100644 --- a/lib/apiv1/exec.go +++ b/lib/apiv1/exec.go @@ -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 diff --git a/lib/apiv1/folders.go b/lib/apiv1/folders.go index b1864a2..b4d2ac0 100644 --- a/lib/apiv1/folders.go +++ b/lib/apiv1/folders.go @@ -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 } diff --git a/lib/apiv1/make.go b/lib/apiv1/make.go index 9596e13..0f7561f 100644 --- a/lib/apiv1/make.go +++ b/lib/apiv1/make.go @@ -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 index 0000000..6687b68 --- /dev/null +++ b/lib/model/folder.go @@ -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 +} diff --git a/lib/session/session.go b/lib/session/session.go index 35dfdc6..d4e1ad3 100644 --- a/lib/session/session.go +++ b/lib/session/session.go @@ -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) } diff --git a/lib/syncthing/st.go b/lib/syncthing/st.go index 7d07b70..15cab0d 100644 --- a/lib/syncthing/st.go +++ b/lib/syncthing/st.go @@ -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 diff --git a/lib/xdsserver/server.go b/lib/webserver/server.go similarity index 93% rename from lib/xdsserver/server.go rename to lib/webserver/server.go index 90d0f38..7be157a 100644 --- a/lib/xdsserver/server.go +++ b/lib/webserver/server.go @@ -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) diff --git a/lib/xdsconfig/config.go b/lib/xdsconfig/config.go index 801891b..3f8a91d 100644 --- a/lib/xdsconfig/config.go +++ b/lib/xdsconfig/config.go @@ -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 { diff --git a/lib/xdsconfig/fileconfig.go b/lib/xdsconfig/fileconfig.go index 7370ed0..3daf77c 100644 --- a/lib/xdsconfig/fileconfig.go +++ b/lib/xdsconfig/fileconfig.go @@ -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/ /agent-config.json file +// 3/ /config.json file // 4/ /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 diff --git a/lib/xdsconfig/folderconfig.go b/lib/xdsconfig/folderconfig.go index f22e76f..e32f46a 100644 --- a/lib/xdsconfig/folderconfig.go +++ b/lib/xdsconfig/folderconfig.go @@ -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 --- 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) }