c011d094ec9394cf719abbbbddbce5a9d2db1dca
[src/xds/xds-server.git] / lib / xdsserver / sdk.go
1 /*
2  * Copyright (C) 2017 "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         "github.com/Sirupsen/logrus"
31         common "github.com/iotbzh/xds-common/golib"
32         "github.com/iotbzh/xds-common/golib/eows"
33         "github.com/iotbzh/xds-server/lib/xsapiv1"
34         uuid "github.com/satori/go.uuid"
35 )
36
37 // Definition of scripts used to managed SDKs
38 const (
39         scriptAdd       = "add"
40         scriptGetConfig = "get-config"
41         scriptList      = "list"
42         scriptRemove    = "remove"
43         scriptUpdate    = "update"
44 )
45
46 var scriptsAll = []string{
47         scriptAdd,
48         scriptGetConfig,
49         scriptList,
50         scriptRemove,
51         scriptUpdate,
52 }
53
54 var sdkCmdID = 0
55
56 // CrossSDK Hold SDK config
57 type CrossSDK struct {
58         *Context
59         sdk        xsapiv1.SDK
60         scripts    map[string]string
61         installCmd *eows.ExecOverWS
62         removeCmd  *eows.ExecOverWS
63
64         bufStdout string
65         bufStderr string
66 }
67
68 // ListCrossSDK List all available and installed SDK  (call "list" script)
69 func ListCrossSDK(scriptDir string, log *logrus.Logger) ([]xsapiv1.SDK, error) {
70         sdksList := []xsapiv1.SDK{}
71
72         // Retrieve SDKs list and info
73         cmd := exec.Command(path.Join(scriptDir, scriptList))
74         stdout, err := cmd.CombinedOutput()
75         if err != nil {
76                 return sdksList, fmt.Errorf("Cannot get sdks list: %v", err)
77         }
78
79         if err = json.Unmarshal(stdout, &sdksList); err != nil {
80                 log.Errorf("SDK list script output:\n%v\n", string(stdout))
81                 return sdksList, fmt.Errorf("Cannot decode sdk list %v", err)
82         }
83
84         return sdksList, nil
85 }
86
87 // NewCrossSDK creates a new instance of Syncthing
88 func NewCrossSDK(ctx *Context, sdk xsapiv1.SDK, scriptDir string) (*CrossSDK, error) {
89         s := CrossSDK{
90                 Context: ctx,
91                 sdk:     sdk,
92                 scripts: make(map[string]string),
93         }
94
95         // Execute get-config script to retrieve SDK configuration
96         getConfFile := path.Join(scriptDir, scriptGetConfig)
97         if !common.Exists(getConfFile) {
98                 return &s, fmt.Errorf("'%s' script file not found in %s", scriptGetConfig, scriptDir)
99         }
100
101         cmd := exec.Command(getConfFile)
102         stdout, err := cmd.CombinedOutput()
103         if err != nil {
104                 return &s, fmt.Errorf("Cannot get sdk config using %s: %v", getConfFile, err)
105         }
106
107         err = json.Unmarshal(stdout, &s.sdk.FamilyConf)
108         if err != nil {
109                 s.Log.Errorf("SDK config script output:\n%v\n", string(stdout))
110                 return &s, fmt.Errorf("Cannot decode sdk config %v", err)
111         }
112         famName := s.sdk.FamilyConf.FamilyName
113
114         // Sanity check
115         if s.sdk.FamilyConf.RootDir == "" {
116                 return &s, fmt.Errorf("SDK config not valid (rootDir not set)")
117         }
118         if s.sdk.FamilyConf.EnvSetupFile == "" {
119                 return &s, fmt.Errorf("SDK config not valid (envSetupFile not set)")
120         }
121
122         // Check that other mandatory scripts are present
123         for _, scr := range scriptsAll {
124                 s.scripts[scr] = path.Join(scriptDir, scr)
125                 if !common.Exists(s.scripts[scr]) {
126                         return &s, fmt.Errorf("Script named '%s' missing in SDK family '%s'", scr, famName)
127                 }
128         }
129
130         // Fixed default fields value
131         sdk.LastError = ""
132         if sdk.Status == "" {
133                 sdk.Status = xsapiv1.SdkStatusNotInstalled
134         }
135
136         // Sanity check
137         errMsg := "Invalid SDK definition "
138         if sdk.Name == "" {
139                 return &s, fmt.Errorf(errMsg + "(name not set)")
140         } else if sdk.Profile == "" {
141                 return &s, fmt.Errorf(errMsg + "(profile not set)")
142         } else if sdk.Version == "" {
143                 return &s, fmt.Errorf(errMsg + "(version not set)")
144         } else if sdk.Arch == "" {
145                 return &s, fmt.Errorf(errMsg + "(arch not set)")
146         }
147         if sdk.Status == xsapiv1.SdkStatusInstalled {
148                 if sdk.SetupFile == "" {
149                         return &s, fmt.Errorf(errMsg + "(setupFile not set)")
150                 } else if !common.Exists(sdk.SetupFile) {
151                         return &s, fmt.Errorf(errMsg + "(setupFile not accessible)")
152                 }
153                 if sdk.Path == "" {
154                         return &s, fmt.Errorf(errMsg + "(path not set)")
155                 } else if !common.Exists(sdk.Path) {
156                         return &s, fmt.Errorf(errMsg + "(path not accessible)")
157                 }
158         }
159
160         // Use V3 to ensure that we get same uuid on restart
161         nm := s.sdk.Name
162         if nm == "" {
163                 nm = s.sdk.Profile + "_" + s.sdk.Arch + "_" + s.sdk.Version
164         }
165         s.sdk.ID = uuid.NewV3(uuid.FromStringOrNil("sdks"), nm).String()
166
167         s.LogSillyf("New SDK: ID=%v, Family=%s, Name=%v", s.sdk.ID[:8], s.sdk.FamilyConf.FamilyName, s.sdk.Name)
168
169         return &s, nil
170 }
171
172 // Install a SDK (non blocking command, IOW run in background)
173 func (s *CrossSDK) Install(file string, force bool, timeout int, sess *ClientSession) error {
174
175         if s.sdk.Status == xsapiv1.SdkStatusInstalled {
176                 return fmt.Errorf("already installed")
177         }
178         if s.sdk.Status == xsapiv1.SdkStatusInstalling {
179                 return fmt.Errorf("installation in progress")
180         }
181
182         // Compute command args
183         cmdArgs := []string{}
184         if file != "" {
185                 cmdArgs = append(cmdArgs, "--file", file)
186         } else {
187                 cmdArgs = append(cmdArgs, "--url", s.sdk.URL)
188         }
189         if force {
190                 cmdArgs = append(cmdArgs, "--force")
191         }
192
193         // Unique command id
194         sdkCmdID++
195         cmdID := "sdk-install-" + strconv.Itoa(sdkCmdID)
196
197         // Create new instance to execute command and sent output over WS
198         s.installCmd = eows.New(s.scripts[scriptAdd], cmdArgs, sess.IOSocket, sess.ID, cmdID)
199         s.installCmd.Log = s.Log
200         if timeout > 0 {
201                 s.installCmd.CmdExecTimeout = timeout
202         } else {
203                 s.installCmd.CmdExecTimeout = 30 * 60 // default 30min
204         }
205
206         // FIXME: temporary hack
207         s.bufStdout = ""
208         s.bufStderr = ""
209         SizeBufStdout := 10
210         SizeBufStderr := 2000
211         if valS, ok := os.LookupEnv("XDS_SDK_BUF_STDOUT"); ok {
212                 if valI, err := strconv.Atoi(valS); err == nil {
213                         SizeBufStdout = valI
214                 }
215         }
216         if valS, ok := os.LookupEnv("XDS_SDK_BUF_STDERR"); ok {
217                 if valI, err := strconv.Atoi(valS); err == nil {
218                         SizeBufStderr = valI
219                 }
220         }
221
222         // Define callback for output (stdout+stderr)
223         s.installCmd.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
224                 // paranoia
225                 data := e.UserData
226                 sdkID := (*data)["SDKID"].(string)
227                 if sdkID != s.sdk.ID {
228                         s.Log.Errorln("BUG: sdk ID differs: %v != %v", sdkID, s.sdk.ID)
229                 }
230
231                 // IO socket can be nil when disconnected
232                 so := s.sessions.IOSocketGet(e.Sid)
233                 if so == nil {
234                         s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.EVTSDKInstall, e.Sid, e.CmdID)
235                         return
236                 }
237
238                 if s.LogLevelSilly {
239                         s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - SDK ID:%s:", xsapiv1.EVTSDKInstall, e.Sid[4:], e.CmdID, sdkID[:16])
240                         if stdout != "" {
241                                 s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
242                         }
243                         if stderr != "" {
244                                 s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
245                         }
246                 }
247
248                 // Temporary "Hack": Buffered sent data to avoid freeze in web Browser
249                 // FIXME: remove bufStdout & bufStderr and implement better algorithm
250                 s.bufStdout += stdout
251                 s.bufStderr += stderr
252                 if len(s.bufStdout) > SizeBufStdout || len(s.bufStderr) > SizeBufStderr {
253                         // Emit event
254                         err := (*so).Emit(xsapiv1.EVTSDKInstall, xsapiv1.SDKManagementMsg{
255                                 CmdID:     e.CmdID,
256                                 Timestamp: time.Now().String(),
257                                 Sdk:       s.sdk,
258                                 Progress:  0, // TODO add progress
259                                 Exited:    false,
260                                 Stdout:    s.bufStdout,
261                                 Stderr:    s.bufStderr,
262                         })
263                         if err != nil {
264                                 s.Log.Errorf("WS Emit : %v", err)
265                         }
266                         s.bufStdout = ""
267                         s.bufStderr = ""
268                 }
269         }
270
271         // Define callback for output
272         s.installCmd.ExitCB = func(e *eows.ExecOverWS, code int, exitError error) {
273                 // paranoia
274                 data := e.UserData
275                 sdkID := (*data)["SDKID"].(string)
276                 if sdkID != s.sdk.ID {
277                         s.Log.Errorln("BUG: sdk ID differs: %v != %v", sdkID, s.sdk.ID)
278                 }
279
280                 s.Log.Debugf("Command SDK ID %s [Cmd ID %s]  exited: code %d, exitError: %v", sdkID[:16], e.CmdID, code, exitError)
281
282                 // IO socket can be nil when disconnected
283                 so := s.sessions.IOSocketGet(e.Sid)
284                 if so == nil {
285                         s.Log.Infof("%s (exit) not emitted - WS closed (id:%s)", xsapiv1.EVTSDKInstall, e.CmdID)
286                         return
287                 }
288
289                 // Emit event remaining data in bufStdout/err
290                 if len(s.bufStderr) > 0 || len(s.bufStdout) > 0 {
291                         err := (*so).Emit(xsapiv1.EVTSDKInstall, xsapiv1.SDKManagementMsg{
292                                 CmdID:     e.CmdID,
293                                 Timestamp: time.Now().String(),
294                                 Sdk:       s.sdk,
295                                 Progress:  50, // 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                 // Update SDK status
308                 if code == 0 && exitError == nil {
309                         s.sdk.LastError = ""
310                         s.sdk.Status = xsapiv1.SdkStatusInstalled
311                 } else {
312                         s.sdk.LastError = "Installation failed (code " + strconv.Itoa(code) +
313                                 ")"
314                         if exitError != nil {
315                                 s.sdk.LastError = ". Error: " + exitError.Error()
316                         }
317                         s.sdk.Status = xsapiv1.SdkStatusNotInstalled
318                 }
319
320                 emitErr := ""
321                 if exitError != nil {
322                         emitErr = exitError.Error()
323                 }
324                 if emitErr == "" && s.sdk.LastError != "" {
325                         emitErr = s.sdk.LastError
326                 }
327
328                 // Emit event
329                 errSoEmit := (*so).Emit(xsapiv1.EVTSDKInstall, xsapiv1.SDKManagementMsg{
330                         CmdID:     e.CmdID,
331                         Timestamp: time.Now().String(),
332                         Sdk:       s.sdk,
333                         Progress:  100,
334                         Exited:    true,
335                         Code:      code,
336                         Error:     emitErr,
337                 })
338                 if errSoEmit != nil {
339                         s.Log.Errorf("WS Emit : %v", errSoEmit)
340                 }
341
342                 // Cleanup command for the next time
343                 s.installCmd = nil
344         }
345
346         // User data (used within callbacks)
347         data := make(map[string]interface{})
348         data["SDKID"] = s.sdk.ID
349         s.installCmd.UserData = &data
350
351         // Start command execution
352         s.Log.Infof("Install SDK %s: cmdID=%v, cmd=%v, args=%v", s.sdk.Name, s.installCmd.CmdID, s.installCmd.Cmd, s.installCmd.Args)
353
354         s.sdk.Status = xsapiv1.SdkStatusInstalling
355         s.sdk.LastError = ""
356
357         err := s.installCmd.Start()
358
359         return err
360 }
361
362 // AbortInstallRemove abort an install or remove command
363 func (s *CrossSDK) AbortInstallRemove(timeout int) error {
364
365         if s.installCmd == nil {
366                 return fmt.Errorf("no installation in progress for this sdk")
367         }
368
369         s.sdk.Status = xsapiv1.SdkStatusNotInstalled
370         return s.installCmd.Signal("SIGKILL")
371 }
372
373 // Remove Used to remove/uninstall a SDK
374 func (s *CrossSDK) Remove() error {
375
376         if s.sdk.Status != xsapiv1.SdkStatusInstalled {
377                 return fmt.Errorf("this sdk is not installed")
378         }
379
380         s.sdk.Status = xsapiv1.SdkStatusUninstalling
381
382         cmdline := s.scripts[scriptRemove] + " " + s.sdk.Path
383         cmd := exec.Command(cmdline)
384         stdout, err := cmd.CombinedOutput()
385         if err != nil {
386                 return fmt.Errorf("Error while uninstalling sdk: %v", err)
387         }
388         s.Log.Debugf("SDK uninstall output:\n %v", stdout)
389
390         return nil
391 }
392
393 // Get Return SDK definition
394 func (s *CrossSDK) Get() *xsapiv1.SDK {
395         return &s.sdk
396 }
397
398 // GetEnvCmd returns the command used to initialized the environment
399 func (s *CrossSDK) GetEnvCmd() []string {
400         return []string{"source", s.sdk.SetupFile}
401 }