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