From 97fac546df4d8f74f471843e790ba83466dd5b4d Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Tue, 13 Oct 2026 05:12:13 -0700 Subject: [PATCH] fix(linux): a supervised serve run from the AppImage stays in place instead of re-exec'ing (#28270) Since a direct `Orca.AppImage serve --supervise` now runs the CLI supervisor (#28227), the supervisor's relocation re-exec ran from the AppImage mount. An exec from node mode closes every inherited descriptor, including the AppImage runtime's keepalive pipe, so the runtime unmounted the AppImage during the exec and the new image failed with 'libffmpeg.so: cannot open shared object file' (exit 127) on every start. Relocation exists so an update cannot swap the app under a running supervisor; an AppImage's mount cannot change during the run, so it stays in place. Co-authored-by: m4air --- .../serve-supervisor-relocation.test.ts | 18 ++++++++++++++++++ .../runtime/serve-supervisor-relocation.ts | 19 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/cli/runtime/serve-supervisor-relocation.test.ts b/src/cli/runtime/serve-supervisor-relocation.test.ts index 99c36ae8e6b..c642048ccb6 100644 --- a/src/cli/runtime/serve-supervisor-relocation.test.ts +++ b/src/cli/runtime/serve-supervisor-relocation.test.ts @@ -89,6 +89,24 @@ describe('relocateServeSupervisor', () => { } }) + // Why: re-exec from node mode closes the AppImage runtime's keepalive pipe, so the mount is + // torn down under the exec and the new image fails to load its libraries (real-host BUG-3). + it('stays in place when running from an AppImage, whose files cannot change under it', async () => { + const d = deps({ + env: { + ELECTRON_RUN_AS_NODE: '1', + APPDIR: resources.replace(/\/Resources$/, ''), + APPIMAGE: '/a/Orca.AppImage' + } + }) + await expect(relocateServeSupervisor(d)).resolves.toBe('in-place') + expect(d.execve).not.toHaveBeenCalled() + expect(existsSync(serveSupervisorSnapshotRoot(userData))).toBe(false) + + const elsewhere = deps({ env: { ELECTRON_RUN_AS_NODE: '1', APPDIR: join(root, 'other') } }) + await expect(relocateServeSupervisor(elsewhere)).resolves.toBe('relocated') + }) + it('keeps running in place when the copy or the re-exec fails', async () => { await writeFile(join(root, 'blocker'), '') const blocked = deps({ userDataPath: join(root, 'blocker') }) diff --git a/src/cli/runtime/serve-supervisor-relocation.ts b/src/cli/runtime/serve-supervisor-relocation.ts index cf68e6b8208..4219c2af743 100644 --- a/src/cli/runtime/serve-supervisor-relocation.ts +++ b/src/cli/runtime/serve-supervisor-relocation.ts @@ -7,7 +7,7 @@ import { createHash } from 'node:crypto' import { constants, readFileSync, statSync } from 'node:fs' import { cp, mkdir, readdir, rename, rm, stat, writeFile } from 'node:fs/promises' -import { basename, dirname, join } from 'node:path' +import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path' import { isProcessAlive } from '../../shared/serve-bundle-coordination' /** Set on the re-run supervisor: the app root its own `__dirname` no longer points at. */ @@ -132,6 +132,20 @@ async function removeUnusedSnapshots( } } +/** + * An AppImage's files are its own mount (or extracted copy) for the whole run, so no update can + * swap them. Re-exec from node mode would also close the runtime's keepalive pipe, which unmounts + * the AppImage under the exec and leaves the new image unable to load its libraries. + */ +function runsFromAppImage(appRoot: string, env: NodeJS.ProcessEnv): boolean { + const appDir = env.APPDIR + if (!appDir || !isAbsolute(appDir)) { + return false + } + const inside = relative(resolve(appDir), resolve(appRoot)) + return inside !== '' && !inside.startsWith('..') && !isAbsolute(inside) +} + /** * Resolves only when the supervisor keeps running from where it is; a successful re-run replaces * this process (same pid, so the service manager keeps tracking it) and never returns. @@ -147,7 +161,8 @@ export async function relocateServeSupervisor( if ( deps.env.ELECTRON_RUN_AS_NODE !== '1' || (typeof override === 'string' && override.trim().length > 0) || - basename(deps.appRoot) !== PACKAGED_APP_ROOT_NAME + basename(deps.appRoot) !== PACKAGED_APP_ROOT_NAME || + runsFromAppImage(deps.appRoot, deps.env) ) { return 'in-place' }