-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathgobot.go
366 lines (329 loc) · 18.5 KB
/
gobot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
// gobot is an attempt to do a single, monolithic Go application which deals with autonomous agents in OpenSimulator.
package main
import (
_ "github.com/go-sql-driver/mysql"
"database/sql"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/op/go-logging" // more complete package to log to different outputs; we start with file, syslog, and stderr; later: WebSockets?
"github.com/Pallinder/go-randomdata"
"github.com/spf13/viper" // to read config files
"golang.org/x/net/websocket"
"gopkg.in/natefinch/lumberjack.v2" // rolling file logs
"log"
"net/http"
"os"
"os/signal"
"os/user"
"runtime"
"path/filepath"
// "sync/atomic" // this is weird since we USE sync/atomic, but the Go compiler complains...
"syscall"
)
var (
// Default configurations, hopefully exported to other files and packages
// we probably should have a struct for this (or even several).
Host, GoBotDSN, URLPathPrefix, PDO_Prefix, PathToStaticFiles,
ServerPort, FrontEnd, MapURL, LSLSignaturePIN string
logFileName string = "log/gobot.log"
logMaxSize, logMaxBackups, logMaxAge int // configuration for the lumberjack rotating logger.
logCompress bool // compress old log files?
logSeverityStderr, logSeverityFile, logSeveritySyslog logging.Level // more configuration for the go-logging logger
ShowPopulation bool = true
Log = logging.MustGetLogger("gobot") // configuration for the go-logging logger, must be available everywhere
logFormat logging.Formatter // must be initialised or all hell breaks loose
)
const NullUUID = "00000000-0000-0000-0000-000000000000" // always useful when we deal with SL/OpenSimulator...
//type templateParameters map[string]string
type templateParameters map[string]interface{}
// loadConfiguration loads all the configuration from the config.toml file.
// It's a separate function because we want to be able to do a killall -HUP gobot to force the configuration to be read again.
// Also, if the configuration file changes, this ought to read it back in again without the need of a HUP signal (20170811).
func loadConfiguration() {
fmt.Print("Reading Gobot configuration:") // note that we might not have go-logging active as yet, so we use fmt.
// Open our config file and extract relevant data from there.
err := viper.ReadInConfig() // Find and read the config file.
if err != nil {
fmt.Println("Error reading config file:", err)
return // we might still get away with this!
}
// Without these set, we cannot do anything.
viper.SetDefault("gobot.EngineRunning", true) // try to set this as quickly as possible, or the engine WILL run!
EngineRunning.Store(viper.GetBool("gobot.EngineRunning")); fmt.Print(".")
viper.SetDefault("gobot.Host", "localhost") // to prevent bombing out with panics.
Host = viper.GetString("gobot.Host"); fmt.Print(".")
viper.SetDefault("gobot.URLPathPrefix", "/go")
URLPathPrefix = viper.GetString("gobot.URLPathPrefix"); fmt.Print(".")
GoBotDSN = viper.GetString("gobot.GoBotDSN"); fmt.Print(".")
viper.SetDefault("PDO_Prefix", "mysql") // for now, nothing else will work anyway...
PDO_Prefix = viper.GetString("gobot.PDO_Prefix"); fmt.Print(".")
viper.SetDefault("gobot.PathToStaticFiles", "~/go/src/gobot")
path, err := expandPath(viper.GetString("gobot.PathToStaticFiles")); fmt.Print(".")
if err != nil {
fmt.Println("Error expanding path:", err)
path = "" // we might get away with this as well.
}
PathToStaticFiles = path
viper.SetDefault("gobot.ServerPort", ":3000")
ServerPort = viper.GetString("gobot.ServerPort"); fmt.Print(".")
FrontEnd = viper.GetString("gobot.FrontEnd"); fmt.Print(".")
MapURL = viper.GetString("opensim.MapURL"); fmt.Print(".")
viper.SetDefault("gobot.LSLSignaturePIN", "0000") // better than no signature at all.
LSLSignaturePIN = viper.GetString("opensim.LSLSignaturePIN"); fmt.Print(".")
viper.SetDefault("gobot.ShowPopulation", true) // try to set this as quickly as possible, or the engine WILL run!
ShowPopulation = viper.GetBool("gobot.ShowPopulation"); fmt.Print(".")
// logging options.
viper.SetDefault("log.FileName", "log/gobot.log")
logFileName = viper.GetString("log.FileName"); fmt.Print(".")
viper.SetDefault("log.Format", `%{color}%{time:2006/01/02 15:04:05.0} %{shortfile} - %{shortfunc} ▶ %{level:.4s}%{color:reset} %{message}`)
logFormat = logging.MustStringFormatter(viper.GetString("log.Format")); fmt.Print(".")
viper.SetDefault("log.MaxSize", 500)
logMaxSize = viper.GetInt("log.MaxSize"); fmt.Print(".")
viper.SetDefault("log.MaxBackups", 3)
logMaxBackups = viper.GetInt("log.MaxBackups"); fmt.Print(".")
viper.SetDefault("log.MaxAge", 28)
logMaxAge = viper.GetInt("log.MaxAge"); fmt.Print(".")
viper.SetDefault("log.Compress", true)
logCompress = viper.GetBool("log.Compress"); fmt.Print(".")
viper.SetDefault("log.SeverityStderr", logging.DEBUG)
switch viper.GetString("log.SeverityStderr") {
case "CRITICAL":
logSeverityStderr = logging.CRITICAL
case "ERROR":
logSeverityStderr = logging.ERROR
case "WARNING":
logSeverityStderr = logging.WARNING
case "NOTICE":
logSeverityStderr = logging.NOTICE
case "INFO":
logSeverityStderr = logging.INFO
case "DEBUG":
logSeverityStderr = logging.DEBUG
// default case is handled directly by viper.
}
fmt.Print(".")
viper.SetDefault("log.SeverityFile", logging.DEBUG)
switch viper.GetString("log.SeverityFile") {
case "CRITICAL":
logSeverityFile = logging.CRITICAL
case "ERROR":
logSeverityFile = logging.ERROR
case "WARNING":
logSeverityFile = logging.WARNING
case "NOTICE":
logSeverityFile = logging.NOTICE
case "INFO":
logSeverityFile = logging.INFO
case "DEBUG":
logSeverityFile = logging.DEBUG
}
fmt.Print(".")
viper.SetDefault("log.SeveritySyslog", logging.CRITICAL) // we don't want to swamp syslog with debugging messages!!
switch viper.GetString("log.SeveritySyslog") {
case "CRITICAL":
logSeveritySyslog = logging.CRITICAL
case "ERROR":
logSeveritySyslog = logging.ERROR
case "WARNING":
logSeveritySyslog = logging.WARNING
case "NOTICE":
logSeveritySyslog = logging.NOTICE
case "INFO":
logSeveritySyslog = logging.INFO
case "DEBUG":
logSeveritySyslog = logging.DEBUG
}
fmt.Print(".")
fmt.Println("read!") // note that we might not have go-logging active as yet, so we use fmt
// Setup the lumberjack rotating logger. This is because we need it for the go-logging logger when writing to files. (20170813)
rotatingLogger := &lumberjack.Logger{
Filename: logFileName, // this is an option set on the config.toml file, eventually the others will be so, too.
MaxSize: logMaxSize, // megabytes
MaxBackups: logMaxBackups, // # of backups to retain on disk
MaxAge: logMaxAge, // days
Compress: logCompress, // true compresses files with gzip
}
// Setup the go-logging Logger. (20170812) We have three loggers: one to stderr, one to a logfile, one to syslog for critical stuff (20170813).
// Update; it's a bad idea to log to both Stderr and Syslog, when gobot might be running from systemd (where stderr is fed to stdout).
// Also, stderr-piped-to-syslog will get messed up if we use fancy colours (?). So for now we'll just log to stderr and a file (20200429).
backendStderr := logging.NewLogBackend(os.Stderr, "", 0)
backendFile := logging.NewLogBackend(rotatingLogger, "", 0)
// backendSyslog,_ := logging.NewSyslogBackend("") // obsoleted, see comment above
// Set formatting for stderr and file (basically the same). I'm assuming syslog has its own format, but I'll have to see what happens (20170813).
// Uodate: what happens is a mess. Now we just have two channels, stderr and file, and stderr gets no colours (20200429).
backendStderrFormatter := logging.NewBackendFormatter(backendStderr,
logging.MustStringFormatter(`%{shortfile} %{shortfunc} -> %{level:.4s} %{message}`)) // no colors! (20200429)
backendFileFormatter := logging.NewBackendFormatter(backendFile, logFormat)
// backendSyslogFormatter := logging.NewBackendFormatter(backendSyslog, syslogFormat) // obsolete, see above
// Check if we're overriding the default severity for each backend. This is user-configurable. By default: DEBUG, DEBUG, CRITICAL.
// TODO(gwyneth): What about a WebSocket backend using https://github.com/cryptix/exp/wslog ? (20170813)
// Update: Dropped the syslog formatter. Now we have only two, stderr and file. The WebSocket backend may still be possible! (20200429)
backendStderrLeveled := logging.AddModuleLevel(backendStderrFormatter)
backendStderrLeveled.SetLevel(logSeverityStderr, "gobot")
backendFileLeveled := logging.AddModuleLevel(backendFileFormatter)
backendFileLeveled.SetLevel(logSeverityFile, "gobot")
// backendSyslogLeveled := logging.AddModuleLevel(backendSyslogFormatter)
// backendSyslogLeveled.SetLevel(logSeveritySyslog, "gobot")
// Set the backends to be used. Logging should commence now.
logging.SetBackend(backendStderrLeveled, backendFileLeveled /*, backendSyslogLeveled */)
fmt.Println("Logging set up.")
}
// main starts here.
func main() {
// to change the flags on the default logger
// see https://stackoverflow.com/a/24809859/1035977
log.SetFlags(log.LstdFlags | log.Lshortfile)
// Config viper, which reads in the configuration file every time it's needed.
// Note that we need some hard-coded variables for the path and config file name.
viper.SetConfigName("config")
viper.SetConfigType("toml") // just to make sure; it's the same format as OpenSimulator (or MySQL) config files
viper.AddConfigPath("$HOME/go/src/gobot/") // that's how I have it
viper.AddConfigPath("$HOME/go/src/github.com/GwynethLlewelyn/gobot/") // that's how you'll have it
viper.AddConfigPath(".") // optionally look for config in the working directory
loadConfiguration() // this gets loaded always, on the first time it runs
viper.WatchConfig() // if the config file is changed, this is supposed to reload it (20170811)
viper.OnConfigChange(func(e fsnotify.Event) {
if (Log == nil) {
fmt.Println("Config file changed:", e.Name) // if we couldn't configure the logging subsystem, it's better to print it to the console
} else {
Log.Info("Config file changed:", e.Name)
}
loadConfiguration() // I think that this needs to be here, or else, how does Viper know what to call?
})
// prepares a special channel to look for termination signals
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGCONT)
// goroutine which listens to signals and calls the loadConfiguration() function if someone sends us a HUP
go func() {
for {
sig := <-sigs
Log.Notice("Got signal", sig)
switch sig {
case syscall.SIGUSR1:
sendMessageToBrowser("status", "", randomdata.FullName(randomdata.Female) + "<br />", "") // defined on engine.go for now
case syscall.SIGUSR2:
sendMessageToBrowser("status", "", randomdata.Country(randomdata.FullCountry) + "<br />", "") // defined on engine.go for now
case syscall.SIGHUP:
// HACK(gwyneth): if the engine dies, send a SIGHUP to get it running again (20170811).
// Moved to HUP (instead of CONT done 20170723) because the configuration is now automatically re-read.
// NOTE(gwyneth): Noticed that when the app is suspended with a Ctrl-Z from the shell, it will get a HUP
// and after that, when putting it in the background, it will get a CONT. So we change the logic appropriately!
sendMessageToBrowser("status", "warning", "<code>SIGHUP</code> caught, stopping engine.", "")
EngineRunning.Store(false)
case syscall.SIGCONT:
sendMessageToBrowser("status", "warning", "<code>SIGCONT</code> caught, restarting engine.", "")
EngineRunning.Store(true)
default:
Log.Warning("Unknown UNIX signal", sig, "caught!! Ignoring...")
}
}
}()
// do some database tests. If it fails, it means the database is broken or corrupted and it's worthless
// to run this application anyway!
Log.Info("Testing opening database connection at ", GoBotDSN, "\nPath to static files is:", PathToStaticFiles)
db, err := sql.Open(PDO_Prefix, GoBotDSN) // presumes mysql for now (supercedes old sqlite3)
checkErrPanic(err) // abort if it cannot even open the database
// query
rows, err := db.Query("SELECT UUID, Name, Location, Position FROM Agents")
checkErrPanic(err) // if select fails, probably the table doesn't even exist; we abort because the database is corrupted!
var agent AgentType // type defined on ui.go to be used on database requests
for rows.Next() {
err = rows.Scan(&agent.UUID, &agent.Name, &agent.Location, &agent.Position)
checkErr(err) // if we get some errors here, we will get in trouble later on; but we might have an empty database, so that's ok.
Log.Debug("Agent '", *agent.Name.Ptr(), "' (", *agent.UUID.Ptr(), ") at", *agent.Location.Ptr(), "Position:", *agent.Position.Ptr())
}
rows.Close()
db.Close()
Log.Infof("\n\nDatabase tests ended, last error was %v:\n\nStarting gobot application at: http://%s%v%s\n\n", err, Host, ServerPort, URLPathPrefix)
// this was just to make tests; now start the engine as a separate goroutine in the background
go engine() // run everything but the kitchen sink in parallel; yay goroutines!
go garbageCollector() // this will periodically remove from the database all old items that are 'dead' (20170730)
// Now prepare the web interface
// Load all templates
err = GobotTemplates.init(PathToStaticFiles + "/templates/*.tpl")
checkErr(err) // abort if templates are not found
// Configure routers for our many inworld scripts
// In my case, paths with /go will be served by gobot, the rest by nginx as before
// Exception is for static files
http.HandleFunc(URLPathPrefix + "/update-inventory/", updateInventory)
http.HandleFunc(URLPathPrefix + "/update-sensor/", updateSensor)
http.HandleFunc(URLPathPrefix + "/register-position/", registerPosition)
http.HandleFunc(URLPathPrefix + "/register-agent/", registerAgent)
http.HandleFunc(URLPathPrefix + "/configure-cube/", configureCube)
http.HandleFunc(URLPathPrefix + "/process-cube/", processCube)
// Static files. This should be handled directly by nginx, but we include it here
// for a standalone version...
fslib := http.FileServer(http.Dir(PathToStaticFiles + "/lib"))
http.Handle(URLPathPrefix + "/lib/", http.StripPrefix(URLPathPrefix + "/lib/", fslib))
templatelib := http.FileServer(http.Dir(PathToStaticFiles + "/templates"))
http.Handle(URLPathPrefix + "/templates/",
http.StripPrefix(URLPathPrefix + "/templates/", templatelib)) // not sure if this is needed
// Deal with templated output for the admin back office, defined on backoffice.go
// For now this is crude, each page is really very similar, but there are not many so each will get its own handler function for now
http.HandleFunc(URLPathPrefix + "/admin/agents/", backofficeAgents)
http.HandleFunc(URLPathPrefix + "/admin/logout/", backofficeLogout)
http.HandleFunc(URLPathPrefix + "/admin/login/", backofficeLogin) // probably not necessary
http.HandleFunc(URLPathPrefix + "/admin/objects/", backofficeObjects)
http.HandleFunc(URLPathPrefix + "/admin/positions/", backofficePositions)
http.HandleFunc(URLPathPrefix + "/admin/inventory/", backofficeInventory)
http.HandleFunc(URLPathPrefix + "/admin/user-management/", backofficeUserManagement)
http.HandleFunc(URLPathPrefix + "/admin/commands/exec/", backofficeCommandsExec)
http.HandleFunc(URLPathPrefix + "/admin/commands/", backofficeCommands)
http.HandleFunc(URLPathPrefix + "/admin/controller-commands/exec/", backofficeControllerCommandsExec)
http.HandleFunc(URLPathPrefix + "/admin/controller-commands/", backofficeControllerCommands)
http.HandleFunc(URLPathPrefix + "/admin/engine/", backofficeEngine)
// LSL Template Generator
http.HandleFunc(URLPathPrefix + "/admin/lsl-register-object/", backofficeLSLRegisterObject)
http.HandleFunc(URLPathPrefix + "/admin/lsl-bot-controller/", backofficeLSLBotController)
http.HandleFunc(URLPathPrefix + "/admin/lsl-agent-scripts/", backofficeLSLAgentScripts)
// fallthrough for admin
http.HandleFunc(URLPathPrefix + "/admin/", backofficeMain)
// deal with agGrid UI elements
http.HandleFunc(URLPathPrefix + "/uiObjects/", uiObjects)
http.HandleFunc(URLPathPrefix + "/uiObjectsUpdate/", uiObjectsUpdate) // to change the database manually
http.HandleFunc(URLPathPrefix + "/uiObjectsRemove/", uiObjectsRemove) // to remove rows of the database manually
http.HandleFunc(URLPathPrefix + "/uiAgents/", uiAgents)
http.HandleFunc(URLPathPrefix + "/uiAgentsUpdate/", uiAgentsUpdate)
http.HandleFunc(URLPathPrefix + "/uiAgentsRemove/", uiAgentsRemove)
http.HandleFunc(URLPathPrefix + "/uiPositions/", uiPositions)
http.HandleFunc(URLPathPrefix + "/uiPositionsUpdate/", uiPositionsUpdate)
http.HandleFunc(URLPathPrefix + "/uiPositionsRemove/", uiPositionsRemove)
http.HandleFunc(URLPathPrefix + "/uiInventory/", uiInventory)
http.HandleFunc(URLPathPrefix + "/uiInventoryUpdate/", uiInventoryUpdate)
http.HandleFunc(URLPathPrefix + "/uiInventoryRemove/", uiInventoryRemove)
http.HandleFunc(URLPathPrefix + "/uiUserManagement/", uiUserManagement)
http.HandleFunc(URLPathPrefix + "/uiUserManagementUpdate/", uiUserManagementUpdate)
http.HandleFunc(URLPathPrefix + "/uiUserManagementRemove/", uiUserManagementRemove)
// Handle Websockets on Engine
http.Handle(URLPathPrefix + "/wsEngine/", websocket.Handler(serveWs))
http.HandleFunc(URLPathPrefix + "/", backofficeLogin) // if not auth, then get auth
err = http.ListenAndServe(ServerPort, nil) // set listen port
checkErr(err) // if it can't listen to all the above, then it has to abort anyway
}
// checkErrPanic logs a fatal error and panics.
func checkErrPanic(err error) {
if err != nil {
pc, file, line, ok := runtime.Caller(1)
Log.Panic(filepath.Base(file), ":", line, ":", pc, ok, " - panic:", err)
}
}
// checkErr checks if there is an error, and if yes, it logs it out and continues.
// this is for 'normal' situations when we want to get a log if something goes wrong but do not need to panic
func checkErr(err error) {
if err != nil {
pc, file, line, ok := runtime.Caller(1)
Log.Error(filepath.Base(file), ":", line, ":", pc, ok, " - error:", err)
}
}
// expandPath expands the tilde as the user's home directory.
// found at http://stackoverflow.com/a/43578461/1035977
func expandPath(path string) (string, error) {
if len(path) == 0 || path[0] != '~' {
return path, nil
}
usr, err := user.Current()
if err != nil {
return "", err
}
return filepath.Join(usr.HomeDir, path[1:]), nil
}