mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* Fix macOS CLI daemon relaunch path * test(cli): mock helper existence in daemon launch test * fix(desktop): launch packaged CLI through Helper The packaged macOS CLI shim entered through the main app executable, so daemon supervision inherited app lifecycle behavior and surfaced Dock icons. Make Helper the required macOS CLI runtime, keep daemon relaunches on process.execPath, and cover cold bundled CLI daemon starts in release smoke. --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
722 lines
20 KiB
JavaScript
722 lines
20 KiB
JavaScript
const { spawn, spawnSync } = require("node:child_process");
|
|
const fs = require("node:fs");
|
|
const net = require("node:net");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { setTimeout: delay } = require("node:timers/promises");
|
|
|
|
const EXECUTABLE_NAME = "Paseo";
|
|
const SMOKE_TIMEOUT_MS = 60_000;
|
|
const EXIT_TIMEOUT_MS = 10_000;
|
|
const TERMINAL_CAPTURE_ATTEMPTS = 20;
|
|
const TERMINAL_CAPTURE_INTERVAL_MS = 500;
|
|
|
|
function createTempDir(prefix) {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
}
|
|
|
|
function assertExecutable(filePath, label) {
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new Error(`${label} does not exist: ${filePath}`);
|
|
}
|
|
if (process.platform !== "win32") {
|
|
fs.accessSync(filePath, fs.constants.X_OK);
|
|
}
|
|
}
|
|
|
|
function getExecutablePath(appPath) {
|
|
if (process.platform === "darwin") {
|
|
return path.join(appPath, "Contents", "MacOS", EXECUTABLE_NAME);
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
return path.join(appPath, `${EXECUTABLE_NAME}.exe`);
|
|
}
|
|
|
|
return path.join(appPath, EXECUTABLE_NAME);
|
|
}
|
|
|
|
function getCliShimPath(appPath) {
|
|
if (process.platform === "darwin") {
|
|
return path.join(appPath, "Contents", "Resources", "bin", "paseo");
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
return path.join(appPath, "resources", "bin", "paseo.cmd");
|
|
}
|
|
|
|
return path.join(appPath, "resources", "bin", "paseo");
|
|
}
|
|
|
|
function getMacMainExecutablePath(appPath) {
|
|
return path.join(appPath, "Contents", "MacOS", EXECUTABLE_NAME);
|
|
}
|
|
|
|
function getLaunchCommand(executablePath) {
|
|
if (process.platform !== "linux") {
|
|
return {
|
|
command: executablePath,
|
|
args: [],
|
|
};
|
|
}
|
|
|
|
return {
|
|
command: "xvfb-run",
|
|
args: ["-a", executablePath],
|
|
};
|
|
}
|
|
|
|
function shellQuote(value) {
|
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
}
|
|
|
|
function shellQuoteCliArg(value) {
|
|
if (process.platform === "win32") {
|
|
return `"${String(value).replace(/"/g, '""')}"`;
|
|
}
|
|
|
|
return shellQuote(String(value));
|
|
}
|
|
|
|
function getShellCommand(script) {
|
|
if (process.platform === "win32") {
|
|
return {
|
|
command: "cmd.exe",
|
|
args: ["/d", "/c", script],
|
|
};
|
|
}
|
|
|
|
return {
|
|
command: "/bin/sh",
|
|
args: ["-lc", script],
|
|
};
|
|
}
|
|
|
|
function createDefaultDaemonEnv(extraEnv) {
|
|
const env = {
|
|
...process.env,
|
|
...extraEnv,
|
|
};
|
|
|
|
delete env.PASEO_HOME;
|
|
delete env.PASEO_LISTEN;
|
|
return env;
|
|
}
|
|
|
|
function reserveLocalTcpPort() {
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.unref();
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", () => {
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") {
|
|
server.close(() => reject(new Error("Failed to reserve a TCP port for smoke test")));
|
|
return;
|
|
}
|
|
const { port } = address;
|
|
server.close((error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve(port);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function waitForFile(filePath, label) {
|
|
const deadline = Date.now() + SMOKE_TIMEOUT_MS;
|
|
while (Date.now() < deadline) {
|
|
if (fs.existsSync(filePath)) {
|
|
return fs.readFileSync(filePath, "utf8");
|
|
}
|
|
await delay(250);
|
|
}
|
|
throw new Error(`Timed out waiting for ${label}: ${filePath}`);
|
|
}
|
|
|
|
async function waitForChildPids(parentPid) {
|
|
const deadline = Date.now() + 10_000;
|
|
while (Date.now() < deadline) {
|
|
const childPids = listChildPids(parentPid);
|
|
if (childPids.length > 0) {
|
|
return childPids;
|
|
}
|
|
await delay(250);
|
|
}
|
|
throw new Error(`Timed out waiting for daemon worker child of supervisor PID ${parentPid}`);
|
|
}
|
|
|
|
function listChildPids(parentPid) {
|
|
const result = spawnSync("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" });
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
if (result.status !== 0) {
|
|
throw new Error(`ps failed while listing child processes: ${result.stderr.trim()}`);
|
|
}
|
|
|
|
const children = [];
|
|
for (const line of result.stdout.split(/\r?\n/)) {
|
|
const [pidText, ppidText] = line.trim().split(/\s+/);
|
|
const pid = Number(pidText);
|
|
const ppid = Number(ppidText);
|
|
if (Number.isInteger(pid) && ppid === parentPid) {
|
|
children.push(pid);
|
|
}
|
|
}
|
|
return children;
|
|
}
|
|
|
|
function listDarwinTextExecutables(pid) {
|
|
const result = spawnSync("lsof", ["-a", "-p", String(pid), "-d", "txt", "-Fn"], {
|
|
encoding: "utf8",
|
|
});
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
if (result.status !== 0) {
|
|
throw new Error(
|
|
`lsof failed while inspecting PID ${pid}: ${
|
|
result.stderr.trim() || result.stdout.trim() || "<empty>"
|
|
}`,
|
|
);
|
|
}
|
|
|
|
return result.stdout
|
|
.split(/\r?\n/)
|
|
.filter((line) => line.startsWith("n"))
|
|
.map((line) => line.slice(1));
|
|
}
|
|
|
|
function assertDarwinProcessDoesNotUseMainAppExecutable({ appPath, pid, label }) {
|
|
const mainExecutablePath = getMacMainExecutablePath(appPath);
|
|
assertExecutable(mainExecutablePath, "Packaged app executable");
|
|
|
|
const textExecutables = listDarwinTextExecutables(pid);
|
|
if (textExecutables.includes(mainExecutablePath)) {
|
|
throw new Error(
|
|
`${label} PID ${pid} launched through the main app executable ${mainExecutablePath}.\nText executables:\n${textExecutables.join(
|
|
"\n",
|
|
)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function parseSmokeLine(line) {
|
|
const prefix = "[paseo-smoke] ";
|
|
if (!line.startsWith(prefix)) {
|
|
return null;
|
|
}
|
|
return JSON.parse(line.slice(prefix.length));
|
|
}
|
|
|
|
function appendChunk(lines, chunk, onSmokeMessage) {
|
|
const text = chunk.toString();
|
|
lines.push(text);
|
|
for (const line of text.split(/\r?\n/)) {
|
|
const parsed = parseSmokeLine(line.trim());
|
|
if (parsed) {
|
|
onSmokeMessage(parsed);
|
|
}
|
|
}
|
|
}
|
|
|
|
function readIfExists(filePath) {
|
|
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : null;
|
|
}
|
|
|
|
function formatLogs({ stdout, stderr, userData }) {
|
|
const desktopLog = readIfExists(path.join(userData, "logs", "main.log"));
|
|
const daemonLog = readIfExists(path.join(os.homedir(), ".paseo", "daemon.log"));
|
|
return [
|
|
`App stdout:\n${stdout.join("").trim() || "<empty>"}`,
|
|
`App stderr:\n${stderr.join("").trim() || "<empty>"}`,
|
|
`Desktop log:\n${desktopLog?.trim() || "<missing>"}`,
|
|
`Daemon log:\n${daemonLog?.trim() || "<missing>"}`,
|
|
].join("\n\n");
|
|
}
|
|
|
|
function isRunning(child) {
|
|
return child.exitCode === null && child.signalCode === null;
|
|
}
|
|
|
|
function terminateChild(child, signal = "SIGTERM") {
|
|
if (!isRunning(child)) {
|
|
return;
|
|
}
|
|
|
|
if (process.platform === "win32" && child.pid) {
|
|
spawnSync("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
|
|
return;
|
|
}
|
|
|
|
if (child.pid) {
|
|
try {
|
|
process.kill(-child.pid, signal);
|
|
return;
|
|
} catch {}
|
|
}
|
|
|
|
child.kill(signal);
|
|
}
|
|
|
|
function waitForChildExit(child, timeoutMs = EXIT_TIMEOUT_MS) {
|
|
if (!isRunning(child)) {
|
|
return Promise.resolve(true);
|
|
}
|
|
|
|
let onExit;
|
|
const exitPromise = new Promise((resolve) => {
|
|
onExit = () => resolve(true);
|
|
child.once("exit", onExit);
|
|
});
|
|
|
|
return Promise.race([exitPromise, delay(timeoutMs, false)]).finally(() => {
|
|
child.off("exit", onExit);
|
|
});
|
|
}
|
|
|
|
function releaseChildHandles(child) {
|
|
child.stdin?.destroy();
|
|
child.stdout?.destroy();
|
|
child.stderr?.destroy();
|
|
child.unref();
|
|
}
|
|
|
|
async function removeTempDir(tempDir) {
|
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
try {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
return;
|
|
} catch (error) {
|
|
if (!["EBUSY", "ENOTEMPTY", "EPERM"].includes(error?.code) || attempt === 4) {
|
|
console.warn(`Packaged desktop smoke: failed to remove temp dir ${tempDir}: ${error}`);
|
|
return;
|
|
}
|
|
await delay(250);
|
|
}
|
|
}
|
|
}
|
|
|
|
function waitForSmokeMessage({ child, stdout, stderr, userData, type, validate }) {
|
|
return new Promise((resolve, reject) => {
|
|
let settled = false;
|
|
|
|
const finish = (callback, value) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
callback(value);
|
|
};
|
|
|
|
const timer = setTimeout(() => {
|
|
terminateChild(child);
|
|
finish(
|
|
reject,
|
|
new Error(
|
|
`Timed out waiting for packaged desktop smoke result.\n${formatLogs({
|
|
stdout,
|
|
stderr,
|
|
userData,
|
|
})}`,
|
|
),
|
|
);
|
|
}, SMOKE_TIMEOUT_MS);
|
|
|
|
const onSmokeMessage = (message) => {
|
|
if (message.type !== type) {
|
|
return;
|
|
}
|
|
if (validate(message)) {
|
|
finish(resolve, message);
|
|
return;
|
|
}
|
|
finish(reject, new Error(`Unexpected desktop smoke message: ${JSON.stringify(message)}`));
|
|
};
|
|
|
|
child.stdout.on("data", (chunk) => appendChunk(stdout, chunk, onSmokeMessage));
|
|
child.stderr.on("data", (chunk) => appendChunk(stderr, chunk, onSmokeMessage));
|
|
child.once("error", (error) => finish(reject, error));
|
|
child.once("exit", (code, signal) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
finish(
|
|
reject,
|
|
new Error(
|
|
`Packaged app exited before reporting smoke success (code ${code}, signal ${
|
|
signal ?? "none"
|
|
}).\n${formatLogs({ stdout, stderr, userData })}`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
function assertRunningDesktopManagedDaemon(message) {
|
|
return (
|
|
message.status?.status === "running" &&
|
|
message.status?.desktopManaged === true &&
|
|
typeof message.status?.pid === "number" &&
|
|
typeof message.status?.listen === "string" &&
|
|
message.status.listen.length > 0
|
|
);
|
|
}
|
|
|
|
function runShellCommand({ script, env, label }) {
|
|
return new Promise((resolve, reject) => {
|
|
let settled = false;
|
|
const shell = getShellCommand(script);
|
|
const child = spawn(shell.command, shell.args, {
|
|
detached: process.platform !== "win32",
|
|
env,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
windowsVerbatimArguments: process.platform === "win32",
|
|
});
|
|
const stdout = [];
|
|
const stderr = [];
|
|
|
|
const finish = (callback, value) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
releaseChildHandles(child);
|
|
callback(value);
|
|
};
|
|
|
|
const timer = setTimeout(() => {
|
|
terminateChild(child);
|
|
finish(
|
|
reject,
|
|
new Error(
|
|
`${label} timed out.\nStdout:\n${stdout.join("").trim() || "<empty>"}\n\nStderr:\n${
|
|
stderr.join("").trim() || "<empty>"
|
|
}`,
|
|
),
|
|
);
|
|
}, SMOKE_TIMEOUT_MS);
|
|
|
|
child.stdout.on("data", (chunk) => stdout.push(chunk.toString()));
|
|
child.stderr.on("data", (chunk) => stderr.push(chunk.toString()));
|
|
child.once("error", (error) => finish(reject, error));
|
|
child.once("exit", (code, signal) => {
|
|
if (code === 0) {
|
|
finish(resolve, {
|
|
stdout: stdout.join(""),
|
|
stderr: stderr.join(""),
|
|
});
|
|
return;
|
|
}
|
|
|
|
finish(
|
|
reject,
|
|
new Error(
|
|
`${label} failed (code ${code}, signal ${signal ?? "none"}).\nStdout:\n${
|
|
stdout.join("").trim() || "<empty>"
|
|
}\n\nStderr:\n${stderr.join("").trim() || "<empty>"}`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getCliShimScript(cliShimPath, args) {
|
|
const commandArgs = args.map(shellQuoteCliArg).join(" ");
|
|
if (process.platform === "win32") {
|
|
return `call "${cliShimPath}" ${commandArgs}`;
|
|
}
|
|
|
|
return `${shellQuote(cliShimPath)} ${commandArgs}`;
|
|
}
|
|
|
|
async function runCliShimCommand({ appPath, env, args, label }) {
|
|
const cliShimPath = getCliShimPath(appPath);
|
|
assertExecutable(cliShimPath, "Bundled CLI shim");
|
|
|
|
return await runShellCommand({
|
|
script: getCliShimScript(cliShimPath, args),
|
|
env,
|
|
label,
|
|
});
|
|
}
|
|
|
|
async function runCliShimJsonCommand({ appPath, env, args, label }) {
|
|
const result = await runCliShimCommand({
|
|
appPath,
|
|
env,
|
|
args: [...args, "--json"],
|
|
label,
|
|
});
|
|
const output = result.stdout.trim();
|
|
if (!output) {
|
|
throw new Error(`${label} produced empty JSON output`);
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(output);
|
|
} catch (error) {
|
|
throw new Error(`${label} produced invalid JSON: ${output}`, { cause: error });
|
|
}
|
|
}
|
|
|
|
async function smokeCliShim({ appPath, env }) {
|
|
console.log("Packaged desktop smoke: running bundled CLI shim daemon status");
|
|
const result = await runCliShimCommand({
|
|
appPath,
|
|
env,
|
|
args: ["daemon", "status"],
|
|
label: "Bundled CLI shim daemon status",
|
|
});
|
|
assertCleanDaemonStatusOutput(`${result.stdout}\n${result.stderr}`);
|
|
}
|
|
|
|
async function smokeColdCliDaemonStart({ appPath }) {
|
|
const home = createTempDir("paseo-smoke-cli-daemon-home-");
|
|
const pidPath = path.join(home, "paseo.pid");
|
|
const port = await reserveLocalTcpPort();
|
|
const listen = `127.0.0.1:${port}`;
|
|
const env = createDefaultDaemonEnv();
|
|
|
|
try {
|
|
console.log("Packaged desktop smoke: cold-starting daemon through bundled CLI shim");
|
|
await runCliShimCommand({
|
|
appPath,
|
|
env,
|
|
args: [
|
|
"daemon",
|
|
"start",
|
|
"--home",
|
|
home,
|
|
"--listen",
|
|
listen,
|
|
"--no-relay",
|
|
"--no-mcp",
|
|
"--no-inject-mcp",
|
|
],
|
|
label: "Bundled CLI shim cold daemon start",
|
|
});
|
|
|
|
const pidInfo = JSON.parse(await waitForFile(pidPath, "cold CLI daemon pid file"));
|
|
if (!pidInfo || typeof pidInfo.pid !== "number") {
|
|
throw new Error(`Cold CLI daemon wrote invalid pid file: ${JSON.stringify(pidInfo)}`);
|
|
}
|
|
|
|
if (process.platform === "darwin") {
|
|
assertDarwinProcessDoesNotUseMainAppExecutable({
|
|
appPath,
|
|
pid: pidInfo.pid,
|
|
label: "Cold CLI daemon supervisor",
|
|
});
|
|
const childPids = await waitForChildPids(pidInfo.pid);
|
|
for (const childPid of childPids) {
|
|
assertDarwinProcessDoesNotUseMainAppExecutable({
|
|
appPath,
|
|
pid: childPid,
|
|
label: "Cold CLI daemon worker",
|
|
});
|
|
}
|
|
}
|
|
} finally {
|
|
if (fs.existsSync(pidPath)) {
|
|
await runCliShimCommand({
|
|
appPath,
|
|
env,
|
|
args: ["daemon", "stop", "--home", home, "--force"],
|
|
label: "Bundled CLI shim cold daemon stop",
|
|
}).catch((error) => {
|
|
console.warn(`Packaged desktop smoke: failed to stop cold CLI daemon: ${error}`);
|
|
});
|
|
}
|
|
await removeTempDir(home);
|
|
}
|
|
}
|
|
|
|
function assertCleanDaemonStatusOutput(output) {
|
|
const failureNeedles = [
|
|
"Get-CimInstance",
|
|
"Get-Process :",
|
|
"Cannot bind parameter",
|
|
"wmic failed",
|
|
];
|
|
const failure = failureNeedles.find((needle) => output.includes(needle));
|
|
if (failure) {
|
|
throw new Error(`Bundled CLI shim daemon status included process lookup failure: ${failure}`);
|
|
}
|
|
}
|
|
|
|
async function smokeCliTerminal({ appPath, env }) {
|
|
const cwd = createTempDir("paseo-smoke-terminal-cwd-");
|
|
const marker = `paseo-packaged-terminal-smoke-${Date.now()}`;
|
|
const name = `packaged-smoke-${process.pid}-${Date.now()}`;
|
|
let terminalId = null;
|
|
|
|
try {
|
|
console.log("Packaged desktop smoke: creating terminal through bundled CLI shim");
|
|
const created = await runCliShimJsonCommand({
|
|
appPath,
|
|
env,
|
|
args: ["terminal", "create", "--cwd", cwd, "--name", name],
|
|
label: "Bundled CLI shim terminal create",
|
|
});
|
|
if (!created || typeof created.id !== "string" || created.id.length === 0) {
|
|
throw new Error(`Terminal create returned unexpected payload: ${JSON.stringify(created)}`);
|
|
}
|
|
terminalId = created.id;
|
|
|
|
const terminals = await runCliShimJsonCommand({
|
|
appPath,
|
|
env,
|
|
args: ["terminal", "ls", "--all"],
|
|
label: "Bundled CLI shim terminal ls",
|
|
});
|
|
if (!Array.isArray(terminals) || !terminals.some((terminal) => terminal.id === terminalId)) {
|
|
throw new Error(`Terminal ${terminalId} was not listed after create`);
|
|
}
|
|
|
|
await runCliShimJsonCommand({
|
|
appPath,
|
|
env,
|
|
args: ["terminal", "send-keys", terminalId, "echo", "Space", marker, "Enter"],
|
|
label: "Bundled CLI shim terminal send-keys",
|
|
});
|
|
|
|
for (let attempt = 1; attempt <= TERMINAL_CAPTURE_ATTEMPTS; attempt += 1) {
|
|
const capture = await runCliShimJsonCommand({
|
|
appPath,
|
|
env,
|
|
args: ["terminal", "capture", terminalId, "--scrollback"],
|
|
label: "Bundled CLI shim terminal capture",
|
|
});
|
|
const lines = Array.isArray(capture?.lines) ? capture.lines : [];
|
|
if (lines.join("\n").includes(marker)) {
|
|
console.log("Packaged desktop smoke: terminal command output captured");
|
|
return;
|
|
}
|
|
|
|
if (attempt < TERMINAL_CAPTURE_ATTEMPTS) {
|
|
await delay(TERMINAL_CAPTURE_INTERVAL_MS);
|
|
}
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for terminal capture marker ${marker}`);
|
|
} finally {
|
|
if (terminalId) {
|
|
await runCliShimJsonCommand({
|
|
appPath,
|
|
env,
|
|
args: ["terminal", "kill", terminalId],
|
|
label: "Bundled CLI shim terminal kill",
|
|
}).catch((error) => {
|
|
console.warn(`Packaged desktop smoke: failed to kill terminal ${terminalId}: ${error}`);
|
|
});
|
|
}
|
|
await removeTempDir(cwd);
|
|
}
|
|
}
|
|
|
|
async function stopCliDaemon({ appPath, env }) {
|
|
console.log("Packaged desktop smoke: stopping daemon through bundled CLI shim");
|
|
await runCliShimCommand({
|
|
appPath,
|
|
env,
|
|
args: ["daemon", "stop", "--force"],
|
|
label: "Bundled CLI shim daemon stop",
|
|
});
|
|
}
|
|
|
|
async function smokePackagedDesktopApp({ appPath }) {
|
|
const executablePath = getExecutablePath(appPath);
|
|
assertExecutable(executablePath, "Packaged app executable");
|
|
await smokeColdCliDaemonStart({ appPath });
|
|
|
|
const userData = createTempDir("paseo-smoke-user-data-");
|
|
const env = createDefaultDaemonEnv({
|
|
PASEO_DESKTOP_SMOKE: "1",
|
|
PASEO_ELECTRON_USER_DATA_DIR: userData,
|
|
});
|
|
|
|
const stdout = [];
|
|
const stderr = [];
|
|
const launch = getLaunchCommand(executablePath);
|
|
console.log(`Packaged desktop smoke: launching ${launch.command} ${launch.args.join(" ")}`);
|
|
const child = spawn(launch.command, launch.args, {
|
|
detached: process.platform !== "win32",
|
|
env,
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
let smokeStarted = false;
|
|
let daemonStopped = false;
|
|
|
|
const stopDaemonForCleanup = async () => {
|
|
if (daemonStopped) {
|
|
return;
|
|
}
|
|
|
|
await stopCliDaemon({ appPath, env: createDefaultDaemonEnv() });
|
|
daemonStopped = true;
|
|
};
|
|
|
|
try {
|
|
const message = await waitForSmokeMessage({
|
|
child,
|
|
stdout,
|
|
stderr,
|
|
userData,
|
|
type: "desktop-daemon-smoke-started",
|
|
validate: assertRunningDesktopManagedDaemon,
|
|
});
|
|
smokeStarted = true;
|
|
console.log("Packaged desktop smoke: desktop-managed daemon reported running");
|
|
const cliEnv = createDefaultDaemonEnv();
|
|
await smokeCliShim({ appPath, env: cliEnv });
|
|
await smokeCliTerminal({ appPath, env: cliEnv });
|
|
await stopDaemonForCleanup();
|
|
console.log(
|
|
`Packaged desktop smoke passed: desktop-managed daemon pid ${message.status.pid}, listen ${message.status.listen}; CLI shim daemon status and terminal smoke succeeded`,
|
|
);
|
|
} catch (error) {
|
|
if (smokeStarted && !daemonStopped) {
|
|
try {
|
|
await stopDaemonForCleanup();
|
|
} catch {}
|
|
}
|
|
throw error;
|
|
} finally {
|
|
if (isRunning(child)) {
|
|
terminateChild(child);
|
|
if (!(await waitForChildExit(child))) {
|
|
terminateChild(child, "SIGKILL");
|
|
await waitForChildExit(child);
|
|
}
|
|
}
|
|
releaseChildHandles(child);
|
|
await removeTempDir(userData);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
smokePackagedDesktopApp,
|
|
};
|
|
|
|
if (require.main === module) {
|
|
const appIndex = process.argv.indexOf("--app");
|
|
const appPath = appIndex >= 0 ? process.argv[appIndex + 1] : null;
|
|
if (!appPath) {
|
|
process.stderr.write("Usage: node smoke-packaged-desktop-app.js --app <Paseo.app>\n");
|
|
process.exit(2);
|
|
}
|
|
|
|
smokePackagedDesktopApp({ appPath }).catch((error) => {
|
|
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
process.stderr.write(`${message}\n`);
|
|
process.exit(1);
|
|
});
|
|
}
|