22 "github.com/Sirupsen/logrus"
23 common "github.com/iotbzh/xds-common/golib"
24 "github.com/iotbzh/xds-server/lib/xdsconfig"
25 "github.com/syncthing/syncthing/lib/config"
29 type SyncThing struct {
41 exitSTChan chan ExitChan
42 exitSTIChan chan ExitChan
43 conf *xdsconfig.Config
44 client *common.HTTPClient
49 // ExitChan Channel used for process exit
50 type ExitChan struct {
55 // ConfigInSync Check whether if Syncthing configuration is in sync
56 type configInSync struct {
57 ConfigInSync bool `json:"configInSync"`
60 // FolderStatus Information about the current status of a folder.
61 type FolderStatus struct {
62 GlobalFiles int `json:"globalFiles"`
63 GlobalDirectories int `json:"globalDirectories"`
64 GlobalSymlinks int `json:"globalSymlinks"`
65 GlobalDeleted int `json:"globalDeleted"`
66 GlobalBytes int64 `json:"globalBytes"`
68 LocalFiles int `json:"localFiles"`
69 LocalDirectories int `json:"localDirectories"`
70 LocalSymlinks int `json:"localSymlinks"`
71 LocalDeleted int `json:"localDeleted"`
72 LocalBytes int64 `json:"localBytes"`
74 NeedFiles int `json:"needFiles"`
75 NeedDirectories int `json:"needDirectories"`
76 NeedSymlinks int `json:"needSymlinks"`
77 NeedDeletes int `json:"needDeletes"`
78 NeedBytes int64 `json:"needBytes"`
80 InSyncFiles int `json:"inSyncFiles"`
81 InSyncBytes int64 `json:"inSyncBytes"`
83 State string `json:"state"`
84 StateChanged time.Time `json:"stateChanged"`
86 Sequence int64 `json:"sequence"`
88 IgnorePatterns bool `json:"ignorePatterns"`
91 // NewSyncThing creates a new instance of Syncthing
92 func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
93 var url, apiKey, home, binDir string
95 stCfg := conf.FileConf.SThgConf
97 url = stCfg.GuiAddress
98 apiKey = stCfg.GuiAPIKey
100 binDir = stCfg.BinDir
104 url = "http://localhost:8385"
106 if url[0:7] != "http://" {
107 url = "http://" + url
111 panic("home parameter must be set")
119 logsDir: conf.FileConf.LogsDir,
124 // Create Events monitoring
125 s.Events = s.NewEventListener()
130 // Start Starts syncthing process
131 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
135 // Kill existing process (useful for debug ;-) )
136 if os.Getenv("DEBUG_MODE") != "" {
137 exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
140 // When not set (or set to '.') set bin to path of xds-agent executable
142 if bdir == "" || bdir == "." {
143 exe, _ := os.Executable()
144 if exeAbsPath, err := filepath.Abs(exe); err == nil {
145 if exePath, err := filepath.EvalSymlinks(exeAbsPath); err == nil {
146 bdir = filepath.Dir(exePath)
151 exePath, err = exec.LookPath(path.Join(bdir, exeName))
153 // Let's try in /opt/AGL/bin
154 exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
156 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
159 cmd := exec.Command(exePath, args...)
160 cmd.Env = os.Environ()
161 for _, ev := range env {
162 cmd.Env = append(cmd.Env, ev)
167 logFilename := filepath.Join(s.logsDir, exeName+".log")
169 outfile, err := os.Create(logFilename)
171 return nil, fmt.Errorf("Cannot create log file %s", logFilename)
174 cmdOut, err := cmd.StdoutPipe()
176 return nil, fmt.Errorf("Pipe stdout error for : %s", err)
179 go io.Copy(outfile, cmdOut)
187 *eChan = make(chan ExitChan, 1)
188 go func(c *exec.Cmd, oF *os.File) {
190 sts, err := c.Process.Wait()
192 s := sts.Sys().(syscall.WaitStatus)
193 status = s.ExitStatus()
198 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
200 *eChan <- ExitChan{status, err}
206 // Start Starts syncthing process
207 func (s *SyncThing) Start() (*exec.Cmd, error) {
210 s.log.Infof(" ST home=%s", s.Home)
211 s.log.Infof(" ST url=%s", s.BaseURL)
216 "--gui-address=" + s.BaseURL,
220 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
221 s.log.Infof(" ST apikey=%s", s.APIKey)
223 if s.log.Level == logrus.DebugLevel {
224 args = append(args, "-verbose")
228 "STNODEFAULTFOLDER=1",
232 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
234 // Use autogenerated apikey if not set by config.json
235 if err == nil && s.APIKey == "" {
236 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
238 if b, err := ioutil.ReadAll(fd); err == nil {
239 re := regexp.MustCompile("<apikey>(.*)</apikey>")
240 key := re.FindStringSubmatch(string(b))
251 // StartInotify Starts syncthing-inotify process
252 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
254 exeName := "syncthing-inotify"
256 s.log.Infof(" STI url=%s", s.BaseURL)
259 "-target=" + s.BaseURL,
262 args = append(args, "-api="+s.APIKey)
263 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
265 if s.log.Level == logrus.DebugLevel {
266 args = append(args, "-verbosity=4")
271 s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
276 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
277 if err := proc.Signal(os.Interrupt); err != nil {
278 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
282 case <-time.After(time.Second):
283 // A bigger bonk on the head.
284 if err := proc.Signal(os.Kill); err != nil {
285 s.log.Infof("Proc term %s error: %s", pname, err.Error())
290 s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
293 // Stop Stops syncthing process
294 func (s *SyncThing) Stop() {
298 s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
302 // StopInotify Stops syncthing process
303 func (s *SyncThing) StopInotify() {
307 s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
311 // Connect Establish HTTP connection with Syncthing
312 func (s *SyncThing) Connect() error {
315 s.client, err = common.HTTPNewClient(s.BaseURL,
316 common.HTTPClientConfig{
318 HeaderClientKeyName: "X-Syncthing-ID",
319 LogOut: s.conf.LogVerboseOut,
320 LogPrefix: "SYNCTHING: ",
321 LogLevel: common.HTTPLogLevelWarning,
323 s.client.SetLogLevel(s.log.Level.String())
326 msg := ": " + err.Error()
327 if strings.Contains(err.Error(), "connection refused") {
328 msg = fmt.Sprintf("(url: %s)", s.BaseURL)
330 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
333 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
336 s.MyID, err = s.IDGet()
338 return fmt.Errorf("ERROR: cannot retrieve ID")
343 // Start events monitoring
344 err = s.Events.Start()
349 // IDGet returns the Syncthing ID of Syncthing instance running locally
350 func (s *SyncThing) IDGet() (string, error) {
352 if err := s.client.HTTPGet("system/status", &data); err != nil {
355 status := make(map[string]interface{})
356 json.Unmarshal(data, &status)
357 return status["myID"].(string), nil
360 // ConfigGet returns the current Syncthing configuration
361 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
363 config := config.Configuration{}
364 if err := s.client.HTTPGet("system/config", &data); err != nil {
367 err := json.Unmarshal(data, &config)
371 // ConfigSet set Syncthing configuration
372 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
373 body, err := json.Marshal(cfg)
377 return s.client.HTTPPost("system/config", string(body))
380 // IsConfigInSync Returns true if configuration is in sync
381 func (s *SyncThing) IsConfigInSync() (bool, error) {
384 if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
387 if err := json.Unmarshal(data, &d); err != nil {
390 return d.ConfigInSync, nil