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