@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
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)
|