Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/kernel/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export function normalizeError(
};
}

const GENERIC_EXIT_MESSAGE = /^\S+ exited with code -?\d+$/;

function maybeEnrichCommandFailedMessage(
code: string,
message: string,
Expand All @@ -114,7 +116,12 @@ function maybeEnrichCommandFailedMessage(
const stderr = typeof details?.stderr === 'string' ? details.stderr : '';
const excerpt = firstStderrLine(stderr);
if (!excerpt) return message;
return excerpt;
// Generic "<tool> exited with code N" wraps carry no context of their own,
// so the stderr excerpt replaces them outright. Curated wrap messages keep
// the specific failure description and gain the excerpt as a suffix.
if (GENERIC_EXIT_MESSAGE.test(message)) return excerpt;
if (message.includes(excerpt)) return message;
return `${message}: ${excerpt}`;
}

function firstStderrLine(stderr: string): string | null {
Expand Down
11 changes: 6 additions & 5 deletions src/platforms/android/app-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppError } from '../../kernel/errors.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import { resolveAppsFilter, type AppsFilter } from '../../contracts/app-inventory.ts';
import type { AndroidAdbExecutor } from './adb-executor.ts';
import {
Expand Down Expand Up @@ -101,11 +102,11 @@ function parseAndroidLaunchablePackageOutput(stdout: string): string[] {
async function listAndroidUserInstalledPackagesWithAdb(adb: AndroidAdbExecutor): Promise<string[]> {
const result = await adb(['shell', 'pm', 'list', 'packages', '-3'], { allowFailure: true });
if (result.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', 'Failed to list Android user-installed apps', {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
});
throw new AppError(
'COMMAND_FAILED',
'Failed to list Android user-installed apps',
execFailureDetails(result),
);
}
return parseAndroidUserInstalledPackages(result.stdout);
}
Expand Down
12 changes: 6 additions & 6 deletions src/platforms/android/app-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { resolveFileOverridePath, runCmd, whichCmd } from '../../utils/exec.ts';
import { execFailureDetails, resolveFileOverridePath, runCmd, whichCmd } from '../../utils/exec.ts';
import { AppError } from '../../kernel/errors.ts';
import { sleep } from '../../utils/timeouts.ts';
import type { AppsFilter } from '../../contracts/app-inventory.ts';
Expand Down Expand Up @@ -726,11 +726,11 @@ async function uninstallAndroidApp(device: DeviceInfo, app: string): Promise<{ p
if (result.exitCode !== 0) {
const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
if (!output.includes('unknown package') && !output.includes('not installed')) {
throw new AppError('COMMAND_FAILED', `adb uninstall failed for ${resolved.value}`, {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
});
throw new AppError(
'COMMAND_FAILED',
`adb uninstall failed for ${resolved.value}`,
execFailureDetails(result),
);
}
}
return { package: resolved.value };
Expand Down
21 changes: 11 additions & 10 deletions src/platforms/android/device-input-state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { emitDiagnostic } from '../../utils/diagnostics.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
import { AppError } from '../../kernel/errors.ts';
import { isClipboardShellUnsupported, sleep } from './adb.ts';
Expand Down Expand Up @@ -84,11 +85,11 @@ export async function getAndroidKeyboardStatusWithAdb(
allowFailure: true,
});
if (result.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', 'Failed to query Android keyboard state', {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
});
throw new AppError(
'COMMAND_FAILED',
'Failed to query Android keyboard state',
execFailureDetails(result),
);
}
return parseAndroidKeyboardState(result.stdout);
}
Expand Down Expand Up @@ -326,11 +327,11 @@ async function runAndroidClipboardShellCommand(
);
}
if (result.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', `Failed to ${operation} Android clipboard text`, {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
});
throw new AppError(
'COMMAND_FAILED',
`Failed to ${operation} Android clipboard text`,
execFailureDetails(result),
);
}
return result.stdout;
}
Expand Down
15 changes: 8 additions & 7 deletions src/platforms/android/devices.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { runCmd, runCmdDetached, whichCmd } from '../../utils/exec.ts';
import { execFailureDetails, runCmd, runCmdDetached, whichCmd } from '../../utils/exec.ts';
import type { ExecResult } from '../../utils/exec.ts';
import { sleep } from '../../utils/timeouts.ts';
import { AppError, asAppError } from '../../kernel/errors.ts';
Expand Down Expand Up @@ -311,12 +311,13 @@ async function listAndroidAvdNames(): Promise<string[]> {
timeoutMs: ANDROID_BOOT_PROP_TIMEOUT_MS,
});
if (result.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', 'Failed to list Android emulator AVDs', {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
hint: 'Verify Android emulator tooling is installed and available in PATH.',
});
throw new AppError(
'COMMAND_FAILED',
'Failed to list Android emulator AVDs',
execFailureDetails(result, {
hint: 'Verify Android emulator tooling is installed and available in PATH.',
}),
);
}
return parseAndroidAvdList(result.stdout);
}
Expand Down
11 changes: 6 additions & 5 deletions src/platforms/android/logcat.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'node:fs';
import { AppError } from '../../kernel/errors.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import type { AndroidAdbExecutor, AndroidAdbProcess, AndroidAdbProvider } from './adb-executor.ts';

