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