diff --git a/release_make/make.bat b/release_make/make.bat index c310b31a..18717c71 100644 --- a/release_make/make.bat +++ b/release_make/make.bat @@ -1,5 +1,6 @@ @echo off SETLOCAL ENABLEEXTENSIONS +SETLOCAL ENABLEDELAYEDEXPANSION set nodeversion=node18 cd /d "%~dp0" rd /s /q .\release-builds @@ -45,17 +46,17 @@ if not "%result%"=="" (goto Error) node make_index.js>.\index.js call :Make %nodeversion%-win-x64 .\release-builds\win\x64\FairyGround.exe -if "%errorlevel%"=="11" (goto Error) +if "%ERROR%"=="1" (goto Error) call :Make %nodeversion%-linux-x64 .\release-builds\linux\x64\FairyGround -if "%errorlevel%"=="11" (goto Error) +if "%ERROR%"=="1" (goto Error) call :Make %nodeversion%-win-arm64 .\release-builds\win\arm64\FairyGround.exe -if "%errorlevel%"=="11" (goto Error) +if "%ERROR%"=="1" (goto Error) call :Make %nodeversion%-linux-arm64 .\release-builds\linux\arm64\FairyGround -if "%errorlevel%"=="11" (goto Error) +if "%ERROR%"=="1" (goto Error) call :Make %nodeversion%-macos-x64 .\release-builds\macos\x64\FairyGround.app -if "%errorlevel%"=="11" (goto Error) +if "%ERROR%"=="1" (goto Error) call :Make %nodeversion%-macos-arm64 .\release-builds\macos\arm64\FairyGround.app -if "%errorlevel%"=="11" (goto Error) +if "%ERROR%"=="1" (goto Error) cd .. set result= @@ -95,13 +96,16 @@ pause exit /b 1 :Make +set ERROR=0 start /WAIT /MIN "" cmd.exe /C ^(npx pkg . --target %~1 --output %2 ^& exit ^) ^> %TEMP%\make_fairyground.log 2^>^&1 set result= FOR /F "usebackq" %%i IN (`findstr /L /I "Error" "%TEMP%\make_fairyground.log"`) DO set result=%%i if not "%result%"=="" ( echo Fail: Bytecode generation failed. Trying --no-bytecode... call :TryNoByteCode %~1 %2 - if "%errorlevel%"=="11" (exit /b 11) + if "!ERROR!"=="1" ( + exit /b 1 + ) echo Pass: %~1 exit /b 0 ) @@ -110,7 +114,9 @@ FOR /F "usebackq" %%i IN (`findstr /L /I "Failed to make bytecode" "%TEMP%\make_ if not "%result%"=="" ( echo Fail: Bytecode generation failed. Trying --no-bytecode... call :TryNoByteCode %~1 %2 - if "%errorlevel%"=="11" (exit /b 11) + if "!ERROR!"=="1" ( + exit /b 1 + ) echo Pass: %~1 exit /b 0 ) @@ -129,7 +135,8 @@ FOR /F "usebackq" %%i IN (`findstr /L /I "Error" "%TEMP%\make_fairyground.log"`) if not "%result%"=="" ( echo Error: Build failed. Check the log below to see what's going on. File: %TEMP%\make_fairyground.log type "%TEMP%\make_fairyground.log" - exit /b 11 + set ERROR=1 + exit /b 1 ) set result= FOR /F "usebackq" %%i IN (`findstr /L /I "Warning" "%TEMP%\make_fairyground.log"`) DO set result=%%i diff --git a/src/README.md b/src/README.md index 5056280a..8d51bcaf 100644 --- a/src/README.md +++ b/src/README.md @@ -15,6 +15,7 @@ The files in this directory are source files used for the build process of the p | `src\html\resources.html` | `public\resources.html` | The "Resources" page. | | `src\js\main.js` | `public\bundle.js` | The main JavaScript file attached to advanced.html which imports ffish.js & Chessground X, and contains all function of position variants (custom positions), board logic and notation system; most of interactive analysis, board setup and search move dialog; part of review mode. | | `src\js\server.js` | `\server.js` | The fairyground server or backend, which is the controller of binary engines and communicates with the webpage through WebSocket. | +| `src\js\server-parallel.js` | (none) | The parallel version of fairyground server or backend, which has the same function as `server.js` but supports load balance for HTTP. Not used by default, as it's only useful under high request frequency circumstances. | | `src\js\BinaryEngineFeature.js` | `public\lib\BinaryEngineFeature.js` | This script exports all functions related to binary engines to advanced.html, such as UCI/UCCI/UCI-Cyclone/USI protocol translation, binary engine control logic and engine management/setup/settings UI. It does not contain GUI logic. | | `src\js\SavedGamesParsingFeature.js` | `public\lib\SavedGamesParsingFeature.js` | This script exports all functions related to PGN/EPD parser, such as file processing and parser UI. It does not contain GUI logic. | | `src\js\*.canvas.worker.js` | `public\lib\canvas\*.canvas.worker.js` | These scripts are canvas workers that renders the animation in an individual thread at page background for UI theme. They receive a OffscreenCanvas from main page and draw things on them. Changes made to the canvas are committed automatically to main page. You can find which UI theme uses the corresponding script in `public\uithemes.txt`. | diff --git a/src/js/server-parallel.js b/src/js/server-parallel.js new file mode 100644 index 00000000..6bd69dab --- /dev/null +++ b/src/js/server-parallel.js @@ -0,0 +1,839 @@ +const fs = require("fs"); +const express = require("express"); +const cluster = require("cluster"); +const WebSocket = require("ws"); +const { spawn } = require("child_process"); +const os = require("os"); +const WebSocketServer = WebSocket.Server; +const PathSplitterMatcher = new RegExp("/", "g"); +const JSONSplitterMatcher = new RegExp("(?<=\\:|\\;|\\,)", "g"); +const OutputDirectory = "/public"; +let port = 5015; + +function GenerateErrorPage( + status, + message, + description, + method, + protocol, + host, + url, + query, + headers, + path, + reasons, + suggestion, +) { + return `Error

Error ${status} - ${message}

${description}

Request Details

Request method: ${method}

Request URL: ${protocol}://${host}${url}

Query string: ${query}

Request headers: ${headers}

Physical path: ${path}

Possible Reasons

${reasons}

Suggestions

${suggestion}

`; +} + +function wrapText(text, width, paddingLeft) { + let tokens = text.split(/[ ]+/); + let result = ""; + let i = 0; + let j = 0; + let linecharcount = 0; + for (i = 0; i < paddingLeft; i++) { + result += " "; + } + for (i = 0; i < tokens.length; i++) { + linecharcount += tokens[i].length + 1; + if (linecharcount > width - paddingLeft && i > 0) { + result += "\n"; + for (j = 0; j < paddingLeft; j++) { + result += " "; + } + linecharcount = tokens[i].length + 1; + } + result += tokens[i] + " "; + } + return result; +} + +if (cluster.isMaster) { + const args = process.argv.slice(2); + const consolewidth = process.stdout.columns; + + if ( + args.includes("--help") || + args.includes("-help") || + args.includes("-h") || + args.includes("/?") + ) { + console.log( + wrapText( + "Starts fairyground HTTP and WebSocket server.", + consolewidth, + 0, + ), + ); + console.log(""); + console.log(wrapText("Usage (Bash or CMD):", consolewidth, 0)); + console.log( + wrapText( + "node server-parallel.js [--number-workers ]", + consolewidth, + 0, + ), + ); + console.log(""); + console.log(wrapText("Options:", consolewidth, 0)); + console.log( + wrapText("--number-workers ", consolewidth, 0), + ); + console.log( + wrapText( + "Sets the count of workers for HTTP server. (Default: CPU thread count)", + consolewidth, + 10, + ), + ); + console.log(""); + console.log(wrapText("Examples:", consolewidth, 0)); + console.log(wrapText("node server-parallel.js", consolewidth, 0)); + console.log( + wrapText("node server-parallel.js --number-workers 1", consolewidth, 0), + ); + console.log( + wrapText("node server-parallel.js --number-workers 8", consolewidth, 0), + ); + console.log(""); + process.exit(0); + } + + let numworkers = 0; + + if (args.includes("--number-workers")) { + let num = parseInt(args[args.indexOf("--number-workers") + 1]); + if (num > 0) { + numworkers = num; + } else { + console.error( + "The count of HTTP server workers must be a positive integer.", + ); + process.exit(1); + } + } + + const NumberParallel = numworkers || os.availableParallelism(); + let i = 0; + console.log("============================================"); + console.log(" SECURITY WARNING"); + console.log("============================================"); + console.log( + "This backend does not check the input from the frontend. The client user can enter anything as the command, which can pose threat to your server security. Therefore you should only allow trusted users to connect.\n\n", + ); + + console.log("======================================="); + console.log(" FairyGround Server"); + console.log("======================================="); + console.log("This is the back end of FairyGround, acting as a server."); + console.log( + "To open FairyGround UI, open your browser and go to the following URL:", + ); + console.log(`http://localhost:${port}`); + console.log( + "Closing this window will close FairyGround. Pages opened in your browser won't work then.\n", + ); + console.log(`[HTTP Server] Master process (${process.pid}) is running.`); + console.log( + "[HTTP Server] Using cluster mode. Parallelism count:", + NumberParallel, + ); + + for (i = 0; i < NumberParallel; i++) { + cluster.fork(); + } + + cluster.on("exit", (worker, code, signal) => { + console.log( + `[HTTP Server] A worker process (${worker.process.pid}) exited unexpectedly (Code ${code}, Signal ${signal}). Restarting...`, + ); + cluster.fork(); + }); +} else { + const app = express(); + + app.use(function (req, res, next) { + if ( + req.method == "PUT" || + req.method == "DELETE" || + req.method == "PATCH" + ) { + res + .status(405) + .send( + GenerateErrorPage( + 405, + "Method Not Allowed", + `Not allowed method: ${req.method}. This server does not allow you to change objects on the server.`, + req.method, + req.protocol, + req.get("host"), + req.originalUrl, + JSON.stringify(req.query).replace(JSONSplitterMatcher, " "), + JSON.stringify(req.headers).replace(JSONSplitterMatcher, " "), + process.platform == "win32" + ? `${process.cwd()}${req.path.replace(PathSplitterMatcher, "\\")}` + : `${process.cwd()}${req.path}`, + "No possible reasons available.", + "No suggestions available.", + ), + ); + } else if (req.path == "" || req.path == "/") { + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + next(); + } else { + if (req.path.startsWith(OutputDirectory)) { + fs.stat("." + req.path, (err, stats) => { + if (err) { + res + .status(404) + .send( + GenerateErrorPage( + 404, + "Not Found", + "The object you requested is not found. It might have been moved, deleted or temporarily unavailable.", + req.method, + req.protocol, + req.get("host"), + req.originalUrl, + JSON.stringify(req.query).replace(JSONSplitterMatcher, " "), + JSON.stringify(req.headers).replace(JSONSplitterMatcher, " "), + process.platform == "win32" + ? `${process.cwd()}${req.path.replace(PathSplitterMatcher, "\\")}` + : `${process.cwd()}${req.path}`, + "1. The object you requested does not exist.\n2. Typo(s) in URL.\n3. Typo(s) in file or directory name on the server.", + "Make sure that the URL and the object name on the server are correct. Create the object on the server if it does not exist.", + ), + ); + } else { + if (stats.isFile()) { + fs.open("." + req.path, "r", (err, fd) => { + if (fd) { + fs.closeSync(fd); + } + if (err) { + res + .status(403) + .send( + GenerateErrorPage( + 403, + "Forbidden", + "Due to the file system access control settings or encryption of the object you requested on the server, you do not have permission to read this object.", + req.method, + req.protocol, + req.get("host"), + req.originalUrl, + JSON.stringify(req.query).replace( + JSONSplitterMatcher, + " ", + ), + JSON.stringify(req.headers).replace( + JSONSplitterMatcher, + " ", + ), + process.platform == "win32" + ? `${process.cwd()}${req.path.replace(PathSplitterMatcher, "\\")}` + : `${process.cwd()}${req.path}`, + "1. The user running this server does not have permission to read this object.\n2. This object is encrypted and the user running this server cannot read it.", + "Check the file or directory access control settings of this object on the server and make sure that the user running this server can read it.", + ), + ); + } else { + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + //res.setHeader("Cache-Control", "no-store,no-cache,must-revalidate,post-check=0,pre-check=0"); + next(); + } + }); + } else { + res + .status(400) + .send( + GenerateErrorPage( + 400, + "Bad Request", + "The object you requested is not a file. This server only accepts file transfer.", + req.method, + req.protocol, + req.get("host"), + req.originalUrl, + JSON.stringify(req.query).replace(JSONSplitterMatcher, " "), + JSON.stringify(req.headers).replace( + JSONSplitterMatcher, + " ", + ), + process.platform == "win32" + ? `${process.cwd()}${req.path.replace(PathSplitterMatcher, "\\")}` + : `${process.cwd()}${req.path}`, + "1. The object you requested is not a file.\n2. Typo(s) in URL.\n3. The name of the object has changed.", + "Make sure that the URL and the file name on the server are correct. Create the object on the server if it does not exist.", + ), + ); + } + } + }); + } else { + res + .status(400) + .send( + GenerateErrorPage( + 400, + "Bad Request", + "The object you requested is not in output directory.", + req.method, + req.protocol, + req.get("host"), + req.originalUrl, + JSON.stringify(req.query).replace(JSONSplitterMatcher, " "), + JSON.stringify(req.headers).replace(JSONSplitterMatcher, " "), + process.platform == "win32" + ? `${process.cwd()}${req.path.replace(PathSplitterMatcher, "\\")}` + : `${process.cwd()}${req.path}`, + `You are accessing an object out of output directory "${OutputDirectory}".`, + "Re-enter your URL.", + ), + ); + } + } + }); + + app.use(OutputDirectory, express.static(OutputDirectory.slice(1))); + + app.all("/", function (req, res) { + console.log("[HTTP Server Worker %d] Redirect to index page.", process.pid); + res.redirect(`${OutputDirectory}/index.html`); + }); + + const httpserver = app.listen(port, function () { + let host = httpserver.address().address; + let port = httpserver.address().port; + console.log( + "[HTTP Server Worker %d] Server is up at http://localhost:%s", + process.pid, + port, + ); + }); +} + +if (cluster.isMaster) { + const LoadEngineTimeout = 15000; + const EngineProtocols = ["UCI", "USI", "UCCI", "UCI_CYCLONE"]; + + function noop() {} + + function heartbeat() { + this.isAlive = true; + } + + const wss = new WebSocketServer({ port: port + 1 }, () => { + console.log( + "[WebSocket Server] Server is up at ws://localhost:%s", + port + 1, + ); + }); + + var ClientEngines = new Map(); + var ConnectingClients = new Set(); + var ConnectedClients = new Set(); + const PathLevelSeperatorMatcher = new RegExp("\\\\|/", ""); + const MessageSplitter = new RegExp("\x10", ""); + + function GetWorkingDirectoryFromExcutablePath(path) { + if (typeof path != "string") { + throw TypeError(); + } + let levels = path.split(PathLevelSeperatorMatcher); + levels = levels.slice(0, -1); + if (os.type == "Windows_NT") { + return levels.join("\\"); + } else { + return levels.join("/"); + } + } + + function ProtocolInitializationCommand(protocol) { + if (typeof protocol != "string") { + throw TypeError(); + } + if (protocol == "UCI" || protocol == "UCI_CYCLONE") { + return "uci"; + } else if (protocol == "USI") { + return "usi"; + } else if (protocol == "UCCI") { + return "ucci"; + } + return null; + } + + function ReadFile(filepath) { + let result = ""; + if (fs.existsSync(filepath)) { + result = fs.readFileSync(filepath, "utf-8").replace(/\r\n|\r/g, "\n"); + } else { + console.warn( + `[WebSocket Server] ${filepath} does not exist. Creating one...`, + ); + fs.writeFile(filepath, "", (writeErr) => { + if (writeErr) { + console.error("[WebSocket Server] Error writing file: ", writeErr); + return; + } + console.log("[WebSocket Server] File created!"); + }); + } + return result; + } + + function WriteFile(filepath, content) { + fs.writeFile(filepath, content, (writeErr) => { + if (writeErr) { + console.error("[WebSocket Server] Error writing file: ", writeErr); + return; + } + console.log("[WebSocket Server] File written!"); + }); + } + + class Engine { + constructor( + ID, + Command, + WorkingDirectory, + Protocol, + Options, + Color, + LoadTimeOut, + WebSocketConnection, + ) { + if ( + typeof ID != "string" || + typeof Command != "string" || + typeof WorkingDirectory != "string" || + typeof Protocol != "string" || + !Array.isArray(Options) || + typeof Color != "string" || + typeof LoadTimeOut != "number" || + !(WebSocketConnection instanceof WebSocket) + ) { + throw TypeError(); + } + if (WebSocketConnection.readyState != WebSocketConnection.OPEN) { + throw Error("WebSocket connection error"); + } + this.ID = ID; + this.Command = Command; + this.WorkingDirectory = WorkingDirectory; + if (WorkingDirectory == "") { + this.WorkingDirectory = GetWorkingDirectoryFromExcutablePath(Command); + } + this.Protocol = Protocol; + this.Options = Options; + this.Color = Color; + this.IsLoading = false; + this.IsLoaded = false; + this.Process = null; + this.WebSocketConnection = WebSocketConnection; + this.Status = "NOT_LOADED"; + this.LoadTimeOut = LoadTimeOut; + if (LoadTimeOut <= 0) { + this.LoadTimeOut = LoadEngineTimeout; + } + this.LoadFinishCallBack = undefined; + this.LoadFailureCallBack = undefined; + this.WebSocketOnMessageHandlerBinded = + this.WebSocketOnMessageHandler.bind(this); + this.WebSocketOnSocketInvalidHandlerBinded = + this.WebSocketOnSocketInvalidHandler.bind(this); + this.WebSocketConnection.addEventListener( + "message", + this.WebSocketOnMessageHandlerBinded, + ); + this.WebSocketConnection.addEventListener( + "close", + this.WebSocketOnSocketInvalidHandlerBinded, + ); + this.WebSocketConnection.addEventListener( + "error", + this.WebSocketOnSocketInvalidHandlerBinded, + ); + } + + destructor() { + this.IsUsing = false; + this.IsLoaded = false; + this.IsLoading = false; + this.Process.removeAllListeners(); + this.WebSocketConnection.removeEventListener( + "close", + this.WebSocketOnSocketInvalidHandlerBinded, + ); + this.WebSocketConnection.removeEventListener( + "message", + this.WebSocketOnMessageHandlerBinded, + ); + this.WebSocketConnection.removeEventListener( + "error", + this.WebSocketOnSocketInvalidHandlerBinded, + ); + if (os.type() == "Windows_NT") { + spawn("taskkill", ["/pid", this.Process.pid, "/f", "/t"]); + } else { + spawn("kill", ["-9", this.Process.pid]); + } + } + + WebSocketOnMessageHandler(event) { + this.OnMessageFromClient(event.data); + } + + WebSocketOnSocketInvalidHandler(event) { + this.IsUsing = false; + this.IsLoaded = false; + this.IsLoading = false; + } + + Load(LoadFinishCallBack, LoadFailureCallBack) { + if ( + typeof LoadFinishCallBack != "function" && + LoadFinishCallBack !== undefined + ) { + throw TypeError(); + } + if ( + typeof LoadFailureCallBack != "function" && + LoadFailureCallBack !== undefined + ) { + throw TypeError(); + } + if ( + this.WebSocketConnection.readyState != this.WebSocketConnection.OPEN + ) { + return false; + } + if (this.IsLoading) { + return; + } + this.IsLoading = true; + this.IsLoaded = false; + this.LoadFinishCallBack = LoadFinishCallBack; + this.LoadFailureCallBack = LoadFailureCallBack; + if (os.type() == "Windows_NT") { + this.Process = spawn("cmd.exe", ["/C", this.Command], { + cwd: this.WorkingDirectory, + }); + } else if (os.type() == "Darwin" || os.type() == "Linux") { + this.Process = spawn(this.Command, [], { cwd: this.WorkingDirectory }); + } else { + console.warn( + `Unknown OS type: ${os.type()}, default handler will be used.`, + ); + this.Process = spawn(this.Command, [], { cwd: this.WorkingDirectory }); + } + this.Process.on("error", (err) => { + console.error( + `[WebSocket Server] Failed to load ${this.Color} Engine (ID: ${this.ID}): `, + err, + ); + this.WebSocketConnection.send( + `ERROR\x10LOAD_ENGINE\x10${this.ID}\x10${this.Color}`, + ); + if (typeof this.LoadFailureCallBack == "function") { + this.LoadFailureCallBack(); + } + return; + }); + this.Process.on("close", (code) => { + if (wss.clients.has(this.WebSocketConnection)) { + if (this.Status == "EXITING") { + return; + } + if (this.IsLoading && this.Status == "TIMEOUT") { + console.error( + `[WebSocket Server] Engine ${this.Color} (ID: ${this.ID}) load timed out.`, + ); + this.WebSocketConnection.send( + `ERROR\x10ENGINE_TIMEOUT\x10${this.ID}\x10${this.Color}`, + ); + } else { + console.error( + `[WebSocket Server] Failed to load ${this.Color} Engine (ID: ${this.ID}): `, + code, + ); + this.WebSocketConnection.send( + `ERROR\x10LOAD_ENGINE\x10${this.ID}\x10${this.Color}`, + ); + } + this.Status = ""; + if (typeof this.LoadFailureCallBack == "function") { + this.LoadFailureCallBack(); + } + return; + } + }); + this.Process.stdout.on("data", (data) => { + this.WebSocketConnection.send( + `ENGINE_STDOUT\x10${this.ID}\x10${this.Color}\x10${data}`, + ); + }); + this.Process.stderr.on("data", (data) => { + this.WebSocketConnection.send( + `ENGINE_STDERR\x10${this.ID}\x10${this.Color}\x10${data}`, + ); + }); + console.log( + "[WebSocket Server] Engine ", + this.Command.split(PathLevelSeperatorMatcher).at(-1).trim(), + " loading for ", + this.Color, + `(ID:${this.ID}).`, + ); + console.log("[WebSocket Server] Engine protocol is " + this.Protocol); + this.Process.stdin.write( + `${ProtocolInitializationCommand(this.Protocol)}\n`, + ); + setTimeout(() => { + if (this.IsLoading) { + this.Status = "TIMEOUT"; + if (os.type() == "Windows_NT") { + spawn("taskkill", ["/pid", this.Process.pid, "/f", "/t"]); + } else { + spawn("kill", ["-9", this.Process.pid]); + } + } + }, this.LoadTimeOut); + } + + Exit() { + this.Status = "EXITING"; + if (this.Process.connected) { + this.Process.stdin.write("quit\n"); + } + this.IsLoaded = false; + this.IsLoading = false; + setTimeout(() => { + this.Status = "NOT_LOADED"; + }, 500); + } + + LoadFinish() { + this.IsLoading = false; + this.IsLoaded = true; + this.Status = "LOADED"; + if (typeof this.LoadFinishCallBack == "function") { + this.LoadFinishCallBack(); + } + } + + OnMessageFromClient(Message) { + if (typeof Message != "string") { + throw TypeError(); + } + let msg = Message.split(MessageSplitter); + if (msg[0] == "POST_MSG") { + if (msg.length != 4) { + console.warn( + "[WebSocket Server] Received bad data from client. Syntax: POST_MSG\x10\x10\x10.", + ); + return; + } + if (msg[1] == this.ID && msg[2] == this.Color) { + this.Process.stdin.write(`${msg[3]}\n`); + } + } else if (msg[0] == "ENGINE_READY") { + if (msg.length != 3) { + console.warn( + "[WebSocket Server] Received bad data from client. Syntax: ENGINE_READY\x10\x10.", + ); + return; + } + if (msg[1] == this.ID && msg[2] == this.Color) { + this.LoadFinish(); + } + } else if ( + msg[0] == "CHANGE_ID" && + msg[1] == this.ID && + msg[2] == this.Color + ) { + if (typeof msg[3] == "string") { + this.ID = msg[3]; + this.WebSocketConnection.send( + `ID_CHANGED\x10${this.ID}\x10${this.Color}`, + ); + } + } + } + } + + wss.on("connection", (ws, req) => { + console.log("[WebSocket Server] Received connection from client."); + ws.isAlive = true; + ws.on("pong", heartbeat); + ws.on("message", (message) => { + let msg = message.toString().split(MessageSplitter); + if (msg[0] == "CONNECT") { + if (ConnectingClients.has(ws) || ConnectedClients.has(ws)) { + console.warn( + "[WebSocket Server] Received connection request from connecting or connected client.", + ); + } + console.log( + `[WebSocket Server] Client connection verification code is ${msg[1]}.`, + ); + ws.send(msg[1], (err) => { + if (err) { + console.error(`[WebSocket Server] Error: `, err); + } + }); + ConnectingClients.add(ws); + } else if (msg[0] == "READYOK") { + if (ConnectingClients.has(ws) && !ConnectedClients.has(ws)) { + console.log(`[WebSocket Server] Client connected.`); + ConnectedClients.add(ws); + ConnectingClients.delete(ws); + ws.send("UPDATE_DATA"); + } else { + console.error( + "[WebSocket Server] Received connection ready mark from connected or unknown client.", + ); + } + } else { + if (!ConnectedClients.has(ws)) { + console.error( + `[WebSocket Server] Received bad data from client when connection established: ${message.toString()}`, + ); + ws.close(); + return; + } + } + if (msg[0] == "LOAD_ENGINE") { + if (msg.length != 7) { + console.warn( + "[WebSocket Server] Received bad data from client. Syntax: LOAD_ENGINE\x10\x10\x10\x10\x10\x10.", + ); + return; + } + if (!EngineProtocols.includes(msg[3])) { + console.warn( + "[WebSocket Server] Received bad engine protocol from client. Available protocols are:\n", + EngineProtocols, + ); + return; + } + if (ClientEngines.has(`${msg[1]}|${msg[2]}`)) { + console.warn( + `[WebSocket Server] Engine with ID: ${msg[1]} Color: ${msg[2]} already loaded. Reloading...`, + ); + let ClientEngine = ClientEngines.get(`${msg[1]}|${msg[2]}`); + ClientEngine.destructor(); + ClientEngines.delete(`${msg[1]}|${msg[2]}`); + } + let engineobj = new Engine( + msg[1], + msg[4], + msg[5], + msg[3], + [], + msg[2], + parseInt(msg[6]), + ws, + ); + ClientEngines.set(`${msg[1]}|${msg[2]}`, engineobj); + engineobj.Load( + () => { + console.log( + `[WebSocket Server] Engine ID: ${msg[1]} Color: ${msg[2]} loaded successfully.`, + ); + }, + () => { + console.error( + `[WebSocket Server] Error loading engine ID: ${msg[1]} Color: ${msg[2]}.`, + ); + }, + ); + } else if (msg[0] == "EXIT_ENGINE") { + if (msg.length != 3) { + console.warn( + "[WebSocket Server] Received bad data from client. Syntax: EXIT_ENGINE\x10\x10.", + ); + return; + } + if (ClientEngines.has(`${msg[1]}|${msg[2]}`)) { + let ClientEngine = ClientEngines.get(`${msg[1]}|${msg[2]}`); + console.log( + `[WebSocket Server] Exiting Engine ID: ${msg[1]} Color: ${msg[2]}`, + ); + ClientEngines.delete(`${msg[1]}|${msg[2]}`); + ClientEngine.Exit(); + ClientEngine.destructor(); + ClientEngine = undefined; + } else { + console.warn( + "[WebSocket Server] Client has not loaded engine: ", + `ID: ${msg[1]} Color: ${msg[2]}`, + ); + return; + } + } else if (msg[0] == "GET_ENGINE_LIST") { + let content = ReadFile("./EngineList.txt"); + ws.send(`ENGINE_LIST\x10${content}`); + } else if (msg[0] == "SAVE_ENGINE_LIST") { + if (msg.length != 2) { + console.warn( + "[WebSocket Server] Received bad data from client. Syntax: SAVE_ENGINE_LIST\x10.", + ); + return; + } + console.log(`[WebSocket Server] Saving engine list: ${msg[1]}`); + WriteFile("./EngineList.txt", msg[1]); + } else if (msg[0] == "CHANGE_ID") { + if (msg.length != 4) { + console.warn( + "[WebSocket Server] Received bad data from client. Syntax: CHANGE_ID\x10\x10\x10.", + ); + return; + } + if (ClientEngines.has(`${msg[1]}|${msg[2]}`)) { + let ClientEngine = ClientEngines.get(`${msg[1]}|${msg[2]}`); + ClientEngines.delete(`${msg[1]}|${msg[2]}`); + ClientEngines.set(`${msg[3]}|${msg[2]}`, ClientEngine); + } + } + }); + ws.on("close", (ws) => { + console.log(`[WebSocket Server] Client disconnected.`); + ClientEngines.forEach(function each(value, key) { + let ClientEngine = value; + if (ClientEngine.WebSocketConnection.readyState != WebSocket.OPEN) { + ClientEngine.destructor(); + ClientEngines.delete(key); + } + }); + ConnectedClients.forEach(function each(value, value2) { + if (!wss.clients.has(value)) { + ConnectedClients.delete(value); + } + }); + ConnectingClients.forEach(function each(value, value2) { + if (!wss.clients.has(value)) { + ConnectingClients.delete(value); + } + }); + }); + }); + + wss.on("close", (ws) => { + console.log(`[WebSocket Server] Shutting Down!`); + process.exit(); + }); + + const interval = setInterval(function ping() { + wss.clients.forEach(function each(ws) { + if (ws.isAlive === false) return ws.terminate(); + ws.isAlive = false; + ws.ping(noop); + }); + }, 30000); +}