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