@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/errors.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Base error for every failure raised by the native CoreSimulator addon. */
|
|
2
|
+
export class NativeSimError extends Error {
|
|
3
|
+
public override name = 'NativeSimError';
|
|
4
|
+
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The class/selector a call needed doesn't exist on the currently loaded CoreSimulator.framework,
|
|
12
|
+
* or a dynamic dispatch raised an NSException that got caught instead of crashing the process.
|
|
13
|
+
* Distinct from {@link NativeSimOperationError}: this means "not supported on this simulator
|
|
14
|
+
* runtime", not "the call ran and Apple's own code rejected it".
|
|
15
|
+
*/
|
|
16
|
+
export class NativeSimUnavailableError extends NativeSimError {
|
|
17
|
+
public override name = 'NativeSimUnavailableError';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param message — human-readable description
|
|
21
|
+
* @param kind — `'class'` | `'selector'` | `'framework'`
|
|
22
|
+
* @param missing — the missing class/selector/framework name
|
|
23
|
+
* @param frameworkVersion — `CFBundleVersion` of the loaded CoreSimulator.framework
|
|
24
|
+
*/
|
|
25
|
+
constructor(
|
|
26
|
+
message: string,
|
|
27
|
+
public readonly kind: string,
|
|
28
|
+
public readonly missing: string,
|
|
29
|
+
public readonly frameworkVersion: string,
|
|
30
|
+
) {
|
|
31
|
+
super(message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A dynamic dispatch raised an NSException that was caught rather than crashing the process. */
|
|
36
|
+
export class NativeSimDispatchError extends NativeSimError {
|
|
37
|
+
public override name = 'NativeSimDispatchError';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param message — human-readable description
|
|
41
|
+
* @param exceptionName — the caught `NSException`'s `name`
|
|
42
|
+
* @param reason — the caught `NSException`'s `reason`
|
|
43
|
+
*/
|
|
44
|
+
constructor(
|
|
45
|
+
message: string,
|
|
46
|
+
public readonly exceptionName: string,
|
|
47
|
+
public readonly reason: string,
|
|
48
|
+
) {
|
|
49
|
+
super(message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The call reached CoreSimulator and Apple's own code rejected it (a structured `NSError`). */
|
|
54
|
+
export class NativeSimOperationError extends NativeSimError {
|
|
55
|
+
public override name = 'NativeSimOperationError';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param message — `NSError.localizedDescription`
|
|
59
|
+
* @param domain — `NSError.domain`
|
|
60
|
+
* @param code — `NSError.code`
|
|
61
|
+
*/
|
|
62
|
+
constructor(
|
|
63
|
+
message: string,
|
|
64
|
+
public readonly domain: string,
|
|
65
|
+
public readonly code: number,
|
|
66
|
+
) {
|
|
67
|
+
super(message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Re-throws a raw error surfaced by the native addon as the matching typed {@link NativeSimError}. */
|
|
72
|
+
export function wrapNativeError(err: unknown): never {
|
|
73
|
+
if (err instanceof NativeSimError) {
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
const error = err as Error & Record<string, unknown>;
|
|
77
|
+
const message = error?.message ?? String(err);
|
|
78
|
+
switch (error?.name) {
|
|
79
|
+
case 'NativeSimUnavailableError':
|
|
80
|
+
throw new NativeSimUnavailableError(
|
|
81
|
+
message,
|
|
82
|
+
String(error.kind ?? ''),
|
|
83
|
+
String(error.missing ?? ''),
|
|
84
|
+
String(error.frameworkVersion ?? ''),
|
|
85
|
+
);
|
|
86
|
+
case 'NativeSimDispatchError':
|
|
87
|
+
throw new NativeSimDispatchError(message, String(error.exceptionName ?? ''), String(error.reason ?? ''));
|
|
88
|
+
case 'NativeSimOperationError':
|
|
89
|
+
throw new NativeSimOperationError(message, String(error.domain ?? ''), Number(error.code ?? 0));
|
|
90
|
+
default:
|
|
91
|
+
throw new NativeSimError(message);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {NativeSimError, NativeSimUnavailableError, NativeSimDispatchError, NativeSimOperationError} from './errors.js';
|
|
2
|
+
export {NativeSimctl} from './native-simctl.js';
|
|
3
|
+
export {SpawnedProcess} from './commands/spawn.js';
|
|
4
|
+
export type {AppContainerType} from './commands/app.js';
|
|
5
|
+
export type {BiometricName} from './commands/biometric.js';
|
|
6
|
+
export {
|
|
7
|
+
SimBootStatus,
|
|
8
|
+
SimDeviceState,
|
|
9
|
+
type ApnsAlert,
|
|
10
|
+
type ApnsPayload,
|
|
11
|
+
type ApnsSound,
|
|
12
|
+
type PushNotificationPayload,
|
|
13
|
+
type ScreenshotOptions,
|
|
14
|
+
type SimBootInfo,
|
|
15
|
+
type SimDeviceInfo,
|
|
16
|
+
type SimDeviceTypeInfo,
|
|
17
|
+
type SimDisplayInfo,
|
|
18
|
+
type SimPermissionService,
|
|
19
|
+
type SimPermissionStatus,
|
|
20
|
+
type SimProcessInfo,
|
|
21
|
+
type SimRuntimeInfo,
|
|
22
|
+
type SpawnOptions,
|
|
23
|
+
} from './types.js';
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <napi.h>
|
|
4
|
+
|
|
5
|
+
#include <functional>
|
|
6
|
+
#include <memory>
|
|
7
|
+
|
|
8
|
+
#include "nserror_bridge.h"
|
|
9
|
+
#include "objc_runtime.h"
|
|
10
|
+
#include "safe_dispatch.h"
|
|
11
|
+
|
|
12
|
+
namespace coresim {
|
|
13
|
+
|
|
14
|
+
// Runs `work` on a libuv threadpool thread (Napi::AsyncWorker::Execute) and resolves/rejects the
|
|
15
|
+
// returned Promise from its result, converting it to a JS value via `toValue` — which, like every
|
|
16
|
+
// other Napi call, must run on the main thread, hence OnOK rather than Execute. This is what
|
|
17
|
+
// keeps Node's event loop responsive regardless of whether the underlying CoreSimulator call is
|
|
18
|
+
// itself async (has a completion-block variant) or a plain blocking objc_msgSend: either way, the
|
|
19
|
+
// blocking happens off the main thread.
|
|
20
|
+
//
|
|
21
|
+
// `T` must be safe to hold as a plain default-constructed class member with no ARC-ownership
|
|
22
|
+
// wrapper needed beyond what the type already provides on its own — plain C++ types (bool, int,
|
|
23
|
+
// long long, std::string) work as expected; `id`/`NSString*`/`NSArray*`/`NSDictionary*`/... also
|
|
24
|
+
// work here because this header is only ever included from an Objective-C++ (.mm) translation
|
|
25
|
+
// unit compiled with ARC, where a bare ObjC-pointer-typed class member already gets correct
|
|
26
|
+
// retain/release semantics, same as every other such member in this addon (e.g. NativeDevice's
|
|
27
|
+
// `device_`). `toValue` runs in OnOK (main thread), so it's the right — and only safe — place to
|
|
28
|
+
// do any Napi-consuming conversion (NSObjectToJsValue, building a NativeDevice, ...), even though
|
|
29
|
+
// the raw ObjC object it receives was produced by `work` on a background thread. `toValue` takes
|
|
30
|
+
// `T` by value, not `T&`: for an ObjC pointer T, a reference parameter's ARC ownership qualifier
|
|
31
|
+
// (implicitly `__strong`) has to match the callable's declared parameter type exactly, which a
|
|
32
|
+
// plain `NSString*&`-style lambda parameter doesn't — by-value sidesteps that entirely, and every
|
|
33
|
+
// T here (primitives, std::string, ObjC pointers) is cheap to copy/retain anyway.
|
|
34
|
+
template <typename T>
|
|
35
|
+
class ValueAsyncWorker : public Napi::AsyncWorker {
|
|
36
|
+
public:
|
|
37
|
+
ValueAsyncWorker(Napi::Env env, std::function<T()> work, std::function<Napi::Value(Napi::Env, T)> toValue)
|
|
38
|
+
: Napi::AsyncWorker(env),
|
|
39
|
+
work_(std::move(work)),
|
|
40
|
+
toValue_(std::move(toValue)),
|
|
41
|
+
deferred_(Napi::Promise::Deferred::New(env)) {}
|
|
42
|
+
|
|
43
|
+
Napi::Promise GetPromise() { return deferred_.Promise(); }
|
|
44
|
+
|
|
45
|
+
void Execute() override {
|
|
46
|
+
// libuv threadpool threads are persistent and reused across many Execute() calls, with no
|
|
47
|
+
// implicit per-iteration autorelease pool the way the main thread gets from a run loop —
|
|
48
|
+
// without one here, every autoreleased Foundation temporary `work_`/its callees produce (e.g.
|
|
49
|
+
// -stringWithFormat:, -pipe) would sit until the thread itself exits, growing without bound.
|
|
50
|
+
// `result_`/the caught exceptions below are real (retained) members, not autoreleased
|
|
51
|
+
// temporaries, so assigning into them before the pool drains keeps them alive regardless.
|
|
52
|
+
@autoreleasepool {
|
|
53
|
+
try {
|
|
54
|
+
result_ = work_();
|
|
55
|
+
} catch (const NativeSimUnavailableError& error) {
|
|
56
|
+
unavailable_ = std::make_unique<NativeSimUnavailableError>(error);
|
|
57
|
+
} catch (const NSErrorException& error) {
|
|
58
|
+
nsError_ = std::make_unique<NSErrorException>(error);
|
|
59
|
+
} catch (const ObjCException& error) {
|
|
60
|
+
objcException_ = std::make_unique<ObjCException>(error);
|
|
61
|
+
} catch (const std::exception& error) {
|
|
62
|
+
SetError(error.what());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
void OnOK() override {
|
|
68
|
+
Napi::Env env = Env();
|
|
69
|
+
Napi::HandleScope scope(env);
|
|
70
|
+
if (unavailable_) {
|
|
71
|
+
deferred_.Reject(UnavailableToJsError(env, *unavailable_).Value());
|
|
72
|
+
} else if (nsError_) {
|
|
73
|
+
deferred_.Reject(NSErrorExceptionToJsError(env, *nsError_).Value());
|
|
74
|
+
} else if (objcException_) {
|
|
75
|
+
deferred_.Reject(ObjCExceptionToJsError(env, *objcException_).Value());
|
|
76
|
+
} else {
|
|
77
|
+
deferred_.Resolve(toValue_(env, result_));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
void OnError(const Napi::Error& error) override {
|
|
82
|
+
Napi::HandleScope scope(Env());
|
|
83
|
+
deferred_.Reject(error.Value());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private:
|
|
87
|
+
std::function<T()> work_;
|
|
88
|
+
std::function<Napi::Value(Napi::Env, T)> toValue_;
|
|
89
|
+
T result_{};
|
|
90
|
+
std::unique_ptr<NativeSimUnavailableError> unavailable_;
|
|
91
|
+
std::unique_ptr<NSErrorException> nsError_;
|
|
92
|
+
std::unique_ptr<ObjCException> objcException_;
|
|
93
|
+
Napi::Promise::Deferred deferred_;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Same as ValueAsyncWorker but for work with no result to convert — resolves with undefined.
|
|
97
|
+
class VoidAsyncWorker : public Napi::AsyncWorker {
|
|
98
|
+
public:
|
|
99
|
+
VoidAsyncWorker(Napi::Env env, std::function<void()> work)
|
|
100
|
+
: Napi::AsyncWorker(env), work_(std::move(work)), deferred_(Napi::Promise::Deferred::New(env)) {}
|
|
101
|
+
|
|
102
|
+
Napi::Promise GetPromise() { return deferred_.Promise(); }
|
|
103
|
+
|
|
104
|
+
void Execute() override {
|
|
105
|
+
// See ValueAsyncWorker::Execute() above for why this pool is needed on a reused threadpool
|
|
106
|
+
// thread.
|
|
107
|
+
@autoreleasepool {
|
|
108
|
+
try {
|
|
109
|
+
work_();
|
|
110
|
+
} catch (const NativeSimUnavailableError& error) {
|
|
111
|
+
unavailable_ = std::make_unique<NativeSimUnavailableError>(error);
|
|
112
|
+
} catch (const NSErrorException& error) {
|
|
113
|
+
nsError_ = std::make_unique<NSErrorException>(error);
|
|
114
|
+
} catch (const ObjCException& error) {
|
|
115
|
+
objcException_ = std::make_unique<ObjCException>(error);
|
|
116
|
+
} catch (const std::exception& error) {
|
|
117
|
+
SetError(error.what());
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
void OnOK() override {
|
|
123
|
+
Napi::Env env = Env();
|
|
124
|
+
Napi::HandleScope scope(env);
|
|
125
|
+
if (unavailable_) {
|
|
126
|
+
deferred_.Reject(UnavailableToJsError(env, *unavailable_).Value());
|
|
127
|
+
} else if (nsError_) {
|
|
128
|
+
deferred_.Reject(NSErrorExceptionToJsError(env, *nsError_).Value());
|
|
129
|
+
} else if (objcException_) {
|
|
130
|
+
deferred_.Reject(ObjCExceptionToJsError(env, *objcException_).Value());
|
|
131
|
+
} else {
|
|
132
|
+
deferred_.Resolve(env.Undefined());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
void OnError(const Napi::Error& error) override {
|
|
137
|
+
Napi::HandleScope scope(Env());
|
|
138
|
+
deferred_.Reject(error.Value());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private:
|
|
142
|
+
std::function<void()> work_;
|
|
143
|
+
std::unique_ptr<NativeSimUnavailableError> unavailable_;
|
|
144
|
+
std::unique_ptr<NSErrorException> nsError_;
|
|
145
|
+
std::unique_ptr<ObjCException> objcException_;
|
|
146
|
+
Napi::Promise::Deferred deferred_;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
template <typename T>
|
|
150
|
+
Napi::Promise RunAsync(Napi::Env env, std::function<T()> work, std::function<Napi::Value(Napi::Env, T)> toValue) {
|
|
151
|
+
auto* worker = new ValueAsyncWorker<T>(env, std::move(work), std::move(toValue));
|
|
152
|
+
Napi::Promise promise = worker->GetPromise();
|
|
153
|
+
worker->Queue();
|
|
154
|
+
return promise;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
inline Napi::Promise RunAsyncVoid(Napi::Env env, std::function<void()> work) {
|
|
158
|
+
auto* worker = new VoidAsyncWorker(env, std::move(work));
|
|
159
|
+
Napi::Promise promise = worker->GetPromise();
|
|
160
|
+
worker->Queue();
|
|
161
|
+
return promise;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} // namespace coresim
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
|
|
5
|
+
#include <napi.h>
|
|
6
|
+
|
|
7
|
+
#include <string>
|
|
8
|
+
|
|
9
|
+
#include "objc_runtime.h"
|
|
10
|
+
#include "safe_dispatch.h"
|
|
11
|
+
|
|
12
|
+
namespace coresim {
|
|
13
|
+
|
|
14
|
+
// Thrown by async work lambdas (see async_bridge.h) when a native call fails via an NSError**
|
|
15
|
+
// out-param rather than raising. domain/code/message are captured immediately at throw time
|
|
16
|
+
// (never the NSError* itself), since this only ever needs to survive within the same background
|
|
17
|
+
// thread's try/catch — it's never constructed on, or crossed onto, the main thread.
|
|
18
|
+
class NSErrorException : public std::runtime_error {
|
|
19
|
+
public:
|
|
20
|
+
explicit NSErrorException(NSError* error)
|
|
21
|
+
: std::runtime_error(error.localizedDescription ? error.localizedDescription.UTF8String : "Operation failed"),
|
|
22
|
+
domain(error.domain ? error.domain.UTF8String : ""),
|
|
23
|
+
code(error.code) {}
|
|
24
|
+
|
|
25
|
+
std::string domain;
|
|
26
|
+
long long code;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Converts an NSError into a JS Error with {name: 'NativeSimOperationError', domain, code,
|
|
30
|
+
// message} — errors.ts wraps this into NativeSimError. Built from an already-caught
|
|
31
|
+
// NSErrorException's extracted fields, never a live NSError*: this only ever runs in OnOK (main
|
|
32
|
+
// thread), after the exception already crossed out of the background thread's try/catch.
|
|
33
|
+
Napi::Error NSErrorExceptionToJsError(Napi::Env env, const NSErrorException& error);
|
|
34
|
+
|
|
35
|
+
// {name: 'NativeSimUnavailableError', kind, missing, frameworkVersion} — errors.ts wraps this
|
|
36
|
+
// into NativeSimUnavailableError.
|
|
37
|
+
Napi::Error UnavailableToJsError(Napi::Env env, const NativeSimUnavailableError& error);
|
|
38
|
+
|
|
39
|
+
// {name: 'NativeSimDispatchError', exceptionName, reason} — errors.ts wraps this into
|
|
40
|
+
// NativeSimDispatchError.
|
|
41
|
+
Napi::Error ObjCExceptionToJsError(Napi::Env env, const ObjCException& error);
|
|
42
|
+
|
|
43
|
+
// Runs `fn` (the Napi-facing body of a synchronous exported method) and converts any of the three
|
|
44
|
+
// exception types this addon throws into the matching pending JS exception, so a C++/ObjC failure
|
|
45
|
+
// always surfaces as a catchable JS Error, never an uncaught native exception. On any exception,
|
|
46
|
+
// returns env.Undefined(); the pending JS exception (set via ThrowAsJavaScriptException) is what the
|
|
47
|
+
// caller actually sees.
|
|
48
|
+
template <typename Fn>
|
|
49
|
+
Napi::Value CatchToJs(Napi::Env env, Fn&& fn) {
|
|
50
|
+
try {
|
|
51
|
+
return fn();
|
|
52
|
+
} catch (const NativeSimUnavailableError& error) {
|
|
53
|
+
UnavailableToJsError(env, error).ThrowAsJavaScriptException();
|
|
54
|
+
} catch (const NSErrorException& error) {
|
|
55
|
+
NSErrorExceptionToJsError(env, error).ThrowAsJavaScriptException();
|
|
56
|
+
} catch (const ObjCException& error) {
|
|
57
|
+
ObjCExceptionToJsError(env, error).ThrowAsJavaScriptException();
|
|
58
|
+
} catch (const std::exception& error) {
|
|
59
|
+
Napi::Error::New(env, error.what()).ThrowAsJavaScriptException();
|
|
60
|
+
}
|
|
61
|
+
return env.Undefined();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
} // namespace coresim
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#include "nserror_bridge.h"
|
|
2
|
+
|
|
3
|
+
namespace coresim {
|
|
4
|
+
|
|
5
|
+
Napi::Error NSErrorExceptionToJsError(Napi::Env env, const NSErrorException& error) {
|
|
6
|
+
Napi::Error jsError = Napi::Error::New(env, error.what());
|
|
7
|
+
jsError.Set("name", "NativeSimOperationError");
|
|
8
|
+
jsError.Set("domain", error.domain);
|
|
9
|
+
jsError.Set("code", static_cast<double>(error.code));
|
|
10
|
+
return jsError;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Napi::Error UnavailableToJsError(Napi::Env env, const NativeSimUnavailableError& error) {
|
|
14
|
+
Napi::Error jsError = Napi::Error::New(env, error.what());
|
|
15
|
+
jsError.Set("name", "NativeSimUnavailableError");
|
|
16
|
+
jsError.Set("kind", error.kind);
|
|
17
|
+
jsError.Set("missing", error.name);
|
|
18
|
+
jsError.Set("frameworkVersion", error.detail);
|
|
19
|
+
return jsError;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Napi::Error ObjCExceptionToJsError(Napi::Env env, const ObjCException& error) {
|
|
23
|
+
Napi::Error jsError = Napi::Error::New(env, error.what());
|
|
24
|
+
jsError.Set("name", "NativeSimDispatchError");
|
|
25
|
+
jsError.Set("exceptionName", error.name);
|
|
26
|
+
jsError.Set("reason", error.reason);
|
|
27
|
+
return jsError;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
} // namespace coresim
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
|
|
5
|
+
#include <stdexcept>
|
|
6
|
+
#include <string>
|
|
7
|
+
|
|
8
|
+
namespace coresim {
|
|
9
|
+
|
|
10
|
+
// Thrown when a class/selector CoreSimulator.framework is expected to have doesn't exist on the
|
|
11
|
+
// currently loaded framework, or when @try/@catch around objc_msgSend caught an NSException.
|
|
12
|
+
// Distinct from ObjCDispatchError (an NSError-carrying operational failure): this means "not
|
|
13
|
+
// supported on this simulator runtime", not "the call ran and failed".
|
|
14
|
+
class NativeSimUnavailableError : public std::runtime_error {
|
|
15
|
+
public:
|
|
16
|
+
NativeSimUnavailableError(std::string kind, std::string name, std::string detail);
|
|
17
|
+
|
|
18
|
+
// "class" | "selector" | "exception"
|
|
19
|
+
std::string kind;
|
|
20
|
+
// The missing class/selector name, or the NSException name.
|
|
21
|
+
std::string name;
|
|
22
|
+
// CFBundleVersion of the loaded CoreSimulator.framework, or the NSException reason.
|
|
23
|
+
std::string detail;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// The loaded CoreSimulator.framework's CFBundleVersion (e.g. "1171.6"), or "unknown" if the
|
|
27
|
+
// framework/its Info.plist can't be found. Cached after first call.
|
|
28
|
+
const std::string& CoreSimulatorFrameworkVersion();
|
|
29
|
+
|
|
30
|
+
// dlopen()s CoreSimulator.framework by absolute path (never statically linked) exactly once.
|
|
31
|
+
// Idempotent; safe to call before every class lookup. Throws NativeSimUnavailableError if the
|
|
32
|
+
// framework can't be loaded at all.
|
|
33
|
+
void EnsureCoreSimulatorLoaded();
|
|
34
|
+
|
|
35
|
+
// NSClassFromString(name), throwing NativeSimUnavailableError if the class doesn't exist.
|
|
36
|
+
Class RequireClass(const std::string& name);
|
|
37
|
+
|
|
38
|
+
// Throws NativeSimUnavailableError unless `target` (instance or Class) responds to `selectorName`.
|
|
39
|
+
void RequireSelector(id target, const std::string& selectorName);
|
|
40
|
+
void RequireClassSelector(Class cls, const std::string& selectorName);
|
|
41
|
+
|
|
42
|
+
// SEL for a selector name; selectors are process-global so this never fails.
|
|
43
|
+
SEL SelectorNamed(const std::string& name);
|
|
44
|
+
|
|
45
|
+
} // namespace coresim
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#include "objc_runtime.h"
|
|
2
|
+
|
|
3
|
+
#import <dlfcn.h>
|
|
4
|
+
#import <objc/message.h>
|
|
5
|
+
#import <objc/runtime.h>
|
|
6
|
+
|
|
7
|
+
namespace coresim {
|
|
8
|
+
|
|
9
|
+
namespace {
|
|
10
|
+
|
|
11
|
+
constexpr auto kCoreSimulatorPath =
|
|
12
|
+
"/Library/Developer/PrivateFrameworks/CoreSimulator.framework/Versions/A/CoreSimulator";
|
|
13
|
+
constexpr auto kCoreSimulatorInfoPlist =
|
|
14
|
+
"/Library/Developer/PrivateFrameworks/CoreSimulator.framework/Versions/A/Resources/Info.plist";
|
|
15
|
+
|
|
16
|
+
} // namespace
|
|
17
|
+
|
|
18
|
+
NativeSimUnavailableError::NativeSimUnavailableError(std::string kind, std::string name, std::string detail)
|
|
19
|
+
: std::runtime_error("CoreSimulator " + kind + " unavailable: " + name + " (" + detail + ")"),
|
|
20
|
+
kind(std::move(kind)),
|
|
21
|
+
name(std::move(name)),
|
|
22
|
+
detail(std::move(detail)) {}
|
|
23
|
+
|
|
24
|
+
const std::string& CoreSimulatorFrameworkVersion() {
|
|
25
|
+
static const std::string version = [] {
|
|
26
|
+
@autoreleasepool {
|
|
27
|
+
NSDictionary* plist = [NSDictionary dictionaryWithContentsOfFile:@(kCoreSimulatorInfoPlist)];
|
|
28
|
+
NSString* bundleVersion = plist[@"CFBundleVersion"];
|
|
29
|
+
return bundleVersion ? std::string(bundleVersion.UTF8String) : std::string("unknown");
|
|
30
|
+
}
|
|
31
|
+
}();
|
|
32
|
+
return version;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void EnsureCoreSimulatorLoaded() {
|
|
36
|
+
static const bool loaded = [] {
|
|
37
|
+
void* handle = dlopen(kCoreSimulatorPath, RTLD_NOW | RTLD_LOCAL);
|
|
38
|
+
return handle != nullptr;
|
|
39
|
+
}();
|
|
40
|
+
if (!loaded) {
|
|
41
|
+
throw NativeSimUnavailableError("framework", "CoreSimulator", dlerror() ?: "dlopen failed");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Class RequireClass(const std::string& name) {
|
|
46
|
+
EnsureCoreSimulatorLoaded();
|
|
47
|
+
Class cls = NSClassFromString(@(name.c_str()));
|
|
48
|
+
if (cls == nil) {
|
|
49
|
+
throw NativeSimUnavailableError("class", name, CoreSimulatorFrameworkVersion());
|
|
50
|
+
}
|
|
51
|
+
return cls;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
SEL SelectorNamed(const std::string& name) { return NSSelectorFromString(@(name.c_str())); }
|
|
55
|
+
|
|
56
|
+
void RequireSelector(id target, const std::string& selectorName) {
|
|
57
|
+
if (target == nil) {
|
|
58
|
+
// A nil deviceType/runtime/etc. is a legitimate CoreSimulator state (e.g. a device whose
|
|
59
|
+
// runtime profile is no longer installed) — distinct from a genuinely missing selector, so
|
|
60
|
+
// callers can tell the two apart instead of getting a misleading "selector unavailable".
|
|
61
|
+
throw NativeSimUnavailableError("nil-target", selectorName, CoreSimulatorFrameworkVersion());
|
|
62
|
+
}
|
|
63
|
+
SEL selector = SelectorNamed(selectorName);
|
|
64
|
+
if (![target respondsToSelector:selector]) {
|
|
65
|
+
throw NativeSimUnavailableError("selector", selectorName, CoreSimulatorFrameworkVersion());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void RequireClassSelector(Class cls, const std::string& selectorName) {
|
|
70
|
+
SEL selector = SelectorNamed(selectorName);
|
|
71
|
+
if (![cls respondsToSelector:selector]) {
|
|
72
|
+
throw NativeSimUnavailableError("selector", selectorName, CoreSimulatorFrameworkVersion());
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
} // namespace coresim
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
|
|
5
|
+
#include <stdexcept>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <utility>
|
|
8
|
+
|
|
9
|
+
namespace coresim {
|
|
10
|
+
|
|
11
|
+
// Thrown when @try/@catch around an objc_msgSend call caught an NSException (e.g.
|
|
12
|
+
// -doesNotRecognizeSelector:, an internal framework assertion, an argument-type mismatch).
|
|
13
|
+
// Distinct from NativeSimUnavailableError: the class/selector existed and responded, but the
|
|
14
|
+
// call itself raised.
|
|
15
|
+
class ObjCException : public std::runtime_error {
|
|
16
|
+
public:
|
|
17
|
+
ObjCException(std::string name, std::string reason)
|
|
18
|
+
: std::runtime_error(name + ": " + reason), name(std::move(name)), reason(std::move(reason)) {}
|
|
19
|
+
|
|
20
|
+
std::string name;
|
|
21
|
+
std::string reason;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Runs `fn` inside @try/@catch, converting any caught NSException into a C++ ObjCException.
|
|
25
|
+
// Every objc_msgSend call in this addon must be wrapped in SafeInvoke — no exceptions
|
|
26
|
+
// (pun intended) — so an uncaught NSException never reaches the caller and kills the process.
|
|
27
|
+
template <typename Fn>
|
|
28
|
+
auto SafeInvoke(Fn&& fn) -> decltype(fn()) {
|
|
29
|
+
@try {
|
|
30
|
+
return fn();
|
|
31
|
+
} @catch (NSException* exception) {
|
|
32
|
+
std::string name = exception.name ? exception.name.UTF8String : "NSException";
|
|
33
|
+
std::string reason = exception.reason ? exception.reason.UTF8String : "";
|
|
34
|
+
throw ObjCException(std::move(name), std::move(reason));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
} // namespace coresim
|