diff --git a/app/components/Viewer/ObjectTree/Base/Controls.vue b/app/components/Viewer/ObjectTree/Base/Controls.vue
index 9c44dbd9..0fd087fd 100644
--- a/app/components/Viewer/ObjectTree/Base/Controls.vue
+++ b/app/components/Viewer/ObjectTree/Base/Controls.vue
@@ -99,22 +99,13 @@ watch(
-
diff --git a/app/utils/local/microservices.js b/app/utils/local/microservices.js
index 97867c16..88447e07 100644
--- a/app/utils/local/microservices.js
+++ b/app/utils/local/microservices.js
@@ -6,15 +6,14 @@ import path from "node:path";
// Third party imports
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json" with { type: "json" };
import { getPort } from "get-port-please";
-import pTimeout from "p-timeout";
// Local imports
import { commandExistsSync, waitForReady } from "./scripts.js";
import { microservicesMetadatasPath, projectMicroservices } from "./cleanup.js";
import { executablePath } from "./path.js";
-const DEFAULT_TIMEOUT_SECONDS = 120;
const MILLISECONDS_PER_SECOND = 1000;
+const DEFAULT_TIMEOUT_SECONDS = 30;
function getAvailablePort() {
return getPort({
@@ -23,6 +22,11 @@ function getAvailablePort() {
});
}
+function resolveCommand(execPath, execName) {
+ const command = commandExistsSync(execName) ? execName : executablePath(execPath, execName);
+ return command;
+}
+
async function runScript(
execPath,
execName,
@@ -30,33 +34,31 @@ 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 +75,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 +101,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 7e72333e..6fe8d730 100644
--- a/app/utils/local/scripts.js
+++ b/app/utils/local/scripts.js
@@ -3,24 +3,89 @@ 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) {
+ // oxlint-disable-next-line promise/avoid-new
+ 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);
+ }
+ }
+
+ function 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) {