package st
import (
"encoding/json"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
"fmt"
"os/exec"
"github.com/Sirupsen/logrus"
"github.com/iotbzh/xds-agent/lib/xdsconfig"
common "github.com/iotbzh/xds-common/golib"
"github.com/syncthing/syncthing/lib/config"
)
// SyncThing .
type SyncThing struct {
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
}
// 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(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,
Home: home,
binDir: binDir,
logsDir: conf.FileConf.LogsDir,
log: log,
conf: conf,
}
// 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 _, 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)
}
}
}
exePath, err = exec.LookPath(path.Join(bdir, exeName))
if err != nil {
// 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(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 : %s", 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, oF *os.File) {
status := 0
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, outfile)
return cmd, nil
}
// 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+"\"")
s.log.Infof(" ST apikey=%s", s.APIKey)
}
if s.log.Level == logrus.DebugLevel {
args = append(args, "-verbose")
}
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(`.*`)
newContents := re.ReplaceAllString(string(read), ""+s.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 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("(.*)")
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{
"-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(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.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.Infof("Proc term %s error: %s", pname, err.Error())
}
<-exit
}
}
s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
}
// Stop Stops syncthing process
func (s *SyncThing) Stop() {
if s.STCmd == nil {
return
}
s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
s.STCmd = nil
}
// StopInotify Stops syncthing process
func (s *SyncThing) StopInotify() {
if s.STICmd == nil {
return
}
s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
s.STICmd = nil
}
// 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") {
msg = fmt.Sprintf("(url: %s)", s.BaseURL)
}
return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
}
if s.client == nil {
return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
}
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
func (s *SyncThing) IDGet() (string, error) {
var data []byte
if err := s.client.HTTPGet("system/status", &data); err != nil {
return "", err
}
status := make(map[string]interface{})
json.Unmarshal(data, &status)
return status["myID"].(string), nil
}
// ConfigGet returns the current Syncthing configuration
func (s *SyncThing) ConfigGet() (config.Configuration, error) {
var data []byte
config := config.Configuration{}
if err := s.client.HTTPGet("system/config", &data); err != nil {
return config, err
}
err := json.Unmarshal(data, &config)
return config, err
}
// ConfigSet set Syncthing configuration
func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
body, err := json.Marshal(cfg)
if err != nil {
return err
}
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
}