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