/* sledjchisl.c - opensim-SC management system. * * Copyright 2020 David Seikel */ #include #ifdef _WIN32 #include #else extern char **environ; #endif #define NO_FCGI_DEFINES #include #undef NO_FCGI_DEFINES //#include "fcgiapp.h" #include #include #include #include #include "fcgi_SC.h" #include "handlekeys.h" // Both my_config.h and fcgi_config.h define the same PACKAGE* variables, which we don't use anyway, soooo - //#undef PACKAGE //#undef PACKAGE_NAME //#undef PACKAGE_STRING //#undef PACKAGE_TARNAME //#undef PACKAGE_VERSION //#undef VERSION // https://mariadb.com/kb/en/about-mariadb-connector-c/ Official docs. // http://dev.mysql.com/doc/refman/5.5/en/c-api-function-overview.html MySQL docs. // http://zetcode.com/db/mysqlc/ MySQL tutorial. #include #include #include #include // Toybox's library has something really odd in it that causes MariaDB library to crash, no idea what. // I've copied the stuff I'm using, and that works. #include "toybox.h" //struct toy_context toys; char toybuf[4096]; int isTmux = 0; int isWeb = 0; char *pwd = ""; char *scRoot = "/opt/opensim_SC"; char *scUser = "opensimsc"; char *Tconsole = "SledjChisl"; char *Tsocket = "caches/opensim-tmux.socket"; char *Ttab = "SC"; char *Tcmd = "tmux -S"; char *webRoot = "/opt/opensim_SC/web"; float loadAverageInc = 0.5; int simTimeOut = 45; char *logTypes[] = { "CRITICAL", "ERROR", "WARNING", "TIMEOUT", "INFO", "DEBUG", }; #define DATE_TIME_LEN 42 void logMe(int v, char *format, ...) { va_list va, va2; int len; char *ret; struct timeval tv; time_t curtime; char date[DATE_TIME_LEN]; va_start(va, format); va_copy(va2, va); // How long is it? len = vsnprintf(0, 0, format, va); len++; va_end(va); // Allocate and do the sprintf() ret = xmalloc(len); vsnprintf(ret, len, format, va2); va_end(va2); gettimeofday(&tv, NULL); curtime = tv.tv_sec; strftime(date, DATE_TIME_LEN, "(%Z %z) %F %R", localtime(&curtime)); fprintf(stderr, "%s.%.6ld %s: %s\n", date, tv.tv_usec, logTypes[v], ret); free(ret); } #define C(...) logMe(0, __VA_ARGS__) #define E(...) logMe(1, __VA_ARGS__) #define W(...) logMe(2, __VA_ARGS__) #define T(...) logMe(3, __VA_ARGS__) #define I(...) logMe(4, __VA_ARGS__) #define D(...) logMe(5, __VA_ARGS__) // In Lua 5.0 reference manual is a table traversal example at page 29. void PrintTable(lua_State *L) { lua_pushnil(L); while (lua_next(L, -2) != 0) { // Numbers can convert to strings, so check for numbers before checking for strings. if (lua_isnumber(L, -1)) printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1)); else if (lua_isstring(L, -1)) printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1)); else if (lua_istable(L, -1)) PrintTable(L); lua_pop(L, 1); } } int sendTmuxKeys(char *dest, char *keys) { int ret = 0, i; char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRoot, Tsocket, Tconsole, dest, keys); i = system(c); if (!WIFEXITED(i)) E("tmux send-keys command failed!"); free(c); return ret; } int sendTmuxCmd(char *dest, char *cmd) { int ret = 0, i; char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRoot, Tsocket, Tconsole, dest, cmd); i = system(c); if (!WIFEXITED(i)) E("tmux send-keys command failed!"); free(c); return ret; } void waitTmuxText(char *dest, char *text) { int i; char *c = xmprintf("sleep 5; %s %s/%s capture-pane -t %s:'%s' -p | grep -E '%s' 2>&1 > /dev/null", Tcmd, scRoot, Tsocket, Tconsole, dest, text); D("Waiting for '%s'.", text); do { i = system(c); if (!WIFEXITED(i)) { E("tmux capture-pane command failed!"); break; } else if (0 == WEXITSTATUS(i)) break; } while (1); free(c); } float waitLoadAverage(float la, float extra, int timeout) { struct sysinfo info; struct timespec timeOut; float l; int to = timeout; I("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout); clock_gettime(CLOCK_MONOTONIC, &timeOut); to += timeOut.tv_sec; do { msleep(5000); sysinfo(&info); l = info.loads[0]/65536.0; clock_gettime(CLOCK_MONOTONIC, &timeOut); timeout -= 5; I("Tick, load average is %.02f, countdown %d seconds.", l, timeout); } while (((la + extra) < l) && (timeOut.tv_sec < to)); return l; } typedef struct _simList simList; struct _simList { int len, num; char **sims; }; static int filterSims(struct tb_dirtree *node) { if (!node->parent) return TB_DIRTREE_RECURSE | TB_DIRTREE_SHUTUP; if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0))) { simList *list = (simList *) node->parent->extra; if ((list->num + 1) > list->len) { list->len = list->len + 1; list->sims = xrealloc(list->sims, list->len * sizeof(char *)); } list->sims[list->num] = xstrdup(node->name); list->num++; } return 0; } simList *getSims() { simList *sims = xmalloc(sizeof(simList)); memset(sims, 0, sizeof(simList)); memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s/config", scRoot); struct tb_dirtree *new = tb_dirtree_add_node(0, toybuf, 0); new->extra = (long) sims; tb_dirtree_handle_callback(new, filterSims); qsort(sims->sims, sims->num, sizeof(char *), qstrcmp); tb_dirtree_free(new); return sims; } static int filterInis(struct tb_dirtree *node) { if (!node->parent) return TB_DIRTREE_RECURSE | TB_DIRTREE_SHUTUP | TB_DIRTREE_SAVE; int l = strlen(node->name); if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0) { node->parent->extra = (long) node->name; return TB_DIRTREE_ABORT; } return 0; } char *getSimName(char *sim) { char *ret = NULL; char *c = xmprintf("%s/config/%s", scRoot, sim); struct tb_dirtree *new = tb_dirtree_add_node(0, c, 0); tb_dirtree_handle_callback(new, filterInis); if (new->extra) { char *temp = NULL; regex_t pat; regmatch_t m[2]; long len; int fd; c = xmprintf("%s/config/%s/%s", scRoot, sim, new->extra); fd = xopenro(c); xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED); do { // TODO - get_line() is slow, and wont help much with DOS and Mac line endings. temp = get_line(fd); if (temp) { if (!regexec(&pat, temp, 2, m, 0)) { // Return first parenthesized subexpression as string. if (pat.re_nsub > 0) { ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so); break; } } } } while (temp); xclose(fd); } tb_dirtree_free(new); return ret; } // Expects either "simXX" or "ROBUST". int checkSimIsRunning(char *sim) { int ret = 0; struct stat st; // Check if it's running. memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s/caches/%s.pid", scRoot, sim); if (0 == stat(toybuf, &st)) { int fd, i; char *pid = NULL; // Double check if it's REALLY running. if ((fd = xopenro(toybuf)) == -1) tb_perror_msg("xopenro(%s)", toybuf); else { pid = get_line(fd); if (NULL == pid) tb_perror_msg("get_line(%s)", toybuf); else { xclose(fd); memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "ps -p %s --no-headers -o comm", pid); i = system(toybuf); if (WIFEXITED(i)) { if (0 != WEXITSTATUS(i)) // No such pid. { memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "rm -f %s/caches/%s.pid", scRoot, sim); D("%s", toybuf); i = system(toybuf); } } } } } // Now check if it's really really running. lol memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s/caches/%s.pid", scRoot, sim); if (0 == stat(toybuf, &st)) ret = 1; return ret; } static void PrintEnv(char *label, char **envp) { FCGI_printf("%s:
\n
\n", label);
    for ( ; *envp != NULL; envp++)
    {
        FCGI_printf("%s\n", *envp);
    }
    FCGI_printf("

\n"); } static void printEnv(char **envp) { for ( ; *envp != NULL; envp++) { D("%s", *envp); } } my_ulonglong dbCount(MYSQL *db, char *table, char *where) { my_ulonglong ret = 0; memset(toybuf, 0, sizeof(toybuf)); if (NULL == where) snprintf(toybuf, sizeof(toybuf), "SELECT Count(*) FROM %s", table); else snprintf(toybuf, sizeof(toybuf), "SELECT Count(*) FROM %s WHERE %s", table, where); if (mysql_query(db, toybuf)) E("Query failed: %s", mysql_error(db)); else { MYSQL_RES *result = mysql_store_result(db); if (!result) E("Couldn't get results set from %s\n: %s", toybuf, mysql_error(db)); else { MYSQL_ROW row = mysql_fetch_row(result); if (!row) E("Couldn't get row from %s\n: %s", toybuf, mysql_error(db)); else ret = atoll(row[0]); mysql_free_result(result); } } return ret; } my_ulonglong dbCountJoin(MYSQL *db, char *table, char *select, char *join, char *where, char *order) { my_ulonglong ret = 0; if (NULL == select) select = "*"; memset(toybuf, 0, sizeof(toybuf)); if (NULL == where) snprintf(toybuf, sizeof(toybuf), "SELECT %s FROM %s", select, table, join); else snprintf(toybuf, sizeof(toybuf), "SELECT %s FROM %s %s WHERE %s", select, table, join, where); if (mysql_query(db, toybuf)) E("Query failed: %s", mysql_error(db)); else { MYSQL_RES *result = mysql_store_result(db); if (!result) E("Couldn't get results set from %s\n: %s", toybuf, mysql_error(db)); else ret = mysql_num_rows(result); mysql_free_result(result); } return ret; } qlist_t *dbSelect(MYSQL *db, char *table, char *select, char *join, char *where, char *order) { qlist_t *ret = qlist(0); if (NULL == select) select = "*"; memset(toybuf, 0, sizeof(toybuf)); if (NULL == where) snprintf(toybuf, sizeof(toybuf), "SELECT %s FROM %s", select, table, join); else snprintf(toybuf, sizeof(toybuf), "SELECT %s FROM %s %s WHERE %s", select, table, join, where); if (mysql_query(db, toybuf)) E("Query failed: %s", mysql_error(db)); else { MYSQL_RES *result = mysql_store_result(db); if (!result) E("Couldn't get results set from %s\n: %s", toybuf, mysql_error(db)); else { MYSQL_FIELD *fields; fields = mysql_fetch_fields(result); if (!fields) E("Faild fetching fields: %s", mysql_error(db)); else { unsigned int i, num_fields = mysql_num_fields(result); MYSQL_ROW row; while ((row = mysql_fetch_row(result))) { qhashtbl_t *flds = qhashtbl(0, 0); for (i = 0; i < num_fields; i++) { flds->putstr(flds, fields[i].name, row[i]); } ret->addlast(ret, flds, sizeof(*flds)); } } } mysql_free_result(result); } return ret; } void replaceStr(qhashtbl_t *ssi, char *key, char *value) { // char *tmp; // I think this is taken care of already. // if ((tmp = ssi->getstr(ssi, key, false)) != NULL) // free(tmp); ssi->putstr(ssi, key, value); } void replaceLong(qhashtbl_t *ssi, char *key, my_ulonglong value) { char *tmp = xmprintf("%lu", value); replaceStr(ssi, key, tmp); free(tmp); } float timeDiff(struct timeval *now, struct timeval *then) { if (0 == gettimeofday(now, NULL)) { struct timeval thisTime = { 0, 0 }; double result = 0.0; thisTime.tv_sec = now->tv_sec; thisTime.tv_usec = now->tv_usec; if (thisTime.tv_usec < then->tv_usec) { thisTime.tv_sec--; thisTime.tv_usec += 1000000; } thisTime.tv_usec -= then->tv_usec; thisTime.tv_sec -= then->tv_sec; result = ((double) thisTime.tv_usec) / ((double) 1000000.0); result += thisTime.tv_sec; return result; } else return 0.0; } typedef struct _gridStats gridStats; struct _gridStats { float next; struct timeval last; qhashtbl_t *stats; }; gridStats *getStats(MYSQL *db, gridStats *stats) { if (NULL == stats) { stats = xmalloc(sizeof(gridStats)); stats->next = 300; gettimeofday(&(stats->last), NULL); stats->stats = qhashtbl(0, 0); stats->stats->putstr(stats->stats, "version", "SledjChisl FCGI Dev 0.1"); stats->stats->putstr(stats->stats, "grid", "my grid"); stats->stats->putstr(stats->stats, "uri", "http://localhost:8002/"); stats->stats->putstr(stats->stats, "gridOnline", "??"); } else { static struct timeval thisTime; if (stats->next > timeDiff(&thisTime, &(stats->last))) return stats; } if (db) { I("Getting fresh grid stats."); char *tmp; my_ulonglong locIn = dbCount(db, "Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world. my_ulonglong HGin = dbCount(db, "Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world. // Collect stats about members. replaceLong(stats->stats, "hgers", HGin); replaceLong(stats->stats, "inworld", locIn - HGin); tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false)); replaceLong(stats->stats, "outworld", dbCount(db, "hg_traveling_data", tmp)); free(tmp); replaceLong(stats->stats, "members", dbCount(db, "UserAccounts", NULL)); // Count local and HG visitors for the last 30 and 60 days. locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))", ""); HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))"); replaceLong(stats->stats, "locDay30", locIn); replaceLong(stats->stats, "day30", HGin); replaceLong(stats->stats, "HGday30", HGin - locIn); locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))", ""); HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))"); replaceLong(stats->stats, "locDay60", locIn); replaceLong(stats->stats, "day60", HGin); replaceLong(stats->stats, "HGday60", HGin - locIn); // Collect stats about sims. replaceLong(stats->stats, "sims", dbCount(db, "regions", NULL)); replaceLong(stats->stats, "onlineSims", dbCount(db, "regions", "sizeX != 0")); replaceLong(stats->stats, "varRegions", dbCount(db, "regions", "sizeX > 256 or sizeY > 256")); replaceLong(stats->stats, "singleSims", dbCount(db, "regions", "sizeX = 256 and sizeY = 256")); replaceLong(stats->stats, "offlineSims", dbCount(db, "regions", "sizeX = 0")); // Calculate total size of all regions. qlist_t *regions = dbSelect(db, "regions", "sizeX,sizeY", "", "sizeX != 0", ""); qlist_obj_t obj; my_ulonglong simSize = 0; memset((void *) &obj, 0, sizeof(obj)); // must be cleared before call regions->lock(regions); while (regions->getnext(regions, &obj, false) == true) { qhashtbl_t *row = (qhashtbl_t *) obj.data; my_ulonglong x = 0, y = 0; tmp = row->getstr(row, "sizeX", false); if (NULL == tmp) E("No regions.sizeX!"); else x = atoll(tmp); tmp = row->getstr(row, "sizeY", false); if (NULL == tmp) E("No regions.sizeY!"); else y = atoll(tmp); simSize += x * y; } regions->unlock(regions); tmp = xmprintf("%lu", simSize); stats->stats->putstr(stats->stats, "simsSize", tmp); free(tmp); gettimeofday(&(stats->last), NULL); } return stats; } enum fragmentType { FT_TEXT, FT_PARAM, FT_LUA }; typedef struct _fragment fragment; struct _fragment { enum fragmentType type; int length; char *text; }; typedef struct _HTMLfile HTMLfile; struct _HTMLfile { struct timespec last; qlist_t *fragments; }; qhashtbl_t *HTMLfileCache = NULL; fragment *newFragment(enum fragmentType type, char *text, int len) { fragment *frg = xmalloc(sizeof(fragment)); frg->type = type; frg->length = len; frg->text = xmalloc(len + 1); memcpy(frg->text, text, len); frg->text[len] = '\0'; return frg; } HTMLfile *checkHTMLcache(char *file) { if (NULL == HTMLfileCache) HTMLfileCache = qhashtbl(0, 0); HTMLfile *ret = (HTMLfile *) HTMLfileCache->get(HTMLfileCache, file, NULL, false); int fd = open(file, O_RDONLY); size_t length = 0; fragment *frg0, *frg1; if (-1 == fd) E("Failed to open %s", file); else { struct stat sb; if (fstat(fd, &sb) == -1) E("Failed to stat %s", file); else { if ((NULL != ret) && (ret->last.tv_sec < sb.st_mtim.tv_sec)) { HTMLfileCache->remove(HTMLfileCache, file); ret = NULL; } if (NULL == ret) { char *mm = MAP_FAILED; ret = xmalloc(sizeof(HTMLfile)); ret->fragments = qlist(QLIST_THREADSAFE); length = sb.st_size; ret->last.tv_sec = sb.st_mtim.tv_sec; ret->last.tv_nsec = sb.st_mtim.tv_nsec; I("Loading web template %s", file); D("Web template %s is %d bytes long.", file, length); mm = mmap(NULL, length, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0); if (mm == MAP_FAILED) E("Failed to mmap %s", file); else { char *h; int i, j = 0, k = 0, l, m; // Scan for server side includes style markings. for (i = 0; i < length; i++) { if (i + 5 < length) { if (('<' == mm[i]) && ('!' == mm[i + 1]) && ('-' == mm[i + 2]) && ('-' == mm[i + 3]) && ('#' == mm[i + 4])) // '' i += 4; } frg0 = newFragment(FT_TEXT, &mm[k], m - k); ret->fragments->addlast(ret->fragments, frg0, sizeof(*frg0)); ret->fragments->addlast(ret->fragments, frg1, sizeof(*frg1)); k = i; break; } } } } } } } } } frg0 = newFragment(FT_TEXT, &mm[k], length - k); ret->fragments->addlast(ret->fragments, frg0, sizeof(*frg0)); if (-1 == munmap(mm, length)) FCGI_fprintf(FCGI_stderr, "Failed to munmap %s\n", file); HTMLfileCache->put(HTMLfileCache, file, ret, sizeof(*ret)); } } close(fd); } } return ret; } int main(int argc, char *argv[], char **env) { // don't segfault if our environment is crazy if (!*argv) return 127; char *cmd = *argv; char *tmp; qhashtbl_t *configs = qhashtbl(0, 0); lua_State *L = luaL_newstate(); MYSQL *database = NULL, *dbconn = NULL; gridStats *stats = NULL; int status, result, i; void *vd; pwd = getcwd(0, 0); if (isatty(1)) { I("Outputting to a terminal, not a web server."); // Check if we are already running inside the proper tmux server. char *eTMUX = getenv("TMUX"); memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s/%s", scRoot, Tsocket); if (((eTMUX) && (0 == strncmp(toybuf, eTMUX, strlen(toybuf))))) { I("Running inside the proper tmux server."); isTmux = 1; } else I("Not running inside the proper tmux server, starting it."); I("libfcgi version: %s", FCGI_VERSION); I("Lua version: %s", LUA_RELEASE); I("LuaJIT version: %s", LUAJIT_VERSION); I("MariaDB / MySQL client version: %s", mysql_get_client_info()); } else isWeb = 1; /* From http://luajit.org/install.html - To change or extend the list of standard libraries to load, copy src/lib_init.c to your project and modify it accordingly. Make sure the jit library is loaded or the JIT compiler will not be activated. */ luaL_openlibs(L); // Load Lua libraries. // Load the config scripts. char *cPaths[] = { "/etc/sledjChisl.conf.lua", // "/etc/sledjChisl.d/*.lua", "~/.sledjChisl.conf.lua", // "~/.config/sledjChisl/*.lua", ".sledjChisl.conf.lua", NULL }; struct stat st; for (i = 0; cPaths[i]; i++) { memset(toybuf, 0, sizeof(toybuf)); if (('/' == cPaths[i][0]) || ('~' == cPaths[i][0])) snprintf(toybuf, sizeof(toybuf), "%s", cPaths[i]); else snprintf(toybuf, sizeof(toybuf), "%s/%s", pwd, cPaths[i]); if (0 != lstat(toybuf, &st)) continue; if (!isWeb) I("Loading configuration file - %s", toybuf); status = luaL_loadfile(L, toybuf); if (status) // If something went wrong, error message is at the top of the stack. E("Couldn't load file: %s", lua_tostring(L, -1)); else { result = lua_pcall(L, 0, LUA_MULTRET, 0); if (result) E("Failed to run script: %s", lua_tostring(L, -1)); else { lua_getglobal(L, "config"); lua_pushnil(L); while(lua_next(L, -2) != 0) { char *n = (char *) lua_tostring(L, -2); // Numbers can convert to strings, so check for numbers before checking for strings. // On the other hand, strings that can be converted to numbers also pass lua_isnumber(). sigh if (lua_isnumber(L, -1)) { float v = lua_tonumber(L, -1); configs->put(configs, n, &v, sizeof(float)); } else if (lua_isstring(L, -1)) configs->putstr(configs, n, (char *) lua_tostring(L, -1)); else { char *v = (char *) lua_tostring(L, -1); E("Unknown config variable type for %s = %s", n, v); } lua_pop(L, 1); } } } } if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); D("Setting loadAverageInc = %f", loadAverageInc);} if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); D("Setting simTimeOut = %d", simTimeOut);} if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; D("Setting scRoot = %s", scRoot);} if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; D("Setting scUser = %s", scUser);} if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; D("Setting Tconsole = %s", Tconsole);} if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; D("Setting Tsocket = %s", Tsocket);} if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; D("Setting Ttab = %s", Ttab);} if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; D("Setting webRoot = %s", webRoot);} if (isTmux || isWeb) { char *d; memset(toybuf, 0, sizeof(toybuf)); // TODO - the problem here is that web server isn't in the opensimmc group, so can't read this file ,with the database credentials. // Other web server things have access to database credentials, so not like this is a big problem. // For now I've just opened up the perms on it on my desktop. snprintf(toybuf, sizeof(toybuf), "%s/config/config.ini", scRoot); qlisttbl_t *qconfig = qconfig_parse_file(NULL, toybuf, '='); d = qstrunchar(qconfig->getstr(qconfig, "Const.ConnectionString", false), '"', '"'); if (NULL == d) { E("No database credentials in %s!", toybuf); goto finished; } else { char *p0, *p1, *p2; if (NULL == (d = strdup(d))) { E("Out of memory!"); goto finished; } // Data Source=MYSQL_HOST;Database=MYSQL_DB;User ID=MYSQL_USER;Password=MYSQL_PASSWORD;Old Guids=true; p0 = d; while (NULL != p0) { p1 = strchr(p0, '='); if (NULL == p1) break; *p1 = '\0'; p2 = strchr(p1 + 1, ';'); if (NULL == p2) break; *p2 = '\0'; configs->putstr(configs, p0, p1 + 1); // NOTE - this allocs memory for it's key and it's data. p0 = p2 + 1; if ('\0' == *p0) p0 = NULL; }; free(d); } if (mysql_library_init(argc, argv, NULL)) { E("mysql_library_init() failed!"); goto finished; } database = mysql_init(NULL); if (NULL == database) { E("mysql_init() failed - %s", mysql_error(database)); goto finished; } else { // I have no idea what evil magic toybox is doing, but this ALWAYS CRASHES, no matter what I do. dbconn = mysql_real_connect(database, configs->getstr(configs, "Data Source", true), configs->getstr(configs, "User ID", true), configs->getstr(configs, "Password", true), configs->getstr(configs, "Database", true), // 3036, "/var/run/mysqld/mysqld.sock", 0, NULL, CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS); if (NULL == dbconn) { E("mysql_real_connect() failed - %s", mysql_error(database)); goto finished; } // Need to kick this off. stats = getStats(database, stats); char *h = qstrunchar(qconfig->getstr(qconfig, "Const.HostName", false), '"', '"'); char *p = qstrunchar(qconfig->getstr(qconfig, "Const.PublicPort", false), '"', '"'); stats->stats->putstr(stats->stats, "grid", qstrunchar(qconfig->getstr(qconfig, "Const.GridName", false), '"', '"')); stats->stats->putstr(stats->stats, "HostName", h); stats->stats->putstr(stats->stats, "PublicPort", p); snprintf(toybuf, sizeof(toybuf), "http://%s:%s/", h, p); stats->stats->putstr(stats->stats, "uri", toybuf); } } if (isWeb) { char **initialEnv = env; int count = 0, entries, bytes; // FCGI_LISTENSOCK_FILENO is the socket to the web server. // STDOUT and STDERR go to the web servers error log, or at least it does in Apache 2. // fprintf(stdout, "STDOUT Started SledjChisl web server.\n"); // fprintf(stderr, "STDERR Started SledjChisl web server.\n"); I("Running SledjChisl inside a web server."); if (1 == argc) D("no args"); else { for (i = 0; argv[i] != NULL; i++) D("ARG %s", argv[i]); } printEnv(env); struct stat statbuf; if (-1 == fstat(FCGI_LISTENSOCK_FILENO, &statbuf)) tb_error_msg("fstat() failed"); else { if (S_ISREG (statbuf.st_mode)) D("regular file"); else if (S_ISDIR (statbuf.st_mode)) D("directory"); else if (S_ISCHR (statbuf.st_mode)) D("character device"); else if (S_ISBLK (statbuf.st_mode)) D("block device"); else if (S_ISFIFO(statbuf.st_mode)) D("FIFO (named pipe)"); else if (S_ISLNK (statbuf.st_mode)) D("symbolic link"); else if (S_ISSOCK(statbuf.st_mode)) D("socket"); else D("unknown file descriptor type"); } while (FCGI_Accept() != -1) { if (NULL == getenv("PATH_INFO")) {msleep(1000); continue;} char *contentLength = getenv("CONTENT_LENGTH"); int len; if (contentLength != NULL) len = strtol(contentLength, NULL, 10); else len = 0; if (FCGX_IsCGI()) D("Started SledjChisl CGI web request ROLE = %s!", getenv("FCGI_ROLE")); else D("Started SledjChisl FCGI web request ROLE = %s.", getenv("FCGI_ROLE")); memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s/html%s", webRoot, getenv("PATH_INFO")); HTMLfile *thisFile = checkHTMLcache(toybuf); getStats(database, stats); // This is dynamic content, it's always gonna be modified. I think. // char *since = getenv("If-Modified-Since"); // if (NULL != since) // { // time_t snc = qtime_parse_gmtstr(since); // if (thisFile->last.tv_sec < snc) // { // D("Status: 304 Not Modified - %s", toybuf); // FCGI_printf("Status: 304 Not Modified\r\n"); // goto fcgiDone; // } // } FCGI_printf("Status: 200 OK\r\n" "Content-type: text/html\r\n" "\r\n"); if (len <= 0) D("No data from standard input."); else { int i, ch; FCGI_printf("Standard input:
\n

\n");
	    for (i = 0; i < len; i++)
	    {
		if ((ch = FCGI_getchar()) < 0)
		{
		    E("Error: Not enough bytes received on standard input

"); break; } FCGI_putchar(ch); } FCGI_printf("\n

\n"); } qlist_obj_t obj; memset((void *) &obj, 0, sizeof(obj)); // must be cleared before call thisFile->fragments->lock(thisFile->fragments); while (thisFile->fragments->getnext(thisFile->fragments, &obj, false) == true) { fragment *frg = (fragment *) obj.data; if (NULL == frg->text) { E("NULL fragment!"); continue; } switch (frg->type) { case FT_TEXT: { // Silly thing triggers a "mod_fcgid: ap_pass_brigade failed in handle_request_ipc function" in Apache 2.4, sometimes. // Haven't seen one for some time. // https://www.tablix.org/~avian/blog/archives/2016/05/on_ap_pass_brigade_failed/ int l = FCGI_fwrite(frg->text, 1, frg->length, FCGI_stdout); if (l != frg->length) E("Failed to write %d != %d", l, frg->length); break; } case FT_PARAM: { if (strcmp("DEBUG", frg->text) == 0) { FCGI_printf("

FastCGI SledjChisl

\n" "Request number %d, Process ID: %d

\n", ++count, getpid()); FCGI_printf("

libfcgi version: %s

\n", FCGI_VERSION); FCGI_printf("

Lua version: %s

\n", LUA_RELEASE); FCGI_printf("

LuaJIT version: %s

\n", LUAJIT_VERSION); FCGI_printf("

MySQL client version: %s

\n", mysql_get_client_info()); PrintEnv("Initial environment", initialEnv); PrintEnv("Request environment", environ); } else if (strcmp("URL", frg->text) == 0) FCGI_printf("%s://%s%s", getenv("REQUEST_SCHEME"), getenv("HTTP_HOST"), getenv("SCRIPT_NAME")); else { if ((tmp = stats->stats->getstr(stats->stats, frg->text, false)) != NULL) FCGI_printf("%s", tmp); else FCGI_printf("%s", frg->text); } break; } case FT_LUA: break; } } thisFile->fragments->unlock(thisFile->fragments); fcgiDone: D("Finishing SledjChisl web request."); FCGI_Finish(); } FCGI_fprintf(FCGI_stdout, "Stopped SledjChisl web server.\n"); D("Stopped SledjChisl web server."); goto finished; } if (!isTmux) { // Let's see if the proper tmux server is even running. memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s %s/%s -q list-sessions 2>/dev/null | grep -q %s:", Tcmd, scRoot, Tsocket, Tconsole); i = system(toybuf); if (WIFEXITED(i)) { if (0 != WEXITSTATUS(i)) // No such sesion, create it. { memset(toybuf, 0, sizeof(toybuf)); // The sudo is only so that the session is owned by opensim, otherwise it's owned by whoever ran this script, which is a likely security hole. // After the session is created, we rely on the caches directory to be group sticky, so that anyone in the opensim group can attach to the tmux socket. snprintf(toybuf, sizeof(toybuf), "sudo -Hu %s %s %s/%s new-session -d -s %s -n '%s' \\; split-window -bhp 50 -t '%s:' bash -c './sledjchisl; cd %s; bash'", scUser, Tcmd, scRoot, Tsocket, Tconsole, Ttab, Tconsole, scRoot); i = system(toybuf); if (!WIFEXITED(i)) E("tmux new-session command failed!"); } // Join the session. memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s %s/%s select-window -t '%s' \\; attach-session -t '%s'", Tcmd, scRoot, Tsocket, Tconsole, Tconsole); i = system(toybuf); if (!WIFEXITED(i)) E("tmux attach-session command failed!"); goto finished; } else E("tmux list-sessions command failed!"); } simList *sims = getSims(); if (1) { struct sysinfo info; float la; sysinfo(&info); la = info.loads[0]/65536.0; if (!checkSimIsRunning("ROBUST")) { char *d = xmprintf("%s.{right}", Ttab); char *c = xmprintf("cd %s/current/bin", scRoot); I("ROBUST is starting up."); sendTmuxCmd(d, c); free(c); c = xmprintf("mono Robust.exe -inidirectory=%s/config/ROBUST", scRoot); sendTmuxCmd(d, c); free(c); waitTmuxText(d, "INITIALIZATION COMPLETE FOR ROBUST"); I("ROBUST is done starting up."); la = waitLoadAverage(la, loadAverageInc / 3.0, simTimeOut / 3); free(d); } for (i = 0; i < sims->num; i++) { char *sim = sims->sims[i], *name = getSimName(sims->sims[i]); if (!checkSimIsRunning(sim)) { I("%s is starting up.", name); memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s %s/%s new-window -dn '%s' -t '%s:%d' 'cd %s/current/bin; mono OpenSim.exe -inidirectory=%s/config/%s'", Tcmd, scRoot, Tsocket, name, Tconsole, i + 1, scRoot, scRoot, sim); int r = system(toybuf); if (!WIFEXITED(r)) E("tmux new-window command failed!"); else { memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name); waitTmuxText(name, toybuf); I("%s is done starting up.", name); la = waitLoadAverage(la, loadAverageInc, simTimeOut); } } } } else if (!strcmp(cmd, "create")) // "create name x,y size" { } else if (!strcmp(cmd, "start")) // "start sim01" "start Welcome" "start" start everything { } else if (!strcmp(cmd, "backup")) // "backup onefang rejected" "backup sim01" "backup Welcome" "backup" backup everything { // If it's not a sim code, and not a sim name, it's an account inventory. } else if (!strcmp(cmd, "gitAR")) // "gitAR i name" { } else if (!strcmp(cmd, "stop")) // "stop sim01" "stop Welcome" "stop" stop everything { } double sum; // Load the file containing the script we are going to run status = luaL_loadfile(L, "script.lua"); if (status) { // If something went wrong, error message is at the top of the stack E("Couldn't load file: %s", lua_tostring(L, -1)); goto finished; } /* * Ok, now here we go: We pass data to the lua script on the stack. * That is, we first have to prepare Lua's virtual stack the way we * want the script to receive it, then ask Lua to run it. */ lua_newtable(L); /* We will pass a table */ /* * To put values into the table, we first push the index, then the * value, and then call lua_rawset() with the index of the table in the * stack. Let's see why it's -3: In Lua, the value -1 always refers to * the top of the stack. When you create the table with lua_newtable(), * the table gets pushed into the top of the stack. When you push the * index and then the cell value, the stack looks like: * * <- [stack bottom] -- table, index, value [top] * * So the -1 will refer to the cell value, thus -3 is used to refer to * the table itself. Note that lua_rawset() pops the two last elements * of the stack, so that after it has been called, the table is at the * top of the stack. */ for (i = 1; i <= 5; i++) { lua_pushnumber(L, i); // Push the table index lua_pushnumber(L, i*2); // Push the cell value lua_rawset(L, -3); // Stores the pair in the table } // By what name is the script going to reference our table? lua_setglobal(L, "foo"); // Ask Lua to run our little script result = lua_pcall(L, 0, LUA_MULTRET, 0); if (result) { E("Failed to run script: %s", lua_tostring(L, -1)); goto finished; } // Get the returned value at the top of the stack (index -1) sum = lua_tonumber(L, -1); I("Script returned: %.0f", sum); lua_pop(L, 1); // Take the returned value out of the stack puts(""); fflush(stdout); finished: if (database) mysql_close(database); mysql_library_end(); lua_close(L); if (stats) { if (stats->stats) stats->stats->free(stats->stats); free(stats); } if (configs) configs->free(configs); return EXIT_SUCCESS; }