-
Notifications
You must be signed in to change notification settings - Fork 219
desktop: fix packaged app crash on startup (bundle the bearer helpers, not the server) #1018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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`); | ||
| 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); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the daemon never writes
server.jsonwithin the 30-second poll window the loop exits silently, and the subsequentJSON.parse(before)on line 92 will throw aSyntaxError(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 athrowafter the loop on timeout would surface the actual failure condition immediately.