@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
package/src/coresim.mm ADDED
@@ -0,0 +1,1066 @@
1
+ // N-API glue: exposes SimServiceContext/SimDeviceSet/SimDevice as JS-facing classes. All actual
2
+ // dynamic dispatch lives in native/*.mm (safe_dispatch-guarded); this file only translates
3
+ // JS <-> Objective-C values, decides sync vs. async exposure, and reports errors/results back to
4
+ // Node.
5
+ //
6
+ // Every method that can trigger a CoreSimulator dispatch (i.e. everything except the handful of
7
+ // trivial in-memory property getters below) is async: work runs in a RunAsync/RunAsyncVoid
8
+ // worker on a libuv threadpool thread (async_bridge.h), so a slow install/erase/create/etc. never
9
+ // blocks Node's event loop, matching how boot already worked before this file's rewrite.
10
+
11
+ #include <napi.h>
12
+
13
+ #import <Foundation/Foundation.h>
14
+
15
+ #include <sys/wait.h>
16
+ #include <unistd.h>
17
+
18
+ #include <cerrno>
19
+ #include <cstring>
20
+ #include <stdexcept>
21
+ #include <string>
22
+ #include <vector>
23
+
24
+ #include "native/async_bridge.h"
25
+ #include "native/nserror_bridge.h"
26
+ #include "native/objc_runtime.h"
27
+ #include "native/sim_device.h"
28
+ #include "native/sim_device_set.h"
29
+ #include "native/sim_pasteboard.h"
30
+ #include "native/sim_process.h"
31
+ #include "native/sim_screenshot.h"
32
+ #include "native/sim_service_context.h"
33
+ #include "native/tcc_privacy.h"
34
+ #include "native/value_bridge.h"
35
+
36
+ namespace coresim {
37
+
38
+ namespace {
39
+
40
+ // Boxes an `id` behind a Napi::External so it survives the single synchronous call into an
41
+ // ObjectWrap constructor (see NewInstance below in each class).
42
+ Napi::Object WrapExternalId(Napi::Env env, const Napi::FunctionReference& ctor, id value) {
43
+ __unsafe_unretained id boxed = value;
44
+ return ctor.New({Napi::External<void>::New(env, &boxed)});
45
+ }
46
+
47
+ id UnwrapExternalId(const Napi::CallbackInfo& info) {
48
+ return *static_cast<__unsafe_unretained id*>(info[0].As<Napi::External<void>>().Data());
49
+ }
50
+
51
+ NSDictionary* OptionsArg(const Napi::CallbackInfo& info, size_t index) {
52
+ if (info.Length() <= index || info[index].IsUndefined() || info[index].IsNull()) {
53
+ return @{};
54
+ }
55
+ return (NSDictionary*)JsValueToNSObject(info.Env(), info[index]);
56
+ }
57
+
58
+ // Throws if `!ok && error`, for the common BOOL-returning NSError**-out-param shape — called from
59
+ // inside a RunAsync/RunAsyncVoid work lambda (background thread), never from the main thread.
60
+ void ThrowIfFailed(BOOL ok, NSError* error) {
61
+ if (!ok && error != nil) {
62
+ throw NSErrorException(error);
63
+ }
64
+ }
65
+
66
+ // Mirrors coresim::TCCAuthStatus (tcc_privacy.h) as the string enum getPermission() resolves with
67
+ // — a plain string, not a raw int, since JS callers have no header to interpret the int against.
68
+ const char* TCCAuthStatusToString(TCCAuthStatus status) {
69
+ switch (status) {
70
+ case kTCCAuthDenied:
71
+ return "denied";
72
+ case kTCCAuthGranted:
73
+ return "granted";
74
+ case kTCCAuthLimited:
75
+ return "limited";
76
+ case kTCCAuthNotDetermined:
77
+ default:
78
+ return "unset";
79
+ }
80
+ }
81
+
82
+ // dup() failure (e.g. EMFILE — the process fd table is full) — surfaced as a normal catchable
83
+ // error via ThrowIfFailed's own throw path, rather than an fd of -1 silently reaching JS.
84
+ NSError* MakeDescriptorError(NSString* which, int savedErrno) {
85
+ return [NSError errorWithDomain:NSPOSIXErrorDomain
86
+ code:savedErrno
87
+ userInfo:@{
88
+ NSLocalizedDescriptionKey : [NSString
89
+ stringWithFormat:@"Failed to duplicate the spawned process's %@ file descriptor: %s",
90
+ which, strerror(savedErrno)]
91
+ }];
92
+ }
93
+
94
+ // Plain data Spawn()'s work lambda (background thread) hands to its toValue callback (main
95
+ // thread) — see NativeDevice::Spawn below.
96
+ struct SpawnResult {
97
+ int pid = 0;
98
+ int stdoutFd = -1;
99
+ int stderrFd = -1;
100
+ };
101
+
102
+ // Plain data for GetBootStatus/SupportedDeviceTypesMethod/SupportedRuntimesMethod's work lambdas
103
+ // to hand to their toValue callbacks. toValue always runs on the main thread (OnOK), never
104
+ // guarded by Execute()'s try/catch — the node-addon-api completion wrapper only catches
105
+ // Napi::Error there unless NODE_ADDON_API_CPP_EXCEPTIONS_ALL is defined (it isn't here), so a
106
+ // native accessor call from inside toValue could throw NativeSimUnavailableError/ObjCException
107
+ // uncaught and crash the process. Extracting these plain values in `work` instead keeps every
108
+ // native call inside the guarded stage; toValue only ever touches already-safe C++/Foundation
109
+ // primitives.
110
+ struct BootStatusResult {
111
+ bool hasValue = false;
112
+ unsigned int status = 0;
113
+ bool isTerminal = false;
114
+ };
115
+
116
+ struct DeviceTypeEntry {
117
+ std::string identifier;
118
+ std::string name;
119
+ };
120
+
121
+ struct RuntimeEntry {
122
+ std::string identifier;
123
+ std::string name;
124
+ std::string versionString;
125
+ };
126
+
127
+ // Node-API explicitly prohibits sharing an Environment's data across Environments (e.g. two
128
+ // worker_threads instances each `require()`-ing this addon) — each gets its own separate call
129
+ // into Init() below. A process-global `static Napi::FunctionReference` per class would let one
130
+ // Environment's object construction use a FunctionReference belonging to another (possibly
131
+ // already-torn-down) Environment, corrupting state or crashing Node. Storing these via
132
+ // Napi::Env::SetInstanceData/GetInstanceData instead keeps each Environment's constructors
133
+ // scoped to it, and cleans them up automatically (the default finalizer just `delete`s this) when
134
+ // that Environment tears down.
135
+ struct AddonInstanceData {
136
+ Napi::FunctionReference deviceConstructor;
137
+ Napi::FunctionReference deviceSetConstructor;
138
+ Napi::FunctionReference serviceContextConstructor;
139
+ };
140
+
141
+ } // namespace
142
+
143
+ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
144
+ public:
145
+ static void Init(Napi::Env env);
146
+ static Napi::Object NewInstance(Napi::Env env, id device);
147
+ explicit NativeDevice(const Napi::CallbackInfo& info);
148
+
149
+ id device_;
150
+
151
+ private:
152
+ // Trivial in-memory accessors (no CoreSimulator dispatch that could block) — kept synchronous;
153
+ // used internally by native-simctl.ts's toDeviceInfo(), never a slow operation on their own.
154
+ Napi::Value Udid(const Napi::CallbackInfo& info) {
155
+ return CatchToJs(info.Env(), [&]() -> Napi::Value {
156
+ return Napi::String::New(info.Env(), DeviceUDID(device_).UUIDString.UTF8String);
157
+ });
158
+ }
159
+ Napi::Value Name(const Napi::CallbackInfo& info) {
160
+ return CatchToJs(info.Env(),
161
+ [&]() -> Napi::Value { return Napi::String::New(info.Env(), DeviceName(device_).UTF8String); });
162
+ }
163
+ Napi::Value State(const Napi::CallbackInfo& info) {
164
+ return CatchToJs(info.Env(), [&]() -> Napi::Value {
165
+ return Napi::Number::New(info.Env(), static_cast<double>(DeviceState(device_)));
166
+ });
167
+ }
168
+ // deviceType/runtime are legitimately nil for a device whose runtime profile is no longer
169
+ // installed (shows as "unavailable" in `simctl list devices`) — not an error, just unknown.
170
+ Napi::Value DeviceTypeIdentifier(const Napi::CallbackInfo& info) {
171
+ return CatchToJs(info.Env(), [&]() -> Napi::Value {
172
+ id deviceType = DeviceDeviceType(device_);
173
+ if (deviceType == nil) {
174
+ return Napi::String::New(info.Env(), "");
175
+ }
176
+ return Napi::String::New(info.Env(), coresim::DeviceTypeIdentifier(deviceType).UTF8String);
177
+ });
178
+ }
179
+ Napi::Value RuntimeIdentifier(const Napi::CallbackInfo& info) {
180
+ return CatchToJs(info.Env(), [&]() -> Napi::Value {
181
+ id runtime = DeviceRuntime(device_);
182
+ if (runtime == nil) {
183
+ return Napi::String::New(info.Env(), "");
184
+ }
185
+ return Napi::String::New(info.Env(), coresim::RuntimeIdentifier(runtime).UTF8String);
186
+ });
187
+ }
188
+ // @internal — not part of the public NativeSimctl API (see types.ts's NativeDeviceHandle).
189
+ Napi::Value RuntimeRootPath(const Napi::CallbackInfo& info) {
190
+ return CatchToJs(info.Env(), [&]() -> Napi::Value {
191
+ id runtime = DeviceRuntime(device_);
192
+ if (runtime == nil) {
193
+ return Napi::String::New(info.Env(), "");
194
+ }
195
+ return Napi::String::New(info.Env(), coresim::RuntimeRootPath(runtime).UTF8String);
196
+ });
197
+ }
198
+
199
+ // Bridges SimDevice's GCD-completion-block-based bootAsyncWithOptions:completionQueue:
200
+ // completionHandler: — this is what replaces the CLI's bootstatus poll-until-timeout race with
201
+ // a deterministic completion signal.
202
+ Napi::Value Boot(const Napi::CallbackInfo& info) {
203
+ Napi::Env env = info.Env();
204
+ id device = device_;
205
+ NSDictionary* options = OptionsArg(info, 0);
206
+ return RunAsyncVoid(env, [device, options]() {
207
+ dispatch_semaphore_t sema = dispatch_semaphore_create(0);
208
+ dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.boot", DISPATCH_QUEUE_SERIAL);
209
+ __block NSError* capturedError = nil;
210
+ BootAsync(device, options, queue, ^(NSError* error) {
211
+ capturedError = error;
212
+ dispatch_semaphore_signal(sema);
213
+ });
214
+ dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
215
+ if (capturedError != nil) {
216
+ throw NSErrorException(capturedError);
217
+ }
218
+ });
219
+ }
220
+
221
+ // Exposes the same underlying signal `simctl bootstatus` itself monitors (see sim_device.h's
222
+ // DeviceBootStatus) — a separate, more granular progress indicator than `state`. Returns `null`
223
+ // if the device has never been booted; otherwise `{status, isTerminal}`, confirmed empirically
224
+ // to *not* reset on shutdown (see CLAUDE.md) — callers wanting "is this device currently and
225
+ // fully booted" must check `state === Booted` too, not `isTerminal` alone.
226
+ Napi::Value GetBootStatus(const Napi::CallbackInfo& info) {
227
+ id device = device_;
228
+ return RunAsync<BootStatusResult>(
229
+ info.Env(),
230
+ [device]() -> BootStatusResult {
231
+ id bootInfo = coresim::DeviceBootStatus(device);
232
+ if (bootInfo == nil) {
233
+ return {};
234
+ }
235
+ return {true, coresim::BootInfoStatus(bootInfo), static_cast<bool>(coresim::BootInfoIsTerminal(bootInfo))};
236
+ },
237
+ [](Napi::Env env, BootStatusResult result) -> Napi::Value {
238
+ if (!result.hasValue) {
239
+ return env.Null();
240
+ }
241
+ Napi::Object obj = Napi::Object::New(env);
242
+ obj.Set("status", static_cast<double>(result.status));
243
+ obj.Set("isTerminal", result.isTerminal);
244
+ return obj;
245
+ });
246
+ }
247
+
248
+ Napi::Value Shutdown(const Napi::CallbackInfo& info) {
249
+ id device = device_;
250
+ return RunAsyncVoid(info.Env(), [device]() {
251
+ NSError* error = nil;
252
+ ThrowIfFailed(coresim::Shutdown(device, &error), error);
253
+ });
254
+ }
255
+
256
+ Napi::Value Erase(const Napi::CallbackInfo& info) {
257
+ id device = device_;
258
+ return RunAsyncVoid(info.Env(), [device]() {
259
+ NSError* error = nil;
260
+ ThrowIfFailed(coresim::Erase(device, &error), error);
261
+ });
262
+ }
263
+
264
+ Napi::Value Getenv(const Napi::CallbackInfo& info) {
265
+ id device = device_;
266
+ NSString* name = @(info[0].As<Napi::String>().Utf8Value().c_str());
267
+ return RunAsync<NSString*>(
268
+ info.Env(),
269
+ [device, name]() -> NSString* {
270
+ NSError* error = nil;
271
+ NSString* result = coresim::Getenv(device, name, &error);
272
+ ThrowIfFailed(result != nil, error);
273
+ return result;
274
+ },
275
+ [](Napi::Env env, NSString* result) -> Napi::Value {
276
+ return Napi::String::New(env, result ? result.UTF8String : "");
277
+ });
278
+ }
279
+
280
+ Napi::Value InstallApp(const Napi::CallbackInfo& info) {
281
+ id device = device_;
282
+ NSURL* url = [NSURL fileURLWithPath:@(info[0].As<Napi::String>().Utf8Value().c_str())];
283
+ NSDictionary* options = OptionsArg(info, 1);
284
+ return RunAsyncVoid(info.Env(), [device, url, options]() {
285
+ NSError* error = nil;
286
+ ThrowIfFailed(coresim::InstallApp(device, url, options, &error), error);
287
+ });
288
+ }
289
+
290
+ Napi::Value UninstallApp(const Napi::CallbackInfo& info) {
291
+ id device = device_;
292
+ NSString* bundleId = @(info[0].As<Napi::String>().Utf8Value().c_str());
293
+ NSDictionary* options = OptionsArg(info, 1);
294
+ return RunAsyncVoid(info.Env(), [device, bundleId, options]() {
295
+ NSError* error = nil;
296
+ ThrowIfFailed(coresim::UninstallApp(device, bundleId, options, &error), error);
297
+ });
298
+ }
299
+
300
+ Napi::Value LaunchApp(const Napi::CallbackInfo& info) {
301
+ id device = device_;
302
+ NSString* bundleId = @(info[0].As<Napi::String>().Utf8Value().c_str());
303
+ NSDictionary* options = OptionsArg(info, 1);
304
+ return RunAsync<int>(
305
+ info.Env(),
306
+ [device, bundleId, options]() -> int {
307
+ NSError* error = nil;
308
+ int pid = coresim::LaunchApp(device, bundleId, options, &error);
309
+ ThrowIfFailed(pid > 0, error);
310
+ return pid;
311
+ },
312
+ [](Napi::Env env, int pid) -> Napi::Value { return Napi::Number::New(env, pid); });
313
+ }
314
+
315
+ Napi::Value TerminateApp(const Napi::CallbackInfo& info) {
316
+ id device = device_;
317
+ NSString* bundleId = @(info[0].As<Napi::String>().Utf8Value().c_str());
318
+ return RunAsyncVoid(info.Env(), [device, bundleId]() {
319
+ NSError* error = nil;
320
+ ThrowIfFailed(coresim::TerminateApp(device, bundleId, &error), error);
321
+ });
322
+ }
323
+
324
+ Napi::Value PropertiesOfApplication(const Napi::CallbackInfo& info) {
325
+ id device = device_;
326
+ NSString* bundleId = @(info[0].As<Napi::String>().Utf8Value().c_str());
327
+ return RunAsync<NSDictionary*>(
328
+ info.Env(),
329
+ [device, bundleId]() -> NSDictionary* {
330
+ NSError* error = nil;
331
+ NSDictionary* result = coresim::PropertiesOfApplication(device, bundleId, &error);
332
+ ThrowIfFailed(result != nil, error);
333
+ return result;
334
+ },
335
+ [](Napi::Env env, NSDictionary* result) -> Napi::Value { return NSObjectToJsValue(env, result); });
336
+ }
337
+
338
+ Napi::Value InstalledApps(const Napi::CallbackInfo& info) {
339
+ id device = device_;
340
+ return RunAsync<NSDictionary*>(
341
+ info.Env(),
342
+ [device]() -> NSDictionary* {
343
+ NSError* error = nil;
344
+ NSDictionary* result = coresim::InstalledApps(device, &error);
345
+ ThrowIfFailed(result != nil, error);
346
+ return result;
347
+ },
348
+ [](Napi::Env env, NSDictionary* result) -> Napi::Value { return NSObjectToJsValue(env, result); });
349
+ }
350
+
351
+ Napi::Value OpenUrl(const Napi::CallbackInfo& info) {
352
+ id device = device_;
353
+ NSURL* url = [NSURL URLWithString:@(info[0].As<Napi::String>().Utf8Value().c_str())];
354
+ return RunAsyncVoid(info.Env(), [device, url]() {
355
+ NSError* error = nil;
356
+ ThrowIfFailed(coresim::OpenURL(device, url, &error), error);
357
+ });
358
+ }
359
+
360
+ Napi::Value SetLocation(const Napi::CallbackInfo& info) {
361
+ id device = device_;
362
+ double latitude = info[0].As<Napi::Number>().DoubleValue();
363
+ double longitude = info[1].As<Napi::Number>().DoubleValue();
364
+ return RunAsyncVoid(info.Env(), [device, latitude, longitude]() {
365
+ NSError* error = nil;
366
+ ThrowIfFailed(coresim::SetLocation(device, latitude, longitude, &error), error);
367
+ });
368
+ }
369
+
370
+ Napi::Value ClearLocation(const Napi::CallbackInfo& info) {
371
+ id device = device_;
372
+ return RunAsyncVoid(info.Env(), [device]() {
373
+ NSError* error = nil;
374
+ ThrowIfFailed(coresim::ClearLocation(device, &error), error);
375
+ });
376
+ }
377
+
378
+ Napi::Value SendPushNotification(const Napi::CallbackInfo& info) {
379
+ id device = device_;
380
+ NSString* bundleId = @(info[0].As<Napi::String>().Utf8Value().c_str());
381
+ NSDictionary* payload = (NSDictionary*)JsValueToNSObject(info.Env(), info[1]);
382
+ return RunAsyncVoid(info.Env(), [device, bundleId, payload]() {
383
+ NSError* error = nil;
384
+ ThrowIfFailed(coresim::SendPushNotification(device, bundleId, payload, &error), error);
385
+ });
386
+ }
387
+
388
+ Napi::Value AddCertificate(const Napi::CallbackInfo& info) {
389
+ id device = device_;
390
+ NSURL* url = [NSURL fileURLWithPath:@(info[0].As<Napi::String>().Utf8Value().c_str())];
391
+ BOOL trustAsRoot = info.Length() > 1 && info[1].As<Napi::Boolean>().Value();
392
+ return RunAsyncVoid(info.Env(), [device, url, trustAsRoot]() {
393
+ NSError* error = nil;
394
+ ThrowIfFailed(coresim::AddCertificate(device, url, trustAsRoot, &error), error);
395
+ });
396
+ }
397
+
398
+ Napi::Value ResetKeychain(const Napi::CallbackInfo& info) {
399
+ id device = device_;
400
+ return RunAsyncVoid(info.Env(), [device]() {
401
+ NSError* error = nil;
402
+ ThrowIfFailed(coresim::ResetKeychain(device, &error), error);
403
+ });
404
+ }
405
+
406
+ Napi::Value GetUIAppearance(const Napi::CallbackInfo& info) {
407
+ id device = device_;
408
+ return RunAsync<long long>(
409
+ info.Env(), [device]() -> long long { return CurrentUIInterfaceStyle(device); },
410
+ [](Napi::Env env, long long style) -> Napi::Value {
411
+ return Napi::Number::New(env, static_cast<double>(style));
412
+ });
413
+ }
414
+
415
+ Napi::Value SetUIAppearance(const Napi::CallbackInfo& info) {
416
+ id device = device_;
417
+ long long style = info[0].As<Napi::Number>().Int64Value();
418
+ return RunAsyncVoid(info.Env(), [device, style]() {
419
+ NSError* error = nil;
420
+ ThrowIfFailed(coresim::SetUIInterfaceStyle(device, style, &error), error);
421
+ });
422
+ }
423
+
424
+ Napi::Value GetIncreaseContrast(const Napi::CallbackInfo& info) {
425
+ id device = device_;
426
+ return RunAsync<long long>(
427
+ info.Env(), [device]() -> long long { return CurrentIncreaseContrastMode(device); },
428
+ [](Napi::Env env, long long mode) -> Napi::Value { return Napi::Number::New(env, static_cast<double>(mode)); });
429
+ }
430
+
431
+ Napi::Value SetIncreaseContrast(const Napi::CallbackInfo& info) {
432
+ id device = device_;
433
+ BOOL enabled = info[0].As<Napi::Boolean>().Value();
434
+ return RunAsyncVoid(info.Env(), [device, enabled]() {
435
+ NSError* error = nil;
436
+ ThrowIfFailed(coresim::SetIncreaseContrastEnabled(device, enabled, &error), error);
437
+ });
438
+ }
439
+
440
+ Napi::Value GetContentSize(const Napi::CallbackInfo& info) {
441
+ id device = device_;
442
+ return RunAsync<long long>(
443
+ info.Env(), [device]() -> long long { return CurrentContentSizeCategory(device); },
444
+ [](Napi::Env env, long long category) -> Napi::Value {
445
+ return Napi::Number::New(env, static_cast<double>(category));
446
+ });
447
+ }
448
+
449
+ Napi::Value SetContentSize(const Napi::CallbackInfo& info) {
450
+ id device = device_;
451
+ long long category = info[0].As<Napi::Number>().Int64Value();
452
+ return RunAsyncVoid(info.Env(), [device, category]() {
453
+ NSError* error = nil;
454
+ ThrowIfFailed(coresim::SetContentSizeCategory(device, category, &error), error);
455
+ });
456
+ }
457
+
458
+ Napi::Value GrantPermission(const Napi::CallbackInfo& info) { return SetPermission(info, YES); }
459
+ Napi::Value RevokePermission(const Napi::CallbackInfo& info) { return SetPermission(info, NO); }
460
+
461
+ // Writes directly to the simulator's own TCC.db instead of calling CoreSimulator's
462
+ // setPrivacyAccessForService:bundleID:granted:error: — that private method requires the calling
463
+ // process to hold an entitlement no ordinary npm package can obtain (see CLAUDE.md).
464
+ Napi::Value SetPermission(const Napi::CallbackInfo& info, BOOL granted) {
465
+ id device = device_;
466
+ NSString* service = @(info[0].As<Napi::String>().Utf8Value().c_str());
467
+ NSString* bundleId = @(info[1].As<Napi::String>().Utf8Value().c_str());
468
+ return RunAsyncVoid(info.Env(), [device, service, bundleId, granted]() {
469
+ NSError* error = nil;
470
+ NSString* dataPath = coresim::DeviceDataPath(device);
471
+ ThrowIfFailed(coresim::SetTCCAccess(dataPath, service, bundleId, granted, &error), error);
472
+ });
473
+ }
474
+
475
+ Napi::Value ResetPermission(const Napi::CallbackInfo& info) {
476
+ id device = device_;
477
+ NSString* service = @(info[0].As<Napi::String>().Utf8Value().c_str());
478
+ NSString* bundleId = @(info[1].As<Napi::String>().Utf8Value().c_str());
479
+ return RunAsyncVoid(info.Env(), [device, service, bundleId]() {
480
+ NSError* error = nil;
481
+ NSString* dataPath = coresim::DeviceDataPath(device);
482
+ ThrowIfFailed(coresim::ResetTCCAccess(dataPath, service, bundleId, &error), error);
483
+ });
484
+ }
485
+
486
+ // Reads directly from the same TCC.db SetPermission/ResetPermission write to — no
487
+ // CoreSimulator getter for privacy-access state exists (see tcc_privacy.h).
488
+ Napi::Value GetPermission(const Napi::CallbackInfo& info) {
489
+ id device = device_;
490
+ NSString* service = @(info[0].As<Napi::String>().Utf8Value().c_str());
491
+ NSString* bundleId = @(info[1].As<Napi::String>().Utf8Value().c_str());
492
+ return RunAsync<TCCAuthStatus>(
493
+ info.Env(),
494
+ [device, service, bundleId]() -> TCCAuthStatus {
495
+ NSError* error = nil;
496
+ TCCAuthStatus status = kTCCAuthNotDetermined;
497
+ NSString* dataPath = coresim::DeviceDataPath(device);
498
+ ThrowIfFailed(coresim::GetTCCAccess(dataPath, service, bundleId, &status, &error), error);
499
+ return status;
500
+ },
501
+ [](Napi::Env env, TCCAuthStatus status) -> Napi::Value {
502
+ return Napi::String::New(env, TCCAuthStatusToString(status));
503
+ });
504
+ }
505
+
506
+ Napi::Value DarwinNotificationGetState(const Napi::CallbackInfo& info) {
507
+ id device = device_;
508
+ NSString* name = @(info[0].As<Napi::String>().Utf8Value().c_str());
509
+ return RunAsync<unsigned long long>(
510
+ info.Env(),
511
+ [device, name]() -> unsigned long long {
512
+ unsigned long long state = 0;
513
+ NSError* error = nil;
514
+ ThrowIfFailed(coresim::DarwinNotificationGetState(device, &state, name, &error), error);
515
+ return state;
516
+ },
517
+ // A JS `number` (double) only has 53 bits of integer precision — this state is a full
518
+ // 64-bit value any process can stuff an arbitrary counter or bit field into (see
519
+ // commands/darwin-notification.ts), so it's exposed as a bigint instead, via the matching
520
+ // Node-API uint64 conversion, rather than silently rounding it on the way out.
521
+ [](Napi::Env env, unsigned long long state) -> Napi::Value { return Napi::BigInt::New(env, state); });
522
+ }
523
+
524
+ Napi::Value DarwinNotificationSetState(const Napi::CallbackInfo& info) {
525
+ id device = device_;
526
+ NSString* name = @(info[0].As<Napi::String>().Utf8Value().c_str());
527
+ bool lossless = false;
528
+ unsigned long long state = info[1].As<Napi::BigInt>().Uint64Value(&lossless);
529
+ // Checked inside the async lambda (not thrown synchronously here) so an out-of-range bigint
530
+ // rejects the returned promise like every other failure this method can have, instead of
531
+ // throwing synchronously and behaving inconsistently with the rest of the async API surface.
532
+ return RunAsyncVoid(info.Env(), [device, name, state, lossless]() {
533
+ if (!lossless) {
534
+ throw std::invalid_argument("state must fit in an unsigned 64-bit integer (0 to 2^64-1)");
535
+ }
536
+ NSError* error = nil;
537
+ ThrowIfFailed(coresim::DarwinNotificationSetState(device, state, name, &error), error);
538
+ });
539
+ }
540
+
541
+ Napi::Value PostDarwinNotification(const Napi::CallbackInfo& info) {
542
+ id device = device_;
543
+ NSString* name = @(info[0].As<Napi::String>().Utf8Value().c_str());
544
+ return RunAsyncVoid(info.Env(), [device, name]() {
545
+ NSError* error = nil;
546
+ ThrowIfFailed(coresim::PostDarwinNotification(device, name, &error), error);
547
+ });
548
+ }
549
+
550
+ Napi::Value AddMedia(const Napi::CallbackInfo& info) {
551
+ id device = device_;
552
+ Napi::Array paths = info[0].As<Napi::Array>();
553
+ NSMutableArray<NSURL*>* fileURLs = [NSMutableArray arrayWithCapacity:paths.Length()];
554
+ for (uint32_t i = 0; i < paths.Length(); i++) {
555
+ Napi::Value path = paths.Get(i);
556
+ [fileURLs addObject:[NSURL fileURLWithPath:@(path.As<Napi::String>().Utf8Value().c_str())]];
557
+ }
558
+ return RunAsyncVoid(info.Env(), [device, fileURLs]() {
559
+ NSError* error = nil;
560
+ ThrowIfFailed(coresim::AddMedia(device, fileURLs, &error), error);
561
+ });
562
+ }
563
+
564
+ Napi::Value AddPhoto(const Napi::CallbackInfo& info) {
565
+ id device = device_;
566
+ NSURL* url = [NSURL fileURLWithPath:@(info[0].As<Napi::String>().Utf8Value().c_str())];
567
+ return RunAsyncVoid(info.Env(), [device, url]() {
568
+ NSError* error = nil;
569
+ ThrowIfFailed(coresim::AddPhoto(device, url, &error), error);
570
+ });
571
+ }
572
+
573
+ Napi::Value AddVideo(const Napi::CallbackInfo& info) {
574
+ id device = device_;
575
+ NSURL* url = [NSURL fileURLWithPath:@(info[0].As<Napi::String>().Utf8Value().c_str())];
576
+ return RunAsyncVoid(info.Env(), [device, url]() {
577
+ NSError* error = nil;
578
+ ThrowIfFailed(coresim::AddVideo(device, url, &error), error);
579
+ });
580
+ }
581
+
582
+ Napi::Value GetPasteboard(const Napi::CallbackInfo& info) {
583
+ id device = device_;
584
+ return RunAsync<NSString*>(
585
+ info.Env(),
586
+ [device]() -> NSString* {
587
+ NSError* error = nil;
588
+ NSString* result = coresim::PullPasteboardString(device, &error);
589
+ ThrowIfFailed(result != nil, error);
590
+ return result;
591
+ },
592
+ [](Napi::Env env, NSString* result) -> Napi::Value {
593
+ return Napi::String::New(env, result ? result.UTF8String : "");
594
+ });
595
+ }
596
+
597
+ Napi::Value SetPasteboard(const Napi::CallbackInfo& info) {
598
+ id device = device_;
599
+ NSString* content = @(info[0].As<Napi::String>().Utf8Value().c_str());
600
+ return RunAsyncVoid(info.Env(), [device, content]() {
601
+ NSError* error = nil;
602
+ ThrowIfFailed(coresim::PushPasteboardString(device, content, &error), error);
603
+ });
604
+ }
605
+
606
+ // See native/sim_process.h.
607
+ Napi::Value GetWebInspectorSocket(const Napi::CallbackInfo& info) {
608
+ NSString* udid = DeviceUDID(device_).UUIDString;
609
+ return RunAsync<NSString*>(
610
+ info.Env(),
611
+ [udid]() -> NSString* {
612
+ NSError* error = nil;
613
+ NSString* result = coresim::FindWebInspectorSocket(udid, &error);
614
+ ThrowIfFailed(result != nil, error);
615
+ return result;
616
+ },
617
+ [](Napi::Env env, NSString* result) -> Napi::Value {
618
+ return Napi::String::New(env, result ? result.UTF8String : "");
619
+ });
620
+ }
621
+
622
+ // `format`/`displayId`/`quality` are this addon's own options, not a CoreSimulator options
623
+ // dictionary — the TS layer (commands/screenshot.ts) already constrains `format` to
624
+ // 'png'/'jpeg' and `quality` to a finite 0-100, so anything else (including absent) is just
625
+ // defaulted here rather than validated again.
626
+ Napi::Value Screenshot(const Napi::CallbackInfo& info) {
627
+ id device = device_;
628
+ coresim::ScreenshotFormat format = coresim::ScreenshotFormat::kPNG;
629
+ NSString* displayId = nil;
630
+ NSNumber* jpegQualityPercent = nil;
631
+ if (info.Length() > 0 && info[0].IsObject()) {
632
+ Napi::Object options = info[0].As<Napi::Object>();
633
+ if (options.Has("format") && options.Get("format").IsString() &&
634
+ options.Get("format").As<Napi::String>().Utf8Value() == "jpeg") {
635
+ format = coresim::ScreenshotFormat::kJPEG;
636
+ }
637
+ if (options.Has("displayId") && options.Get("displayId").IsString()) {
638
+ displayId = @(options.Get("displayId").As<Napi::String>().Utf8Value().c_str());
639
+ }
640
+ if (options.Has("quality") && options.Get("quality").IsNumber()) {
641
+ jpegQualityPercent = @(options.Get("quality").As<Napi::Number>().DoubleValue());
642
+ }
643
+ }
644
+ return RunAsync<NSData*>(
645
+ info.Env(),
646
+ [device, displayId, format, jpegQualityPercent]() -> NSData* {
647
+ NSError* error = nil;
648
+ NSData* result = coresim::CaptureScreenshot(device, displayId, format, jpegQualityPercent, &error);
649
+ ThrowIfFailed(result != nil, error);
650
+ return result;
651
+ },
652
+ [](Napi::Env env, NSData* result) -> Napi::Value {
653
+ return Napi::Buffer<uint8_t>::Copy(env, static_cast<const uint8_t*>(result.bytes), result.length);
654
+ });
655
+ }
656
+
657
+ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
658
+ id device = device_;
659
+ return RunAsync<NSArray*>(
660
+ info.Env(),
661
+ [device]() -> NSArray* {
662
+ NSError* error = nil;
663
+ NSArray* result = coresim::ListDisplays(device, &error);
664
+ ThrowIfFailed(result != nil, error);
665
+ return result;
666
+ },
667
+ [](Napi::Env env, NSArray* result) -> Napi::Value { return NSObjectToJsValue(env, result); });
668
+ }
669
+
670
+ // Option dictionary keys for `spawnWithPath:options:...` aren't part of the ObjC runtime
671
+ // metadata this addon resolves selectors from (they're string literals inside CoreSimulator's
672
+ // own implementation) — confirmed by resolving each `SimDeviceSpawnKey*` symbol at runtime via
673
+ // `dlsym` against the already-loaded framework (see CLAUDE.md).
674
+ // `stdout`/`stderr` are always set here, internally, to a pipe we own — never passed through
675
+ // from `options` (SpawnOptions, src/types.ts, deliberately has no such keys: a wrong value type
676
+ // there crashes the whole process uncatchably, see CLAUDE.md).
677
+ Napi::Value Spawn(const Napi::CallbackInfo& info) {
678
+ Napi::Env env = info.Env();
679
+ id device = device_;
680
+ NSString* path = @(info[0].As<Napi::String>().Utf8Value().c_str());
681
+ NSDictionary* userOptions = OptionsArg(info, 1);
682
+ Napi::Function onExit = info[2].As<Napi::Function>();
683
+
684
+ // Must be constructed on the main thread; released exactly once — either below (if the spawn
685
+ // call never actually starts a process) or inside terminationHandler once CoreSimulator
686
+ // invokes it (which it does exactly once per successfully spawned process).
687
+ Napi::ThreadSafeFunction exitTsfn = Napi::ThreadSafeFunction::New(env, onExit, "coresim spawn exit", 0, 1);
688
+
689
+ return RunAsync<SpawnResult>(
690
+ env,
691
+ [device, path, userOptions, exitTsfn]() -> SpawnResult {
692
+ // NSFileHandle wrapping a pipe's write end is a confirmed-safe value for stdout/stderr
693
+ // (see CLAUDE.md) — verified empirically against a live simulator, including that our
694
+ // own copy of the write end must be closed right after spawning (below) for EOF to ever
695
+ // reach the read end once the child exits.
696
+ NSPipe* stdoutPipe = [NSPipe pipe];
697
+ NSPipe* stderrPipe = [NSPipe pipe];
698
+
699
+ // dup() the read ends — and check both results — before ever starting the process, so a
700
+ // descriptor-table exhaustion (EMFILE) is reported as a normal catchable error instead of
701
+ // either leaving an untracked spawned process behind or handing JS an fd of -1 (which
702
+ // would only surface later, as an opaque failure to wrap it in a net.Socket). The dup()'d
703
+ // fds must outlive these NSFileHandle/NSPipe objects' own ARC lifetime, which would
704
+ // otherwise close the original fd out from under Node once nothing in this function
705
+ // references them anymore (verified empirically necessary).
706
+ int stdoutFd = dup(stdoutPipe.fileHandleForReading.fileDescriptor);
707
+ if (stdoutFd < 0) {
708
+ int savedErrno = errno;
709
+ // No process has started at this point, so terminationHandler (the only other place
710
+ // that releases exitTsfn) will never run — release it here too, or the still-referenced
711
+ // ThreadSafeFunction keeps Node's event loop alive after the caller handles the
712
+ // rejection.
713
+ exitTsfn.Release();
714
+ throw NSErrorException(MakeDescriptorError(@"stdout", savedErrno));
715
+ }
716
+ int stderrFd = dup(stderrPipe.fileHandleForReading.fileDescriptor);
717
+ if (stderrFd < 0) {
718
+ int savedErrno = errno;
719
+ close(stdoutFd);
720
+ exitTsfn.Release();
721
+ throw NSErrorException(MakeDescriptorError(@"stderr", savedErrno));
722
+ }
723
+
724
+ NSMutableDictionary* options = [userOptions mutableCopy];
725
+ options[@"stdout"] = stdoutPipe.fileHandleForWriting;
726
+ options[@"stderr"] = stderrPipe.fileHandleForWriting;
727
+
728
+ void (^terminationHandler)(int) = ^(int status) {
729
+ // Confirmed empirically (see CLAUDE.md): `status` is a raw wait(2)-style status, not a
730
+ // plain exit code — a normal exit(127) arrived here as 32512 (127 << 8), and a
731
+ // SIGTERM kill arrived as the bare signal number 15. Decode to Node child_process-
732
+ // style (code, signal), exactly one of which is set.
733
+ exitTsfn.BlockingCall([status](Napi::Env env, Napi::Function jsCallback) {
734
+ Napi::Value code = env.Null();
735
+ Napi::Value signal = env.Null();
736
+ if (WIFEXITED(status)) {
737
+ code = Napi::Number::New(env, WEXITSTATUS(status));
738
+ } else if (WIFSIGNALED(status)) {
739
+ signal = Napi::Number::New(env, WTERMSIG(status));
740
+ }
741
+ jsCallback.Call({code, signal});
742
+ });
743
+ exitTsfn.Release();
744
+ };
745
+
746
+ NSError* error = nil;
747
+ int pid;
748
+ try {
749
+ pid = coresim::Spawn(device, path, options, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0),
750
+ terminationHandler, &error);
751
+ } catch (...) {
752
+ [stdoutPipe.fileHandleForWriting closeFile];
753
+ [stderrPipe.fileHandleForWriting closeFile];
754
+ close(stdoutFd);
755
+ close(stderrFd);
756
+ exitTsfn.Release();
757
+ throw;
758
+ }
759
+ // Our own copies of the write ends must close regardless of outcome: on failure, so
760
+ // they don't dangle open in this process; on success, so EOF ever reaches the read ends
761
+ // once the child (the only remaining writer) exits.
762
+ [stdoutPipe.fileHandleForWriting closeFile];
763
+ [stderrPipe.fileHandleForWriting closeFile];
764
+ if (pid <= 0) {
765
+ // terminationHandler will never fire for a process that never started.
766
+ exitTsfn.Release();
767
+ close(stdoutFd);
768
+ close(stderrFd);
769
+ }
770
+ ThrowIfFailed(pid > 0, error);
771
+
772
+ SpawnResult result;
773
+ result.pid = pid;
774
+ result.stdoutFd = stdoutFd;
775
+ result.stderrFd = stderrFd;
776
+ return result;
777
+ },
778
+ [](Napi::Env env, SpawnResult result) -> Napi::Value {
779
+ Napi::Object obj = Napi::Object::New(env);
780
+ obj.Set("pid", result.pid);
781
+ obj.Set("stdoutFd", result.stdoutFd);
782
+ obj.Set("stderrFd", result.stderrFd);
783
+ return obj;
784
+ });
785
+ }
786
+ };
787
+
788
+ NativeDevice::NativeDevice(const Napi::CallbackInfo& info) : Napi::ObjectWrap<NativeDevice>(info) {
789
+ device_ = UnwrapExternalId(info);
790
+ }
791
+
792
+ void NativeDevice::Init(Napi::Env env) {
793
+ Napi::Function ctor =
794
+ DefineClass(env, "NativeDevice",
795
+ {
796
+ InstanceMethod<&NativeDevice::Udid>("udid"),
797
+ InstanceMethod<&NativeDevice::Name>("name"),
798
+ InstanceMethod<&NativeDevice::State>("state"),
799
+ InstanceMethod<&NativeDevice::DeviceTypeIdentifier>("deviceTypeIdentifier"),
800
+ InstanceMethod<&NativeDevice::RuntimeIdentifier>("runtimeIdentifier"),
801
+ InstanceMethod<&NativeDevice::RuntimeRootPath>("runtimeRootPath"),
802
+ InstanceMethod<&NativeDevice::Boot>("boot"),
803
+ InstanceMethod<&NativeDevice::GetBootStatus>("getBootStatus"),
804
+ InstanceMethod<&NativeDevice::Shutdown>("shutdown"),
805
+ InstanceMethod<&NativeDevice::Erase>("erase"),
806
+ InstanceMethod<&NativeDevice::Getenv>("getenv"),
807
+ InstanceMethod<&NativeDevice::InstallApp>("installApp"),
808
+ InstanceMethod<&NativeDevice::UninstallApp>("uninstallApp"),
809
+ InstanceMethod<&NativeDevice::LaunchApp>("launchApp"),
810
+ InstanceMethod<&NativeDevice::TerminateApp>("terminateApp"),
811
+ InstanceMethod<&NativeDevice::PropertiesOfApplication>("propertiesOfApplication"),
812
+ InstanceMethod<&NativeDevice::InstalledApps>("installedApps"),
813
+ InstanceMethod<&NativeDevice::OpenUrl>("openUrl"),
814
+ InstanceMethod<&NativeDevice::SetLocation>("setLocation"),
815
+ InstanceMethod<&NativeDevice::ClearLocation>("clearLocation"),
816
+ InstanceMethod<&NativeDevice::SendPushNotification>("sendPushNotification"),
817
+ InstanceMethod<&NativeDevice::AddCertificate>("addCertificate"),
818
+ InstanceMethod<&NativeDevice::ResetKeychain>("resetKeychain"),
819
+ InstanceMethod<&NativeDevice::GetUIAppearance>("getUIAppearance"),
820
+ InstanceMethod<&NativeDevice::SetUIAppearance>("setUIAppearance"),
821
+ InstanceMethod<&NativeDevice::GetIncreaseContrast>("getIncreaseContrast"),
822
+ InstanceMethod<&NativeDevice::SetIncreaseContrast>("setIncreaseContrast"),
823
+ InstanceMethod<&NativeDevice::GetContentSize>("getContentSize"),
824
+ InstanceMethod<&NativeDevice::SetContentSize>("setContentSize"),
825
+ InstanceMethod<&NativeDevice::GrantPermission>("grantPermission"),
826
+ InstanceMethod<&NativeDevice::RevokePermission>("revokePermission"),
827
+ InstanceMethod<&NativeDevice::ResetPermission>("resetPermission"),
828
+ InstanceMethod<&NativeDevice::GetPermission>("getPermission"),
829
+ InstanceMethod<&NativeDevice::DarwinNotificationGetState>("darwinNotificationGetState"),
830
+ InstanceMethod<&NativeDevice::DarwinNotificationSetState>("darwinNotificationSetState"),
831
+ InstanceMethod<&NativeDevice::PostDarwinNotification>("postDarwinNotification"),
832
+ InstanceMethod<&NativeDevice::AddMedia>("addMedia"),
833
+ InstanceMethod<&NativeDevice::AddPhoto>("addPhoto"),
834
+ InstanceMethod<&NativeDevice::AddVideo>("addVideo"),
835
+ InstanceMethod<&NativeDevice::GetPasteboard>("getPasteboard"),
836
+ InstanceMethod<&NativeDevice::SetPasteboard>("setPasteboard"),
837
+ InstanceMethod<&NativeDevice::GetWebInspectorSocket>("getWebInspectorSocket"),
838
+ InstanceMethod<&NativeDevice::Screenshot>("screenshot"),
839
+ InstanceMethod<&NativeDevice::GetDisplays>("getDisplays"),
840
+ InstanceMethod<&NativeDevice::Spawn>("spawn"),
841
+ });
842
+ env.GetInstanceData<AddonInstanceData>()->deviceConstructor = Napi::Persistent(ctor);
843
+ }
844
+
845
+ Napi::Object NativeDevice::NewInstance(Napi::Env env, id device) {
846
+ return WrapExternalId(env, env.GetInstanceData<AddonInstanceData>()->deviceConstructor, device);
847
+ }
848
+
849
+ class NativeDeviceSet : public Napi::ObjectWrap<NativeDeviceSet> {
850
+ public:
851
+ static void Init(Napi::Env env);
852
+ static Napi::Object NewInstance(Napi::Env env, id deviceSet, id serviceContext);
853
+ explicit NativeDeviceSet(const Napi::CallbackInfo& info);
854
+
855
+ private:
856
+ id deviceSet_;
857
+ id serviceContext_;
858
+
859
+ Napi::Value GetDevices(const Napi::CallbackInfo& info) {
860
+ id deviceSet = deviceSet_;
861
+ return RunAsync<NSArray*>(
862
+ info.Env(), [deviceSet]() -> NSArray* { return Devices(deviceSet); },
863
+ [](Napi::Env env, NSArray* devices) -> Napi::Value {
864
+ Napi::Array result = Napi::Array::New(env, devices.count);
865
+ for (NSUInteger i = 0; i < devices.count; i++) {
866
+ result[static_cast<uint32_t>(i)] = NativeDevice::NewInstance(env, devices[i]);
867
+ }
868
+ return result;
869
+ });
870
+ }
871
+
872
+ Napi::Value CreateDeviceMethod(const Napi::CallbackInfo& info) {
873
+ id deviceSet = deviceSet_;
874
+ id serviceContext = serviceContext_;
875
+ NSString* typeId = @(info[0].As<Napi::String>().Utf8Value().c_str());
876
+ NSString* runtimeId = @(info[1].As<Napi::String>().Utf8Value().c_str());
877
+ NSString* name = @(info[2].As<Napi::String>().Utf8Value().c_str());
878
+ return RunAsync<id>(
879
+ info.Env(),
880
+ [deviceSet, serviceContext, typeId, runtimeId, name]() -> id {
881
+ id deviceType = SupportedDeviceTypesByIdentifier(serviceContext)[typeId];
882
+ id runtime = SupportedRuntimesByIdentifier(serviceContext)[runtimeId];
883
+ if (deviceType == nil) {
884
+ throw std::invalid_argument("Unknown device type identifier: " + std::string(typeId.UTF8String));
885
+ }
886
+ if (runtime == nil) {
887
+ throw std::invalid_argument("Unknown runtime identifier: " + std::string(runtimeId.UTF8String));
888
+ }
889
+ NSError* error = nil;
890
+ id device = CreateDevice(deviceSet, deviceType, runtime, name, &error);
891
+ ThrowIfFailed(device != nil, error);
892
+ return device;
893
+ },
894
+ [](Napi::Env env, id device) -> Napi::Value { return NativeDevice::NewInstance(env, device); });
895
+ }
896
+
897
+ Napi::Value DeleteDeviceMethod(const Napi::CallbackInfo& info) {
898
+ id deviceSet = deviceSet_;
899
+ // Napi::ObjectWrap::Unwrap must run on the main thread, so this stays outside the work lambda.
900
+ NativeDevice* device = Napi::ObjectWrap<NativeDevice>::Unwrap(info[0].As<Napi::Object>());
901
+ id rawDevice = device->device_;
902
+ return RunAsyncVoid(info.Env(), [deviceSet, rawDevice]() {
903
+ NSError* error = nil;
904
+ ThrowIfFailed(DeleteDevice(deviceSet, rawDevice, &error), error);
905
+ });
906
+ }
907
+ };
908
+
909
+ NativeDeviceSet::NativeDeviceSet(const Napi::CallbackInfo& info) : Napi::ObjectWrap<NativeDeviceSet>(info) {
910
+ deviceSet_ = UnwrapExternalId(info);
911
+ serviceContext_ = *static_cast<__unsafe_unretained id*>(info[1].As<Napi::External<void>>().Data());
912
+ }
913
+
914
+ void NativeDeviceSet::Init(Napi::Env env) {
915
+ Napi::Function ctor = DefineClass(env, "NativeDeviceSet",
916
+ {
917
+ InstanceMethod<&NativeDeviceSet::GetDevices>("devices"),
918
+ InstanceMethod<&NativeDeviceSet::CreateDeviceMethod>("createDevice"),
919
+ InstanceMethod<&NativeDeviceSet::DeleteDeviceMethod>("deleteDevice"),
920
+ });
921
+ env.GetInstanceData<AddonInstanceData>()->deviceSetConstructor = Napi::Persistent(ctor);
922
+ }
923
+
924
+ Napi::Object NativeDeviceSet::NewInstance(Napi::Env env, id deviceSet, id serviceContext) {
925
+ __unsafe_unretained id boxedDeviceSet = deviceSet;
926
+ __unsafe_unretained id boxedServiceContext = serviceContext;
927
+ return env.GetInstanceData<AddonInstanceData>()->deviceSetConstructor.New(
928
+ {Napi::External<void>::New(env, &boxedDeviceSet), Napi::External<void>::New(env, &boxedServiceContext)});
929
+ }
930
+
931
+ class NativeServiceContext : public Napi::ObjectWrap<NativeServiceContext> {
932
+ public:
933
+ static void Init(Napi::Env env);
934
+ static Napi::Object NewInstance(Napi::Env env, id serviceContext);
935
+ explicit NativeServiceContext(const Napi::CallbackInfo& info);
936
+
937
+ private:
938
+ id serviceContext_;
939
+
940
+ Napi::Value DefaultDeviceSetMethod(const Napi::CallbackInfo& info) {
941
+ id serviceContext = serviceContext_;
942
+ return RunAsync<id>(
943
+ info.Env(),
944
+ [serviceContext]() -> id {
945
+ NSError* error = nil;
946
+ id deviceSet = DefaultDeviceSet(serviceContext, &error);
947
+ ThrowIfFailed(deviceSet != nil, error);
948
+ return deviceSet;
949
+ },
950
+ [serviceContext](Napi::Env env, id deviceSet) -> Napi::Value {
951
+ return NativeDeviceSet::NewInstance(env, deviceSet, serviceContext);
952
+ });
953
+ }
954
+
955
+ Napi::Value SupportedDeviceTypesMethod(const Napi::CallbackInfo& info) {
956
+ id serviceContext = serviceContext_;
957
+ return RunAsync<std::vector<DeviceTypeEntry>>(
958
+ info.Env(),
959
+ [serviceContext]() -> std::vector<DeviceTypeEntry> {
960
+ NSArray* types = coresim::SupportedDeviceTypes(serviceContext);
961
+ std::vector<DeviceTypeEntry> result;
962
+ result.reserve(types.count);
963
+ for (NSUInteger i = 0; i < types.count; i++) {
964
+ result.push_back({DeviceTypeIdentifier(types[i]).UTF8String, DeviceTypeName(types[i]).UTF8String});
965
+ }
966
+ return result;
967
+ },
968
+ [](Napi::Env env, std::vector<DeviceTypeEntry> types) -> Napi::Value {
969
+ Napi::Array result = Napi::Array::New(env, types.size());
970
+ for (size_t i = 0; i < types.size(); i++) {
971
+ Napi::Object entry = Napi::Object::New(env);
972
+ entry.Set("identifier", types[i].identifier);
973
+ entry.Set("name", types[i].name);
974
+ result[static_cast<uint32_t>(i)] = entry;
975
+ }
976
+ return result;
977
+ });
978
+ }
979
+
980
+ Napi::Value SupportedRuntimesMethod(const Napi::CallbackInfo& info) {
981
+ id serviceContext = serviceContext_;
982
+ return RunAsync<std::vector<RuntimeEntry>>(
983
+ info.Env(),
984
+ [serviceContext]() -> std::vector<RuntimeEntry> {
985
+ NSArray* runtimes = coresim::SupportedRuntimes(serviceContext);
986
+ std::vector<RuntimeEntry> result;
987
+ result.reserve(runtimes.count);
988
+ for (NSUInteger i = 0; i < runtimes.count; i++) {
989
+ result.push_back({RuntimeIdentifier(runtimes[i]).UTF8String, RuntimeName(runtimes[i]).UTF8String,
990
+ RuntimeVersionString(runtimes[i]).UTF8String});
991
+ }
992
+ return result;
993
+ },
994
+ [](Napi::Env env, std::vector<RuntimeEntry> runtimes) -> Napi::Value {
995
+ Napi::Array result = Napi::Array::New(env, runtimes.size());
996
+ for (size_t i = 0; i < runtimes.size(); i++) {
997
+ Napi::Object entry = Napi::Object::New(env);
998
+ entry.Set("identifier", runtimes[i].identifier);
999
+ entry.Set("name", runtimes[i].name);
1000
+ entry.Set("versionString", runtimes[i].versionString);
1001
+ result[static_cast<uint32_t>(i)] = entry;
1002
+ }
1003
+ return result;
1004
+ });
1005
+ }
1006
+ };
1007
+
1008
+ NativeServiceContext::NativeServiceContext(const Napi::CallbackInfo& info)
1009
+ : Napi::ObjectWrap<NativeServiceContext>(info) {
1010
+ serviceContext_ = UnwrapExternalId(info);
1011
+ }
1012
+
1013
+ void NativeServiceContext::Init(Napi::Env env) {
1014
+ Napi::Function ctor =
1015
+ DefineClass(env, "NativeServiceContext",
1016
+ {
1017
+ InstanceMethod<&NativeServiceContext::DefaultDeviceSetMethod>("defaultDeviceSet"),
1018
+ InstanceMethod<&NativeServiceContext::SupportedDeviceTypesMethod>("supportedDeviceTypes"),
1019
+ InstanceMethod<&NativeServiceContext::SupportedRuntimesMethod>("supportedRuntimes"),
1020
+ });
1021
+ env.GetInstanceData<AddonInstanceData>()->serviceContextConstructor = Napi::Persistent(ctor);
1022
+ }
1023
+
1024
+ Napi::Object NativeServiceContext::NewInstance(Napi::Env env, id serviceContext) {
1025
+ return WrapExternalId(env, env.GetInstanceData<AddonInstanceData>()->serviceContextConstructor, serviceContext);
1026
+ }
1027
+
1028
+ Napi::Value SharedServiceContextBinding(const Napi::CallbackInfo& info) {
1029
+ Napi::Env env = info.Env();
1030
+ NSString* developerDir = @(info[0].As<Napi::String>().Utf8Value().c_str());
1031
+ return RunAsync<id>(
1032
+ env,
1033
+ [developerDir]() -> id {
1034
+ NSError* error = nil;
1035
+ id context = SharedServiceContext(developerDir, &error);
1036
+ ThrowIfFailed(context != nil, error);
1037
+ return context;
1038
+ },
1039
+ [](Napi::Env env, id context) -> Napi::Value { return NativeServiceContext::NewInstance(env, context); });
1040
+ }
1041
+
1042
+ Napi::Value FrameworkVersionBinding(const Napi::CallbackInfo& info) {
1043
+ return RunAsync<std::string>(
1044
+ info.Env(), []() -> std::string { return CoreSimulatorFrameworkVersion(); },
1045
+ [](Napi::Env env, std::string version) -> Napi::Value { return Napi::String::New(env, version); });
1046
+ }
1047
+
1048
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1049
+ // Runs once per Environment (see AddonInstanceData above) — never shared across a
1050
+ // worker_threads instance also `require()`-ing this addon.
1051
+ env.SetInstanceData(new AddonInstanceData());
1052
+ NativeDevice::Init(env);
1053
+ NativeDeviceSet::Init(env);
1054
+ NativeServiceContext::Init(env);
1055
+ exports.Set("sharedServiceContext", Napi::Function::New(env, SharedServiceContextBinding));
1056
+ exports.Set("frameworkVersion", Napi::Function::New(env, FrameworkVersionBinding));
1057
+ return exports;
1058
+ }
1059
+
1060
+ } // namespace coresim
1061
+
1062
+ namespace {
1063
+ Napi::Object InitCoreSimModule(Napi::Env env, Napi::Object exports) { return coresim::Init(env, exports); }
1064
+ } // namespace
1065
+
1066
+ NODE_API_MODULE(coresim, InitCoreSimModule)