@appium/coresim 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +66 -0
  3. package/binding.gyp +53 -0
  4. package/lib/src/commands/app.d.ts +88 -0
  5. package/lib/src/commands/app.d.ts.map +1 -0
  6. package/lib/src/commands/app.js +133 -0
  7. package/lib/src/commands/app.js.map +1 -0
  8. package/lib/src/commands/biometric.d.ts +33 -0
  9. package/lib/src/commands/biometric.d.ts.map +1 -0
  10. package/lib/src/commands/biometric.js +44 -0
  11. package/lib/src/commands/biometric.js.map +1 -0
  12. package/lib/src/commands/darwin-notification.d.ts +36 -0
  13. package/lib/src/commands/darwin-notification.d.ts.map +1 -0
  14. package/lib/src/commands/darwin-notification.js +35 -0
  15. package/lib/src/commands/darwin-notification.js.map +1 -0
  16. package/lib/src/commands/interaction.d.ts +48 -0
  17. package/lib/src/commands/interaction.d.ts.map +1 -0
  18. package/lib/src/commands/interaction.js +48 -0
  19. package/lib/src/commands/interaction.js.map +1 -0
  20. package/lib/src/commands/keychain.d.ts +25 -0
  21. package/lib/src/commands/keychain.d.ts.map +1 -0
  22. package/lib/src/commands/keychain.js +52 -0
  23. package/lib/src/commands/keychain.js.map +1 -0
  24. package/lib/src/commands/lifecycle.d.ts +101 -0
  25. package/lib/src/commands/lifecycle.d.ts.map +1 -0
  26. package/lib/src/commands/lifecycle.js +165 -0
  27. package/lib/src/commands/lifecycle.js.map +1 -0
  28. package/lib/src/commands/media.d.ts +28 -0
  29. package/lib/src/commands/media.d.ts.map +1 -0
  30. package/lib/src/commands/media.js +27 -0
  31. package/lib/src/commands/media.js.map +1 -0
  32. package/lib/src/commands/misc.d.ts +15 -0
  33. package/lib/src/commands/misc.d.ts.map +1 -0
  34. package/lib/src/commands/misc.js +12 -0
  35. package/lib/src/commands/misc.js.map +1 -0
  36. package/lib/src/commands/pasteboard.d.ts +24 -0
  37. package/lib/src/commands/pasteboard.d.ts.map +1 -0
  38. package/lib/src/commands/pasteboard.js +22 -0
  39. package/lib/src/commands/pasteboard.js.map +1 -0
  40. package/lib/src/commands/permissions.d.ts +48 -0
  41. package/lib/src/commands/permissions.d.ts.map +1 -0
  42. package/lib/src/commands/permissions.js +84 -0
  43. package/lib/src/commands/permissions.js.map +1 -0
  44. package/lib/src/commands/process.d.ts +16 -0
  45. package/lib/src/commands/process.d.ts.map +1 -0
  46. package/lib/src/commands/process.js +68 -0
  47. package/lib/src/commands/process.js.map +1 -0
  48. package/lib/src/commands/screenshot.d.ts +29 -0
  49. package/lib/src/commands/screenshot.d.ts.map +1 -0
  50. package/lib/src/commands/screenshot.js +30 -0
  51. package/lib/src/commands/screenshot.js.map +1 -0
  52. package/lib/src/commands/spawn.d.ts +48 -0
  53. package/lib/src/commands/spawn.d.ts.map +1 -0
  54. package/lib/src/commands/spawn.js +108 -0
  55. package/lib/src/commands/spawn.js.map +1 -0
  56. package/lib/src/commands/ui.d.ts +42 -0
  57. package/lib/src/commands/ui.d.ts.map +1 -0
  58. package/lib/src/commands/ui.js +44 -0
  59. package/lib/src/commands/ui.js.map +1 -0
  60. package/lib/src/commands/webinspector.d.ts +19 -0
  61. package/lib/src/commands/webinspector.d.ts.map +1 -0
  62. package/lib/src/commands/webinspector.js +16 -0
  63. package/lib/src/commands/webinspector.js.map +1 -0
  64. package/lib/src/errors.d.ts +51 -0
  65. package/lib/src/errors.d.ts.map +1 -0
  66. package/lib/src/errors.js +82 -0
  67. package/lib/src/errors.js.map +1 -0
  68. package/lib/src/index.d.ts +7 -0
  69. package/lib/src/index.d.ts.map +1 -0
  70. package/lib/src/index.js +5 -0
  71. package/lib/src/index.js.map +1 -0
  72. package/lib/src/native-simctl.d.ts +62 -0
  73. package/lib/src/native-simctl.d.ts.map +1 -0
  74. package/lib/src/native-simctl.js +221 -0
  75. package/lib/src/native-simctl.js.map +1 -0
  76. package/lib/src/types.d.ts +267 -0
  77. package/lib/src/types.d.ts.map +1 -0
  78. package/lib/src/types.js +29 -0
  79. package/lib/src/types.js.map +1 -0
  80. package/lib/src/utils/index.d.ts +3 -0
  81. package/lib/src/utils/index.d.ts.map +1 -0
  82. package/lib/src/utils/index.js +3 -0
  83. package/lib/src/utils/index.js.map +1 -0
  84. package/lib/src/utils/pkg-root.d.ts +6 -0
  85. package/lib/src/utils/pkg-root.d.ts.map +1 -0
  86. package/lib/src/utils/pkg-root.js +12 -0
  87. package/lib/src/utils/pkg-root.js.map +1 -0
  88. package/lib/src/utils/run-catching.d.ts +3 -0
  89. package/lib/src/utils/run-catching.d.ts.map +1 -0
  90. package/lib/src/utils/run-catching.js +11 -0
  91. package/lib/src/utils/run-catching.js.map +1 -0
  92. package/package.json +73 -0
  93. package/scripts/install.mjs +22 -0
  94. package/src/commands/app.ts +186 -0
  95. package/src/commands/biometric.ts +69 -0
  96. package/src/commands/darwin-notification.ts +51 -0
  97. package/src/commands/interaction.ts +74 -0
  98. package/src/commands/keychain.ts +63 -0
  99. package/src/commands/lifecycle.ts +210 -0
  100. package/src/commands/media.ts +38 -0
  101. package/src/commands/misc.ts +20 -0
  102. package/src/commands/pasteboard.ts +31 -0
  103. package/src/commands/permissions.ts +122 -0
  104. package/src/commands/process.ts +80 -0
  105. package/src/commands/screenshot.ts +46 -0
  106. package/src/commands/spawn.ts +133 -0
  107. package/src/commands/ui.ts +61 -0
  108. package/src/commands/webinspector.ts +23 -0
  109. package/src/coresim.mm +1066 -0
  110. package/src/errors.ts +93 -0
  111. package/src/index.ts +23 -0
  112. package/src/native/async_bridge.h +164 -0
  113. package/src/native/nserror_bridge.h +64 -0
  114. package/src/native/nserror_bridge.mm +30 -0
  115. package/src/native/objc_runtime.h +45 -0
  116. package/src/native/objc_runtime.mm +76 -0
  117. package/src/native/safe_dispatch.h +38 -0
  118. package/src/native/sim_device.h +143 -0
  119. package/src/native/sim_device.mm +355 -0
  120. package/src/native/sim_device_set.h +16 -0
  121. package/src/native/sim_device_set.mm +40 -0
  122. package/src/native/sim_pasteboard.h +16 -0
  123. package/src/native/sim_pasteboard.mm +295 -0
  124. package/src/native/sim_process.h +13 -0
  125. package/src/native/sim_process.mm +139 -0
  126. package/src/native/sim_screenshot.h +28 -0
  127. package/src/native/sim_screenshot.mm +226 -0
  128. package/src/native/sim_service_context.h +42 -0
  129. package/src/native/sim_service_context.mm +92 -0
  130. package/src/native/tcc_privacy.h +35 -0
  131. package/src/native/tcc_privacy.mm +256 -0
  132. package/src/native/value_bridge.h +18 -0
  133. package/src/native/value_bridge.mm +111 -0
  134. package/src/native-simctl.ts +288 -0
  135. package/src/types.ts +302 -0
  136. package/src/utils/index.ts +2 -0
  137. package/src/utils/pkg-root.ts +14 -0
  138. package/src/utils/run-catching.ts +10 -0
