Take care of ST connection lost in ST event monitor.
[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
38         // Private fields
39         binDir      string
40         logsDir     string
41         exitSTChan  chan ExitChan
42         exitSTIChan chan ExitChan
43         conf        *xdsconfig.Config
44         client      *common.HTTPClient
45         log         *logrus.Logger
46         Events      *Events
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         var err error
95
96         stCfg := conf.FileConf.SThgConf
97         if stCfg != nil {
98                 url = stCfg.GuiAddress
99                 apiKey = stCfg.GuiAPIKey
100                 home = stCfg.Home
101                 binDir = stCfg.BinDir
102         }
103
104         if url == "" {
105                 url = "http://localhost:8384"
106         }
107         if url[0:7] != "http://" {
108                 url = "http://" + url
109         }
110
111         if home == "" {
112                 home = "/mnt/share"
113         }
114
115         if binDir == "" {
116                 if binDir, err = filepath.Abs(filepath.Dir(os.Args[0])); err != nil {
117                         binDir = "/usr/local/bin"
118                 }
119         }
120
121         s := SyncThing{
122                 BaseURL: url,
123                 APIKey:  apiKey,
124                 Home:    home,
125                 binDir:  binDir,
126                 logsDir: conf.FileConf.LogsDir,
127                 log:     log,
128                 conf:    conf,
129         }
130
131         // Create Events monitoring
132         s.Events = s.NewEventListener()
133
134         return &s
135 }
136
137 // Start Starts syncthing process
138 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
139
140         // Kill existing process (useful for debug ;-) )
141         if os.Getenv("DEBUG_MODE") != "" {
142                 exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
143         }
144
145         path, err := exec.LookPath(path.Join(s.binDir, exeName))
146         if err != nil {
147                 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, s.binDir)
148         }
149         cmd := exec.Command(path, args...)
150         cmd.Env = os.Environ()
151         for _, ev := range env {
152                 cmd.Env = append(cmd.Env, ev)
153         }
154
155         // open log file
156         var outfile *os.File
157         logFilename := filepath.Join(s.logsDir, exeName+".log")
158         if s.logsDir != "" {
159                 outfile, err := os.Create(logFilename)
160                 if err != nil {
161                         return nil, fmt.Errorf("Cannot create log file %s", logFilename)
162                 }
163
164                 cmdOut, err := cmd.StdoutPipe()
165                 if err != nil {
166                         return nil, fmt.Errorf("Pipe stdout error for : %s", err)
167                 }
168
169                 go io.Copy(outfile, cmdOut)
170         }
171
172         err = cmd.Start()
173         if err != nil {
174                 return nil, err
175         }
176
177         *eChan = make(chan ExitChan, 1)
178         go func(c *exec.Cmd, oF *os.File) {
179                 status := 0
180                 sts, err := c.Process.Wait()
181                 if !sts.Success() {
182                         s := sts.Sys().(syscall.WaitStatus)
183                         status = s.ExitStatus()
184                 }
185                 if oF != nil {
186                         oF.Close()
187                 }
188                 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
189
190                 *eChan <- ExitChan{status, err}
191         }(cmd, outfile)
192
193         return cmd, nil
194 }
195
196 // Start Starts syncthing process
197 func (s *SyncThing) Start() (*exec.Cmd, error) {
198         var err error
199
200         s.log.Infof(" ST home=%s", s.Home)
201         s.log.Infof(" ST  url=%s", s.BaseURL)
202
203         args := []string{
204                 "--home=" + s.Home,
205                 "-no-browser",
206                 "--gui-address=" + s.BaseURL,
207         }
208
209         if s.APIKey != "" {
210                 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
211                 s.log.Infof(" ST apikey=%s", s.APIKey)
212         }
213         if s.log.Level == logrus.DebugLevel {
214                 args = append(args, "-verbose")
215         }
216
217         env := []string{
218                 "STNODEFAULTFOLDER=1",
219                 "STNOUPGRADE=1",
220                 "STNORESTART=1", // FIXME SEB remove ?
221         }
222
223         s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
224
225         // Use autogenerated apikey if not set by config.json
226         if err == nil && s.APIKey == "" {
227                 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
228                         defer fd.Close()
229                         if b, err := ioutil.ReadAll(fd); err == nil {
230                                 re := regexp.MustCompile("<apikey>(.*)</apikey>")
231                                 key := re.FindStringSubmatch(string(b))
232                                 if len(key) >= 1 {
233                                         s.APIKey = key[1]
234                                 }
235                         }
236                 }
237         }
238
239         return s.STCmd, err
240 }
241
242 // StartInotify Starts syncthing-inotify process
243 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
244         var err error
245         exeName := "syncthing-inotify"
246
247         s.log.Infof(" STI  url=%s", s.BaseURL)
248
249         args := []string{
250                 "-target=" + s.BaseURL,
251         }
252         if s.APIKey != "" {
253                 args = append(args, "-api="+s.APIKey)
254                 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
255         }
256         if s.log.Level == logrus.DebugLevel {
257                 args = append(args, "-verbosity=4")
258         }
259
260         env := []string{}
261
262         s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
263
264         return s.STICmd, err
265 }
266
267 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
268         if err := proc.Signal(os.Interrupt); err != nil {
269                 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
270
271                 select {
272                 case <-exit:
273                 case <-time.After(time.Second):
274                         // A bigger bonk on the head.
275                         if err := proc.Signal(os.Kill); err != nil {
276                                 s.log.Infof("Proc term %s error: %s", pname, err.Error())
277                         }
278                         <-exit
279                 }
280         }
281         s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
282 }
283
284 // Stop Stops syncthing process
285 func (s *SyncThing) Stop() {
286         if s.STCmd == nil {
287                 return
288         }
289         s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
290         s.STCmd = nil
291 }
292
293 // StopInotify Stops syncthing process
294 func (s *SyncThing) StopInotify() {
295         if s.STICmd == nil {
296                 return
297         }
298         s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
299         s.STICmd = nil
300 }
301
302 // Connect Establish HTTP connection with Syncthing
303 func (s *SyncThing) Connect() error {
304         var err error
305         s.Connected = false
306         s.client, err = common.HTTPNewClient(s.BaseURL,
307                 common.HTTPClientConfig{
308                         URLPrefix:           "/rest",
309                         HeaderClientKeyName: "X-Syncthing-ID",
310                 })
311         if err != nil {
312                 msg := ": " + err.Error()
313                 if strings.Contains(err.Error(), "connection refused") {
314                         msg = fmt.Sprintf("(url: %s)", s.BaseURL)
315                 }
316                 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
317         }
318         if s.client == nil {
319                 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
320         }
321
322         // Redirect HTTP log into a file
323         s.client.SetLogLevel(s.conf.Log.Level.String())
324         s.client.LoggerPrefix = "SYNCTHING: "
325         s.client.LoggerOut = s.conf.LogVerboseOut
326
327         s.MyID, err = s.IDGet()
328         if err != nil {
329                 return fmt.Errorf("ERROR: cannot retrieve ID")
330         }
331
332         s.Connected = true
333
334         // Start events monitoring
335         err = s.Events.Start()
336
337         return err
338 }
339
340 // IDGet returns the Syncthing ID of Syncthing instance running locally
341 func (s *SyncThing) IDGet() (string, error) {
342         var data []byte
343         if err := s.client.HTTPGet("system/status", &data); err != nil {
344                 return "", err
345         }
346         status := make(map[string]interface{})
347         json.Unmarshal(data, &status)
348         return status["myID"].(string), nil
349 }
350
351 // ConfigGet returns the current Syncthing configuration
352 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
353         var data []byte
354         config := config.Configuration{}
355         if err := s.client.HTTPGet("system/config", &data); err != nil {
356                 return config, err
357         }
358         err := json.Unmarshal(data, &config)
359         return config, err
360 }
361
362 // ConfigSet set Syncthing configuration
363 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
364         body, err := json.Marshal(cfg)
365         if err != nil {
366                 return err
367         }
368         return s.client.HTTPPost("system/config", string(body))
369 }
370
371 // IsConfigInSync Returns true if configuration is in sync
372 func (s *SyncThing) IsConfigInSync() (bool, error) {
373         var data []byte
374         var d configInSync
375         if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
376                 return false, err
377         }
378         if err := json.Unmarshal(data, &d); err != nil {
379                 return false, err
380         }
381         return d.ConfigInSync, nil
382 }