export type AndroidLogcatCaptureOptions = {
Expand Down Expand Up @@ -28,11 +29,11 @@ export async function captureAndroidLogcatWithAdb(
signal: options.signal,
});
if (result.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', 'Failed to capture Android logcat', {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
});
throw new AppError(
'COMMAND_FAILED',
'Failed to capture Android logcat',
execFailureDetails(result),
);
}
return result.stdout;
}
Expand Down
24 changes: 11 additions & 13 deletions src/platforms/android/multitouch-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import { AppError, normalizeError } from '../../kernel/errors.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import { emitDiagnostic, withDiagnosticTimer } from '../../utils/diagnostics.ts';
import { findProjectRoot, readVersion } from '../../utils/version.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
Expand Down Expand Up @@ -396,12 +397,11 @@ export async function runAndroidMultiTouchHelperGesture(options: {
);
}
if (result.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', 'Android multi-touch helper failed', {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
helper: output,
});
throw new AppError(
'COMMAND_FAILED',
'Android multi-touch helper failed',
execFailureDetails(result, { helper: output }),
);
}
return output;
}
Expand Down Expand Up @@ -528,13 +528,11 @@ export async function ensureAndroidMultiTouchHelper(options: {
timeoutMs: ANDROID_MULTITOUCH_HELPER_INSTALL_TIMEOUT_MS,
});
if (result.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', 'Failed to install Android multi-touch helper', {
packageName,
versionCode,
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
});
throw new AppError(
'COMMAND_FAILED',
'Failed to install Android multi-touch helper',
execFailureDetails(result, { packageName, versionCode }),
);
}
installedMultiTouchHelpers.add(cacheKey);
return {
Expand Down
31 changes: 15 additions & 16 deletions src/platforms/android/perf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
import path from 'node:path';
import type { DeviceInfo } from '../../kernel/device.ts';
import { AppError } from '../../kernel/errors.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import { splitNonEmptyTrimmedLines } from '../../utils/parsing.ts';
import { resolveAndroidAdbExecutor, type AndroidAdbExecutor } from './adb-executor.ts';
import { parseNumericToken } from './perf-parsing.ts';
Expand Down Expand Up @@ -133,16 +134,13 @@ export async function captureAndroidHeapSnapshot(
throw new AppError(
'COMMAND_FAILED',
`Failed to capture Android heap dump for ${packageName}`,
{
execFailureDetails(dumpResult, {
kind: 'android-hprof',
package: packageName,
pid,
remotePath,
exitCode: dumpResult.exitCode,
stdout: dumpResult.stdout,
stderr: dumpResult.stderr,
hint: resolveAndroidHeapDumpHint(dumpResult.stdout, dumpResult.stderr),
},
}),
);
}

Expand All @@ -152,17 +150,18 @@ export async function captureAndroidHeapSnapshot(
});
if (pullResult.exitCode !== 0) {
await cleanupLocalArtifact(outPath, hadLocalArtifact);
throw new AppError('COMMAND_FAILED', `Failed to pull Android heap dump for ${packageName}`, {
kind: 'android-hprof',
package: packageName,
pid,
remotePath,
path: outPath,
exitCode: pullResult.exitCode,
stdout: pullResult.stdout,
stderr: pullResult.stderr,
hint: 'Verify the daemon can write the requested --out path and retry. The heap dump stays on-device only until cleanup runs.',
});
throw new AppError(
'COMMAND_FAILED',
`Failed to pull Android heap dump for ${packageName}`,
execFailureDetails(pullResult, {
kind: 'android-hprof',
package: packageName,
pid,
remotePath,
path: outPath,
hint: 'Verify the daemon can write the requested --out path and retry. The heap dump stays on-device only until cleanup runs.',
}),
);
}

