Added target and terminal support. v1.1.0
authorSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 23 Feb 2018 17:45:15 +0000 (18:45 +0100)
committerSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 23 Feb 2018 17:45:15 +0000 (18:45 +0100)
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
19 files changed:
.vscode/settings.json
glide.yaml
lib/xdsconfig/fileconfig.go
lib/xdsserver/apiv1-exec.go
lib/xdsserver/apiv1-targets.go [new file with mode: 0644]
lib/xdsserver/apiv1.go
lib/xdsserver/events.go
lib/xdsserver/folders.go
lib/xdsserver/sdks.go
lib/xdsserver/sessions.go
lib/xdsserver/target-interface.go [new file with mode: 0644]
lib/xdsserver/target-standard.go [new file with mode: 0644]
lib/xdsserver/targets.go [new file with mode: 0644]
lib/xdsserver/terminal-interface.go [new file with mode: 0644]
lib/xdsserver/terminal-ssh.go [new file with mode: 0644]
lib/xdsserver/terminals.go [new file with mode: 0644]
lib/xdsserver/webserver.go
lib/xdsserver/xdsserver.go
lib/xsapiv1/targets.go [new file with mode: 0644]

index 9367870..7787bfa 100644 (file)
@@ -28,8 +28,9 @@
         "rpath", "WSID", "sess", "IXDS", "xdsconfig", "xdsserver", "mfolder",
         "inotify", "Inot", "pname", "pkill", "sdkid", "CLOUDSYNC", "xdsagent",
         "gdbserver", "golib", "eows", "mfolders", "IFOLDER", "flds", "dflt",
-        "stconfig", "reflectme", "franciscocpg", "crosssdk", "urfave", "EXEPATH", "conv", "Sillyf", "xsapiv",
-        "EVTSDK", "zillode", "gerrit"
+        "stconfig", "reflectme", "franciscocpg", "crosssdk", "urfave", "EXEPATH",
+        "conv", "Sillyf", "xsapiv", "EVTSDK", "zillode", "gerrit", "ITARGET",
+        "tgts", "ITERMINAL", "unregister", "Cifs"
     ],
     "editor.insertSpaces": true,
     "editor.detectIndentation": true
index 0b12db8..70502be 100644 (file)
@@ -25,7 +25,7 @@ import:
 - package: github.com/satori/go.uuid
   version: ^1.1.0
 - package: gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git
-  version: ^0.1.0
+  version: ~0.2.0
   subpackages:
   - golib/common
   - golib/eows
index ef207ec..86f39b5 100644 (file)
@@ -38,6 +38,8 @@ const (
        ServerDataFilename = "server-data.xml"
        // FoldersConfigFilename Folders config filename
        FoldersConfigFilename = "server-config_folders.xml"
+       // TargetsConfigFilename Targets config filename
+       TargetsConfigFilename = "server-config_targets.xml"
 )
 
 // SyncThingConf definition
@@ -182,6 +184,11 @@ func FoldersConfigFilenameGet() (string, error) {
        return configFilenameGet(FoldersConfigFilename)
 }
 