@@ -0,0 +1,295 @@
1
+ #include "sim_pasteboard.h"
2
+
3
+ #import <AppKit/AppKit.h>
4
+ #import <dlfcn.h>
5
+ #import <objc/message.h>
6
+
7
+ #include "nserror_bridge.h"
8
+ #include "objc_runtime.h"
9
+ #include "safe_dispatch.h"
10
+ #include "sim_device.h"
11
+
12
+ // Bridges SimPasteboardInterface's delegate callbacks to plain dispatch semaphores. Not declared
13
+ // `<SimPasteboardDelegate>` — that protocol has no compile-time header, but matching selector
14
+ // names is all Objective-C dispatch needs. Sits outside coresim:: since ObjC declarations can't
15
+ // live inside a C++ namespace.
16
+ @interface CoresimPasteboardDelegate : NSObject
17
+ @property(atomic) dispatch_semaphore_t activeSema;
18
+ @property(atomic) dispatch_semaphore_t appliedSema;
19
+ @property(atomic, strong) NSError* appliedError;
20
+ @property(atomic) BOOL connectionLost;
21
+ @end
22
+
23
+ @implementation CoresimPasteboardDelegate
24
+
25
+ - (void)simPasteboardDidBecomeActive:(id)interface {
26
+ dispatch_semaphore_signal(self.activeSema);
27
+ }
28
+
29
+ - (void)simPasteboardDidLoseConnection:(id)interface {
30
+ // Flip the flag before signalling — both waiters below check it after waking, so a disconnect
31
+ // must never look like a plain successful wakeup.
32
+ self.connectionLost = YES;
33
+ dispatch_semaphore_signal(self.activeSema);
34
+ dispatch_semaphore_signal(self.appliedSema);
35
+ }
36
+
37
+ - (BOOL)simPasteboardShouldApplySnapshot:(id)interface
38
+ fromHost:(NSUUID*)host
39
+ generation:(long long)generation
40
+ toPasteboard:(NSPasteboard*)pasteboard {
41
+ return YES;
42
+ }
43
+
44
+ - (void)simPasteboardDidApplySnapshot:(id)interface
45
+ fromHost:(NSUUID*)host
46
+ incomingGeneration:(long long)incomingGeneration
47
+ toPasteboard:(NSPasteboard*)pasteboard
48
+ atGeneration:(long long)atGeneration {
49
+ dispatch_semaphore_signal(self.appliedSema);
50
+ }
51
+
52
+ - (void)simPasteboardErrorApplyingSnapshot:(id)interface
53
+ fromHost:(NSUUID*)host
54
+ generation:(long long)generation
55
+ error:(NSError*)error
56
+ toPasteboard:(NSPasteboard*)pasteboard {
57
+ self.appliedError = error;
58
+ dispatch_semaphore_signal(self.appliedSema);
59
+ }
60
+
61
+ - (void)simPasteboardDidChangeAutoNotifyState:(id)interface
62
+ forPasteboard:(NSPasteboard*)pasteboard
63
+ toStatus:(BOOL)status {
64
+ }
65
+
66
+ @end
67
+
68
+ namespace coresim {
69
+
70
+ namespace {
71
+
72
+ NSString* const kPasteboardErrorDomain = @"com.appium.coresim.Pasteboard";
73
+
74
+ // The UTI both pasteboard mechanisms below exchange plain text under.
75
+ NSString* const kPlainTextUTI = @"public.utf8-plain-text";
76
+
77
+ constexpr auto kSimPasteboardPlusPath =
78
+ "/Library/Developer/PrivateFrameworks/CoreSimulator.framework/Versions/A/Frameworks/"
79
+ "SimPasteboardPlus.framework/Versions/A/SimPasteboardPlus";
80
+
81
+ constexpr auto kListenerClassName = "_TtC17SimPasteboardPlus30SimPasteboardInterfaceListener";
82
+ constexpr auto kInterfaceClassName = "_TtC17SimPasteboardPlus22SimPasteboardInterface";
83
+
84
+ // Unlike CoreSimDeviceIO, this isn't pulled in by loading CoreSimulator.framework itself — must be
85
+ // dlopen()'d explicitly before NSClassFromString can find its classes.
86
+ bool EnsureSimPasteboardPlusLoaded() {
87
+ static const bool loaded = dlopen(kSimPasteboardPlusPath, RTLD_NOW | RTLD_LOCAL) != nullptr;
88
+ return loaded;
89
+ }
90
+
91
+ // The real availability gate: -[SimDevice respondsToSelector:@selector(pasteboard)] stays true on
92
+ // CoreSimulator releases that have already dropped the legacy item class behind it, so the modern
93
+ // path's own class presence — not that selector — decides which mechanism to use.
94
+ bool ModernPasteboardAvailable() {
95
+ return EnsureSimPasteboardPlusLoaded() && NSClassFromString(@(kListenerClassName)) != nil;
96
+ }
97
+
98
+ // Swift `@objc` classes with no ObjC rename — resolve only by their mangled runtime name, not the
99
+ // clean one (see CLAUDE.md).
100
+ Class RequireListenerClass() { return RequireClass(kListenerClassName); }
101
+ Class RequireInterfaceClass() { return RequireClass(kInterfaceClassName); }
102
+
103
+ NSError* MakeError(NSInteger code, NSString* message) {
104
+ return [NSError errorWithDomain:kPasteboardErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
105
+ }
106
+
107
+ constexpr int64_t kOperationTimeoutSeconds = 10;
108
+ // -push has no completion callback (see CLAUDE.md) — an arbitrary safety margin, not a value
109
+ // Apple documents or guarantees.
110
+ constexpr double kPushSettleSeconds = 0.5;
111
+
112
+ // Shared setup for pull/push: resolves the sync service's Mach port in `device`'s bootstrap
113
+ // namespace and connects a SimPasteboardInterface to it, waiting (bounded) for it to go active.
114
+ id ConnectPasteboardInterface(id device, NSPasteboard* pasteboard, CoresimPasteboardDelegate* delegate,
115
+ dispatch_queue_t queue) {
116
+ Class listenerClass = RequireListenerClass();
117
+ Class interfaceClass = RequireInterfaceClass();
118
+
119
+ NSString* serviceName = SafeInvoke([&] {
120
+ using Fn = id (*)(id, SEL);
121
+ return ((Fn)objc_msgSend)((id)listenerClass, NSSelectorFromString(@"machServiceName"));
122
+ });
123
+
124
+ NSError* lookupError = nil;
125
+ unsigned int port = LookupMachPort(device, serviceName, &lookupError);
126
+ // The port is the authoritative success signal, not the error (see CLAUDE.md).
127
+ if (port == 0) {
128
+ throw NSErrorException(
129
+ lookupError
130
+ ?: MakeError(1, [NSString stringWithFormat:@"No mach port named '%@' is registered in the "
131
+ @"device's bootstrap namespace — is it booted?",
132
+ serviceName]));
133
+ }
134
+
135
+ id interface = SafeInvoke([&] {
136
+ id allocedInterface = [interfaceClass alloc]; // ARC-safe: alloc is a recognized family method
137
+ // initWithConnectingToPort:... is resolved dynamically, so the compiler can't apply its usual
138
+ // init-family ARC bookkeeping — ns_consumed/ns_returns_retained spell it out by hand instead
139
+ // (getting this wrong either leaks the interface or over-releases and crashes).
140
+ using InitFn =
141
+ id (*__attribute__((ns_returns_retained)))(id __attribute__((ns_consumed)), SEL, unsigned int, id, id, id);
142
+ return ((InitFn)objc_msgSend)(
143
+ allocedInterface, NSSelectorFromString(@"initWithConnectingToPort:managingPasteboard:delegate:delegateQueue:"),
144
+ port, pasteboard, delegate, queue);
145
+ });
146
+ if (interface == nil) {
147
+ throw NSErrorException(MakeError(2, @"Failed to construct a connection to the device's pasteboard"));
148
+ }
149
+
150
+ // Bounded, not DISPATCH_TIME_FOREVER — a device shutting down mid-operation could hang forever
151
+ // otherwise (simPasteboardDidLoseConnection: also signals this semaphore for a prompt failure).
152
+ long timedOut = dispatch_semaphore_wait(delegate.activeSema,
153
+ dispatch_time(DISPATCH_TIME_NOW, kOperationTimeoutSeconds * NSEC_PER_SEC));
154
+ if (timedOut != 0) {
155
+ throw NSErrorException(MakeError(6, @"Timed out waiting for the device's pasteboard connection to activate"));
156
+ }
157
+ if (delegate.connectionLost) {
158
+ throw NSErrorException(MakeError(7, @"Lost the connection to the device's pasteboard before it became active"));
159
+ }
160
+ return interface;
161
+ }
162
+
163
+ NSString* PullPasteboardStringModern(id device, NSError** error) {
164
+ NSPasteboard* pasteboard = [NSPasteboard pasteboardWithUniqueName];
165
+ @try {
166
+ CoresimPasteboardDelegate* delegate = [CoresimPasteboardDelegate new];
167
+ delegate.activeSema = dispatch_semaphore_create(0);
168
+ delegate.appliedSema = dispatch_semaphore_create(0);
169
+ dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.pasteboard.pull", DISPATCH_QUEUE_SERIAL);
170
+
171
+ id interface = ConnectPasteboardInterface(device, pasteboard, delegate, queue);
172
+ SafeInvoke([&] {
173
+ ((void (*)(id, SEL))objc_msgSend)(interface, NSSelectorFromString(@"pull"));
174
+ return true;
175
+ });
176
+
177
+ long timedOut = dispatch_semaphore_wait(delegate.appliedSema,
178
+ dispatch_time(DISPATCH_TIME_NOW, kOperationTimeoutSeconds * NSEC_PER_SEC));
179
+ if (timedOut != 0) {
180
+ throw NSErrorException(MakeError(3, @"Timed out waiting for the device's pasteboard contents"));
181
+ }
182
+ if (delegate.connectionLost) {
183
+ throw NSErrorException(MakeError(8, @"Lost the connection to the device's pasteboard before it applied"));
184
+ }
185
+ if (delegate.appliedError != nil) {
186
+ throw NSErrorException(delegate.appliedError);
187
+ }
188
+ return [pasteboard stringForType:NSPasteboardTypeString] ?: @"";
189
+ } @finally {
190
+ [pasteboard releaseGlobally];
191
+ }
192
+ }
193
+
194
+ BOOL PushPasteboardStringModern(id device, NSString* content, NSError** error) {
195
+ NSPasteboard* pasteboard = [NSPasteboard pasteboardWithUniqueName];
196
+ @try {
197
+ [pasteboard clearContents];
198
+ [pasteboard setString:content forType:NSPasteboardTypeString];
199
+
200
+ CoresimPasteboardDelegate* delegate = [CoresimPasteboardDelegate new];
201
+ delegate.activeSema = dispatch_semaphore_create(0);
202
+ delegate.appliedSema = dispatch_semaphore_create(0);
203
+ dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.pasteboard.push", DISPATCH_QUEUE_SERIAL);
204
+
205
+ id interface = ConnectPasteboardInterface(device, pasteboard, delegate, queue);
206
+ SafeInvoke([&] {
207
+ ((void (*)(id, SEL))objc_msgSend)(interface, NSSelectorFromString(@"push"));
208
+ return true;
209
+ });
210
+ [NSThread sleepForTimeInterval:kPushSettleSeconds];
211
+ // No completion callback to wait on (see kPushSettleSeconds above), but a disconnect during
212
+ // the settle sleep is still a real, already-known failure — report it instead of a false YES.
213
+ if (delegate.connectionLost) {
214
+ throw NSErrorException(MakeError(9, @"Lost the connection to the device's pasteboard before push settled"));
215
+ }
216
+ return YES;
217
+ } @finally {
218
+ [pasteboard releaseGlobally];
219
+ }
220
+ }
221
+
222
+ // -[SimDevice pasteboard] — the older, synchronous, in-process pasteboard API (see CLAUDE.md).
223
+ id LegacyPasteboardAccessor(id device) {
224
+ return SafeInvoke([&] {
225
+ using Fn = id (*)(id, SEL);
226
+ return ((Fn)objc_msgSend)(device, NSSelectorFromString(@"pasteboard"));
227
+ });
228
+ }
229
+
230
+ NSString* PullPasteboardStringLegacy(id device, NSError** error) {
231
+ id pasteboard = LegacyPasteboardAccessor(device);
232
+ NSError* itemsError = nil;
233
+ NSArray* items = SafeInvoke([&] {
234
+ using Fn = NSArray* (*)(id, SEL, NSArray*, NSError**);
235
+ return ((Fn)objc_msgSend)(pasteboard, NSSelectorFromString(@"itemsFromPasteboardWithTypes:error:"),
236
+ @[ kPlainTextUTI ], &itemsError);
237
+ });
238
+ if (items == nil) {
239
+ throw NSErrorException(itemsError ?: MakeError(4, @"Failed to read the device's pasteboard"));
240
+ }
241
+ if (items.count == 0) {
242
+ return @"";
243
+ }
244
+ id value = SafeInvoke([&] {
245
+ using Fn = id (*)(id, SEL, NSString*);
246
+ return ((Fn)objc_msgSend)(items.firstObject, NSSelectorFromString(@"valueForType:"), kPlainTextUTI);
247
+ });
248
+ // Text flavours may come back as NSString or NSData of UTF-8 bytes.
249
+ if ([value isKindOfClass:[NSData class]]) {
250
+ return [[NSString alloc] initWithData:value encoding:NSUTF8StringEncoding] ?: @"";
251
+ }
252
+ return [value isKindOfClass:[NSString class]] ? value : @"";
253
+ }
254
+
255
+ BOOL PushPasteboardStringLegacy(id device, NSString* content, NSError** error) {
256
+ id pasteboard = LegacyPasteboardAccessor(device);
257
+ Class itemClass = RequireClass("SimPasteboardItem");
258
+ // Plain alloc/init, both literal selectors the compiler resolves and ARC-balances itself — no
259
+ // raw dispatch (and its ownership pitfalls, see ConnectPasteboardInterface) needed here.
260
+ id item = SafeInvoke([&] { return [[itemClass alloc] init]; });
261
+ BOOL valueSet = SafeInvoke([&] {
262
+ using Fn = BOOL (*)(id, SEL, id, NSString*);
263
+ return ((Fn)objc_msgSend)(item, NSSelectorFromString(@"setValue:forType:"), content, kPlainTextUTI);
264
+ });
265
+ if (!valueSet) {
266
+ throw NSErrorException(MakeError(5, @"Failed to prepare a pasteboard item for the device's pasteboard"));
267
+ }
268
+ NSError* setError = nil;
269
+ SafeInvoke([&] {
270
+ using Fn = unsigned long long (*)(id, SEL, NSArray*, NSError**);
271
+ return ((Fn)objc_msgSend)(pasteboard, NSSelectorFromString(@"setPasteboardWithItems:error:"), @[ item ], &setError);
272
+ });
273
+ if (setError != nil) {
274
+ throw NSErrorException(setError);
275
+ }
276
+ return YES;
277
+ }
278
+
279
+ } // namespace
280
+
281
+ // Needs no special entitlement, unlike CoreDevice.framework's Mach services — confirmed
282
+ // empirically with an unsigned, unentitled probe (see git history). Prefers the modern
283
+ // SimPasteboardPlus mechanism when available, falling back to the legacy SimDevicePasteboard API
284
+ // (see ModernPasteboardAvailable above for why that choice isn't simply an Xcode-version check).
285
+ NSString* PullPasteboardString(id device, NSError** error) {
286
+ return ModernPasteboardAvailable() ? PullPasteboardStringModern(device, error)
287
+ : PullPasteboardStringLegacy(device, error);
288
+ }
289
+
290
+ BOOL PushPasteboardString(id device, NSString* content, NSError** error) {
291
+ return ModernPasteboardAvailable() ? PushPasteboardStringModern(device, content, error)
292
+ : PushPasteboardStringLegacy(device, content, error);
293
+ }
294
+
295
+ } // namespace coresim
@@ -0,0 +1,13 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ namespace coresim {
6
+
7
+ // Locates the Unix-domain socket the device's `com.apple.webinspectord_sim` launchd job listens
8
+ // on, for WebKit remote-debugging tools to connect to. No ObjC dispatch here (see CLAUDE.md) —
9
+ // CoreSimulator itself has no API for this; it's libproc/sysctl process introspection instead,
10
+ // the same mechanism `lsof -aUc launchd_sim` uses.
11
+ NSString* FindWebInspectorSocket(NSString* udid, NSError** error);
12
+
13
+ } // namespace coresim
@@ -0,0 +1,139 @@
1
+ #include "sim_process.h"
2
+
3
+ #include <libproc.h>
4
+ #include <sys/socket.h>
5
+ #include <sys/sysctl.h>
6
+ #include <sys/un.h>
7
+
8
+ #include <cstring>
9
+ #include <vector>
10
+
11
+ namespace coresim {
12
+
13
+ namespace {
14
+
15
+ NSString* const kProcessErrorDomain = @"com.appium.coresim.Process";
16
+ NSString* const kWebInspectorSocketSuffix = @"com.apple.webinspectord_sim.socket";
17
+
18
+ NSError* MakeError(NSInteger code, NSString* message) {
19
+ return [NSError errorWithDomain:kProcessErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
20
+ }
21
+
22
+ NSString* ExecutablePath(pid_t pid) {
23
+ char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
24
+ int size = proc_pidpath(pid, pathBuf, sizeof(pathBuf));
25
+ return size > 0 ? [NSString stringWithUTF8String:pathBuf] : nil;
26
+ }
27
+
28
+ // argv[1] via the unprivileged KERN_PROCARGS2 sysctl (same as `ps`/`lsof`) — for `launchd_sim`
29
+ // that's its bootstrap plist path, which embeds the device's UDID.
30
+ NSString* FirstArgument(pid_t pid) {
31
+ int mib[3] = {CTL_KERN, KERN_PROCARGS2, pid};
32
+ size_t size = 0;
33
+ if (sysctl(mib, 3, nullptr, &size, nullptr, 0) != 0 || size < sizeof(int)) {
34
+ return nil;
35
+ }
36
+ std::vector<char> buffer(size);
37
+ if (sysctl(mib, 3, buffer.data(), &size, nullptr, 0) != 0) {
38
+ return nil;
39
+ }
40
+ // Layout: argc, exec path, NUL padding, then argv[0], argv[1], ..., each NUL-terminated.
41
+ int argc = 0;
42
+ std::memcpy(&argc, buffer.data(), sizeof(argc));
43
+ const char* cursor = buffer.data() + sizeof(argc);
44
+ const char* end = buffer.data() + size;
45
+ cursor += strnlen(cursor, static_cast<size_t>(end - cursor)); // skip the exec path
46
+ while (cursor < end && *cursor == '\0') cursor++; // skip the NUL padding
47
+ cursor += strnlen(cursor, static_cast<size_t>(end - cursor)); // skip argv[0]
48
+ if (cursor < end) cursor++; // skip its NUL terminator
49
+ if (argc < 2 || cursor >= end) {
50
+ return nil;
51
+ }
52
+ return [NSString stringWithUTF8String:cursor];
53
+ }
54
+
55
+ // Each booted simulator has its own `launchd_sim`; finds the one owning `udid`.
56
+ pid_t FindLaunchdSimPid(NSString* udid) {
57
+ int neededBytes = proc_listpids(PROC_ALL_PIDS, 0, nullptr, 0);
58
+ if (neededBytes <= 0) {
59
+ return -1;
60
+ }
61
+ // Headroom for processes started between the sizing call above and the listing call below.
62
+ std::vector<pid_t> pids(neededBytes / sizeof(pid_t) + 64);
63
+ int bytes = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), static_cast<int>(pids.size() * sizeof(pid_t)));
64
+ if (bytes <= 0) {
65
+ return -1;
66
+ }
67
+ int count = bytes / static_cast<int>(sizeof(pid_t));
68
+ for (int i = 0; i < count; i++) {
69
+ pid_t pid = pids[i];
70
+ if (pid <= 0) {
71
+ continue;
72
+ }
73
+ NSString* path = ExecutablePath(pid);
74
+ if (![path hasSuffix:@"/launchd_sim"]) {
75
+ continue;
76
+ }
77
+ NSString* arg = FirstArgument(pid);
78
+ if (arg != nil && [arg rangeOfString:udid options:NSCaseInsensitiveSearch].location != NSNotFound) {
79
+ return pid;
80
+ }
81
+ }
82
+ return -1;
83
+ }
84
+
85
+ // Scans `pid`'s open Unix-domain socket fds for one bound to a path ending in `suffix`.
86
+ NSString* FindUnixSocketPath(pid_t pid, NSString* suffix) {
87
+ int neededBytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nullptr, 0);
88
+ if (neededBytes <= 0) {
89
+ return nil;
90
+ }
91
+ std::vector<uint8_t> buffer(static_cast<size_t>(neededBytes) + 64 * sizeof(struct proc_fdinfo));
92
+ int bytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, buffer.data(), static_cast<int>(buffer.size()));
93
+ if (bytes <= 0) {
94
+ return nil;
95
+ }
96
+ auto* fdInfoList = reinterpret_cast<struct proc_fdinfo*>(buffer.data());
97
+ int fdCount = bytes / static_cast<int>(sizeof(struct proc_fdinfo));
98
+ for (int i = 0; i < fdCount; i++) {
99
+ if (fdInfoList[i].proc_fdtype != PROX_FDTYPE_SOCKET) {
100
+ continue;
101
+ }
102
+ struct socket_fdinfo socketInfo;
103
+ int socketInfoBytes =
104
+ proc_pidfdinfo(pid, fdInfoList[i].proc_fd, PROC_PIDFDSOCKETINFO, &socketInfo, sizeof(socketInfo));
105
+ if (socketInfoBytes != sizeof(socketInfo) || socketInfo.psi.soi_kind != SOCKINFO_UN) {
106
+ continue;
107
+ }
108
+ const char* sunPath = socketInfo.psi.soi_proto.pri_un.unsi_addr.ua_sun.sun_path;
109
+ if (sunPath[0] == '\0') {
110
+ continue;
111
+ }
112
+ NSString* path = [NSString stringWithUTF8String:sunPath];
113
+ if ([path hasSuffix:suffix]) {
114
+ return path;
115
+ }
116
+ }
117
+ return nil;
118
+ }
119
+
120
+ } // namespace
121
+
122
+ NSString* FindWebInspectorSocket(NSString* udid, NSError** error) {
123
+ pid_t launchdSimPid = FindLaunchdSimPid(udid);
124
+ if (launchdSimPid <= 0) {
125
+ if (error) {
126
+ *error = MakeError(1, [NSString stringWithFormat:@"No launchd_sim process was found for device '%@' — "
127
+ @"is it booted?",
128
+ udid]);
129
+ }
130
+ return nil;
131
+ }
132
+ NSString* socketPath = FindUnixSocketPath(launchdSimPid, kWebInspectorSocketSuffix);
133
+ if (socketPath == nil && error) {
134
+ *error = MakeError(2, [NSString stringWithFormat:@"No WebInspector socket was found for device '%@'", udid]);
135
+ }
136
+ return socketPath;
137
+ }
138
+
139
+ } // namespace coresim
@@ -0,0 +1,28 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ namespace coresim {
6
+
7
+ // Enumerates the device's renderable display IO ports — the same information `simctl io <udid>
8
+ // enumerate` reports for its own `--display` selection. Each element has "id" (NSString, a port
9
+ // UUID — pass to CaptureScreenshot's displayId to target it), "displayClass" (NSNumber,
10
+ // unsigned short — 0 is always the primary display), and "isMain" (NSNumber/BOOL). Returns nil
11
+ // (and sets *error) if the device has no IO client at all (e.g. not booted); an empty (non-nil)
12
+ // array means the device has an IO client but no renderable display.
13
+ NSArray<NSDictionary*>* ListDisplays(id device, NSError** error);
14
+
15
+ enum class ScreenshotFormat { kPNG, kJPEG };
16
+
17
+ // Captures a display as an image, reading the same in-process framebuffer surface `simctl io
18
+ // <udid> screenshot` itself reads (see sim_screenshot.mm) — no temp file, no subprocess.
19
+ // `displayId`, if non-nil, selects a specific port by its UUID (see ListDisplays); nil selects the
20
+ // primary display (displayClass 0), falling back to the first renderable display found (e.g. for
21
+ // tvOS, which has no class-0 display). `jpegQualityPercent` (0-100, nil for ImageIO's own default)
22
+ // only applies when `format` is kJPEG — ignored for kPNG, which is always lossless. Returns nil
23
+ // (and sets *error) if `displayId` doesn't match any port, the resolved display has no renderable
24
+ // surface yet (e.g. not booted), or rendering/encoding failed.
25
+ NSData* CaptureScreenshot(id device, NSString* displayId, ScreenshotFormat format, NSNumber* jpegQualityPercent,
26
+ NSError** error);
27
+
28
+ } // namespace coresim