Fixed syncthing binDir when set to '.'
[src/xds/xds-agent.git] / lib / syncthing / st.go
1 package st
2
3 import (
4         "encoding/json"
5         "io"
6         "io/ioutil"
7         "os"
8         "path"
9         "path/filepath"
10         "regexp"
11         "strings"
12         "syscall"
13         "time"
14
15         "fmt"
16
17         "os/exec"
18
19         "github.com/Sirupsen/logrus"
20         "github.com/iotbzh/xds-agent/lib/xdsconfig"
21         common "github.com/iotbzh/xds-common/golib"
22         "github.com/syncthing/syncthing/lib/config"
23 )
24
25 // SyncThing .
26 type SyncThing struct {
27         BaseURL string
28         APIKey  string
29         Home    string
30         STCmd   *exec.Cmd
31         STICmd  *exec.Cmd
32
33         // Private fields
34         binDir      string
35         logsDir     string
36         exitSTChan  chan ExitChan
37         exitSTIChan chan ExitChan
38         client      *common.HTTPClient
39         log         *logrus.Logger
40 }
41
42 // ExitChan Channel used for process exit
43 type ExitChan struct {
44         status int
45         err    error
46 }
47
48 // NewSyncThing creates a new instance of Syncthing
49 func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
50         var url, apiKey, home, binDir string
51         var err error
52
53         stCfg := conf.FileConf.SThgConf
54         if stCfg != nil {
55                 url = stCfg.GuiAddress
56                 apiKey = stCfg.GuiAPIKey
57                 home = stCfg.Home
58                 binDir = stCfg.BinDir
59         }
60
61         if url == "" {
62                 url = "http://localhost:8384"
63         }
64         if url[0:7] != "http://" {
65                 url = "http://" + url
66         }
67
68         if home == "" {
69                 home = "/mnt/share"
70         }
71
72         if binDir == "" {
73                 if binDir, err = filepath.Abs(filepath.Dir(os.Args[0])); err != nil {
74                         binDir = "/usr/local/bin"
75                 }
76         }
77
78         s := SyncThing{
79                 BaseURL: url,
80                 APIKey:  apiKey,
81                 Home:    home,
82                 binDir:  binDir,
83                 logsDir: conf.FileConf.LogsDir,
84                 log:     log,
85         }
86
87         return &s
88 }
89
90 // Start Starts syncthing process
91 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
92
93         // Kill existing process (useful for debug ;-) )
94         if os.Getenv("DEBUG_MODE") != "" {
95                 exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
96         }
97
98         // When not set (or set to '.') set bin to path of xds-agent executable
99         bdir := s.binDir
100         if bdir == "" || bdir == "." {
101                 if dir, err := filepath.Abs(filepath.Dir(os.Args[0])); err == nil {
102                         bdir = dir
103                 }
104         }
105
106         path, err := exec.LookPath(path.Join(bdir, exeName))
107         if err != nil {
108                 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
109         }
110         cmd := exec.Command(path, args...)
111         cmd.Env = os.Environ()
112         for _, ev := range env {
113                 cmd.Env = append(cmd.Env, ev)
114         }
115
116         // open log file
117         var outfile *os.File
118         logFilename := filepath.Join(s.logsDir, exeName+".log")
119         if s.logsDir != "" {
120                 outfile, err := os.Create(logFilename)
121                 if err != nil {
122                         return nil, fmt.Errorf("Cannot create log file %s", logFilename)
123                 }
124
125                 cmdOut, err := cmd.StdoutPipe()
126                 if err != nil {
127                         return nil, fmt.Errorf("Pipe stdout error for : %s", err)
128                 }
129
130                 go io.Copy(outfile, cmdOut)
131         }
132
133         err = cmd.Start()
134         if err != nil {
135                 return nil, err
136         }
137
138         *eChan = make(chan ExitChan, 1)
139         go func(c *exec.Cmd, oF *os.File) {
140                 status := 0
141                 sts, err := c.Process.Wait()
142                 if !sts.Success() {
143                         s := sts.Sys().(syscall.WaitStatus)
144                         status = s.ExitStatus()
145                 }
146                 if oF != nil {
147                         oF.Close()
148                 }
149                 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
150
151                 *eChan <- ExitChan{status, err}
152         }(cmd, outfile)
153
154         return cmd, nil
155 }
156
157 // Start Starts syncthing process
158 func (s *SyncThing) Start() (*exec.Cmd, error) {
159         var err error
160
161         s.log.Infof(" ST home=%s", s.Home)
162         s.log.Infof(" ST  url=%s", s.BaseURL)
163
164         args := []string{
165                 "--home=" + s.Home,
166                 "-no-browser",
167                 "--gui-address=" + s.BaseURL,
168         }
169
170         if s.APIKey != "" {
171                 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
172                 s.log.Infof(" ST apikey=%s", s.APIKey)
173         }
174         if s.log.Level == logrus.DebugLevel {
175                 args = append(args, "-verbose")
176         }
177
178         env := []string{
179                 "STNODEFAULTFOLDER=1",
180                 "STNOUPGRADE=1",
181         }
182
183         // XXX - temporary hack because -gui-apikey seems to correctly handle by
184         // syncthing the early first time
185         stConfigFile := filepath.Join(s.Home, "config.xml")
186         if s.APIKey != "" && !common.Exists(stConfigFile) {
187                 s.log.Infof("Stop and restart Syncthing (hack for apikey setting)")
188                 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
189                 tmo := 20
190                 for ; tmo > 0; tmo-- {
191                         s.log.Debugf("Waiting Syncthing config.xml creation (%v)\n", tmo)
192                         time.Sleep(500 * time.Millisecond)
193                         if common.Exists(stConfigFile) {
194                                 break
195                         }
196                 }
197                 if tmo <= 0 {
198                         return nil, fmt.Errorf("Cannot start Syncthing for config file creation")
199                 }
200                 s.Stop()
201                 read, err := ioutil.ReadFile(stConfigFile)
202                 if err != nil {
203                         return nil, fmt.Errorf("Cannot read Syncthing config file for apikey setting")
204                 }
205                 re := regexp.MustCompile(`<apikey>.*</apikey>`)
206                 newContents := re.ReplaceAllString(string(read), "<apikey>"+s.APIKey+"</apikey>")
207                 err = ioutil.WriteFile(stConfigFile, []byte(newContents), 0)
208                 if err != nil {
209                         return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
210                 }
211         }
212
213         s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
214
215         // Use autogenerated apikey if not set by config.json
216         if s.APIKey == "" {
217                 if fd, err := os.Open(stConfigFile); err == nil {
218                         defer fd.Close()
219                         if b, err := ioutil.ReadAll(fd); err == nil {
220                                 re := regexp.MustCompile("<apikey>(.*)</apikey>")
221                                 key := re.FindStringSubmatch(string(b))
222                                 if len(key) >= 1 {
223                                         s.APIKey = key[1]
224                                 }
225                         }
226                 }
227         }
228
229         return s.STCmd, err
230 }
231
232 // StartInotify Starts syncthing-inotify process
233 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
234         var err error
235         exeName := "syncthing-inotify"
236
237         s.log.Infof(" STI  url=%s", s.BaseURL)
238
239         args := []string{
240                 "-target=" + s.BaseURL,
241         }
242         if s.APIKey != "" {
243                 args = append(args, "-api="+s.APIKey)
244                 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
245         }
246         if s.log.Level == logrus.DebugLevel {
247                 args = append(args, "-verbosity=4")
248         }
249
250         env := []string{}
251
252         s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
253
254         return s.STICmd, err
255 }
256
257 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
258         if err := proc.Signal(os.Interrupt); err != nil {
259                 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
260
261                 select {
262                 case <-exit:
263                 case <-time.After(time.Second):
264                         // A bigger bonk on the head.
265                         if err := proc.Signal(os.Kill); err != nil {
266                                 s.log.Infof("Proc term %s error: %s", pname, err.Error())
267                         }
268                         <-exit
269                 }
270         }
271         s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
272 }
273
274 // Stop Stops syncthing process
275 func (s *SyncThing) Stop() {
276         if s.STCmd == nil {
277                 return
278         }
279         s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
280         s.STCmd = nil
281 }
282
283 // StopInotify Stops syncthing process
284 func (s *SyncThing) StopInotify() {
285         if s.STICmd == nil {
286                 return
287         }
288         s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
289         s.STICmd = nil
290 }
291
292 // Connect Establish HTTP connection with Syncthing
293 func (s *SyncThing) Connect() error {
294         var err error
295         s.client, err = common.HTTPNewClient(s.BaseURL,
296                 common.HTTPClientConfig{
297                         URLPrefix:           "/rest",
298                         HeaderClientKeyName: "X-Syncthing-ID",
299                 })
300         if err != nil {
301                 msg := ": " + err.Error()
302                 if strings.Contains(err.Error(), "connection refused") {
303                         msg = fmt.Sprintf("(url: %s)", s.BaseURL)
304                 }
305                 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
306         }
307         if s.client == nil {
308                 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
309         }
310
311         s.client.SetLogLevel(s.log.Level.String())
312         s.client.LoggerPrefix = "SYNCTHING: "
313         s.client.LoggerOut = s.log.Out
314
315         return nil
316 }
317
318 // IDGet returns the Syncthing ID of Syncthing instance running locally
319 func (s *SyncThing) IDGet() (string, error) {
320         var data []byte
321         if err := s.client.HTTPGet("system/status", &data); err != nil {
322                 return "", err
323         }
324         status := make(map[string]interface{})
325         json.Unmarshal(data, &status)
326         return status["myID"].(string), nil
327 }
328
329 // ConfigGet returns the current Syncthing configuration
330 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
331         var data []byte
332         config := config.Configuration{}
333         if err := s.client.HTTPGet("system/config", &data); err != nil {
334                 return config, err
335         }
336         err := json.Unmarshal(data, &config)
337         return config, err
338 }
339
340 // ConfigSet set Syncthing configuration
341 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
342         body, err := json.Marshal(cfg)
343         if err != nil {
344                 return err
345         }
346         return s.client.HTTPPost("system/config", string(body))
347 }