diff options
author | Sebastien Douheret <sebastien.douheret@iot.bzh> | 2017-11-29 08:54:00 +0100 |
---|---|---|
committer | Sebastien Douheret <sebastien.douheret@iot.bzh> | 2017-11-29 11:10:30 +0100 |
commit | 2f7828d01f4c4ca2909f95f098627cd5475ed225 (patch) | |
tree | b5e71920b813b95cae3e32044be08b99223348ec /lib/xdsserver | |
parent | 5caebfb4b7c3b73988f067082b219ce5b7f409ba (diff) |
Refit source files to have a public xs-apiv1 lib package.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
Diffstat (limited to 'lib/xdsserver')
-rw-r--r-- | lib/xdsserver/apiv1-config.go | 40 | ||||
-rw-r--r-- | lib/xdsserver/apiv1-events.go | 132 | ||||
-rw-r--r-- | lib/xdsserver/apiv1-exec.go | 342 | ||||
-rw-r--r-- | lib/xdsserver/apiv1-folders.go | 131 | ||||
-rw-r--r-- | lib/xdsserver/apiv1-make.go | 214 | ||||
-rw-r--r-- | lib/xdsserver/apiv1-sdks.go | 29 | ||||
-rw-r--r-- | lib/xdsserver/apiv1-version.go | 20 | ||||
-rw-r--r-- | lib/xdsserver/apiv1.go | 47 | ||||
-rw-r--r-- | lib/xdsserver/folder-interface.go | 22 | ||||
-rw-r--r-- | lib/xdsserver/folder-pathmap.go | 175 | ||||
-rw-r--r-- | lib/xdsserver/folder-st-disable.go | 91 | ||||
-rw-r--r-- | lib/xdsserver/folder-st.go | 213 | ||||
-rw-r--r-- | lib/xdsserver/folders.go | 458 | ||||
-rw-r--r-- | lib/xdsserver/sdk.go | 56 | ||||
-rw-r--r-- | lib/xdsserver/sdks.go | 127 | ||||
-rw-r--r-- | lib/xdsserver/sessions.go | 225 | ||||
-rw-r--r-- | lib/xdsserver/webserver.go | 181 | ||||
-rw-r--r-- | lib/xdsserver/xdsserver.go | 202 |
18 files changed, 2705 insertions, 0 deletions
diff --git a/lib/xdsserver/apiv1-config.go b/lib/xdsserver/apiv1-config.go new file mode 100644 index 0000000..5a5bb6e --- /dev/null +++ b/lib/xdsserver/apiv1-config.go @@ -0,0 +1,40 @@ +package xdsserver + +import ( + "net/http" + "sync" + + "github.com/gin-gonic/gin" + common "github.com/iotbzh/xds-common/golib" + "github.com/iotbzh/xds-server/lib/xsapiv1" +) + +var confMut sync.Mutex + +// GetConfig returns server configuration +func (s *APIService) getConfig(c *gin.Context) { + confMut.Lock() + defer confMut.Unlock() + + c.JSON(http.StatusOK, s.Config) +} + +// SetConfig sets server configuration +func (s *APIService) setConfig(c *gin.Context) { + // FIXME - must be tested + c.JSON(http.StatusNotImplemented, "Not implemented") + + var cfgArg xsapiv1.APIConfig + + if c.BindJSON(&cfgArg) != nil { + common.APIError(c, "Invalid arguments") + return + } + + confMut.Lock() + defer confMut.Unlock() + + s.Log.Debugln("SET config: ", cfgArg) + + common.APIError(c, "Not Supported") +} diff --git a/lib/xdsserver/apiv1-events.go b/lib/xdsserver/apiv1-events.go new file mode 100644 index 0000000..3823f9e --- /dev/null +++ b/lib/xdsserver/apiv1-events.go @@ -0,0 +1,132 @@ +package xdsserver + +import ( + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + common "github.com/iotbzh/xds-common/golib" + "github.com/iotbzh/xds-server/lib/xsapiv1" +) + +// eventsList Registering for events that will be send over a WS +func (s *APIService) eventsList(c *gin.Context) { + +} + +// eventsRegister Registering for events that will be send over a WS +func (s *APIService) eventsRegister(c *gin.Context) { + var args xsapiv1.EventRegisterArgs + + if c.BindJSON(&args) != nil { + common.APIError(c, "Invalid arguments") + return + } + + sess := s.sessions.Get(c) + if sess == nil { + common.APIError(c, "Unknown sessions") + return + } + + evType := strings.TrimPrefix(xsapiv1.EVTFolderStateChange, xsapiv1.EventTypePrefix) + if args.Name != evType { + common.APIError(c, "Unsupported event name") + return + } + + /* XXX - to be removed if no plan to support "generic" event + var cbFunc st.EventsCB + cbFunc = func(ev st.Event, data *st.EventsCBData) { + + evid, _ := strconv.Atoi((*data)["id"].(string)) + ssid := (*data)["sid"].(string) + so := s.sessions.IOSocketGet(ssid) + if so == nil { + s.log.Infof("Event %s not emitted - sid: %s", ev.Type, ssid) + + // Consider that client disconnected, so unregister this event + s.mfolders.SThg.Events.UnRegister(ev.Type, evid) + return + } + + msg := EventMsg{ + Time: ev.Time, + Type: ev.Type, + Data: ev.Data, + } + + if err := (*so).Emit(EVTAll, msg); err != nil { + s.log.Errorf("WS Emit Event : %v", err) + } + + if err := (*so).Emit(EventTypePrefix+ev.Type, msg); err != nil { + s.log.Errorf("WS Emit Event : %v", err) + } + } + + data := make(st.EventsCBData) + data["sid"] = sess.ID + + id, err := s.mfolders.SThg.Events.Register(args.Name, cbFunc, args.ProjectID, &data) + */ + + var cbFunc FolderEventCB + cbFunc = func(cfg *xsapiv1.FolderConfig, data *FolderEventCBData) { + ssid := (*data)["sid"].(string) + so := s.sessions.IOSocketGet(ssid) + if so == nil { + //s.log.Infof("Event %s not emitted - sid: %s", ev.Type, ssid) + + // Consider that client disconnected, so unregister this event + // SEB FIXMEs.mfolders.RegisterEventChange(ev.Type) + return + } + + msg := xsapiv1.EventMsg{ + Time: time.Now().String(), + Type: evType, + Folder: *cfg, + } + + s.Log.Debugf("WS Emit %s - Status=%10s, IsInSync=%6v, ID=%s", + xsapiv1.EventTypePrefix+evType, cfg.Status, cfg.IsInSync, cfg.ID) + + if err := (*so).Emit(xsapiv1.EventTypePrefix+evType, msg); err != nil { + s.Log.Errorf("WS Emit Folder StateChanged event : %v", err) + } + } + data := make(FolderEventCBData) + data["sid"] = sess.ID + + prjID, err := s.mfolders.ResolveID(args.ProjectID) + if err != nil { + common.APIError(c, err.Error()) + return + } + if err = s.mfolders.RegisterEventChange(prjID, &cbFunc, &data); err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "OK"}) +} + +// eventsRegister Registering for events that will be send over a WS +func (s *APIService) eventsUnRegister(c *gin.Context) { + var args xsapiv1.EventUnRegisterArgs + + if c.BindJSON(&args) != nil || args.Name == "" || args.ID < 0 { + common.APIError(c, "Invalid arguments") + return + } + /* TODO + if err := s.mfolders.SThg.Events.UnRegister(args.Name, args.ID); err != nil { + common.APIError(c, err.Error()) + return + } + c.JSON(http.StatusOK, gin.H{"status": "OK"}) + */ + common.APIError(c, "Not implemented yet") +} diff --git a/lib/xdsserver/apiv1-exec.go b/lib/xdsserver/apiv1-exec.go new file mode 100644 index 0000000..ce5e7b7 --- /dev/null +++ b/lib/xdsserver/apiv1-exec.go @@ -0,0 +1,342 @@ +package xdsserver + +import ( + "fmt" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + common "github.com/iotbzh/xds-common/golib" + "github.com/iotbzh/xds-common/golib/eows" + "github.com/iotbzh/xds-server/lib/xsapiv1" + "github.com/kr/pty" +) + +var execCommandID = 1 + +// ExecCmd executes remotely a command +func (s *APIService) execCmd(c *gin.Context) { + var gdbPty, gdbTty *os.File + var err error + var args xsapiv1.ExecArgs + if c.BindJSON(&args) != nil { + common.APIError(c, "Invalid arguments") + return + } + + // TODO: add permission ? + + // Retrieve session info + sess := s.sessions.Get(c) + if sess == nil { + common.APIError(c, "Unknown sessions") + return + } + sop := sess.IOSocket + if sop == nil { + common.APIError(c, "Websocket not established") + return + } + + // Allow to pass id in url (/exec/:id) or as JSON argument + idArg := c.Param("id") + if idArg == "" { + idArg = args.ID + } + if idArg == "" { + common.APIError(c, "Invalid id") + return + } + id, err := s.mfolders.ResolveID(idArg) + if err != nil { + common.APIError(c, err.Error()) + return + } + f := s.mfolders.Get(id) + if f == nil { + common.APIError(c, "Unknown id") + return + } + fld := *f + prj := fld.GetConfig() + + // Build command line + cmd := []string{} + // Setup env var regarding Sdk ID (used for example to setup cross toolchain) + if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 { + cmd = append(cmd, envCmd...) + cmd = append(cmd, "&&") + } else { + // It's an error if no envcmd found while a sdkid has been provided + if args.SdkID != "" { + common.APIError(c, "Unknown sdkid") + return + } + } + + cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"") + // FIXME - add 'exec' prevents to use syntax: + // xds-exec -l debug -c xds-config.env -- "cd build && cmake .." + // but exec is mandatory to allow to pass correctly signals + // As workaround, exec is set for now on client side (eg. in xds-gdb) + //cmd = append(cmd, "&&", "exec", args.Cmd) + cmd = append(cmd, "&&", args.Cmd) + + // Process command arguments + cmdArgs := make([]string, len(args.Args)+1) + + // Copy and Translate path from client to server + for _, aa := range args.Args { + if strings.Contains(aa, prj.ClientPath) { + cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa)) + } else { + cmdArgs = append(cmdArgs, aa) + } + } + + // Allocate pts if tty if used + if args.TTY { + gdbPty, gdbTty, err = pty.Open() + if err != nil { + common.APIError(c, err.Error()) + return + } + + s.Log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name()) + cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name()) + } + + // Unique ID for each commands + if args.CmdID == "" { + args.CmdID = s.Config.ServerUID[:18] + "_" + strconv.Itoa(execCommandID) + execCommandID++ + } + + // Create new execution over WS context + execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID) + execWS.Log = s.Log + + // Append client project dir to environment + execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath) + + // Set command execution timeout + if args.CmdTimeout == 0 { + // 0 : default timeout + // TODO get default timeout from config.json file + execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day + } else { + execWS.CmdExecTimeout = args.CmdTimeout + } + + // Define callback for input (stdin) + execWS.InputEvent = xsapiv1.ExecInEvent + execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) { + s.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1)) + + // Handle Ctrl-D + if len(stdin) == 1 && stdin == "\x04" { + // Close stdin + errMsg := fmt.Errorf("close stdin: %v", stdin) + return "", errMsg + } + + // Set correct path + data := e.UserData + prjID := (*data)["ID"].(string) + f := s.mfolders.Get(prjID) + if f == nil { + s.Log.Errorf("InputCB: Cannot get folder ID %s", prjID) + } else { + // Translate paths from client to server + stdin = (*f).ConvPathCli2Svr(stdin) + } + + return stdin, nil + } + + // Define callback for output (stdout+stderr) + execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) { + // IO socket can be nil when disconnected + so := s.sessions.IOSocketGet(e.Sid) + if so == nil { + s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID) + return + } + + // Retrieve project ID and RootPath + data := e.UserData + prjID := (*data)["ID"].(string) + gdbServerTTY := (*data)["gdbServerTTY"].(string) + + f := s.mfolders.Get(prjID) + if f == nil { + s.Log.Errorf("OutputCB: Cannot get folder ID %s", prjID) + } else { + // Translate paths from server to client + stdout = (*f).ConvPathSvr2Cli(stdout) + stderr = (*f).ConvPathSvr2Cli(stderr) + } + + s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.ExecOutEvent, e.Sid[4:], e.CmdID, prjID) + if stdout != "" { + s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1)) + } + if stderr != "" { + s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1)) + } + + // FIXME replace by .BroadcastTo a room + err := (*so).Emit(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{ + CmdID: e.CmdID, + Timestamp: time.Now().String(), + Stdout: stdout, + Stderr: stderr, + }) + if err != nil { + s.Log.Errorf("WS Emit : %v", err) + } + + // XXX - Workaround due to gdbserver bug that doesn't redirect + // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13) + if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' { + + // Extract and cleanup string like &"bla bla\n" + re := regexp.MustCompile("&\"(.*)\"") + rer := re.FindAllStringSubmatch(stdout, -1) + out := "" + if rer != nil && len(rer) > 0 { + for _, o := range rer { + if len(o) >= 1 { + out = strings.Replace(o[1], "\\n", "\n", -1) + out = strings.Replace(out, "\\r", "\r", -1) + out = strings.Replace(out, "\\t", "\t", -1) + + s.Log.Debugf("STDOUT INFERIOR: <<%v>>", out) + err := (*so).Emit(xsapiv1.ExecInferiorOutEvent, xsapiv1.ExecOutMsg{ + CmdID: e.CmdID, + Timestamp: time.Now().String(), + Stdout: out, + Stderr: "", + }) + if err != nil { + s.Log.Errorf("WS Emit : %v", err) + } + } + } + } else { + s.Log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout) + } + } + } + + // Define callback for output + execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) { + s.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err) + + // Close client tty + defer func() { + if gdbPty != nil { + gdbPty.Close() + } + if gdbTty != nil { + gdbTty.Close() + } + }() + + // IO socket can be nil when disconnected + so := s.sessions.IOSocketGet(e.Sid) + if so == nil { + s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.ExecExitEvent, e.CmdID) + return + } + + // Retrieve project ID and RootPath + data := e.UserData + prjID := (*data)["ID"].(string) + exitImm := (*data)["ExitImmediate"].(bool) + + // XXX - workaround to be sure that Syncthing detected all changes + if err := s.mfolders.ForceSync(prjID); err != nil { + s.Log.Errorf("Error while syncing folder %s: %v", prjID, err) + } + if !exitImm { + // Wait end of file sync + // FIXME pass as argument + tmo := 60 + for t := tmo; t > 0; t-- { + s.Log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo) + if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil { + if err != nil { + s.Log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err) + } + break + } + time.Sleep(time.Second) + } + s.Log.Debugf("OK file are synchronized.") + } + + // FIXME replace by .BroadcastTo a room + errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{ + CmdID: e.CmdID, + Timestamp: time.Now().String(), + Code: code, + Error: err, + }) + if errSoEmit != nil { + s.Log.Errorf("WS Emit : %v", errSoEmit) + } + } + + // User data (used within callbacks) + data := make(map[string]interface{}) + data["ID"] = prj.ID + data["ExitImmediate"] = args.ExitImmediate + if args.TTY && args.TTYGdbserverFix { + data["gdbServerTTY"] = "workaround" + } else { + data["gdbServerTTY"] = "" + } + execWS.UserData = &data + + // Start command execution + s.Log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args) + + err = execWS.Start() + if err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID}) +} + +// ExecCmd executes remotely a command +func (s *APIService) execSignalCmd(c *gin.Context) { + var args xsapiv1.ExecSignalArgs + + if c.BindJSON(&args) != nil { + common.APIError(c, "Invalid arguments") + return + } + + s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID) + + e := eows.GetEows(args.CmdID) + if e == nil { + common.APIError(c, "unknown cmdID") + return + } + + err := e.Signal(args.Signal) + if err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID}) +} diff --git a/lib/xdsserver/apiv1-folders.go b/lib/xdsserver/apiv1-folders.go new file mode 100644 index 0000000..fe11e52 --- /dev/null +++ b/lib/xdsserver/apiv1-folders.go @@ -0,0 +1,131 @@ +package xdsserver + +import ( + "net/http" + "os" + + "github.com/gin-gonic/gin" + common "github.com/iotbzh/xds-common/golib" + "github.com/iotbzh/xds-server/lib/xsapiv1" +) + +// getFolders returns all folders configuration +func (s *APIService) getFolders(c *gin.Context) { + c.JSON(http.StatusOK, s.mfolders.GetConfigArr()) +} + +// getFolder returns a specific folder configuration +func (s *APIService) getFolder(c *gin.Context) { + id, err := s.mfolders.ResolveID(c.Param("id")) + if err != nil { + common.APIError(c, err.Error()) + return + } + f := s.mfolders.Get(id) + if f == nil { + common.APIError(c, "Invalid id") + return + } + + c.JSON(http.StatusOK, (*f).GetConfig()) +} + +// addFolder adds a new folder to server config +func (s *APIService) addFolder(c *gin.Context) { + var cfgArg xsapiv1.FolderConfig + if c.BindJSON(&cfgArg) != nil { + common.APIError(c, "Invalid arguments") + return + } + + s.Log.Debugln("Add folder config: ", cfgArg) + + newFld, err := s.mfolders.Add(cfgArg) + if err != nil { + common.APIError(c, err.Error()) + return + } + + // Create xds-project.conf file + // FIXME: move to folders.createUpdate func (but gin context needed) + fld := s.mfolders.Get(newFld.ID) + prjConfFile := (*fld).GetFullPath("xds-project.conf") + if !common.Exists(prjConfFile) { + fd, err := os.OpenFile(prjConfFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) + if err != nil { + common.APIError(c, err.Error()) + return + } + fd.WriteString("# XDS project settings\n") + fd.WriteString("export XDS_SERVER_URL=" + c.Request.Host + "\n") + fd.WriteString("export XDS_PROJECT_ID=" + newFld.ID + "\n") + if newFld.DefaultSdk == "" { + sdks := s.sdks.GetAll() + newFld.DefaultSdk = sdks[0].ID + } + fd.WriteString("export XDS_SDK_ID=" + newFld.DefaultSdk + "\n") + fd.Close() + } + + c.JSON(http.StatusOK, newFld) +} + +// syncFolder force synchronization of folder files +func (s *APIService) syncFolder(c *gin.Context) { + id, err := s.mfolders.ResolveID(c.Param("id")) + if err != nil { + common.APIError(c, err.Error()) + return + } + s.Log.Debugln("Sync folder id: ", id) + + err = s.mfolders.ForceSync(id) + if err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, "") +} + +// delFolder deletes folder from server config +func (s *APIService) delFolder(c *gin.Context) { + id, err := s.mfolders.ResolveID(c.Param("id")) + if err != nil { + common.APIError(c, err.Error()) + return + } + + s.Log.Debugln("Delete folder id ", id) + + delEntry, err := s.mfolders.Delete(id) + if err != nil { + common.APIError(c, err.Error()) + return + } + c.JSON(http.StatusOK, delEntry) +} + +// updateFolder update some field of a folder +func (s *APIService) updateFolder(c *gin.Context) { + id, err := s.mfolders.ResolveID(c.Param("id")) + if err != nil { + common.APIError(c, err.Error()) + return + } + + s.Log.Debugln("Update folder id ", id) + + var cfgArg xsapiv1.FolderConfig + if c.BindJSON(&cfgArg) != nil { + common.APIError(c, "Invalid arguments") + return + } + + upFld, err := s.mfolders.Update(id, cfgArg) + if err != nil { + common.APIError(c, err.Error()) + return + } + c.JSON(http.StatusOK, upFld) +} diff --git a/lib/xdsserver/apiv1-make.go b/lib/xdsserver/apiv1-make.go new file mode 100644 index 0000000..bcb4d95 --- /dev/null +++ b/lib/xdsserver/apiv1-make.go @@ -0,0 +1,214 @@ +package xdsserver + +import ( + "github.com/gin-gonic/gin" + common "github.com/iotbzh/xds-common/golib" +) + +/* TODO: Deprecated - should be removed +// MakeArgs is the parameters (json format) of /make command +type MakeArgs struct { + ID string `json:"id"` + SdkID string `json:"sdkID"` // sdk ID to use for setting env + CmdID string `json:"cmdID"` // command unique ID + Args []string `json:"args"` // args to pass to make command + Env []string `json:"env"` + RPath string `json:"rpath"` // relative path into project + ExitImmediate bool `json:"exitImmediate"` // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization) + CmdTimeout int `json:"timeout"` // command completion timeout in Second +} + +// MakeOutMsg Message send on each output (stdout+stderr) of make command +type MakeOutMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:"timestamp"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +// MakeExitMsg Message send on make command exit +type MakeExitMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:"timestamp"` + Code int `json:"code"` + Error error `json:"error"` +} + +// MakeOutEvent Event send in WS when characters are received on stdout/stderr +const MakeOutEvent = "make:output" + +// MakeExitEvent Event send in WS when command exited +const MakeExitEvent = "make:exit" + +var makeCommandID = 1 +*/ + +func (s *APIService) buildMake(c *gin.Context) { + common.APIError(c, "/make route is not longer supported, use /exec instead") + + /* + var args MakeArgs + + if c.BindJSON(&args) != nil { + common.APIError(c, "Invalid arguments") + return + } + + sess := s.sessions.Get(c) + if sess == nil { + common.APIError(c, "Unknown sessions") + return + } + sop := sess.IOSocket + if sop == nil { + common.APIError(c, "Websocket not established") + return + } + + // Allow to pass id in url (/make/:id) or as JSON argument + idArg := c.Param("id") + if idArg == "" { + idArg = args.ID + } + if idArg == "" { + common.APIError(c, "Invalid id") + return + } + id, err := s.mfolders.ResolveID(idArg) + if err != nil { + common.APIError(c, err.Error()) + return + } + pf := s.mfolders.Get(id) + if pf == nil { + common.APIError(c, "Unknown id") + return + } + folder := *pf + prj := folder.GetConfig() + + execTmo := args.CmdTimeout + if execTmo == 0 { + // TODO get default timeout from config.json file + execTmo = 24 * 60 * 60 // 1 day + } + + // TODO merge all code below with exec.go + + // Define callback for output + var oCB common.EmitOutputCB + oCB = func(sid string, cmdID string, stdout, stderr string, data *map[string]interface{}) { + // IO socket can be nil when disconnected + so := s.sessions.IOSocketGet(sid) + if so == nil { + s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%s", MakeOutEvent, sid, cmdID) + return + } + + // Retrieve project ID and RootPath + prjID := (*data)["ID"].(string) + prjRootPath := (*data)["RootPath"].(string) + + // Cleanup any references to internal rootpath in stdout & stderr + stdout = strings.Replace(stdout, prjRootPath, "", -1) + stderr = strings.Replace(stderr, prjRootPath, "", -1) + + s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", MakeOutEvent, sid, id, prjID) + + // FIXME replace by .BroadcastTo a room + err := (*so).Emit(MakeOutEvent, MakeOutMsg{ + CmdID: cmdID, + Timestamp: time.Now().String(), + Stdout: stdout, + Stderr: stderr, + }) + if err != nil { + s.log.Errorf("WS Emit : %v", err) + } + } + + // Define callback for output + eCB := func(sid string, cmdID string, code int, err error, data *map[string]interface{}) { + s.log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", cmdID, code, err) + + // IO socket can be nil when disconnected + so := s.sessions.IOSocketGet(sid) + if so == nil { + s.log.Infof("%s not emitted - WS closed (id:%s", MakeExitEvent, cmdID) + return + } + + // Retrieve project ID and RootPath + prjID := (*data)["ID"].(string) + exitImm := (*data)["ExitImmediate"].(bool) + + // XXX - workaround to be sure that Syncthing detected all changes + if err := s.mfolders.ForceSync(prjID); err != nil { + s.log.Errorf("Error while syncing folder %s: %v", prjID, err) + } + if !exitImm { + // Wait end of file sync + // FIXME pass as argument + tmo := 60 + for t := tmo; t > 0; t-- { + s.log.Debugf("Wait file insync for %s (%d/%d)", prjID, t, tmo) + if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil { + if err != nil { + s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err) + } + break + } + time.Sleep(time.Second) + } + } + + // FIXME replace by .BroadcastTo a room + e := (*so).Emit(MakeExitEvent, MakeExitMsg{ + CmdID: id, + Timestamp: time.Now().String(), + Code: code, + Error: err, + }) + if e != nil { + s.log.Errorf("WS Emit : %v", e) + } + } + + // Unique ID for each commands + if args.CmdID == "" { + args.CmdID = s.cfg.ServerUID[:18] + "_" + strconv.Itoa(makeCommandID) + makeCommandID++ + } + cmd := []string{} + + // Retrieve env command regarding Sdk ID + if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 { + cmd = append(cmd, envCmd...) + cmd = append(cmd, "&&") + } + + cmd = append(cmd, "cd", folder.GetFullPath(args.RPath), "&&", "make") + if len(args.Args) > 0 { + cmd = append(cmd, args.Args...) + } + + s.log.Debugf("Execute [Cmd ID %d]: %v", args.CmdID, cmd) + + data := make(map[string]interface{}) + data["ID"] = prj.ID + data["RootPath"] = prj.RootPath + data["ExitImmediate"] = args.ExitImmediate + + err = common.ExecPipeWs(cmd, args.Env, sop, sess.ID, args.CmdID, execTmo, s.log, oCB, eCB, &data) + if err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, + gin.H{ + "status": "OK", + "cmdID": args.CmdID, + }) + */ +} diff --git a/lib/xdsserver/apiv1-sdks.go b/lib/xdsserver/apiv1-sdks.go new file mode 100644 index 0000000..be9fcf7 --- /dev/null +++ b/lib/xdsserver/apiv1-sdks.go @@ -0,0 +1,29 @@ +package xdsserver + +import ( + "net/http" + + "github.com/gin-gonic/gin" + common "github.com/iotbzh/xds-common/golib" +) + +// getSdks returns all SDKs configuration +func (s *APIService) getSdks(c *gin.Context) { + c.JSON(http.StatusOK, s.sdks.GetAll()) +} + +// getSdk returns a specific Sdk configuration +func (s *APIService) getSdk(c *gin.Context) { + id, err := s.sdks.ResolveID(c.Param("id")) + if err != nil { + common.APIError(c, err.Error()) + return + } + sdk := s.sdks.Get(id) + if sdk.Profile == "" { + common.APIError(c, "Invalid id") + return + } + + c.JSON(http.StatusOK, sdk) +} diff --git a/lib/xdsserver/apiv1-version.go b/lib/xdsserver/apiv1-version.go new file mode 100644 index 0000000..2c2547c --- /dev/null +++ b/lib/xdsserver/apiv1-version.go @@ -0,0 +1,20 @@ +package xdsserver + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/iotbzh/xds-server/lib/xsapiv1" +) + +// getInfo : return various information about server +func (s *APIService) getVersion(c *gin.Context) { + response := xsapiv1.Version{ + ID: s.Config.ServerUID, + Version: s.Config.Version, + APIVersion: s.Config.APIVersion, + VersionGitTag: s.Config.VersionGitTag, + } + + c.JSON(http.StatusOK, response) +} diff --git a/lib/xdsserver/apiv1.go b/lib/xdsserver/apiv1.go new file mode 100644 index 0000000..1f6df9e --- /dev/null +++ b/lib/xdsserver/apiv1.go @@ -0,0 +1,47 @@ +package xdsserver + +import ( + "github.com/gin-gonic/gin" +) + +// APIService . +type APIService struct { + *Context + apiRouter *gin.RouterGroup +} + +// NewAPIV1 creates a new instance of API service +func NewAPIV1(ctx *Context) *APIService { + s := &APIService{ + Context: ctx, + apiRouter: ctx.WWWServer.router.Group("/api/v1"), + } + + s.apiRouter.GET("/version", s.getVersion) + + s.apiRouter.GET("/config", s.getConfig) + s.apiRouter.POST("/config", s.setConfig) + + s.apiRouter.GET("/folders", s.getFolders) + s.apiRouter.GET("/folders/:id", s.getFolder) + s.apiRouter.PUT("/folders/:id", s.updateFolder) + s.apiRouter.POST("/folders", s.addFolder) + s.apiRouter.POST("/folders/sync/:id", s.syncFolder) + s.apiRouter.DELETE("/folders/:id", s.delFolder) + + s.apiRouter.GET("/sdks", s.getSdks) + s.apiRouter.GET("/sdks/:id", s.getSdk) + + s.apiRouter.POST("/make", s.buildMake) + s.apiRouter.POST("/make/:id", s.buildMake) + + s.apiRouter.POST("/exec", s.execCmd) + s.apiRouter.POST("/exec/:id", s.execCmd) + s.apiRouter.POST("/signal", s.execSignalCmd) + + s.apiRouter.GET("/events", s.eventsList) + s.apiRouter.POST("/events/register", s.eventsRegister) + s.apiRouter.POST("/events/unregister", s.eventsUnRegister) + + return s +} diff --git a/lib/xdsserver/folder-interface.go b/lib/xdsserver/folder-interface.go new file mode 100644 index 0000000..c2b2ada --- /dev/null +++ b/lib/xdsserver/folder-interface.go @@ -0,0 +1,22 @@ +package xdsserver + +import "github.com/iotbzh/xds-server/lib/xsapiv1" + +type FolderEventCBData map[string]interface{} +type FolderEventCB func(cfg *xsapiv1.FolderConfig, data *FolderEventCBData) + +// IFOLDER Folder interface +type IFOLDER interface { + NewUID(suffix string) string // Get a new folder UUID + Add(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) // Add a new folder + GetConfig() xsapiv1.FolderConfig // Get folder public configuration + GetFullPath(dir string) string // Get folder full path + ConvPathCli2Svr(s string) string // Convert path from Client to Server + ConvPathSvr2Cli(s string) string // Convert path from Server to Client + Remove() error // Remove a folder + Update(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) // Update a new folder + RegisterEventChange(cb *FolderEventCB, data *FolderEventCBData) error // Request events registration (sent through WS) + UnRegisterEventChange() error // Un-register events + Sync() error // Force folder files synchronization + IsInSync() (bool, error) // Check if folder files are in-sync +} diff --git a/lib/xdsserver/folder-pathmap.go b/lib/xdsserver/folder-pathmap.go new file mode 100644 index 0000000..c5318de --- /dev/null +++ b/lib/xdsserver/folder-pathmap.go @@ -0,0 +1,175 @@ +package xdsserver + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + common "github.com/iotbzh/xds-common/golib" + "github.com/iotbzh/xds-server/lib/xsapiv1" + uuid "github.com/satori/go.uuid" +) + +// IFOLDER interface implementation for native/path mapping folders + +// PathMap . +type PathMap struct { + *Context + config xsapiv1.FolderConfig +} + +// NewFolderPathMap Create a new instance of PathMap +func NewFolderPathMap(ctx *Context) *PathMap { + f := PathMap{ + Context: ctx, + config: xsapiv1.FolderConfig{ + Status: xsapiv1.StatusDisable, + }, + } + return &f +} + +// NewUID Get a UUID +func (f *PathMap) NewUID(suffix string) string { + uuid := uuid.NewV1().String() + if len(suffix) > 0 { + uuid += "_" + suffix + } + return uuid +} + +// Add a new folder +func (f *PathMap) Add(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) { + if cfg.DataPathMap.ServerPath == "" { + return nil, fmt.Errorf("ServerPath must be set") + } + + // Use shareRootDir if ServerPath is a relative path + dir := cfg.DataPathMap.ServerPath + if !filepath.IsAbs(dir) { + dir = filepath.Join(f.Config.FileConf.ShareRootDir, dir) + } + + // Sanity check + if !common.Exists(dir) { + // try to create if not existing + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, fmt.Errorf("Cannot create ServerPath directory: %s", dir) + } + } + if !common.Exists(dir) { + return nil, fmt.Errorf("ServerPath directory is not accessible: %s", dir) + } + + f.config = cfg + f.config.RootPath = dir + f.config.DataPathMap.ServerPath = dir + f.config.IsInSync = true + + // Verify file created by XDS agent when needed + if cfg.DataPathMap.CheckFile != "" { + errMsg := "ServerPath sanity check error (%d): %v" + ckFile := f.ConvPathCli2Svr(cfg.DataPathMap.CheckFile) + if !common.Exists(ckFile) { + return nil, fmt.Errorf(errMsg, 1, "file not present") + } + if cfg.DataPathMap.CheckContent != "" { + fd, err := os.OpenFile(ckFile, os.O_APPEND|os.O_RDWR, 0600) + if err != nil { + return nil, fmt.Errorf(errMsg, 2, err) + } + defer fd.Close() + + // Check specific message written by agent + content, err := ioutil.ReadAll(fd) + if err != nil { + return nil, fmt.Errorf(errMsg, 3, err) + } + if string(content) != cfg.DataPathMap.CheckContent { + return nil, fmt.Errorf(errMsg, 4, "file content differ") + } + + // Write a specific message that will be check back on agent side + msg := "Pathmap checked message written by xds-server ID: " + f.Config.ServerUID + "\n" + if n, err := fd.WriteString(msg); n != len(msg) || err != nil { + return nil, fmt.Errorf(errMsg, 5, err) + } + } + } + + f.config.Status = xsapiv1.StatusEnable + + return &f.config, nil +} + +// GetConfig Get public part of folder config +func (f *PathMap) GetConfig() xsapiv1.FolderConfig { + return f.config +} + +// GetFullPath returns the full path of a directory (from server POV) +func (f *PathMap) GetFullPath(dir string) string { + if &dir == nil { + return f.config.DataPathMap.ServerPath + } + return filepath.Join(f.config.DataPathMap.ServerPath, dir) +} + +// ConvPathCli2Svr Convert path from Client to Server +func (f *PathMap) ConvPathCli2Svr(s string) string { + if f.config.ClientPath != "" && f.config.DataPathMap.ServerPath != "" { + return strings.Replace(s, + f.config.ClientPath, + f.config.DataPathMap.ServerPath, + -1) + } + return s +} + +// ConvPathSvr2Cli Convert path from Server to Client +func (f *PathMap) ConvPathSvr2Cli(s string) string { + if f.config.ClientPath != "" && f.config.DataPathMap.ServerPath != "" { + return strings.Replace(s, + f.config.DataPathMap.ServerPath, + f.config.ClientPath, + -1) + } + return s +} + +// Remove a folder +func (f *PathMap) Remove() error { + // nothing to do + return nil +} + +// Update update some fields of a folder +func (f *PathMap) Update(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) { + if f.config.ID != cfg.ID { + return nil, fmt.Errorf("Invalid id") + } + f.config = cfg + return &f.config, nil +} + +// RegisterEventChange requests registration for folder change event +func (f *PathMap) RegisterEventChange(cb *FolderEventCB, data *FolderEventCBData) error { + return nil +} + +// UnRegisterEventChange remove registered callback +func (f *PathMap) UnRegisterEventChange() error { + return nil +} + +// Sync Force folder files synchronization +func (f *PathMap) Sync() error { + return nil +} + +// IsInSync Check if folder files are in-sync +func (f *PathMap) IsInSync() (bool, error) { + return true, nil +} diff --git a/lib/xdsserver/folder-st-disable.go b/lib/xdsserver/folder-st-disable.go new file mode 100644 index 0000000..64b1efc --- /dev/null +++ b/lib/xdsserver/folder-st-disable.go @@ -0,0 +1,91 @@ +package xdsserver + +import ( + "github.com/iotbzh/xds-server/lib/xsapiv1" + uuid "github.com/satori/go.uuid" +) + +// IFOLDER interface implementation for disabled Syncthing folders +// It's a "fallback" interface used to keep syncthing folders config even +// when syncthing is not running. + +// STFolderDisable . +type STFolderDisable struct { + *Context + config xsapiv1.FolderConfig +} + +// NewFolderSTDisable Create a new instance of STFolderDisable +func NewFolderSTDisable(ctx *Context) *STFolderDisable { + f := STFolderDisable{ + Context: ctx, + } + return &f +} + +// NewUID Get a UUID +func (f *STFolderDisable) NewUID(suffix string) string { + uuid := uuid.NewV1().String() + if len(suffix) > 0 { + uuid += "_" + suffix + } + return uuid +} + +// Add a new folder +func (f *STFolderDisable) Add(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) { + f.config = cfg + f.config.Status = xsapiv1.StatusDisable + f.config.IsInSync = false + return &f.config, nil +} + +// GetConfig Get public part of folder config +func (f *STFolderDisable) GetConfig() xsapiv1.FolderConfig { + return f.config +} + +// GetFullPath returns the full path of a directory (from server POV) +func (f *STFolderDisable) GetFullPath(dir string) string { + return "" +} + +// ConvPathCli2Svr Convert path from Client to Server +func (f *STFolderDisable) ConvPathCli2Svr(s string) string { + return "" +} + +// ConvPathSvr2Cli Convert path from Server to Client +func (f *STFolderDisable) ConvPathSvr2Cli(s string) string { + return "" +} + +// Remove a folder +func (f *STFolderDisable) Remove() error { + return nil +} + +// Update update some fields of a folder +func (f *STFolderDisable) Update(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) { + return nil, nil +} + +// RegisterEventChange requests registration for folder change event +func (f *STFolderDisable) RegisterEventChange(cb *FolderEventCB, data *FolderEventCBData) error { + return nil +} + +// UnRegisterEventChange remove registered callback +func (f *STFolderDisable) UnRegisterEventChange() error { + return nil +} + +// Sync Force folder files synchronization +func (f *STFolderDisable) Sync() error { + return nil +} + +// IsInSync Check if folder files are in-sync +func (f *STFolderDisable) IsInSync() (bool, error) { + return false, nil +} diff --git a/lib/xdsserver/folder-st.go b/lib/xdsserver/folder-st.go new file mode 100644 index 0000000..04bbf76 --- /dev/null +++ b/lib/xdsserver/folder-st.go @@ -0,0 +1,213 @@ +package xdsserver + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/iotbzh/xds-server/lib/xsapiv1" + st "github.com/iotbzh/xds-server/lib/syncthing" + uuid "github.com/satori/go.uuid" + "github.com/syncthing/syncthing/lib/config" +) + +// IFOLDER interface implementation for syncthing + +// STFolder . +type STFolder struct { + *Context + st *st.SyncThing + fConfig xsapiv1.FolderConfig + stfConfig config.FolderConfiguration + eventIDs []int + eventChangeCB *FolderEventCB + eventChangeCBData *FolderEventCBData +} + +// NewFolderST Create a new instance of STFolder +func NewFolderST(ctx *Context, sthg *st.SyncThing) *STFolder { + return &STFolder{ + Context: ctx, + st: sthg, + } +} + +// NewUID Get a UUID +func (f *STFolder) NewUID(suffix string) string { + i := len(f.st.MyID) + if i > 15 { + i = 15 + } + uuid := uuid.NewV1().String()[:14] + f.st.MyID[:i] + if len(suffix) > 0 { + uuid += "_" + suffix + } + return uuid +} + +// Add a new folder +func (f *STFolder) Add(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) { + + // Sanity check + if cfg.DataCloudSync.SyncThingID == "" { + return nil, fmt.Errorf("device id not set (SyncThingID field)") + } + + // rootPath should not be empty + if cfg.RootPath == "" { + cfg.RootPath = f.Config.FileConf.ShareRootDir + } + + f.fConfig = cfg + + // Update Syncthing folder + // (except if status is ErrorConfig) + // TODO: add cache to avoid multiple requests on startup + if f.fConfig.Status != xsapiv1.StatusErrorConfig { + id, err := f.st.FolderChange(f.fConfig) + if err != nil { + return nil, err + } + + f.stfConfig, err = f.st.FolderConfigGet(id) + if err != nil { + f.fConfig.Status = xsapiv1.StatusErrorConfig + return nil, err + } + + // Register to events to update folder status + for _, evName := range []string{st.EventStateChanged, st.EventFolderPaused} { + evID, err := f.st.Events.Register(evName, f.cbEventState, id, nil) + if err != nil { + return nil, err + } + f.eventIDs = append(f.eventIDs, evID) + } + + f.fConfig.IsInSync = false // will be updated later by events + f.fConfig.Status = xsapiv1.StatusEnable + } + + return &f.fConfig, nil +} + +// GetConfig Get public part of folder config +func (f *STFolder) GetConfig() xsapiv1.FolderConfig { + return f.fConfig +} + +// GetFullPath returns the full path of a directory (from server POV) +func (f *STFolder) GetFullPath(dir string) string { + if &dir == nil { + dir = "" + } + if filepath.IsAbs(dir) { + return filepath.Join(f.fConfig.RootPath, dir) + } + return filepath.Join(f.fConfig.RootPath, f.fConfig.ClientPath, dir) +} + +// ConvPathCli2Svr Convert path from Client to Server +func (f *STFolder) ConvPathCli2Svr(s string) string { + if f.fConfig.ClientPath != "" && f.fConfig.RootPath != "" { + return strings.Replace(s, + f.fConfig.ClientPath, + f.fConfig.RootPath+"/"+f.fConfig.ClientPath, + -1) + } + return s +} + +// ConvPathSvr2Cli Convert path from Server to Client +func (f *STFolder) ConvPathSvr2Cli(s string) string { + if f.fConfig.ClientPath != "" && f.fConfig.RootPath != "" { + return strings.Replace(s, + f.fConfig.RootPath+"/"+f.fConfig.ClientPath, + f.fConfig.ClientPath, + -1) + } + return s +} + +// Remove a folder +func (f *STFolder) Remove() error { + err := f.st.FolderDelete(f.stfConfig.ID) + + // Delete folder on server side + err2 := os.RemoveAll(f.GetFullPath("")) + + if err != nil { + return err + } + return err2 +} + +// Update update some fields of a folder +func (f *STFolder) Update(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) { + if f.fConfig.ID != cfg.ID { + return nil, fmt.Errorf("Invalid id") + } + f.fConfig = cfg + return &f.fConfig, nil +} + +// RegisterEventChange requests registration for folder event change +func (f *STFolder) RegisterEventChange(cb *FolderEventCB, data *FolderEventCBData) error { + f.eventChangeCB = cb + f.eventChangeCBData = data + return nil +} + +// UnRegisterEventChange remove registered callback +func (f *STFolder) UnRegisterEventChange() error { + f.eventChangeCB = nil + f.eventChangeCBData = nil + return nil +} + +// Sync Force folder files synchronization +func (f *STFolder) Sync() error { + return f.st.FolderScan(f.stfConfig.ID, "") +} + +// IsInSync Check if folder files are in-sync +func (f *STFolder) IsInSync() (bool, error) { + sts, err := f.st.IsFolderInSync(f.stfConfig.ID) + if err != nil { + return false, err + } + f.fConfig.IsInSync = sts + return sts, nil +} + +// callback use to update IsInSync status +func (f *STFolder) cbEventState(ev st.Event, data *st.EventsCBData) { + prevSync := f.fConfig.IsInSync + prevStatus := f.fConfig.Status + + switch ev.Type { + + case st.EventStateChanged: + to := ev.Data["to"] + switch to { + case "scanning", "syncing": + f.fConfig.Status = xsapiv1.StatusSyncing + case "idle": + f.fConfig.Status = xsapiv1.StatusEnable + } + f.fConfig.IsInSync = (to == "idle") + + case st.EventFolderPaused: + if f.fConfig.Status == xsapiv1.StatusEnable { + f.fConfig.Status = xsapiv1.StatusPause + } + f.fConfig.IsInSync = false + } + + if f.eventChangeCB != nil && + (prevSync != f.fConfig.IsInSync || prevStatus != f.fConfig.Status) { + cpConf := f.fConfig + (*f.eventChangeCB)(&cpConf, f.eventChangeCBData) + } +} diff --git a/lib/xdsserver/folders.go b/lib/xdsserver/folders.go new file mode 100644 index 0000000..e36f84c --- /dev/null +++ b/lib/xdsserver/folders.go @@ -0,0 +1,458 @@ +package xdsserver + +import ( + "encoding/xml" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "time" + + "github.com/franciscocpg/reflectme" + common "github.com/iotbzh/xds-common/golib" + "github.com/iotbzh/xds-server/lib/xsapiv1" + "github.com/iotbzh/xds-server/lib/xdsconfig" + "github.com/syncthing/syncthing/lib/sync" +) + +// Folders Represent a an XDS folders +type Folders struct { + *Context + fileOnDisk string + folders map[string]*IFOLDER + registerCB []RegisteredCB +} + +// RegisteredCB Hold registered callbacks +type RegisteredCB struct { + cb *FolderEventCB + data *FolderEventCBData +} + +// Mutex to make add/delete atomic +var fcMutex = sync.NewMutex() +var ffMutex = sync.NewMutex() + +// FoldersNew Create a new instance of Model Folders +func FoldersNew(ctx *Context) *Folders { + file, _ := xdsconfig.FoldersConfigFilenameGet() + return &Folders{ + Context: ctx, + fileOnDisk: file, + folders: make(map[string]*IFOLDER), + registerCB: []RegisteredCB{}, + } +} + +// LoadConfig Load folders configuration from disk +func (f *Folders) LoadConfig() error { + var flds []xsapiv1.FolderConfig + var stFlds []xsapiv1.FolderConfig + + // load from disk + if f.Config.Options.NoFolderConfig { + f.Log.Infof("Don't read folder config file (-no-folderconfig option is set)") + } else if f.fileOnDisk != "" { + f.Log.Infof("Use folder config file: %s", f.fileOnDisk) + err := foldersConfigRead(f.fileOnDisk, &flds) + if err != nil { + if strings.HasPrefix(err.Error(), "No folder config") { + f.Log.Warnf(err.Error()) + } else { + return err + } + } + } else { + f.Log.Warnf("Folders config filename not set") + } + + // Retrieve initial Syncthing config (just append don't overwrite existing ones) + if f.SThg != nil { + f.Log.Infof("Retrieve syncthing folder config") + if err := f.SThg.FolderLoadFromStConfig(&stFlds); err != nil { + // Don't exit on such error, just log it + f.Log.Errorf(err.Error()) + } + } else { + f.Log.Infof("Syncthing support is disabled.") + } + + // Merge syncthing folders into XDS folders + for _, stf := range stFlds { + found := false + for i, xf := range flds { + if xf.ID == stf.ID { + found = true + // sanity check + if xf.Type != xsapiv1.TypeCloudSync { + flds[i].Status = xsapiv1.StatusErrorConfig + } + break + } + } + // add it + if !found { + flds = append(flds, stf) + } + } + + // Detect ghost project + // (IOW existing in xds file config and not in syncthing database) + if f.SThg != nil { + for i, xf := range flds { + // only for syncthing project + if xf.Type != xsapiv1.TypeCloudSync { + continue + } + found := false + for _, stf := range stFlds { + if stf.ID == xf.ID { + found = true + break + } + } + if !found { + flds[i].Status = xsapiv1.StatusErrorConfig + } + } + } + + // Update folders + f.Log.Infof("Loading initial folders config: %d folders found", len(flds)) + for _, fc := range flds { + if _, err := f.createUpdate(fc, false, true); err != nil { + return err + } + } + + // Save config on disk + err := f.SaveConfig() + + return err +} + +// SaveConfig Save folders configuration to disk +func (f *Folders) SaveConfig() error { + if f.fileOnDisk == "" { + return fmt.Errorf("Folders config filename not set") + } + + // FIXME: buffered save or avoid to write on disk each time + return foldersConfigWrite(f.fileOnDisk, f.getConfigArrUnsafe()) +} + +// ResolveID Complete a Folder ID (helper for user that can use partial ID value) +func (f *Folders) ResolveID(id string) (string, error) { + if id == "" { + return "", nil + } + + match := []string{} + for iid := range f.folders { + if strings.HasPrefix(iid, id) { + match = append(match, iid) + } + } + + if len(match) == 1 { + return match[0], nil + } else if len(match) == 0 { + return id, fmt.Errorf("Unknown id") + } + return id, fmt.Errorf("Multiple IDs found with provided prefix: " + id) +} + +// Get returns the folder config or nil if not existing +func (f *Folders) Get(id string) *IFOLDER { + if id == "" { + return nil + } + fc, exist := f.folders[id] + if !exist { + return nil + } + return fc +} + +// GetConfigArr returns the config of all folders as an array +func (f *Folders) GetConfigArr() []xsapiv1.FolderConfig { + fcMutex.Lock() + defer fcMutex.Unlock() + + return f.getConfigArrUnsafe() +} + +// getConfigArrUnsafe Same as GetConfigArr without mutex protection +func (f *Folders) getConfigArrUnsafe() []xsapiv1.FolderConfig { + conf := []xsapiv1.FolderConfig{} + for _, v := range f.folders { + conf = append(conf, (*v).GetConfig()) + } + return conf +} + +// Add adds a new folder +func (f *Folders) Add(newF xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) { + return f.createUpdate(newF, true, false) +} + +// CreateUpdate creates or update a folder +func (f *Folders) createUpdate(newF xsapiv1.FolderConfig, create bool, initial bool) (*xsapiv1.FolderConfig, error) { + + fcMutex.Lock() + defer fcMutex.Unlock() + + // Sanity check + if _, exist := f.folders[newF.ID]; create && exist { + return nil, fmt.Errorf("ID already exists") + } + if newF.ClientPath == "" { + return nil, fmt.Errorf("ClientPath must be set") + } + + // Create a new folder object + var fld IFOLDER + switch newF.Type { + // SYNCTHING + case xsapiv1.TypeCloudSync: + if f.SThg != nil { + fld = NewFolderST(f.Context, f.SThg) + } else { + f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID) + fld = NewFolderSTDisable(f.Context) + } + + // PATH MAP + case xsapiv1.TypePathMap: + fld = NewFolderPathMap(f.Context) + default: + return nil, fmt.Errorf("Unsupported folder type") + } + + // Allocate a new UUID + if create { + newF.ID = fld.NewUID("") + } + if !create && newF.ID == "" { + return nil, fmt.Errorf("Cannot update folder with null ID") + } + + // Set default value if needed + if newF.Status == "" { + newF.Status = xsapiv1.StatusDisable + } + if newF.Label == "" { + newF.Label = filepath.Base(newF.ClientPath) + if len(newF.ID) > 8 { + newF.Label += "_" + newF.ID[0:8] + } + } + + // Normalize path (needed for Windows path including bashlashes) + newF.ClientPath = common.PathNormalize(newF.ClientPath) + + // Add new folder + newFolder, err := fld.Add(newF) + if err != nil { + newF.Status = xsapiv1.StatusErrorConfig + log.Printf("ERROR Adding folder: %v\n", err) + return newFolder, err + } + + // Add to folders list + f.folders[newF.ID] = &fld + + // Save config on disk + if !initial { + if err := f.SaveConfig(); err != nil { + return newFolder, err + } + } + + // Register event change callback + for _, rcb := range f.registerCB { + if err := fld.RegisterEventChange(rcb.cb, rcb.data); err != nil { + return newFolder, err + } + } + + // Force sync after creation + // (need to defer to be sure that WS events will arrive after HTTP creation reply) + go func() { + time.Sleep(time.Millisecond * 500) + fld.Sync() + }() + + return newFolder, nil +} + +// Delete deletes a specific folder +func (f *Folders) Delete(id string) (xsapiv1.FolderConfig, error) { + var err error + + fcMutex.Lock() + defer fcMutex.Unlock() + + fld := xsapiv1.FolderConfig{} + fc, exist := f.folders[id] + if !exist { + return fld, fmt.Errorf("unknown id") + } + + fld = (*fc).GetConfig() + + if err = (*fc).Remove(); err != nil { + return fld, err + } + + delete(f.folders, id) + + // Save config on disk + err = f.SaveConfig() + + return fld, err +} + +// Update Update a specific folder +func (f *Folders) Update(id string, cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) { + fcMutex.Lock() + defer fcMutex.Unlock() + + fc, exist := f.folders[id] + if !exist { + return nil, fmt.Errorf("unknown id") + } + + // Copy current in a new object to change nothing in case of an error rises + newCfg := xsapiv1.FolderConfig{} + reflectme.Copy((*fc).GetConfig(), &newCfg) + + // Only update some fields + dirty := false + for _, fieldName := range xsapiv1.FolderConfigUpdatableFields { + valNew, err := reflectme.GetField(cfg, fieldName) + if err == nil { + valCur, err := reflectme.GetField(newCfg, fieldName) + if err == nil && valNew != valCur { + err = reflectme.SetField(&newCfg, fieldName, valNew) + if err != nil { + return nil, err + } + dirty = true + } + } + } + + if !dirty { + return &newCfg, nil + } + + fld, err := (*fc).Update(newCfg) + if err != nil { + return fld, err + } + + // Save config on disk + err = f.SaveConfig() + + // Send event to notified changes + // TODO emit folder change event + + return fld, err +} + +// RegisterEventChange requests registration for folder event change +func (f *Folders) RegisterEventChange(id string, cb *FolderEventCB, data *FolderEventCBData) error { + + flds := make(map[string]*IFOLDER) + if id != "" { + // Register to a specific folder + flds[id] = f.Get(id) + } else { + // Register to all folders + flds = f.folders + f.registerCB = append(f.registerCB, RegisteredCB{cb: cb, data: data}) + } + + for _, fld := range flds { + err := (*fld).RegisterEventChange(cb, data) + if err != nil { + return err + } + } + + return nil +} + +// ForceSync Force the synchronization of a folder +func (f *Folders) ForceSync(id string) error { + fc := f.Get(id) + if fc == nil { + return fmt.Errorf("Unknown id") + } + return (*fc).Sync() +} + +// IsFolderInSync Returns true when folder is in sync +func (f *Folders) IsFolderInSync(id string) (bool, error) { + fc := f.Get(id) + if fc == nil { + return false, fmt.Errorf("Unknown id") + } + return (*fc).IsInSync() +} + +//*** Private functions *** + +// Use XML format and not json to be able to save/load all fields including +// ones that are masked in json (IOW defined with `json:"-"`) +type xmlFolders struct { + XMLName xml.Name `xml:"folders"` + Version string `xml:"version,attr"` + Folders []xsapiv1.FolderConfig `xml:"folders"` +} + +// foldersConfigRead reads folders config from disk +func foldersConfigRead(file string, folders *[]xsapiv1.FolderConfig) error { + if !common.Exists(file) { + return fmt.Errorf("No folder config file found (%s)", file) + } + + ffMutex.Lock() + defer ffMutex.Unlock() + + fd, err := os.Open(file) + defer fd.Close() + if err != nil { + return err + } + + data := xmlFolders{} + err = xml.NewDecoder(fd).Decode(&data) + if err == nil { + *folders = data.Folders + } + return err +} + +// foldersConfigWrite writes folders config on disk +func foldersConfigWrite(file string, folders []xsapiv1.FolderConfig) error { + ffMutex.Lock() + defer ffMutex.Unlock() + + fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) + defer fd.Close() + if err != nil { + return err + } + + data := &xmlFolders{ + Version: "1", + Folders: folders, + } + + enc := xml.NewEncoder(fd) + enc.Indent("", " ") + return enc.Encode(data) +} diff --git a/lib/xdsserver/sdk.go b/lib/xdsserver/sdk.go new file mode 100644 index 0000000..c0acb24 --- /dev/null +++ b/lib/xdsserver/sdk.go @@ -0,0 +1,56 @@ +package xdsserver + +import ( + "fmt" + "path/filepath" + + "github.com/iotbzh/xds-server/lib/xsapiv1" + uuid "github.com/satori/go.uuid" +) + +// CrossSDK Hold SDK config +type CrossSDK struct { + sdk xsapiv1.SDK +} + +// NewCrossSDK creates a new instance of Syncthing +func NewCrossSDK(path string) (*CrossSDK, error) { + // Assume that we have .../<profile>/<version>/<arch> + s := CrossSDK{ + sdk: xsapiv1.SDK{Path: path}, + } + + s.sdk.Arch = filepath.Base(path) + + d := filepath.Dir(path) + s.sdk.Version = filepath.Base(d) + + d = filepath.Dir(d) + s.sdk.Profile = filepath.Base(d) + + // Use V3 to ensure that we get same uuid on restart + s.sdk.ID = uuid.NewV3(uuid.FromStringOrNil("sdks"), s.sdk.Profile+"_"+s.sdk.Arch+"_"+s.sdk.Version).String() + s.sdk.Name = s.sdk.Arch + " (" + s.sdk.Version + ")" + + envFile := filepath.Join(path, "environment-setup*") + ef, err := filepath.Glob(envFile) + if err != nil { + return nil, fmt.Errorf("Cannot retrieve environment setup file: %v", err) + } + if len(ef) != 1 { + return nil, fmt.Errorf("No environment setup file found match %s", envFile) + } + s.sdk.EnvFile = ef[0] + + return &s, nil +} + +// Get Return SDK definition +func (s *CrossSDK) Get() *xsapiv1.SDK { + return &s.sdk +} + +// GetEnvCmd returns the command used to initialized the environment +func (s *CrossSDK) GetEnvCmd() []string { + return []string{"source", s.sdk.EnvFile} +} diff --git a/lib/xdsserver/sdks.go b/lib/xdsserver/sdks.go new file mode 100644 index 0000000..1a40ab5 --- /dev/null +++ b/lib/xdsserver/sdks.go @@ -0,0 +1,127 @@ +package xdsserver + +import ( + "fmt" + "path" + "path/filepath" + "strings" + "sync" + + common "github.com/iotbzh/xds-common/golib" + "github.com/iotbzh/xds-server/lib/xsapiv1" +) + +// SDKs List of installed SDK +type SDKs struct { + *Context + Sdks map[string]*CrossSDK + + mutex sync.Mutex +} + +// NewSDKs creates a new instance of SDKs +func NewSDKs(ctx *Context) (*SDKs, error) { + s := SDKs{ + Context: ctx, + Sdks: make(map[string]*CrossSDK), + } + + // Retrieve installed sdks + sdkRD := ctx.Config.FileConf.SdkRootDir + + if common.Exists(sdkRD) { + + // Assume that SDK install tree is <rootdir>/<profile>/<version>/<arch> + dirs, err := filepath.Glob(path.Join(sdkRD, "*", "*", "*")) + if err != nil { + ctx.Log.Debugf("Error while retrieving SDKs: dir=%s, error=%s", sdkRD, err.Error()) + return &s, err + } + s.mutex.Lock() + defer s.mutex.Unlock() + + for _, d := range dirs { + if !common.IsDir(d) { + continue + } + cSdk, err := NewCrossSDK(d) + if err != nil { + ctx.Log.Debugf("Error while processing SDK dir=%s, err=%s", d, err.Error()) + continue + } + s.Sdks[cSdk.sdk.ID] = cSdk + } + } + + ctx.Log.Debugf("SDKs: %d cross sdks found", len(s.Sdks)) + + return &s, nil +} + +// ResolveID Complete an SDK ID (helper for user that can use partial ID value) +func (s *SDKs) ResolveID(id string) (string, error) { + if id == "" { + return "", nil + } + + match := []string{} + for iid := range s.Sdks { + if strings.HasPrefix(iid, id) { + match = append(match, iid) + } + } + + if len(match) == 1 { + return match[0], nil + } else if len(match) == 0 { + return id, fmt.Errorf("Unknown sdk id") + } + return id, fmt.Errorf("Multiple sdk IDs found with provided prefix: " + id) +} + +// Get returns an SDK from id +func (s *SDKs) Get(id string) *xsapiv1.SDK { + s.mutex.Lock() + defer s.mutex.Unlock() + + sc, exist := s.Sdks[id] + if !exist { + return nil + } + return (*sc).Get() +} + +// GetAll returns all existing SDKs +func (s *SDKs) GetAll() []xsapiv1.SDK { + s.mutex.Lock() + defer s.mutex.Unlock() + res := []xsapiv1.SDK{} + for _, v := range s.Sdks { + res = append(res, *(*v).Get()) + } + return res +} + +// GetEnvCmd returns the command used to initialized the environment for an SDK +func (s *SDKs) GetEnvCmd(id string, defaultID string) []string { + if id == "" && defaultID == "" { + // no env cmd + return []string{} + } + + s.mutex.Lock() + defer s.mutex.Unlock() + + if iid, err := s.ResolveID(id); err == nil { + if sdk, exist := s.Sdks[iid]; exist { + return sdk.GetEnvCmd() + } + } + + if sdk, exist := s.Sdks[defaultID]; defaultID != "" && exist { + return sdk.GetEnvCmd() + } + + // Return default env that may be empty + return []string{} +} diff --git a/lib/xdsserver/sessions.go b/lib/xdsserver/sessions.go new file mode 100644 index 0000000..6da9fd8 --- /dev/null +++ b/lib/xdsserver/sessions.go @@ -0,0 +1,225 @@ +package xdsserver + +import ( + "encoding/base64" + "strconv" + "time" + + "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/googollee/go-socket.io" + uuid "github.com/satori/go.uuid" + "github.com/syncthing/syncthing/lib/sync" +) + +const sessionCookieName = "xds-sid" +const sessionHeaderName = "XDS-SID" + +const sessionMonitorTime = 10 // Time (in seconds) to schedule monitoring session tasks + +const initSessionMaxAge = 10 // Initial session max age in seconds +const maxSessions = 100000 // Maximum number of sessions in sessMap map + +const secureCookie = false // TODO: see https://github.com/astaxie/beego/blob/master/session/session.go#L218 + +// ClientSession contains the info of a user/client session +type ClientSession struct { + ID string + WSID string // only one WebSocket per client/session + MaxAge int64 + IOSocket *socketio.Socket + + // private + expireAt time.Time + useCount int64 +} + +// Sessions holds client sessions +type Sessions struct { + *Context + cookieMaxAge int64 + sessMap map[string]ClientSession + mutex sync.Mutex + log *logrus.Logger + LogLevelSilly bool + stop chan struct{} // signals intentional stop +} + +// NewClientSessions . +func NewClientSessions(ctx *Context, cookieMaxAge string) *Sessions { + ckMaxAge, err := strconv.ParseInt(cookieMaxAge, 10, 0) + if err != nil { + ckMaxAge = 0 + } + s := Sessions{ + Context: ctx, + cookieMaxAge: ckMaxAge, + sessMap: make(map[string]ClientSession), + mutex: sync.NewMutex(), + stop: make(chan struct{}), + } + s.WWWServer.router.Use(s.Middleware()) + + // Start monitoring of sessions Map (use to manage expiration and cleanup) + go s.monitorSessMap() + + return &s +} + +// Stop sessions management +func (s *Sessions) Stop() { + close(s.stop) +} + +// Middleware is used to managed session +func (s *Sessions) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + // FIXME Add CSRF management + + // Get session + sess := s.Get(c) + if sess == nil { + // Allocate a new session key and put in cookie + sess = s.newSession("") + } else { + s.refresh(sess.ID) + } + + // Set session in cookie and in header + // Do not set Domain to localhost (http://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain) + c.SetCookie(sessionCookieName, sess.ID, int(sess.MaxAge), "/", "", + secureCookie, false) + c.Header(sessionHeaderName, sess.ID) + + // Save session id in gin metadata + c.Set(sessionCookieName, sess.ID) + + c.Next() + } +} + +// Get returns the client session for a specific ID +func (s *Sessions) Get(c *gin.Context) *ClientSession { + var sid string + + // First get from gin metadata + v, exist := c.Get(sessionCookieName) + if v != nil { + sid = v.(string) + } + + // Then look in cookie + if !exist || sid == "" { + sid, _ = c.Cookie(sessionCookieName) + } + + // Then look in Header + if sid == "" { + sid = c.Request.Header.Get(sessionCookieName) + } + if sid != "" { + s.mutex.Lock() + defer s.mutex.Unlock() + if key, ok := s.sessMap[sid]; ok { + // TODO: return a copy ??? + return &key + } + } + return nil +} + +// IOSocketGet Get socketio definition from sid +func (s *Sessions) IOSocketGet(sid string) *socketio.Socket { + s.mutex.Lock() + defer s.mutex.Unlock() + sess, ok := s.sessMap[sid] + if ok { + return sess.IOSocket + } + return nil +} + +// UpdateIOSocket updates the IO Socket definition for of a session +func (s *Sessions) UpdateIOSocket(sid string, so *socketio.Socket) error { + s.mutex.Lock() + defer s.mutex.Unlock() + if _, ok := s.sessMap[sid]; ok { + sess := s.sessMap[sid] + if so == nil { + // Could be the case when socketio is closed/disconnected + sess.WSID = "" + } else { + sess.WSID = (*so).Id() + } + sess.IOSocket = so + s.sessMap[sid] = sess + } + return nil +} + +// nesSession Allocate a new client session +func (s *Sessions) newSession(prefix string) *ClientSession { + uuid := prefix + uuid.NewV4().String() + id := base64.URLEncoding.EncodeToString([]byte(uuid)) + se := ClientSession{ + ID: id, + WSID: "", + MaxAge: initSessionMaxAge, + IOSocket: nil, + expireAt: time.Now().Add(time.Duration(initSessionMaxAge) * time.Second), + useCount: 0, + } + s.mutex.Lock() + defer s.mutex.Unlock() + + s.sessMap[se.ID] = se + + s.log.Debugf("NEW session (%d): %s", len(s.sessMap), id) + return &se +} + +// refresh Move this session ID to the head of the list +func (s *Sessions) refresh(sid string) { + s.mutex.Lock() + defer s.mutex.Unlock() + + sess := s.sessMap[sid] + sess.useCount++ + if sess.MaxAge < s.cookieMaxAge && sess.useCount > 1 { + sess.MaxAge = s.cookieMaxAge + sess.expireAt = time.Now().Add(time.Duration(sess.MaxAge) * time.Second) + } + + // TODO - Add flood detection (like limit_req of nginx) + // (delayed request when to much requests in a short period of time) + + s.sessMap[sid] = sess +} + +func (s *Sessions) monitorSessMap() { + for { + select { + case <-s.stop: + s.log.Debugln("Stop monitorSessMap") + return + case <-time.After(sessionMonitorTime * time.Second): + if s.LogLevelSilly { + s.log.Debugf("Sessions Map size: %d", len(s.sessMap)) + s.log.Debugf("Sessions Map : %v", s.sessMap) + } + + if len(s.sessMap) > maxSessions { + s.log.Errorln("TOO MUCH sessions, cleanup old ones !") + } + + s.mutex.Lock() + for _, ss := range s.sessMap { + if ss.expireAt.Sub(time.Now()) < 0 { + s.log.Debugf("Delete expired session id: %s", ss.ID) + delete(s.sessMap, ss.ID) + } + } + s.mutex.Unlock() + } + } +} diff --git a/lib/xdsserver/webserver.go b/lib/xdsserver/webserver.go new file mode 100644 index 0000000..0e1676a --- /dev/null +++ b/lib/xdsserver/webserver.go @@ -0,0 +1,181 @@ +package xdsserver + +import ( + "fmt" + "log" + "net/http" + "os" + + "path" + + "github.com/Sirupsen/logrus" + "github.com/gin-contrib/static" + "github.com/gin-gonic/gin" + "github.com/googollee/go-socket.io" +) + +// WebServer . +type WebServer struct { + *Context + router *gin.Engine + api *APIService + sIOServer *socketio.Server + webApp *gin.RouterGroup + stop chan struct{} // signals intentional stop +} + +const indexFilename = "index.html" + +// NewWebServer creates an instance of WebServer +func NewWebServer(ctx *Context) *WebServer { + + // Setup logging for gin router + if ctx.Log.Level == logrus.DebugLevel { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + + // Redirect gin logs into another logger (LogVerboseOut may be stderr or a file) + gin.DefaultWriter = ctx.Config.LogVerboseOut + gin.DefaultErrorWriter = ctx.Config.LogVerboseOut + log.SetOutput(ctx.Config.LogVerboseOut) + + // FIXME - fix pb about isTerminal=false when out is in VSC Debug Console + + // Creates gin router + r := gin.New() + + svr := &WebServer{ + router: r, + api: nil, + sIOServer: nil, + webApp: nil, + stop: make(chan struct{}), + } + + return svr +} + +// Serve starts a new instance of the Web Server +func (s *WebServer) Serve() error { + var err error + + // Setup middlewares + s.router.Use(gin.Logger()) + s.router.Use(gin.Recovery()) + s.router.Use(s.middlewareXDSDetails()) + s.router.Use(s.middlewareCORS()) + + // Create REST API + s.api = NewAPIV1(s.Context) + + // Websocket routes + s.sIOServer, err = socketio.NewServer(nil) + if err != nil { + s.Log.Fatalln(err) + } + + s.router.GET("/socket.io/", s.socketHandler) + s.router.POST("/socket.io/", s.socketHandler) + /* TODO: do we want to support ws://... ? + s.router.Handle("WS", "/socket.io/", s.socketHandler) + s.router.Handle("WSS", "/socket.io/", s.socketHandler) + */ + + // Web Application (serve on / ) + idxFile := path.Join(s.Config.FileConf.WebAppDir, indexFilename) + if _, err := os.Stat(idxFile); err != nil { + s.Log.Fatalln("Web app directory not found, check/use webAppDir setting in config file: ", idxFile) + } + s.Log.Infof("Serve WEB app dir: %s", s.Config.FileConf.WebAppDir) + s.router.Use(static.Serve("/", static.LocalFile(s.Config.FileConf.WebAppDir, true))) + s.webApp = s.router.Group("/", s.serveIndexFile) + { + s.webApp.GET("/") + } + + // Serve in the background + serveError := make(chan error, 1) + go func() { + msg := fmt.Sprintf("Web Server running on localhost:%s ...\n", s.Config.FileConf.HTTPPort) + s.Log.Infof(msg) + fmt.Printf(msg) + serveError <- http.ListenAndServe(":"+s.Config.FileConf.HTTPPort, s.router) + }() + + // Wait for stop, restart or error signals + select { + case <-s.stop: + // Shutting down permanently + s.sessions.Stop() + s.Log.Infoln("shutting down (stop)") + case err = <-serveError: + // Error due to listen/serve failure + s.Log.Errorln(err) + } + + return nil +} + +// Stop web server +func (s *WebServer) Stop() { + close(s.stop) +} + +// serveIndexFile provides initial file (eg. index.html) of webapp +func (s *WebServer) serveIndexFile(c *gin.Context) { + c.HTML(200, indexFilename, gin.H{}) +} + +// Add details in Header +func (s *WebServer) middlewareXDSDetails() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("XDS-Version", s.Config.Version) + c.Header("XDS-API-Version", s.Config.APIVersion) + c.Next() + } +} + +// CORS middleware +func (s *WebServer) middlewareCORS() gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Method == "OPTIONS" { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Headers", "Content-Type") + c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE") + c.Header("Access-Control-Max-Age", cookieMaxAge) + c.AbortWithStatus(204) + return + } + + c.Next() + } +} + +// socketHandler is the handler for the "main" websocket connection +func (s *WebServer) socketHandler(c *gin.Context) { + + // Retrieve user session + sess := s.sessions.Get(c) + if sess == nil { + c.JSON(500, gin.H{"error": "Cannot retrieve session"}) + return + } + + s.sIOServer.On("connection", func(so socketio.Socket) { + s.Log.Debugf("WS Connected (SID=%v)", so.Id()) + s.sessions.UpdateIOSocket(sess.ID, &so) + + so.On("disconnection", func() { + s.Log.Debugf("WS disconnected (SID=%v)", so.Id()) + s.sessions.UpdateIOSocket(sess.ID, nil) + }) + }) + + s.sIOServer.On("error", func(so socketio.Socket, err error) { + s.Log.Errorf("WS SID=%v Error : %v", so.Id(), err.Error()) + }) + + s.sIOServer.ServeHTTP(c.Writer, c.Request) +} diff --git a/lib/xdsserver/xdsserver.go b/lib/xdsserver/xdsserver.go new file mode 100644 index 0000000..dcdedc4 --- /dev/null +++ b/lib/xdsserver/xdsserver.go @@ -0,0 +1,202 @@ +package xdsserver + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" + "github.com/iotbzh/xds-server/lib/xsapiv1" + + "github.com/iotbzh/xds-server/lib/syncthing" + "github.com/iotbzh/xds-server/lib/xdsconfig" +) + +const cookieMaxAge = "3600" + +// Context holds the XDS server context +type Context struct { + ProgName string + Cli *cli.Context + Config *xdsconfig.Config + Log *logrus.Logger + LogLevelSilly bool + SThg *st.SyncThing + SThgCmd *exec.Cmd + SThgInotCmd *exec.Cmd + mfolders *Folders + sdks *SDKs + WWWServer *WebServer + sessions *Sessions + Exit chan os.Signal +} + +// NewXdsServer Create a new instance of XDS server +func NewXdsServer(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{} + + sillyVal, sillyLog := os.LookupEnv("XDS_LOG_SILLY") + + // Define default configuration + ctx := Context{ + ProgName: cliCtx.App.Name, + Cli: cliCtx, + Log: log, + LogLevelSilly: (sillyLog && sillyVal == "1"), + Exit: make(chan os.Signal, 1), + } + + // register handler on SIGTERM / exit + signal.Notify(ctx.Exit, os.Interrupt, syscall.SIGTERM) + go handlerSigTerm(&ctx) + + return &ctx +} + +// Run Main function called to run XDS Server +func (ctx *Context) Run() (int, error) { + var err error + + // Logs redirected into a file when logfile option or logsDir config is set + ctx.Config.LogVerboseOut = os.Stderr + if ctx.Config.FileConf.LogsDir != "" { + if ctx.Config.Options.LogFile != "stdout" { + logFile := ctx.Config.Options.LogFile + + fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) + if err != nil { + msgErr := fmt.Sprintf("Cannot create log file %s", logFile) + return int(syscall.EPERM), fmt.Errorf(msgErr) + } + ctx.Log.Out = fdL + + ctx._logPrint("Logging file: %s\n", logFile) + } + + logFileHTTPReq := filepath.Join(ctx.Config.FileConf.LogsDir, "xds-server-verbose.log") + fdLH, err := os.OpenFile(logFileHTTPReq, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) + if err != nil { + msgErr := fmt.Sprintf("Cannot create log file %s", logFileHTTPReq) + return int(syscall.EPERM), fmt.Errorf(msgErr) + } + ctx.Config.LogVerboseOut = fdLH + + ctx._logPrint("Logging file for HTTP requests: %s\n", logFileHTTPReq) + } + + // Create syncthing instance when section "syncthing" is present in config.json + if ctx.Config.FileConf.SThgConf != nil { + ctx.SThg = st.NewSyncThing(ctx.Config, ctx.Log) + } + + // Start local instance of Syncthing and Syncthing-notify + if ctx.SThg != nil { + ctx.Log.Infof("Starting Syncthing...") + ctx.SThgCmd, err = ctx.SThg.Start() + if err != nil { + return -4, err + } + ctx._logPrint("Syncthing started (PID %d)\n", ctx.SThgCmd.Process.Pid) + + ctx.Log.Infof("Starting Syncthing-inotify...") + ctx.SThgInotCmd, err = ctx.SThg.StartInotify() + if err != nil { + return -4, err + } + ctx._logPrint("Syncthing-inotify started (PID %d)\n", ctx.SThgInotCmd.Process.Pid) + + // Establish connection with local Syncthing (retry if connection fail) + ctx._logPrint("Establishing connection with Syncthing...\n") + time.Sleep(2 * time.Second) + maxRetry := 30 + retry := maxRetry + err = nil + for retry > 0 { + if err = ctx.SThg.Connect(); err == nil { + break + } + ctx.Log.Warningf("Establishing connection to Syncthing (retry %d/%d)", retry, maxRetry) + time.Sleep(time.Second) + retry-- + } + if err != nil || retry == 0 { + return -4, err + } + + // FIXME: do we still need Builder notion ? if no cleanup + if ctx.Config.Builder, err = xdsconfig.NewBuilderConfig(ctx.SThg.MyID); err != nil { + return -4, err + } + ctx.Config.SupportedSharing[xsapiv1.TypeCloudSync] = true + } + + // Init model folder + ctx.mfolders = FoldersNew(ctx) + + // Load initial folders config from disk + if err := ctx.mfolders.LoadConfig(); err != nil { + return -5, err + } + + // Init cross SDKs + ctx.sdks, err = NewSDKs(ctx) + if err != nil { + return -6, err + } + + // Create Web Server + ctx.WWWServer = NewWebServer(ctx) + + // Sessions manager + ctx.sessions = NewClientSessions(ctx, cookieMaxAge) + + // Run Web Server until exit requested (blocking call) + if err = ctx.WWWServer.Serve(); err != nil { + ctx.Log.Println(err) + return -7, err + } + + return -99, fmt.Errorf("Program exited ") +} + +// Helper function to log message on both stdout and logger +func (ctx *Context) _logPrint(format string, args ...interface{}) { + fmt.Printf(format, args...) + if ctx.Log.Out != os.Stdout { + ctx.Log.Infof(format, args...) + } +} + +// Handle exit and properly stop/close all stuff +func handlerSigTerm(ctx *Context) { + <-ctx.Exit + if ctx.SThg != nil { + ctx.Log.Infof("Stoping Syncthing... (PID %d)", ctx.SThgCmd.Process.Pid) + ctx.SThg.Stop() + ctx.Log.Infof("Stoping Syncthing-inotify... (PID %d)", ctx.SThgInotCmd.Process.Pid) + ctx.SThg.StopInotify() + } + if ctx.WWWServer != nil { + ctx.Log.Infof("Stoping Web server...") + ctx.WWWServer.Stop() + } + os.Exit(0) +} |