Skip to content
68 changes: 40 additions & 28 deletions app/utils/local/microservices.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
// 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";

Check failure on line 9 in app/utils/local/microservices.js

View workflow job for this annotation

GitHub Actions / test / oxlint

eslint(no-unused-vars)

Identifier 'pTimeout' is imported but never used.

// Local imports
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;

Check failure on line 20 in app/utils/local/microservices.js

View workflow job for this annotation

GitHub Actions / test / oxlint

eslint(no-unused-vars)

Variable 'MAX_ERROR_BUFFER_BYTES' is declared but never used. Unused variables should start with a '_'.

function getAvailablePort() {
return getPort({
Expand All @@ -23,40 +26,41 @@
});
}

function resolveCommand(execPath, execName) {
const command = commandExistsSync(execName) ? execName : executablePath(execPath, execName);
return command;
}

async function runScript(
execPath,
execName,
args,
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();

Check failure on line 56 in app/utils/local/microservices.js

View workflow job for this annotation

GitHub Actions / test / oxlint

eslint(curly)

Expected { after 'if' condition.

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;
}
Expand All @@ -73,11 +77,16 @@
}
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");
Expand All @@ -94,9 +103,12 @@
}
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");
Expand Down
80 changes: 71 additions & 9 deletions app/utils/local/scripts.js
Original file line number Diff line number Diff line change
@@ -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);

Check failure on line 28 in app/utils/local/scripts.js

View workflow job for this annotation

GitHub Actions / test / oxlint

eslint(prefer-template)

Unexpected string concatenation.
}
}
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);
}
};

Check failure on line 50 in app/utils/local/scripts.js

View workflow job for this annotation

GitHub Actions / test / oxlint

eslint(func-style)

Expected a function declaration.

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}` : ""),

Check failure on line 68 in app/utils/local/scripts.js

View workflow job for this annotation

GitHub Actions / test / oxlint

eslint(prefer-template)

Unexpected string concatenation.
),
);
}

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 });
}
});

Check failure on line 85 in app/utils/local/scripts.js

View workflow job for this annotation

GitHub Actions / test / oxlint

promise(avoid-new)

Avoid creating new promises
}

async function waitNuxt(nuxtProcess) {
Expand Down
Loading