Merge branch 'master' into wip
[src/xds/xds-agent.git] / lib / agent / agent.go
1 package agent
2
3 import (
4         "fmt"
5         "log"
6         "os"
7         "os/exec"
8         "os/signal"
9         "path/filepath"
10         "syscall"
11         "time"
12
13         "github.com/Sirupsen/logrus"
14         "github.com/codegangsta/cli"
15         "github.com/iotbzh/xds-agent/lib/syncthing"
16         "github.com/iotbzh/xds-agent/lib/xdsconfig"
17 )
18
19 const cookieMaxAge = "3600"
20
21 // Context holds the Agent context structure
22 type Context struct {
23         ProgName      string
24         Config        *xdsconfig.Config
25         Log           *logrus.Logger
26         LogLevelSilly bool
27         SThg          *st.SyncThing
28         SThgCmd       *exec.Cmd
29         SThgInotCmd   *exec.Cmd
30
31         webServer  *WebServer
32         xdsServers map[string]*XdsServer
33         sessions   *Sessions
34         events     *Events
35         projects   *Projects
36
37         Exit chan os.Signal
38 }
39
40 // NewAgent Create a new instance of Agent
41 func NewAgent(cliCtx *cli.Context) *Context {
42         var err error
43
44         // Set logger level and formatter
45         log := cliCtx.App.Metadata["logger"].(*logrus.Logger)
46
47         logLevel := cliCtx.GlobalString("log")
48         if logLevel == "" {
49                 logLevel = "error" // FIXME get from Config DefaultLogLevel
50         }
51         if log.Level, err = logrus.ParseLevel(logLevel); err != nil {
52                 fmt.Printf("Invalid log level : \"%v\"\n", logLevel)
53                 os.Exit(1)
54         }
55         log.Formatter = &logrus.TextFormatter{}
56
57         sillyVal, sillyLog := os.LookupEnv("XDS_LOG_SILLY")
58
59         // Define default configuration
60         ctx := Context{
61                 ProgName:      cliCtx.App.Name,
62                 Log:           log,
63                 LogLevelSilly: (sillyLog && sillyVal == "1"),
64                 Exit:          make(chan os.Signal, 1),
65
66                 webServer:  nil,
67                 xdsServers: make(map[string]*XdsServer),
68                 events:     nil,
69         }
70
71         // register handler on SIGTERM / exit
72         signal.Notify(ctx.Exit, os.Interrupt, syscall.SIGTERM)
73         go handlerSigTerm(&ctx)
74
75         return &ctx
76 }
77
78 // Run Main function called to run agent
79 func (ctx *Context) Run() (int, error) {
80         var err error
81
82         // Logs redirected into a file when logfile option or logsDir config is set
83         ctx.Config.LogVerboseOut = os.Stderr
84         if ctx.Config.FileConf.LogsDir != "" {
85                 if ctx.Config.Options.LogFile != "stdout" {
86                         logFile := ctx.Config.Options.LogFile
87
88                         fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
89                         if err != nil {
90                                 msgErr := fmt.Errorf("Cannot create log file %s", logFile)
91                                 return int(syscall.EPERM), msgErr
92                         }
93                         ctx.Log.Out = fdL
94
95                         ctx._logPrint("Logging file: %s\n", logFile)
96                 }
97
98                 logFileHTTPReq := filepath.Join(ctx.Config.FileConf.LogsDir, "xds-agent-verbose.log")
99                 fdLH, err := os.OpenFile(logFileHTTPReq, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
100                 if err != nil {
101                         msgErr := fmt.Errorf("Cannot create log file %s", logFileHTTPReq)
102                         return int(syscall.EPERM), msgErr
103                 }
104                 ctx.Config.LogVerboseOut = fdLH
105
106                 ctx._logPrint("Logging file for HTTP requests: %s\n", logFileHTTPReq)
107         }
108
109         // Create syncthing instance when section "syncthing" is present in config.json
110         if ctx.Config.FileConf.SThgConf != nil {
111                 ctx.SThg = st.NewSyncThing(ctx.Config, ctx.Log)
112         }
113
114         // Start local instance of Syncthing and Syncthing-notify
115         if ctx.SThg != nil {
116                 ctx.Log.Infof("Starting Syncthing...")
117                 ctx.SThgCmd, err = ctx.SThg.Start()
118                 if err != nil {
119                         return 2, err
120                 }
121                 fmt.Printf("Syncthing started (PID %d)\n", ctx.SThgCmd.Process.Pid)
122
123                 ctx.Log.Infof("Starting Syncthing-inotify...")
124                 ctx.SThgInotCmd, err = ctx.SThg.StartInotify()
125                 if err != nil {
126                         return 2, err
127                 }
128                 fmt.Printf("Syncthing-inotify started (PID %d)\n", ctx.SThgInotCmd.Process.Pid)
129
130                 // Establish connection with local Syncthing (retry if connection fail)
131                 time.Sleep(3 * time.Second)
132                 maxRetry := 30
133                 retry := maxRetry
134                 for retry > 0 {
135                         if err := ctx.SThg.Connect(); err == nil {
136                                 break
137                         }
138                         ctx.Log.Infof("Establishing connection to Syncthing (retry %d/%d)", retry, maxRetry)
139                         time.Sleep(time.Second)
140                         retry--
141                 }
142                 if err != nil || retry == 0 {
143                         return 2, err
144                 }
145
146                 // Retrieve Syncthing config
147                 id, err := ctx.SThg.IDGet()
148                 if err != nil {
149                         return 2, err
150                 }
151                 ctx.Log.Infof("Local Syncthing ID: %s", id)
152
153         } else {
154                 ctx.Log.Infof("Cloud Sync / Syncthing not supported")
155         }
156
157         // Create Web Server
158         ctx.webServer = NewWebServer(ctx)
159
160         // Sessions manager
161         ctx.sessions = NewClientSessions(ctx, cookieMaxAge)
162
163         // Create events management
164         ctx.events = NewEvents(ctx)
165
166         // Create projects management
167         ctx.projects = NewProjects(ctx, ctx.SThg)
168
169         // Run Web Server until exit requested (blocking call)
170         if err = ctx.webServer.Serve(); err != nil {
171                 log.Println(err)
172                 return 3, err
173         }
174
175         return 4, fmt.Errorf("Program exited")
176 }
177
178 // Helper function to log message on both stdout and logger
179 func (ctx *Context) _logPrint(format string, args ...interface{}) {
180         fmt.Printf(format, args...)
181         if ctx.Log.Out != os.Stdout {
182                 ctx.Log.Infof(format, args...)
183         }
184 }
185
186 // Handle exit and properly stop/close all stuff
187 func handlerSigTerm(ctx *Context) {
188         <-ctx.Exit
189         if ctx.SThg != nil {
190                 ctx.Log.Infof("Stoping Syncthing... (PID %d)",
191                         ctx.SThgCmd.Process.Pid)
192                 ctx.Log.Infof("Stoping Syncthing-inotify... (PID %d)",
193                         ctx.SThgInotCmd.Process.Pid)
194                 ctx.SThg.Stop()
195                 ctx.SThg.StopInotify()
196         }
197         if ctx.webServer != nil {
198                 ctx.Log.Infof("Stoping Web server...")
199                 ctx.webServer.Stop()
200         }
201         os.Exit(1)
202 }