@appium/coresim 1.0.0

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