const stat = await fs.stat(outPath).catch(() => null);
Expand Down
11 changes: 6 additions & 5 deletions src/platforms/android/settings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppError } from '../../kernel/errors.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
import { requireLocationCoordinates } from '../../utils/location-coordinates.ts';
import {
Expand Down Expand Up @@ -252,11 +253,11 @@ async function resolveAndroidAppearanceTarget(
allowFailure: true,
});
if (currentResult.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', 'Failed to read current Android appearance', {
stdout: currentResult.stdout,
stderr: currentResult.stderr,
exitCode: currentResult.exitCode,
});
throw new AppError(
'COMMAND_FAILED',
'Failed to read current Android appearance',
execFailureDetails(currentResult),
);
}
const current = parseAndroidAppearance(currentResult.stdout, currentResult.stderr);
if (!current) {
Expand Down
12 changes: 6 additions & 6 deletions src/platforms/android/snapshot-helper-capture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppError } from '../../kernel/errors.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import type { SnapshotOptions } from '../../kernel/snapshot.ts';
import {
parseInstrumentationRecords,
Expand Down Expand Up @@ -58,12 +59,11 @@ export async function captureAndroidSnapshotWithHelper(
await removeHelperOutputFile(options.adb, resolved.outputPath);
}
if (result.exitCode !== 0) {
throw new AppError('COMMAND_FAILED', 'Android snapshot helper failed', {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
helper: output.metadata,
});
throw new AppError(
'COMMAND_FAILED',
'Android snapshot helper failed',
execFailureDetails(result, { helper: output.metadata }),
);
}
return output;
}
Expand Down
13 changes: 6 additions & 7 deletions src/platforms/android/snapshot-helper-install.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppError } from '../../kernel/errors.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import {
readAndroidSnapshotHelperInstallOptions,
verifyAndroidSnapshotHelperArtifact,
Expand Down Expand Up @@ -117,13 +118,11 @@ export async function ensureAndroidSnapshotHelper(options: {
);
if (result.exitCode !== 0) {
forgetInstalledSnapshotHelper(installCacheKey);
throw new AppError('COMMAND_FAILED', 'Failed to install Android snapshot helper', {
packageName,
versionCode,
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
});
throw new AppError(
'COMMAND_FAILED',
'Failed to install Android snapshot helper',
execFailureDetails(result, { packageName, versionCode }),
);
}

rememberInstalledSnapshotHelper(installCacheKey, versionCode);
Expand Down
12 changes: 6 additions & 6 deletions src/platforms/android/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { withRetry } from '../../utils/retry.ts';
import { execFailureDetails } from '../../utils/exec.ts';
import { AppError, normalizeError, toAppErrorCode } from '../../kernel/errors.ts';
import { emitDiagnostic, withDiagnosticTimer } from '../../utils/diagnostics.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
Expand Down Expand Up @@ -682,12 +683,11 @@ async function dumpUiHierarchyOnce(adb: AndroidAdbExecutor): Promise<string> {
});
const reportedPath = readDumpPath(dumpResult.stdout, dumpResult.stderr);
if (dumpResult.exitCode !== 0 && !reportedPath) {
throw new AppError('COMMAND_FAILED', 'uiautomator dump did not return XML', {
stdout: dumpResult.stdout,
stderr: dumpResult.stderr,
exitCode: dumpResult.exitCode,
reason: 'missing_fresh_dump',
});
throw new AppError(
'COMMAND_FAILED',
'uiautomator dump did not return XML',
execFailureDetails(dumpResult, { reason: 'missing_fresh_dump' }),
);
}
const actualPath = reportedPath ?? dumpPath;

Expand Down
Loading
Loading