Update default syncthing port to 8386
[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
52         stCfg := conf.FileConf.SThgConf
53         if stCfg != nil {
54                 url = stCfg.GuiAddress
55                 apiKey = stCfg.GuiAPIKey
56                 home = stCfg.Home
57                 binDir = stCfg.BinDir
58         }
59
60         if url == "" {
61                 url = "http://localhost:8386"
62         }
63         if url[0:7] != "http://" {
64                 url = "http://" + url
65         }
66
67         if home == "" {
68                 panic("home parameter must be set")
69         }
70
71         s := SyncThing{
72                 BaseURL: url,
73                 APIKey:  apiKey,
74                 Home:    home,
75                 binDir:  binDir,
76                 logsDir: conf.FileConf.LogsDir,
77                 log:     log,
78         }
79
80         return &s
81 }
82
83 // Start Starts syncthing process
84 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
85         var err error
86         var exePath string
87
88         // Kill existing process (useful for debug ;-) )
89         if os.Getenv("DEBUG_MODE") != "" {
90                 exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
91         }
92
93         // When not set (or set to '.') set bin to path of xds-agent executable
94         bdir := s.binDir
95         if bdir == "" || bdir == "." {
96                 exe, _ := os.Executable()
97                 if exeAbsPath, err := filepath.Abs(exe); err == nil {
98                         if exePath, err := filepath.EvalSymlinks(exeAbsPath); err == nil {
99                                 bdir = filepath.Dir(exePath)
100                         }
101                 }
102         }
103
104         exePath, err = exec.LookPath(path.Join(bdir, exeName))
105         if err != nil {
106                 // Let's try in /opt/AGL/bin
107                 exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
108                 if err != nil {
109                         return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
110                 }
111         }
112         cmd := exec.Command(exePath, args...)
113         cmd.Env = os.Environ()
114         for _, ev := range env {
115                 cmd.Env = append(cmd.Env, ev)
116         }
117
118         // open log file
119         var outfile *os.File
120         logFilename := filepath.Join(s.logsDir, exeName+".log")
121         if s.logsDir != "" {
122                 outfile, err := os.Create(logFilename)
123                 if err != nil {
124                         return nil, fmt.Errorf("Cannot create log file %s", logFilename)
125                 }
126
127                 cmdOut, err := cmd.StdoutPipe()
128                 if err != nil {
129                         return nil, fmt.Errorf("Pipe stdout error for : %s", err)
130                 }
131
132                 go io.Copy(outfile, cmdOut)
133         }
134
135         err = cmd.Start()
136         if err != nil {
137                 return nil, err
138         }
139
140         *eChan = make(chan ExitChan, 1)
141         go func(c *exec.Cmd, oF *os.File) {
142                 status := 0
143                 sts, err := c.Process.Wait()
144                 if !sts.Success() {
145                         s := sts.Sys().(syscall.WaitStatus)
146                         status = s.ExitStatus()
147                 }
148                 if oF != nil {
149                         oF.Close()
150                 }
151                 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
152
153                 *eChan <- ExitChan{status, err}
154         }(cmd, outfile)
155
156         return cmd, nil
157 }
158
159 // Start Starts syncthing process
160 func (s *SyncThing) Start() (*exec.Cmd, error) {
161         var err error
162
163         s.log.Infof(" ST home=%s", s.Home)
164         s.log.Infof(" ST  url=%s", s.BaseURL)
165
166         args := []string{
167                 "--home=" + s.Home,
168                 "-no-browser",
169                 "--gui-address=" + s.BaseURL,
170         }
171
172         if s.APIKey != "" {
173                 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
174                 s.log.Infof(" ST apikey=%s", s.APIKey)
175         }
176         if s.log.Level == logrus.DebugLevel {
177                 args = append(args, "-verbose")
178         }
179
180         env := []string{
181                 "STNODEFAULTFOLDER=1",
182                 "STNOUPGRADE=1",
183         }
184
185         // XXX - temporary hack because -gui-apikey seems to correctly handle by
186         // syncthing the early first time
187         stConfigFile := filepath.Join(s.Home, "config.xml")
188         if s.APIKey != "" && !common.Exists(stConfigFile) {
189                 s.log.Infof("Stop and restart Syncthing (hack for apikey setting)")
190                 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
191                 tmo := 20
192                 for ; tmo > 0; tmo-- {
193                         s.log.Debugf("Waiting Syncthing config.xml creation (%v)\n", tmo)
194                         time.Sleep(500 * time.Millisecond)
195                         if common.Exists(stConfigFile) {
196                                 break
197                         }
198                 }
199                 if tmo <= 0 {
200                         return nil, fmt.Errorf("Cannot start Syncthing for config file creation")
201                 }
202                 s.Stop()
203                 read, err := ioutil.ReadFile(stConfigFile)
204                 if err != nil {
205                         return nil, fmt.Errorf("Cannot read Syncthing config file for apikey setting")
206                 }
207                 re := regexp.MustCompile(`<apikey>.*</apikey>`)
208                 newContents := re.ReplaceAllString(string(read), "<apikey>"+s.APIKey+"</apikey>")
209                 err = ioutil.WriteFile(stConfigFile, []byte(newContents), 0)
210                 if err != nil {
211                         return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
212                 }
213         }
214
215         s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
216
217         // Use autogenerated apikey if not set by config.json
218         if s.APIKey == "" {
219                 if fd, err := os.Open(stConfigFile); err == nil {
220                         defer fd.Close()
221                         if b, err := ioutil.ReadAll(fd); err == nil {
222                                 re := regexp.MustCompile("<apikey>(.*)</apikey>")
223                                 key := re.FindStringSubmatch(string(b))
224                                 if len(key) >= 1 {
225                                         s.APIKey = key[1]
226                                 }
227                         }
228                 }
229         }
230
231         return s.STCmd, err
232 }
233
234 // StartInotify Starts syncthing-inotify process
235 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
236         var err error
237         exeName := "syncthing-inotify"
238
239         s.log.Infof(" STI  url=%s", s.BaseURL)
240
241         args := []string{
242                 "-target=" + s.BaseURL,
243         }
244         if s.APIKey != "" {
245                 args = append(args, "-api="+s.APIKey)
246                 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
247         }
248         if s.log.Level == logrus.DebugLevel {
249                 args = append(args, "-verbosity=4")
250         }
251
252         env := []string{}
253
254         s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
255
256         return s.STICmd, err
257 }
258
259 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
260         if err := proc.Signal(os.Interrupt); err != nil {
261                 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
262
263                 select {
264                 case <-exit:
265                 case <-time.After(time.Second):
266                         // A bigger bonk on the head.
267                         if err := proc.Signal(os.Kill); err != nil {
268                                 s.log.Infof("Proc term %s error: %s", pname, err.Error())
269                         }
270                         <-exit
271                 }
272         }
273         s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
274 }
275
276 // Stop Stops syncthing process
277 func (s *SyncThing) Stop() {
278         if s.STCmd == nil {
279                 return
280         }
281         s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
282         s.STCmd = nil
283 }
284
285 // StopInotify Stops syncthing process
286 func (s *SyncThing) StopInotify() {
287         if s.STICmd == nil {
288                 return
289         }
290         s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
291         s.STICmd = nil
292 }
293
294 // Connect Establish HTTP connection with Syncthing
295 func (s *SyncThing) Connect() error {
296         var err error
297         s.client, err = common.HTTPNewClient(s.BaseURL,
298                 common.HTTPClientConfig{
299                         URLPrefix:           "/rest",
300                         HeaderClientKeyName: "X-Syncthing-ID",
301                 })
302         if err != nil {
303                 msg := ": " + err.Error()
304                 if strings.Contains(err.Error(), "connection refused") {
305                         msg = fmt.Sprintf("(url: %s)", s.BaseURL)
306                 }
307                 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
308         }
309         if s.client == nil {
310                 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
311         }
312
313         s.client.SetLogLevel(s.log.Level.String())
314         s.client.LoggerPrefix = "SYNCTHING: "
315         s.client.LoggerOut = s.log.Out
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 }