2 * Copyright (C) 2017-2018 "IoT.bzh"
3 * Author Sebastien Douheret <sebastien@iot.bzh>
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
36 "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xdsconfig"
37 common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git"
38 "github.com/Sirupsen/logrus"
39 "github.com/syncthing/syncthing/lib/config"
43 type SyncThing struct {
56 exitSTChan chan ExitChan
57 exitSTIChan chan ExitChan
58 client *common.HTTPClient
60 conf *xdsconfig.Config
63 // ExitChan Channel used for process exit
64 type ExitChan struct {
69 // ConfigInSync Check whether if Syncthing configuration is in sync
70 type configInSync struct {
71 ConfigInSync bool `json:"configInSync"`
74 // FolderStatus Information about the current status of a folder.
75 type FolderStatus struct {
76 GlobalFiles int `json:"globalFiles"`
77 GlobalDirectories int `json:"globalDirectories"`
78 GlobalSymlinks int `json:"globalSymlinks"`
79 GlobalDeleted int `json:"globalDeleted"`
80 GlobalBytes int64 `json:"globalBytes"`
82 LocalFiles int `json:"localFiles"`
83 LocalDirectories int `json:"localDirectories"`
84 LocalSymlinks int `json:"localSymlinks"`
85 LocalDeleted int `json:"localDeleted"`
86 LocalBytes int64 `json:"localBytes"`
88 NeedFiles int `json:"needFiles"`
89 NeedDirectories int `json:"needDirectories"`
90 NeedSymlinks int `json:"needSymlinks"`
91 NeedDeletes int `json:"needDeletes"`
92 NeedBytes int64 `json:"needBytes"`
94 InSyncFiles int `json:"inSyncFiles"`
95 InSyncBytes int64 `json:"inSyncBytes"`
97 State string `json:"state"`
98 StateChanged time.Time `json:"stateChanged"`
100 Sequence int64 `json:"sequence"`
102 IgnorePatterns bool `json:"ignorePatterns"`
105 // NewSyncThing creates a new instance of Syncthing
106 func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
107 var url, apiKey, home, binDir string
109 stCfg := conf.FileConf.SThgConf
111 url = stCfg.GuiAddress
112 apiKey = stCfg.GuiAPIKey
114 binDir = stCfg.BinDir
118 url = "http://localhost:8386"
120 if url[0:7] != "http://" {
121 url = "http://" + url
125 panic("home parameter must be set")
133 logsDir: conf.FileConf.LogsDir,
138 // Create Events monitoring
139 s.Events = s.NewEventListener()
144 // Start Starts syncthing process
145 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
149 // Kill existing process (useful for debug ;-) )
150 if _, dbg := os.LookupEnv("XDS_DEBUG_MODE"); dbg {
151 fmt.Printf("\n!!! DEBUG_MODE set: KILL existing %s process(es) !!!\n", exeName)
152 exec.Command("bash", "-c", "ps -ax |grep "+exeName+" |grep "+s.BaseURL+" |cut -d' ' -f 1|xargs -I{} kill -9 {}").Output()
155 // When not set (or set to '.') set bin to path of xds-agent executable
157 if bdir == "" || bdir == "." {
158 exe, _ := os.Executable()
159 if exeAbsPath, err := filepath.Abs(exe); err == nil {
160 if exePath, err := filepath.EvalSymlinks(exeAbsPath); err == nil {
161 bdir = filepath.Dir(exePath)
166 exePath, err = exec.LookPath(path.Join(bdir, exeName))
168 // Let's try in /opt/AGL/bin
169 exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
171 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
174 cmd := exec.Command(exePath, args...)
175 cmd.Env = os.Environ()
176 for _, ev := range env {
177 cmd.Env = append(cmd.Env, ev)
182 logFilename := filepath.Join(s.logsDir, exeName+".log")
184 outfile, err := os.Create(logFilename)
186 return nil, fmt.Errorf("Cannot create log file %s", logFilename)
189 cmdOut, err := cmd.StdoutPipe()
191 return nil, fmt.Errorf("Pipe stdout error for : %v", err)
194 go io.Copy(outfile, cmdOut)
202 *eChan = make(chan ExitChan, 1)
203 go func(c *exec.Cmd, oF *os.File) {
205 sts, err := c.Process.Wait()
207 s := sts.Sys().(syscall.WaitStatus)
208 status = s.ExitStatus()
213 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
215 *eChan <- ExitChan{status, err}
221 // Start Starts syncthing process
222 func (s *SyncThing) Start() (*exec.Cmd, error) {
225 s.log.Infof(" ST home=%s", s.Home)
226 s.log.Infof(" ST url=%s", s.BaseURL)
231 "--gui-address=" + s.BaseURL,
235 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
236 s.log.Infof(" ST apikey=%s", s.APIKey)
238 if s.log.Level == logrus.DebugLevel {
239 args = append(args, "-verbose")
243 "STNODEFAULTFOLDER=1",
247 /* FIXME - STILL NEEDED ?, if not SUP code
249 // XXX - temporary hack because -gui-apikey seems to correctly handle by
250 // syncthing the early first time
251 stConfigFile := filepath.Join(s.Home, "config.xml")
252 if s.APIKey != "" && !common.Exists(stConfigFile) {
253 s.log.Infof("Stop and restart Syncthing (hack for apikey setting)")
254 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
256 for ; tmo > 0; tmo-- {
257 s.log.Debugf("Waiting Syncthing config.xml creation (%v)\n", tmo)
258 time.Sleep(500 * time.Millisecond)
259 if common.Exists(stConfigFile) {
264 return nil, fmt.Errorf("Cannot start Syncthing for config file creation")
267 read, err := ioutil.ReadFile(stConfigFile)
269 return nil, fmt.Errorf("Cannot read Syncthing config file for apikey setting")
271 re := regexp.MustCompile(`<apikey>.*</apikey>`)
272 newContents := re.ReplaceAllString(string(read), "<apikey>"+s.APIKey+"</apikey>")
273 err = ioutil.WriteFile(stConfigFile, []byte(newContents), 0)
275 return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
279 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
281 // Use autogenerated apikey if not set by agent-config.json
282 if err == nil && s.APIKey == "" {
283 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
285 if b, err := ioutil.ReadAll(fd); err == nil {
286 re := regexp.MustCompile("<apikey>(.*)</apikey>")
287 key := re.FindStringSubmatch(string(b))
298 // StartInotify Starts syncthing-inotify process
299 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
301 exeName := "syncthing-inotify"
303 s.log.Infof(" STI url=%s", s.BaseURL)
306 "-target=" + s.BaseURL,
309 args = append(args, "-api="+s.APIKey)
310 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
312 if s.log.Level == logrus.DebugLevel {
313 args = append(args, "-verbosity=4")
318 s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
323 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
324 if err := proc.Signal(os.Interrupt); err != nil {
325 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
329 case <-time.After(time.Second):
330 // A bigger bonk on the head.
331 if err := proc.Signal(os.Kill); err != nil {
332 s.log.Infof("Proc term %s error: %s", pname, err.Error())
337 s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
340 // Stop Stops syncthing process
341 func (s *SyncThing) Stop() {
345 s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
349 // StopInotify Stops syncthing process
350 func (s *SyncThing) StopInotify() {
354 s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
358 // Connect Establish HTTP connection with Syncthing
359 func (s *SyncThing) Connect() error {
362 s.client, err = common.HTTPNewClient(s.BaseURL,
363 common.HTTPClientConfig{
365 HeaderClientKeyName: "X-Syncthing-ID",
366 LogOut: s.conf.LogVerboseOut,
367 LogPrefix: "SYNCTHING: ",
368 LogLevel: common.HTTPLogLevelWarning,
370 s.client.SetLogLevel(s.log.Level.String())
373 msg := ": " + err.Error()
374 if strings.Contains(err.Error(), "connection refused") {
375 msg = fmt.Sprintf("(url: %s)", s.BaseURL)
377 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
380 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
383 s.MyID, err = s.IDGet()
385 return fmt.Errorf("ERROR: cannot retrieve ID")
390 // Start events monitoring
391 err = s.Events.Start()
396 // IDGet returns the Syncthing ID of Syncthing instance running locally
397 func (s *SyncThing) IDGet() (string, error) {
399 if err := s.client.HTTPGet("system/status", &data); err != nil {
402 status := make(map[string]interface{})
403 json.Unmarshal(data, &status)
404 return status["myID"].(string), nil
407 // ConfigGet returns the current Syncthing configuration
408 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
410 config := config.Configuration{}
411 if err := s.client.HTTPGet("system/config", &data); err != nil {
414 err := json.Unmarshal(data, &config)
418 // ConfigSet set Syncthing configuration
419 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
420 body, err := json.Marshal(cfg)
424 return s.client.HTTPPost("system/config", string(body))
427 // IsConfigInSync Returns true if configuration is in sync
428 func (s *SyncThing) IsConfigInSync() (bool, error) {
431 if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
434 if err := json.Unmarshal(data, &d); err != nil {
437 return d.ConfigInSync, nil