Don't send command output too fast.
[src/xds/xds-server.git] / lib / xdsserver / sdk.go
1 /*
2  * Copyright (C) 2017-2018 "IoT.bzh"
3  * Author Sebastien Douheret <sebastien@iot.bzh>
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *   http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package xdsserver
19
20 import (
21         "encoding/json"
22         "fmt"
23         "os/exec"
24         "path"
25         "strconv"
26         "strings"
27         "time"
28
29         common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
30         "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib/eows"
31         "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
32         "github.com/Sirupsen/logrus"
33         uuid "github.com/satori/go.uuid"
34 )
35
36 // Definition of scripts used to managed SDKs
37 const (
38         scriptAdd          = "add"
39         scriptDbDump       = "db-dump"
40         scriptDbUpdate     = "db-update"
41         scriptGetFamConfig = "get-family-config"
42         scriptGetSdkInfo   = "get-sdk-info"
43         scriptRemove       = "remove"
44 )
45
46 var scriptsAll = []string{
47         scriptAdd,
48         scriptDbDump,
49         scriptDbUpdate,
50         scriptGetFamConfig,
51         scriptGetSdkInfo,
52         scriptRemove,
53 }
54
55 var sdkCmdID = 0
56
57 // CrossSDK Hold SDK config
58 type CrossSDK struct {
59         *Context
60         sdk        xsapiv1.SDK
61         scripts    map[string]string
62         installCmd *eows.ExecOverWS
63         removeCmd  *eows.ExecOverWS
64 }
65
66 // ListCrossSDK List all available and installed SDK  (call "db-dump" script)
67 func ListCrossSDK(scriptDir string, log *logrus.Logger) ([]xsapiv1.SDK, error) {
68         sdksList := []xsapiv1.SDK{}
69
70         // Retrieve SDKs list and info
71         cmd := exec.Command(path.Join(scriptDir, scriptDbDump))
72         stdout, err := cmd.CombinedOutput()
73         if err != nil {
74                 return sdksList, fmt.Errorf("Cannot get sdks list: %v", err)
75         }
76
77         if err = json.Unmarshal(stdout, &sdksList); err != nil {
78                 log.Errorf("SDK %s script output:\n%v\n", scriptDbDump, string(stdout))
79                 return sdksList, fmt.Errorf("Cannot decode sdk list %v", err)
80         }
81
82         return sdksList, nil
83 }
84
85 // GetSDKInfo Used get-sdk-info script to extract SDK get info from a SDK file/tarball
86 func GetSDKInfo(scriptDir, url, filename, md5sum string, log *logrus.Logger) (xsapiv1.SDK, error) {
87         sdk := xsapiv1.SDK{}
88
89         args := []string{}
90         if url != "" {
91                 args = append(args, "--url", url)
92         } else if filename != "" {
93                 args = append(args, "--file", filename)
94                 if md5sum != "" {
95                         args = append(args, "--md5", md5sum)
96                 }
97         } else {
98                 return sdk, fmt.Errorf("url of filename must be set")
99         }
100
101         cmd := exec.Command(path.Join(scriptDir, scriptGetSdkInfo), args...)
102         stdout, err := cmd.CombinedOutput()
103         if err != nil {
104                 return sdk, fmt.Errorf("%v %v", string(stdout), err)
105         }
106
107         if err = json.Unmarshal(stdout, &sdk); err != nil {
108                 log.Errorf("SDK %s script output:\n%v\n", scriptGetSdkInfo, string(stdout))
109                 return sdk, fmt.Errorf("Cannot decode sdk info %v", err)
110         }
111         return sdk, nil
112 }
113
114 // NewCrossSDK creates a new instance of CrossSDK
115 func NewCrossSDK(ctx *Context, sdk xsapiv1.SDK, scriptDir string) (*CrossSDK, error) {
116         s := CrossSDK{
117                 Context: ctx,
118                 sdk:     sdk,
119                 scripts: make(map[string]string),
120         }
121
122         // Execute get-config script to retrieve SDK configuration
123         getConfFile := path.Join(scriptDir, scriptGetFamConfig)
124         if !common.Exists(getConfFile) {
125                 return &s, fmt.Errorf("'%s' script file not found in %s", scriptGetFamConfig, scriptDir)
126         }
127
128         cmd := exec.Command(getConfFile)
129         stdout, err := cmd.CombinedOutput()
130         if err != nil {
131                 return &s, fmt.Errorf("Cannot get sdk config using %s: %v", getConfFile, err)
132         }
133
134         err = json.Unmarshal(stdout, &s.sdk.FamilyConf)
135         if err != nil {
136                 s.Log.Errorf("SDK config script output:\n%v\n", string(stdout))
137                 return &s, fmt.Errorf("Cannot decode sdk config %v", err)
138         }
139         famName := s.sdk.FamilyConf.FamilyName
140
141         // Sanity check
142         if s.sdk.FamilyConf.RootDir == "" {
143                 return &s, fmt.Errorf("SDK config not valid (rootDir not set)")
144         }
145         if s.sdk.FamilyConf.EnvSetupFile == "" {
146                 return &s, fmt.Errorf("SDK config not valid (envSetupFile not set)")
147         }
148
149         // Check that other mandatory scripts are present
150         for _, scr := range scriptsAll {
151                 s.scripts[scr] = path.Join(scriptDir, scr)
152                 if !common.Exists(s.scripts[scr]) {
153                         return &s, fmt.Errorf("Script named '%s' missing in SDK family '%s'", scr, famName)
154                 }
155         }
156
157         // Fixed default fields value
158         sdk.LastError = ""
159         if sdk.Status == "" {
160                 sdk.Status = xsapiv1.SdkStatusNotInstalled
161         }
162
163         // Sanity check
164         errMsg := "Invalid SDK definition "
165         if sdk.Name == "" {
166                 return &s, fmt.Errorf(errMsg + "(name not set)")
167         } else if sdk.Profile == "" {
168                 return &s, fmt.Errorf(errMsg + "(profile not set)")
169         } else if sdk.Version == "" {
170                 return &s, fmt.Errorf(errMsg + "(version not set)")
171         } else if sdk.Arch == "" {
172                 return &s, fmt.Errorf(errMsg + "(arch not set)")
173         }
174         if sdk.Status == xsapiv1.SdkStatusInstalled {
175                 if sdk.SetupFile == "" {
176                         return &s, fmt.Errorf(errMsg + "(setupFile not set)")
177                 } else if !common.Exists(sdk.SetupFile) {
178                         return &s, fmt.Errorf(errMsg + "(setupFile not accessible)")
179                 }
180                 if sdk.Path == "" {
181                         return &s, fmt.Errorf(errMsg + "(path not set)")
182                 } else if !common.Exists(sdk.Path) {
183                         return &s, fmt.Errorf(errMsg + "(path not accessible)")
184                 }
185         }
186
187         // Use V3 to ensure that we get same uuid on restart
188         nm := s.sdk.Name
189         if nm == "" {
190                 nm = s.sdk.Profile + "_" + s.sdk.Arch + "_" + s.sdk.Version
191         }
192         s.sdk.ID = uuid.NewV3(uuid.FromStringOrNil("sdks"), nm).String()
193
194         s.LogSillyf("New SDK: ID=%v, Family=%s, Name=%v", s.sdk.ID[:8], s.sdk.FamilyConf.FamilyName, s.sdk.Name)
195
196         return &s, nil
197 }
198
199 // Install a SDK (non blocking command, IOW run in background)
200 func (s *CrossSDK) Install(file string, force bool, timeout int, args []string, sess *ClientSession) error {
201
202         if s.sdk.Status == xsapiv1.SdkStatusInstalled {
203                 return fmt.Errorf("already installed")
204         }
205         if s.sdk.Status == xsapiv1.SdkStatusInstalling {
206                 return fmt.Errorf("installation in progress")
207         }
208
209         // Compute command args
210         cmdArgs := []string{}
211         if file != "" {
212                 cmdArgs = append(cmdArgs, "--file", file)
213         } else {
214                 cmdArgs = append(cmdArgs, "--url", s.sdk.URL)
215         }
216         if force {
217                 cmdArgs = append(cmdArgs, "--force")
218         }
219
220         // Append additional args (passthrough arguments)
221         if len(args) > 0 {
222                 cmdArgs = append(cmdArgs, args...)
223         }
224
225         // Unique command id
226         sdkCmdID++
227         cmdID := "sdk-install-" + strconv.Itoa(sdkCmdID)
228
229         // Create new instance to execute command and sent output over WS
230         s.installCmd = eows.New(s.scripts[scriptAdd], cmdArgs, sess.IOSocket, sess.ID, cmdID)
231         s.installCmd.Log = s.Log
232         s.installCmd.LineTimeSpan = 500 * time.Millisecond.Nanoseconds()
233         if timeout > 0 {
234                 s.installCmd.CmdExecTimeout = timeout
235         } else {
236                 s.installCmd.CmdExecTimeout = 30 * 60 // default 30min
237         }
238
239         // Define callback for output (stdout+stderr)
240         s.installCmd.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
241                 // paranoia
242                 data := e.UserData
243                 sdkID := (*data)["SDKID"].(string)
244                 if sdkID != s.sdk.ID {
245                         s.Log.Errorln("BUG: sdk ID differs: %v != %v", sdkID, s.sdk.ID)
246                 }
247
248                 // IO socket can be nil when disconnected
249                 so := s.sessions.IOSocketGet(e.Sid)
250                 if so == nil {
251                         s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.EVTSDKManagement, e.Sid, e.CmdID)
252                         return
253                 }
254
255                 if s.LogLevelSilly {
256                         s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - SDK ID:%s:", xsapiv1.EVTSDKManagement, e.Sid[4:], e.CmdID, sdkID[:16])
257                         if stdout != "" {
258                                 s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
259                         }
260                         if stderr != "" {
261                                 s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
262                         }
263                 }
264
265                 err := (*so).Emit(xsapiv1.EVTSDKManagement, xsapiv1.SDKManagementMsg{
266                         CmdID:     e.CmdID,
267                         Timestamp: time.Now().String(),
268                         Action:    xsapiv1.SdkMgtActionInstall,
269                         Sdk:       s.sdk,
270                         Progress:  0, // TODO add progress
271                         Exited:    false,
272                         Stdout:    stdout,
273                         Stderr:    stderr,
274                 })
275                 if err != nil {
276                         s.Log.Errorf("WS Emit : %v", err)
277                 }
278         }
279
280         // Define callback for output
281         s.installCmd.ExitCB = func(e *eows.ExecOverWS, code int, exitError error) {
282                 // paranoia
283                 data := e.UserData
284                 sdkID := (*data)["SDKID"].(string)
285                 if sdkID != s.sdk.ID {
286                         s.Log.Errorln("BUG: sdk ID differs: %v != %v", sdkID, s.sdk.ID)
287                 }
288
289                 s.Log.Infof("Command SDK ID %s [Cmd ID %s]  exited: code %d, exitError: %v", sdkID[:16], e.CmdID, code, exitError)
290
291                 // IO socket can be nil when disconnected
292                 so := s.sessions.IOSocketGet(e.Sid)
293                 if so == nil {
294                         s.Log.Infof("%s (exit) not emitted - WS closed (id:%s)", xsapiv1.EVTSDKManagement, e.CmdID)
295                         return
296                 }
297
298                 // Update SDK status
299                 if code == 0 && exitError == nil {
300                         s.sdk.LastError = ""
301                         s.sdk.Status = xsapiv1.SdkStatusInstalled
302
303                         // FIXME: better update it using monitoring install dir (inotify)
304                         // (see sdks.go / monitorSDKInstallation )
305                         // Update SetupFile when n
306                         if s.sdk.SetupFile == "" {
307                                 sdkDef, err := GetSDKInfo(s.sdk.FamilyConf.ScriptsDir, s.sdk.URL, "", "", s.Log)
308                                 if err != nil || sdkDef.SetupFile == "" {
309                                         code = 1
310                                         s.sdk.LastError = "Installation failed (cannot init SetupFile path)"
311                                         s.sdk.Status = xsapiv1.SdkStatusNotInstalled
312                                 } else {
313                                         s.sdk.SetupFile = sdkDef.SetupFile
314                                 }
315                         }
316
317                 } else {
318                         s.sdk.LastError = "Installation failed (code " + strconv.Itoa(code) +
319                                 ")"
320                         if exitError != nil {
321                                 s.sdk.LastError = ". Error: " + exitError.Error()
322                         }
323                         s.sdk.Status = xsapiv1.SdkStatusNotInstalled
324                 }
325
326                 emitErr := ""
327                 if exitError != nil {
328                         emitErr = exitError.Error()
329                 }
330                 if emitErr == "" && s.sdk.LastError != "" {
331                         emitErr = s.sdk.LastError
332                 }
333
334                 // Emit event
335                 errSoEmit := (*so).Emit(xsapiv1.EVTSDKManagement, xsapiv1.SDKManagementMsg{
336                         CmdID:     e.CmdID,
337                         Timestamp: time.Now().String(),
338                         Action:    xsapiv1.SdkMgtActionInstall,
339                         Sdk:       s.sdk,
340                         Progress:  100,
341                         Exited:    true,
342                         Code:      code,
343                         Error:     emitErr,
344                 })
345                 if errSoEmit != nil {
346                         s.Log.Errorf("WS Emit EVTSDKManagement : %v", errSoEmit)
347                 }
348
349                 errSoEmit = s.events.Emit(xsapiv1.EVTSDKStateChange, s.sdk, e.Sid)
350                 if errSoEmit != nil {
351                         s.Log.Errorf("WS Emit EVTSDKStateChange : %v", errSoEmit)
352                 }
353
354                 // Cleanup command for the next time
355                 s.installCmd = nil
356         }
357
358         // User data (used within callbacks)
359         data := make(map[string]interface{})
360         data["SDKID"] = s.sdk.ID
361         s.installCmd.UserData = &data
362
363         // Start command execution
364         s.Log.Infof("Install SDK %s: cmdID=%v, cmd=%v, args=%v", s.sdk.Name, s.installCmd.CmdID, s.installCmd.Cmd, s.installCmd.Args)
365
366         s.sdk.Status = xsapiv1.SdkStatusInstalling
367         s.sdk.LastError = ""
368
369         err := s.installCmd.Start()
370
371         return err
372 }
373
374 // AbortInstallRemove abort an install or remove command
375 func (s *CrossSDK) AbortInstallRemove(timeout int) error {
376
377         if s.installCmd == nil {
378                 return fmt.Errorf("no installation in progress for this sdk")
379         }
380
381         s.sdk.Status = xsapiv1.SdkStatusNotInstalled
382         return s.installCmd.Signal("SIGKILL")
383 }
384
385 // Remove Used to remove/uninstall a SDK
386 func (s *CrossSDK) Remove(timeout int, sess *ClientSession) error {
387
388         if s.sdk.Status != xsapiv1.SdkStatusInstalled {
389                 return fmt.Errorf("this sdk is not installed")
390         }
391
392         // IO socket can be nil when disconnected
393         so := s.sessions.IOSocketGet(sess.ID)
394         if so == nil {
395                 return fmt.Errorf("Cannot retrieve socket ")
396         }
397
398         s.sdk.Status = xsapiv1.SdkStatusUninstalling
399
400         // Notify state change
401         if err := s.events.Emit(xsapiv1.EVTSDKStateChange, s.sdk, sess.ID); err != nil {
402                 s.Log.Warningf("Cannot notify SDK remove: %v", err)
403         }
404
405         script := s.scripts[scriptRemove]
406         args := s.sdk.Path
407         s.Log.Infof("Uninstall SDK %s: script=%v args=%v", s.sdk.Name, script, args)
408
409         // Notify start removing
410         evData := xsapiv1.SDKManagementMsg{
411                 Timestamp: time.Now().String(),
412                 Action:    xsapiv1.SdkMgtActionRemove,
413                 Sdk:       s.sdk,
414                 Progress:  0,
415                 Exited:    false,
416                 Code:      0,
417                 Error:     "",
418         }
419         if errEmit := (*so).Emit(xsapiv1.EVTSDKManagement, evData); errEmit != nil {
420                 s.Log.Warningf("Cannot notify EVTSDKManagement end: %v", errEmit)
421         }
422
423         // Run command to remove SDK
424         cmd := exec.Command(script, args)
425         stdout, err := cmd.CombinedOutput()
426
427         s.sdk.Status = xsapiv1.SdkStatusNotInstalled
428         s.Log.Debugf("SDK uninstall err %v, output:\n %v", err, string(stdout))
429
430         // Emit end of removing process
431         evData = xsapiv1.SDKManagementMsg{
432                 Timestamp: time.Now().String(),
433                 Action:    xsapiv1.SdkMgtActionRemove,
434                 Sdk:       s.sdk,
435                 Progress:  100,
436                 Exited:    true,
437                 Code:      0,
438                 Error:     "",
439         }
440
441         // Update error code on error
442         if err != nil {
443                 evData.Code = 1
444                 evData.Error = err.Error()
445         }
446
447         if errEmit := (*so).Emit(xsapiv1.EVTSDKManagement, evData); errEmit != nil {
448                 s.Log.Warningf("Cannot notify EVTSDKManagement end: %v", errEmit)
449         }
450
451         // Notify state change
452         if errEmit := s.events.Emit(xsapiv1.EVTSDKStateChange, s.sdk, sess.ID); errEmit != nil {
453                 s.Log.Warningf("Cannot notify EVTSDKStateChange end: %v", errEmit)
454         }
455
456         if err != nil {
457                 return fmt.Errorf("Error while uninstalling sdk: %v", err)
458         }
459         return nil
460 }
461
462 // Get Return SDK definition
463 func (s *CrossSDK) Get() *xsapiv1.SDK {
464         return &s.sdk
465 }
466
467 // GetEnvCmd returns the command used to initialized the environment
468 func (s *CrossSDK) GetEnvCmd() []string {
469         if s.sdk.SetupFile == "" {
470                 return []string{}
471         }
472         return []string{"source", s.sdk.SetupFile}
473
474 }