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