+// TargetsConfigFilenameGet Return the TargetsConfig filename
+func TargetsConfigFilenameGet() (string, error) {
+       return configFilenameGet(TargetsConfigFilename)
+}
+
 // ServerDataFilenameGet Return the ServerData filename
 func ServerDataFilenameGet() (string, error) {
        return configFilenameGet(ServerDataFilename)
index 2337de6..327c4c5 100644 (file)
@@ -136,6 +136,7 @@ func (s *APIService) execCmd(c *gin.Context) {
        // Create new execution over WS context
        execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
        execWS.Log = s.Log
+       execWS.OutSplit = eows.SplitChar
 
        // Append client project dir to environment
        execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
@@ -180,7 +181,7 @@ func (s *APIService) execCmd(c *gin.Context) {
                // 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)
+                       s.Log.Infof("%s not emitted: WS closed (sid:%s, CmdID:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
                        return
                }
 
diff --git a/lib/xdsserver/apiv1-targets.go b/lib/xdsserver/apiv1-targets.go
new file mode 100644 (file)
index 0000000..978dc75
--- /dev/null
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xdsserver
+
+import (
+       "net/http"
+
+       common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
+       "github.com/gin-gonic/gin"
+)
+
+/***
+ * Targets
+ ***/
+
+// getTargets returns all targets configuration
+func (s *APIService) getTargets(c *gin.Context) {
+       c.JSON(http.StatusOK, s.targets.GetConfigArr())
+}
+
+// getTarget returns a specific target configuration
+func (s *APIService) getTarget(c *gin.Context) {
+       id, err := s.targets.ResolveID(c.Param("id"))
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       f := s.targets.Get(id)
+       if f == nil {
+               common.APIError(c, "Invalid id")
+               return
+       }
+
+       c.JSON(http.StatusOK, (*f).GetConfig())
+}
+
+// addTarget adds a new target to server config
+func (s *APIService) addTarget(c *gin.Context) {
+       var cfgArg xsapiv1.TargetConfig
+       if c.BindJSON(&cfgArg) != nil {
+               common.APIError(c, "Invalid arguments")
+               return
+       }
+
+       s.Log.Debugln("Add target config: ", cfgArg)
+
+       newTgt, err := s.targets.Add(cfgArg)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       c.JSON(http.StatusOK, newTgt)
+}
+
+// delTarget deletes target from server config
+func (s *APIService) delTarget(c *gin.Context) {
+       id, err := s.targets.ResolveID(c.Param("id"))
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       s.Log.Debugln("Delete target id ", id)
+
+       delEntry, err := s.targets.Delete(id)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       c.JSON(http.StatusOK, delEntry)
+}
+
+/***
+ * Terminals
+ ***/
+// getTgtTerms Get list of all terminals
+func (s *APIService) getTgtTerms(c *gin.Context) {
+       id, err := s.targets.ResolveID(c.Param("id"))
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       res, err := s.targets.GetTerminalsArr(id)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       c.JSON(http.StatusOK, res)
+}
+
+// getTgtTerm Get info a terminal
+func (s *APIService) getTgtTerm(c *gin.Context) {
+       id, tid, err := s._decodeTermArgs(c)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       iTerm, err := s.targets.GetTerminal(id, tid)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       c.JSON(http.StatusOK, (*iTerm).GetConfig())
+}
+
+// createTgtTerm Create a new terminal
+func (s *APIService) createTgtTerm(c *gin.Context) {
+       s.updateTgtTerm(c)
+}
+
+// updateTgtTerm Update terminal config
+func (s *APIService) updateTgtTerm(c *gin.Context) {
+       var cfgArg xsapiv1.TerminalConfig
+
+       tgtID, termID, err := s._decodeTermArgs(c)
+       if tgtID == "" && err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       if err := c.BindJSON(&cfgArg); err != nil {
+               common.APIError(c, "Invalid arguments")
+               return
+       }
+       if cfgArg.ID == "" {
+               cfgArg.ID = termID
+       }
+       if termID != "" && cfgArg.ID != termID {
+               common.APIError(c, "Invalid arguments, inconsistent terminal id ")
+               return
+       }
+       s.Log.Debugln("Add or Update terminal config: ", cfgArg)
+       term, err := s.targets.CreateUpdateTerminal(tgtID, cfgArg, false)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       c.JSON(http.StatusOK, term)
+}
+
+// delTgtTerm Delete a terminal
+func (s *APIService) delTgtTerm(c *gin.Context) {
+
+       tgtID, termID, err := s._decodeTermArgs(c)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       term, err := s.targets.DeleteTerminal(tgtID, termID)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       c.JSON(http.StatusOK, term)
+}
+
+// openTgtTerm Open a target terminal/console
+func (s *APIService) openTgtTerm(c *gin.Context) {
+
+       id, tid, err := s._decodeTermArgs(c)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       // Retrieve session info
+       sess := s.sessions.Get(c)
+       if sess == nil {
+               common.APIError(c, "Unknown sessions")
+               return
+       }
+       sock := sess.IOSocket
+       if sock == nil {
+               common.APIError(c, "Websocket not established")
+               return
+       }
+
+       term, err := s.targets.OpenTerminal(id, tid, sock, sess.ID)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       c.JSON(http.StatusOK, term)
+}
+
+// closeTgtTerm Close a terminal
+func (s *APIService) closeTgtTerm(c *gin.Context) {
+       id, tid, err := s._decodeTermArgs(c)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       term, err := s.targets.CloseTerminal(id, tid)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       c.JSON(http.StatusOK, term)
+}
+
+// resizeTgtTerm Resize a terminal
+func (s *APIService) resizeTgtTerm(c *gin.Context) {
+       var sizeArg xsapiv1.TerminalResizeArgs
+
+       id, tid, err := s._decodeTermArgs(c)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       if err := c.BindJSON(&sizeArg); err != nil {
+               common.APIError(c, "Invalid arguments")
+               return
+       }
+
+       term, err := s.targets.ResizeTerminal(id, tid, sizeArg.Cols, sizeArg.Rows)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       c.JSON(http.StatusOK, term)
+}
+
+// signalTgtTerm Send a signal to a terminal
+func (s *APIService) signalTgtTerm(c *gin.Context) {
+       var sigArg xsapiv1.TerminalSignalArgs
+
+       id, tid, err := s._decodeTermArgs(c)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       sigName := c.Param("sig")
+       if sigName == "" {
+               if err := c.BindJSON(&sigArg); err != nil {
+                       common.APIError(c, "Invalid arguments")
+                       return
+               }
+               sigName = sigArg.Signal
+       }
+       if sigName == "" {
+               common.APIError(c, "Invalid arguments")
+               return
+       }
+
+       if err := s.targets.SignalTerminal(id, tid, sigName); err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       c.JSON(http.StatusOK, "")
+}
+
+// _decodeTermArgs Helper to decode arguments of Terminal routes
+func (s *APIService) _decodeTermArgs(c *gin.Context) (string, string, error) {
+       id, err := s.targets.ResolveID(c.Param("id"))
+       if err != nil {
+               return "", "", err
+       }
+
+       termID, err := s.targets.ResolveTerminalID(c.Param("tid"))
+       if err != nil {
+               return id, "", err
+       }
+
+       return id, termID, nil
+}
index 67d09b5..e0bfa7f 100644 (file)
@@ -63,5 +63,20 @@ func NewAPIV1(ctx *Context) *APIService {
        s.apiRouter.POST("/events/register", s.eventsRegister)
        s.apiRouter.POST("/events/unregister", s.eventsUnRegister)
 
+       s.apiRouter.GET("/targets", s.getTargets)
+       s.apiRouter.GET("/targets/:id", s.getTarget)
+       s.apiRouter.POST("/targets", s.addTarget)
+       s.apiRouter.DELETE("/targets/:id", s.delTarget)
+       s.apiRouter.GET("/targets/:id/terminals", s.getTgtTerms)
+       s.apiRouter.GET("/targets/:id/terminals/:tid", s.getTgtTerm)
+       s.apiRouter.POST("/targets/:id/terminals", s.createTgtTerm)
+       s.apiRouter.PUT("/targets/:id/terminals/:tid", s.updateTgtTerm)
+       s.apiRouter.DELETE("/targets/:id/terminals/:tid", s.delTgtTerm)
+       s.apiRouter.POST("/targets/:id/terminals/:tid/open", s.openTgtTerm)
+       s.apiRouter.POST("/targets/:id/terminals/:tid/close", s.closeTgtTerm)
+       s.apiRouter.POST("/targets/:id/terminals/:tid/resize", s.resizeTgtTerm)
+       s.apiRouter.POST("/targets/:id/terminals/:tid/signal", s.signalTgtTerm)
+       s.apiRouter.POST("/targets/:id/terminals/:tid/signal/:sig", s.signalTgtTerm)
+
        return s
 }
index 2528725..0a02ecd 100644 (file)
@@ -35,8 +35,8 @@ type Events struct {
        eventsMap map[string]*EventDef
 }
 
-// NewEvents creates an instance of Events
-func NewEvents(ctx *Context) *Events {
+// EventsConstructor creates an instance of Events
+func EventsConstructor(ctx *Context) *Events {
        evMap := make(map[string]*EventDef)
        for _, ev := range xsapiv1.EVTAllList {
                evMap[ev] = &EventDef{
index fa24878..d27a329 100644 (file)
@@ -51,8 +51,8 @@ type RegisteredCB struct {
 var fcMutex = sync.NewMutex()
 var ffMutex = sync.NewMutex()
 
-// FoldersNew Create a new instance of Model Folders
-func FoldersNew(ctx *Context) *Folders {
+// FoldersConstructor Create a new instance of Model Folders
+func FoldersConstructor(ctx *Context) *Folders {
        file, _ := xdsconfig.FoldersConfigFilenameGet()
        return &Folders{
                Context:    ctx,
@@ -74,7 +74,9 @@ func (f *Folders) LoadConfig() error {
                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") {
+                       if strings.HasPrefix(err.Error(), "EOF") {
+                               f.Log.Warnf("Empty folder config file")
+                       } else if strings.HasPrefix(err.Error(), "No folder config") {
                                f.Log.Warnf(err.Error())
                        } else {
                                return err
index 6094045..4a7ba84 100644 (file)
@@ -38,8 +38,8 @@ type SDKs struct {
        stop  chan struct{} // signals intentional stop
 }
 
-// NewSDKs creates a new instance of SDKs
-func NewSDKs(ctx *Context) (*SDKs, error) {
+// SDKsConstructor creates a new instance of SDKs
+func SDKsConstructor(ctx *Context) (*SDKs, error) {
        s := SDKs{
                Context:      ctx,
                Sdks:         make(map[string]*CrossSDK),
index 69fe819..0c16b99 100644 (file)
@@ -59,8 +59,8 @@ type Sessions struct {
        stop         chan struct{} // signals intentional stop
 }
 
-// NewClientSessions .
-func NewClientSessions(ctx *Context, cookieMaxAge string) *Sessions {
+// ClientSessionsConstructor .
+func ClientSessionsConstructor(ctx *Context, cookieMaxAge string) *Sessions {
        ckMaxAge, err := strconv.ParseInt(cookieMaxAge, 10, 0)
        if err != nil {
                ckMaxAge = 0
@@ -226,7 +226,7 @@ func (s *Sessions) monitorSessMap() {
 
                        s.mutex.Lock()
                        for _, ss := range s.sessMap {
-                               if ss.expireAt.Sub(time.Now()) < 0 {
+                               if ss.expireAt.Sub(time.Now()) <= 0 {
                                        s.Log.Debugf("Delete expired session id: %s", ss.ID)
                                        delete(s.sessMap, ss.ID)
                                }
diff --git a/lib/xdsserver/target-interface.go b/lib/xdsserver/target-interface.go
new file mode 100644 (file)
index 0000000..6d5bd7b
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xdsserver
+
+import "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
+
+// ITARGET Target interface
+type ITARGET interface {
+       NewUID(suffix string) string                                                     // Get a new target UUID
+       Add(cfg xsapiv1.TargetConfig, terms *Terminals) (*xsapiv1.TargetConfig, error)   // Add a new target
+       Delete() error                                                                   // Remove a target
+       Setup(prj xsapiv1.TargetConfig, terms *Terminals) (*xsapiv1.TargetConfig, error) // Local setup of the folder
+       GetConfig() xsapiv1.TargetConfig                                                 // Get target public configuration
+}
diff --git a/lib/xdsserver/target-standard.go b/lib/xdsserver/target-standard.go
new file mode 100644 (file)
index 0000000..2c1b068
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017-2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xdsserver
+
+import (
+       "fmt"
+
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
+       uuid "github.com/satori/go.uuid"
+)
+
+// ITARGET interface implementation for standard targets
+
+// TgtStd .
+type TgtStd struct {
+       *Context
+       TgtConfig xsapiv1.TargetConfig
+       terminals *Terminals
+}
+
+// NewTargetStandard Create a new instance of TgtStd
+func NewTargetStandard(ctx *Context) *TgtStd {
+       t := TgtStd{
+               Context: ctx,
+               TgtConfig: xsapiv1.TargetConfig{
+                       Status: xsapiv1.StatusTgtDisable,
+               },
+       }
+       return &t
+}
+
+// NewUID Get a UUID
+func (t *TgtStd) NewUID(suffix string) string {
+       uuid := uuid.NewV1().String()
+       if len(suffix) > 0 {
+               uuid += "_" + suffix
+       }
+       return uuid
+}
+
+// Add a new target
+func (t *TgtStd) Add(cfg xsapiv1.TargetConfig, terms *Terminals) (*xsapiv1.TargetConfig, error) {
+       return t.Setup(cfg, terms)
+}
+
+// Delete a target
+func (t *TgtStd) Delete() error {
+       // nothing to do
+       return nil
+}
+
+// Setup Setup local project config
+func (t *TgtStd) Setup(cfg xsapiv1.TargetConfig, terms *Terminals) (*xsapiv1.TargetConfig, error) {
+
+       if cfg.IP == "" {
+               return nil, fmt.Errorf("IP address must be set")
+       }
+
+       t.TgtConfig = cfg
+       t.terminals = terms
+
+       // FIXME: sanity check test ping IP
+
+       t.TgtConfig.Status = xsapiv1.StatusTgtEnable
+
+       return &t.TgtConfig, nil
+}
+
+// GetConfig Get public part of target config
+func (t *TgtStd) GetConfig() xsapiv1.TargetConfig {
+       // XXX - Need to manually update terminal definition ()
+       t.TgtConfig.Terms = (*t.terminals).GetConfigArr()
+       return t.TgtConfig
+}
diff --git a/lib/xdsserver/targets.go b/lib/xdsserver/targets.go
new file mode 100644 (file)
index 0000000..663233d
--- /dev/null
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xdsserver
+
+import (
+       "encoding/xml"
+       "fmt"
+       "log"
+       "os"
+       "strings"
+
+       common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xdsconfig"
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
+       socketio "github.com/googollee/go-socket.io"
+       "github.com/syncthing/syncthing/lib/sync"
+)
+
+// Targets Represent a XDS targets
+type Targets struct {
+       *Context
+       fileOnDisk string
+       tgts       map[string]*ITARGET
+       terminals  map[string]*Terminals
+}
+
+// Mutex to make add/delete atomic
+var tcMutex = sync.NewMutex()
+
+/***
+ * Targets
+ ***/
+
+// TargetsConstructor Create a new instance of Model Target
+func TargetsConstructor(ctx *Context) *Targets {
+       file, _ := xdsconfig.TargetsConfigFilenameGet()
+       return &Targets{
+               Context:    ctx,
+               fileOnDisk: file,
+               tgts:       make(map[string]*ITARGET),
+               terminals:  make(map[string]*Terminals),
+       }
+}
+
+// LoadConfig Load targets configuration from disk
+func (t *Targets) LoadConfig() error {
+       var tgts []xsapiv1.TargetConfig
+
+       if t.fileOnDisk != "" {
+               t.Log.Infof("Use target config file: %s", t.fileOnDisk)
+               err := targetsConfigRead(t.fileOnDisk, &tgts)
+               if err != nil {
+                       if strings.HasPrefix(err.Error(), "EOF") {
+                               t.Log.Warnf("Empty target config file")
+                       } else if strings.HasPrefix(err.Error(), "No target config") {
+                               t.Log.Warnf(err.Error())
+                       } else {
+                               return err
+                       }
+               }
+       } else {
+               t.Log.Warnf("Targets config filename not set")
+       }
+
+       // Update targets
+       t.Log.Infof("Loading initial targets config: %d targets found", len(tgts))
+       for _, tc := range tgts {
+               if _, err := t.createUpdate(tc, false, true); err != nil {
+                       return err
+               }
+       }
+
+       return nil
+}
+
+// SaveConfig Save targets configuration to disk
+func (t *Targets) SaveConfig() error {
+       if t.fileOnDisk == "" {
+               return fmt.Errorf("Targets config filename not set")
+       }
+
+       // FIXME: buffered save or avoid to write on disk each time
+       return targetsConfigWrite(t.fileOnDisk, t.getConfigArrUnsafe())
+}
+
+// ResolveID Complete a Target ID (helper for user that can use partial ID value)
+func (t *Targets) ResolveID(id string) (string, error) {
+       if id == "" {
+               return "", nil
+       }
+
+       match := []string{}
+       for iid := range t.tgts {
+               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 %v", match)
+}
+
+// Get returns the target config or nil if not existing
+func (t *Targets) Get(id string) *ITARGET {
+       if id == "" {
+               return nil
+       }
+       tc, exist := t.tgts[id]
+       if !exist {
+               return nil
+       }
+       return tc
+}
+
+// GetConfigArr returns the config of all targets as an array
+func (t *Targets) GetConfigArr() []xsapiv1.TargetConfig {
+       tcMutex.Lock()
+       defer tcMutex.Unlock()
+
+       return t.getConfigArrUnsafe()
+}
+
+// getConfigArrUnsafe Same as GetConfigArr without mutex protection
+func (t *Targets) getConfigArrUnsafe() []xsapiv1.TargetConfig {
+       conf := []xsapiv1.TargetConfig{}
+       for _, v := range t.tgts {
+               conf = append(conf, (*v).GetConfig())
+       }
+       return conf
+}
+
+// Add adds a new target
+func (t *Targets) Add(newT xsapiv1.TargetConfig) (*xsapiv1.TargetConfig, error) {
+       return t.createUpdate(newT, true, false)
+}
+
+// CreateUpdate creates or update a target
+func (t *Targets) createUpdate(newT xsapiv1.TargetConfig, create bool, initial bool) (*xsapiv1.TargetConfig, error) {
+       var err error
+
+       tcMutex.Lock()
+       defer tcMutex.Unlock()
+
+       // Sanity check
+       if _, exist := t.tgts[newT.ID]; exist {
+               return nil, fmt.Errorf("ID already exists")
+       }
+
+       var tgt ITARGET
+       switch newT.Type {
+       case xsapiv1.TypeTgtStandard:
+               tgt = NewTargetStandard(t.Context)
+       default:
+               return nil, fmt.Errorf("Unsupported target type")
+       }
+
+       // Allocate a new UUID
+       if create {
+               newT.ID = tgt.NewUID("")
+       }
+       if !create && newT.ID == "" {
+               return nil, fmt.Errorf("Cannot update target with null ID")
+       }
+
+       if newT.Name == "" {
+               newT.Name = "Target"
+               if len(newT.ID) > 8 {
+                       newT.Name += "_" + newT.ID[0:8]
+               } else {
+                       newT.Name += "_" + newT.ID
+               }
+       }
+
+       // Call terminals constructor the first time
+       var terms *Terminals
+       if _, exist := t.terminals[newT.ID]; !exist {
+               terms = TerminalsConstructor(t.Context)
+               t.terminals[newT.ID] = terms
+       } else {
+               terms = t.terminals[newT.ID]
+       }
+
+       var newTarget *xsapiv1.TargetConfig
+       if create {
+               // Add target
+               if newTarget, err = tgt.Add(newT, terms); err != nil {
+                       newT.Status = xsapiv1.StatusTgtErrorConfig
+                       log.Printf("ERROR Adding target: %v\n", err)
+                       return newTarget, err
+               }
+       } else {
+               // Just update target config
+               if newTarget, err = tgt.Setup(newT, terms); err != nil {
+                       newT.Status = xsapiv1.StatusTgtErrorConfig
+                       log.Printf("ERROR Updating target: %v\n", err)
+                       return newTarget, err
+               }
+       }
+
+       // Create terminals
+       for _, tc := range newT.Terms {
+               _, err := t.CreateUpdateTerminal(newT.ID, tc, initial)
+               if err != nil {
+                       return newTarget, err
+               }
+       }
+
+       // Add to folders list
+       t.tgts[newT.ID] = &tgt
+
+       // Save config on disk
+       if !initial {
+               if err := t.SaveConfig(); err != nil {
+                       return newTarget, err
+               }
+       }
+
+       newTgt := tgt.GetConfig()
+       return &newTgt, nil
+}
+
+// Delete deletes a specific target
+func (t *Targets) Delete(id string) (xsapiv1.TargetConfig, error) {
+       var err error
+
+       tcMutex.Lock()
+       defer tcMutex.Unlock()
+
+       tgc := xsapiv1.TargetConfig{}
+       tc, exist := t.tgts[id]
+       if !exist {
+               return tgc, fmt.Errorf("unknown id")
+       }
+
+       tgc = (*tc).GetConfig()
+
+       if err = (*tc).Delete(); err != nil {
+               return tgc, err
+       }
+
+       delete(t.tgts, id)
+
+       // Save config on disk
+       err = t.SaveConfig()
+
+       return tgc, err
+}
+
+/***
+ * Terminals
+ ***/
+
+// GetTerminalsArr Return list of existing terminals
+func (t *Targets) GetTerminalsArr(targetID string) ([]xsapiv1.TerminalConfig, error) {
+       arr := []xsapiv1.TerminalConfig{}
+
+       tm, exist := t.terminals[targetID]
+       if !exist {
+               return arr, fmt.Errorf("unknown target id")
+       }
+
+       for _, tt := range (*tm).terms {
+               arr = append(arr, (*tt).GetConfig())
+       }
+       return arr, nil
+}
+
+// GetTerminal Return info of a specific terminal
+func (t *Targets) GetTerminal(targetID, termID string) (*ITERMINAL, error) {
+       tm, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+       term, exist := (*tm).terms[termID]
+       if !exist {
+               return nil, fmt.Errorf("unknown terminal id")
+       }
+       return term, nil
+}
+
+// ResolveTerminalID Complete a Terminal ID (helper for user that can use partial ID value)
+func (t *Targets) ResolveTerminalID(termID string) (string, error) {
+       if termID == "" {
+               return "", fmt.Errorf("unknown terminal id")
+       }
+
+       match := []string{}
+       for _, tm := range t.terminals {
+               for tid := range tm.terms {
+                       if strings.HasPrefix(tid, termID) {
+                               match = append(match, tid)
+                       }
+               }
+       }
+
+       if len(match) == 1 {
+               return match[0], nil
+       } else if len(match) == 0 {
+               return termID, fmt.Errorf("Unknown id")
+       }
+       return termID, fmt.Errorf("Multiple IDs found %v", match)
+}
+
+// CreateUpdateTerminal Create or Update a target terminal definition
+func (t *Targets) CreateUpdateTerminal(targetID string, tmCfg xsapiv1.TerminalConfig, initial bool) (*xsapiv1.TerminalConfig, error) {
+
+       var term *xsapiv1.TerminalConfig
+
+       iTerm, err := t.GetTerminal(targetID, tmCfg.ID)
+       if err != nil && strings.Contains(err.Error(), "unknown target") {
+               return nil, err
+       }
+
+       if iTerm != nil {
+               // Update terminal config
+               term = (*iTerm).UpdateConfig(tmCfg)
+       } else {
+               // Auto create a new terminal when needed
+               var err error
+               if term, err = t.terminals[targetID].New(tmCfg, targetID); err != nil {
+                       return nil, err
+               }
+       }
+
+       term.Status = xsapiv1.StatusTermEnable
+
+       // Save config on disk
+       if !initial {
+               if err := t.SaveConfig(); err != nil {
+                       return term, err
+               }
+       }
+
+       return term, nil
+}
+
+// DeleteTerminal Delete a target terminal definition
+func (t *Targets) DeleteTerminal(targetID, termID string) (*xsapiv1.TerminalConfig, error) {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+
+       term, err := (*terms).Free(termID)
+       if err != nil {
+               return term, err
+       }
+
+       // Save config on disk
+       if err := t.SaveConfig(); err != nil {
+               return term, err
+       }
+
+       return term, nil
+}
+
+// OpenTerminal Open a target terminal
+func (t *Targets) OpenTerminal(targetID, termID string, sock *socketio.Socket, sessID string) (*xsapiv1.TerminalConfig, error) {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+       return (*terms).Open(termID, sock, sessID)
+}
+
+// CloseTerminal Close a target terminal
+func (t *Targets) CloseTerminal(targetID, termID string) (*xsapiv1.TerminalConfig, error) {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+       return (*terms).Close(termID)
+}
+
+// ResizeTerminal Set size (row+col) of a target terminal
+func (t *Targets) ResizeTerminal(targetID, termID string, cols, rows uint16) (*xsapiv1.TerminalConfig, error) {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+       return (*terms).Resize(termID, cols, rows)
+}
+
+// SignalTerminal Send a signal to a target terminal
+func (t *Targets) SignalTerminal(targetID, termID, sigNum string) error {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return fmt.Errorf("unknown target id")
+       }
+       return (*terms).Signal(termID, sigNum)
+}
+
+/**
+ * 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 xmlTargets struct {
+       XMLName xml.Name               `xml:"targets"`
+       Version string                 `xml:"version,attr"`
+       Targets []xsapiv1.TargetConfig `xml:"targets"`
+}
+
+// targetsConfigRead reads targets config from disk
+func targetsConfigRead(file string, targets *[]xsapiv1.TargetConfig) error {
+       if !common.Exists(file) {
+               return fmt.Errorf("No target config file found (%s)", file)
+       }
+
+       ffMutex.Lock()
+       defer ffMutex.Unlock()
+
+       fd, err := os.Open(file)
+       defer fd.Close()
+       if err != nil {
+               return err
+       }
+
+       data := xmlTargets{}
+       err = xml.NewDecoder(fd).Decode(&data)
+       if err == nil {
+               *targets = data.Targets
+       }
+       return err
+}
+
+// targetsConfigWrite writes targets config on disk
+func targetsConfigWrite(file string, targets []xsapiv1.TargetConfig) 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 := &xmlTargets{
+               Version: "1",
+               Targets: targets,
+       }
+
+       enc := xml.NewEncoder(fd)
+       enc.Indent("", "  ")
+       return enc.Encode(data)
+}
diff --git a/lib/xdsserver/terminal-interface.go b/lib/xdsserver/terminal-interface.go
new file mode 100644 (file)
index 0000000..8542448
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xdsserver
+
+import (
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
+       socketio "github.com/googollee/go-socket.io"
+)
+
+// ITERMINAL Terminal interface
+type ITERMINAL interface {
+       GetConfig() xsapiv1.TerminalConfig                                          // Get terminal public configuration
+       UpdateConfig(cfg xsapiv1.TerminalConfig) *xsapiv1.TerminalConfig            // Update terminal config
+       Open(sock *socketio.Socket, sessID string) (*xsapiv1.TerminalConfig, error) // Open a terminal session
+       Close() (*xsapiv1.TerminalConfig, error)                                    // Close a terminal session
+       Resize(cols, rows uint16) (*xsapiv1.TerminalConfig, error)                  // Resize a terminal session
+       Signal(sigName string) error                                                // Send a signal to a terminal session
+}
diff --git a/lib/xdsserver/terminal-ssh.go b/lib/xdsserver/terminal-ssh.go
new file mode 100644 (file)
index 0000000..3f4a344
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xdsserver
+
+import (
+       "fmt"
+       "strings"
+       "time"
+
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib/eows"
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
+       socketio "github.com/googollee/go-socket.io"
+       uuid "github.com/satori/go.uuid"
+)
+
+// ITARGET interface implementation for standard targets
+
+// TermSSH .
+type TermSSH struct {
+       *Context
+       termCfg  xsapiv1.TerminalConfig
+       targetID string
+       sshWS    *eows.ExecOverWS
+}
+
+// NewTermSSH Create a new instance of TermSSH
+func NewTermSSH(ctx *Context, cfg xsapiv1.TerminalConfig, targetID string) *TermSSH {
+
+       // Allocate and set default settings
+       t := TermSSH{
+               Context: ctx,
+               termCfg: xsapiv1.TerminalConfig{
+                       ID:      cfg.ID,
+                       Name:    "ssh",
+                       Type:    xsapiv1.TypeTermSSH,
+                       Status:  xsapiv1.StatusTermClose,
+                       User:    "",
+                       Options: []string{""},
+                       Cols:    80,
+                       Rows:    24,
+               },
+               targetID: targetID,
+       }
+
+       t.UpdateConfig(cfg)
+       return &t
+}
+
+// NewUID Get a UUID
+func (t *TermSSH) _NewUID(suffix string) string {
+       uuid := uuid.NewV1().String()
+       if len(suffix) > 0 {
+               uuid += "_" + suffix
+       }
+       return uuid
+}
+
+// GetConfig Get public part of terminal config
+func (t *TermSSH) GetConfig() xsapiv1.TerminalConfig {
+       return t.termCfg
+}
+
+// UpdateConfig Update terminal config
+func (t *TermSSH) UpdateConfig(newCfg xsapiv1.TerminalConfig) *xsapiv1.TerminalConfig {
+
+       if t.termCfg.ID == "" {
+               if newCfg.ID != "" {
+                       t.termCfg.ID = newCfg.ID
+               } else {
+                       t.termCfg.ID = t._NewUID("")
+               }
+       }
+       if newCfg.Name != "" {
+               t.termCfg.Name = newCfg.Name
+       }
+       if newCfg.User != "" {
+               t.termCfg.User = newCfg.User
+       }
+       if len(newCfg.Options) > 0 {
+               t.termCfg.Options = newCfg.Options
+       }
+
+       // Adjust terminal size
+       t.Resize(newCfg.Cols, newCfg.Rows)
+
+       return &t.termCfg
+}
+
+// Open a new terminal - execute ssh command and bind stdin/stdout to WebSocket
+func (t *TermSSH) Open(sock *socketio.Socket, sessID string) (*xsapiv1.TerminalConfig, error) {
+
+       // Get target info to retrieve IP
+       tgt := t.targets.Get(t.targetID)
+       if tgt == nil {
+               return nil, fmt.Errorf("Cannot retrieve target definition")
+       }
+       tgtCfg := (*tgt).GetConfig()
+
+       // Sanity check
+       if tgtCfg.IP == "" {
+               return nil, fmt.Errorf("null target IP")
+       }
+       userStr := ""
+       if t.termCfg.User != "" {
+               userStr = t.termCfg.User + "@"
+       }
+
+       // Compute ssh command
+       cmd := "ssh"
+       cmdID := "ssh_term_" + t.termCfg.ID
+       args := t.termCfg.Options
+       args = append(args, userStr+tgtCfg.IP)
+
+       t.sshWS = eows.New(cmd, args, sock, sessID, cmdID)
+       t.sshWS.Log = t.Log
+       t.sshWS.OutSplit = eows.SplitChar
+       t.sshWS.PtsMode = true
+
+       // Define callback for input (stdin)
+       t.sshWS.InputEvent = xsapiv1.TerminalInEvent
+       t.sshWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
+               t.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
+               }
+
+               return stdin, nil
+       }
+
+       // Define callback for output (stdout+stderr)
+       t.sshWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
+               // IO socket can be nil when disconnected
+               so := t.sessions.IOSocketGet(e.Sid)
+               if so == nil {
+                       t.Log.Infof("%s not emitted: WS closed (sid:%s, CmdID:%s)", xsapiv1.TerminalOutEvent, e.Sid, e.CmdID)
+                       return
+               }
+
+               // Retrieve project ID and RootPath
+               data := e.UserData
+               termID := (*data)["ID"].(string)
+
+               t.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - termID:%s", xsapiv1.TerminalOutEvent, e.Sid[4:], e.CmdID, termID)
+               if stdout != "" {
+                       t.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
+               }
+               if stderr != "" {
+                       t.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
+               }
+
+               // FIXME replace by .BroadcastTo a room
+               err := (*so).Emit(xsapiv1.TerminalOutEvent, xsapiv1.TerminalOutMsg{
+                       TermID:    termID,
+                       Timestamp: time.Now().String(),
+                       Stdout:    stdout,
+                       Stderr:    stderr,
+               })
+               if err != nil {
+                       t.Log.Errorf("WS Emit : %v", err)
+               }
+       }
+
+       // Define callback for output
+       t.sshWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
+               t.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
+
+               // IO socket can be nil when disconnected
+               so := t.sessions.IOSocketGet(e.Sid)
+               if so == nil {
+                       t.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.TerminalExitEvent, e.CmdID)
+                       return
+               }
+
+               // Retrieve project ID and RootPath
+               data := e.UserData
+               termID := (*data)["ID"].(string)
+
+               // FIXME replace by .BroadcastTo a room
+               errSoEmit := (*so).Emit(xsapiv1.TerminalExitEvent, xsapiv1.TerminalExitMsg{
+                       TermID:    termID,
+                       Timestamp: time.Now().String(),
+                       Code:      code,
+                       Error:     err,
+               })
+               if errSoEmit != nil {
+                       t.Log.Errorf("WS Emit : %v", errSoEmit)
+               }
+
+               t.termCfg.Status = xsapiv1.StatusTermClose
+               t.sshWS = nil
+       }
+
+       // data (used within callbacks)
+       data := make(map[string]interface{})
+       data["ID"] = t.termCfg.ID
+       t.sshWS.UserData = &data
+
+       // Start ssh command
+       t.Log.Infof("Execute [Cmd ID %s]: %v %v", t.sshWS.CmdID, t.sshWS.Cmd, t.sshWS.Args)
+
+       if err := t.sshWS.Start(); err != nil {
+               return &t.termCfg, err
+       }
+
+       t.termCfg.Status = xsapiv1.StatusTermOpen
+
+       return &t.termCfg, nil
+}
+
+// Close a terminal
+func (t *TermSSH) Close() (*xsapiv1.TerminalConfig, error) {
+       // nothing to do when not open
+       if t.termCfg.Status != xsapiv1.StatusTermOpen {
+               return &t.termCfg, nil
+       }
+
+       err := t.sshWS.Signal("SIGTERM")
+
+       return &t.termCfg, err
+}
+
+// Resize a terminal
+func (t *TermSSH) Resize(cols, rows uint16) (*xsapiv1.TerminalConfig, error) {
+       if t.sshWS == nil {
+               return &t.termCfg, fmt.Errorf("ssh session not initialized")
+       }
+
+       if cols > 0 {
+               t.termCfg.Cols = cols
+       }
+       if rows > 0 {
+               t.termCfg.Rows = rows
+       }
+
+       err := t.sshWS.TerminalSetSize(t.termCfg.Rows, t.termCfg.Cols)
+       if err != nil {
+               t.Log.Errorf("Error ssh TerminalSetSize: %v", err)
+       }
+
+       return &t.termCfg, err
+}
+
+// Signal Send a signal to a terminal
+func (t *TermSSH) Signal(sigName string) error {
+       return t.sshWS.Signal(sigName)
+}
diff --git a/lib/xdsserver/terminals.go b/lib/xdsserver/terminals.go
new file mode 100644 (file)
index 0000000..36623ab
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xdsserver
+
+import (
+       "fmt"
+
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
+       socketio "github.com/googollee/go-socket.io"
+       "github.com/syncthing/syncthing/lib/sync"
+)
+
+// Terminals Represent a XDS terminals
+type Terminals struct {
+       *Context
+       terms map[string]*ITERMINAL
+}
+
+// Mutex to make add/delete atomic
+var tmMutex = sync.NewMutex()
+
+// TerminalsConstructor Create a new instance of Model Terminal
+func TerminalsConstructor(ctx *Context) *Terminals {
+       return &Terminals{
+               Context: ctx,
+               terms:   make(map[string]*ITERMINAL),
+       }
+}
+
+// New Create a new terminal
+func (t *Terminals) New(cfg xsapiv1.TerminalConfig, targetID string) (*xsapiv1.TerminalConfig, error) {
+
+       tmMutex.Lock()
+       defer tmMutex.Unlock()
+
+       var newT ITERMINAL
+
+       // For now, only SSH term is supported
+       switch cfg.Type {
+       case xsapiv1.TypeTermSSH:
+               newT = NewTermSSH(t.Context, cfg, targetID)
+       default:
+               return nil, fmt.Errorf("terminal type not set")
+       }
+
+       termCfg := newT.GetConfig()
+
+       t.terms[termCfg.ID] = &newT
+
+       return &termCfg, nil
+}
+
+// Free a specific terminal
+func (t *Terminals) Free(id string) (*xsapiv1.TerminalConfig, error) {
+
+       tmMutex.Lock()
+       defer tmMutex.Unlock()
+
+       tc := t.Get(id)
+       if tc == nil {
+               return nil, fmt.Errorf("Unknown id")
+       }
+
+       if _, err := (*tc).Close(); err != nil {
+               return nil, err
+       }
+
+       resTerm := (*tc).GetConfig()
+
+       delete(t.terms, id)
+
+       return &resTerm, nil
+}
+
+// Get returns the terminal config or nil if not existing
+func (t *Terminals) Get(id string) *ITERMINAL {
+       if id == "" {
+               return nil
+       }
+       tc, exist := t.terms[id]
+       if !exist {
+               return nil
+       }
+       return tc
+}
+
+// GetConfigArr returns the config of all terminals as an array
+func (t *Terminals) GetConfigArr() []xsapiv1.TerminalConfig {
+       tmMutex.Lock()
+       defer tmMutex.Unlock()
+
+       return t.getConfigArrUnsafe()
+}
+
+// getConfigArrUnsafe Same as GetConfigArr without mutex protection
+func (t *Terminals) getConfigArrUnsafe() []xsapiv1.TerminalConfig {
+       conf := []xsapiv1.TerminalConfig{}
+       for _, v := range t.terms {
+               conf = append(conf, (*v).GetConfig())
+       }
+       return conf
+}
+
+// Open adds a new terminal
+func (t *Terminals) Open(id string, sock *socketio.Socket, sessID string) (*xsapiv1.TerminalConfig, error) {
+       tc := t.Get(id)
+       if tc == nil {
+               return nil, fmt.Errorf("Unknown id")
+       }
+       return (*tc).Open(sock, sessID)
+}
+
+// Close a specific terminal
+func (t *Terminals) Close(id string) (*xsapiv1.TerminalConfig, error) {
+       tc := t.Get(id)
+       if tc == nil {
+               return nil, fmt.Errorf("Unknown id")
+       }
+       return (*tc).Close()
+}
+
+// Resize a specific terminal
+func (t *Terminals) Resize(id string, cols, rows uint16) (*xsapiv1.TerminalConfig, error) {
+       tmMutex.Lock()
+       defer tmMutex.Unlock()
+
+       tc := t.Get(id)
+       if tc == nil {
+               return nil, fmt.Errorf("Unknown id")
+       }
+       return (*tc).Resize(cols, rows)
+}
+
+// Signal Send a Signal a specific terminal
+func (t *Terminals) Signal(id, sigName string) error {
+       tmMutex.Lock()
+       defer tmMutex.Unlock()
+
+       tc := t.Get(id)
+       if tc == nil {
+               return fmt.Errorf("Unknown id")
+       }
+       return (*tc).Signal(sigName)
+}
index f1c88d2..24456b9 100644 (file)
@@ -43,8 +43,8 @@ type WebServer struct {
 
 const indexFilename = "index.html"
 
-// NewWebServer creates an instance of WebServer
-func NewWebServer(ctx *Context) *WebServer {
+// WebServerConstructor creates an instance of WebServer
+func WebServerConstructor(ctx *Context) *WebServer {
 
        // Setup logging for gin router
        if ctx.Log.Level == logrus.DebugLevel {
@@ -183,11 +183,12 @@ func (s *WebServer) socketHandler(c *gin.Context) {
        }
 
        s.sIOServer.On("connection", func(so socketio.Socket) {
-               s.Log.Debugf("WS Connected (SID=%v)", so.Id())
-               s.sessions.UpdateIOSocket(sess.ID, &so)
+               sessID := sess.ID
+               s.Log.Debugf("WS Connected (sessID=%v, SID=%v)", sessID, so.Id())
+               s.sessions.UpdateIOSocket(sessID, &so)
 
                so.On("disconnection", func() {
-                       s.Log.Debugf("WS disconnected (SID=%v)", so.Id())
+                       s.Log.Debugf("WS disconnected (sessID=%v, SID=%v)", sessID, so.Id())
                        s.sessions.UpdateIOSocket(sess.ID, nil)
                })
        })
index bb8f755..1079eba 100644 (file)
@@ -48,6 +48,7 @@ type Context struct {
        SThgInotCmd   *exec.Cmd
        mfolders      *Folders
        sdks          *SDKs
+       targets       *Targets
        WWWServer     *WebServer
        sessions      *Sessions
        events        *Events
@@ -129,7 +130,7 @@ func (ctx *Context) Run() (int, error) {
        }
 
        // Create events management
-       ctx.events = NewEvents(ctx)
+       ctx.events = EventsConstructor(ctx)
 
        // Create syncthing instance when section "syncthing" is present in server-config.json
        if ctx.Config.FileConf.SThgConf != nil {
@@ -178,7 +179,7 @@ func (ctx *Context) Run() (int, error) {
        }
 
        // Init model folder
-       ctx.mfolders = FoldersNew(ctx)
+       ctx.mfolders = FoldersConstructor(ctx)
 
        // Load initial folders config from disk
        if err := ctx.mfolders.LoadConfig(); err != nil {
@@ -186,16 +187,24 @@ func (ctx *Context) Run() (int, error) {
        }
 
        // Init cross SDKs
-       ctx.sdks, err = NewSDKs(ctx)
+       ctx.sdks, err = SDKsConstructor(ctx)
        if err != nil {
                return -6, err
        }
 
+       // Init target and terminals model
+       ctx.targets = TargetsConstructor(ctx)
+
+       // Load initial target & terminal config
+       if err := ctx.targets.LoadConfig(); err != nil {
+               return -6, err
+       }
+
        // Create Web Server
-       ctx.WWWServer = NewWebServer(ctx)
+       ctx.WWWServer = WebServerConstructor(ctx)
 
        // Sessions manager
-       ctx.sessions = NewClientSessions(ctx, cookieMaxAge)
+       ctx.sessions = ClientSessionsConstructor(ctx, cookieMaxAge)
 
        // Run Web Server until exit requested (blocking call)
        if err = ctx.WWWServer.Serve(); err != nil {
diff --git a/lib/xsapiv1/targets.go b/lib/xsapiv1/targets.go
new file mode 100644 (file)
index 0000000..3f2b3c4
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xsapiv1
+
+/**
+ * Target
+ **/
+
+// TargetType definition
+type TargetType string
+
+const (
+       // TypeTgtStandard Standard target type
+       TypeTgtStandard = "standard"
+)
+
+// Target Status definition
+const (
+       StatusTgtErrorConfig = "ErrorConfig"
+       StatusTgtDisable     = "Disable"
+       StatusTgtEnable      = "Enable"
+)
+
+// TargetConfig config of a target / board
+type TargetConfig struct {
+       ID     string           `json:"id"`
+       Name   string           `json:"name"`
+       Type   TargetType       `json:"type"`
+       IP     string           `json:"ip"`
+       Status string           `json:"status"`
+       Terms  []TerminalConfig `json:"terms"`
+}
+
+/**
+ * Terminal
+ **/
+
+// TerminalType definition
+type TerminalType string
+
+const (
+       // TypeTermSSH ssh terminal type
+       TypeTermSSH = "ssh"
+
+       // StatusTermErrorConfig Configuration error
+       StatusTermErrorConfig = "ErrorConfig"
+       // StatusTermEnable Enable state
+       StatusTermEnable = "Enable"
+       // StatusTermOpen Open state
+       StatusTermOpen = "Open"
+       // StatusTermClose Close state
+       StatusTermClose = "Close"
+
+       // TerminalInEvent Event send in WS when characters are sent (stdin)
+       TerminalInEvent = "term:input"
+       // TerminalOutEvent Event send in WS when characters are received (stdout or stderr)
+       TerminalOutEvent = "term:output"
+       // TerminalExitEvent Event send in WS on terminal/console exit
+       TerminalExitEvent = "term:exit"
+)
+
+type (
+
+       // TerminalConfig config of a board terminal
+       TerminalConfig struct {
+               ID      string       `json:"id"`
+               Name    string       `json:"name"`
+               Type    TerminalType `json:"type"`
+               User    string       `json:"user"`
+               Options []string     `json:"options"`
+               Status  string       `json:"status"`
+               Cols    uint16       `json:"cols"`
+               Rows    uint16       `json:"rows"`
+       }
+
+       // TerminalInMsg Message used to received input characters (stdin)
+       TerminalInMsg struct {
+               TermID    string `json:"termID"`
+               Timestamp string `json:"timestamp"`
+               Stdin     string `json:"stdin"`
+       }
+
+       // TerminalOutMsg Message used to send output characters (stdout+stderr)
+       TerminalOutMsg struct {
+               TermID    string `json:"termID"`
+               Timestamp string `json:"timestamp"`
+               Stdout    string `json:"stdout"`
+               Stderr    string `json:"stderr"`
+       }
+
+       // TerminalExitMsg Message sent on terminal/console exit
+       TerminalExitMsg struct {
+               TermID    string `json:"termID"`
+               Timestamp string `json:"timestamp"`
+               Code      int    `json:"code"`
+               Error     error  `json:"error"`
+       }
+
+       // TerminalResizeArgs JSON parameters of /terminal/:tid/resize command
+       TerminalResizeArgs struct {
+               Cols uint16 `json:"cols"`
+               Rows uint16 `json:"rows"`
+       }
+
+       // TerminalSignalArgs JSON parameters of /terminal/:tid/signal command
+       TerminalSignalArgs struct {
+               Signal string `json:"signal" binding:"required"` // signal number
+       }
+)