From c92e835cb1e0542d10469411a294cc133c0bd30e Mon Sep 17 00:00:00 2001 From: Jackson Miller Date: Sun, 25 Oct 2026 00:12:52 -0400 Subject: [PATCH] fix(server): device list warnings keep the hub's error detail When expo-device-hub's device list reports errors, the Device panel's discovery warning and the device_list MCP tool showed only each error's message, such as "[apple-utils] Failed to run `xcrun simctl list devices --json`:". The hub's error text with the actual cause was dropped. That text is a util.inspect of Node's exec error, so append only its head: the lines before the first stack frame, joined into one line. The stack and the inspected fields (code, cmd, stdout, stderr) are dropped. Each warning stays bounded to 2,000 characters, the bound the boot path already applies to its diagnostics. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/device/DeviceHubOperations.ts | 28 +++++++--- apps/server/src/device/DeviceService.test.ts | 51 +++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/apps/server/src/device/DeviceHubOperations.ts b/apps/server/src/device/DeviceHubOperations.ts index 9371484f4c93f..910c063da73da 100644 --- a/apps/server/src/device/DeviceHubOperations.ts +++ b/apps/server/src/device/DeviceHubOperations.ts @@ -39,6 +39,21 @@ const HubActionResult = Schema.Struct({ ), }); +// Hub diagnostics can carry a whole stderr dump. +const boundDiagnostics = (text: string) => + text.length > 2_000 ? `${text.slice(0, 1_999)}…` : text; + +// The hub's `error` is a util.inspect of Node's exec error; the stack and the +// inspected fields follow the first frame, so keep the lines before it. +const errorHead = (error: string) => { + const lines = error.split(/\r?\n/); + const firstFrame = lines.findIndex((line) => /^\s+at /.test(line)); + return (firstFrame === -1 ? lines : lines.slice(0, firstFrame)) + .map((line) => line.trim()) + .filter(Boolean) + .join(" "); +}; + const vendorPrefix = (platform: DevicePlatform) => platform === "ios" ? "/vendor/serve-sim" : "/vendor/serve-emu"; @@ -109,7 +124,11 @@ export const makeDeviceHubOperations = (options: { /(?:^Error:\s*ENOENT:|\bcode:\s*['"]ENOENT['"])/.test(error.error ?? "") ), ); - const details = warnings?.map((error) => error.message) ?? []; + const details = + warnings?.map((error) => { + const head = errorHead(error.error ?? ""); + return boundDiagnostics(head ? `${error.message} ${head}` : error.message); + }) ?? []; let androidDiscoveryIncomplete = false; if (androidAvailable) { const avds = yield* run("emulator", ["-list-avds"]); @@ -167,12 +186,7 @@ export const makeDeviceHubOperations = (options: { : /timed? out|timeout/i.test(diagnostics) ? "timeout" : "launch_failed", - ...(diagnostics - ? { - diagnostics: - diagnostics.length > 2_000 ? `${diagnostics.slice(0, 1_999)}…` : diagnostics, - } - : {}), + ...(diagnostics ? { diagnostics: boundDiagnostics(diagnostics) } : {}), cause: result, }); } diff --git a/apps/server/src/device/DeviceService.test.ts b/apps/server/src/device/DeviceService.test.ts index c13179d9d4932..4031cdcf25e6a 100644 --- a/apps/server/src/device/DeviceService.test.ts +++ b/apps/server/src/device/DeviceService.test.ts @@ -522,6 +522,57 @@ it.effect("hides Android discovery failures when the host has no Android SDK", ( }).pipe(Effect.scoped), ); +// The hub reports `error` as a util.inspect of Node's exec error. +const simctlMessage = "[apple-utils] Failed to run `xcrun simctl list devices --json`:"; +const simctlCause = + "An error was encountered processing the command (domain=NSPOSIXErrorDomain, code=60): Unable to connect to CoreSimulatorService [xcrun shim fail mode]"; +const simctlError = `Error: Command failed: xcrun simctl list devices --json +${simctlCause} + + at genericNodeError (node:internal/errors:986:15) + at wrappedFn (node:internal/errors:540:14) + at ChildProcess.exithandler (node:child_process:417:12) + at ChildProcess._handle.onexit (node:internal/child_process:335:5) { + code: 1, + killed: false, + signal: null, + cmd: 'xcrun simctl list devices --json', + stdout: '', + stderr: '${simctlCause}\\n' +}`; +const avdmanager = "/Users/test/Library/Android/sdk/cmdline-tools/latest/bin/avdmanager"; +const avdmanagerError = `Error: spawn ${avdmanager} ENOENT + at ChildProcess._handle.onexit (node:internal/child_process:316:19) + at onErrorNT (node:internal/child_process:524:16) { + errno: -2, + code: 'ENOENT', + syscall: 'spawn ${avdmanager}', + path: '${avdmanager}', + spawnargs: [ 'list', 'avd' ], + cmd: '${avdmanager} list avd', + stdout: '', + stderr: '' +}`; + +it.effect("keeps the cause from the hub's error in discovery warnings, without the stack", () => + Effect.gen(function* () { + const { service, discoveryErrors } = yield* fixture(); + yield* Ref.set(discoveryErrors, [ + { message: simctlMessage, error: simctlError }, + { message: "[android-utils] Failed to run `avdmanager list avd`:", error: avdmanagerError }, + ]); + const state = yield* service.configure({ enabled: true }); + expect(state.hostStatusDetail).toBe( + [ + `${simctlMessage} Error: Command failed: xcrun simctl list devices --json ${simctlCause}`, + `[android-utils] Failed to run \`avdmanager list avd\`: Error: spawn ${avdmanager} ENOENT`, + ].join("\n"), + ); + expect(state.hostStatusDetail).not.toContain(" at "); + expect(state.hostStatusDetail).not.toContain("stderr:"); + }).pipe(Effect.scoped), +); + it.effect.each([ { diagnostic: "Insufficient disk space at /private/user/path",