Files
orca/config/scripts/run-headless-serve-shutdown-docker.mjs
T
Jinwoo Hong ff8dda81e8 fix(serve): exit cleanly after headless Linux signals (#14334)
* fix(serve): keep owned Xvfb alive through Electron teardown

* test(serve): gate packaged signal shutdown

* test: harden headless shutdown lifecycle gate

* fix(serve): isolate Xvfb from foreground signals

* docs(serve): preserve Xvfb during systemd stop

* test(serve): pin shutdown policy to owned Xvfb unit

* test(serve): harden shutdown gate portability

* test(serve): bound systemd unit parsing
2026-08-13 15:45:00 -07:00

159 lines
4.5 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const args = process.argv.slice(2)
const appImageArg = valueAfter('--appimage')
const platform = valueAfter('--platform') ?? 'linux/amd64'
const signalTarget = valueAfter('--signal-target') ?? 'app'
const entrypoint = valueAfter('--entrypoint') ?? 'app'
const intDelivery = valueAfter('--int-delivery') ?? 'foreground-process-group'
const launcherExecOverlay = args.includes('--launcher-exec-overlay')
if (!appImageArg) {
fail('Usage: run-headless-serve-shutdown-docker.mjs --appimage /path/to/orca.AppImage')
}
if (!['app', 'serving-electron'].includes(signalTarget)) {
fail(`Unsupported --signal-target: ${signalTarget}`)
}
if (!['app', 'launcher'].includes(entrypoint)) {
fail(`Unsupported --entrypoint: ${entrypoint}`)
}
if (!['pid', 'foreground-process-group'].includes(intDelivery)) {
fail(`Unsupported --int-delivery: ${intDelivery}`)
}
if (intDelivery === 'foreground-process-group' && signalTarget !== 'app') {
fail('--int-delivery foreground-process-group requires --signal-target app')
}
if (launcherExecOverlay && entrypoint !== 'launcher') {
fail('--launcher-exec-overlay requires --entrypoint launcher')
}
const appImage = resolve(appImageArg)
const shutdownDockerDirectory = resolve('config', 'docker', 'headless-serve-shutdown')
const shutdownDockerfile = resolve(shutdownDockerDirectory, 'Dockerfile')
if (!existsSync(appImage)) {
fail(`AppImage not found: ${appImage}`)
}
const suffix = `${process.pid}-${Date.now()}`
const image = `orca-headless-serve-shutdown:${suffix}`
const artifactVolume = `orca-headless-serve-shutdown-${suffix}`
const sha256 = createHash('sha256').update(readFileSync(appImage)).digest('hex')
try {
docker([
'build',
'--platform',
platform,
'-f',
shutdownDockerfile,
'-t',
image,
shutdownDockerDirectory
])
docker(['volume', 'create', artifactVolume])
docker([
'run',
'--rm',
'--platform',
platform,
'--entrypoint',
'bash',
'-v',
`${appImage}:/input/orca.AppImage:ro`,
'-v',
`${artifactVolume}:/artifacts`,
image,
'-lc',
[
'7z x /input/orca.AppImage -o/artifacts/root -y >/dev/null',
launcherExecOverlay
? "sed -i 's/^ELECTRON_RUN_AS_NODE=1 /export ELECTRON_RUN_AS_NODE=1\\nexec /' /artifacts/root/resources/bin/orca-ide"
: ':',
'chmod -R a+rX /artifacts/root'
].join(' && ')
])
console.log(
JSON.stringify({
type: 'appimage_under_test',
appImage,
sha256,
platform,
signalTarget,
entrypoint,
intDelivery,
launcherExecOverlay
})
)
const failedSignals = []
for (const signal of ['INT', 'TERM']) {
const result = docker(
[
'run',
'--rm',
'--init',
'--platform',
platform,
'--shm-size',
'256m',
'--name',
`orca-headless-serve-shutdown-${signal.toLowerCase()}-${suffix}`,
'-e',
`ORCA_SIGNAL_TARGET=${signalTarget}`,
'-e',
`ORCA_TEST_ENTRYPOINT=${entrypoint}`,
'-e',
`ORCA_INT_DELIVERY=${intDelivery}`,
'-v',
`${artifactVolume}:/artifacts:ro`,
image,
signal
],
{ allowFailure: true }
)
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
if (result.status !== 0) {
failedSignals.push(`${signal}:${result.status}`)
}
}
if (failedSignals.length > 0) {
fail(`Shutdown oracle failed: ${failedSignals.join(', ')}`)
}
console.log('Headless serve packaged shutdown Docker validation passed.')
} finally {
docker(['volume', 'rm', artifactVolume], { allowFailure: true })
docker(['image', 'rm', image], { allowFailure: true })
}
function valueAfter(flag) {
const index = args.indexOf(flag)
return index === -1 ? null : (args[index + 1] ?? null)
}
function docker(dockerArgs, options = {}) {
const result = spawnSync('docker', dockerArgs, {
cwd: process.cwd(),
encoding: 'utf8',
maxBuffer: 20 * 1024 * 1024
})
if (result.error) {
throw result.error
}
if (result.status !== 0 && !options.allowFailure) {
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
fail(`docker ${dockerArgs[0]} failed with status ${result.status}`)
}
return result
}
function fail(message) {
console.error(message)
process.exitCode = 1
throw new Error(message)
}