Merge remote-tracking branch 'origin/master' into wip
[src/xds/xds-server.git] / lib / apiv1 / make.go
1 package apiv1
2
3 import (
4         "net/http"
5         "strings"
6
7         "time"
8
9         "strconv"
10
11         "github.com/gin-gonic/gin"
12         common "github.com/iotbzh/xds-common/golib"
13 )
14
15 // MakeArgs is the parameters (json format) of /make command
16 type MakeArgs struct {
17         ID            string   `json:"id"`
18         SdkID         string   `json:"sdkID"` // sdk ID to use for setting env
19         CmdID         string   `json:"cmdID"` // command unique ID
20         Args          []string `json:"args"`  // args to pass to make command
21         Env           []string `json:"env"`
22         RPath         string   `json:"rpath"`         // relative path into project
23         ExitImmediate bool     `json:"exitImmediate"` // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization)
24         CmdTimeout    int      `json:"timeout"`       // command completion timeout in Second
25 }
26
27 // MakeOutMsg Message send on each output (stdout+stderr) of make command
28 type MakeOutMsg struct {
29         CmdID     string `json:"cmdID"`
30         Timestamp string `json:"timestamp"`
31         Stdout    string `json:"stdout"`
32         Stderr    string `json:"stderr"`
33 }
34
35 // MakeExitMsg Message send on make command exit
36 type MakeExitMsg struct {
37         CmdID     string `json:"cmdID"`
38         Timestamp string `json:"timestamp"`
39         Code      int    `json:"code"`
40         Error     error  `json:"error"`
41 }
42
43 // MakeOutEvent Event send in WS when characters are received on stdout/stderr
44 const MakeOutEvent = "make:output"
45
46 // MakeExitEvent Event send in WS when command exited
47 const MakeExitEvent = "make:exit"
48
49 var makeCommandID = 1
50
51 func (s *APIService) buildMake(c *gin.Context) {
52         var args MakeArgs
53
54         if c.BindJSON(&args) != nil {
55                 common.APIError(c, "Invalid arguments")
56                 return
57         }
58
59         sess := s.sessions.Get(c)
60         if sess == nil {
61                 common.APIError(c, "Unknown sessions")
62                 return
63         }
64         sop := sess.IOSocket
65         if sop == nil {
66                 common.APIError(c, "Websocket not established")
67                 return
68         }
69
70         // Allow to pass id in url (/make/:id) or as JSON argument
71         idArg := c.Param("id")
72         if idArg == "" {
73                 idArg = args.ID
74         }
75         if idArg == "" {
76                 common.APIError(c, "Invalid id")
77                 return
78         }
79         id, err := s.mfolders.ResolveID(idArg)
80         if err != nil {
81                 common.APIError(c, err.Error())
82                 return
83         }
84         pf := s.mfolders.Get(id)
85         if pf == nil {
86                 common.APIError(c, "Unknown id")
87                 return
88         }
89         folder := *pf
90         prj := folder.GetConfig()
91
92         execTmo := args.CmdTimeout
93         if execTmo == 0 {
94                 // TODO get default timeout from config.json file
95                 execTmo = 24 * 60 * 60 // 1 day
96         }
97
98         // TODO merge all code below with exec.go
99
100         // Define callback for output
101         var oCB common.EmitOutputCB
102         oCB = func(sid string, cmdID string, stdout, stderr string, data *map[string]interface{}) {
103                 // IO socket can be nil when disconnected
104                 so := s.sessions.IOSocketGet(sid)
105                 if so == nil {
106                         s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%s", MakeOutEvent, sid, cmdID)
107                         return
108                 }
109
110                 // Retrieve project ID and RootPath
111                 prjID := (*data)["ID"].(string)
112                 prjRootPath := (*data)["RootPath"].(string)
113
114                 // Cleanup any references to internal rootpath in stdout & stderr
115                 stdout = strings.Replace(stdout, prjRootPath, "", -1)
116                 stderr = strings.Replace(stderr, prjRootPath, "", -1)
117
118                 s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", MakeOutEvent, sid, id, prjID)
119
120                 // FIXME replace by .BroadcastTo a room
121                 err := (*so).Emit(MakeOutEvent, MakeOutMsg{
122                         CmdID:     cmdID,
123                         Timestamp: time.Now().String(),
124                         Stdout:    stdout,
125                         Stderr:    stderr,
126                 })
127                 if err != nil {
128                         s.log.Errorf("WS Emit : %v", err)
129                 }
130         }
131
132         // Define callback for output
133         eCB := func(sid string, cmdID string, code int, err error, data *map[string]interface{}) {
134                 s.log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", cmdID, code, err)
135
136                 // IO socket can be nil when disconnected
137                 so := s.sessions.IOSocketGet(sid)
138                 if so == nil {
139                         s.log.Infof("%s not emitted - WS closed (id:%s", MakeExitEvent, cmdID)
140                         return
141                 }
142
143                 // Retrieve project ID and RootPath
144                 prjID := (*data)["ID"].(string)
145                 exitImm := (*data)["ExitImmediate"].(bool)
146
147                 // XXX - workaround to be sure that Syncthing detected all changes
148                 if err := s.mfolders.ForceSync(prjID); err != nil {
149                         s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
150                 }
151                 if !exitImm {
152                         // Wait end of file sync
153                         // FIXME pass as argument
154                         tmo := 60
155                         for t := tmo; t > 0; t-- {
156                                 s.log.Debugf("Wait file insync for %s (%d/%d)", prjID, t, tmo)
157                                 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
158                                         if err != nil {
159                                                 s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
160                                         }
161                                         break
162                                 }
163                                 time.Sleep(time.Second)
164                         }
165                 }
166
167                 // FIXME replace by .BroadcastTo a room
168                 e := (*so).Emit(MakeExitEvent, MakeExitMsg{
169                         CmdID:     id,
170                         Timestamp: time.Now().String(),
171                         Code:      code,
172                         Error:     err,
173                 })
174                 if e != nil {
175                         s.log.Errorf("WS Emit : %v", e)
176                 }
177         }
178
179         // Unique ID for each commands
180         if args.CmdID == "" {
181                 args.CmdID = s.cfg.ServerUID[:18] + "_" + strconv.Itoa(makeCommandID)
182                 makeCommandID++
183         }
184         cmd := []string{}
185
186         // Retrieve env command regarding Sdk ID
187         if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
188                 cmd = append(cmd, envCmd...)
189                 cmd = append(cmd, "&&")
190         }
191
192         cmd = append(cmd, "cd", folder.GetFullPath(args.RPath), "&&", "make")
193         if len(args.Args) > 0 {
194                 cmd = append(cmd, args.Args...)
195         }
196
197         s.log.Debugf("Execute [Cmd ID %d]: %v", args.CmdID, cmd)
198
199         data := make(map[string]interface{})
200         data["ID"] = prj.ID
201         data["RootPath"] = prj.RootPath
202         data["ExitImmediate"] = args.ExitImmediate
203
204         err = common.ExecPipeWs(cmd, args.Env, sop, sess.ID, args.CmdID, execTmo, s.log, oCB, eCB, &data)
205         if err != nil {
206                 common.APIError(c, err.Error())
207                 return
208         }
209
210         c.JSON(http.StatusOK,
211                 gin.H{
212                         "status": "OK",
213                         "cmdID":  args.CmdID,
214                 })
215 }