Skip to content

Commit dbc0161

Browse files
committed
fix(android): fail doctor when adb is the Windows binary on a POSIX host
Under WSL, Windows adb.exe passes adb version but resolves host paths as Windows paths, so recording/pull/install break. Read adb's Installed as line and fail the toolchain check with reason android_adb_windows_binary_on_posix_host plus a Linux platform-tools hint.
1 parent 9a245d0 commit dbc0161

5 files changed

Lines changed: 109 additions & 8 deletions

File tree

‎packages/platform-android/src/__tests__/doctor.test.ts‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import assert from 'node:assert/strict';
2+
import { promises as fs } from 'node:fs';
3+
import path from 'node:path';
24
import { afterEach, test, vi } from 'vitest';
35

46
const HELPER_SERVICE = 'com.callstack.agentdevice.imehelper/.TestInputMethodService';
@@ -29,7 +31,8 @@ vi.mock('../ime-helper.ts', async (importOriginal) => {
2931

3032
import { ANDROID_EMULATOR } from './test-utils/device-fixtures.ts';
3133
import type { HostDiagnosticsContext } from '@agent-device/contracts/host-diagnostics';
32-
import { androidDeviceChecks } from '../doctor.ts';
34+
import { androidDeviceChecks, androidToolchainCheck } from '../doctor.ts';
35+
import { mkdtempForTest } from './test-utils/tmp-dir.ts';
3336
import {
3437
resetAndroidTestImeActivationCacheForTests,
3538
setAndroidTestImeActiveForTests,
@@ -39,6 +42,7 @@ import type { DoctorCheck } from '@agent-device/contracts/observability';
3942

4043
afterEach(() => {
4144
resetAndroidTestImeActivationCacheForTests();
45+
vi.unstubAllEnvs();
4246
});
4347

4448
function fakeAdb(currentIme: string, previousIme = 'null'): AndroidAdbExecutor {
@@ -96,3 +100,53 @@ test('falls back to ime list -s when no previous-IME record was persisted', asyn
96100
assert.equal(check?.status, 'fail');
97101
assert.equal(check?.command, `adb -s ${ANDROID_EMULATOR.id} shell ime list -s`);
98102
});
103+
104+
const WINDOWS_ADB_PATH = String.raw`C:\Users\dev\AppData\Local\Android\Sdk\platform-tools\adb.exe`;
105+
const WINDOWS_ADB_VERSION = [
106+
'Android Debug Bridge version 1.0.41',
107+
'Version 35.0.2-12147458',
108+
`Installed as ${WINDOWS_ADB_PATH}`,
109+
'Running on Windows 10.0.26100',
110+
].join('\n');
111+
const LINUX_ADB_VERSION = [
112+
'Android Debug Bridge version 1.0.41',
113+
'Version 35.0.2-12147458',
114+
'Installed as /home/dev/Android/Sdk/platform-tools/adb',
115+
'Running on Linux 6.18.0-microsoft-standard-WSL2 (x86_64)',
116+
].join('\n');
117+
118+
async function toolchainCheckWithAdbVersion(
119+
versionOutput: string,
120+
hostPlatform: NodeJS.Platform = 'linux',
121+
): Promise<DoctorCheck> {
122+
const binDir = await mkdtempForTest('agent-device-android-doctor-');
123+
const adbPath = path.join(binDir, 'adb');
124+
await fs.writeFile(adbPath, `#!/bin/sh\ncat <<'EOF'\n${versionOutput}\nEOF\n`, 'utf8');
125+
await fs.chmod(adbPath, 0o755);
126+
vi.stubEnv('PATH', `${binDir}${path.delimiter}${process.env.PATH ?? ''}`);
127+
return await androidToolchainCheck(
128+
{ ANDROID_HOME: '/mnt/c/Users/dev/AppData/Local/Android/Sdk' },
129+
hostPlatform,
130+
{ access: async () => {} },
131+
);
132+
}
133+
134+
test('fails the toolchain when adb on a POSIX host is the Windows binary', async () => {
135+
const check = await toolchainCheckWithAdbVersion(WINDOWS_ADB_VERSION);
136+
assert.equal(check.status, 'fail');
137+
assert.equal(check.evidence?.reason, 'android_adb_windows_binary_on_posix_host');
138+
assert.equal(check.evidence?.adbPath, WINDOWS_ADB_PATH);
139+
assert.match(check.hint ?? '', /Linux Android platform-tools/);
140+
});
141+
142+
test('passes the toolchain when adb is the Linux binary, even under WSL', async () => {
143+
const check = await toolchainCheckWithAdbVersion(LINUX_ADB_VERSION);
144+
assert.equal(check.status, 'pass');
145+
assert.equal(check.evidence?.reason, undefined);
146+
});
147+
148+
test('passes the toolchain when the Windows binary runs on a Windows host', async () => {
149+
const check = await toolchainCheckWithAdbVersion(WINDOWS_ADB_VERSION, 'win32');
150+
assert.equal(check.status, 'pass');
151+
assert.equal(check.evidence?.reason, undefined);
152+
});

‎packages/platform-android/src/doctor.ts‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device';
33
import { normalizeError } from '@agent-device/kernel/errors';
44
import type { DoctorCheck } from '@agent-device/contracts/observability';
55
import type { HostDiagnosticsContext } from '@agent-device/contracts/host-diagnostics';
6-
import { commandFirstLine } from '@agent-device/provision-kit/toolchain-probe';
6+
import { commandOutput, firstOutputLine } from '@agent-device/provision-kit/toolchain-probe';
77
import { resolveAndroidAdbExecutor, runAdbShell, type AndroidAdbExecutor } from './adb-executor.ts';
88
import {
99
isAndroidTestImeActive,
@@ -18,6 +18,7 @@ import {
1818
} from './adb-host.ts';
1919

2020
const ANDROID_PROBE_TIMEOUT_MS = 2000;
21+
const WINDOWS_ADB_ON_POSIX_HOST_REASON = 'android_adb_windows_binary_on_posix_host';
2122

2223
type AndroidLicenseState = 'accepted' | 'missing' | 'unknown';
2324
type AndroidToolchainProbe = {
@@ -27,13 +28,19 @@ type AndroidToolchainProbe = {
2728
};
2829

2930
export async function androidToolchainCheck(
30-
environment: AndroidAdbEnvironment = requireAndroidAdbHost().environment,
31+
environment: AndroidAdbEnvironment,
32+
hostPlatform: NodeJS.Platform,
3133
files: Pick<AndroidAdbFileHost, 'access'> = requireAndroidAdbHost().files,
3234
): Promise<DoctorCheck> {
3335
const sdkRoot = environment.ANDROID_HOME || environment.ANDROID_SDK_ROOT;
3436
const license = await androidLicenseState(sdkRoot, files);
35-
const versionLine = await commandFirstLine('adb', ['version']);
36-
if (!versionLine) return missingAndroidAdbCheck(sdkRoot, license);
37+
const versionOutput = await commandOutput('adb', ['version']);
38+
const versionLine = versionOutput === undefined ? undefined : firstOutputLine(versionOutput);
39+
if (!versionOutput || !versionLine) return missingAndroidAdbCheck(sdkRoot, license);
40+
const adbPath = androidAdbInstallPath(versionOutput);
41+
if (hostPlatform !== 'win32' && adbPath && isWindowsHostPath(adbPath)) {
42+
return windowsAdbOnPosixHostCheck({ adbPath, sdkRoot, versionLine });
43+
}
3744

3845
return androidAdbCheck({
3946
license,
@@ -173,6 +180,37 @@ async function probeAndroidReverse(
173180
}
174181
}
175182

183+
/** The binary path adb reports for itself on its `Installed as` version line. */
184+
function androidAdbInstallPath(versionOutput: string): string | undefined {
185+
return /^Installed as (.+)$/m.exec(versionOutput)?.[1]?.trim();
186+
}
187+
188+
/** A drive-letter or UNC path, which only a Windows binary reports. */
189+
function isWindowsHostPath(candidate: string): boolean {
190+
return /^(?:[A-Za-z]:[\\/]|\\\\)/.test(candidate);
191+
}
192+
193+
/**
194+
* A Windows adb.exe reached from a Linux host (WSL interop) answers `adb version`, but it resolves
195+
* every host path it is handed as a Windows path, so pulls, pushes, and installs miss the Linux files.
196+
*/
197+
function windowsAdbOnPosixHostCheck(
198+
probe: Readonly<{ adbPath: string; sdkRoot: string | undefined; versionLine: string }>,
199+
): DoctorCheck {
200+
return {
201+
id: 'toolchain',
202+
status: 'fail',
203+
summary: `Android toolchain: adb on PATH is the Windows binary ${probe.adbPath}, which cannot use this host's file paths.`,
204+
hint: 'Under WSL, install Linux Android platform-tools, put them first on PATH, and point ANDROID_HOME at a Linux SDK instead of one under /mnt/<drive>.',
205+
evidence: {
206+
reason: WINDOWS_ADB_ON_POSIX_HOST_REASON,
207+
adbPath: probe.adbPath,
208+
adbVersion: probe.versionLine,
209+
androidHome: probe.sdkRoot ?? null,
210+
},
211+
};
212+
}
213+
176214
function androidAdbCheck(probe: AndroidToolchainProbe): DoctorCheck {
177215
return {
178216
id: 'toolchain',

‎packages/provision-kit/src/toolchain-probe.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ import { runCmd } from '@agent-device/host-kit/command';
33
export const TOOLCHAIN_TIMEOUT_MS = 3_000;
44

55
export async function commandFirstLine(cmd: string, args: string[]): Promise<string | undefined> {
6+
const output = await commandOutput(cmd, args);
7+
return output === undefined ? undefined : firstOutputLine(output);
8+
}
9+
10+
/** Stdout of a toolchain probe that exited 0, or undefined when the tool is missing or failed. */
11+
export async function commandOutput(cmd: string, args: string[]): Promise<string | undefined> {
612
try {
713
const result = await runCmd(cmd, args, { allowFailure: true, timeoutMs: TOOLCHAIN_TIMEOUT_MS });
8-
if (result.exitCode !== 0) return undefined;
9-
return firstOutputLine(result.stdout);
14+
return result.exitCode === 0 ? result.stdout : undefined;
1015
} catch {
1116
return undefined;
1217
}

‎src/platform-runtime-host-diagnostics.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function createHostDiagnostics(): HostDiagnostics {
1919
): Promise<DoctorCheck | undefined> => {
2020
if (platform === 'android') {
2121
const { androidToolchainCheck } = await loadAndroidMechanics();
22-
return await androidToolchainCheck(process.env);
22+
return await androidToolchainCheck(process.env, process.platform);
2323
}
2424
if (platform === 'vega') {
2525
const { vegaToolchainCheck } = await import('@agent-device/platform-vega');

‎website/docs/docs/known-limitations.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ Some OEM builds gate the first install of a package behind the system package in
4444

4545
An unattended first Android snapshot therefore times out with a helper install failure whose hint says to check the device screen for a pending install confirmation. Confirm the prompts on the device and retry; if no dialog is showing, restart the ADB server as the hint says.
4646

47+
## Android: WSL needs Linux platform-tools
48+
49+
Under WSL, the Windows `adb.exe` (for example from an `ANDROID_HOME` under `/mnt/c`) answers `adb version` but resolves every host path it is given as a Windows path, so recordings, pulls, and installs fail. Install Linux Android platform-tools inside WSL, put them first on `PATH`, and point `ANDROID_HOME` at a Linux SDK. `agent-device doctor` fails the toolchain check with reason `android_adb_windows_binary_on_posix_host` when `adb` reports a Windows install path.
50+
4751
## Android: no clipboard access over adb on Android 16
4852

4953
`agent-device` reaches the Android clipboard through `adb shell cmd clipboard`. That command works only on a build whose clipboard service implements a shell command, and AOSP's `ClipboardService` does not: the class carries no shell command at `android13-release`, `android14-release`, `android15-release` or `android16-release`, nor on current AOSP `main`, and a physical device runs that same class. On Android 16 (API 36) every `cmd clipboard get text` and `cmd clipboard set text <text>` call is answered by the framework's default `Binder.handleShellCommand` — `No shell command implementation.` on stderr, exit status **0** — so the clipboard is never touched even though the call reports success.

0 commit comments

Comments
 (0)