gosl.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. // gosl is a basic example of how to develop external web services for Second Life/OpenSimulator using the Go programming language.
  2. package main
  3. import (
  4. "bufio"
  5. "compress/bzip2"
  6. "encoding/csv"
  7. "encoding/json"
  8. "flag"
  9. "fmt"
  10. "github.com/dgraph-io/badger"
  11. "github.com/dgraph-io/badger/options"
  12. "github.com/op/go-logging"
  13. "gopkg.in/natefinch/lumberjack.v2"
  14. "io"
  15. // "io/ioutil"
  16. "net/http"
  17. "net/http/fcgi"
  18. "os"
  19. "path/filepath"
  20. "regexp"
  21. "runtime"
  22. "strings"
  23. "sync"
  24. "time"
  25. )
  26. const NullUUID = "00000000-0000-0000-0000-000000000000" // always useful when we deal with SL/OpenSimulator...
  27. // Logging setup.
  28. var log = logging.MustGetLogger("gosl") // configuration for the go-logging logger, must be available everywhere
  29. var logFormat logging.Formatter
  30. // Opt is used for KV database setup.
  31. var Opt badger.Options
  32. // AvatarUUID is the type that we store in the database; we keep a record from which grid it came from.
  33. type avatarUUID struct {
  34. UUID string // needs to be capitalised for JSON marshalling (it has to do with the way it works)
  35. Grid string
  36. }
  37. var BATCH_BLOCK = 100000 // how many entries to write to the database as a block; the bigger, the faster, but the more memory it consumes
  38. // the authors say that 100000 is way too much
  39. /*
  40. .__
  41. _____ _____ |__| ____
  42. / \\__ \ | |/ \
  43. | Y Y \/ __ \| | | \
  44. |__|_| (____ /__|___| /
  45. \/ \/ \/
  46. */
  47. var noMemory *bool
  48. // main() starts here.
  49. func main() {
  50. // Flag setup
  51. var myPort = flag.String("port", "3000", "Server port")
  52. var myDir = flag.String("dir", "slkvdb", "Directory where database files are stored")
  53. var isServer = flag.Bool("server", false, "Run as server on port " + *myPort)
  54. var isShell = flag.Bool("shell", false, "Run as an interactive shell")
  55. var importFilename = flag.String("import", "", "Import database from W-Hat (use the csv.bz2 version)")
  56. noMemory = flag.Bool("nomemory", false, "Attempt to use only disk to save memory (important for shared webservers)")
  57. // default is FastCGI
  58. flag.Parse()
  59. // We cannot write to stdout if we're running as FastCGI, only to logs!
  60. if *isServer || *isShell {
  61. fmt.Println("gosl is starting...")
  62. } else { // FastCGI: we cannot write to stdio, we need to setup the logger so that we can write to disk
  63. *noMemory = true
  64. }
  65. // Setup the lumberjack rotating logger. This is because we need it for the go-logging logger when writing to files. (20170813)
  66. rotatingLogger := &lumberjack.Logger{
  67. Filename: "gosl.log",
  68. MaxSize: 10, // megabytes
  69. MaxBackups: 3,
  70. MaxAge: 28, //days
  71. }
  72. // Set formatting for stderr and file (basically the same).
  73. logFormat := logging.MustStringFormatter(`%{color}%{time:2006/01/02 15:04:05.0} %{shortfile} - %{shortfunc} ▶ %{level:.4s}%{color:reset} %{message}`) // must be initialised or all hell breaks loose
  74. // Setup the go-logging Logger. Do **not** log to stderr if running as FastCGI!
  75. backendFile := logging.NewLogBackend(rotatingLogger, "", 0)
  76. backendFileFormatter := logging.NewBackendFormatter(backendFile, logFormat)
  77. backendFileLeveled := logging.AddModuleLevel(backendFileFormatter)
  78. backendFileLeveled.SetLevel(logging.INFO, "gosl") // we just send debug data to logs if we run as shell
  79. if *isServer || *isShell {
  80. backendStderr := logging.NewLogBackend(os.Stderr, "", 0)
  81. backendStderrFormatter := logging.NewBackendFormatter(backendStderr, logFormat)
  82. backendStderrLeveled := logging.AddModuleLevel(backendStderrFormatter)
  83. if *isShell {
  84. backendStderrLeveled.SetLevel(logging.DEBUG, "gosl") // shell is meant to be for debugging mostly
  85. } else {
  86. backendStderrLeveled.SetLevel(logging.INFO, "gosl")
  87. }
  88. logging.SetBackend(backendStderrLeveled, backendFileLeveled)
  89. } else {
  90. logging.SetBackend(backendFileLeveled) // FastCGI only logs to file
  91. }
  92. Opt = badger.DefaultOptions
  93. // Check if this directory actually exists; if not, create it. Panic if something wrong happens (we cannot proceed without a valid directory for the database to be written
  94. if stat, err := os.Stat(*myDir); err == nil && stat.IsDir() {
  95. // path is a valid directory
  96. log.Infof("Valid directory: %s\n", *myDir)
  97. } else {
  98. // try to create directory
  99. err = os.Mkdir(*myDir, 0700)
  100. checkErrPanic(err) // cannot make directory, panic and exit logging what went wrong
  101. log.Debugf("Created new directory: %s\n", *myDir)
  102. }
  103. Opt.Dir = *myDir
  104. Opt.ValueDir = Opt.Dir
  105. Opt.TableLoadingMode = options.MemoryMap
  106. //Opt.TableLoadingMode = options.FileIO
  107. if *noMemory {
  108. // Opt.TableLoadingMode = options.FileIO // use standard file I/O operations for tables instead of LoadRAM
  109. // Opt.TableLoadingMode = options.MemoryMap // MemoryMap indicates that that the file must be memory-mapped - https://github.com/dgraph-io/badger/issues/224#issuecomment-329643771
  110. Opt.TableLoadingMode = options.FileIO
  111. Opt.ValueLogFileSize = 1048576
  112. BATCH_BLOCK = 1000 // try to import less at each time, it will take longer but hopefully work
  113. log.Info("Trying to avoid too much memory consumption")
  114. }
  115. //if *isShell {
  116. // Do some testing to see if the database is available
  117. const testAvatarName = "Nobody Here"
  118. var err error
  119. log.Info("gosl started and logging is set up. Proceeding to test KV database.")
  120. kv, err := badger.NewKV(&Opt)
  121. checkErrPanic(err) // should probably panic, cannot prep new database
  122. var testValue = avatarUUID{ NullUUID, "all grids" }
  123. jsonTestValue, err := json.Marshal(testValue)
  124. checkErrPanic(err) // something went VERY wrong
  125. kv.Set([]byte(testAvatarName), jsonTestValue, 0x00)
  126. log.Debugf("SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  127. kv.Close()
  128. key, grid := searchKVname(testAvatarName)
  129. log.Debugf("GET '%s' returned '%s' [grid '%s']\n", testAvatarName, key, grid)
  130. log.Info("KV database seems fine.")
  131. //}
  132. if *importFilename != "" {
  133. log.Info("Attempting to import", *importFilename, "...")
  134. importDatabase(*importFilename)
  135. log.Info("Database finished import.")
  136. }
  137. if *isShell {
  138. log.Info("Starting to run as interactive shell")
  139. reader := bufio.NewReader(os.Stdin)
  140. fmt.Println("Ctrl-C to quit.")
  141. var err error // to avoid assigning text in a different scope (this is a bit awkward, but that's the problem with bi-assignment)
  142. var checkInput, avatarName, avatarKey, gridName string
  143. for {
  144. // Prompt and read
  145. fmt.Print("Enter avatar name or UUID: ")
  146. checkInput, err = reader.ReadString('\n')
  147. checkErr(err)
  148. checkInput = strings.TrimRight(checkInput, "\r\n")
  149. // fmt.Printf("Ok, got %s length is %d and UUID is %v\n", checkInput, len(checkInput), isValidUUID(checkInput))
  150. if (len(checkInput) == 36) && isValidUUID(checkInput) {
  151. avatarName, gridName = searchKVUUID(checkInput)
  152. avatarKey = checkInput
  153. } else {
  154. avatarKey, gridName = searchKVname(checkInput)
  155. avatarName = checkInput
  156. }
  157. if avatarName != NullUUID && avatarKey != NullUUID {
  158. fmt.Println(avatarName, "which has UUID:", avatarKey, "comes from grid:", gridName)
  159. } else {
  160. fmt.Println("Sorry, unknown input", checkInput)
  161. }
  162. }
  163. // never leaves until Ctrl-C
  164. }
  165. // set up routing.
  166. // NOTE(gwyneth): one function only because FastCGI seems to have problems with multiple handlers.
  167. http.HandleFunc("/", handler)
  168. log.Info("Directory for database:", *myDir)
  169. if (*isServer) {
  170. log.Info("Starting to run as web server on port " + *myPort)
  171. err := http.ListenAndServe(":" + *myPort, nil) // set listen port
  172. checkErrPanic(err) // if it can't listen to all the above, then it has to abort anyway
  173. } else {
  174. // default is to run as FastCGI!
  175. // works like a charm thanks to http://www.dav-muz.net/blog/2013/09/how-to-use-go-and-fastcgi/
  176. log.Debug("http.DefaultServeMux is", http.DefaultServeMux)
  177. if err := fcgi.Serve(nil, nil); err != nil {
  178. checkErrPanic(err)
  179. }
  180. }
  181. // we should never have reached this point!
  182. log.Error("Unknown usage! This application may run as a standalone server, as FastCGI application, or as an interactive shell")
  183. if *isServer || *isShell {
  184. flag.PrintDefaults()
  185. }
  186. }
  187. // handler deals with incoming queries and/or associates avatar names with keys depending on parameters.
  188. // Basically we check if both an avatar name and a UUID key has been received: if yes, this means a new entry;
  189. // if just the avatar name was received, it means looking up its key;
  190. // if just the key was received, it means looking up the name (not necessary since llKey2Name does that, but it's just to illustrate);
  191. // if nothing is received, then return an error
  192. func handler(w http.ResponseWriter, r *http.Request) {
  193. if err := r.ParseForm(); err != nil {
  194. logErrHTTP(w, http.StatusNotFound, "No avatar and/or UUID received")
  195. return
  196. }
  197. // test first if this comes from Second Life or OpenSimulator
  198. /*
  199. if r.Header.Get("X-Secondlife-Region") == "" {
  200. logErrHTTP(w, http.StatusForbidden, "Sorry, this application only works inside Second Life.")
  201. return
  202. }
  203. */
  204. name := r.Form.Get("name") // can be empty
  205. key := r.Form.Get("key") // can be empty
  206. compat := r.Form.Get("compat") // compatibility mode with W-Hat
  207. var valueToInsert avatarUUID
  208. messageToSL := "" // this is what we send back to SL - defined here due to scope issues.
  209. if name != "" {
  210. if key != "" {
  211. // we received both: add a new entry
  212. kv, err := badger.NewKV(&Opt)
  213. checkErrPanic(err) // should probably panic
  214. valueToInsert.UUID = key
  215. valueToInsert.Grid = r.Header.Get("X-Secondlife-Shard")
  216. jsonValueToInsert, err := json.Marshal(valueToInsert)
  217. checkErr(err)
  218. kv.Set([]byte(name), jsonValueToInsert, 0x00)
  219. kv.Close()
  220. messageToSL += "Added new entry for '" + name + "' which is: " + valueToInsert.UUID + " from grid: '" + valueToInsert.Grid + "'"
  221. } else {
  222. // we received a name: look up its UUID key and grid.
  223. key, grid := searchKVname(name)
  224. if compat == "false" {
  225. messageToSL += "UUID for '" + name + "' is: " + key + " from grid: '" + grid + "'"
  226. } else { // empty also means true!
  227. messageToSL += key
  228. }
  229. }
  230. } else if key != "" {
  231. // in this scenario, we have the UUID key but no avatar name: do the equivalent of a llKey2Name (slow)
  232. name, grid := searchKVUUID(key)
  233. if compat == "false" {
  234. messageToSL += "Avatar name for " + key + "' is '" + name + "' on grid: '" + grid + "'"
  235. } else { // empty also means true!
  236. messageToSL += name
  237. }
  238. } else {
  239. // neither UUID key nor avatar received, this is an error
  240. logErrHTTP(w, http.StatusNotFound, "Empty avatar name and UUID key received, cannot proceed")
  241. return
  242. }
  243. w.WriteHeader(http.StatusOK)
  244. w.Header().Set("Content-type", "text/plain; charset=utf-8")
  245. fmt.Fprintf(w, messageToSL)
  246. }
  247. // searchKVname searches the KV database for an avatar name.
  248. func searchKVname(avatarName string) (UUID string, grid string) {
  249. time_start := time.Now()
  250. kv, err := badger.NewKV(&Opt)
  251. defer kv.Close()
  252. var item badger.KVItem
  253. if err := kv.Get([]byte(avatarName), &item); err != nil {
  254. log.Errorf("Error while getting name: '%s' - (%v)\n", avatarName, err)
  255. return NullUUID, ""
  256. }
  257. var val = avatarUUID{ NullUUID, "" }
  258. if err = item.Value(func(v []byte) error {
  259. return json.Unmarshal(v, &val)
  260. }); err != nil {
  261. log.Errorf("Error while unparsing UUID for name: '%s' (%v)\n", avatarName, err)
  262. return NullUUID, ""
  263. }
  264. time_end := time.Now()
  265. diffTime := time_end.Sub(time_start)
  266. log.Debugf("Time to lookup '%s': %v\n", avatarName, diffTime)
  267. return val.UUID, val.Grid
  268. }
  269. // searchKVUUID searches the KV database for an avatar key.
  270. func searchKVUUID(avatarKey string) (name string, grid string) {
  271. time_start := time.Now()
  272. kv, err := badger.NewKV(&Opt)
  273. checkErr(err) // should probably panic
  274. itOpt := badger.DefaultIteratorOptions
  275. if !*noMemory {
  276. itOpt.PrefetchValues = true
  277. itOpt.PrefetchSize = 1000 // attempt to get this a little bit more efficient; we have many small entries, so this is not too much
  278. } else {
  279. itOpt.PrefetchValues = false
  280. }
  281. itr := kv.NewIterator(itOpt)
  282. var val = avatarUUID{ NullUUID, "" }
  283. var found string
  284. checks := 0
  285. for itr.Rewind(); itr.Valid(); itr.Next() {
  286. item := itr.Item()
  287. if err = item.Value(func(v []byte) error {
  288. return json.Unmarshal(v, &val)
  289. }); err == nil {
  290. checks++ //Just to see how many checks we made, for statistical purposes
  291. if avatarKey == val.UUID {
  292. found = string(item.Key())
  293. break
  294. }
  295. }
  296. }
  297. time_end := time.Now()
  298. diffTime := time_end.Sub(time_start)
  299. log.Debugf("Made %d checks for '%s' in %v\n", checks, avatarKey, diffTime)
  300. itr.Close()
  301. kv.Close()
  302. return found, val.Grid
  303. }
  304. // importDatabase is essentially reading a bzip2'ed CSV file with UUID,AvatarName downloaded from http://w-hat.com/#name2key .
  305. // One could theoretically set a cron job to get this file, save it on disk periodically, and keep the database up-to-date
  306. // see https://stackoverflow.com/questions/24673335/how-do-i-read-a-gzipped-csv-file for the actual usage of these complicated things!
  307. func importDatabase(filename string) {
  308. filehandler, err := os.Open(filename)
  309. if err != nil {
  310. log.Fatal(err)
  311. }
  312. defer filehandler.Close()
  313. gr := bzip2.NewReader(filehandler) // open bzip2 reader
  314. cr := csv.NewReader(gr) // open csv reader and feed the bzip2 reader into it
  315. // prepare connection to KV database
  316. kv, err := badger.NewKV(&Opt)
  317. checkErrPanic(err) // should probably panic
  318. defer kv.Close()
  319. time_start := time.Now() // we want to get an idea on how long this takes
  320. // prepare stuff for asynchronous key reading; see https://godoc.org/github.com/dgraph-io/badger#KV.BatchSetAsync
  321. wg := new(sync.WaitGroup)
  322. // var oneEntry badger.Entry // make sure this one has at least some memory assigned to it
  323. limit := 0 // outside of for loop so that we can count how many entries we had in total
  324. batch := make([]*badger.Entry, 0, BATCH_BLOCK) // this is the actual data
  325. // define a error function for async reads
  326. f := func(err error) {
  327. defer wg.Done()
  328. if err != nil {
  329. // At this point you can retry writing keys or send error over a channel to handle
  330. // in some other goroutine.
  331. log.Errorf("Got error: %+v\n", err)
  332. }
  333. // Check for error in entries which could be non-nil if the user supplies a CasCounter.
  334. for _, e := range batch {
  335. if e.Error != nil {
  336. log.Errorf("Got error: %+v\n", e.Error)
  337. }
  338. }
  339. // You can do cleanup now. Like deleting keys from cache.
  340. log.Info("All async sets complete.")
  341. }
  342. for ;;limit++ {
  343. record, err := cr.Read()
  344. if err == io.EOF {
  345. break
  346. }
  347. if err != nil {
  348. log.Fatal(err)
  349. }
  350. jsonNewEntry, err := json.Marshal(avatarUUID{ record[0], "Production" }) // W-Hat keys come all from the main LL grid, known as 'Production'
  351. if err != nil {
  352. log.Warning(err)
  353. } else {
  354. // oneEntry = badger.Entry{ Key: []byte(record[1]), Value: []byte(jsonNewEntry)}
  355. batch = append(batch, &badger.Entry{
  356. Key: []byte(record[1]),
  357. Value: []byte(jsonNewEntry),
  358. })
  359. }
  360. if limit % BATCH_BLOCK == 0 && limit != 0 { // we do not run on the first time, and then only every BATCH_BLOCK times
  361. log.Debug("Processing:", limit)
  362. // go writeOneBatch(kv, batch)
  363. wg.Add(1)
  364. kv.BatchSetAsync(batch, f)
  365. log.Debug("Finished")
  366. wg.Wait()
  367. batch = nil // clear all entries, start a new batch
  368. runtime.GC()
  369. batch = make([]*badger.Entry, 0, BATCH_BLOCK)
  370. }
  371. }
  372. // NOTE(gwyneth): these are the final ones (i.e. from the last round number of BATCH_BLOCK up to the end)
  373. // and we do not run them as goroutine because the function might terminate and close our channel before
  374. // this finishes
  375. // BUG(gwyneth): if BATCH_BLOCK is too small, or we finish exactly on the modulo, we may still have a
  376. // racing condition!
  377. wg.Add(1)
  378. kv.BatchSetAsync(batch, f)
  379. log.Debug("Finished last batch")
  380. wg.Wait()
  381. batch = nil // flag the garbage collector that we are finished with this array
  382. runtime.GC()
  383. time_end := time.Now()
  384. diffTime := time_end.Sub(time_start)
  385. log.Info("Total read", limit, "records (or thereabouts) in", diffTime)
  386. }
  387. // NOTE(gwyneth):Auxiliary functions which I'm always using...
  388. // checkErrPanic logs a fatal error and panics.
  389. func checkErrPanic(err error) {
  390. if err != nil {
  391. pc, file, line, ok := runtime.Caller(1)
  392. log.Panic(filepath.Base(file), ":", line, ":", pc, ok, " - panic:", err)
  393. }
  394. }
  395. // checkErr checks if there is an error, and if yes, it logs it out and continues.
  396. // this is for 'normal' situations when we want to get a log if something goes wrong but do not need to panic
  397. func checkErr(err error) {
  398. if err != nil {
  399. pc, file, line, ok := runtime.Caller(1)
  400. log.Error(filepath.Base(file), ":", line, ":", pc, ok, " - error:", err)
  401. }
  402. }
  403. // Auxiliary functions for HTTP handling
  404. // checkErrHTTP returns an error via HTTP and also logs the error.
  405. func checkErrHTTP(w http.ResponseWriter, httpStatus int, errorMessage string, err error) {
  406. if err != nil {
  407. http.Error(w, fmt.Sprintf(errorMessage, err), httpStatus)
  408. pc, file, line, ok := runtime.Caller(1)
  409. log.Error("(", http.StatusText(httpStatus), ") ", filepath.Base(file), ":", line, ":", pc, ok, " - error:", errorMessage, err)
  410. }
  411. }
  412. // checkErrPanicHTTP returns an error via HTTP and logs the error with a panic.
  413. func checkErrPanicHTTP(w http.ResponseWriter, httpStatus int, errorMessage string, err error) {
  414. if err != nil {
  415. http.Error(w, fmt.Sprintf(errorMessage, err), httpStatus)
  416. pc, file, line, ok := runtime.Caller(1)
  417. log.Panic("(", http.StatusText(httpStatus), ") ", filepath.Base(file), ":", line, ":", pc, ok, " - panic:", errorMessage, err)
  418. }
  419. }
  420. // logErrHTTP assumes that the error message was already composed and writes it to HTTP and logs it.
  421. // this is mostly to avoid code duplication and make sure that all entries are written similarly
  422. func logErrHTTP(w http.ResponseWriter, httpStatus int, errorMessage string) {
  423. http.Error(w, errorMessage, httpStatus)
  424. log.Error("(" + http.StatusText(httpStatus) + ") " + errorMessage)
  425. }
  426. // funcName is @Sonia's solution to get the name of the function that Go is currently running.
  427. // This will be extensively used to deal with figuring out where in the code the errors are!
  428. // Source: https://stackoverflow.com/a/10743805/1035977 (20170708)
  429. func funcName() string {
  430. pc, _, _, _ := runtime.Caller(1)
  431. return runtime.FuncForPC(pc).Name()
  432. }
  433. // isValidUUID checks if the UUID is valid.
  434. // Thanks to Patrick D'Appollonio https://stackoverflow.com/questions/25051675/how-to-validate-uuid-v4-in-go
  435. func isValidUUID(uuid string) bool {
  436. r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
  437. return r.MatchString(uuid)
  438. }