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