@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.
Files changed (138) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +66 -0
  3. package/binding.gyp +53 -0
  4. package/lib/src/commands/app.d.ts +88 -0
  5. package/lib/src/commands/app.d.ts.map +1 -0
  6. package/lib/src/commands/app.js +133 -0
  7. package/lib/src/commands/app.js.map +1 -0
  8. package/lib/src/commands/biometric.d.ts +33 -0
  9. package/lib/src/commands/biometric.d.ts.map +1 -0
  10. package/lib/src/commands/biometric.js +44 -0
  11. package/lib/src/commands/biometric.js.map +1 -0
  12. package/lib/src/commands/darwin-notification.d.ts +36 -0
  13. package/lib/src/commands/darwin-notification.d.ts.map +1 -0
  14. package/lib/src/commands/darwin-notification.js +35 -0
  15. package/lib/src/commands/darwin-notification.js.map +1 -0
  16. package/lib/src/commands/interaction.d.ts +48 -0
  17. package/lib/src/commands/interaction.d.ts.map +1 -0
  18. package/lib/src/commands/interaction.js +48 -0
  19. package/lib/src/commands/interaction.js.map +1 -0
  20. package/lib/src/commands/keychain.d.ts +25 -0
  21. package/lib/src/commands/keychain.d.ts.map +1 -0
  22. package/lib/src/commands/keychain.js +52 -0
  23. package/lib/src/commands/keychain.js.map +1 -0
  24. package/lib/src/commands/lifecycle.d.ts +101 -0
  25. package/lib/src/commands/lifecycle.d.ts.map +1 -0
  26. package/lib/src/commands/lifecycle.js +165 -0
  27. package/lib/src/commands/lifecycle.js.map +1 -0
  28. package/lib/src/commands/media.d.ts +28 -0
  29. package/lib/src/commands/media.d.ts.map +1 -0
  30. package/lib/src/commands/media.js +27 -0
  31. package/lib/src/commands/media.js.map +1 -0
  32. package/lib/src/commands/misc.d.ts +15 -0
  33. package/lib/src/commands/misc.d.ts.map +1 -0
  34. package/lib/src/commands/misc.js +12 -0
  35. package/lib/src/commands/misc.js.map +1 -0
  36. package/lib/src/commands/pasteboard.d.ts +24 -0
  37. package/lib/src/commands/pasteboard.d.ts.map +1 -0
  38. package/lib/src/commands/pasteboard.js +22 -0
  39. package/lib/src/commands/pasteboard.js.map +1 -0
  40. package/lib/src/commands/permissions.d.ts +48 -0
  41. package/lib/src/commands/permissions.d.ts.map +1 -0
  42. package/lib/src/commands/permissions.js +84 -0
  43. package/lib/src/commands/permissions.js.map +1 -0
  44. package/lib/src/commands/process.d.ts +16 -0
  45. package/lib/src/commands/process.d.ts.map +1 -0
  46. package/lib/src/commands/process.js +68 -0
  47. package/lib/src/commands/process.js.map +1 -0
  48. package/lib/src/commands/screenshot.d.ts +29 -0
  49. package/lib/src/commands/screenshot.d.ts.map +1 -0
  50. package/lib/src/commands/screenshot.js +30 -0
  51. package/lib/src/commands/screenshot.js.map +1 -0
  52. package/lib/src/commands/spawn.d.ts +48 -0
  53. package/lib/src/commands/spawn.d.ts.map +1 -0
  54. package/lib/src/commands/spawn.js +108 -0
  55. package/lib/src/commands/spawn.js.map +1 -0
  56. package/lib/src/commands/ui.d.ts +42 -0
  57. package/lib/src/commands/ui.d.ts.map +1 -0
  58. package/lib/src/commands/ui.js +44 -0
  59. package/lib/src/commands/ui.js.map +1 -0
  60. package/lib/src/commands/webinspector.d.ts +19 -0
  61. package/lib/src/commands/webinspector.d.ts.map +1 -0
  62. package/lib/src/commands/webinspector.js +16 -0
  63. package/lib/src/commands/webinspector.js.map +1 -0
  64. package/lib/src/errors.d.ts +51 -0
  65. package/lib/src/errors.d.ts.map +1 -0
  66. package/lib/src/errors.js +82 -0
  67. package/lib/src/errors.js.map +1 -0
  68. package/lib/src/index.d.ts +7 -0
  69. package/lib/src/index.d.ts.map +1 -0
  70. package/lib/src/index.js +5 -0
  71. package/lib/src/index.js.map +1 -0
  72. package/lib/src/native-simctl.d.ts +62 -0
  73. package/lib/src/native-simctl.d.ts.map +1 -0
  74. package/lib/src/native-simctl.js +221 -0
  75. package/lib/src/native-simctl.js.map +1 -0
  76. package/lib/src/types.d.ts +267 -0
  77. package/lib/src/types.d.ts.map +1 -0
  78. package/lib/src/types.js +29 -0
  79. package/lib/src/types.js.map +1 -0
  80. package/lib/src/utils/index.d.ts +3 -0
  81. package/lib/src/utils/index.d.ts.map +1 -0
  82. package/lib/src/utils/index.js +3 -0
  83. package/lib/src/utils/index.js.map +1 -0
  84. package/lib/src/utils/pkg-root.d.ts +6 -0
  85. package/lib/src/utils/pkg-root.d.ts.map +1 -0
  86. package/lib/src/utils/pkg-root.js +12 -0
  87. package/lib/src/utils/pkg-root.js.map +1 -0
  88. package/lib/src/utils/run-catching.d.ts +3 -0
  89. package/lib/src/utils/run-catching.d.ts.map +1 -0
  90. package/lib/src/utils/run-catching.js +11 -0
  91. package/lib/src/utils/run-catching.js.map +1 -0
  92. package/package.json +73 -0
  93. package/scripts/install.mjs +22 -0
  94. package/src/commands/app.ts +186 -0
  95. package/src/commands/biometric.ts +69 -0
  96. package/src/commands/darwin-notification.ts +51 -0
  97. package/src/commands/interaction.ts +74 -0
  98. package/src/commands/keychain.ts +63 -0
  99. package/src/commands/lifecycle.ts +210 -0
  100. package/src/commands/media.ts +38 -0
  101. package/src/commands/misc.ts +20 -0
  102. package/src/commands/pasteboard.ts +31 -0
  103. package/src/commands/permissions.ts +122 -0
  104. package/src/commands/process.ts +80 -0
  105. package/src/commands/screenshot.ts +46 -0
  106. package/src/commands/spawn.ts +133 -0
  107. package/src/commands/ui.ts +61 -0
  108. package/src/commands/webinspector.ts +23 -0
  109. package/src/coresim.mm +1066 -0
  110. package/src/errors.ts +93 -0
  111. package/src/index.ts +23 -0
  112. package/src/native/async_bridge.h +164 -0
  113. package/src/native/nserror_bridge.h +64 -0
  114. package/src/native/nserror_bridge.mm +30 -0
  115. package/src/native/objc_runtime.h +45 -0
  116. package/src/native/objc_runtime.mm +76 -0
  117. package/src/native/safe_dispatch.h +38 -0
  118. package/src/native/sim_device.h +143 -0
  119. package/src/native/sim_device.mm +355 -0
  120. package/src/native/sim_device_set.h +16 -0
  121. package/src/native/sim_device_set.mm +40 -0
  122. package/src/native/sim_pasteboard.h +16 -0
  123. package/src/native/sim_pasteboard.mm +295 -0
  124. package/src/native/sim_process.h +13 -0
  125. package/src/native/sim_process.mm +139 -0
  126. package/src/native/sim_screenshot.h +28 -0
  127. package/src/native/sim_screenshot.mm +226 -0
  128. package/src/native/sim_service_context.h +42 -0
  129. package/src/native/sim_service_context.mm +92 -0
  130. package/src/native/tcc_privacy.h +35 -0
  131. package/src/native/tcc_privacy.mm +256 -0
  132. package/src/native/value_bridge.h +18 -0
  133. package/src/native/value_bridge.mm +111 -0
  134. package/src/native-simctl.ts +288 -0
  135. package/src/types.ts +302 -0
  136. package/src/utils/index.ts +2 -0
  137. package/src/utils/pkg-root.ts +14 -0
  138. 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
+ }