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