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 <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-13 05:12:13 -07:00
committed by GitHub
co-authored by m4air
parent a10ca391ff
commit 97fac546df
2 changed files with 35 additions and 2 deletions
@@ -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') })
+17 -2
View File
@@ -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'
}