304cfca5fe9af83c4cce78485c963d416fb9c835
[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                 fmt.Printf("\n!!! DEBUG_MODE set: KILL existing %s process(es) !!!\n", exeName)
135                 exec.Command("bash", "-c", "ps -ax |grep "+exeName+" |grep "+s.BaseURL+" |cut  -d' ' -f 1|xargs -I{} kill -9 {}").Output()
136         }
137
138         // When not set (or set to '.') set bin to path of xds-agent executable
139         bdir := s.binDir
140         if bdir == "" || bdir == "." {
141                 exe, _ := os.Executable()
142                 if exeAbsPath, err := filepath.Abs(exe); err == nil {
143                         if exePath, err := filepath.EvalSymlinks(exeAbsPath); err == nil {
144                                 bdir = filepath.Dir(exePath)
145                         }
146                 }
147         }
148
149         exePath, err = exec.LookPath(path.Join(bdir, exeName))
150         if err != nil {
151                 // Let's try in /opt/AGL/bin
152                 exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
153                 if err != nil {
154                         return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
155                 }
156         }
157         cmd := exec.Command(exePath, args...)
158         cmd.Env = os.Environ()
159         for _, ev := range env {
160                 cmd.Env = append(cmd.Env, ev)
161         }
162
163         // open log file
164         var outfile *os.File
165         logFilename := filepath.Join(s.logsDir, exeName+".log")
166         if s.logsDir != "" {
167                 outfile, err := os.Create(logFilename)
168                 if err != nil {
169                         return nil, fmt.Errorf("Cannot create log file %s", logFilename)
170                 }
171
172                 cmdOut, err := cmd.StdoutPipe()
173                 if err != nil {
174                         return nil, fmt.Errorf("Pipe stdout error for : %s", err)
175                 }
176
177                 go io.Copy(outfile, cmdOut)
178         }
179
180         err = cmd.Start()
181         if err != nil {
182                 return nil, err
183         }
184
185         *eChan = make(chan ExitChan, 1)
186         go func(c *exec.Cmd, oF *os.File) {
187                 status := 0
188                 sts, err := c.Process.Wait()
189                 if !sts.Success() {
190                         s := sts.Sys().(syscall.WaitStatus)
191                         status = s.ExitStatus()
192                 }
193                 if oF != nil {
194                         oF.Close()
195                 }
196                 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
197
198                 *eChan <- ExitChan{status, err}
199         }(cmd, outfile)
200
201         return cmd, nil
202 }
203
204 // Start Starts syncthing process
205 func (s *SyncThing) Start() (*exec.Cmd, error) {
206         var err error
207
208         s.log.Infof(" ST home=%s", s.Home)
209         s.log.Infof(" ST  url=%s", s.BaseURL)
210
211         args := []string{
212                 "--home=" + s.Home,
213                 "-no-browser",
214                 "--gui-address=" + s.BaseURL,
215         }
216
217         if s.APIKey != "" {
218                 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
219                 s.log.Infof(" ST apikey=%s", s.APIKey)
220         }
221         if s.log.Level == logrus.DebugLevel {
222                 args = append(args, "-verbose")
223         }
224
225         env := []string{
226                 "STNODEFAULTFOLDER=1",
227                 "STNOUPGRADE=1",
228         }
229
230         /* SEB STILL NEEDED, if not SUP code
231
232         // XXX - temporary hack because -gui-apikey seems to correctly handle by
233         // syncthing the early first time
234         stConfigFile := filepath.Join(s.Home, "config.xml")
235         if s.APIKey != "" && !common.Exists(stConfigFile) {
236                 s.log.Infof("Stop and restart Syncthing (hack for apikey setting)")
237                 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
238                 tmo := 20
239                 for ; tmo > 0; tmo-- {
240                         s.log.Debugf("Waiting Syncthing config.xml creation (%v)\n", tmo)
241                         time.Sleep(500 * time.Millisecond)
242                         if common.Exists(stConfigFile) {
243                                 break
244                         }
245                 }
246                 if tmo <= 0 {
247                         return nil, fmt.Errorf("Cannot start Syncthing for config file creation")
248                 }
249                 s.Stop()
250                 read, err := ioutil.ReadFile(stConfigFile)
251                 if err != nil {
252                         return nil, fmt.Errorf("Cannot read Syncthing config file for apikey setting")
253                 }
254                 re := regexp.MustCompile(`<apikey>.*</apikey>`)
255                 newContents := re.ReplaceAllString(string(read), "<apikey>"+s.APIKey+"</apikey>")
256                 err = ioutil.WriteFile(stConfigFile, []byte(newContents), 0)
257                 if err != nil {
258                         return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
259                 }
260         }
261         */
262         s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
263
264         // Use autogenerated apikey if not set by config.json
265         if err == nil && s.APIKey == "" {
266                 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
267                         defer fd.Close()
268                         if b, err := ioutil.ReadAll(fd); err == nil {
269                                 re := regexp.MustCompile("<apikey>(.*)</apikey>")
270                                 key := re.FindStringSubmatch(string(b))
271                                 if len(key) >= 1 {
272                                         s.APIKey = key[1]
273                                 }
274                         }
275                 }
276         }
277
278         return s.STCmd, err
279 }
280
281 // StartInotify Starts syncthing-inotify process
282 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
283         var err error
284         exeName := "syncthing-inotify"
285
286         s.log.Infof(" STI  url=%s", s.BaseURL)
287
288         args := []string{
289                 "-target=" + s.BaseURL,
290         }
291         if s.APIKey != "" {
292                 args = append(args, "-api="+s.APIKey)
293                 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
294         }
295         if s.log.Level == logrus.DebugLevel {
296                 args = append(args, "-verbosity=4")
297         }
298
299         env := []string{}
300
301         s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
302
303         return s.STICmd, err
304 }
305
306 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
307         if err := proc.Signal(os.Interrupt); err != nil {
308                 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
309
310                 select {
311                 case <-exit:
312                 case <-time.After(time.Second):
313                         // A bigger bonk on the head.
314                         if err := proc.Signal(os.Kill); err != nil {
315                                 s.log.Infof("Proc term %s error: %s", pname, err.Error())
316                         }
317                         <-exit
318                 }
319         }
320         s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
321 }
322
323 // Stop Stops syncthing process
324 func (s *SyncThing) Stop() {
325         if s.STCmd == nil {
326                 return
327         }
328         s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
329         s.STCmd = nil
330 }
331
332 // StopInotify Stops syncthing process
333 func (s *SyncThing) StopInotify() {
334         if s.STICmd == nil {
335                 return
336         }
337         s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
338         s.STICmd = nil
339 }
340
341 // Connect Establish HTTP connection with Syncthing
342 func (s *SyncThing) Connect() error {
343         var err error
344         s.Connected = false
345         s.client, err = common.HTTPNewClient(s.BaseURL,
346                 common.HTTPClientConfig{
347                         URLPrefix:           "/rest",
348                         HeaderClientKeyName: "X-Syncthing-ID",
349                         LogOut:              s.conf.LogVerboseOut,
350                         LogPrefix:           "SYNCTHING: ",
351                         LogLevel:            common.HTTPLogLevelWarning,
352                 })
353         s.client.SetLogLevel(s.log.Level.String())
354
355         if err != nil {
356                 msg := ": " + err.Error()
357                 if strings.Contains(err.Error(), "connection refused") {
358                         msg = fmt.Sprintf("(url: %s)", s.BaseURL)
359                 }
360                 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
361         }
362         if s.client == nil {
363                 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
364         }
365
366         s.MyID, err = s.IDGet()
367         if err != nil {
368                 return fmt.Errorf("ERROR: cannot retrieve ID")
369         }
370
371         s.Connected = true
372
373         // Start events monitoring
374         //SEB TODO err = s.Events.Start()
375
376         return err
377 }
378
379 // IDGet returns the Syncthing ID of Syncthing instance running locally
380 func (s *SyncThing) IDGet() (string, error) {
381         var data []byte
382         if err := s.client.HTTPGet("system/status", &data); err != nil {
383                 return "", err
384         }
385         status := make(map[string]interface{})
386         json.Unmarshal(data, &status)
387         return status["myID"].(string), nil
388 }
389
390 // ConfigGet returns the current Syncthing configuration
391 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
392         var data []byte
393         config := config.Configuration{}
394         if err := s.client.HTTPGet("system/config", &data); err != nil {
395                 return config, err
396         }
397         err := json.Unmarshal(data, &config)
398         return config, err
399 }
400
401 // ConfigSet set Syncthing configuration
402 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
403         body, err := json.Marshal(cfg)
404         if err != nil {
405                 return err
406         }
407         return s.client.HTTPPost("system/config", string(body))
408 }
409
410 // IsConfigInSync Returns true if configuration is in sync
411 func (s *SyncThing) IsConfigInSync() (bool, error) {
412         var data []byte
413         var d configInSync
414         if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
415                 return false, err
416         }
417         if err := json.Unmarshal(data, &d); err != nil {
418                 return false, err
419         }
420         return d.ConfigInSync, nil
421 }