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