Use go module as dependency tool instead of glide
[src/xds/xds-agent.git] / lib / syncthing / st.go
index e513876..ea3db38 100644 (file)
@@ -1,10 +1,30 @@
+/*
+ * Copyright (C) 2017-2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package st
 
 import (
        "encoding/json"
+       "io"
        "io/ioutil"
        "os"
        "path"
+       "path/filepath"
+       "regexp"
        "strings"
        "syscall"
        "time"
@@ -13,99 +33,187 @@ import (
 
        "os/exec"
 
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xdsconfig"
+       common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git"
        "github.com/Sirupsen/logrus"
-       "github.com/iotbzh/xds-agent/lib/common"
-       "github.com/iotbzh/xds-agent/lib/xdsconfig"
        "github.com/syncthing/syncthing/lib/config"
 )
 
 // SyncThing .
 type SyncThing struct {
-       BaseURL string
-       ApiKey  string
-       Home    string
-       STCmd   *exec.Cmd
-       STICmd  *exec.Cmd
+       BaseURL   string
+       APIKey    string
+       Home      string
+       STCmd     *exec.Cmd
+       STICmd    *exec.Cmd
+       MyID      string
+       Connected bool
+       Events    *Events
 
        // Private fields
        binDir      string
+       logsDir     string
        exitSTChan  chan ExitChan
        exitSTIChan chan ExitChan
        client      *common.HTTPClient
        log         *logrus.Logger
+       conf        *xdsconfig.Config
 }
 
-// Monitor process exit
+// ExitChan Channel used for process exit
 type ExitChan struct {
        status int
        err    error
 }
 
+// ConfigInSync Check whether if Syncthing configuration is in sync
+type configInSync struct {
+       ConfigInSync bool `json:"configInSync"`
+}
+
+// FolderStatus Information about the current status of a folder.
+type FolderStatus struct {
+       GlobalFiles       int   `json:"globalFiles"`
+       GlobalDirectories int   `json:"globalDirectories"`
+       GlobalSymlinks    int   `json:"globalSymlinks"`
+       GlobalDeleted     int   `json:"globalDeleted"`
+       GlobalBytes       int64 `json:"globalBytes"`
+
+       LocalFiles       int   `json:"localFiles"`
+       LocalDirectories int   `json:"localDirectories"`
+       LocalSymlinks    int   `json:"localSymlinks"`
+       LocalDeleted     int   `json:"localDeleted"`
+       LocalBytes       int64 `json:"localBytes"`
+
+       NeedFiles       int   `json:"needFiles"`
+       NeedDirectories int   `json:"needDirectories"`
+       NeedSymlinks    int   `json:"needSymlinks"`
+       NeedDeletes     int   `json:"needDeletes"`
+       NeedBytes       int64 `json:"needBytes"`
+
+       InSyncFiles int   `json:"inSyncFiles"`
+       InSyncBytes int64 `json:"inSyncBytes"`
+
+       State        string    `json:"state"`
+       StateChanged time.Time `json:"stateChanged"`
+
+       Sequence int64 `json:"sequence"`
+
+       IgnorePatterns bool `json:"ignorePatterns"`
+}
+
 // NewSyncThing creates a new instance of Syncthing
-//func NewSyncThing(url string, apiKey string, home string, log *logrus.Logger) *SyncThing {
-func NewSyncThing(conf *xdsconfig.SyncThingConf, log *logrus.Logger) *SyncThing {
-       url := conf.GuiAddress
-       apiKey := conf.GuiAPIKey
-       home := conf.Home
+func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
+       var url, apiKey, home, binDir string
+
+       stCfg := conf.FileConf.SThgConf
+       if stCfg != nil {
+               url = stCfg.GuiAddress
+               apiKey = stCfg.GuiAPIKey
+               home = stCfg.Home
+               binDir = stCfg.BinDir
+       }
+
+       if url == "" {
+               url = "http://localhost:8386"
+       }
+       if url[0:7] != "http://" {
+               url = "http://" + url
+       }
+
+       if home == "" {
+               panic("home parameter must be set")
+       }
 
        s := SyncThing{
                BaseURL: url,
-               ApiKey:  apiKey,
+               APIKey:  apiKey,
                Home:    home,
-               binDir:  conf.BinDir,
+               binDir:  binDir,
+               logsDir: conf.FileConf.LogsDir,
                log:     log,
+               conf:    conf,
        }
 
-       if s.BaseURL == "" {
-               s.BaseURL = "http://localhost:8384"
-       }
-       if s.BaseURL[0:7] != "http://" {
-               s.BaseURL = "http://" + s.BaseURL
-       }
+       // Create Events monitoring
+       s.Events = s.NewEventListener()
 
        return &s
 }
 
 // Start Starts syncthing process
 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
+       var err error
+       var exePath string
 
        // Kill existing process (useful for debug ;-) )
-       if os.Getenv("DEBUG_MODE") != "" {
-               exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
+       if _, dbg := os.LookupEnv("XDS_DEBUG_MODE"); dbg {
+               fmt.Printf("\n!!! DEBUG_MODE set: KILL existing %s process(es) !!!\n", exeName)
+               exec.Command("bash", "-c", "ps -ax |grep "+exeName+" |grep "+s.BaseURL+" |cut  -d' ' -f 1|xargs -I{} kill -9 {}").Output()
+       }
+
+       // When not set (or set to '.') set bin to path of xds-agent executable
+       bdir := s.binDir
+       if bdir == "" || bdir == "." {
+               exe, _ := os.Executable()
+               if exeAbsPath, err := filepath.Abs(exe); err == nil {
+                       if exePath, err := filepath.EvalSymlinks(exeAbsPath); err == nil {
+                               bdir = filepath.Dir(exePath)
+                       }
+               }
        }
 
-       path, err := exec.LookPath(path.Join(s.binDir, exeName))
+       exePath, err = exec.LookPath(path.Join(bdir, exeName))
        if err != nil {
-               return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, s.binDir)
+               // Let's try in /opt/AGL/bin
+               exePath, err = exec.LookPath(path.Join("opt", "AGL", "bin", exeName))
+               if err != nil {
+                       return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, bdir)
+               }
        }
-       cmd := exec.Command(path, args...)
+       cmd := exec.Command(exePath, args...)
        cmd.Env = os.Environ()
        for _, ev := range env {
                cmd.Env = append(cmd.Env, ev)
        }
 
+       // open log file
+       var outfile *os.File
+       logFilename := filepath.Join(s.logsDir, exeName+".log")
+       if s.logsDir != "" {
+               outfile, err := os.Create(logFilename)
+               if err != nil {
+                       return nil, fmt.Errorf("Cannot create log file %s", logFilename)
+               }
+
+               cmdOut, err := cmd.StdoutPipe()
+               if err != nil {
+                       return nil, fmt.Errorf("Pipe stdout error for : %v", err)
+               }
+
+               go io.Copy(outfile, cmdOut)
+       }
+
        err = cmd.Start()
        if err != nil {
                return nil, err
        }
 
        *eChan = make(chan ExitChan, 1)
-       go func(c *exec.Cmd) {
+       go func(c *exec.Cmd, oF *os.File) {
                status := 0
-               cmdOut, err := c.StdoutPipe()
-               if err == nil {
-                       s.log.Errorf("Pipe stdout error for : %s", err)
-               } else if cmdOut != nil {
-                       stdOutput, _ := ioutil.ReadAll(cmdOut)
-                       fmt.Printf("STDOUT: %s\n", stdOutput)
-               }
                sts, err := c.Process.Wait()
                if !sts.Success() {
                        s := sts.Sys().(syscall.WaitStatus)
                        status = s.ExitStatus()
                }
+               if oF != nil {
+                       oF.Close()
+               }
+               s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
+
                *eChan <- ExitChan{status, err}
-       }(cmd)
+       }(cmd, outfile)
 
        return cmd, nil
 }
@@ -113,14 +221,19 @@ func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan
 // Start Starts syncthing process
 func (s *SyncThing) Start() (*exec.Cmd, error) {
        var err error
+
+       s.log.Infof(" ST home=%s", s.Home)
+       s.log.Infof(" ST  url=%s", s.BaseURL)
+
        args := []string{
                "--home=" + s.Home,
                "-no-browser",
                "--gui-address=" + s.BaseURL,
        }
 
-       if s.ApiKey != "" {
-               args = append(args, "-gui-apikey=\""+s.ApiKey+"\"")
+       if s.APIKey != "" {
+               args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
+               s.log.Infof(" ST apikey=%s", s.APIKey)
        }
        if s.log.Level == logrus.DebugLevel {
                args = append(args, "-verbose")
@@ -128,42 +241,95 @@ func (s *SyncThing) Start() (*exec.Cmd, error) {
 
        env := []string{
                "STNODEFAULTFOLDER=1",
+               "STNOUPGRADE=1",
        }
 
+       /* FIXME - STILL NEEDED ?, if not SUP code
+
+       // XXX - temporary hack because -gui-apikey seems to correctly handle by
+       // syncthing the early first time
+       stConfigFile := filepath.Join(s.Home, "config.xml")
+       if s.APIKey != "" && !common.Exists(stConfigFile) {
+               s.log.Infof("Stop and restart Syncthing (hack for apikey setting)")
+               s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
+               tmo := 20
+               for ; tmo > 0; tmo-- {
+                       s.log.Debugf("Waiting Syncthing config.xml creation (%v)\n", tmo)
+                       time.Sleep(500 * time.Millisecond)
+                       if common.Exists(stConfigFile) {
+                               break
+                       }
+               }
+               if tmo <= 0 {
+                       return nil, fmt.Errorf("Cannot start Syncthing for config file creation")
+               }
+               s.Stop()
+               read, err := ioutil.ReadFile(stConfigFile)
+               if err != nil {
+                       return nil, fmt.Errorf("Cannot read Syncthing config file for apikey setting")
+               }
+               re := regexp.MustCompile(`<apikey>.*</apikey>`)
+               newContents := re.ReplaceAllString(string(read), "<apikey>"+s.APIKey+"</apikey>")
+               err = ioutil.WriteFile(stConfigFile, []byte(newContents), 0)
+               if err != nil {
+                       return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
+               }
+       }
+       */
        s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
 
