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 {
42 exitSTChan chan ExitChan
43 exitSTIChan chan ExitChan
44 client *common.HTTPClient
46 conf *xdsconfig.Config
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 fmt.Printf("\n!!! DEBUG_MODE set: KILL existing %s process(es) !!!\n", exeName)
138 exec.Command("bash", "-c", "ps -ax |grep "+exeName+" |grep "+s.BaseURL+" |cut -d' ' -f 1|xargs -I{} kill -9 {}").Output()
141 // When not set (or set to '.') set bin to path of xds-agent executable
143 if bdir == "" || bdir == "." {
144 exe, _ := os.Executable()
145 if exeAbsPath, err := filepath.Abs(exe); err == nil {
146 if exePath, err := filepath.EvalSymlinks(exeAbsPath); err == nil {
147 bdir = filepath.Dir(exePath)
152 exePath, err = exec.LookPath(path.Join(bdir, exeName))
154 // Let's try in /opt/AGL/bin
155 exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
157 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
160 cmd := exec.Command(exePath, args...)
161 cmd.Env = os.Environ()
162 for _, ev := range env {
163 cmd.Env = append(cmd.Env, ev)
168 logFilename := filepath.Join(s.logsDir, exeName+".log")
170 outfile, err := os.Create(logFilename)
172 return nil, fmt.Errorf("Cannot create log file %s", logFilename)
175 cmdOut, err := cmd.StdoutPipe()
177 return nil, fmt.Errorf("Pipe stdout error for : %s", err)
180 go io.Copy(outfile, cmdOut)
188 *eChan = make(chan ExitChan, 1)
189 go func(c *exec.Cmd, oF *os.File) {
191 sts, err := c.Process.Wait()
193 s := sts.Sys().(syscall.WaitStatus)
194 status = s.ExitStatus()
199 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
201 *eChan <- ExitChan{status, err}
207 // Start Starts syncthing process
208 func (s *SyncThing) Start() (*exec.Cmd, error) {
211 s.log.Infof(" ST home=%s", s.Home)
212 s.log.Infof(" ST url=%s", s.BaseURL)
217 "--gui-address=" + s.BaseURL,
221 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
222 s.log.Infof(" ST apikey=%s", s.APIKey)
224 if s.log.Level == logrus.DebugLevel {
225 args = append(args, "-verbose")
229 "STNODEFAULTFOLDER=1",
233 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
235 // Use autogenerated apikey if not set by config.json
236 if err == nil && s.APIKey == "" {
237 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
239 if b, err := ioutil.ReadAll(fd); err == nil {
240 re := regexp.MustCompile("<apikey>(.*)</apikey>")
241 key := re.FindStringSubmatch(string(b))
252 // StartInotify Starts syncthing-inotify process
253 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
255 exeName := "syncthing-inotify"
257 s.log.Infof(" STI url=%s", s.BaseURL)
260 "-target=" + s.BaseURL,
263 args = append(args, "-api="+s.APIKey)
264 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
266 if s.log.Level == logrus.DebugLevel {
267 args = append(args, "-verbosity=4")
272 s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
277 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
278 if err := proc.Signal(os.Interrupt); err != nil {
279 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
283 case <-time.After(time.Second):
284 // A bigger bonk on the head.
285 if err := proc.Signal(os.Kill); err != nil {
286 s.log.Infof("Proc term %s error: %s", pname, err.Error())
291 s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
294 // Stop Stops syncthing process
295 func (s *SyncThing) Stop() {
299 s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
303 // StopInotify Stops syncthing process
304 func (s *SyncThing) StopInotify() {
308 s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
312 // Connect Establish HTTP connection with Syncthing
313 func (s *SyncThing) Connect() error {
316 s.client, err = common.HTTPNewClient(s.BaseURL,
317 common.HTTPClientConfig{
319 HeaderClientKeyName: "X-Syncthing-ID",
320 LogOut: s.conf.LogVerboseOut,
321 LogPrefix: "SYNCTHING: ",
322 LogLevel: common.HTTPLogLevelWarning,
324 s.client.SetLogLevel(s.log.Level.String())
327 msg := ": " + err.Error()
328 if strings.Contains(err.Error(), "connection refused") {
329 msg = fmt.Sprintf("(url: %s)", s.BaseURL)
331 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
334 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
337 s.MyID, err = s.IDGet()
339 return fmt.Errorf("ERROR: cannot retrieve ID")
344 // Start events monitoring
345 err = s.Events.Start()
350 // IDGet returns the Syncthing ID of Syncthing instance running locally
351 func (s *SyncThing) IDGet() (string, error) {
353 if err := s.client.HTTPGet("system/status", &data); err != nil {
356 status := make(map[string]interface{})
357 json.Unmarshal(data, &status)
358 return status["myID"].(string), nil
361 // ConfigGet returns the current Syncthing configuration
362 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
364 config := config.Configuration{}
365 if err := s.client.HTTPGet("system/config", &data); err != nil {
368 err := json.Unmarshal(data, &config)
372 // ConfigSet set Syncthing configuration
373 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
374 body, err := json.Marshal(cfg)
378 return s.client.HTTPPost("system/config", string(body))
381 // IsConfigInSync Returns true if configuration is in sync
382 func (s *SyncThing) IsConfigInSync() (bool, error) {
385 if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
388 if err := json.Unmarshal(data, &d); err != nil {
391 return d.ConfigInSync, nil