@appium/coresim 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +66 -0
- package/binding.gyp +53 -0
- package/lib/src/commands/app.d.ts +88 -0
- package/lib/src/commands/app.d.ts.map +1 -0
- package/lib/src/commands/app.js +133 -0
- package/lib/src/commands/app.js.map +1 -0
- package/lib/src/commands/biometric.d.ts +33 -0
- package/lib/src/commands/biometric.d.ts.map +1 -0
- package/lib/src/commands/biometric.js +44 -0
- package/lib/src/commands/biometric.js.map +1 -0
- package/lib/src/commands/darwin-notification.d.ts +36 -0
- package/lib/src/commands/darwin-notification.d.ts.map +1 -0
- package/lib/src/commands/darwin-notification.js +35 -0
- package/lib/src/commands/darwin-notification.js.map +1 -0
- package/lib/src/commands/interaction.d.ts +48 -0
- package/lib/src/commands/interaction.d.ts.map +1 -0
- package/lib/src/commands/interaction.js +48 -0
- package/lib/src/commands/interaction.js.map +1 -0
- package/lib/src/commands/keychain.d.ts +25 -0
- package/lib/src/commands/keychain.d.ts.map +1 -0
- package/lib/src/commands/keychain.js +52 -0
- package/lib/src/commands/keychain.js.map +1 -0
- package/lib/src/commands/lifecycle.d.ts +101 -0
- package/lib/src/commands/lifecycle.d.ts.map +1 -0
- package/lib/src/commands/lifecycle.js +165 -0
- package/lib/src/commands/lifecycle.js.map +1 -0
- package/lib/src/commands/media.d.ts +28 -0
- package/lib/src/commands/media.d.ts.map +1 -0
- package/lib/src/commands/media.js +27 -0
- package/lib/src/commands/media.js.map +1 -0
- package/lib/src/commands/misc.d.ts +15 -0
- package/lib/src/commands/misc.d.ts.map +1 -0
- package/lib/src/commands/misc.js +12 -0
- package/lib/src/commands/misc.js.map +1 -0
- package/lib/src/commands/pasteboard.d.ts +24 -0
- package/lib/src/commands/pasteboard.d.ts.map +1 -0
- package/lib/src/commands/pasteboard.js +22 -0
- package/lib/src/commands/pasteboard.js.map +1 -0
- package/lib/src/commands/permissions.d.ts +48 -0
- package/lib/src/commands/permissions.d.ts.map +1 -0
- package/lib/src/commands/permissions.js +84 -0
- package/lib/src/commands/permissions.js.map +1 -0
- package/lib/src/commands/process.d.ts +16 -0
- package/lib/src/commands/process.d.ts.map +1 -0
- package/lib/src/commands/process.js +68 -0
- package/lib/src/commands/process.js.map +1 -0
- package/lib/src/commands/screenshot.d.ts +29 -0
- package/lib/src/commands/screenshot.d.ts.map +1 -0
- package/lib/src/commands/screenshot.js +30 -0
- package/lib/src/commands/screenshot.js.map +1 -0
- package/lib/src/commands/spawn.d.ts +48 -0
- package/lib/src/commands/spawn.d.ts.map +1 -0
- package/lib/src/commands/spawn.js +108 -0
- package/lib/src/commands/spawn.js.map +1 -0
- package/lib/src/commands/ui.d.ts +42 -0
- package/lib/src/commands/ui.d.ts.map +1 -0
- package/lib/src/commands/ui.js +44 -0
- package/lib/src/commands/ui.js.map +1 -0
- package/lib/src/commands/webinspector.d.ts +19 -0
- package/lib/src/commands/webinspector.d.ts.map +1 -0
- package/lib/src/commands/webinspector.js +16 -0
- package/lib/src/commands/webinspector.js.map +1 -0
- package/lib/src/errors.d.ts +51 -0
- package/lib/src/errors.d.ts.map +1 -0
- package/lib/src/errors.js +82 -0
- package/lib/src/errors.js.map +1 -0
- package/lib/src/index.d.ts +7 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/src/index.js +5 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/native-simctl.d.ts +62 -0
- package/lib/src/native-simctl.d.ts.map +1 -0
- package/lib/src/native-simctl.js +221 -0
- package/lib/src/native-simctl.js.map +1 -0
- package/lib/src/types.d.ts +267 -0
- package/lib/src/types.d.ts.map +1 -0
- package/lib/src/types.js +29 -0
- package/lib/src/types.js.map +1 -0
- package/lib/src/utils/index.d.ts +3 -0
- package/lib/src/utils/index.d.ts.map +1 -0
- package/lib/src/utils/index.js +3 -0
- package/lib/src/utils/index.js.map +1 -0
- package/lib/src/utils/pkg-root.d.ts +6 -0
- package/lib/src/utils/pkg-root.d.ts.map +1 -0
- package/lib/src/utils/pkg-root.js +12 -0
- package/lib/src/utils/pkg-root.js.map +1 -0
- package/lib/src/utils/run-catching.d.ts +3 -0
- package/lib/src/utils/run-catching.d.ts.map +1 -0
- package/lib/src/utils/run-catching.js +11 -0
- package/lib/src/utils/run-catching.js.map +1 -0
- package/package.json +73 -0
- package/scripts/install.mjs +22 -0
- package/src/commands/app.ts +186 -0
- package/src/commands/biometric.ts +69 -0
- package/src/commands/darwin-notification.ts +51 -0
- package/src/commands/interaction.ts +74 -0
- package/src/commands/keychain.ts +63 -0
- package/src/commands/lifecycle.ts +210 -0
- package/src/commands/media.ts +38 -0
- package/src/commands/misc.ts +20 -0
- package/src/commands/pasteboard.ts +31 -0
- package/src/commands/permissions.ts +122 -0
- package/src/commands/process.ts +80 -0
- package/src/commands/screenshot.ts +46 -0
- package/src/commands/spawn.ts +133 -0
- package/src/commands/ui.ts +61 -0
- package/src/commands/webinspector.ts +23 -0
- package/src/coresim.mm +1066 -0
- package/src/errors.ts +93 -0
- package/src/index.ts +23 -0
- package/src/native/async_bridge.h +164 -0
- package/src/native/nserror_bridge.h +64 -0
- package/src/native/nserror_bridge.mm +30 -0
- package/src/native/objc_runtime.h +45 -0
- package/src/native/objc_runtime.mm +76 -0
- package/src/native/safe_dispatch.h +38 -0
- package/src/native/sim_device.h +143 -0
- package/src/native/sim_device.mm +355 -0
- package/src/native/sim_device_set.h +16 -0
- package/src/native/sim_device_set.mm +40 -0
- package/src/native/sim_pasteboard.h +16 -0
- package/src/native/sim_pasteboard.mm +295 -0
- package/src/native/sim_process.h +13 -0
- package/src/native/sim_process.mm +139 -0
- package/src/native/sim_screenshot.h +28 -0
- package/src/native/sim_screenshot.mm +226 -0
- package/src/native/sim_service_context.h +42 -0
- package/src/native/sim_service_context.mm +92 -0
- package/src/native/tcc_privacy.h +35 -0
- package/src/native/tcc_privacy.mm +256 -0
- package/src/native/value_bridge.h +18 -0
- package/src/native/value_bridge.mm +111 -0
- package/src/native-simctl.ts +288 -0
- package/src/types.ts +302 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/pkg-root.ts +14 -0
- package/src/utils/run-catching.ts +10 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
2
|
+
import {runCatchingAsync} from '../utils/index.js';
|
|
3
|
+
|
|
4
|
+
declare module '../native-simctl.js' {
|
|
5
|
+
interface NativeSimctl {
|
|
6
|
+
addMedia(udid: string, filePaths: string[]): Promise<void>;
|
|
7
|
+
addPhoto(udid: string, filePath: string): Promise<void>;
|
|
8
|
+
addVideo(udid: string, filePath: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Adds one or more photo/video files to the device's Photos library — the native equivalent of
|
|
14
|
+
* `simctl addmedia`. Each file's type is auto-detected; use {@link addPhoto}/{@link addVideo}
|
|
15
|
+
* instead when the file's kind is already known.
|
|
16
|
+
*
|
|
17
|
+
* @param udid — UDID of the target device
|
|
18
|
+
* @param filePaths — paths to the media files on the local filesystem
|
|
19
|
+
*/
|
|
20
|
+
export async function addMedia(this: NativeSimctl, udid: string, filePaths: string[]): Promise<void> {
|
|
21
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).addMedia(filePaths));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param udid — UDID of the target device
|
|
26
|
+
* @param filePath — path to a photo file on the local filesystem
|
|
27
|
+
*/
|
|
28
|
+
export async function addPhoto(this: NativeSimctl, udid: string, filePath: string): Promise<void> {
|
|
29
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).addPhoto(filePath));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param udid — UDID of the target device
|
|
34
|
+
* @param filePath — path to a video file on the local filesystem
|
|
35
|
+
*/
|
|
36
|
+
export async function addVideo(this: NativeSimctl, udid: string, filePath: string): Promise<void> {
|
|
37
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).addVideo(filePath));
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
2
|
+
|
|
3
|
+
declare module '../native-simctl.js' {
|
|
4
|
+
interface NativeSimctl {
|
|
5
|
+
shake(udid: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SHAKE_NOTIFICATION_NAME = 'com.apple.UIKit.SimulatorShake';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Simulates a shake gesture (e.g. to trigger "Shake to Undo" or a shake-triggered debug menu) —
|
|
13
|
+
* the same Darwin notification (see darwin-notification.ts) Simulator.app's own Device > Shake
|
|
14
|
+
* menu item posts.
|
|
15
|
+
*
|
|
16
|
+
* @param udid — UDID of the target device
|
|
17
|
+
*/
|
|
18
|
+
export async function shake(this: NativeSimctl, udid: string): Promise<void> {
|
|
19
|
+
await this.postDarwinNotification(udid, SHAKE_NOTIFICATION_NAME);
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
2
|
+
import {runCatchingAsync} from '../utils/index.js';
|
|
3
|
+
|
|
4
|
+
declare module '../native-simctl.js' {
|
|
5
|
+
interface NativeSimctl {
|
|
6
|
+
getPasteboard(udid: string): Promise<string>;
|
|
7
|
+
setPasteboard(udid: string, content: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reads the device's current pasteboard content — the native equivalent of `simctl pbpaste`
|
|
13
|
+
* (see native/sim_pasteboard.mm for which private CoreSimulator API this uses). Rejects with
|
|
14
|
+
* `NativeSimUnavailableError` if neither is present.
|
|
15
|
+
*
|
|
16
|
+
* @param udid — UDID of the device to read from; must be booted
|
|
17
|
+
* @returns the pasteboard's string content, or `""` if it holds no string-typed content
|
|
18
|
+
*/
|
|
19
|
+
export async function getPasteboard(this: NativeSimctl, udid: string): Promise<string> {
|
|
20
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).getPasteboard());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sets the device's pasteboard content — the native equivalent of `simctl pbcopy`.
|
|
25
|
+
*
|
|
26
|
+
* @param udid — UDID of the target device; must be booted
|
|
27
|
+
* @param content — string content to set
|
|
28
|
+
*/
|
|
29
|
+
export async function setPasteboard(this: NativeSimctl, udid: string, content: string): Promise<void> {
|
|
30
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).setPasteboard(content));
|
|
31
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
2
|
+
import type {SimPermissionService, SimPermissionStatus} from '../types.js';
|
|
3
|
+
import {runCatchingAsync} from '../utils/index.js';
|
|
4
|
+
|
|
5
|
+
declare module '../native-simctl.js' {
|
|
6
|
+
interface NativeSimctl {
|
|
7
|
+
grantPermission(udid: string, service: SimPermissionService, bundleId: string): Promise<void>;
|
|
8
|
+
revokePermission(udid: string, service: SimPermissionService, bundleId: string): Promise<void>;
|
|
9
|
+
resetPermission(udid: string, service: SimPermissionService, bundleId: string): Promise<void>;
|
|
10
|
+
getPermission(udid: string, service: SimPermissionService, bundleId: string): Promise<SimPermissionStatus>;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Grants a privacy permission to the given app on the given device, by writing directly to the
|
|
16
|
+
* simulator's own TCC (privacy) database — see CLAUDE.md for why this bypasses CoreSimulator's own
|
|
17
|
+
* privacy API.
|
|
18
|
+
*
|
|
19
|
+
* @param udid — UDID of the target device
|
|
20
|
+
* @param service — permission to grant, e.g. `"camera"`, `"contacts"`, `"photos"`
|
|
21
|
+
* @param bundleId — bundle identifier of the app the permission applies to
|
|
22
|
+
*/
|
|
23
|
+
export async function grantPermission(
|
|
24
|
+
this: NativeSimctl,
|
|
25
|
+
udid: string,
|
|
26
|
+
service: SimPermissionService,
|
|
27
|
+
bundleId: string,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
return runCatchingAsync(async () => {
|
|
30
|
+
const tccIdentifier = toTCCIdentifier(service);
|
|
31
|
+
return (await this._findDevice(udid)).grantPermission(tccIdentifier, bundleId);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Revokes a previously granted privacy permission from the given app.
|
|
37
|
+
*
|
|
38
|
+
* @param udid — UDID of the target device
|
|
39
|
+
* @param service — permission to revoke (see {@link grantPermission})
|
|
40
|
+
* @param bundleId — bundle identifier of the app the permission applies to
|
|
41
|
+
*/
|
|
42
|
+
export async function revokePermission(
|
|
43
|
+
this: NativeSimctl,
|
|
44
|
+
udid: string,
|
|
45
|
+
service: SimPermissionService,
|
|
46
|
+
bundleId: string,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
return runCatchingAsync(async () => {
|
|
49
|
+
const tccIdentifier = toTCCIdentifier(service);
|
|
50
|
+
return (await this._findDevice(udid)).revokePermission(tccIdentifier, bundleId);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resets a privacy permission for the given app to its default (unprompted) state.
|
|
56
|
+
*
|
|
57
|
+
* @param udid — UDID of the target device
|
|
58
|
+
* @param service — permission to reset (see {@link grantPermission})
|
|
59
|
+
* @param bundleId — bundle identifier of the app the permission applies to
|
|
60
|
+
*/
|
|
61
|
+
export async function resetPermission(
|
|
62
|
+
this: NativeSimctl,
|
|
63
|
+
udid: string,
|
|
64
|
+
service: SimPermissionService,
|
|
65
|
+
bundleId: string,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
return runCatchingAsync(async () => {
|
|
68
|
+
const tccIdentifier = toTCCIdentifier(service);
|
|
69
|
+
return (await this._findDevice(udid)).resetPermission(tccIdentifier, bundleId);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reads a privacy permission's current status directly from the simulator's own TCC database —
|
|
75
|
+
* there's no CoreSimulator getter for this, only the setters {@link grantPermission}/
|
|
76
|
+
* {@link revokePermission}/{@link resetPermission} write to (see CLAUDE.md).
|
|
77
|
+
*
|
|
78
|
+
* @param udid — UDID of the device to read from
|
|
79
|
+
* @param service — permission to check (see {@link grantPermission})
|
|
80
|
+
* @param bundleId — bundle identifier of the app the permission applies to
|
|
81
|
+
* @returns `'unset'` if the app has never been prompted/decided for this permission
|
|
82
|
+
*/
|
|
83
|
+
export async function getPermission(
|
|
84
|
+
this: NativeSimctl,
|
|
85
|
+
udid: string,
|
|
86
|
+
service: SimPermissionService,
|
|
87
|
+
bundleId: string,
|
|
88
|
+
): Promise<SimPermissionStatus> {
|
|
89
|
+
return runCatchingAsync(async () => {
|
|
90
|
+
const tccIdentifier = toTCCIdentifier(service);
|
|
91
|
+
return (await this._findDevice(udid)).getPermission(tccIdentifier, bundleId);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Maps a friendly service name to the internal TCC service identifier its row in the simulator's
|
|
96
|
+
// own TCC.db is keyed on (see native/tcc_privacy.h). `location` is deliberately absent: it isn't a
|
|
97
|
+
// plain TCC row (CoreLocation simulation has its own subsystem), so it isn't supported by this
|
|
98
|
+
// TCC.db-based implementation.
|
|
99
|
+
const SERVICE_TO_TCC_IDENTIFIER: Record<SimPermissionService, string> = {
|
|
100
|
+
calendar: 'kTCCServiceCalendar',
|
|
101
|
+
camera: 'kTCCServiceCamera',
|
|
102
|
+
contacts: 'kTCCServiceAddressBook',
|
|
103
|
+
health: 'kTCCServiceMSO',
|
|
104
|
+
homekit: 'kTCCServiceWillow',
|
|
105
|
+
medialibrary: 'kTCCServiceMediaLibrary',
|
|
106
|
+
microphone: 'kTCCServiceMicrophone',
|
|
107
|
+
motion: 'kTCCServiceMotion',
|
|
108
|
+
photos: 'kTCCServicePhotos',
|
|
109
|
+
reminders: 'kTCCServiceReminders',
|
|
110
|
+
siri: 'kTCCServiceSiri',
|
|
111
|
+
speech: 'kTCCServiceSpeechRecognition',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function toTCCIdentifier(service: SimPermissionService): string {
|
|
115
|
+
const identifier = SERVICE_TO_TCC_IDENTIFIER[service];
|
|
116
|
+
if (!identifier) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`'${service}' is not a supported permission. Supported: ${Object.keys(SERVICE_TO_TCC_IDENTIFIER).join(', ')}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return identifier;
|
|
122
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {once} from 'node:events';
|
|
2
|
+
|
|
3
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
4
|
+
import type {SimProcessInfo} from '../types.js';
|
|
5
|
+
import {runCatchingAsync} from '../utils/index.js';
|
|
6
|
+
|
|
7
|
+
declare module '../native-simctl.js' {
|
|
8
|
+
interface NativeSimctl {
|
|
9
|
+
listProcesses(udid: string): Promise<SimProcessInfo[]>;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lists currently-running processes/launchd jobs on the given (booted) device, by spawning
|
|
15
|
+
* `launchctl list` inside it and parsing its `PID / Status / Label` output. A `-` PID column
|
|
16
|
+
* (registered but not running) is dropped; an app process's `name` is its bundle identifier.
|
|
17
|
+
*
|
|
18
|
+
* @param udid — UDID of the device to inspect; must be booted
|
|
19
|
+
*/
|
|
20
|
+
export async function listProcesses(this: NativeSimctl, udid: string): Promise<SimProcessInfo[]> {
|
|
21
|
+
return runCatchingAsync(async () => {
|
|
22
|
+
const device = await this._findDevice(udid);
|
|
23
|
+
// The guest runtime ships its own launchctl, distinct from the host's /bin/launchctl (see
|
|
24
|
+
// CLAUDE.md) — spawn() takes a literal path, so resolve it via RuntimeRoot ourselves.
|
|
25
|
+
const launchctlPath = `${device.runtimeRootPath()}/bin/launchctl`;
|
|
26
|
+
const proc = await this.spawnProcess(udid, launchctlPath, {arguments: [launchctlPath, 'list']});
|
|
27
|
+
let stdout = '';
|
|
28
|
+
let stderr = '';
|
|
29
|
+
proc.stdout.on('data', (chunk: Buffer) => {
|
|
30
|
+
stdout += chunk;
|
|
31
|
+
});
|
|
32
|
+
proc.stderr.on('data', (chunk: Buffer) => {
|
|
33
|
+
stderr += chunk;
|
|
34
|
+
});
|
|
35
|
+
// stdout/stderr are plain net.Sockets over raw fds — an unhandled 'error' on either would
|
|
36
|
+
// crash the whole process, not just reject this promise, so race it in as a real rejection.
|
|
37
|
+
const streamError = Promise.race([once(proc.stdout, 'error'), once(proc.stderr, 'error')]).then(([err]) => {
|
|
38
|
+
throw err;
|
|
39
|
+
});
|
|
40
|
+
// 'exit' can fire before the stdout stream has finished delivering its buffered data (see
|
|
41
|
+
// spawnProcess's own integration test) — wait for both before parsing.
|
|
42
|
+
const [[code, signal]] = await Promise.race([
|
|
43
|
+
Promise.all([once(proc, 'exit'), once(proc.stdout, 'end')]),
|
|
44
|
+
streamError,
|
|
45
|
+
]);
|
|
46
|
+
if (code !== 0) {
|
|
47
|
+
const reason = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
48
|
+
throw new Error(`'launchctl list' failed with ${reason}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result: SimProcessInfo[] = [];
|
|
52
|
+
for (const line of stdout.split('\n')) {
|
|
53
|
+
const trimmedLine = line.trim();
|
|
54
|
+
if (!trimmedLine) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const [pidText, , label] = trimmedLine.split(/\s+/);
|
|
58
|
+
const pid = Number.parseInt(pidText, 10);
|
|
59
|
+
if (!pid || !label) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
result.push({pid, group: extractGroup(label), name: extractName(label)});
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractGroup(label: string): string | null {
|
|
69
|
+
const colonIdx = label.indexOf(':');
|
|
70
|
+
return colonIdx >= 0 ? label.slice(0, colonIdx) : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractName(label: string): string {
|
|
74
|
+
let name = label.includes(':') ? label.slice(label.indexOf(':') + 1) : label;
|
|
75
|
+
const bracketIdx = name.indexOf('[');
|
|
76
|
+
if (bracketIdx >= 0) {
|
|
77
|
+
name = name.slice(0, bracketIdx);
|
|
78
|
+
}
|
|
79
|
+
return name;
|
|
80
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
2
|
+
import type {ScreenshotOptions, SimDisplayInfo} from '../types.js';
|
|
3
|
+
import {runCatchingAsync} from '../utils/index.js';
|
|
4
|
+
|
|
5
|
+
declare module '../native-simctl.js' {
|
|
6
|
+
interface NativeSimctl {
|
|
7
|
+
getScreenshot(udid: string, options?: ScreenshotOptions): Promise<Buffer>;
|
|
8
|
+
getDisplays(udid: string): Promise<SimDisplayInfo[]>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Captures a device display as an image — the native equivalent of `simctl io <udid> screenshot`
|
|
14
|
+
* (see native/sim_screenshot.mm for how this reads the framebuffer directly, with no temp file or
|
|
15
|
+
* subprocess). Rejects if the resolved display has no renderable surface yet (e.g. not booted), or
|
|
16
|
+
* if `options.displayId` doesn't match any display from {@link getDisplays}.
|
|
17
|
+
*
|
|
18
|
+
* @param udid — UDID of the device to capture; must be booted
|
|
19
|
+
* @param options — `format` (defaults to `'png'`), `displayId` (defaults to the primary display),
|
|
20
|
+
* and `quality` (JPEG only, 0-100 percent)
|
|
21
|
+
* @returns image data encoded as `options.format`
|
|
22
|
+
*/
|
|
23
|
+
export async function getScreenshot(
|
|
24
|
+
this: NativeSimctl,
|
|
25
|
+
udid: string,
|
|
26
|
+
options: ScreenshotOptions = {},
|
|
27
|
+
): Promise<Buffer> {
|
|
28
|
+
if (
|
|
29
|
+
options.quality !== undefined &&
|
|
30
|
+
(!Number.isFinite(options.quality) || options.quality < 0 || options.quality > 100)
|
|
31
|
+
) {
|
|
32
|
+
throw new RangeError(`quality must be a number between 0 and 100, got ${options.quality}`);
|
|
33
|
+
}
|
|
34
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).screenshot(options));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Lists the device's renderable displays — the same information `simctl io <udid> enumerate`
|
|
39
|
+
* reports for its own `--display` selection. Most devices report exactly one (the primary
|
|
40
|
+
* display); a device with a secondary display (e.g. tvOS's TVOut) reports more than one.
|
|
41
|
+
*
|
|
42
|
+
* @param udid — UDID of the device to inspect; must be booted
|
|
43
|
+
*/
|
|
44
|
+
export async function getDisplays(this: NativeSimctl, udid: string): Promise<SimDisplayInfo[]> {
|
|
45
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).getDisplays());
|
|
46
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {EventEmitter} from 'node:events';
|
|
2
|
+
import {Socket} from 'node:net';
|
|
3
|
+
import {constants as osConstants} from 'node:os';
|
|
4
|
+
|
|
5
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
6
|
+
import type {SpawnOptions} from '../types.js';
|
|
7
|
+
import {runCatchingAsync} from '../utils/index.js';
|
|
8
|
+
|
|
9
|
+
declare module '../native-simctl.js' {
|
|
10
|
+
interface NativeSimctl {
|
|
11
|
+
spawnProcess(udid: string, path: string, options?: SpawnOptions): Promise<SpawnedProcess>;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Some signal numbers have more than one name (e.g. SIGABRT/SIGIOT are both 6) — built with a
|
|
16
|
+
// for-of rather than Object.fromEntries so the *first* name Node lists for a number wins instead
|
|
17
|
+
// of whichever happens to be last, which would otherwise make an aborted process unpredictably
|
|
18
|
+
// report as the obscure historical alias (confirmed: this reversed Object.fromEntries reported a
|
|
19
|
+
// real SIGABRT as 'SIGIOT').
|
|
20
|
+
const SIGNAL_NAME_BY_NUMBER: Record<number, NodeJS.Signals> = {};
|
|
21
|
+
for (const [name, num] of Object.entries(osConstants.signals)) {
|
|
22
|
+
SIGNAL_NAME_BY_NUMBER[num] ??= name as NodeJS.Signals;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SpawnedProcessEvents {
|
|
26
|
+
exit: [code: number | null, signal: NodeJS.Signals | null];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A process spawned on a simulator device via `NativeSimctl.spawnProcess`. The simulator shares
|
|
31
|
+
* the host kernel and filesystem, so `pid` is a real host OS process id — {@link kill} just calls
|
|
32
|
+
* Node's own `process.kill()`, no native call needed. `stdout`/`stderr` stream live output as the
|
|
33
|
+
* process runs; `'exit'` fires exactly once, with a decoded `(code, signal)` pair mirroring
|
|
34
|
+
* `child_process.ChildProcess`'s own semantics (exactly one of the two is non-null).
|
|
35
|
+
*/
|
|
36
|
+
export class SpawnedProcess extends EventEmitter<SpawnedProcessEvents> {
|
|
37
|
+
readonly stdout: Socket;
|
|
38
|
+
readonly stderr: Socket;
|
|
39
|
+
exitCode: number | null = null;
|
|
40
|
+
signalCode: NodeJS.Signals | null = null;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
readonly pid: number,
|
|
44
|
+
stdoutFd: number,
|
|
45
|
+
stderrFd: number,
|
|
46
|
+
) {
|
|
47
|
+
super();
|
|
48
|
+
// `net.Socket` (not `fs.createReadStream`) deliberately: these fds are blocking NSPipe read
|
|
49
|
+
// ends, and fs's reads always run as blocking syscalls on the shared libuv threadpool (default
|
|
50
|
+
// size 4) regardless of the fd's actual type — a single quiet process's idle stdout+stderr
|
|
51
|
+
// reads permanently occupy 2 of those 4 workers until output or EOF arrives, and two quiet
|
|
52
|
+
// processes exhaust the pool entirely, stalling unrelated fs work *and* this addon's own
|
|
53
|
+
// AsyncWorkers (confirmed empirically: an unrelated fs.readFile took 8+ seconds instead of
|
|
54
|
+
// ~1ms while two quiet spawned processes' output was being read this way). A pipe fd is
|
|
55
|
+
// recognized by libuv as a named-pipe handle regardless of whether it's an anonymous pipe(2),
|
|
56
|
+
// so wrapping it in a Socket gets real event-driven (kqueue/epoll) I/O instead, exactly like
|
|
57
|
+
// Node's own child_process does for a child's stdio pipes.
|
|
58
|
+
this.stdout = new Socket({fd: stdoutFd, readable: true, writable: false});
|
|
59
|
+
this.stderr = new Socket({fd: stderrFd, readable: true, writable: false});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether the process has neither exited nor been killed yet. */
|
|
63
|
+
get running(): boolean {
|
|
64
|
+
return this.exitCode === null && this.signalCode === null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sends a signal to the process — a thin wrapper over `process.kill()`. A no-op returning
|
|
69
|
+
* `false` once exit has already been observed, rather than risking `process.kill()` throwing
|
|
70
|
+
* `ESRCH` on an already-reaped pid or, worse, hitting an unrelated process if the pid has since
|
|
71
|
+
* been recycled by the OS.
|
|
72
|
+
*/
|
|
73
|
+
kill(signal: NodeJS.Signals | number = 'SIGTERM'): boolean {
|
|
74
|
+
if (!this.running) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return process.kill(this.pid, signal);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @internal Invoked once by NativeSimctl when the native termination callback fires. */
|
|
81
|
+
_handleExit(code: number | null, signal: number | null): void {
|
|
82
|
+
this.exitCode = code;
|
|
83
|
+
this.signalCode = signal === null ? null : (SIGNAL_NAME_BY_NUMBER[signal] ?? null);
|
|
84
|
+
this.emit('exit', this.exitCode, this.signalCode);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Native equivalent of `simctl spawn`. Streams live stdout/stderr and eventually reports an
|
|
90
|
+
* exit code/signal — see {@link SpawnedProcess}.
|
|
91
|
+
*
|
|
92
|
+
* @param udid — UDID of the target device
|
|
93
|
+
* @param path — path to the executable to spawn; not auto-prepended to `options.arguments`
|
|
94
|
+
* @param options — see {@link SpawnOptions}
|
|
95
|
+
* @returns a handle to the spawned process
|
|
96
|
+
*/
|
|
97
|
+
export async function spawnProcess(
|
|
98
|
+
this: NativeSimctl,
|
|
99
|
+
udid: string,
|
|
100
|
+
path: string,
|
|
101
|
+
options: SpawnOptions = {},
|
|
102
|
+
): Promise<SpawnedProcess> {
|
|
103
|
+
return runCatchingAsync(async () => {
|
|
104
|
+
const device = await this._findDevice(udid);
|
|
105
|
+
// The native termination callback (delivered via a ThreadSafeFunction/GCD path) and
|
|
106
|
+
// device.spawn()'s own promise resolution (an AsyncWorker completion) are independent async
|
|
107
|
+
// signals with no guaranteed relative order — a process that exits almost immediately can have
|
|
108
|
+
// its termination callback fire before the promise below resolves and `proc` exists. Buffer the
|
|
109
|
+
// exit args in that case and deliver them once the handle is constructed, rather than assuming
|
|
110
|
+
// the callback always arrives second.
|
|
111
|
+
let proc: SpawnedProcess | undefined;
|
|
112
|
+
let pendingExit: [code: number | null, signal: number | null] | undefined;
|
|
113
|
+
const {pid, stdoutFd, stderrFd} = await device.spawn(path, options, (code, signal) => {
|
|
114
|
+
if (proc) {
|
|
115
|
+
proc._handleExit(code, signal);
|
|
116
|
+
} else {
|
|
117
|
+
pendingExit = [code, signal];
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
proc = new SpawnedProcess(pid, stdoutFd, stderrFd);
|
|
121
|
+
if (pendingExit) {
|
|
122
|
+
// Delivering this synchronously would emit 'exit' before spawnProcess()'s own promise has
|
|
123
|
+
// even resolved — a caller doing `const proc = await sim.spawnProcess(...); proc.on('exit', ...)`
|
|
124
|
+
// would never see it, since its listener can't be attached until after that await returns.
|
|
125
|
+
// setImmediate defers past both promise-resolution microtasks and any synchronous listener
|
|
126
|
+
// setup that runs right after them, guaranteeing the listener is attached first.
|
|
127
|
+
const exitArgs = pendingExit;
|
|
128
|
+
const deliverTo = proc;
|
|
129
|
+
setImmediate(() => deliverTo._handleExit(...exitArgs));
|
|
130
|
+
}
|
|
131
|
+
return proc;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
2
|
+
import {runCatchingAsync} from '../utils/index.js';
|
|
3
|
+
|
|
4
|
+
declare module '../native-simctl.js' {
|
|
5
|
+
interface NativeSimctl {
|
|
6
|
+
getAppearance(udid: string): Promise<number>;
|
|
7
|
+
setAppearance(udid: string, style: number): Promise<void>;
|
|
8
|
+
getIncreaseContrast(udid: string): Promise<number>;
|
|
9
|
+
setIncreaseContrast(udid: string, enabled: boolean): Promise<void>;
|
|
10
|
+
getContentSize(udid: string): Promise<number>;
|
|
11
|
+
setContentSize(udid: string, category: number): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param udid — UDID of the device to read from
|
|
17
|
+
* @returns the device's current UI appearance style, as a raw `UIUserInterfaceStyle` value
|
|
18
|
+
*/
|
|
19
|
+
export async function getAppearance(this: NativeSimctl, udid: string): Promise<number> {
|
|
20
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).getUIAppearance());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param udid — UDID of the target device
|
|
25
|
+
* @param style — a raw `UIUserInterfaceStyle` value to apply
|
|
26
|
+
*/
|
|
27
|
+
export async function setAppearance(this: NativeSimctl, udid: string, style: number): Promise<void> {
|
|
28
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).setUIAppearance(style));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param udid — UDID of the device to read from
|
|
33
|
+
* @returns the device's current Increase Contrast accessibility setting
|
|
34
|
+
*/
|
|
35
|
+
export async function getIncreaseContrast(this: NativeSimctl, udid: string): Promise<number> {
|
|
36
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).getIncreaseContrast());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param udid — UDID of the target device
|
|
41
|
+
* @param enabled — whether Increase Contrast should be enabled
|
|
42
|
+
*/
|
|
43
|
+
export async function setIncreaseContrast(this: NativeSimctl, udid: string, enabled: boolean): Promise<void> {
|
|
44
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).setIncreaseContrast(enabled));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param udid — UDID of the device to read from
|
|
49
|
+
* @returns the device's current Dynamic Type content size category
|
|
50
|
+
*/
|
|
51
|
+
export async function getContentSize(this: NativeSimctl, udid: string): Promise<number> {
|
|
52
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).getContentSize());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param udid — UDID of the target device
|
|
57
|
+
* @param category — the Dynamic Type content size category to apply
|
|
58
|
+
*/
|
|
59
|
+
export async function setContentSize(this: NativeSimctl, udid: string, category: number): Promise<void> {
|
|
60
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).setContentSize(category));
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {NativeSimctl} from '../native-simctl.js';
|
|
2
|
+
import {runCatchingAsync} from '../utils/index.js';
|
|
3
|
+
|
|
4
|
+
declare module '../native-simctl.js' {
|
|
5
|
+
interface NativeSimctl {
|
|
6
|
+
getWebInspectorSocket(udid: string): Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Locates the Unix-domain socket a booted device's WebInspector service listens on, for WebKit
|
|
12
|
+
* remote-debugging tools to connect to directly.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const socketPath = await sim.getWebInspectorSocket(udid);
|
|
16
|
+
* const socket = net.connect(socketPath); // speak the WebInspector wire protocol from here
|
|
17
|
+
*
|
|
18
|
+
* @param udid — UDID of the device to inspect; must be booted
|
|
19
|
+
* @returns the socket's filesystem path
|
|
20
|
+
*/
|
|
21
|
+
export async function getWebInspectorSocket(this: NativeSimctl, udid: string): Promise<string> {
|
|
22
|
+
return runCatchingAsync(async () => (await this._findDevice(udid)).getWebInspectorSocket());
|
|
23
|
+
}
|