diff --git a/app/utils/local/microservices.js b/app/utils/local/microservices.js index f5318311..09b92d5b 100644 --- a/app/utils/local/microservices.js +++ b/app/utils/local/microservices.js @@ -13,8 +13,11 @@ import { commandExistsSync, waitForReady } from "./scripts.js"; import { microservicesMetadatasPath, projectMicroservices } from "./cleanup.js"; import { executablePath } from "./path.js"; -const DEFAULT_TIMEOUT_SECONDS = 60; const MILLISECONDS_PER_SECOND = 1000; +const DEFAULT_TIMEOUT_SECONDS = 30; +const BYTES_PER_KIBIBYTE = 1024; +const MAX_ERROR_BUFFER_KIBIBYTES = 64; +const MAX_ERROR_BUFFER_BYTES = MAX_ERROR_BUFFER_KIBIBYTES * BYTES_PER_KIBIBYTE; function getAvailablePort() { return getPort({ @@ -23,6 +26,11 @@ function getAvailablePort() { }); } +function resolveCommand(execPath, execName) { + const command = commandExistsSync(execName) ? execName : executablePath(execPath, execName); + return command; +} + async function runScript( execPath, execName, @@ -30,33 +38,29 @@ async function runScript( expectedResponse, timeoutSeconds = DEFAULT_TIMEOUT_SECONDS, ) { - let command = ""; - if (commandExistsSync(execName)) { - command = execName; - } else { - command = path.join(executablePath(execPath, execName)); - } + const command = resolveCommand(execPath, execName); console.log("runScript", command, args); - const child = child_process.spawn(process.platform === "win32" ? command : `"${command}"`, args, { - encoding: "utf8", - shell: true, + const child = child_process.spawn(command, args, { + stdio: ["ignore", "pipe", "pipe"], }); - child.stdout.on("data", (data) => console.log(`[${execName}] ${data.toString()}`)); - child.stderr.on("data", (data) => console.log(`[${execName}] ${data.toString()}`)); - child.on("close", (code) => console.log(`[${execName}] exited with code ${code}`)); - child.on("kill", () => { - console.log(`[${execName}] process killed`); - }); child.name = command.replace(/^.*[\\/]/u, ""); + child.on("spawn", () => { + console.log(`[${child.name}] spawned, pid=${child.pid}`); + }); + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutSeconds * MILLISECONDS_PER_SECOND); + if (typeof timer.unref === "function") timer.unref(); + try { - return await pTimeout(waitForReady(child, expectedResponse), { - milliseconds: timeoutSeconds * MILLISECONDS_PER_SECOND, - message: `Timed out after ${timeoutSeconds} seconds`, - }); + const result = await waitForReady(child, expectedResponse, controller.signal); + clearTimeout(timer); + return result; } catch (error) { + clearTimeout(timer); child.kill(); throw error; } @@ -73,11 +77,16 @@ async function runBack(execName, execPath, args = {}) { } const port = await getAvailablePort(); const backArgs = [ - `--port ${port}`, - `--data_folder_path ${projectFolderPath}`, - `--upload_folder_path ${uploadFolderPath}`, - `--allowed_origin http://localhost:*`, - `--timeout ${0}`, + "--port", + String(port), + "--data_folder_path", + projectFolderPath, + "--upload_folder_path", + uploadFolderPath, + "--allowed_origin", + "http://localhost:*", + "--timeout", + "0", ]; if (process.env.NODE_ENV === "development" || !process.env.NODE_ENV) { backArgs.push("--debug"); @@ -94,9 +103,12 @@ async function runViewer(execName, execPath, args = {}) { } const port = await getAvailablePort(); const viewerArgs = [ - `--port ${port}`, - `--data_folder_path ${projectFolderPath}`, - `--timeout ${0}`, + "--port", + String(port), + "--data_folder_path", + projectFolderPath, + "--timeout", + "0", ]; console.log("runViewer", execPath, execName, viewerArgs); await runScript(execPath, execName, viewerArgs, "Starting factory"); diff --git a/app/utils/local/scripts.js b/app/utils/local/scripts.js index 954e5cf0..7933f97f 100644 --- a/app/utils/local/scripts.js +++ b/app/utils/local/scripts.js @@ -1,26 +1,88 @@ // Node imports -import child_process from "node:child_process"; import fs from "node:fs"; import { on } from "node:events"; import path from "node:path"; +import readline from "node:readline"; import { appMode } from "./app_mode.js"; +const BYTES_PER_KIBIBYTE = 1024; +const MAX_ERROR_BUFFER_KIBIBYTES = 64; +const MAX_ERROR_BUFFER_BYTES = MAX_ERROR_BUFFER_KIBIBYTES * BYTES_PER_KIBIBYTE; + function commandExistsSync(execName) { const envPath = process.env.PATH || ""; - return envPath.split(path.delimiter).some((dir) => { - const filePath = path.join(dir, execName); + return envPath.split(path.delimiter).some((directory) => { + const filePath = path.join(directory, execName); return fs.existsSync(filePath) && fs.statSync(filePath).isFile(); }); } -async function waitForReady(child, expectedResponse) { - for await (const [data] of on(child.stdout, "data")) { - if (data.toString().includes(expectedResponse)) { - return child; +function waitForReady(child, expectedResponse, signal) { + return new Promise((resolve, reject) => { + const readlineStdout = readline.createInterface({ input: child.stdout }); + const readlineStderr = readline.createInterface({ input: child.stderr }); + + let recentOutput = ""; + function recordOutput(line) { + recentOutput = (recentOutput + line + "\n").slice(-MAX_ERROR_BUFFER_BYTES); } - } - throw new Error("Process closed before signal"); + + function cleanup() { + readlineStdout.removeAllListeners(); + readlineStdout.close(); + readlineStderr.removeAllListeners(); + readlineStderr.close(); + child.removeListener("error", onError); + child.removeListener("close", onClose); + if (signal) { + signal.removeEventListener("abort", onAbort); + } + } + + const onLine = (line) => { + console.log(`[${child.name}] ${line}`); + recordOutput(line); + if (line.includes(expectedResponse)) { + cleanup(); + resolve(child); + } + }; + + function onErrLine(line) { + console.log(`[${child.name}] ${line}`); + recordOutput(line); + } + + function onError(err) { + cleanup(); + reject(err); + } + + function onClose(code) { + console.log(`[${child.name}] exited with code ${code}`); + cleanup(); + reject( + new Error( + `[${child.name}] exited with code ${code} before becoming ready.` + + (recentOutput ? `\nRecent output:\n${recentOutput}` : ""), + ), + ); + } + + function onAbort() { + cleanup(); + reject(new Error(`[${child.name}] timed out waiting for "${expectedResponse}"`)); + } + + readlineStdout.on("line", onLine); + readlineStderr.on("line", onErrLine); + child.once("error", onError); + child.once("close", onClose); + if (signal) { + signal.addEventListener("abort", onAbort, { once: true }); + } + }); } async function waitNuxt(nuxtProcess) {