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