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