fix(emulator): make iPhone Duo fold work in release builds and show serve-sim's real error (#41051)

* fix(emulator): let iPhone Duo fold in release builds

Release signing gave serve-sim's in-simulator helpers the hardened runtime,
which drops the DYLD_ROOT_PATH that simctl spawn hands them, so the Duo HID
and accessibility helpers aborted at launch. Sign just those two with only
allow-dyld-environment-variables.

serve-sim's uncaught errors print its minified source, which contains the
"No serve-sim server running" text, so every crash was reported as no active
emulator. Report the thrown error line instead.

Fixes #40509

* fix(emulator): keep multi-line serve-sim error messages

* refactor(emulator): simplify serve-sim failure message parsing
This commit is contained in:
Kelvin Amoaba
2026-11-12 00:10:11 +00:00
committed by GitHub
parent b72ef23702
commit d9d3dfdd08
3 changed files with 61 additions and 3 deletions
@@ -1,8 +1,21 @@
import { createRequire } from 'node:module'
// serve-sim launches these with `simctl spawn`, whose DYLD_ROOT_PATH the hardened runtime drops (#40509).
const IOS_SIMULATOR_HELPER_PATHS = [
'/node_modules/serve-sim/dist/simduo/serve-sim-duo-hid',
'/node_modules/serve-sim/dist/simax/serve-sim-ax-settings'
]
const IOS_SIMULATOR_HELPER_ENTITLEMENTS = ['com.apple.security.cs.allow-dyld-environment-variables']
export function signMacApp(options, { sign, retry }) {
const optionsForFile = (filePath, context) => {
const fileOptions = options.optionsForFile?.(filePath, context)
return IOS_SIMULATOR_HELPER_PATHS.some((helperPath) => filePath.endsWith(helperPath))
? { ...fileOptions, entitlements: IOS_SIMULATOR_HELPER_ENTITLEMENTS }
: fileOptions
}
// A custom signer bypasses electron-builder's retry wrapper.
return retry(() => sign({ ...options, batchCodesignCalls: true }), {
return retry(() => sign({ ...options, optionsForFile, batchCodesignCalls: true }), {
retries: 3,
interval: 5000,
backoff: 5000
@@ -0,0 +1,39 @@
import { expect, it, vi } from 'vitest'
const { runProcessMock } = vi.hoisted(() => ({ runProcessMock: vi.fn() }))
vi.mock('@orca/process-host', () => ({ runProcess: runProcessMock }))
import { execServeSimCommand } from './serve-sim-execution'
// Shape of serve-sim 0.1.47's real `fold 180 -d <udid> -q` crash: the minified source line holds
// the "No serve-sim server running" text of every other command (#40509).
const UNCAUGHT_FOLD_FAILURE = `file:///Users/me/Library/Application Support/orca/serve-sim-runtime/1.4.255/node_modules/serve-sim/dist/serve-sim.js:161
Shutting down...\`);async function GG($,Q){let Z=s(Q);if(!Z)console.error("No serve-sim server running. Run \`serve-sim\` first."),process.exit(1);let J=await C5(Z.wsUrl)}
Error: Simulator rejected the fold. An iPhone Duo with guest HID support is required.
at J.onmessage (file:///Users/me/Library/Application Support/orca/serve-sim-runtime/1.4.255/node_modules/serve-sim/dist/serve-sim.js:161:6107)
at callListener (/Users/me/node_modules/ws/lib/event-target.js:290:14)
Node.js v24.22.1
`
it('reports the thrown serve-sim error instead of no active emulator', async () => {
runProcessMock.mockResolvedValueOnce({
code: 1,
signal: null,
stdout: '',
stderr: UNCAUGHT_FOLD_FAILURE,
timedOut: false
})
await expect(
execServeSimCommand({ command: 'serve-sim', baseArgs: [], usesElectronAsNode: false }, [
'fold',
'180'
])
).rejects.toMatchObject({
code: 'emulator_error',
message: 'Error: Simulator rejected the fold. An iPhone Duo with guest HID support is required.'
})
})
+8 -2
View File
@@ -171,6 +171,12 @@ export function stripEmulatorTargetArgs(args: string[]): string[] {
return stripped
}
function serveSimFailureMessage(output: string): string {
// Uncaught throws print serve-sim's minified source (every CLI message) first; keep the thrown error.
const thrown = /^\w*Error: [\s\S]*?(?=^\s+at |^Node\.js v)/m.exec(output)
return thrown?.[0].trim() || output.trim() || 'serve-sim command failed'
}
export async function execServeSimCommand(
executable: ServeSimExecutable,
args: string[],
@@ -198,9 +204,9 @@ export async function execServeSimCommand(
)
}
if (result.code !== 0 || result.timedOut) {
const message = result.stdout || result.stderr || 'serve-sim command failed'
const message = serveSimFailureMessage(result.stdout || result.stderr)
// Why: serve-sim also says "not running" for a missing camera helper; that needs its own message.
if (/no serve-sim server/i.test(message)) {
if (/^no serve-sim server/im.test(message)) {
throw new EmulatorError(
'emulator_no_active',
'No active emulator for this worktree — use orca emulator devices/attach or open the pane'