summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2017-05-16 22:51:32 +0200
committerSebastien Douheret <sebastien.douheret@iot.bzh>2017-05-17 14:04:32 +0200
commitc07adb807c41a1545a9a0f5bbf40080d86946538 (patch)
tree21e00efbcd03360416698663a1ea89536717ae86
parent8983eebdbe74489d62eae4097580fc430d75bd07 (diff)
Auto start Syncthing and Syncthing-inotify.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
-rw-r--r--.vscode/settings.json2
-rw-r--r--Makefile29
-rw-r--r--README.md2
-rw-r--r--config.json.in2
-rw-r--r--lib/apiv1/apiv1.go7
-rw-r--r--lib/apiv1/config.go2
-rw-r--r--lib/apiv1/exec.go2
-rw-r--r--lib/apiv1/folders.go4
-rw-r--r--lib/apiv1/make.go2
-rw-r--r--lib/model/folder.go99
-rw-r--r--lib/session/session.go2
-rw-r--r--lib/syncthing/st.go204
-rw-r--r--lib/webserver/server.go (renamed from lib/xdsserver/server.go)13
-rw-r--r--lib/xdsconfig/config.go180
-rw-r--r--lib/xdsconfig/fileconfig.go14
-rw-r--r--lib/xdsconfig/folderconfig.go26
-rw-r--r--main.go158
17 files changed, 512 insertions, 236 deletions
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
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/ <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
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)
}