From 2f7828d01f4c4ca2909f95f098627cd5475ed225 Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Wed, 29 Nov 2017 08:54:00 +0100 Subject: Refit source files to have a public xs-apiv1 lib package. Signed-off-by: Sebastien Douheret --- lib/apiv1/apiv1.go | 63 ----- lib/apiv1/config.go | 40 ---- lib/apiv1/events.go | 162 ------------- lib/apiv1/exec.go | 416 --------------------------------- lib/apiv1/folders.go | 131 ----------- lib/apiv1/make.go | 215 ----------------- lib/apiv1/sdks.go | 29 --- lib/apiv1/version.go | 26 --- lib/crosssdk/sdk.go | 56 ----- lib/crosssdk/sdks.go | 126 ---------- lib/folder/folder-interface.go | 80 ------- lib/folder/folder-pathmap.go | 175 -------------- lib/folder/folder-st-disable.go | 91 -------- lib/model/folders.go | 463 ------------------------------------- lib/session/session.go | 227 ------------------ lib/syncthing/folder-st.go | 213 ----------------- lib/syncthing/stfolder.go | 14 +- lib/webserver/server.go | 201 ---------------- lib/xdsconfig/builderconfig.go | 15 +- lib/xdsconfig/config.go | 23 +- lib/xdsserver/apiv1-config.go | 40 ++++ lib/xdsserver/apiv1-events.go | 132 +++++++++++ lib/xdsserver/apiv1-exec.go | 342 +++++++++++++++++++++++++++ lib/xdsserver/apiv1-folders.go | 131 +++++++++++ lib/xdsserver/apiv1-make.go | 214 +++++++++++++++++ lib/xdsserver/apiv1-sdks.go | 29 +++ lib/xdsserver/apiv1-version.go | 20 ++ lib/xdsserver/apiv1.go | 47 ++++ lib/xdsserver/folder-interface.go | 22 ++ lib/xdsserver/folder-pathmap.go | 175 ++++++++++++++ lib/xdsserver/folder-st-disable.go | 91 ++++++++ lib/xdsserver/folder-st.go | 213 +++++++++++++++++ lib/xdsserver/folders.go | 458 ++++++++++++++++++++++++++++++++++++ lib/xdsserver/sdk.go | 56 +++++ lib/xdsserver/sdks.go | 127 ++++++++++ lib/xdsserver/sessions.go | 225 ++++++++++++++++++ lib/xdsserver/webserver.go | 181 +++++++++++++++ lib/xdsserver/xdsserver.go | 202 ++++++++++++++++ lib/xsapiv1/config.go | 18 ++ lib/xsapiv1/events.go | 31 +++ lib/xsapiv1/exec.go | 76 ++++++ lib/xsapiv1/folders.go | 61 +++++ lib/xsapiv1/sdks.go | 14 ++ lib/xsapiv1/version.go | 9 + 44 files changed, 2937 insertions(+), 2743 deletions(-) delete mode 100644 lib/apiv1/apiv1.go delete mode 100644 lib/apiv1/config.go delete mode 100644 lib/apiv1/events.go delete mode 100644 lib/apiv1/exec.go delete mode 100644 lib/apiv1/folders.go delete mode 100644 lib/apiv1/make.go delete mode 100644 lib/apiv1/sdks.go delete mode 100644 lib/apiv1/version.go delete mode 100644 lib/crosssdk/sdk.go delete mode 100644 lib/crosssdk/sdks.go delete mode 100644 lib/folder/folder-interface.go delete mode 100644 lib/folder/folder-pathmap.go delete mode 100644 lib/folder/folder-st-disable.go delete mode 100644 lib/model/folders.go delete mode 100644 lib/session/session.go delete mode 100644 lib/syncthing/folder-st.go delete mode 100644 lib/webserver/server.go create mode 100644 lib/xdsserver/apiv1-config.go create mode 100644 lib/xdsserver/apiv1-events.go create mode 100644 lib/xdsserver/apiv1-exec.go create mode 100644 lib/xdsserver/apiv1-folders.go create mode 100644 lib/xdsserver/apiv1-make.go create mode 100644 lib/xdsserver/apiv1-sdks.go create mode 100644 lib/xdsserver/apiv1-version.go create mode 100644 lib/xdsserver/apiv1.go create mode 100644 lib/xdsserver/folder-interface.go create mode 100644 lib/xdsserver/folder-pathmap.go create mode 100644 lib/xdsserver/folder-st-disable.go create mode 100644 lib/xdsserver/folder-st.go create mode 100644 lib/xdsserver/folders.go create mode 100644 lib/xdsserver/sdk.go create mode 100644 lib/xdsserver/sdks.go create mode 100644 lib/xdsserver/sessions.go create mode 100644 lib/xdsserver/webserver.go create mode 100644 lib/xdsserver/xdsserver.go create mode 100644 lib/xsapiv1/config.go create mode 100644 lib/xsapiv1/events.go create mode 100644 lib/xsapiv1/exec.go create mode 100644 lib/xsapiv1/folders.go create mode 100644 lib/xsapiv1/sdks.go create mode 100644 lib/xsapiv1/version.go (limited to 'lib') diff --git a/lib/apiv1/apiv1.go b/lib/apiv1/apiv1.go deleted file mode 100644 index fffed2d..0000000 --- a/lib/apiv1/apiv1.go +++ /dev/null @@ -1,63 +0,0 @@ -package apiv1 - -import ( - "github.com/Sirupsen/logrus" - "github.com/gin-gonic/gin" - - "github.com/iotbzh/xds-server/lib/crosssdk" - "github.com/iotbzh/xds-server/lib/model" - "github.com/iotbzh/xds-server/lib/session" - "github.com/iotbzh/xds-server/lib/xdsconfig" -) - -// APIService . -type APIService struct { - router *gin.Engine - apiRouter *gin.RouterGroup - sessions *session.Sessions - cfg *xdsconfig.Config - mfolders *model.Folders - sdks *crosssdk.SDKs - log *logrus.Logger -} - -// New creates a new instance of API service -func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolders *model.Folders, sdks *crosssdk.SDKs) *APIService { - s := &APIService{ - router: r, - sessions: sess, - apiRouter: r.Group("/api/v1"), - cfg: cfg, - mfolders: mfolders, - sdks: sdks, - log: cfg.Log, - } - - 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/apiv1/config.go b/lib/apiv1/config.go deleted file mode 100644 index 4b53217..0000000 --- a/lib/apiv1/config.go +++ /dev/null @@ -1,40 +0,0 @@ -package apiv1 - -import ( - "net/http" - "sync" - - "github.com/gin-gonic/gin" - common "github.com/iotbzh/xds-common/golib" - "github.com/iotbzh/xds-server/lib/xdsconfig" -) - -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.cfg) -} - -// SetConfig sets server configuration -func (s *APIService) setConfig(c *gin.Context) { - // FIXME - must be tested - c.JSON(http.StatusNotImplemented, "Not implemented") - - var cfgArg xdsconfig.Config - - 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/apiv1/events.go b/lib/apiv1/events.go deleted file mode 100644 index d837571..0000000 --- a/lib/apiv1/events.go +++ /dev/null @@ -1,162 +0,0 @@ -package apiv1 - -import ( - "net/http" - "strings" - "time" - - "github.com/iotbzh/xds-server/lib/folder" - - "github.com/gin-gonic/gin" - common "github.com/iotbzh/xds-common/golib" -) - -// EventArgs is the parameters (json format) of /events/register command -type EventRegisterArgs struct { - Name string `json:"name"` - ProjectID string `json:"filterProjectID"` -} - -type EventUnRegisterArgs struct { - Name string `json:"name"` - ID int `json:"id"` -} - -// EventMsg Message send -type EventMsg struct { - Time string `json:"time"` - Type string `json:"type"` - Folder folder.FolderConfig `json:"folder"` -} - -// EventEvent Event send in WS when an internal event (eg. Syncthing event is received) -const ( - // EventTypePrefix Used as event prefix - EventTypePrefix = "event:" // following by event type - - // Supported Events type - EVTAll = EventTypePrefix + "all" - EVTFolderChange = EventTypePrefix + "folder-change" // type EventMsg with Data type apiv1.??? - EVTFolderStateChange = EventTypePrefix + "folder-state-change" // type EventMsg with Data type apiv1.??? -) - -// 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 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(EVTFolderStateChange, 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 folder.EventCB - cbFunc = func(cfg *folder.FolderConfig, data *folder.EventCBData) { - 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 := EventMsg{ - Time: time.Now().String(), - Type: evType, - Folder: *cfg, - } - - s.log.Debugf("WS Emit %s - Status=%10s, IsInSync=%6v, ID=%s", - EventTypePrefix+evType, cfg.Status, cfg.IsInSync, cfg.ID) - - if err := (*so).Emit(EventTypePrefix+evType, msg); err != nil { - s.log.Errorf("WS Emit Folder StateChanged event : %v", err) - } - } - data := make(folder.EventCBData) - 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 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/apiv1/exec.go b/lib/apiv1/exec.go deleted file mode 100644 index baf431f..0000000 --- a/lib/apiv1/exec.go +++ /dev/null @@ -1,416 +0,0 @@ -package apiv1 - -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/kr/pty" -) - -type ( - // ExecArgs JSON parameters of /exec command - ExecArgs struct { - ID string `json:"id" binding:"required"` - SdkID string `json:"sdkID"` // sdk ID to use for setting env - CmdID string `json:"cmdID"` // command unique ID - Cmd string `json:"cmd" binding:"required"` - Args []string `json:"args"` - Env []string `json:"env"` - RPath string `json:"rpath"` // relative path into project - TTY bool `json:"tty"` // Use a tty, specific to gdb --tty option - TTYGdbserverFix bool `json:"ttyGdbserverFix"` // Set to true to activate gdbserver workaround about inferior output - 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 - } - - // ExecRes JSON result of /exec command - ExecRes struct { - Status string `json:"status"` // status OK - CmdID string `json:"cmdID"` // command unique ID - } - - // ExecSigRes JSON result of /signal command - ExecSigRes struct { - Status string `json:"status"` // status OK - CmdID string `json:"cmdID"` // command unique ID - } - - // ExecInMsg Message used to received input characters (stdin) - ExecInMsg struct { - CmdID string `json:"cmdID"` - Timestamp string `json:"timestamp"` - Stdin string `json:"stdin"` - } - - // ExecOutMsg Message used to send output characters (stdout+stderr) - ExecOutMsg struct { - CmdID string `json:"cmdID"` - Timestamp string `json:"timestamp"` - Stdout string `json:"stdout"` - Stderr string `json:"stderr"` - } - - // ExecExitMsg Message sent when executed command exited - ExecExitMsg struct { - CmdID string `json:"cmdID"` - Timestamp string `json:"timestamp"` - Code int `json:"code"` - Error error `json:"error"` - } - - // ExecSignalArgs JSON parameters of /exec/signal command - ExecSignalArgs struct { - CmdID string `json:"cmdID" binding:"required"` // command id - Signal string `json:"signal" binding:"required"` // signal number - } -) - -const ( - // ExecInEvent Event send in WS when characters are sent (stdin) - ExecInEvent = "exec:input" - - // ExecOutEvent Event send in WS when characters are received (stdout or stderr) - ExecOutEvent = "exec:output" - - // ExecExitEvent Event send in WS when program exited - ExecExitEvent = "exec:exit" - - // ExecInferiorInEvent Event send in WS when characters are sent to an inferior (used by gdb inferior/tty) - ExecInferiorInEvent = "exec:inferior-input" - - // ExecInferiorOutEvent Event send in WS when characters are received by an inferior - ExecInferiorOutEvent = "exec:inferior-output" -) - -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 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.cfg.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 = 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)", 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", 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(ExecOutEvent, 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(ExecInferiorOutEvent, 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)", 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(ExecExitEvent, 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, ExecRes{Status: "OK", CmdID: execWS.CmdID}) -} - -// ExecCmd executes remotely a command -func (s *APIService) execSignalCmd(c *gin.Context) { - var args 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, ExecSigRes{Status: "OK", CmdID: args.CmdID}) -} diff --git a/lib/apiv1/folders.go b/lib/apiv1/folders.go deleted file mode 100644 index 073445c..0000000 --- a/lib/apiv1/folders.go +++ /dev/null @@ -1,131 +0,0 @@ -package apiv1 - -import ( - "net/http" - "os" - - "github.com/gin-gonic/gin" - common "github.com/iotbzh/xds-common/golib" - "github.com/iotbzh/xds-server/lib/folder" -) - -// 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 folder.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 folder.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/apiv1/make.go b/lib/apiv1/make.go deleted file mode 100644 index 6e0c7d6..0000000 --- a/lib/apiv1/make.go +++ /dev/null @@ -1,215 +0,0 @@ -package apiv1 - -import ( - "net/http" - "strings" - - "time" - - "strconv" - - "github.com/gin-gonic/gin" - common "github.com/iotbzh/xds-common/golib" -) - -// 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) { - 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/apiv1/sdks.go b/lib/apiv1/sdks.go deleted file mode 100644 index f67a0ef..0000000 --- a/lib/apiv1/sdks.go +++ /dev/null @@ -1,29 +0,0 @@ -package apiv1 - -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/apiv1/version.go b/lib/apiv1/version.go deleted file mode 100644 index 8f928ec..0000000 --- a/lib/apiv1/version.go +++ /dev/null @@ -1,26 +0,0 @@ -package apiv1 - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -type version struct { - ID string `json:"id"` - Version string `json:"version"` - APIVersion string `json:"apiVersion"` - VersionGitTag string `json:"gitTag"` -} - -// getInfo : return various information about server -func (s *APIService) getVersion(c *gin.Context) { - response := version{ - ID: s.cfg.ServerUID, - Version: s.cfg.Version, - APIVersion: s.cfg.APIVersion, - VersionGitTag: s.cfg.VersionGitTag, - } - - c.JSON(http.StatusOK, response) -} diff --git a/lib/crosssdk/sdk.go b/lib/crosssdk/sdk.go deleted file mode 100644 index 5be8954..0000000 --- a/lib/crosssdk/sdk.go +++ /dev/null @@ -1,56 +0,0 @@ -package crosssdk - -import ( - "fmt" - "path/filepath" - - uuid "github.com/satori/go.uuid" -) - -// SDK Define a cross tool chain used to build application -type SDK struct { - ID string `json:"id" binding:"required"` - Name string `json:"name"` - Profile string `json:"profile"` - Version string `json:"version"` - Arch string `json:"arch"` - Path string `json:"path"` - - // Not exported fields - EnvFile string `json:"-"` -} - -// NewCrossSDK creates a new instance of Syncthing -func NewCrossSDK(path string) (*SDK, error) { - // Assume that we have .../// - s := SDK{Path: path} - - s.Arch = filepath.Base(path) - - d := filepath.Dir(path) - s.Version = filepath.Base(d) - - d = filepath.Dir(d) - s.Profile = filepath.Base(d) - - // Use V3 to ensure that we get same uuid on restart - s.ID = uuid.NewV3(uuid.FromStringOrNil("sdks"), s.Profile+"_"+s.Arch+"_"+s.Version).String() - s.Name = s.Arch + " (" + s.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.EnvFile = ef[0] - - return &s, nil -} - -// GetEnvCmd returns the command used to initialized the environment -func (s *SDK) GetEnvCmd() []string { - return []string{"source", s.EnvFile} -} diff --git a/lib/crosssdk/sdks.go b/lib/crosssdk/sdks.go deleted file mode 100644 index a3da184..0000000 --- a/lib/crosssdk/sdks.go +++ /dev/null @@ -1,126 +0,0 @@ -package crosssdk - -import ( - "fmt" - "path" - "path/filepath" - "strings" - "sync" - - "github.com/Sirupsen/logrus" - common "github.com/iotbzh/xds-common/golib" - "github.com/iotbzh/xds-server/lib/xdsconfig" -) - -// SDKs List of installed SDK -type SDKs struct { - Sdks map[string]*SDK - - mutex sync.Mutex -} - -// Init creates a new instance of Syncthing -func Init(cfg *xdsconfig.Config, log *logrus.Logger) (*SDKs, error) { - s := SDKs{ - Sdks: make(map[string]*SDK), - } - - // Retrieve installed sdks - sdkRD := cfg.FileConf.SdkRootDir - - if common.Exists(sdkRD) { - - // Assume that SDK install tree is /// - dirs, err := filepath.Glob(path.Join(sdkRD, "*", "*", "*")) - if err != nil { - 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 - } - sdk, err := NewCrossSDK(d) - if err != nil { - log.Debugf("Error while processing SDK dir=%s, err=%s", d, err.Error()) - continue - } - s.Sdks[sdk.ID] = sdk - } - } - - 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) *SDK { - s.mutex.Lock() - defer s.mutex.Unlock() - - sc, exist := s.Sdks[id] - if !exist { - return nil - } - return sc -} - -// GetAll returns all existing SDKs -func (s *SDKs) GetAll() []SDK { - s.mutex.Lock() - defer s.mutex.Unlock() - res := []SDK{} - for _, v := range s.Sdks { - res = append(res, *v) - } - 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/folder/folder-interface.go b/lib/folder/folder-interface.go deleted file mode 100644 index 3208869..0000000 --- a/lib/folder/folder-interface.go +++ /dev/null @@ -1,80 +0,0 @@ -package folder - -// FolderType definition -type FolderType string - -const ( - TypePathMap = "PathMap" - TypeCloudSync = "CloudSync" - TypeCifsSmb = "CIFS" -) - -// Folder Status definition -const ( - StatusErrorConfig = "ErrorConfig" - StatusDisable = "Disable" - StatusEnable = "Enable" - StatusPause = "Pause" - StatusSyncing = "Syncing" -) - -type EventCBData map[string]interface{} -type EventCB func(cfg *FolderConfig, data *EventCBData) - -// IFOLDER Folder interface -type IFOLDER interface { - NewUID(suffix string) string // Get a new folder UUID - Add(cfg FolderConfig) (*FolderConfig, error) // Add a new folder - GetConfig() 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 FolderConfig) (*FolderConfig, error) // Update a new folder - RegisterEventChange(cb *EventCB, data *EventCBData) 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 -} - -// FolderConfig is the config for one folder -type FolderConfig struct { - ID string `json:"id"` - Label string `json:"label"` - ClientPath string `json:"path"` - Type FolderType `json:"type"` - Status string `json:"status"` - IsInSync bool `json:"isInSync"` - DefaultSdk string `json:"defaultSdk"` - ClientData string `json:"clientData"` // free form field that can used by client - - // Not exported fields from REST API point of view - RootPath string `json:"-"` - - // FIXME: better to define an equivalent to union data and then implement - // UnmarshalJSON/MarshalJSON to decode/encode according to Type value - // Data interface{} `json:"data"` - - // Specific data depending on which Type is used - DataPathMap PathMapConfig `json:"dataPathMap,omitempty"` - DataCloudSync CloudSyncConfig `json:"dataCloudSync,omitempty"` -} - -// FolderConfigUpdatableFields List fields that can be updated using Update function -var FolderConfigUpdatableFields = []string{ - "Label", "DefaultSdk", "ClientData", -} - -// PathMapConfig Path mapping specific data -type PathMapConfig struct { - ServerPath string `json:"serverPath"` - - // Don't keep temporary file name (IOW we don't want to save it and reuse it) - CheckFile string `json:"checkFile" xml:"-"` - CheckContent string `json:"checkContent" xml:"-"` -} - -// CloudSyncConfig CloudSync (AKA Syncthing) specific data -type CloudSyncConfig struct { - SyncThingID string `json:"syncThingID"` -} diff --git a/lib/folder/folder-pathmap.go b/lib/folder/folder-pathmap.go deleted file mode 100644 index c5691a3..0000000 --- a/lib/folder/folder-pathmap.go +++ /dev/null @@ -1,175 +0,0 @@ -package folder - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - - common "github.com/iotbzh/xds-common/golib" - "github.com/iotbzh/xds-server/lib/xdsconfig" - uuid "github.com/satori/go.uuid" -) - -// IFOLDER interface implementation for native/path mapping folders - -// PathMap . -type PathMap struct { - globalConfig *xdsconfig.Config - config FolderConfig -} - -// NewFolderPathMap Create a new instance of PathMap -func NewFolderPathMap(gc *xdsconfig.Config) *PathMap { - f := PathMap{ - globalConfig: gc, - config: FolderConfig{ - Status: 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 FolderConfig) (*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.globalConfig.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.globalConfig.ServerUID + "\n" - if n, err := fd.WriteString(msg); n != len(msg) || err != nil { - return nil, fmt.Errorf(errMsg, 5, err) - } - } - } - - f.config.Status = StatusEnable - - return &f.config, nil -} - -// GetConfig Get public part of folder config -func (f *PathMap) GetConfig() 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 FolderConfig) (*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 *EventCB, data *EventCBData) 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/folder/folder-st-disable.go b/lib/folder/folder-st-disable.go deleted file mode 100644 index e936494..0000000 --- a/lib/folder/folder-st-disable.go +++ /dev/null @@ -1,91 +0,0 @@ -package folder - -import ( - "github.com/iotbzh/xds-server/lib/xdsconfig" - 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 { - globalConfig *xdsconfig.Config - config FolderConfig -} - -// NewFolderSTDisable Create a new instance of STFolderDisable -func NewFolderSTDisable(gc *xdsconfig.Config) *STFolderDisable { - f := STFolderDisable{ - globalConfig: gc, - } - 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 FolderConfig) (*FolderConfig, error) { - f.config = cfg - f.config.Status = StatusDisable - f.config.IsInSync = false - return &f.config, nil -} - -// GetConfig Get public part of folder config -func (f *STFolderDisable) GetConfig() 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 FolderConfig) (*FolderConfig, error) { - return nil, nil -} - -// RegisterEventChange requests registration for folder change event -func (f *STFolderDisable) RegisterEventChange(cb *EventCB, data *EventCBData) 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/model/folders.go b/lib/model/folders.go deleted file mode 100644 index 0e28538..0000000 --- a/lib/model/folders.go +++ /dev/null @@ -1,463 +0,0 @@ -package model - -import ( - "encoding/xml" - "fmt" - "log" - "os" - "path/filepath" - "strings" - "time" - - "github.com/Sirupsen/logrus" - "github.com/franciscocpg/reflectme" - common "github.com/iotbzh/xds-common/golib" - "github.com/iotbzh/xds-server/lib/folder" - "github.com/iotbzh/xds-server/lib/syncthing" - "github.com/iotbzh/xds-server/lib/xdsconfig" - "github.com/syncthing/syncthing/lib/sync" -) - -// Folders Represent a an XDS folders -type Folders struct { - fileOnDisk string - Conf *xdsconfig.Config - Log *logrus.Logger - SThg *st.SyncThing - folders map[string]*folder.IFOLDER - registerCB []RegisteredCB -} - -type RegisteredCB struct { - cb *folder.EventCB - data *folder.EventCBData -} - -// Mutex to make add/delete atomic -var fcMutex = sync.NewMutex() -var ffMutex = sync.NewMutex() - -// FoldersNew Create a new instance of Model Folders -func FoldersNew(cfg *xdsconfig.Config, st *st.SyncThing) *Folders { - file, _ := xdsconfig.FoldersConfigFilenameGet() - return &Folders{ - fileOnDisk: file, - Conf: cfg, - Log: cfg.Log, - SThg: st, - folders: make(map[string]*folder.IFOLDER), - registerCB: []RegisteredCB{}, - } -} - -// LoadConfig Load folders configuration from disk -func (f *Folders) LoadConfig() error { - var flds []folder.FolderConfig - var stFlds []folder.FolderConfig - - // load from disk - if f.Conf.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 != folder.TypeCloudSync { - flds[i].Status = folder.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 != folder.TypeCloudSync { - continue - } - found := false - for _, stf := range stFlds { - if stf.ID == xf.ID { - found = true - break - } - } - if !found { - flds[i].Status = folder.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) *folder.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() []folder.FolderConfig { - fcMutex.Lock() - defer fcMutex.Unlock() - - return f.getConfigArrUnsafe() -} - -// getConfigArrUnsafe Same as GetConfigArr without mutex protection -func (f *Folders) getConfigArrUnsafe() []folder.FolderConfig { - conf := []folder.FolderConfig{} - for _, v := range f.folders { - conf = append(conf, (*v).GetConfig()) - } - return conf -} - -// Add adds a new folder -func (f *Folders) Add(newF folder.FolderConfig) (*folder.FolderConfig, error) { - return f.createUpdate(newF, true, false) -} - -// CreateUpdate creates or update a folder -func (f *Folders) createUpdate(newF folder.FolderConfig, create bool, initial bool) (*folder.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 folder.IFOLDER - switch newF.Type { - // SYNCTHING - case folder.TypeCloudSync: - if f.SThg != nil { - fld = f.SThg.NewFolderST(f.Conf) - } else { - f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID) - fld = folder.NewFolderSTDisable(f.Conf) - } - - // PATH MAP - case folder.TypePathMap: - fld = folder.NewFolderPathMap(f.Conf) - 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 = folder.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 = folder.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) (folder.FolderConfig, error) { - var err error - - fcMutex.Lock() - defer fcMutex.Unlock() - - fld := folder.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 folder.FolderConfig) (*folder.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 := folder.FolderConfig{} - reflectme.Copy((*fc).GetConfig(), &newCfg) - - // Only update some fields - dirty := false - for _, fieldName := range folder.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 *folder.EventCB, data *folder.EventCBData) error { - - flds := make(map[string]*folder.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 []folder.FolderConfig `xml:"folders"` -} - -// foldersConfigRead reads folders config from disk -func foldersConfigRead(file string, folders *[]folder.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 []folder.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/session/session.go b/lib/session/session.go deleted file mode 100644 index 60b7b8a..0000000 --- a/lib/session/session.go +++ /dev/null @@ -1,227 +0,0 @@ -package session - -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 { - router *gin.Engine - cookieMaxAge int64 - sessMap map[string]ClientSession - mutex sync.Mutex - log *logrus.Logger - LogLevelSilly bool - stop chan struct{} // signals intentional stop -} - -// NewClientSessions . -func NewClientSessions(router *gin.Engine, log *logrus.Logger, cookieMaxAge string, sillyLog bool) *Sessions { - ckMaxAge, err := strconv.ParseInt(cookieMaxAge, 10, 0) - if err != nil { - ckMaxAge = 0 - } - s := Sessions{ - router: router, - cookieMaxAge: ckMaxAge, - sessMap: make(map[string]ClientSession), - mutex: sync.NewMutex(), - log: log, - LogLevelSilly: sillyLog, - stop: make(chan struct{}), - } - s.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/syncthing/folder-st.go b/lib/syncthing/folder-st.go deleted file mode 100644 index 74aa4bb..0000000 --- a/lib/syncthing/folder-st.go +++ /dev/null @@ -1,213 +0,0 @@ -package st - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/iotbzh/xds-server/lib/folder" - "github.com/iotbzh/xds-server/lib/xdsconfig" - uuid "github.com/satori/go.uuid" - "github.com/syncthing/syncthing/lib/config" -) - -// IFOLDER interface implementation for syncthing - -// STFolder . -type STFolder struct { - globalConfig *xdsconfig.Config - st *SyncThing - fConfig folder.FolderConfig - stfConfig config.FolderConfiguration - eventIDs []int - eventChangeCB *folder.EventCB - eventChangeCBData *folder.EventCBData -} - -// NewFolderST Create a new instance of STFolder -func (s *SyncThing) NewFolderST(gc *xdsconfig.Config) *STFolder { - return &STFolder{ - globalConfig: gc, - st: s, - } -} - -// 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 folder.FolderConfig) (*folder.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.globalConfig.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 != folder.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 = folder.StatusErrorConfig - return nil, err - } - - // Register to events to update folder status - for _, evName := range []string{EventStateChanged, 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 = folder.StatusEnable - } - - return &f.fConfig, nil -} - -// GetConfig Get public part of folder config -func (f *STFolder) GetConfig() folder.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 folder.FolderConfig) (*folder.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 *folder.EventCB, data *folder.EventCBData) 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 Event, data *EventsCBData) { - prevSync := f.fConfig.IsInSync - prevStatus := f.fConfig.Status - - switch ev.Type { - - case EventStateChanged: - to := ev.Data["to"] - switch to { - case "scanning", "syncing": - f.fConfig.Status = folder.StatusSyncing - case "idle": - f.fConfig.Status = folder.StatusEnable - } - f.fConfig.IsInSync = (to == "idle") - - case EventFolderPaused: - if f.fConfig.Status == folder.StatusEnable { - f.fConfig.Status = folder.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/syncthing/stfolder.go b/lib/syncthing/stfolder.go index 503ba4b..1a5a7ec 100644 --- a/lib/syncthing/stfolder.go +++ b/lib/syncthing/stfolder.go @@ -6,13 +6,13 @@ import ( "path/filepath" "strings" - "github.com/iotbzh/xds-server/lib/folder" + "github.com/iotbzh/xds-server/lib/xsapiv1" stconfig "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/protocol" ) // FolderLoadFromStConfig Load/Retrieve folder config from syncthing database -func (s *SyncThing) FolderLoadFromStConfig(f *[]folder.FolderConfig) error { +func (s *SyncThing) FolderLoadFromStConfig(f *[]xsapiv1.FolderConfig) error { defaultSdk := "" // cannot know which was the default sdk @@ -36,15 +36,15 @@ func (s *SyncThing) FolderLoadFromStConfig(f *[]folder.FolderConfig) error { if cliPath == "" { cliPath = stFld.Path } - *f = append(*f, folder.FolderConfig{ + *f = append(*f, xsapiv1.FolderConfig{ ID: stFld.ID, Label: stFld.Label, ClientPath: strings.TrimRight(cliPath, "/"), - Type: folder.TypeCloudSync, - Status: folder.StatusDisable, + Type: xsapiv1.TypeCloudSync, + Status: xsapiv1.StatusDisable, DefaultSdk: defaultSdk, RootPath: s.conf.FileConf.ShareRootDir, - DataCloudSync: folder.CloudSyncConfig{SyncThingID: devID}, + DataCloudSync: xsapiv1.CloudSyncConfig{SyncThingID: devID}, }) } @@ -52,7 +52,7 @@ func (s *SyncThing) FolderLoadFromStConfig(f *[]folder.FolderConfig) error { } // FolderChange is called when configuration has changed -func (s *SyncThing) FolderChange(f folder.FolderConfig) (string, error) { +func (s *SyncThing) FolderChange(f xsapiv1.FolderConfig) (string, error) { // Get current config stCfg, err := s.ConfigGet() diff --git a/lib/webserver/server.go b/lib/webserver/server.go deleted file mode 100644 index 85a2c40..0000000 --- a/lib/webserver/server.go +++ /dev/null @@ -1,201 +0,0 @@ -package webserver - -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" - "github.com/iotbzh/xds-server/lib/apiv1" - "github.com/iotbzh/xds-server/lib/crosssdk" - "github.com/iotbzh/xds-server/lib/model" - "github.com/iotbzh/xds-server/lib/session" - "github.com/iotbzh/xds-server/lib/xdsconfig" -) - -// Server . -type Server struct { - router *gin.Engine - api *apiv1.APIService - sIOServer *socketio.Server - webApp *gin.RouterGroup - cfg *xdsconfig.Config - sessions *session.Sessions - mfolders *model.Folders - sdks *crosssdk.SDKs - log *logrus.Logger - sillyLog bool - stop chan struct{} // signals intentional stop -} - -const indexFilename = "index.html" -const cookieMaxAge = "3600" - -// New creates an instance of Server -func New(cfg *xdsconfig.Config, mfolders *model.Folders, sdks *crosssdk.SDKs, logr *logrus.Logger, sillyLog bool) *Server { - - // Setup logging for gin router - if logr.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 = cfg.LogVerboseOut - gin.DefaultErrorWriter = cfg.LogVerboseOut - log.SetOutput(cfg.LogVerboseOut) - - // FIXME - fix pb about isTerminal=false when out is in VSC Debug Console - - // Creates gin router - r := gin.New() - - svr := &Server{ - router: r, - api: nil, - sIOServer: nil, - webApp: nil, - cfg: cfg, - sessions: nil, - mfolders: mfolders, - sdks: sdks, - log: logr, - sillyLog: sillyLog, - stop: make(chan struct{}), - } - - return svr -} - -// Serve starts a new instance of the Web Server -func (s *Server) 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()) - - // Sessions manager - s.sessions = session.NewClientSessions(s.router, s.log, cookieMaxAge, s.sillyLog) - - // Create REST API - s.api = apiv1.New(s.router, s.sessions, s.cfg, s.mfolders, s.sdks) - - // 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.cfg.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.cfg.FileConf.WebAppDir) - s.router.Use(static.Serve("/", static.LocalFile(s.cfg.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.cfg.FileConf.HTTPPort) - s.log.Infof(msg) - fmt.Printf(msg) - serveError <- http.ListenAndServe(":"+s.cfg.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 *Server) Stop() { - close(s.stop) -} - -// serveIndexFile provides initial file (eg. index.html) of webapp -func (s *Server) serveIndexFile(c *gin.Context) { - c.HTML(200, indexFilename, gin.H{}) -} - -// Add details in Header -func (s *Server) middlewareXDSDetails() gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("XDS-Version", s.cfg.Version) - c.Header("XDS-API-Version", s.cfg.APIVersion) - c.Next() - } -} - -// CORS middleware -func (s *Server) 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 *Server) 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/xdsconfig/builderconfig.go b/lib/xdsconfig/builderconfig.go index 6fc1814..3b1ca55 100644 --- a/lib/xdsconfig/builderconfig.go +++ b/lib/xdsconfig/builderconfig.go @@ -3,24 +3,19 @@ package xdsconfig import ( "errors" "net" -) -// BuilderConfig represents the builder container configuration -type BuilderConfig struct { - IP string `json:"ip"` - Port string `json:"port"` - SyncThingID string `json:"syncThingID"` -} + "github.com/iotbzh/xds-server/lib/xsapiv1" +) // NewBuilderConfig creates a new BuilderConfig instance -func NewBuilderConfig(stID string) (BuilderConfig, error) { +func NewBuilderConfig(stID string) (xsapiv1.BuilderConfig, error) { // Do we really need it ? may be not accessible from client side ip, err := getLocalIP() if err != nil { - return BuilderConfig{}, err + return xsapiv1.BuilderConfig{}, err } - b := BuilderConfig{ + b := xsapiv1.BuilderConfig{ IP: ip, // TODO currently not used Port: "", // TODO currently not used SyncThingID: stID, diff --git a/lib/xdsconfig/config.go b/lib/xdsconfig/config.go index 0fc1346..0201d40 100644 --- a/lib/xdsconfig/config.go +++ b/lib/xdsconfig/config.go @@ -9,16 +9,13 @@ import ( "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" common "github.com/iotbzh/xds-common/golib" + "github.com/iotbzh/xds-server/lib/xsapiv1" ) // Config parameters (json format) of /config command type Config struct { - ServerUID string `json:"id"` - Version string `json:"version"` - APIVersion string `json:"apiVersion"` - VersionGitTag string `json:"gitTag"` - SupportedSharing map[string]bool `json:"supportedSharing"` - Builder BuilderConfig `json:"builder"` + // Public APIConfig fields + xsapiv1.APIConfig // Private (un-exported fields in REST GET /config route) Options Options `json:"-"` @@ -64,12 +61,14 @@ func Init(cliCtx *cli.Context, log *logrus.Logger) (*Config, error) { // Define default configuration c := Config{ - ServerUID: uuid, - Version: cliCtx.App.Metadata["version"].(string), - APIVersion: DefaultAPIVersion, - VersionGitTag: cliCtx.App.Metadata["git-tag"].(string), - Builder: BuilderConfig{}, - SupportedSharing: map[string]bool{ /*FIXME USE folder.TypePathMap*/ "PathMap": true}, + APIConfig: xsapiv1.APIConfig{ + ServerUID: uuid, + Version: cliCtx.App.Metadata["version"].(string), + APIVersion: DefaultAPIVersion, + VersionGitTag: cliCtx.App.Metadata["git-tag"].(string), + Builder: xsapiv1.BuilderConfig{}, + SupportedSharing: map[string]bool{ /*FIXME USE folder.TypePathMap*/ "PathMap": true}, + }, Options: Options{ ConfigFile: cliCtx.GlobalString("config"), 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 .../// + 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 /// + 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) +} diff --git a/lib/xsapiv1/config.go b/lib/xsapiv1/config.go new file mode 100644 index 0000000..33bc116 --- /dev/null +++ b/lib/xsapiv1/config.go @@ -0,0 +1,18 @@ +package xsapiv1 + +// APIConfig parameters (json format) of /config command +type APIConfig struct { + ServerUID string `json:"id"` + Version string `json:"version"` + APIVersion string `json:"apiVersion"` + VersionGitTag string `json:"gitTag"` + SupportedSharing map[string]bool `json:"supportedSharing"` + Builder BuilderConfig `json:"builder"` +} + +// BuilderConfig represents the builder container configuration +type BuilderConfig struct { + IP string `json:"ip"` + Port string `json:"port"` + SyncThingID string `json:"syncThingID"` +} diff --git a/lib/xsapiv1/events.go b/lib/xsapiv1/events.go new file mode 100644 index 0000000..1304b3a --- /dev/null +++ b/lib/xsapiv1/events.go @@ -0,0 +1,31 @@ +package xsapiv1 + +// EventRegisterArgs Parameters (json format) of /events/register command +type EventRegisterArgs struct { + Name string `json:"name"` + ProjectID string `json:"filterProjectID"` +} + +// EventUnRegisterArgs Parameters of /events/unregister command +type EventUnRegisterArgs struct { + Name string `json:"name"` + ID int `json:"id"` +} + +// EventMsg Message send +type EventMsg struct { + Time string `json:"time"` + Type string `json:"type"` + Folder FolderConfig `json:"folder"` +} + +// EventEvent Event send in WS when an internal event (eg. Syncthing event is received) +const ( + // EventTypePrefix Used as event prefix + EventTypePrefix = "event:" // following by event type + + // Supported Events type + EVTAll = EventTypePrefix + "all" + EVTFolderChange = EventTypePrefix + "folder-change" // type EventMsg with Data type xsapiv1.??? + EVTFolderStateChange = EventTypePrefix + "folder-state-change" // type EventMsg with Data type xsapiv1.??? +) diff --git a/lib/xsapiv1/exec.go b/lib/xsapiv1/exec.go new file mode 100644 index 0000000..dce9eff --- /dev/null +++ b/lib/xsapiv1/exec.go @@ -0,0 +1,76 @@ +package xsapiv1 + +type ( + // ExecArgs JSON parameters of /exec command + ExecArgs struct { + ID string `json:"id" binding:"required"` + SdkID string `json:"sdkID"` // sdk ID to use for setting env + CmdID string `json:"cmdID"` // command unique ID + Cmd string `json:"cmd" binding:"required"` + Args []string `json:"args"` + Env []string `json:"env"` + RPath string `json:"rpath"` // relative path into project + TTY bool `json:"tty"` // Use a tty, specific to gdb --tty option + TTYGdbserverFix bool `json:"ttyGdbserverFix"` // Set to true to activate gdbserver workaround about inferior output + 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 + } + + // ExecResult JSON result of /exec command + ExecResult struct { + Status string `json:"status"` // status OK + CmdID string `json:"cmdID"` // command unique ID + } + + // ExecSigResult JSON result of /signal command + ExecSigResult struct { + Status string `json:"status"` // status OK + CmdID string `json:"cmdID"` // command unique ID + } + + // ExecInMsg Message used to received input characters (stdin) + ExecInMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:"timestamp"` + Stdin string `json:"stdin"` + } + + // ExecOutMsg Message used to send output characters (stdout+stderr) + ExecOutMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:"timestamp"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + } + + // ExecExitMsg Message sent when executed command exited + ExecExitMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:"timestamp"` + Code int `json:"code"` + Error error `json:"error"` + } + + // ExecSignalArgs JSON parameters of /exec/signal command + ExecSignalArgs struct { + CmdID string `json:"cmdID" binding:"required"` // command id + Signal string `json:"signal" binding:"required"` // signal number + } +) + +const ( + // ExecInEvent Event send in WS when characters are sent (stdin) + ExecInEvent = "exec:input" + + // ExecOutEvent Event send in WS when characters are received (stdout or stderr) + ExecOutEvent = "exec:output" + + // ExecExitEvent Event send in WS when program exited + ExecExitEvent = "exec:exit" + + // ExecInferiorInEvent Event send in WS when characters are sent to an inferior (used by gdb inferior/tty) + ExecInferiorInEvent = "exec:inferior-input" + + // ExecInferiorOutEvent Event send in WS when characters are received by an inferior + ExecInferiorOutEvent = "exec:inferior-output" +) diff --git a/lib/xsapiv1/folders.go b/lib/xsapiv1/folders.go new file mode 100644 index 0000000..9506a1d --- /dev/null +++ b/lib/xsapiv1/folders.go @@ -0,0 +1,61 @@ +package xsapiv1 + +// FolderType definition +type FolderType string + +const ( + TypePathMap = "PathMap" + TypeCloudSync = "CloudSync" + TypeCifsSmb = "CIFS" +) + +// Folder Status definition +const ( + StatusErrorConfig = "ErrorConfig" + StatusDisable = "Disable" + StatusEnable = "Enable" + StatusPause = "Pause" + StatusSyncing = "Syncing" +) + +// FolderConfig is the config for one folder +type FolderConfig struct { + ID string `json:"id"` + Label string `json:"label"` + ClientPath string `json:"path"` + Type FolderType `json:"type"` + Status string `json:"status"` + IsInSync bool `json:"isInSync"` + DefaultSdk string `json:"defaultSdk"` + ClientData string `json:"clientData"` // free form field that can used by client + + // Not exported fields from REST API point of view + RootPath string `json:"-"` + + // FIXME: better to define an equivalent to union data and then implement + // UnmarshalJSON/MarshalJSON to decode/encode according to Type value + // Data interface{} `json:"data"` + + // Specific data depending on which Type is used + DataPathMap PathMapConfig `json:"dataPathMap,omitempty"` + DataCloudSync CloudSyncConfig `json:"dataCloudSync,omitempty"` +} + +// FolderConfigUpdatableFields List fields that can be updated using Update function +var FolderConfigUpdatableFields = []string{ + "Label", "DefaultSdk", "ClientData", +} + +// PathMapConfig Path mapping specific data +type PathMapConfig struct { + ServerPath string `json:"serverPath"` + + // Don't keep temporary file name (IOW we don't want to save it and reuse it) + CheckFile string `json:"checkFile" xml:"-"` + CheckContent string `json:"checkContent" xml:"-"` +} + +// CloudSyncConfig CloudSync (AKA Syncthing) specific data +type CloudSyncConfig struct { + SyncThingID string `json:"syncThingID"` +} diff --git a/lib/xsapiv1/sdks.go b/lib/xsapiv1/sdks.go new file mode 100644 index 0000000..5ca4dd5 --- /dev/null +++ b/lib/xsapiv1/sdks.go @@ -0,0 +1,14 @@ +package xsapiv1 + +// SDK Define a cross tool chain used to build application +type SDK struct { + ID string `json:"id" binding:"required"` + Name string `json:"name"` + Profile string `json:"profile"` + Version string `json:"version"` + Arch string `json:"arch"` + Path string `json:"path"` + + // Not exported fields + EnvFile string `json:"-"` +} diff --git a/lib/xsapiv1/version.go b/lib/xsapiv1/version.go new file mode 100644 index 0000000..8c3a742 --- /dev/null +++ b/lib/xsapiv1/version.go @@ -0,0 +1,9 @@ +package xsapiv1 + +// XDS server Version +type Version struct { + ID string `json:"id"` + Version string `json:"version"` + APIVersion string `json:"apiVersion"` + VersionGitTag string `json:"gitTag"` +} -- cgit 1.2.3-korg