Skip to content

Commit 0258fe2

Browse files
committed
Use Android real device names, and change Frida API to id-details map
1 parent 70ebc6d commit 0258fe2

File tree

8 files changed

+147
-64
lines changed

8 files changed

+147
-64
lines changed

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"tar-stream": "^2.2.0",
9292
"tmp": "0.0.33",
9393
"tslib": "^1.9.3",
94-
"usbmux-client": "^0.1.1",
94+
"usbmux-client": "^0.2.0",
9595
"win-version-info": "^5.0.1"
9696
},
9797
"devDependencies": {

src/interceptors/android/adb-commands.ts

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,39 +71,111 @@ const batchCalls = <A extends any[], R>(
7171
};
7272
}
7373

74-
export const getConnectedDevices = batchCalls(async (adbClient: Adb.Client) => {
75-
try {
76-
const devices = await (adbClient.listDevices() as Promise<Adb.Device[]>);
77-
return devices
78-
.filter((d) =>
79-
d.type !== 'offline' &&
80-
d.type !== 'unauthorized' &&
81-
!d.type.startsWith("no permissions")
82-
).map(d => d.id);
83-
} catch (e) {
84-
if (isErrorLike(e) && (
85-
e.code === 'ENOENT' || // No ADB available
86-
e.code === 'EACCES' || // ADB available, but we aren't allowed to run it
87-
e.code === 'EPERM' || // Permissions error launching ADB
88-
e.code === 'ECONNREFUSED' || // Tried to start ADB, but still couldn't connect
89-
e.code === 'ENOTDIR' || // ADB path contains something that's not a directory
90-
e.signal === 'SIGKILL' || // In some envs 'adb start-server' is always killed (why?)
91-
(e.cmd && e.code) // ADB available, but "adb start-server" failed
92-
)
93-
) {
94-
if (e.code !== 'ENOENT') {
95-
console.log(`ADB unavailable, ${e.cmd
96-
? `${e.cmd} exited with ${e.code}`
97-
: `due to ${e.code}`
98-
}`);
74+
export const getConnectedDevices = batchCalls(
75+
async (adbClient: Adb.Client): Promise<Record<string, Record<string, string>>> => {
76+
try {
77+
const devices = await (adbClient.listDevices() as Promise<Adb.Device[]>);
78+
const deviceIds = devices
79+
.filter((d) =>
80+
d.type !== 'offline' &&
81+
d.type !== 'unauthorized' &&
82+
!d.type.startsWith("no permissions")
83+
).map(d => d.id);
84+
85+
const deviceDetails = Object.fromEntries(await Promise.all(
86+
deviceIds.map(async (id): Promise<[string, Record<string, string>]> => {
87+
const name = await getDeviceName(adbClient, id);
88+
return [id, { id, name }];
89+
})
90+
));
91+
92+
// Clear any non-present device names from the cache
93+
filterDeviceNameCache(deviceIds);
94+
return deviceDetails;
95+
} catch (e) {
96+
if (isErrorLike(e) && (
97+
e.code === 'ENOENT' || // No ADB available
98+
e.code === 'EACCES' || // ADB available, but we aren't allowed to run it
99+
e.code === 'EPERM' || // Permissions error launching ADB
100+
e.code === 'ECONNREFUSED' || // Tried to start ADB, but still couldn't connect
101+
e.code === 'ENOTDIR' || // ADB path contains something that's not a directory
102+
e.signal === 'SIGKILL' || // In some envs 'adb start-server' is always killed (why?)
103+
(e.cmd && e.code) // ADB available, but "adb start-server" failed
104+
)
105+
) {
106+
if (e.code !== 'ENOENT') {
107+
console.log(`ADB unavailable, ${e.cmd
108+
? `${e.cmd} exited with ${e.code}`
109+
: `due to ${e.code}`
110+
}`);
111+
}
112+
return {};
113+
} else {
114+
logError(e);
115+
throw e;
99116
}
100-
return [];
117+
}
118+
}
119+
);
120+
121+
122+
const cachedDeviceNames: { [deviceId: string]: string | undefined } = {};
123+
124+
const getDeviceName = async (adbClient: Adb.Client, deviceId: string) => {
125+
if (cachedDeviceNames[deviceId]) {
126+
return cachedDeviceNames[deviceId]!;
127+
}
128+
129+
let deviceName: string;
130+
try {
131+
const device = adbClient.getDevice(deviceId);
132+
133+
if (deviceId.startsWith('emulator-')) {
134+
const props = await device.getProperties();
135+
136+
const avdName = (
137+
props['ro.boot.qemu.avd_name'] || // New emulators
138+
props['ro.kernel.qemu.avd_name'] // Old emulators
139+
)?.replace(/_/g, ' ');
140+
141+
const osVersion = props['ro.build.version.release'];
142+
143+
deviceName = avdName || `Android ${osVersion} emulator`;
101144
} else {
102-
logError(e);
103-
throw e;
145+
const name = (
146+
await run(device, ['settings', 'get', 'global', 'device_name'])
147+
.catch(() => {})
148+
)?.trim();
149+
150+
if (name) {
151+
deviceName = name;
152+
} else {
153+
const props = await device.getProperties();
154+
155+
deviceName = props['ro.product.model'] ||
156+
deviceId;
157+
}
104158
}
159+
} catch (e: any) {
160+
console.log(`Error getting device name for ${deviceId}`, e.message);
161+
deviceName = deviceId;
162+
// N.b. we do cache despite the error - many errors could be persistent, and it's
163+
// no huge problem (and more consistent) to stick with the raw id instead.
105164
}
106-
})
165+
166+
cachedDeviceNames[deviceId] = deviceName;
167+
return deviceName;
168+
};
169+
170+
// Clear any non-connected device names from the cache (to avoid leaks, and
171+
// so that we do update the name if they reconnect later.)
172+
const filterDeviceNameCache = (connectedIds: string[]) => {
173+
Object.keys(cachedDeviceNames).forEach((id) => {
174+
if (!connectedIds.includes(id)) {
175+
delete cachedDeviceNames[id];
176+
}
177+
});
178+
};
107179

108180
export function stringAsStream(input: string) {
109181
const contentStream = new stream.Readable();

src/interceptors/android/android-adb-interceptor.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class AndroidAdbInterceptor implements Interceptor {
4848
) { }
4949

5050
async isActivable(): Promise<boolean> {
51-
return (await getConnectedDevices(this.adbClient)).length > 0;
51+
return Object.keys(await getConnectedDevices(this.adbClient)).length > 0;
5252
}
5353

5454
activableTimeout = 3000; // Increase timeout for device detection slightly
@@ -57,9 +57,12 @@ export class AndroidAdbInterceptor implements Interceptor {
5757
return false;
5858
}
5959

60-
async getMetadata(): Promise<{ deviceIds: string[] }> {
60+
async getMetadata(): Promise<{ deviceIds: string[], devices: Record<string, Record<string, string>> }> {
61+
const devices = await getConnectedDevices(this.adbClient);
62+
6163
return {
62-
deviceIds: await getConnectedDevices(this.adbClient)
64+
deviceIds: Object.keys(devices),
65+
devices: devices
6366
};
6467
}
6568

src/interceptors/frida/frida-android-integration.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,21 @@ const isDevicePortOpen = (deviceClient: DeviceClient, port: number) =>
4949
return false
5050
});
5151

52-
export async function getAndroidFridaHosts(adbClient: AdbClient): Promise<FridaHost[]> {
52+
export async function getAndroidFridaHosts(adbClient: AdbClient): Promise<Record<string, FridaHost>> {
5353
const devices = await getConnectedDevices(adbClient);
5454

55-
const result = await Promise.all(
56-
devices.map((deviceId) => getHostStatus(adbClient, deviceId)
55+
return Object.fromEntries(await Promise.all(
56+
Object.entries(devices).map(async ([id, device]) => [
57+
id, {
58+
...device,
59+
type: 'android',
60+
state: await getHostState(adbClient, id)
61+
}
62+
])
5763
));
58-
59-
return result;
6064
}
6165

62-
const getHostStatus = async (adbClient: AdbClient, deviceId: string) => {
66+
const getHostState = async (adbClient: AdbClient, deviceId: string) => {
6367
const deviceClient = adbClient.getDevice(deviceId);
6468

6569
let state: FridaHost['state'] = 'unavailable';
@@ -83,12 +87,7 @@ const getHostStatus = async (adbClient: AdbClient, deviceId: string) => {
8387
state = 'unavailable';
8488
}
8589

86-
return {
87-
id: deviceId,
88-
name: deviceId,
89-
type: 'android',
90-
state
91-
} as const;
90+
return state;
9291
};
9392

9493
const ANDROID_ABI_FRIDA_ARCH_MAP = {
@@ -143,8 +142,7 @@ export async function launchAndroidHost(adbClient: AdbClient, hostId: string) {
143142
try {
144143
await waitUntil(500, 10, async () => {
145144
try {
146-
const status = await getHostStatus(adbClient, hostId);
147-
return status.state === 'available';
145+
return await getHostState(adbClient, hostId) === 'available';
148146
} catch (e: any) {
149147
console.log(e.message ?? e);
150148
return false;

src/interceptors/frida/frida-android-interceptor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Interceptor } from "..";
66
import { HtkConfig } from '../../config';
77

88
import { createAdbClient } from '../android/adb-commands';
9-
import { FridaTarget, killProcess } from './frida-integration';
9+
import { FridaHost, FridaTarget, killProcess } from './frida-integration';
1010
import {
1111
getAndroidFridaHosts,
1212
getAndroidFridaTargets,
@@ -30,14 +30,14 @@ export class FridaAndroidInterceptor implements Interceptor {
3030
getFridaHosts = combineParallelCalls(() => getAndroidFridaHosts(this.adbClient));
3131

3232
async isActivable(): Promise<boolean> {
33-
return (await this.getFridaHosts()).length > 0;
33+
return Object.keys(await this.getFridaHosts()).length > 0;
3434
}
3535

3636
isActive(): boolean {
3737
return false;
3838
}
3939

40-
async getMetadata() {
40+
async getMetadata(): Promise<{ hosts: Record<string, FridaHost> }> {
4141
const fridaHosts = await this.getFridaHosts();
4242
return {
4343
hosts: fridaHosts

src/interceptors/frida/frida-ios-integration.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const isDevicePortOpen = (usbmuxClient: UsbmuxClient, deviceId: number, port: nu
2626
// time that we fail to reach Usbmux (just for general reference of any issues).
2727
let loggedUsbmuxFailure = false;
2828

29-
export async function getIosFridaHosts(usbmuxClient: UsbmuxClient): Promise<FridaHost[]> {
29+
export async function getIosFridaHosts(usbmuxClient: UsbmuxClient): Promise<Record<string, FridaHost>> {
3030
const devices = await usbmuxClient.getDevices().catch((e) => {
3131
if (!loggedUsbmuxFailure) {
3232
console.log('Usbmux iOS scanning failed:', e.message);
@@ -35,14 +35,24 @@ export async function getIosFridaHosts(usbmuxClient: UsbmuxClient): Promise<Frid
3535
return [];
3636
});
3737

38-
const result = await Promise.all(
39-
Object.values(devices).map(({ DeviceID }) => getHostStatus(usbmuxClient, DeviceID as number)
38+
return Object.fromEntries(await Promise.all(
39+
Object.values(devices) // N.b. we drop the key, which is just an index (not a useful consistent id)
40+
.map(async (device): Promise<[string, FridaHost]> => {
41+
const details = await getHostDetails(usbmuxClient, device.DeviceID);
42+
return [
43+
details.id, {
44+
...device,
45+
type: 'ios',
46+
id: details.id, // iOS HostId !== DeviceId
47+
name: details.name,
48+
state: details.state
49+
}
50+
];
51+
})
4052
));
41-
42-
return result;
4353
}
4454

45-
const getHostStatus = async (usbmuxClient: UsbmuxClient, deviceId: number) => {
55+
const getHostDetails = async (usbmuxClient: UsbmuxClient, deviceId: number) => {
4656
const deviceMetadataPromise = usbmuxClient.queryAllDeviceValues(deviceId);
4757

4858
let state: FridaHost['state'] = 'unavailable';

src/interceptors/frida/frida-ios-interceptor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { combineParallelCalls } from '@httptoolkit/util';
66
import { Interceptor } from "..";
77
import { HtkConfig } from '../../config';
88

9-
import { FridaTarget, killProcess } from './frida-integration';
9+
import { FridaHost, FridaTarget, killProcess } from './frida-integration';
1010
import {
1111
getIosFridaHosts,
1212
getIosFridaTargets,
@@ -27,14 +27,14 @@ export class FridaIosInterceptor implements Interceptor {
2727
getFridaHosts = combineParallelCalls(() => getIosFridaHosts(this.usbmuxClient));
2828

2929
async isActivable(): Promise<boolean> {
30-
return (await this.getFridaHosts()).length > 0;
30+
return Object.keys(await this.getFridaHosts()).length > 0;
3131
}
3232

3333
isActive(): boolean {
3434
return false;
3535
}
3636

37-
async getMetadata() {
37+
async getMetadata(): Promise<{ hosts: Record<string, FridaHost> }> {
3838
const fridaHosts = await this.getFridaHosts();
3939
return {
4040
hosts: fridaHosts

0 commit comments

Comments
 (0)