Add Server UUID and use it build CmdID.
[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         id := c.Param("id")
72         if id == "" {
73                 id = args.ID
74         }
75         if id == "" {
76                 common.APIError(c, "Invalid id")
77                 return
78         }
79
80         pf := s.mfolders.Get(id)
81         if pf == nil {
82                 common.APIError(c, "Unknown id")
83                 return
84         }
85         folder := *pf
86         prj := folder.GetConfig()
87
88         execTmo := args.CmdTimeout
89         if execTmo == 0 {
90                 // TODO get default timeout from config.json file
91                 execTmo = 24 * 60 * 60 // 1 day
92         }
93
94         // TODO merge all code below with exec.go
95
96         // Define callback for output
97         var oCB common.EmitOutputCB
98         oCB = func(sid string, cmdID string, stdout, stderr string, data *map[string]interface{}) {
99                 // IO socket can be nil when disconnected
100                 so := s.sessions.IOSocketGet(sid)
101                 if so == nil {
102                         s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%s", MakeOutEvent, sid, cmdID)
103                         return
104                 }
105
106                 // Retrieve project ID and RootPath
107                 prjID := (*data)["ID"].(string)
108                 prjRootPath := (*data)["RootPath"].(string)
109
110                 // Cleanup any references to internal rootpath in stdout & stderr
111                 stdout = strings.Replace(stdout, prjRootPath, "", -1)
112                 stderr = strings.Replace(stderr, prjRootPath, "", -1)
113
114                 s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", MakeOutEvent, sid, id, prjID)
115
116                 // FIXME replace by .BroadcastTo a room
117                 err := (*so).Emit(MakeOutEvent, MakeOutMsg{
118                         CmdID:     cmdID,
119                         Timestamp: time.Now().String(),
120                         Stdout:    stdout,
121                         Stderr:    stderr,
122                 })
123                 if err != nil {
124                         s.log.Errorf("WS Emit : %v", err)
125                 }
126         }
127
128         // Define callback for output
129         eCB := func(sid string, cmdID string, code int, err error, data *map[string]interface{}) {
130                 s.log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", cmdID, code, err)
131
132                 // IO socket can be nil when disconnected
133                 so := s.sessions.IOSocketGet(sid)
134                 if so == nil {
135                         s.log.Infof("%s not emitted - WS closed (id:%s", MakeExitEvent, cmdID)
136                         return
137                 }
138
139                 // Retrieve project ID and RootPath
140                 prjID := (*data)["ID"].(string)
141                 exitImm := (*data)["ExitImmediate"].(bool)
142
143                 // XXX - workaround to be sure that Syncthing detected all changes
144                 if err := s.mfolders.ForceSync(prjID); err != nil {
145                         s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
146                 }
147                 if !exitImm {
148                         // Wait end of file sync
149                         // FIXME pass as argument
150                         tmo := 60
151                         for t := tmo; t > 0; t-- {
152                                 s.log.Debugf("Wait file insync for %s (%d/%d)", prjID, t, tmo)
153                                 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
154                                         if err != nil {
155                                                 s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
156                                         }
157                                         break
158                                 }
159                                 time.Sleep(time.Second)
160                         }
161                 }
162
163                 // FIXME replace by .BroadcastTo a room
164                 e := (*so).Emit(MakeExitEvent, MakeExitMsg{
165                         CmdID:     id,
166                         Timestamp: time.Now().String(),
167                         Code:      code,
168                         Error:     err,
169                 })
170                 if e != nil {
171                         s.log.Errorf("WS Emit : %v", e)
172                 }
173         }
174
175         // Unique ID for each commands
176         if args.CmdID == "" {
177                 args.CmdID = s.cfg.ServerUID[:18] + "_" + strconv.Itoa(makeCommandID)
178                 makeCommandID++
179         }
180         cmd := []string{}
181
182         // Retrieve env command regarding Sdk ID
183         if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
184                 cmd = append(cmd, envCmd...)
185                 cmd = append(cmd, "&&")
186         }
187
188         cmd = append(cmd, "cd", folder.GetFullPath(args.RPath), "&&", "make")
189         if len(args.Args) > 0 {
190                 cmd = append(cmd, args.Args...)
191         }
192
193         s.log.Debugf("Execute [Cmd ID %d]: %v", args.CmdID, cmd)
194
195         data := make(map[string]interface{})
196         data["ID"] = prj.ID
197         data["RootPath"] = prj.RootPath
198         data["ExitImmediate"] = args.ExitImmediate
199
200         err := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, args.CmdID, execTmo, s.log, oCB, eCB, &data)
201         if err != nil {
202                 common.APIError(c, err.Error())
203                 return
204         }
205
206         c.JSON(http.StatusOK,
207                 gin.H{
208                         "status": "OK",
209                         "cmdID":  args.CmdID,
210                 })
211 }