Auto start Syncthing and Syncthing-inotify.
[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 // ExecOutEvent Event send in WS when characters are received
39 const ExecOutEvent = "exec:output"
40
41 // ExecExitEvent Event send in WS when program exited
42 const ExecExitEvent = "exec:exit"
43
44 var execCommandID = 1
45
46 // ExecCmd executes remotely a command
47 func (s *APIService) execCmd(c *gin.Context) {
48         var args ExecArgs
49         if c.BindJSON(&args) != nil {
50                 common.APIError(c, "Invalid arguments")
51                 return
52         }
53
54         // TODO: add permission
55
56         // Retrieve session info
57         sess := s.sessions.Get(c)
58         if sess == nil {
59                 common.APIError(c, "Unknown sessions")
60                 return
61         }
62         sop := sess.IOSocket
63         if sop == nil {
64                 common.APIError(c, "Websocket not established")
65                 return
66         }
67
68         // Allow to pass id in url (/exec/:id) or as JSON argument
69         id := c.Param("id")
70         if id == "" {
71                 id = args.ID
72         }
73         if id == "" {
74                 common.APIError(c, "Invalid id")
75                 return
76         }
77
78         prj := s.mfolder.GetFolderFromID(id)
79         if prj == nil {
80                 common.APIError(c, "Unknown id")
81                 return
82         }
83
84         execTmo := args.CmdTimeout
85         if execTmo == 0 {
86                 // TODO get default timeout from config.json file
87                 execTmo = 24 * 60 * 60 // 1 day
88         }
89
90         // Define callback for output
91         var oCB common.EmitOutputCB
92         oCB = func(sid string, id int, stdout, stderr string) {
93                 // IO socket can be nil when disconnected
94                 so := s.sessions.IOSocketGet(sid)
95                 if so == nil {
96                         s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id)
97                         return
98                 }
99                 s.log.Debugf("%s emitted - WS sid %s - id:%d", ExecOutEvent, sid, id)
100
101                 // FIXME replace by .BroadcastTo a room
102                 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
103                         CmdID:     strconv.Itoa(id),
104                         Timestamp: time.Now().String(),
105                         Stdout:    stdout,
106                         Stderr:    stderr,
107                 })
108                 if err != nil {
109                         s.log.Errorf("WS Emit : %v", err)
110                 }
111         }
112
113         // Define callback for output
114         eCB := func(sid string, id int, code int, err error) {
115                 s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err)
116
117                 // IO socket can be nil when disconnected
118                 so := s.sessions.IOSocketGet(sid)
119                 if so == nil {
120                         s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id)
121                         return
122                 }
123
124                 // FIXME replace by .BroadcastTo a room
125                 e := (*so).Emit(ExecExitEvent, ExecExitMsg{
126                         CmdID:     strconv.Itoa(id),
127                         Timestamp: time.Now().String(),
128                         Code:      code,
129                         Error:     err,
130                 })
131                 if e != nil {
132                         s.log.Errorf("WS Emit : %v", e)
133                 }
134         }
135
136         cmdID := execCommandID
137         execCommandID++
138
139         cmd := "cd " + prj.GetFullPath(args.RPath) + " && " + args.Cmd
140         if len(args.Args) > 0 {
141                 cmd += " " + strings.Join(args.Args, " ")
142         }
143
144         s.log.Debugf("Execute [Cmd ID %d]: %v %v", cmdID, cmd)
145         err := common.ExecPipeWs(cmd, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB)
146         if err != nil {
147                 common.APIError(c, err.Error())
148                 return
149         }
150
151         c.JSON(http.StatusOK,
152                 gin.H{
153                         "status": "OK",
154                         "cmdID":  cmdID,
155                 })
156 }