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 {
39 exitSTChan chan ExitChan
40 exitSTIChan chan ExitChan
41 client *common.HTTPClient
43 conf *xdsconfig.Config
46 // ExitChan Channel used for process exit
47 type ExitChan struct {
52 // ConfigInSync Check whether if Syncthing configuration is in sync
53 type configInSync struct {
54 ConfigInSync bool `json:"configInSync"`
57 // FolderStatus Information about the current status of a folder.
58 type FolderStatus struct {
59 GlobalFiles int `json:"globalFiles"`
60 GlobalDirectories int `json:"globalDirectories"`
61 GlobalSymlinks int `json:"globalSymlinks"`
62 GlobalDeleted int `json:"globalDeleted"`
63 GlobalBytes int64 `json:"globalBytes"`
65 LocalFiles int `json:"localFiles"`
66 LocalDirectories int `json:"localDirectories"`
67 LocalSymlinks int `json:"localSymlinks"`
68 LocalDeleted int `json:"localDeleted"`
69 LocalBytes int64 `json:"localBytes"`
71 NeedFiles int `json:"needFiles"`
72 NeedDirectories int `json:"needDirectories"`
73 NeedSymlinks int `json:"needSymlinks"`
74 NeedDeletes int `json:"needDeletes"`
75 NeedBytes int64 `json:"needBytes"`
77 InSyncFiles int `json:"inSyncFiles"`
78 InSyncBytes int64 `json:"inSyncBytes"`
80 State string `json:"state"`
81 StateChanged time.Time `json:"stateChanged"`
83 Sequence int64 `json:"sequence"`
85 IgnorePatterns bool `json:"ignorePatterns"`
88 // NewSyncThing creates a new instance of Syncthing
89 func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
90 var url, apiKey, home, binDir string
92 stCfg := conf.FileConf.SThgConf
94 url = stCfg.GuiAddress
95 apiKey = stCfg.GuiAPIKey
101 url = "http://localhost:8384"
103 if url[0:7] != "http://" {
104 url = "http://" + url
108 panic("home parameter must be set")
116 logsDir: conf.FileConf.LogsDir,
121 // Create Events monitoring
122 // SEB TO TEST s.Events = s.NewEventListener()
127 // Start Starts syncthing process
128 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
132 // Kill existing process (useful for debug ;-) )
133 if os.Getenv("DEBUG_MODE") != "" {
134 exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
137 // When not set (or set to '.') set bin to path of xds-agent executable
139 if bdir == "" || bdir == "." {
140 exe, _ := os.Executable()
141 if exeAbsPath, err := filepath.Abs(exe); err == nil {
142 if exePath, err := filepath.EvalSymlinks(exeAbsPath); err == nil {
143 bdir = filepath.Dir(exePath)
148 exePath, err = exec.LookPath(path.Join(bdir, exeName))
150 // Let's try in /opt/AGL/bin
151 exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
153 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
156 cmd := exec.Command(exePath, args...)
157 cmd.Env = os.Environ()
158 for _, ev := range env {
159 cmd.Env = append(cmd.Env, ev)
164 logFilename := filepath.Join(s.logsDir, exeName+".log")
166 outfile, err := os.Create(logFilename)
168 return nil, fmt.Errorf("Cannot create log file %s", logFilename)
171 cmdOut, err := cmd.StdoutPipe()
173 return nil, fmt.Errorf("Pipe stdout error for : %s", err)
176 go io.Copy(outfile, cmdOut)
184 *eChan = make(chan ExitChan, 1)
185 go func(c *exec.Cmd, oF *os.File) {
187 sts, err := c.Process.Wait()
189 s := sts.Sys().(syscall.WaitStatus)
190 status = s.ExitStatus()
195 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
197 *eChan <- ExitChan{status, err}
203 // Start Starts syncthing process
204 func (s *SyncThing) Start() (*exec.Cmd, error) {
207 s.log.Infof(" ST home=%s", s.Home)
208 s.log.Infof(" ST url=%s", s.BaseURL)
213 "--gui-address=" + s.BaseURL,
217 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
218 s.log.Infof(" ST apikey=%s", s.APIKey)
220 if s.log.Level == logrus.DebugLevel {
221 args = append(args, "-verbose")
225 "STNODEFAULTFOLDER=1",
229 /* SEB STILL NEEDED, if not SUP code
231 // XXX - temporary hack because -gui-apikey seems to correctly handle by
232 // syncthing the early first time
233 stConfigFile := filepath.Join(s.Home, "config.xml")
234 if s.APIKey != "" && !common.Exists(stConfigFile) {
235 s.log.Infof("Stop and restart Syncthing (hack for apikey setting)")
236 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
238 for ; tmo > 0; tmo-- {
239 s.log.Debugf("Waiting Syncthing config.xml creation (%v)\n", tmo)
240 time.Sleep(500 * time.Millisecond)
241 if common.Exists(stConfigFile) {
246 return nil, fmt.Errorf("Cannot start Syncthing for config file creation")
249 read, err := ioutil.ReadFile(stConfigFile)
251 return nil, fmt.Errorf("Cannot read Syncthing config file for apikey setting")
253 re := regexp.MustCompile(`<apikey>.*</apikey>`)
254 newContents := re.ReplaceAllString(string(read), "<apikey>"+s.APIKey+"</apikey>")
255 err = ioutil.WriteFile(stConfigFile, []byte(newContents), 0)
257 return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
261 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
263 // Use autogenerated apikey if not set by config.json
264 if err == nil && s.APIKey == "" {
265 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
267 if b, err := ioutil.ReadAll(fd); err == nil {
268 re := regexp.MustCompile("<apikey>(.*)</apikey>")
269 key := re.FindStringSubmatch(string(b))
280 // StartInotify Starts syncthing-inotify process
281 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
283 exeName := "syncthing-inotify"
285 s.log.Infof(" STI url=%s", s.BaseURL)
288 "-target=" + s.BaseURL,
291 args = append(args, "-api="+s.APIKey)
292 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
294 if s.log.Level == logrus.DebugLevel {
295 args = append(args, "-verbosity=4")
300 s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
305 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
306 if err := proc.Signal(os.Interrupt); err != nil {
307 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
311 case <-time.After(time.Second):
312 // A bigger bonk on the head.
313 if err := proc.Signal(os.Kill); err != nil {
314 s.log.Infof("Proc term %s error: %s", pname, err.Error())
319 s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
322 // Stop Stops syncthing process
323 func (s *SyncThing) Stop() {
327 s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
331 // StopInotify Stops syncthing process
332 func (s *SyncThing) StopInotify() {
336 s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
340 // Connect Establish HTTP connection with Syncthing
341 func (s *SyncThing) Connect() error {
344 s.client, err = common.HTTPNewClient(s.BaseURL,
345 common.HTTPClientConfig{
347 HeaderClientKeyName: "X-Syncthing-ID",
348 LogOut: s.conf.LogVerboseOut,
349 LogPrefix: "SYNCTHING: ",
350 LogLevel: common.HTTPLogLevelWarning,
352 s.client.SetLogLevel(s.log.Level.String())
355 msg := ": " + err.Error()
356 if strings.Contains(err.Error(), "connection refused") {
357 msg = fmt.Sprintf("(url: %s)", s.BaseURL)
359 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
362 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
365 s.MyID, err = s.IDGet()
367 return fmt.Errorf("ERROR: cannot retrieve ID")
372 // Start events monitoring
373 //SEB TODO err = s.Events.Start()
378 // IDGet returns the Syncthing ID of Syncthing instance running locally
379 func (s *SyncThing) IDGet() (string, error) {
381 if err := s.client.HTTPGet("system/status", &data); err != nil {
384 status := make(map[string]interface{})
385 json.Unmarshal(data, &status)
386 return status["myID"].(string), nil
389 // ConfigGet returns the current Syncthing configuration
390 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
392 config := config.Configuration{}
393 if err := s.client.HTTPGet("system/config", &data); err != nil {
396 err := json.Unmarshal(data, &config)
400 // ConfigSet set Syncthing configuration
401 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
402 body, err := json.Marshal(cfg)
406 return s.client.HTTPPost("system/config", string(body))
409 // IsConfigInSync Returns true if configuration is in sync
410 func (s *SyncThing) IsConfigInSync() (bool, error) {
413 if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
416 if err := json.Unmarshal(data, &d); err != nil {
419 return d.ConfigInSync, nil