Use go module as dependency tool instead of glide
[src/xds/xds-agent.git] / lib / syncthing / st.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 st
19
20 import (
21         "encoding/json"
22         "io"
23         "io/ioutil"
24         "os"
25         "path"
26         "path/filepath"
27         "regexp"
28         "strings"
29         "syscall"
30         "time"
31
32         "fmt"
33
34         "os/exec"
35
36         "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xdsconfig"
37         common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git"
38         "github.com/Sirupsen/logrus"
39         "github.com/syncthing/syncthing/lib/config"
40 )
41
42 // SyncThing .
43 type SyncThing struct {
44         BaseURL   string
45         APIKey    string
46         Home      string
47         STCmd     *exec.Cmd
48         STICmd    *exec.Cmd
49         MyID      string
50         Connected bool
51         Events    *Events
52
53         // Private fields
54         binDir      string
55         logsDir     string
56         exitSTChan  chan ExitChan
57         exitSTIChan chan ExitChan
58         client      *common.HTTPClient
59         log         *logrus.Logger
60         conf        *xdsconfig.Config
61 }
62
63 // ExitChan Channel used for process exit
64 type ExitChan struct {
65         status int
66         err    error
67 }
68
69 // ConfigInSync Check whether if Syncthing configuration is in sync
70 type configInSync struct {
71         ConfigInSync bool `json:"configInSync"`
72 }
73
74 // FolderStatus Information about the current status of a folder.
75 type FolderStatus struct {
76         GlobalFiles       int   `json:"globalFiles"`
77         GlobalDirectories int   `json:"globalDirectories"`
78         GlobalSymlinks    int   `json:"globalSymlinks"`
79         GlobalDeleted     int   `json:"globalDeleted"`
80         GlobalBytes       int64 `json:"globalBytes"`
81
82         LocalFiles       int   `json:"localFiles"`
83         LocalDirectories int   `json:"localDirectories"`
84         LocalSymlinks    int   `json:"localSymlinks"`
85         LocalDeleted     int   `json:"localDeleted"`
86         LocalBytes       int64 `json:"localBytes"`
87
88         NeedFiles       int   `json:"needFiles"`
89         NeedDirectories int   `json:"needDirectories"`
90         NeedSymlinks    int   `json:"needSymlinks"`
91         NeedDeletes     int   `json:"needDeletes"`
92         NeedBytes       int64 `json:"needBytes"`
93
94         InSyncFiles int   `json:"inSyncFiles"`
95         InSyncBytes int64 `json:"inSyncBytes"`
96
97         State        string    `json:"state"`
98         StateChanged time.Time `json:"stateChanged"`
99
100         Sequence int64 `json:"sequence"`
101
102         IgnorePatterns bool `json:"ignorePatterns"`
103 }
104
105 // NewSyncThing creates a new instance of Syncthing
106 func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
107         var url, apiKey, home, binDir string
108
109         stCfg := conf.FileConf.SThgConf
110         if stCfg != nil {
111                 url = stCfg.GuiAddress
112                 apiKey = stCfg.GuiAPIKey
113                 home = stCfg.Home
114                 binDir = stCfg.BinDir
115         }
116
117         if url == "" {
118                 url = "http://localhost:8386"
119         }
120         if url[0:7] != "http://" {
121                 url = "http://" + url
122         }
123
124         if home == "" {
125                 panic("home parameter must be set")
126         }
127
128         s := SyncThing{
129                 BaseURL: url,
130                 APIKey:  apiKey,
131                 Home:    home,
132                 binDir:  binDir,
133                 logsDir: conf.FileConf.LogsDir,
134                 log:     log,
135                 conf:    conf,
136         }
137
138         // Create Events monitoring
139         s.Events = s.NewEventListener()
140
141         return &s
142 }
143
144 // Start Starts syncthing process
145 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
146         var err error
147         var exePath string
148
149         // Kill existing process (useful for debug ;-) )
150         if _, dbg := os.LookupEnv("XDS_DEBUG_MODE"); dbg {
151                 fmt.Printf("\n!!! DEBUG_MODE set: KILL existing %s process(es) !!!\n", exeName)
152                 exec.Command("bash", "-c", "ps -ax |grep "+exeName+" |grep "+s.BaseURL+" |cut  -d' ' -f 1|xargs -I{} kill -9 {}").Output()
153         }
154
155         // When not set (or set to '.') set bin to path of xds-agent executable
156         bdir := s.binDir
157         if bdir == "" || bdir == "." {
158                 exe, _ := os.Executable()
159                 if exeAbsPath, err := filepath.Abs(exe); err == nil {
160                         if exePath, err := filepath.EvalSymlinks(exeAbsPath); err == nil {
161                                 bdir = filepath.Dir(exePath)
162                         }
163                 }
164         }
165
166         exePath, err = exec.LookPath(path.Join(bdir, exeName))
167         if err != nil {
168                 // Let's try in /opt/AGL/bin
169                 exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
170                 if err != nil {
171                         return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
172                 }
173         }
174         cmd := exec.Command(exePath, args...)
175         cmd.Env = os.Environ()
176         for _, ev := range env {
177                 cmd.Env = append(cmd.Env, ev)
178         }
179
180         // open log file
181         var outfile *os.File
182         logFilename := filepath.Join(s.logsDir, exeName+".log")
183         if s.logsDir != "" {
184                 outfile, err := os.Create(logFilename)
185                 if err != nil {
186                         return nil, fmt.Errorf("Cannot create log file %s", logFilename)
187                 }
188
189                 cmdOut, err := cmd.StdoutPipe()
190                 if err != nil {
191                         return nil, fmt.Errorf("Pipe stdout error for : %v", err)
192                 }
193
194                 go io.Copy(outfile, cmdOut)
195         }
196
197         err = cmd.Start()
198         if err != nil {
199                 return nil, err
200         }
201
202         *eChan = make(chan ExitChan, 1)
203         go func(c *exec.Cmd, oF *os.File) {
204                 status := 0
205                 sts, err := c.Process.Wait()
206                 if !sts.Success() {
207                         s := sts.Sys().(syscall.WaitStatus)
208                         status = s.ExitStatus()
209                 }
210                 if oF != nil {
211                         oF.Close()
212                 }
213                 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
214
215                 *eChan <- ExitChan{status, err}
216         }(cmd, outfile)
217
218         return cmd, nil
219 }
220
221 // Start Starts syncthing process
222 func (s *SyncThing) Start() (*exec.Cmd, error) {
223         var err error
224
225         s.log.Infof(" ST home=%s", s.Home)
226         s.log.Infof(" ST  url=%s", s.BaseURL)
227
228         args := []string{
229                 "--home=" + s.Home,
230                 "-no-browser",
231                 "--gui-address=" + s.BaseURL,
232         }
233
234         if s.APIKey != "" {
235                 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
236                 s.log.Infof(" ST apikey=%s", s.APIKey)
237         }
238         if s.log.Level == logrus.DebugLevel {
239                 args = append(args, "-verbose")
240         }
241
242         env := []string{
243                 "STNODEFAULTFOLDER=1",
244                 "STNOUPGRADE=1",
245         }
246
247         /* FIXME - STILL NEEDED ?, if not SUP code
248
249         // XXX - temporary hack because -gui-apikey seems to correctly handle by
250         // syncthing the early first time
251         stConfigFile := filepath.Join(s.Home, "config.xml")
252         if s.APIKey != "" && !common.Exists(stConfigFile) {
253                 s.log.Infof("Stop and restart Syncthing (hack for apikey setting)")
254                 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
255                 tmo := 20
256                 for ; tmo > 0; tmo-- {
257                         s.log.Debugf("Waiting Syncthing config.xml creation (%v)\n", tmo)
258                         time.Sleep(500 * time.Millisecond)
259                         if common.Exists(stConfigFile) {
260                                 break
261                         }
262                 }
263                 if tmo <= 0 {
264                         return nil, fmt.Errorf("Cannot start Syncthing for config file creation")
265                 }
266                 s.Stop()
267                 read, err := ioutil.ReadFile(stConfigFile)
268                 if err != nil {
269                         return nil, fmt.Errorf("Cannot read Syncthing config file for apikey setting")
270                 }
271                 re := regexp.MustCompile(`<apikey>.*</apikey>`)
272                 newContents := re.ReplaceAllString(string(read), "<apikey>"+s.APIKey+"</apikey>")
273                 err = ioutil.WriteFile(stConfigFile, []byte(newContents), 0)
274                 if err != nil {
275                         return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
276                 }
277         }
278         */
279         s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
280
281         // Use autogenerated apikey if not set by agent-config.json
282         if err == nil && s.APIKey == "" {
283                 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
284                         defer fd.Close()
285                         if b, err := ioutil.ReadAll(fd); err == nil {
286                                 re := regexp.MustCompile("<apikey>(.*)</apikey>")
287                                 key := re.FindStringSubmatch(string(b))
288                                 if len(key) >= 1 {
289                                         s.APIKey = key[1]
290                                 }
291                         }
292                 }
293         }
294
295         return s.STCmd, err
296 }
297
298 // StartInotify Starts syncthing-inotify process
299 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
300         var err error
301         exeName := "syncthing-inotify"
302
303         s.log.Infof(" STI  url=%s", s.BaseURL)
304
305         args := []string{
306                 "-target=" + s.BaseURL,
307         }
308         if s.APIKey != "" {
309                 args = append(args, "-api="+s.APIKey)
310                 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
311         }
312         if s.log.Level == logrus.DebugLevel {
313                 args = append(args, "-verbosity=4")
314         }
315
316         env := []string{}
317
318         s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
319
320         return s.STICmd, err
321 }
322
323 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
324         if err := proc.Signal(os.Interrupt); err != nil {
325                 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
326
327                 select {
328                 case <-exit:
329                 case <-time.After(time.Second):
330                         // A bigger bonk on the head.
331                         if err := proc.Signal(os.Kill); err != nil {
332                                 s.log.Infof("Proc term %s error: %s", pname, err.Error())
333                         }
334                         <-exit
335                 }
336         }
337         s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
338 }
339
340 // Stop Stops syncthing process
341 func (s *SyncThing) Stop() {
342         if s.STCmd == nil {
343                 return
344         }
345         s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
346         s.STCmd = nil
347 }
348
349 // StopInotify Stops syncthing process
350 func (s *SyncThing) StopInotify() {
351         if s.STICmd == nil {
352                 return
353         }
354         s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
355         s.STICmd = nil
356 }
357
358 // Connect Establish HTTP connection with Syncthing
359 func (s *SyncThing) Connect() error {
360         var err error
361         s.Connected = false
362         s.client, err = common.HTTPNewClient(s.BaseURL,
363                 common.HTTPClientConfig{
364                         URLPrefix:           "/rest",
365                         HeaderClientKeyName: "X-Syncthing-ID",
366                         LogOut:              s.conf.LogVerboseOut,
367                         LogPrefix:           "SYNCTHING: ",
368                         LogLevel:            common.HTTPLogLevelWarning,
369                 })
370         s.client.SetLogLevel(s.log.Level.String())
371
372         if err != nil {
373                 msg := ": " + err.Error()
374                 if strings.Contains(err.Error(), "connection refused") {
375                         msg = fmt.Sprintf("(url: %s)", s.BaseURL)
376                 }
377                 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
378         }
379         if s.client == nil {
380                 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
381         }
382
383         s.MyID, err = s.IDGet()
384         if err != nil {
385                 return fmt.Errorf("ERROR: cannot retrieve ID")
386         }
387
388         s.Connected = true
389
390         // Start events monitoring
391         err = s.Events.Start()
392
393         return err
394 }
395
396 // IDGet returns the Syncthing ID of Syncthing instance running locally
397 func (s *SyncThing) IDGet() (string, error) {
398         var data []byte
399         if err := s.client.HTTPGet("system/status", &data); err != nil {
400                 return "", err
401         }
402         status := make(map[string]interface{})
403         json.Unmarshal(data, &status)
404         return status["myID"].(string), nil
405 }
406
407 // ConfigGet returns the current Syncthing configuration
408 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
409         var data []byte
410         config := config.Configuration{}
411         if err := s.client.HTTPGet("system/config", &data); err != nil {
412                 return config, err
413         }
414         err := json.Unmarshal(data, &config)
415         return config, err
416 }
417
418 // ConfigSet set Syncthing configuration
419 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
420         body, err := json.Marshal(cfg)
421         if err != nil {
422                 return err
423         }
424         return s.client.HTTPPost("system/config", string(body))
425 }
426
427 // IsConfigInSync Returns true if configuration is in sync
428 func (s *SyncThing) IsConfigInSync() (bool, error) {
429         var data []byte
430         var d configInSync
431         if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
432                 return false, err
433         }
434         if err := json.Unmarshal(data, &d); err != nil {
435                 return false, err
436         }
437         return d.ConfigInSync, nil
438 }