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"
26 type SyncThing struct {
36 exitSTChan chan ExitChan
37 exitSTIChan chan ExitChan
38 client *common.HTTPClient
42 // ExitChan Channel used for process exit
43 type ExitChan struct {
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
52 stCfg := conf.FileConf.SThgConf
54 url = stCfg.GuiAddress
55 apiKey = stCfg.GuiAPIKey
61 url = "http://localhost:8386"
63 if url[0:7] != "http://" {
68 panic("home parameter must be set")
76 logsDir: conf.FileConf.LogsDir,
83 // Start Starts syncthing process
84 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
88 // Kill existing process (useful for debug ;-) )
89 if os.Getenv("DEBUG_MODE") != "" {
90 exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
93 // When not set (or set to '.') set bin to path of xds-agent executable
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)
104 exePath, err = exec.LookPath(path.Join(bdir, exeName))
106 // Let's try in /opt/AGL/bin
107 exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
109 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
112 cmd := exec.Command(exePath, args...)
113 cmd.Env = os.Environ()
114 for _, ev := range env {
115 cmd.Env = append(cmd.Env, ev)
120 logFilename := filepath.Join(s.logsDir, exeName+".log")
122 outfile, err := os.Create(logFilename)
124 return nil, fmt.Errorf("Cannot create log file %s", logFilename)
127 cmdOut, err := cmd.StdoutPipe()
129 return nil, fmt.Errorf("Pipe stdout error for : %s", err)
132 go io.Copy(outfile, cmdOut)
140 *eChan = make(chan ExitChan, 1)
141 go func(c *exec.Cmd, oF *os.File) {
143 sts, err := c.Process.Wait()
145 s := sts.Sys().(syscall.WaitStatus)
146 status = s.ExitStatus()
151 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
153 *eChan <- ExitChan{status, err}
159 // Start Starts syncthing process
160 func (s *SyncThing) Start() (*exec.Cmd, error) {
163 s.log.Infof(" ST home=%s", s.Home)
164 s.log.Infof(" ST url=%s", s.BaseURL)
169 "--gui-address=" + s.BaseURL,
173 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
174 s.log.Infof(" ST apikey=%s", s.APIKey)
176 if s.log.Level == logrus.DebugLevel {
177 args = append(args, "-verbose")
181 "STNODEFAULTFOLDER=1",
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)
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) {
200 return nil, fmt.Errorf("Cannot start Syncthing for config file creation")
203 read, err := ioutil.ReadFile(stConfigFile)
205 return nil, fmt.Errorf("Cannot read Syncthing config file for apikey setting")
207 re := regexp.MustCompile(`<apikey>.*</apikey>`)
208 newContents := re.ReplaceAllString(string(read), "<apikey>"+s.APIKey+"</apikey>")
209 err = ioutil.WriteFile(stConfigFile, []byte(newContents), 0)
211 return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
215 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
217 // Use autogenerated apikey if not set by config.json
219 if fd, err := os.Open(stConfigFile); err == nil {
221 if b, err := ioutil.ReadAll(fd); err == nil {
222 re := regexp.MustCompile("<apikey>(.*)</apikey>")
223 key := re.FindStringSubmatch(string(b))
234 // StartInotify Starts syncthing-inotify process
235 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
237 exeName := "syncthing-inotify"
239 s.log.Infof(" STI url=%s", s.BaseURL)
242 "-target=" + s.BaseURL,
245 args = append(args, "-api="+s.APIKey)
246 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
248 if s.log.Level == logrus.DebugLevel {
249 args = append(args, "-verbosity=4")
254 s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
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())
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())
273 s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
276 // Stop Stops syncthing process
277 func (s *SyncThing) Stop() {
281 s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
285 // StopInotify Stops syncthing process
286 func (s *SyncThing) StopInotify() {
290 s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
294 // Connect Establish HTTP connection with Syncthing
295 func (s *SyncThing) Connect() error {
297 s.client, err = common.HTTPNewClient(s.BaseURL,
298 common.HTTPClientConfig{
300 HeaderClientKeyName: "X-Syncthing-ID",
303 msg := ": " + err.Error()
304 if strings.Contains(err.Error(), "connection refused") {
305 msg = fmt.Sprintf("(url: %s)", s.BaseURL)
307 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
310 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
313 s.client.SetLogLevel(s.log.Level.String())
314 s.client.LoggerPrefix = "SYNCTHING: "
315 s.client.LoggerOut = s.log.Out
320 // IDGet returns the Syncthing ID of Syncthing instance running locally
321 func (s *SyncThing) IDGet() (string, error) {
323 if err := s.client.HTTPGet("system/status", &data); err != nil {
326 status := make(map[string]interface{})
327 json.Unmarshal(data, &status)
328 return status["myID"].(string), nil
331 // ConfigGet returns the current Syncthing configuration
332 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
334 config := config.Configuration{}
335 if err := s.client.HTTPGet("system/config", &data); err != nil {
338 err := json.Unmarshal(data, &config)
342 // ConfigSet set Syncthing configuration
343 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
344 body, err := json.Marshal(cfg)
348 return s.client.HTTPPost("system/config", string(body))