gobot.go 19 KB

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