1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
350
351
352
353
354
355
356
357
358
359
|
/*
* Copyright (C) 2017 "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"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
common "github.com/iotbzh/xds-common/golib"
"github.com/iotbzh/xds-common/golib/eows"
"github.com/iotbzh/xds-server/lib/xsapiv1"
"github.com/kr/pty"
)
var execCommandID = 1
// ExecCmd executes remotely a command
func (s *APIService) execCmd(c *gin.Context) {
var gdbPty, gdbTty *os.File
var err error
var args xsapiv1.ExecArgs
if c.BindJSON(&args) != nil {
common.APIError(c, "Invalid arguments")
return
}
// TODO: add permission ?
// Retrieve session info
sess := s.sessions.Get(c)
if sess == nil {
common.APIError(c, "Unknown sessions")
return
}
sop := sess.IOSocket
if sop == nil {
common.APIError(c, "Websocket not established")
return
}
// Allow to pass id in url (/exec/:id) or as JSON argument
idArg := c.Param("id")
if idArg == "" {
idArg = args.ID
}
if idArg == "" {
common.APIError(c, "Invalid id")
return
}
id, err := s.mfolders.ResolveID(idArg)
if err != nil {
common.APIError(c, err.Error())
return
}
f := s.mfolders.Get(id)
if f == nil {
common.APIError(c, "Unknown id")
return
}
fld := *f
prj := fld.GetConfig()
// Build command line
cmd := []string{}
// Setup env var regarding Sdk ID (used for example to setup cross toolchain)
if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
cmd = append(cmd, envCmd...)
cmd = append(cmd, "&&")
} else {
// It's an error if no envcmd found while a sdkid has been provided
if args.SdkID != "" {
common.APIError(c, "Unknown sdkid")
return
}
}
cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"")
// FIXME - add 'exec' prevents to use syntax:
// xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
// but exec is mandatory to allow to pass correctly signals
// As workaround, exec is set for now on client side (eg. in xds-gdb)
//cmd = append(cmd, "&&", "exec", args.Cmd)
cmd = append(cmd, "&&", args.Cmd)
// Process command arguments
cmdArgs := make([]string, len(args.Args)+1)
// Copy and Translate path from client to server
for _, aa := range args.Args {
if strings.Contains(aa, prj.ClientPath) {
cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa))
} else {
cmdArgs = append(cmdArgs, aa)
}
}
// Allocate pts if tty if used
if args.TTY {
gdbPty, gdbTty, err = pty.Open()
if err != nil {
common.APIError(c, err.Error())
return
}
s.Log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
}
// Unique ID for each commands
if args.CmdID == "" {
args.CmdID = s.Config.ServerUID[:18] + "_" + strconv.Itoa(execCommandID)
execCommandID++
}
// Create new execution over WS context
execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
execWS.Log = s.Log
// Append client project dir to environment
execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
// Set command execution timeout
if args.CmdTimeout == 0 {
// 0 : default timeout
// TODO get default timeout from server-config.json file
execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
} else {
execWS.CmdExecTimeout = args.CmdTimeout
}
// Define callback for input (stdin)
execWS.InputEvent = xsapiv1.ExecInEvent
execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
s.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
// Handle Ctrl-D
if len(stdin) == 1 && stdin == "\x04" {
// Close stdin
errMsg := fmt.Errorf("close stdin: %v", stdin)
return "", errMsg
}
// Set correct path
data := e.UserData
prjID := (*data)["ID"].(string)
f := s.mfolders.Get(prjID)
if f == nil {
s.Log.Errorf("InputCB: Cannot get folder ID %s", prjID)
} else {
// Translate paths from client to server
stdin = (*f).ConvPathCli2Svr(stdin)
}
return stdin, nil
}
// Define callback for output (stdout+stderr)
execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
// IO socket can be nil when disconnected
so := s.sessions.IOSocketGet(e.Sid)
if so == nil {
s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
return
}
// Retrieve project ID and RootPath
data := e.UserData
prjID := (*data)["ID"].(string)
gdbServerTTY := (*data)["gdbServerTTY"].(string)
f := s.mfolders.Get(prjID)
if f == nil {
s.Log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
} else {
// Translate paths from server to client
stdout = (*f).ConvPathSvr2Cli(stdout)
stderr = (*f).ConvPathSvr2Cli(stderr)
}
s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
if stdout != "" {
s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
}
if stderr != "" {
s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
}
// FIXME replace by .BroadcastTo a room
err := (*so).Emit(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{
CmdID: e.CmdID,
Timestamp: time.Now().String(),
Stdout: stdout,
Stderr: stderr,
})
if err != nil {
s.Log.Errorf("WS Emit : %v", err)
}
// XXX - Workaround due to gdbserver bug that doesn't redirect
// inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
// Extract and cleanup string like &"bla bla\n"
re := regexp.MustCompile("&\"(.*)\"")
rer := re.FindAllStringSubmatch(stdout, -1)
out := ""
if rer != nil && len(rer) > 0 {
for _, o := range rer {
if len(o) >= 1 {
out = strings.Replace(o[1], "\\n", "\n", -1)
out = strings.Replace(out, "\\r", "\r", -1)
out = strings.Replace(out, "\\t", "\t", -1)
s.Log.Debugf("STDOUT INFERIOR: <<%v>>", out)
err := (*so).Emit(xsapiv1.ExecInferiorOutEvent, xsapiv1.ExecOutMsg{
CmdID: e.CmdID,
Timestamp: time.Now().String(),
Stdout: out,
Stderr: "",
})
if err != nil {
s.Log.Errorf("WS Emit : %v", err)
}
}
}
} else {
s.Log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
}
}
}
// Define callback for output
execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
s.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
// Close client tty
defer func() {
if gdbPty != nil {
gdbPty.Close()
}
if gdbTty != nil {
gdbTty.Close()
}
}()
// IO socket can be nil when disconnected
so := s.sessions.IOSocketGet(e.Sid)
if so == nil {
s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.ExecExitEvent, e.CmdID)
return
}
// Retrieve project ID and RootPath
data := e.UserData
prjID := (*data)["ID"].(string)
exitImm := (*data)["ExitImmediate"].(bool)
// XXX - workaround to be sure that Syncthing detected all changes
if err := s.mfolders.ForceSync(prjID); err != nil {
s.Log.Errorf("Error while syncing folder %s: %v", prjID, err)
}
if !exitImm {
// Wait end of file sync
// FIXME pass as argument
tmo := 60
for t := tmo; t > 0; t-- {
s.Log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
if err != nil {
s.Log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
}
break
}
time.Sleep(time.Second)
}
s.Log.Debugf("OK file are synchronized.")
}
// FIXME replace by .BroadcastTo a room
errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{
CmdID: e.CmdID,
Timestamp: time.Now().String(),
Code: code,
Error: err,
})
if errSoEmit != nil {
s.Log.Errorf("WS Emit : %v", errSoEmit)
}
}
// User data (used within callbacks)
data := make(map[string]interface{})
data["ID"] = prj.ID
data["ExitImmediate"] = args.ExitImmediate
if args.TTY && args.TTYGdbserverFix {
data["gdbServerTTY"] = "workaround"
} else {
data["gdbServerTTY"] = ""
}
execWS.UserData = &data
// Start command execution
s.Log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
err = execWS.Start()
if err != nil {
common.APIError(c, err.Error())
return
}
c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID})
}
// ExecCmd executes remotely a command
func (s *APIService) execSignalCmd(c *gin.Context) {
var args xsapiv1.ExecSignalArgs
if c.BindJSON(&args) != nil {
common.APIError(c, "Invalid arguments")
return
}
s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
e := eows.GetEows(args.CmdID)
if e == nil {
common.APIError(c, "unknown cmdID")
return
}
err := e.Signal(args.Signal)
if err != nil {
common.APIError(c, err.Error())
return
}
c.JSON(http.StatusOK, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID})
}
|