From 2b07f0f3b64d81fa2bca2a2aba1282c92f99a13a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:52:55 -0700 Subject: [PATCH] fix(managed-server): snapshot capture and restore run on BusyBox before 1.30 (#30840) The state mutation wrapper ran `timeout -s KILL 900 sh -c ...`, which old BusyBox (`timeout [-t SECS] [-s SIG] PROG`) reads as running a program named 900. The script never ran, the capture answered nothing and parsed as failed, so every update stopped the server and rolled back. Probe the syntax as the room check (#30618) already does. Co-authored-by: Claude --- .../orcad-state-snapshot-shell.test.ts | 20 +++++++++++++++++++ .../host-state/orcad-state-snapshot.ts | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/managed-server/host-state/orcad-state-snapshot-shell.test.ts b/src/main/managed-server/host-state/orcad-state-snapshot-shell.test.ts index 565804db789..29ba3c9d81d 100644 --- a/src/main/managed-server/host-state/orcad-state-snapshot-shell.test.ts +++ b/src/main/managed-server/host-state/orcad-state-snapshot-shell.test.ts @@ -1,4 +1,5 @@ import { + chmodSync, existsSync, mkdirSync, mkdtempSync, @@ -115,6 +116,25 @@ describe.skipIf(process.platform === 'win32')('snapshot commands on a real shell expect(existsSync(join(root, '.orcad-state-restore-stage'))).toBe(false) }) + it('captures on a BusyBox before 1.30, whose timeout only takes `-t SECS`', async () => { + const bin = join(base, 'bin') + mkdirSync(bin) + // Old BusyBox reads a bare `900` as the program to run. + writeFileSync( + join(bin, 'timeout'), + '#!/bin/sh\n[ "$1" = "-t" ] || exit 127\nshift 2\n[ "$1" = "-s" ] && shift 2\nexec "$@"\n' + ) + chmodSync(join(bin, 'timeout'), 0o755) + + expect( + parseOrcadSnapshotCapture( + await sh(captureOrcadStateSnapshotCommand(posix, root, snapshot, remoteBase), { + PATH: `${bin}:${process.env.PATH ?? ''}` + }) + ) + ).toBe('captured') + }) + it('keeps live state when the archive cannot be extracted', async () => { await sh(captureOrcadStateSnapshotCommand(posix, root, snapshot, remoteBase)) writeFileSync(join(snapshot, 'state.tar'), 'not a tar archive') diff --git a/src/main/managed-server/host-state/orcad-state-snapshot.ts b/src/main/managed-server/host-state/orcad-state-snapshot.ts index f7ec816e289..fbcee78aab6 100644 --- a/src/main/managed-server/host-state/orcad-state-snapshot.ts +++ b/src/main/managed-server/host-state/orcad-state-snapshot.ts @@ -135,7 +135,9 @@ export function serializedStateMutationCommand( 'else "$@"; fi; };', // Why timeout inside the group: KILL then reaches tar and rm, not only the shell. // The deadline is a word list, so the script is sent once, not once per branch. - `orca_deadline=; command -v timeout >/dev/null 2>&1 && orca_deadline="timeout -s KILL ${ORCAD_STATE_MUTATION_DEADLINE_SECONDS}";`, + // BusyBox before 1.30 spells it `timeout -t SECS`, as the room probe found. + `orca_deadline=; if timeout 1 true 2>/dev/null; then orca_deadline="timeout -s KILL ${ORCAD_STATE_MUTATION_DEADLINE_SECONDS}";`, + `elif timeout -t 1 true 2>/dev/null; then orca_deadline="timeout -t ${ORCAD_STATE_MUTATION_DEADLINE_SECONDS} -s KILL"; fi;`, `orca_state_group $orca_deadline ${run};`, `status=$?; ${owned ? `[ "$status" -eq ${ORCAD_FENCE_LOST_EXIT} ] && exit ${ORCAD_FENCE_LOST_EXIT}; ` : ''}if [ "$status" -eq 124 ] || [ "$status" -eq 137 ]; then echo ${ORCAD_STATE_MUTATION_DEADLINE}; fi` ].join(' ')