From 65e09e831cf13343ac713fbf15281174d1f13a94 Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Fri, 23 Feb 2018 18:45:15 +0100 Subject: Added target and terminal support. Signed-off-by: Sebastien Douheret --- lib/xdsserver/apiv1-exec.go | 3 +- lib/xdsserver/apiv1-targets.go | 288 ++++++++++++++++++++++ lib/xdsserver/apiv1.go | 15 ++ lib/xdsserver/events.go | 4 +- lib/xdsserver/folders.go | 8 +- lib/xdsserver/sdks.go | 4 +- lib/xdsserver/sessions.go | 6 +- lib/xdsserver/target-interface.go | 29 +++ lib/xdsserver/target-standard.go | 89 +++++++ lib/xdsserver/targets.go | 466 ++++++++++++++++++++++++++++++++++++ lib/xdsserver/terminal-interface.go | 33 +++ lib/xdsserver/terminal-ssh.go | 265 ++++++++++++++++++++ lib/xdsserver/terminals.go | 159 ++++++++++++ lib/xdsserver/webserver.go | 11 +- lib/xdsserver/xdsserver.go | 19 +- 15 files changed, 1378 insertions(+), 21 deletions(-) create mode 100644 lib/xdsserver/apiv1-targets.go create mode 100644 lib/xdsserver/target-interface.go create mode 100644 lib/xdsserver/target-standard.go create mode 100644 lib/xdsserver/targets.go create mode 100644 lib/xdsserver/terminal-interface.go create mode 100644 lib/xdsserver/terminal-ssh.go create mode 100644 lib/xdsserver/terminals.go (limited to 'lib/xdsserver') diff --git a/lib/xdsserver/apiv1-exec.go b/lib/xdsserver/apiv1-exec.go index 2337de6..327c4c5 100644 --- a/lib/xdsserver/apiv1-exec.go +++ b/lib/xdsserver/apiv1-exec.go @@ -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 index 0000000..978dc75 --- /dev/null +++ b/lib/xdsserver/apiv1-targets.go @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2018 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 +} diff --git a/lib/xdsserver/apiv1.go b/lib/xdsserver/apiv1.go index 67d09b5..e0bfa7f 100644 --- a/lib/xdsserver/apiv1.go +++ b/lib/xdsserver/apiv1.go @@ -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 } diff --git a/lib/xdsserver/events.go b/lib/xdsserver/events.go index 2528725..0a02ecd 100644 --- a/lib/xdsserver/events.go +++ b/lib/xdsserver/events.go @@ -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{ diff --git a/lib/xdsserver/folders.go b/lib/xdsserver/folders.go index fa24878..d27a329 100644 --- a/lib/xdsserver/folders.go +++ b/lib/xdsserver/folders.go @@ -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 diff --git a/lib/xdsserver/sdks.go b/lib/xdsserver/sdks.go index 6094045..4a7ba84 100644 --- a/lib/xdsserver/sdks.go +++ b/lib/xdsserver/sdks.go @@ -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), diff --git a/lib/xdsserver/sessions.go b/lib/xdsserver/sessions.go index 69fe819..0c16b99 100644 --- a/lib/xdsserver/sessions.go +++ b/lib/xdsserver/sessions.go @@ -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 index 0000000..6d5bd7b --- /dev/null +++ b/lib/xdsserver/target-interface.go @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 index 0000000..2c1b068 --- /dev/null +++ b/lib/xdsserver/target-standard.go @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017-2018 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 index 0000000..663233d --- /dev/null +++ b/lib/xdsserver/targets.go @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2018 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 index 0000000..8542448 --- /dev/null +++ b/lib/xdsserver/terminal-interface.go @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 index 0000000..3f4a344 --- /dev/null +++ b/lib/xdsserver/terminal-ssh.go @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2018 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 index 0000000..36623ab --- /dev/null +++ b/lib/xdsserver/terminals.go @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018 "IoT.bzh" + * Author Sebastien Douheret + * + * 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) +} diff --git a/lib/xdsserver/webserver.go b/lib/xdsserver/webserver.go index f1c88d2..24456b9 100644 --- a/lib/xdsserver/webserver.go +++ b/lib/xdsserver/webserver.go @@ -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) }) }) diff --git a/lib/xdsserver/xdsserver.go b/lib/xdsserver/xdsserver.go index bb8f755..1079eba 100644 --- a/lib/xdsserver/xdsserver.go +++ b/lib/xdsserver/xdsserver.go @@ -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 { -- cgit 1.2.3-korg