diff options
author | Tor Andersson <tor@ccxvii.net> | 2024-03-13 18:01:42 +0100 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2024-03-13 18:01:42 +0100 |
commit | 7b01c04086c8c689f0b40680464be69c3335f8f1 (patch) | |
tree | 6b0c7a46996672e099230ddc6c60b3e741ff0f84 | |
parent | a1e140a126a2a2121bb56230e87e00bb92cbd9a5 (diff) | |
download | server-7b01c04086c8c689f0b40680464be69c3335f8f1.tar.gz |
Hot reload modules without restarting the server.
NOTE: Does not update the list of dependencies when reloading, so
adding or removing require() calls in the rules will not be accurately
watched.
In these cases touching the rules.js file may be necessary to trigger
a reload.
-rw-r--r-- | nodemon.json | 10 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | server.js | 154 |
3 files changed, 116 insertions, 49 deletions
diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..3f2e7ed --- /dev/null +++ b/nodemon.json @@ -0,0 +1,10 @@ +{ + "delay": 2000, + "ext": "js,pug", + "ignore": [ + ".git", + "node_modules", + "public", + "tools" + ] +} diff --git a/package.json b/package.json index aa1885a..8a50d86 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "better-sqlite3": "^9.4.1", "bufferutil": "^4.0.8", + "chokidar": "^3.6.0", "dotenv": "^16.4.4", "express": "^4.18.2", "nodemailer": "^6.9.9", @@ -9,6 +9,7 @@ const https = require("https") // for webhook requests const { WebSocketServer } = require("ws") const express = require("express") const url = require("url") +const chokidar = require("chokidar") const sqlite3 = require("better-sqlite3") require("dotenv").config() @@ -270,6 +271,10 @@ function is_valid_email(email) { return REGEX_MAIL.test(email) } +function is_forbidden_mail(mail) { + return SQL_BLACKLIST_MAIL.get(mail) +} + function clean_user_name(name) { name = name.replace(/^ */, "").replace(/ *$/, "").replace(/ */g, " ") if (name.length > 50) @@ -338,10 +343,6 @@ const SQL_FIND_TOKEN = SQL("SELECT token FROM tokens WHERE user_id=? AND juliand const SQL_CREATE_TOKEN = SQL("INSERT OR REPLACE INTO tokens (user_id,token,time) VALUES (?, lower(hex(randomblob(16))), datetime()) RETURNING token").pluck() const SQL_VERIFY_TOKEN = SQL("SELECT EXISTS ( SELECT 1 FROM tokens WHERE user_id=? AND julianday('now') < julianday(time, '+20 minutes') AND token=? )").pluck() -function is_forbidden_mail(mail) { - return SQL_BLACKLIST_MAIL.get(mail) -} - app.use(function (req, res, next) { let ip = req.headers["x-real-ip"] || req.ip || req.connection.remoteAddress || "0.0.0.0" @@ -1076,12 +1077,12 @@ app.get("/forum/search", must_be_logged_in, function (req, res) { * GAME LOBBY */ +const SQL_SELECT_SETUPS = SQL("select * from setups where title_id=? order by setup_id") + let RULES = {} let TITLE_TABLE = app.locals.TITLE_TABLE = {} let TITLE_LIST = app.locals.TITLE_LIST = [] let TITLE_NAME = app.locals.TITLE_NAME = {} -let SETUP_LIST = app.locals.SETUP_LIST = [] -let SETUP_TABLE = app.locals.SETUP_TABLE = {} const STATUS_OPEN = 0 const STATUS_ACTIVE = 1 @@ -1095,40 +1096,6 @@ const PACE_SLOW = 3 const PACE_NAME = [ "Any", "Live", "Fast", "Slow" ] -function load_rules() { - const SQL_SELECT_TITLES = SQL("select * from titles") - const SQL_SELECT_SETUPS = SQL("select * from setups where title_id=? order by setup_id") - for (let title of SQL_SELECT_TITLES.all()) { - let title_id = title.title_id - if (fs.existsSync(__dirname + "/public/" + title_id + "/rules.js")) { - console.log("Loading rules for " + title_id) - try { - RULES[title_id] = require("./public/" + title_id + "/rules.js") - TITLE_LIST.push(title) - TITLE_TABLE[title_id] = title - TITLE_NAME[title_id] = title.title_name - title.setups = SQL_SELECT_SETUPS.all(title_id) - for (let setup of title.setups) { - if (!setup.setup_name) { - if (title.setups.length > 1 && setup.scenario !== "Standard") - setup.setup_name = title.title_name + " - " + setup.scenario - else - setup.setup_name = title.title_name - } - SETUP_LIST.push(setup) - SETUP_TABLE[setup.setup_id] = setup - } - title.about_html = fs.readFileSync("./public/" + title_id + "/about.html") - title.create_html = fs.readFileSync("./public/" + title_id + "/create.html") - } catch (err) { - console.log(err) - } - } else { - console.log("Cannot find rules for " + title_id) - } - } -} - const PARSE_OPTIONS_CACHE = {} const HUMAN_OPTIONS_CACHE = { @@ -1182,7 +1149,85 @@ function is_game_ready(player_count, players) { return true } -load_rules() +function unload_module(filename) { + // Remove a module and its dependencies from require.cache so they can be reloaded. + let mod = require.cache[filename] + if (mod) { + delete require.cache[filename] + for (let child of mod.children) + unload_module(child.filename) + } +} + +function load_rules(rules_dir, rules_file, title) { + RULES[title.title_id] = require(rules_file) + + title.setups = SQL_SELECT_SETUPS.all(title.title_id) + for (let setup of title.setups) { + if (!setup.setup_name) { + if (title.setups.length > 1 && setup.scenario !== "Standard") + setup.setup_name = title.title_name + " - " + setup.scenario + else + setup.setup_name = title.title_name + } + setup.roles = get_game_roles(setup.title_id, setup.scenario, parse_game_options(setup.options)) + } + + title.about_html = fs.readFileSync(rules_dir + "/about.html") + title.create_html = fs.readFileSync(rules_dir + "/create.html") +} + +function watch_rules(rules_dir, rules_file, title) { + let watch_list = [ rules_file ] + + let mod = require.cache[rules_file] + if (mod) { + for (let child of mod.children) + watch_list.push(child.filename) + } + + function reload_rules() { + try { + console.log("*** RELOAD", title.title_id, "***") + unload_module(rules_file) + load_rules(rules_dir, rules_file, title) + sync_client_state_for_title(title.title_id) + } catch (err) { + console.log(err) + } + } + + // ALSO: for (let file of watch_list) fs.watchFile(file, reload_rules) + chokidar.watch(watch_list, { ignoreInitial: true, awaitWriteFinish: true }).on("all", reload_rules) +} + +function load_titles() { + const SQL_SELECT_TITLES = SQL("select * from titles") + for (let title of SQL_SELECT_TITLES.all()) { + let title_id = title.title_id + let rules_dir = __dirname + "/public/" + title_id + let rules_file = rules_dir + "/rules.js" + + TITLE_LIST.push(title) + TITLE_TABLE[title_id] = title + TITLE_NAME[title_id] = title.title_name + + try { + if (fs.existsSync(rules_file)) { + console.log("Loading rules for " + title_id) + load_rules(rules_dir, rules_file, title) + } else { + console.log("Cannot find rules for " + title_id) + } + } catch (err) { + console.log(err) + } + + watch_rules(rules_dir, rules_file, title) + } +} + +load_titles() const SQL_INSERT_GAME = SQL("INSERT INTO games (owner_id,title_id,scenario,options,player_count,pace,is_private,is_random,notice,is_match) VALUES (?,?,?,?,?,?,?,?,?,?) returning game_id").pluck() const SQL_DELETE_GAME_BY_OWNER = SQL("delete from games where game_id=? and owner_id=?") @@ -2552,7 +2597,7 @@ function send_message(socket, cmd, arg) { function send_state(socket, state) { try { - let view = socket.rules.view(state, socket.role) + let view = RULES[socket.title_id].view(state, socket.role) if (socket.seen < view.log.length) view.log_start = socket.seen else @@ -2579,6 +2624,18 @@ function get_game_state(game_id) { return JSON.parse(game_state) } +function sync_client_state(game_id) { + for (let socket of game_clients[game_id]) + send_state(socket, get_game_state(socket.game_id)) +} + +function sync_client_state_for_title(title_id) { + for (let game_id in game_clients) + for (let socket of game_clients[game_id]) + if (socket.title_id === title_id) + send_state(socket, get_game_state(socket.game_id)) +} + function snap_from_state(state) { // return JSON of game state without undo and with log replaced by log length let save_undo = state.undo @@ -2671,7 +2728,7 @@ function on_action(socket, action, args, cookie) { if (old_active !== "Both") game_cookies[socket.game_id] ++ - state = socket.rules.action(state, socket.role, action, args) + state = RULES[socket.title_id].action(state, socket.role, action, args) put_new_state(socket.game_id, state, old_active, socket.role, action, args) } catch (err) { console.log(err) @@ -2754,9 +2811,9 @@ function on_save(socket) { function on_query(socket, q, params) { SLOG(socket, "QUERY", q, JSON.stringify(params)) try { - if (socket.rules.query) { + if (RULES[socket.title_id].query) { let state = get_game_state(socket.game_id) - let reply = socket.rules.query(state, socket.role, q, params) + let reply = RULES[socket.title_id].query(state, socket.role, q, params) send_message(socket, "reply", [ q, reply ]) } } catch (err) { @@ -2768,9 +2825,9 @@ function on_query(socket, q, params) { function on_query_snap(socket, snap_id, q, params) { SLOG(socket, "QUERYSNAP", snap_id, JSON.stringify(params)) try { - if (socket.rules.query) { + if (RULES[socket.title_id].query) { let state = JSON.parse(SQL_SELECT_SNAP_STATE.get(socket.game_id, snap_id)) - let reply = socket.rules.query(state, socket.role, q, params) + let reply = RULES[socket.title_id].query(state, socket.role, q, params) send_message(socket, "reply", [ q, reply ]) } } catch (err) { @@ -2853,7 +2910,7 @@ function on_snap(socket, snap_id) { let snap_state = SQL_SELECT_SNAP_STATE.get(socket.game_id, snap_id) if (snap_state) { let state = JSON.parse(snap_state) - let view = socket.rules.view(state, socket.role) + let view = RULES[socket.title_id].view(state, socket.role) view.prompt = undefined view.actions = undefined view.log = state.log @@ -2954,7 +3011,6 @@ wss.on("connection", (socket, req) => { socket.game_id = req.query.game | 0 socket.role = req.query.role socket.seen = req.query.seen | 0 - socket.rules = RULES[socket.title_id] SLOG(socket, "OPEN " + socket.seen) |