From 28c28345bf5b1ae7218b74ceee443bf5deb0d8e4 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:14:07 +0100 Subject: [PATCH 1/3] refactor(clientonly): improve server parameter handling and error reporting --- clientonly/index.js | 153 ++++++++++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 61 deletions(-) diff --git a/clientonly/index.js b/clientonly/index.js index e1ad59c5de..e8813dae84 100644 --- a/clientonly/index.js +++ b/clientonly/index.js @@ -1,17 +1,23 @@ "use strict"; +const http = require("node:http"); +const https = require("node:https"); + // Use separate scope to prevent global scope pollution (function () { - const config = {}; /** * Helper function to get server address/hostname from either the commandline or env + * @returns {object} config object containing address, port, and tls properties */ - function getServerAddress () { + function getServerParameters () { + const config = {}; /** * Get command line parameters * Assumes that a cmdline parameter is defined with `--key [value]` + * + * example: ` node clientonly --address localhost --port 8080 --use-tls` * @param {string} key key to look for at the command line * @param {string} defaultValue value if no key is given at the command line * @returns {string} the value of the parameter @@ -23,12 +29,14 @@ } // Prefer command line arguments over environment variables - ["address", "port"].forEach((key) => { - config[key] = getCommandLineParameter(key, process.env[key.toUpperCase()]); - }); + config.address = getCommandLineParameter("address", process.env.ADDRESS); + const portValue = getCommandLineParameter("port", process.env.PORT); + config.port = portValue ? parseInt(portValue, 10) : undefined; // determine if "--use-tls"-flag was provided - config.tls = process.argv.indexOf("--use-tls") > 0; + config.tls = process.argv.includes("--use-tls"); + + return config; } /** @@ -40,7 +48,7 @@ // Return new pending promise return new Promise((resolve, reject) => { // Select http or https module, depending on requested url - const lib = url.startsWith("https") ? require("node:https") : require("node:http"); + const lib = url.startsWith("https") ? https : http; const request = lib.get(url, (response) => { let configData = ""; @@ -50,12 +58,16 @@ }); // Resolve promise at the end of the HTTP/HTTPS stream response.on("end", function () { - resolve(JSON.parse(configData)); + try { + resolve(JSON.parse(configData)); + } catch (parseError) { + reject(new Error(`Failed to parse server response as JSON: ${parseError.message}`)); + } }); }); request.on("error", function (error) { - reject(new Error(`Unable to read config from server (${url} (${error.message}`)); + reject(new Error(`Unable to read config from server (${url}) (${error.message})`)); }); }); } @@ -67,69 +79,88 @@ */ function fail (message, code = 1) { if (message !== undefined && typeof message === "string") { - console.log(message); + console.error(message); } else { - console.log("Usage: 'node clientonly --address 192.168.1.10 --port 8080 [--use-tls]'"); + console.error("Usage: 'node clientonly --address 192.168.1.10 --port 8080 [--use-tls]'"); } process.exit(code); } - getServerAddress(); + /** + * Starts the client by connecting to the server and launching the Electron application + * @param {object} config server configuration + * @param {string} prefix http or https prefix + * @async + */ + async function startClient (config, prefix) { + try { + const configReturn = await getServerConfig(`${prefix}${config.address}:${config.port}/config/`); + + // check environment for DISPLAY or WAYLAND_DISPLAY + const elecParams = ["js/electron.js"]; + if (process.env.WAYLAND_DISPLAY) { + console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`); + elecParams.push("--enable-features=UseOzonePlatform"); + elecParams.push("--ozone-platform=wayland"); + } else if (process.env.DISPLAY) { + console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`); + } else { + fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided."); + } + + // Pass along the server config via an environment variable + const env = { ...process.env }; + env.clientonly = true; + const options = { env: env }; + configReturn.address = config.address; + configReturn.port = config.port; + configReturn.tls = config.tls; + env.config = JSON.stringify(configReturn); + + // Spawn electron application + const electron = require("electron"); + const child = require("node:child_process").spawn(electron, elecParams, options); + + // Pipe all child process output to current stdout + child.stdout.on("data", function (buf) { + process.stdout.write(`Client: ${buf}`); + }); - (config.address && config.port) || fail(); - const prefix = config.tls ? "https://" : "http://"; + // Pipe all child process errors to current stderr + child.stderr.on("data", function (buf) { + process.stderr.write(`Client: ${buf}`); + }); + + child.on("error", function (err) { + process.stderr.write(`Client: ${err}`); + }); - // Only start the client if a non-local server was provided - if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) { - getServerConfig(`${prefix}${config.address}:${config.port}/config/`) - .then(function (configReturn) { - // check environment for DISPLAY or WAYLAND_DISPLAY - const elecParams = ["js/electron.js"]; - if (process.env.WAYLAND_DISPLAY) { - console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`); - elecParams.push("--enable-features=UseOzonePlatform"); - elecParams.push("--ozone-platform=wayland"); - } else if (process.env.DISPLAY) { - console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`); - } else { - fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided."); + child.on("close", (code) => { + if (code !== 0) { + fail(`There is something wrong. The clientonly process exited with code ${code}.`); } - // Pass along the server config via an environment variable - const env = Object.create(process.env); - env.clientonly = true; // set to pass to electron.js - const options = { env: env }; - configReturn.address = config.address; - configReturn.port = config.port; - configReturn.tls = config.tls; - env.config = JSON.stringify(configReturn); - - // Spawn electron application - const electron = require("electron"); - const child = require("node:child_process").spawn(electron, elecParams, options); - - // Pipe all child process output to current stdout - child.stdout.on("data", function (buf) { - process.stdout.write(`Client: ${buf}`); - }); + }); + } catch (reason) { + fail(`Unable to connect to server: (${reason})`); + } + } - // Pipe all child process errors to current stderr - child.stderr.on("data", function (buf) { - process.stderr.write(`Client: ${buf}`); - }); + const config = getServerParameters(); + const prefix = config.tls ? "https://" : "http://"; - child.on("error", function (err) { - process.stdout.write(`Client: ${err}`); - }); + // Validate port + if (config.port !== undefined && (isNaN(config.port) || config.port < 1 || config.port > 65535)) { + fail(`Invalid port number: ${config.port}. Port must be between 1 and 65535.`); + } - child.on("close", (code) => { - if (code !== 0) { - console.log(`There something wrong. The clientonly is not running code ${code}`); - } - }); - }) - .catch(function (reason) { - fail(`Unable to connect to server: (${reason})`); - }); + // Only start the client if a non-local server was provided and address/port are set + const LOCAL_ADDRESSES = ["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1"]; + if ( + config.address + && config.port + && !LOCAL_ADDRESSES.includes(config.address) + ) { + startClient(config, prefix); } else { fail(); } From dbc7ced787e6cd01b6761150c60742222079e490 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:10:29 +0100 Subject: [PATCH 2/3] test(clientonly): add e2e tests for clientonly - Test missing/incomplete parameters (address, port) - Test rejection of local addresses (localhost, 127.0.0.1, ::1, etc.) - Test port validation (0, negative, >65535, non-numeric) - Test --use-tls flag parsing - Test server /config/ endpoint availability --- tests/e2e/clientonly_spec.js | 179 +++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 tests/e2e/clientonly_spec.js diff --git a/tests/e2e/clientonly_spec.js b/tests/e2e/clientonly_spec.js new file mode 100644 index 0000000000..6df9a3c325 --- /dev/null +++ b/tests/e2e/clientonly_spec.js @@ -0,0 +1,179 @@ +const { spawnSync, spawn } = require("node:child_process"); + +const delay = (time) => { + return new Promise((resolve) => setTimeout(resolve, time)); +}; + +/** + * Run clientonly with given arguments and return result + * @param {string[]} args command line arguments + * @param {object} env environment variables to merge (replaces process.env) + * @returns {object} result with status and stderr + */ +const runClientOnly = (args = [], env = {}) => { + // Start with minimal env and merge provided env + const testEnv = { + PATH: process.env.PATH, + NODE_PATH: process.env.NODE_PATH, + ...env + }; + const result = spawnSync("node", ["clientonly/index.js", ...args], { + env: testEnv, + encoding: "utf-8", + timeout: 5000 + }); + return result; +}; + +describe("Clientonly parameter handling", () => { + + describe("Missing parameters", () => { + it("should fail without any parameters", () => { + const result = runClientOnly(); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Usage:"); + }); + + it("should fail with only address parameter", () => { + const result = runClientOnly(["--address", "192.168.1.10"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Usage:"); + }); + + it("should fail with only port parameter", () => { + const result = runClientOnly(["--port", "8080"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Usage:"); + }); + }); + + describe("Local address rejection", () => { + it("should fail with localhost address", () => { + const result = runClientOnly(["--address", "localhost", "--port", "8080"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Usage:"); + }); + + it("should fail with 127.0.0.1 address", () => { + const result = runClientOnly(["--address", "127.0.0.1", "--port", "8080"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Usage:"); + }); + + it("should fail with ::1 address", () => { + const result = runClientOnly(["--address", "::1", "--port", "8080"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Usage:"); + }); + + it("should fail with ::ffff:127.0.0.1 address", () => { + const result = runClientOnly(["--address", "::ffff:127.0.0.1", "--port", "8080"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Usage:"); + }); + }); + + describe("Port validation", () => { + it("should fail with port 0", () => { + const result = runClientOnly(["--address", "192.168.1.10", "--port", "0"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Invalid port number"); + }); + + it("should fail with negative port", () => { + const result = runClientOnly(["--address", "192.168.1.10", "--port", "-1"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Invalid port number"); + }); + + it("should fail with port above 65535", () => { + const result = runClientOnly(["--address", "192.168.1.10", "--port", "65536"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Invalid port number"); + }); + + it("should fail with non-numeric port", () => { + const result = runClientOnly(["--address", "192.168.1.10", "--port", "abc"]); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Invalid port number"); + }); + + it("should accept valid port 8080", () => { + const result = runClientOnly(["--address", "192.168.1.10", "--port", "8080"]); + // Should not fail on port validation (will fail on connection or display) + expect(result.stderr).not.toContain("Invalid port number"); + }); + + it("should accept valid port 1", () => { + const result = runClientOnly(["--address", "192.168.1.10", "--port", "1"]); + expect(result.stderr).not.toContain("Invalid port number"); + }); + + it("should accept valid port 65535", () => { + const result = runClientOnly(["--address", "192.168.1.10", "--port", "65535"]); + expect(result.stderr).not.toContain("Invalid port number"); + }); + }); + + describe("TLS flag parsing", () => { + // Note: These tests verify the flag is parsed, not the actual connection behavior + // Connection tests would timeout as they try to reach unreachable addresses + + it("should not fail on port validation when using --use-tls", () => { + // Verify --use-tls doesn't interfere with other parameter parsing + const result = runClientOnly(["--address", "192.168.1.10", "--port", "443", "--use-tls"]); + expect(result.stderr).not.toContain("Invalid port number"); + }); + + it("should accept --use-tls flag with valid parameters", () => { + const result = runClientOnly(["--address", "192.168.1.10", "--port", "443", "--use-tls"]); + // Should not fail on parameter parsing (will fail on connection or display) + expect(result.stderr).not.toContain("Usage:"); + }); + }); + + describe("Display environment check", () => { + it("should fail without DISPLAY or WAYLAND_DISPLAY when connecting to valid server", async () => { + // This test needs a running server to get past the connection phase + // Without DISPLAY, it should fail with display error + // For now, we just verify it fails (connection error comes first without server) + const result = runClientOnly(["--address", "192.168.1.10", "--port", "1"]); + // Either exits with code 1 or times out (null status means killed/timeout) + expect(result.status === 1 || result.status === null).toBe(true); + }); + }); +}); + +describe("Clientonly with running server", () => { + let serverProcess; + const testPort = 8081; + + beforeAll(async () => { + process.env.MM_CONFIG_FILE = "tests/configs/default.js"; + process.env.MM_PORT = testPort.toString(); + serverProcess = spawn("node", ["--run", "server"], { + env: process.env, + detached: true + }); + // Wait for server to start + await delay(2000); + }); + + afterAll(async () => { + if (serverProcess && serverProcess.pid) { + try { + process.kill(-serverProcess.pid); + } catch { + // Process may already be dead + } + } + }); + + it("should be able to fetch config from server", async () => { + const res = await fetch(`http://localhost:${testPort}/config/`); + expect(res.status).toBe(200); + const config = await res.json(); + expect(config).toBeDefined(); + expect(typeof config).toBe("object"); + }); +}); From 7bd7c0344a6d37f372dc30d56dd15cadea92d965 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:50:47 +0100 Subject: [PATCH 3/3] refactor(clientonly): remove unnecessary IIFE wrapper - Remove IIFE (Node.js modules are already scoped) - Extract getCommandLineParameter as top-level function - Add connection logging for better debugging --- clientonly/index.js | 302 ++++++++++++++++++++++---------------------- 1 file changed, 151 insertions(+), 151 deletions(-) diff --git a/clientonly/index.js b/clientonly/index.js index e8813dae84..bf6d6cdf87 100644 --- a/clientonly/index.js +++ b/clientonly/index.js @@ -3,165 +3,165 @@ const http = require("node:http"); const https = require("node:https"); -// Use separate scope to prevent global scope pollution -(function () { - - /** - * Helper function to get server address/hostname from either the commandline or env - * @returns {object} config object containing address, port, and tls properties - */ - function getServerParameters () { - const config = {}; - - /** - * Get command line parameters - * Assumes that a cmdline parameter is defined with `--key [value]` - * - * example: ` node clientonly --address localhost --port 8080 --use-tls` - * @param {string} key key to look for at the command line - * @param {string} defaultValue value if no key is given at the command line - * @returns {string} the value of the parameter - */ - function getCommandLineParameter (key, defaultValue = undefined) { - const index = process.argv.indexOf(`--${key}`); - const value = index > -1 ? process.argv[index + 1] : undefined; - return value !== undefined ? String(value) : defaultValue; - } - - // Prefer command line arguments over environment variables - config.address = getCommandLineParameter("address", process.env.ADDRESS); - const portValue = getCommandLineParameter("port", process.env.PORT); - config.port = portValue ? parseInt(portValue, 10) : undefined; - - // determine if "--use-tls"-flag was provided - config.tls = process.argv.includes("--use-tls"); - - return config; - } - - /** - * Gets the config from the specified server url - * @param {string} url location where the server is running. - * @returns {Promise} the config - */ - function getServerConfig (url) { - // Return new pending promise - return new Promise((resolve, reject) => { - // Select http or https module, depending on requested url - const lib = url.startsWith("https") ? https : http; - const request = lib.get(url, (response) => { - let configData = ""; - - // Gather incoming data - response.on("data", function (chunk) { - configData += chunk; - }); - // Resolve promise at the end of the HTTP/HTTPS stream - response.on("end", function () { - try { - resolve(JSON.parse(configData)); - } catch (parseError) { - reject(new Error(`Failed to parse server response as JSON: ${parseError.message}`)); - } - }); +/** + * Get command line parameters + * Assumes that a cmdline parameter is defined with `--key [value]` + * + * example: `node clientonly --address localhost --port 8080 --use-tls` + * @param {string} key key to look for at the command line + * @param {string} defaultValue value if no key is given at the command line + * @returns {string} the value of the parameter + */ +function getCommandLineParameter (key, defaultValue = undefined) { + const index = process.argv.indexOf(`--${key}`); + const value = index > -1 ? process.argv[index + 1] : undefined; + return value !== undefined ? String(value) : defaultValue; +} + +/** + * Helper function to get server address/hostname from either the commandline or env + * @returns {object} config object containing address, port, and tls properties + */ +function getServerParameters () { + const config = {}; + + // Prefer command line arguments over environment variables + config.address = getCommandLineParameter("address", process.env.ADDRESS); + const portValue = getCommandLineParameter("port", process.env.PORT); + config.port = portValue ? parseInt(portValue, 10) : undefined; + + // determine if "--use-tls"-flag was provided + config.tls = process.argv.includes("--use-tls"); + + return config; +} + +/** + * Gets the config from the specified server url + * @param {string} url location where the server is running. + * @returns {Promise} the config + */ +function getServerConfig (url) { + // Return new pending promise + return new Promise((resolve, reject) => { + // Select http or https module, depending on requested url + const lib = url.startsWith("https") ? https : http; + const request = lib.get(url, (response) => { + let configData = ""; + + // Gather incoming data + response.on("data", function (chunk) { + configData += chunk; }); - - request.on("error", function (error) { - reject(new Error(`Unable to read config from server (${url}) (${error.message})`)); + // Resolve promise at the end of the HTTP/HTTPS stream + response.on("end", function () { + try { + resolve(JSON.parse(configData)); + } catch (parseError) { + reject(new Error(`Failed to parse server response as JSON: ${parseError.message}`)); + } }); }); - } - /** - * Print a message to the console in case of errors - * @param {string} message error message to print - * @param {number} code error code for the exit call - */ - function fail (message, code = 1) { - if (message !== undefined && typeof message === "string") { - console.error(message); + request.on("error", function (error) { + reject(new Error(`Unable to read config from server (${url}) (${error.message})`)); + }); + }); +} + +/** + * Print a message to the console in case of errors + * @param {string} message error message to print + * @param {number} code error code for the exit call + */ +function fail (message, code = 1) { + if (message !== undefined && typeof message === "string") { + console.error(message); + } else { + console.error("Usage: 'node clientonly --address 192.168.1.10 --port 8080 [--use-tls]'"); + } + process.exit(code); +} + +/** + * Starts the client by connecting to the server and launching the Electron application + * @param {object} config server configuration + * @param {string} prefix http or https prefix + * @async + */ +async function startClient (config, prefix) { + try { + const serverUrl = `${prefix}${config.address}:${config.port}/config/`; + console.log(`Client: Connecting to server at ${serverUrl}`); + const configReturn = await getServerConfig(serverUrl); + console.log("Client: Successfully retrieved config from server"); + + // check environment for DISPLAY or WAYLAND_DISPLAY + const elecParams = ["js/electron.js"]; + if (process.env.WAYLAND_DISPLAY) { + console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`); + elecParams.push("--enable-features=UseOzonePlatform"); + elecParams.push("--ozone-platform=wayland"); + } else if (process.env.DISPLAY) { + console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`); } else { - console.error("Usage: 'node clientonly --address 192.168.1.10 --port 8080 [--use-tls]'"); + fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided."); } - process.exit(code); - } - - /** - * Starts the client by connecting to the server and launching the Electron application - * @param {object} config server configuration - * @param {string} prefix http or https prefix - * @async - */ - async function startClient (config, prefix) { - try { - const configReturn = await getServerConfig(`${prefix}${config.address}:${config.port}/config/`); - - // check environment for DISPLAY or WAYLAND_DISPLAY - const elecParams = ["js/electron.js"]; - if (process.env.WAYLAND_DISPLAY) { - console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`); - elecParams.push("--enable-features=UseOzonePlatform"); - elecParams.push("--ozone-platform=wayland"); - } else if (process.env.DISPLAY) { - console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`); - } else { - fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided."); - } - - // Pass along the server config via an environment variable - const env = { ...process.env }; - env.clientonly = true; - const options = { env: env }; - configReturn.address = config.address; - configReturn.port = config.port; - configReturn.tls = config.tls; - env.config = JSON.stringify(configReturn); - - // Spawn electron application - const electron = require("electron"); - const child = require("node:child_process").spawn(electron, elecParams, options); - - // Pipe all child process output to current stdout - child.stdout.on("data", function (buf) { - process.stdout.write(`Client: ${buf}`); - }); - - // Pipe all child process errors to current stderr - child.stderr.on("data", function (buf) { - process.stderr.write(`Client: ${buf}`); - }); - - child.on("error", function (err) { - process.stderr.write(`Client: ${err}`); - }); - child.on("close", (code) => { - if (code !== 0) { - fail(`There is something wrong. The clientonly process exited with code ${code}.`); - } - }); - } catch (reason) { - fail(`Unable to connect to server: (${reason})`); - } - } + // Pass along the server config via an environment variable + const env = { ...process.env }; + env.clientonly = true; + const options = { env: env }; + configReturn.address = config.address; + configReturn.port = config.port; + configReturn.tls = config.tls; + env.config = JSON.stringify(configReturn); + + // Spawn electron application + const electron = require("electron"); + const child = require("node:child_process").spawn(electron, elecParams, options); + + // Pipe all child process output to current stdout + child.stdout.on("data", function (buf) { + process.stdout.write(`Client: ${buf}`); + }); - const config = getServerParameters(); - const prefix = config.tls ? "https://" : "http://"; + // Pipe all child process errors to current stderr + child.stderr.on("data", function (buf) { + process.stderr.write(`Client: ${buf}`); + }); - // Validate port - if (config.port !== undefined && (isNaN(config.port) || config.port < 1 || config.port > 65535)) { - fail(`Invalid port number: ${config.port}. Port must be between 1 and 65535.`); - } + child.on("error", function (err) { + process.stderr.write(`Client: ${err}`); + }); - // Only start the client if a non-local server was provided and address/port are set - const LOCAL_ADDRESSES = ["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1"]; - if ( - config.address - && config.port - && !LOCAL_ADDRESSES.includes(config.address) - ) { - startClient(config, prefix); - } else { - fail(); + child.on("close", (code) => { + if (code !== 0) { + fail(`There is something wrong. The clientonly process exited with code ${code}.`); + } + }); + } catch (reason) { + fail(`Unable to connect to server: (${reason})`); } -}()); +} + +// Main execution +const config = getServerParameters(); +const prefix = config.tls ? "https://" : "http://"; + +// Validate port +if (config.port !== undefined && (isNaN(config.port) || config.port < 1 || config.port > 65535)) { + fail(`Invalid port number: ${config.port}. Port must be between 1 and 65535.`); +} + +// Only start the client if a non-local server was provided and address/port are set +const LOCAL_ADDRESSES = ["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1"]; +if ( + config.address + && config.port + && !LOCAL_ADDRESSES.includes(config.address) +) { + startClient(config, prefix); +} else { + fail(); +}