f7beea65ed0a434bd31754ef486d3a4f0efb5e3d
[src/xds/xds-server.git] / lib / apiv1 / exec.go
1 package apiv1
2
3 import (
4         "net/http"
5         "strconv"
6         "strings"
7         "time"
8
9         "github.com/gin-gonic/gin"
10         "github.com/iotbzh/xds-server/lib/common"
11 )
12
13 // ExecArgs JSON parameters of /exec command
14 type ExecArgs struct {
15         ID         string   `json:"id"`
16         RPath      string   `json:"rpath"` // relative path into project
17         Cmd        string   `json:"cmd" binding:"required"`
18         Args       []string `json:"args"`
19         CmdTimeout int      `json:"timeout"` // command completion timeout in Second
20 }
21
22 // ExecOutMsg Message send on each output (stdout+stderr) of executed command
23 type ExecOutMsg struct {
24         CmdID     string `json:"cmdID"`
25         Timestamp string `json:timestamp`
26         Stdout    string `json:"stdout"`
27         Stderr    string `json:"stderr"`
28 }
29
30 // ExecExitMsg Message send when executed command exited
31 type ExecExitMsg struct {
32         CmdID     string `json:"cmdID"`
33         Timestamp string `json:timestamp`
34         Code      int    `json:"code"`
35         Error     error  `json:"error"`
36 }
37
38 // Event name send in WS
39 const ExecOutEvent = "exec:output"
40 const ExecExitEvent = "exec:exit"
41
42 var execCommandID = 1
43
44 // ExecCmd executes remotely a command
45 func (s *APIService) execCmd(c *gin.Context) {
46         var args ExecArgs
47         if c.BindJSON(&args) != nil {
48                 common.APIError(c, "Invalid arguments")
49                 return
50         }
51
52         // TODO: add permission
53
54         // Retrieve session info
55         sess := s.sessions.Get(c)
56         if sess == nil {
57                 common.APIError(c, "Unknown sessions")
58                 return
59         }
60         sop := sess.IOSocket
61         if sop == nil {
62                 common.APIError(c, "Websocket not established")
63                 return
64         }
65
66         // Allow to pass id in url (/exec/:id) or as JSON argument
67         id := c.Param("id")
68         if id == "" {
69                 id = args.ID
70         }
71         if id == "" {
72                 common.APIError(c, "Invalid id")
73                 return
74         }
75
76         prj := s.cfg.GetFolderFromID(id)
77         if prj == nil {
78                 common.APIError(c, "Unknown id")
79                 return
80         }
81
82         execTmo := args.CmdTimeout
83         if execTmo == 0 {
84                 // TODO get default timeout from config.json file
85                 execTmo = 24 * 60 * 60 // 1 day
86         }
87
88         // Define callback for output
89         var oCB common.EmitOutputCB
90         oCB = func(sid string, id int, stdout, stderr string) {
91                 // IO socket can be nil when disconnected
92                 so := s.sessions.IOSocketGet(sid)
93                 if so == nil {
94                         s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id)
95                         return
96                 }
97                 s.log.Debugf("%s emitted - WS sid %s - id:%d", ExecOutEvent, sid, id)
98
99                 // FIXME replace by .BroadcastTo a room
100                 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
101                         CmdID:     strconv.Itoa(id),
102                         Timestamp: time.Now().String(),
103                         Stdout:    stdout,
104                         Stderr:    stderr,
105                 })
106                 if err != nil {
107                         s.log.Errorf("WS Emit : %v", err)
108                 }
109         }
110
111         // Define callback for output
112         eCB := func(sid string, id int, code int, err error) {
113                 s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err)
114
115                 // IO socket can be nil when disconnected
116                 so := s.sessions.IOSocketGet(sid)
117                 if so == nil {
118                         s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id)
119                         return
120                 }
121
122                 // FIXME replace by .BroadcastTo a room
123                 e := (*so).Emit(ExecExitEvent, ExecExitMsg{
124                         CmdID:     strconv.Itoa(id),
125                         Timestamp: time.Now().String(),
126                         Code:      code,
127                         Error:     err,
128                 })
129                 if e != nil {
130                         s.log.Errorf("WS Emit : %v", e)
131                 }
132         }
133
134         cmdID := execCommandID
135         execCommandID++
136
137         cmd := "cd " + prj.GetFullPath(args.RPath) + " && " + args.Cmd
138         if len(args.Args) > 0 {
139                 cmd += " " + strings.Join(args.Args, " ")
140         }
141
142         s.log.Debugf("Execute [Cmd ID %d]: %v %v", cmdID, cmd)
143         err := common.ExecPipeWs(cmd, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB)
144         if err != nil {
145                 common.APIError(c, err.Error())
146                 return
147         }
148
149         c.JSON(http.StatusOK,
150                 gin.H{
151                         "status": "OK",
152                         "cmdID":  cmdID,
153                 })
154 }