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