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