Skip to content

Commit b9db59e

Browse files
committed
fix(open): boot the resolved stopped AVD without forwarding --serial
The shared boot step forwarded the request's --serial to readiness, so an install or open of a stopped AVD with --serial naming another device could wait for that device instead of the resolved AVD. The resolved device is the boot target, so only the Android serial allowlist reaches readiness. `boot` keeps passing --serial through resolveDeviceReadinessInput. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_016qAu53rMak1bXMuR5U2gF4
1 parent dd69b16 commit b9db59e

2 files changed

Lines changed: 62 additions & 11 deletions

File tree

‎packages/daemon/src/core/stopped-local-android-device.ts‎

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,29 @@ import type { EnvMap } from '@agent-device/kernel/source-value';
77
import type { DaemonRequest } from './daemon-request.ts';
88
import type { BindDeviceRuntime } from './request-runtime-binding.ts';
99

10-
type DeviceReadinessFlags = Pick<
11-
NonNullable<DaemonRequest['flags']>,
12-
'serial' | 'androidDeviceAllowlist'
13-
>;
10+
type RequestFlags = NonNullable<DaemonRequest['flags']>;
11+
12+
/** The request's Android serial allowlist, from `--android-device-allowlist` or the environment. */
13+
function androidSerialAllowlistInput(
14+
flags: Pick<RequestFlags, 'androidDeviceAllowlist'>,
15+
env: EnvMap,
16+
): readonly string[] | undefined {
17+
const allowlist = resolveAndroidSerialAllowlist(flags.androidDeviceAllowlist, env);
18+
return allowlist ? [...allowlist].sort() : undefined;
19+
}
1420

1521
/**
16-
* The readiness input of a request that boots a device. It carries the request's `--serial` and
17-
* Android serial allowlist, so a boot only accepts a device the request may use.
22+
* The readiness input of `boot`. It carries the request's `--serial` and Android serial
23+
* allowlist, so a boot only accepts a device the request may use.
1824
*/
1925
export function resolveDeviceReadinessInput(params: {
20-
flags: DeviceReadinessFlags;
26+
flags: Pick<RequestFlags, 'serial' | 'androidDeviceAllowlist'>;
2127
env: EnvMap;
2228
deadlineAtMs: number | undefined;
2329
}): EnsureReadyInput {
24-
const allowlist = resolveAndroidSerialAllowlist(params.flags.androidDeviceAllowlist, params.env);
2530
return {
2631
serial: params.flags.serial,
27-
androidSerialAllowlist: allowlist ? [...allowlist].sort() : undefined,
32+
androidSerialAllowlist: androidSerialAllowlistInput(params.flags, params.env),
2833
deadlineAtMs: params.deadlineAtMs,
2934
};
3035
}
@@ -34,12 +39,15 @@ export function resolveDeviceReadinessInput(params: {
3439
* its AVD name, and returns the booted device with its adb serial. A command that addresses such
3540
* a device continues with the returned identity, so every later binding, claim and lock keys on
3641
* the serial. Returns undefined when the device needs no boot here.
42+
*
43+
* The resolved device is the boot target, so the request's `--serial` does not reach readiness.
44+
* Only the Android serial allowlist does: it bounds which serial the boot may come up on.
3745
*/
3846
export async function bootStoppedLocalAndroidDevice(params: {
3947
device: DeviceInfo;
4048
owner: RuntimeOwnerRef;
4149
bindDevice: BindDeviceRuntime;
42-
flags: DeviceReadinessFlags;
50+
flags: Pick<RequestFlags, 'androidDeviceAllowlist'>;
4351
env: EnvMap;
4452
deadlineAtMs: number | undefined;
4553
}): Promise<DeviceInfo | undefined> {
@@ -48,5 +56,8 @@ export async function bootStoppedLocalAndroidDevice(params: {
4856
return undefined;
4957
}
5058
const readiness = await params.bindDevice(device, ensureReadyUse);
51-
return await readiness.operations.ensureReady(resolveDeviceReadinessInput(params));
59+
return await readiness.operations.ensureReady({
60+
androidSerialAllowlist: androidSerialAllowlistInput(params.flags, params.env),
61+
deadlineAtMs: params.deadlineAtMs,
62+
});
5263
}

‎packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,46 @@ test('open of a stopped AVD under an allowlist that excludes its booted serial c
261261
assert.equal(store.get('claim-allowlist'), undefined);
262262
});
263263

264+
test('open of a stopped AVD boots the resolved AVD whatever --serial names', async () => {
265+
const { store, stateDir } = setup();
266+
const stoppedAvd = { ...android, id: 'pixel-avd', booted: false };
267+
mockResolveTargetDevice.mockResolvedValue(stoppedAvd);
268+
// Android readiness waits for the serial it is given, so a forwarded --serial for another
269+
// device would wait for that device instead of the AVD that was resolved.
270+
const readinessInputs: EnsureReadyInput[] = [];
271+
const ensureReady = async (input: EnsureReadyInput): Promise<DeviceInfo> => {
272+
readinessInputs.push(input);
273+
if (input.serial && input.serial !== android.id) {
274+
throw new AppError('COMMAND_FAILED', 'Android emulator did not appear in time', {
275+
reason: BOOT_TIMEOUT_REASON,
276+
});
277+
}
278+
return android;
279+
};
280+
281+
const response = await handleOpenCommand({
282+
host: createTestHostEnvironment(),
283+
createCommandSurface: fakeCreateCommandSurface,
284+
platformServices: daemonPlatformServicesFixture(),
285+
req: {
286+
command: 'open',
287+
token: 'test',
288+
session: 'claim-serial',
289+
positionals: ['Demo'],
290+
flags: { platform: 'android', device: stoppedAvd.name, serial: 'emulator-5560' },
291+
},
292+
sessionName: 'claim-serial',
293+
logPath: path.join(stateDir, 'daemon.log'),
294+
sessionStore: store,
295+
bindDevice: bindBootingLifecycleRuntime(ensureReady),
296+
});
297+
298+
assert.equal(response.ok, true);
299+
assert.equal(readinessInputs.length, 1);
300+
assert.equal(readinessInputs[0]?.serial, undefined);
301+
assert.equal(store.get('claim-serial')?.device.id, android.id);
302+
});
303+
264304
test('failed local open after dispatch retains its device claim for recovery', async () => {
265305
const { store, stateDir } = setup();
266306
mockResolveTargetDevice.mockResolvedValue(android);

0 commit comments

Comments
 (0)