Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/src/main/settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Store from "electron-store";
import { rotateLocalAuthToken } from "@executor-js/local";
import { rotateLocalAuthToken } from "@executor-js/local/auth";
import { DEFAULT_SERVER_SETTINGS, type DesktopServerSettings } from "../shared/server-settings";

interface PersistedShape {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/sidecar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
parseExecutorLocalServerManifest,
serializeExecutorLocalServerManifest,
} from "@executor-js/sdk/shared";
import { loadOrMintLocalAuthToken } from "@executor-js/local";
import { loadOrMintLocalAuthToken } from "@executor-js/local/auth";
import { getServerSettings } from "./settings";
import { reportSidecarCrash, sidecarCrashReportingEnv } from "./diagnostics";
import { SERVER_SETTINGS_USERNAME, type DesktopServerSettings } from "../shared/server-settings";
Expand Down
3 changes: 2 additions & 1 deletion apps/local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./auth": "./src/auth.ts"
},
"scripts": {
"dev": "bun run dev:proxy && bun run dev:vite",
Expand Down
150 changes: 150 additions & 0 deletions e2e/scripts/verify-linux-desktop-attach.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Controlled, observable proof that the PACKAGED Linux desktop bundle attaches
// to an OS-supervised daemon — a manual harness (run by hand, not in the vitest
// matrix). Linux + Xvfb avoids the macOS Aqua-session limitation: the packaged
// Electron app runs headless under a virtual X server in a tart guest, so the
// attach path can be proven without a logged-in GUI session (which the
// desktop-packaged vitest project needs and skips without). Mirrors that test's
// intent, with a finally that always discards the VM.
//
// bun e2e/scripts/verify-linux-desktop-attach.ts /tmp/linux-desktop-bundle.tgz
import { execFile } from "node:child_process";
import { promisify } from "node:util";

import { tartVm } from "../src/vm/tart";

const execFileP = promisify(execFile);
const t0 = Date.now();
const log = (m: string) => console.log(`[+${((Date.now() - t0) / 1000).toFixed(0)}s] ${m}`);

const bundleTgz = process.argv[2] ?? "/tmp/linux-desktop-bundle.tgz";
// Ubuntu 24.04 (noble) renamed several libs to `…t64`; apt fails the whole
// transaction if ANY name is missing, so install each separately and list both
// the pre-noble and t64 names — only the available one installs.
const DEPS = [
"xvfb",
"libnss3",
"libnspr4",
"libatk1.0-0",
"libatk1.0-0t64",
"libatk-bridge2.0-0",
"libatk-bridge2.0-0t64",
"libcups2",
"libcups2t64",
"libdrm2",
"libxkbcommon0",
"libxcomposite1",
"libxdamage1",
"libxfixes3",
"libxrandr2",
"libgbm1",
"libpango-1.0-0",
"libcairo2",
"libasound2",
"libasound2t64",
"libatspi2.0-0",
"libatspi2.0-0t64",
"libgtk-3-0",
"libgtk-3-0t64",
];

const main = async () => {
log("provisioning tart linux guest...");
const vm = await tartVm("linux", "arm64").provision();
log(`provisioned host=${vm.host}`);
try {
log("pushing packaged bundle tarball...");
await execFileP("sshpass", [
"-p",
"admin",
"scp",
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
bundleTgz,
`admin@${vm.host}:/tmp/app.tgz`,
]);
log("extracting + installing Electron runtime deps (apt)...");
await vm.ssh("mkdir -p /tmp/app && tar -xzf /tmp/app.tgz -C /tmp/app");
await vm.ssh("sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq");
// Install each package independently so one missing name (noble t64 rename)
// doesn't fail the whole transaction.
const apt = await vm.ssh(
`for p in ${DEPS.join(" ")}; do sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $p >/dev/null 2>&1 && echo "ok $p" || echo "skip $p"; done`,
);
log(`apt:\n${apt.stdout.trim()}`);
const nss = await vm.ssh("ldconfig -p | grep -c libnss3 || true");
log(`libnss3 present: ${nss.stdout.trim()}`);

const home = "/tmp/eh";
const exe = "/tmp/app/executor-desktop";
const sidecar = "/tmp/app/resources/sidecar/executor-sidecar";
const webui = "/tmp/app/resources/web-ui";

log("starting bundled sidecar as supervised daemon...");
await vm.ssh(
`rm -rf ${home}; mkdir -p ${home}; nohup env HOME=${home} EXECUTOR_SUPERVISED=1 EXECUTOR_DATA_DIR=${home}/.executor EXECUTOR_PORT=4789 EXECUTOR_HOST=127.0.0.1 EXECUTOR_AUTH_TOKEN=linux-attach EXECUTOR_CLIENT_DIR=${webui} ${sidecar} > /tmp/daemon.log 2>&1 &`,
);
// Wait for the daemon to publish its manifest + serve.
for (let i = 0; i < 30; i++) {
const r = await vm.ssh(`cat ${home}/.executor/server-control/server.json 2>/dev/null`);
Comment on lines +83 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent timeout before manifest parse

If the daemon never writes server.json within the 30-second poll window the loop exits silently, and the subsequent JSON.parse(before) on line 92 will throw a SyntaxError (empty string) rather than a clear diagnostic. The intent check on line 95 (if (beforeManifest.kind !== "cli-daemon") throw …) only helps if the file exists with wrong content. Adding a throw after the loop on timeout would surface the actual failure condition immediately.

if (r.stdout.includes("cli-daemon")) break;
await new Promise((res) => setTimeout(res, 1000));
}
const before = (await vm.ssh(`cat ${home}/.executor/server-control/server.json`)).stdout.trim();
const beforeManifest = JSON.parse(before) as { kind: string; pid: number };
log(`daemon manifest: kind=${beforeManifest.kind} pid=${beforeManifest.pid}`);
if (beforeManifest.kind !== "cli-daemon") throw new Error("daemon did not publish cli-daemon");
const health = await vm.ssh(
`curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:4789/api/health`,
);
log(`/api/health = ${health.stdout.trim()}`);

log("launching PACKAGED app under Xvfb...");
await vm.ssh(
`nohup xvfb-run -a env HOME=${home} ELECTRON_ENABLE_LOGGING=1 ${exe} --no-sandbox > /tmp/app.log 2>&1 &`,
);

// Positive attach signal: the main process logs "attaching to supervised
// daemon" (sidecar.ts) — proves the attach CODE ran and succeeded, not just
// "the manifest is unchanged" (which a crash would also leave).
let attached = false;
for (let i = 0; i < 40; i++) {
const logs = await vm.ssh(
`cat /tmp/app.log ${home}/.config/Executor/logs/main.log 2>/dev/null`,
);
if (/attaching to supervised daemon/i.test(logs.stdout)) {
attached = true;
break;
}
await new Promise((res) => setTimeout(res, 2000));
}
const appLog = (await vm.ssh(`tail -40 /tmp/app.log 2>/dev/null`)).stdout.trim();
log(`app.log tail:\n${appLog}`);

const after = (await vm.ssh(`cat ${home}/.executor/server-control/server.json`)).stdout.trim();
const afterManifest = JSON.parse(after) as { kind: string; pid: number };
log(`manifest after app launch: kind=${afterManifest.kind} pid=${afterManifest.pid}`);

if (!attached) {
throw new Error("no 'attaching to supervised daemon' log — app did not attach (see app.log)");
}
if (afterManifest.kind !== "cli-daemon" || afterManifest.pid !== beforeManifest.pid) {
throw new Error(
`manifest changed (kind=${afterManifest.kind} pid=${afterManifest.pid}) — app spawned its own sidecar instead of attaching`,
);
}
log("RESULT=PASS — packaged Linux app attached to the supervised daemon (Xvfb)");
} finally {
log("discarding guest VM");
await vm.discard();
log("discarded");
}
};

main()
.then(() => process.exit(0))
.catch((e) => {
console.error("RESULT=FAIL", e);
process.exit(1);
});
Loading