// 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) + "
", "") // defined on engine.go for now
case syscall.SIGUSR2:
sendMessageToBrowser("status", "", randomdata.Country(randomdata.FullCountry) + "
", "") // 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", "SIGHUP
caught, stopping engine.", "")
EngineRunning.Store(false)
case syscall.SIGCONT:
sendMessageToBrowser("status", "warning", "SIGCONT
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
}