Skip to content

Commit 45745fe

Browse files
authored
fix(android): select available emulator port and improve emulator detection (#399)
1 parent e9f573f commit 45745fe

2 files changed

Lines changed: 170 additions & 2 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { Device } from '../adb';
2+
import { isLikelyEmulator, findAvailableEmulatorPort } from '../run';
3+
4+
describe('android/utils/run', () => {
5+
describe('isLikelyEmulator', () => {
6+
it('detects emulator by serial', () => {
7+
const d = {
8+
serial: 'emulator-5554',
9+
state: 'device',
10+
type: 'hardware',
11+
connection: null,
12+
properties: {},
13+
manufacturer: '',
14+
model: '',
15+
product: '',
16+
sdkVersion: '',
17+
} as Device;
18+
expect(isLikelyEmulator(d)).toBe(true);
19+
});
20+
21+
it('detects emulator by type', () => {
22+
const d = {
23+
serial: 'device-1',
24+
state: 'device',
25+
type: 'emulator',
26+
connection: null,
27+
properties: {},
28+
manufacturer: '',
29+
model: '',
30+
product: '',
31+
sdkVersion: '',
32+
} as Device;
33+
expect(isLikelyEmulator(d)).toBe(true);
34+
});
35+
36+
it('detects emulator by properties.device', () => {
37+
const d = {
38+
serial: 'device-2',
39+
state: 'device',
40+
type: 'hardware',
41+
connection: null,
42+
properties: { device: 'emu64a' },
43+
manufacturer: '',
44+
model: '',
45+
product: '',
46+
sdkVersion: '',
47+
} as Device;
48+
expect(isLikelyEmulator(d)).toBe(true);
49+
});
50+
51+
it('detects emulator by properties.product', () => {
52+
const d = {
53+
serial: 'device-3',
54+
state: 'device',
55+
type: 'hardware',
56+
connection: null,
57+
properties: { product: 'sdk_gphone_arm64' },
58+
manufacturer: '',
59+
model: '',
60+
product: '',
61+
sdkVersion: '',
62+
} as Device;
63+
expect(isLikelyEmulator(d)).toBe(true);
64+
});
65+
66+
it('returns false for real hardware', () => {
67+
const d = {
68+
serial: '0123456789',
69+
state: 'device',
70+
type: 'hardware',
71+
connection: null,
72+
properties: { product: 'samsung' },
73+
manufacturer: '',
74+
model: 'SM-G',
75+
product: '',
76+
sdkVersion: '',
77+
} as Device;
78+
expect(isLikelyEmulator(d)).toBe(false);
79+
});
80+
});
81+
82+
describe('findAvailableEmulatorPort', () => {
83+
it('picks first free even port', async () => {
84+
const devices: Device[] = [
85+
{ serial: 'emulator-5554', type: 'emulator', properties: {}, model: '' } as Device,
86+
{ serial: 'emulator-5556', type: 'emulator', properties: {}, model: '' } as Device,
87+
];
88+
89+
const port = await findAvailableEmulatorPort(devices, 5554, 5560);
90+
expect(port).toBe(5558);
91+
});
92+
93+
it('falls back to 5554 when none found in range', async () => {
94+
// mark the only port in the range as already used
95+
const devices: Device[] = [
96+
{
97+
serial: 'emulator-6000',
98+
state: 'device',
99+
type: 'emulator',
100+
connection: null,
101+
properties: {},
102+
manufacturer: '',
103+
model: '',
104+
product: '',
105+
sdkVersion: '',
106+
} as Device,
107+
];
108+
109+
const port = await findAvailableEmulatorPort(devices, 6000, 6000);
110+
expect(port).toBe(5554);
111+
});
112+
});
113+
});

src/android/utils/run.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,59 @@ import type { SDK } from './sdk';
1717

1818
const modulePrefix = 'native-run:android:utils:run';
1919

20+
export async function findAvailableEmulatorPort(devices: readonly Device[], start = 5554, end = 5584): Promise<number> {
21+
const debug = Debug(`${modulePrefix}:${findAvailableEmulatorPort.name}`);
22+
const usedPorts = new Set<number>();
23+
24+
for (const d of devices) {
25+
const m = d.serial.match(/^emulator-(\d+)$/);
26+
if (m) {
27+
usedPorts.add(Number(m[1]));
28+
}
29+
}
30+
31+
for (let port = start; port <= end; port += 2) {
32+
if (!usedPorts.has(port)) {
33+
debug('Available emulator port found: %d', port);
34+
return port;
35+
}
36+
}
37+
38+
debug('No available emulator ports found in range %d-%d; defaulting to 5554', start, end);
39+
return 5554;
40+
}
41+
42+
export function isLikelyEmulator(device: Device): boolean {
43+
const serialEmu = /^emulator-(\d+)$/;
44+
45+
if (serialEmu.test(device.serial)) {
46+
return true;
47+
}
48+
49+
if (device.type === 'emulator') {
50+
return true;
51+
}
52+
53+
const props = device.properties || {};
54+
const deviceProp = (props['device'] || '').toLowerCase();
55+
const productProp = (props['product'] || '').toLowerCase();
56+
const model = (device.model || '').toLowerCase();
57+
58+
if (deviceProp.startsWith('emu') || deviceProp.includes('generic')) {
59+
return true;
60+
}
61+
62+
if (productProp.includes('sdk_gphone') || productProp.includes('google_sdk')) {
63+
return true;
64+
}
65+
66+
if (model.includes('android_sdk') || model.includes('sdk_gphone')) {
67+
return true;
68+
}
69+
70+
return false;
71+
}
72+
2073
export async function selectDeviceByTarget(
2174
sdk: SDK,
2275
devices: readonly Device[],
@@ -34,7 +87,7 @@ export async function selectDeviceByTarget(
3487
return device;
3588
}
3689

37-
const emulatorDevices = devices.filter((d) => d.type === 'emulator');
90+
const emulatorDevices = devices.filter(isLikelyEmulator);
3891

3992
const pairAVD = async (emulator: Device): Promise<[Device, AVD | undefined]> => {
4093
let avd: AVD | undefined;
@@ -65,7 +118,9 @@ export async function selectDeviceByTarget(
65118

66119
if (avd) {
67120
debug('AVD found by ID: %s', avd.id);
68-
const device = await runEmulator(sdk, avd, 5554); // TODO: 5554 will not always be available at this point
121+
const port = await findAvailableEmulatorPort(devices);
122+
debug('Using emulator port: %d', port);
123+
const device = await runEmulator(sdk, avd, port);
69124
debug('Emulator ready, running avd: %s on %s', avd.id, device.serial);
70125

71126
return device;

0 commit comments

Comments
 (0)