+       // Use autogenerated apikey if not set by agent-config.json
+       if err == nil && s.APIKey == "" {
+               if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
+                       defer fd.Close()
+                       if b, err := ioutil.ReadAll(fd); err == nil {
+                               re := regexp.MustCompile("<apikey>(.*)</apikey>")
+                               key := re.FindStringSubmatch(string(b))
+                               if len(key) >= 1 {
+                                       s.APIKey = key[1]
+                               }
+                       }
+               }
+       }
+
        return s.STCmd, err
 }
 
 // StartInotify Starts syncthing-inotify process
 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
        var err error
+       exeName := "syncthing-inotify"
+
+       s.log.Infof(" STI  url=%s", s.BaseURL)
 
        args := []string{
-               "--home=" + s.Home,
                "-target=" + s.BaseURL,
        }
+       if s.APIKey != "" {
+               args = append(args, "-api="+s.APIKey)
+               s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
+       }
        if s.log.Level == logrus.DebugLevel {
                args = append(args, "-verbosity=4")
        }
 
        env := []string{}
 
-       s.STICmd, err = s.startProc("syncthing-inotify", args, env, &s.exitSTIChan)
+       s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
 
        return s.STICmd, err
 }
 
 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
        if err := proc.Signal(os.Interrupt); err != nil {
-               s.log.Errorf("Proc interrupt %s error: %s", pname, err.Error())
+               s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
 
                select {
                case <-exit:
                case <-time.After(time.Second):
                        // A bigger bonk on the head.
                        if err := proc.Signal(os.Kill); err != nil {
-                               s.log.Errorf("Proc term %s error: %s", pname, err.Error())
+                               s.log.Infof("Proc term %s error: %s", pname, err.Error())
                        }
                        <-exit
                }
@@ -192,11 +358,17 @@ func (s *SyncThing) StopInotify() {
 // Connect Establish HTTP connection with Syncthing
 func (s *SyncThing) Connect() error {
        var err error
+       s.Connected = false
        s.client, err = common.HTTPNewClient(s.BaseURL,
                common.HTTPClientConfig{
                        URLPrefix:           "/rest",
                        HeaderClientKeyName: "X-Syncthing-ID",
+                       LogOut:              s.conf.LogVerboseOut,
+                       LogPrefix:           "SYNCTHING: ",
+                       LogLevel:            common.HTTPLogLevelWarning,
                })
+       s.client.SetLogLevel(s.log.Level.String())
+
        if err != nil {
                msg := ": " + err.Error()
                if strings.Contains(err.Error(), "connection refused") {
@@ -207,7 +379,18 @@ func (s *SyncThing) Connect() error {
        if s.client == nil {
                return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
        }
-       return nil
+
+       s.MyID, err = s.IDGet()
+       if err != nil {
+               return fmt.Errorf("ERROR: cannot retrieve ID")
+       }
+
+       s.Connected = true
+
+       // Start events monitoring
+       err = s.Events.Start()
+
+       return err
 }
 
 // IDGet returns the Syncthing ID of Syncthing instance running locally
@@ -240,3 +423,16 @@ func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
        }
        return s.client.HTTPPost("system/config", string(body))
 }
+
+// IsConfigInSync Returns true if configuration is in sync
+func (s *SyncThing) IsConfigInSync() (bool, error) {
+       var data []byte
+       var d configInSync
+       if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
+               return false, err
+       }
+       if err := json.Unmarshal(data, &d); err != nil {
+               return false, err
+       }
+       return d.ConfigInSync, nil
+}