@appium/coresim 1.8.0 → 1.9.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/CHANGELOG.md +6 -0
- package/binding.gyp +1 -0
- package/lib/src/commands/ui.d.ts +21 -0
- package/lib/src/commands/ui.d.ts.map +1 -1
- package/lib/src/commands/ui.js +22 -0
- package/lib/src/commands/ui.js.map +1 -1
- package/lib/src/index.d.ts +1 -1
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js +1 -1
- package/lib/src/index.js.map +1 -1
- package/lib/src/native-simctl.d.ts.map +1 -1
- package/lib/src/native-simctl.js +3 -1
- package/lib/src/native-simctl.js.map +1 -1
- package/lib/src/types.d.ts +9 -0
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +8 -0
- package/lib/src/types.js.map +1 -1
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
- package/src/commands/ui.ts +27 -0
- package/src/coresim.mm +29 -0
- package/src/index.ts +1 -0
- package/src/native/av_recording.h +1 -1
- package/src/native/av_recording.mm +75 -26
- package/src/native/sim_device.h +11 -0
- package/src/native/sim_device.mm +53 -0
- package/src/native/sim_orientation.h +16 -0
- package/src/native/sim_orientation.mm +58 -0
- package/src/native/sim_screenshot.h +5 -0
- package/src/native/sim_screenshot.mm +21 -0
- package/src/native/sim_video_recording.mm +2 -43
- package/src/native/video_encoder.h +7 -0
- package/src/native/video_encoder.mm +239 -19
- package/src/native-simctl.ts +4 -0
- package/src/types.ts +10 -0
package/src/native/sim_device.h
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
#import <Foundation/Foundation.h>
|
|
4
4
|
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
|
|
5
7
|
namespace coresim {
|
|
6
8
|
|
|
7
9
|
// -[SimDevice UDID] / -[SimDevice name] / -[SimDevice state] / -[SimDevice deviceType] /
|
|
@@ -19,6 +21,11 @@ id DeviceRuntime(id device);
|
|
|
19
21
|
// privacy-permission access (see tcc_privacy.h).
|
|
20
22
|
NSString* DeviceDataPath(id device);
|
|
21
23
|
|
|
24
|
+
// -[SimDevice lastBootedAt] -> the current boot session's start time (nil if never booted). Used to
|
|
25
|
+
// tell a guest preference file that predates this boot from one already rewritten this boot (see
|
|
26
|
+
// sim_orientation.mm).
|
|
27
|
+
NSDate* DeviceLastBootedAt(id device);
|
|
28
|
+
|
|
22
29
|
// -[SimDevice bootWithOptions:error:]
|
|
23
30
|
BOOL Boot(id device, NSDictionary* options, NSError** error);
|
|
24
31
|
|
|
@@ -64,6 +71,10 @@ NSString* Getenv(id device, NSString* name, NSError** error);
|
|
|
64
71
|
// the same way Apple's own Simulator.app does.
|
|
65
72
|
unsigned int LookupMachPort(id device, NSString* serviceName, NSError** error);
|
|
66
73
|
|
|
74
|
+
// Rotates the device via a raw GSEvent mach message to SpringBoard's "PurpleWorkspacePort" (see
|
|
75
|
+
// CLAUDE.md). `orientation`: 1=portrait, 2=portraitUpsideDown, 3=landscapeRight, 4=landscapeLeft.
|
|
76
|
+
BOOL SetDeviceOrientation(id device, int32_t orientation, NSError** error);
|
|
77
|
+
|
|
67
78
|
// -[SimDevice installApplication:withOptions:error:]
|
|
68
79
|
BOOL InstallApp(id device, NSURL* appURL, NSDictionary* options, NSError** error);
|
|
69
80
|
|
package/src/native/sim_device.mm
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#include "sim_device.h"
|
|
2
2
|
|
|
3
|
+
#import <mach/mach.h>
|
|
3
4
|
#import <objc/message.h>
|
|
4
5
|
|
|
6
|
+
#include <cstring>
|
|
7
|
+
|
|
5
8
|
#include "objc_runtime.h"
|
|
6
9
|
#include "safe_dispatch.h"
|
|
7
10
|
|
|
@@ -9,6 +12,12 @@ namespace coresim {
|
|
|
9
12
|
|
|
10
13
|
namespace {
|
|
11
14
|
|
|
15
|
+
NSString* const kOrientationErrorDomain = @"io.appium.coresim.Orientation";
|
|
16
|
+
|
|
17
|
+
NSError* MakeOrientationError(NSInteger code, NSString* message) {
|
|
18
|
+
return [NSError errorWithDomain:kOrientationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
// Shared shapes reused across the many identical-signature getters/setters below.
|
|
13
22
|
id IdGetter(id target, const std::string& selectorName) {
|
|
14
23
|
RequireSelector(target, selectorName);
|
|
@@ -53,6 +62,7 @@ NSString* DeviceName(id device) { return IdGetter(device, "name"); }
|
|
|
53
62
|
id DeviceDeviceType(id device) { return IdGetter(device, "deviceType"); }
|
|
54
63
|
id DeviceRuntime(id device) { return IdGetter(device, "runtime"); }
|
|
55
64
|
NSString* DeviceDataPath(id device) { return IdGetter(device, "dataPath"); }
|
|
65
|
+
NSDate* DeviceLastBootedAt(id device) { return IdGetter(device, "lastBootedAt"); }
|
|
56
66
|
|
|
57
67
|
unsigned long long DeviceState(id device) {
|
|
58
68
|
static const std::string kSelectorName = "state";
|
|
@@ -115,6 +125,49 @@ unsigned int LookupMachPort(id device, NSString* serviceName, NSError** error) {
|
|
|
115
125
|
});
|
|
116
126
|
}
|
|
117
127
|
|
|
128
|
+
BOOL SetDeviceOrientation(id device, int32_t orientation, NSError** error) {
|
|
129
|
+
NSError* lookupError = nil;
|
|
130
|
+
unsigned int purplePort = LookupMachPort(device, @"PurpleWorkspacePort", &lookupError);
|
|
131
|
+
// The port is the authoritative success signal, not the error (see LookupMachPort's own callers,
|
|
132
|
+
// e.g. sim_pasteboard.mm).
|
|
133
|
+
if (purplePort == 0) {
|
|
134
|
+
*error = lookupError ?: MakeOrientationError(1, @"PurpleWorkspacePort is not available — is the device booted?");
|
|
135
|
+
return NO;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// GSEvent wire format, undocumented (see CLAUDE.md): mach_msg_header_t + a fixed-layout event
|
|
139
|
+
// record, in a 112-byte buffer (8-byte aligned, >= the 108-byte message mach_msg sends).
|
|
140
|
+
constexpr uint32_t kGSEventTypeDeviceOrientationChanged = 50;
|
|
141
|
+
constexpr uint32_t kGSEventHostFlag = 0x20000;
|
|
142
|
+
constexpr mach_msg_id_t kGSEventMachMessageID = 0x7B;
|
|
143
|
+
constexpr mach_msg_timeout_t kSendTimeoutMs = 2000;
|
|
144
|
+
|
|
145
|
+
uint8_t buffer[112] = {0};
|
|
146
|
+
auto* header = reinterpret_cast<mach_msg_header_t*>(buffer);
|
|
147
|
+
header->msgh_bits = 0x13; // MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0)
|
|
148
|
+
header->msgh_size = 108; // align4(4 + 0x6B) — matches Simulator.app's own sendPurpleEvent:
|
|
149
|
+
header->msgh_remote_port = purplePort;
|
|
150
|
+
header->msgh_local_port = MACH_PORT_NULL;
|
|
151
|
+
header->msgh_id = kGSEventMachMessageID;
|
|
152
|
+
|
|
153
|
+
uint32_t type = kGSEventTypeDeviceOrientationChanged | kGSEventHostFlag;
|
|
154
|
+
std::memcpy(buffer + 0x18, &type, sizeof(type));
|
|
155
|
+
uint32_t recordInfoSize = 4;
|
|
156
|
+
std::memcpy(buffer + 0x48, &recordInfoSize, sizeof(recordInfoSize));
|
|
157
|
+
uint32_t orientationValue = static_cast<uint32_t>(orientation);
|
|
158
|
+
std::memcpy(buffer + 0x4C, &orientationValue, sizeof(orientationValue));
|
|
159
|
+
|
|
160
|
+
kern_return_t kr = mach_msg(header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, header->msgh_size, 0, MACH_PORT_NULL,
|
|
161
|
+
kSendTimeoutMs, MACH_PORT_NULL);
|
|
162
|
+
if (kr != KERN_SUCCESS) {
|
|
163
|
+
*error = MakeOrientationError(
|
|
164
|
+
2, [NSString stringWithFormat:@"Failed to send the orientation change (kern_return_t %d): %s", kr,
|
|
165
|
+
mach_error_string(kr)]);
|
|
166
|
+
return NO;
|
|
167
|
+
}
|
|
168
|
+
return YES;
|
|
169
|
+
}
|
|
170
|
+
|
|
118
171
|
BOOL InstallApp(id device, NSURL* appURL, NSDictionary* options, NSError** error) {
|
|
119
172
|
static const std::string kSelectorName = "installApplication:withOptions:error:";
|
|
120
173
|
RequireSelector(device, kSelectorName);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
|
|
7
|
+
namespace coresim {
|
|
8
|
+
|
|
9
|
+
// A live orientation read straight off backboardd's own persisted preference file on the host side
|
|
10
|
+
// (see CLAUDE.md) — reflects a rotation from any source, not just this process's own
|
|
11
|
+
// SetDeviceOrientation calls. Calls `handler` once, async, with a DeviceOrientation wire value
|
|
12
|
+
// (types.ts) — always 1 (portrait) if the device hasn't rotated yet this boot, or on any read
|
|
13
|
+
// failure.
|
|
14
|
+
void ReadGuestOrientation(id device, void (^handler)(int32_t orientation));
|
|
15
|
+
|
|
16
|
+
} // namespace coresim
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#include "sim_orientation.h"
|
|
2
|
+
|
|
3
|
+
#include "sim_device.h"
|
|
4
|
+
|
|
5
|
+
namespace coresim {
|
|
6
|
+
|
|
7
|
+
namespace {
|
|
8
|
+
|
|
9
|
+
// backboardd's persisted GraphicsOrientation swaps landscape left/right vs. our own
|
|
10
|
+
// DeviceOrientation wire values — confirmed empirically (see CLAUDE.md).
|
|
11
|
+
int32_t TranslateGraphicsOrientation(NSInteger graphicsOrientation) {
|
|
12
|
+
switch (graphicsOrientation) {
|
|
13
|
+
case 3:
|
|
14
|
+
return 4;
|
|
15
|
+
case 4:
|
|
16
|
+
return 3;
|
|
17
|
+
default:
|
|
18
|
+
return static_cast<int32_t>(graphicsOrientation);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The live digitizer entry for the current boot, or nil if it hasn't rotated yet this boot (or
|
|
23
|
+
// `plist` is nil/malformed). Reading straight from the file (a real binary plist) gives properly
|
|
24
|
+
// typed values — no ASCII-vs-binary parsing ambiguity to handle.
|
|
25
|
+
NSNumber* CurrentGraphicsOrientation(NSDictionary* plist) {
|
|
26
|
+
NSArray* entries = [plist[@"BKDigitizerPersistentServiceProperties"] isKindOfClass:[NSArray class]]
|
|
27
|
+
? plist[@"BKDigitizerPersistentServiceProperties"]
|
|
28
|
+
: nil;
|
|
29
|
+
NSDictionary* last = [entries.lastObject isKindOfClass:[NSDictionary class]] ? entries.lastObject : nil;
|
|
30
|
+
NSDictionary* props = [last[@"props"] isKindOfClass:[NSDictionary class]] ? last[@"props"] : nil;
|
|
31
|
+
return [props[@"GraphicsOrientation"] isKindOfClass:[NSNumber class]] ? props[@"GraphicsOrientation"] : nil;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
} // namespace
|
|
35
|
+
|
|
36
|
+
void ReadGuestOrientation(id device, void (^handler)(int32_t orientation)) {
|
|
37
|
+
NSString* plistPath =
|
|
38
|
+
[DeviceDataPath(device) stringByAppendingPathComponent:@"Library/Preferences/com.apple.backboardd.plist"];
|
|
39
|
+
NSDate* lastBootedAt = DeviceLastBootedAt(device);
|
|
40
|
+
|
|
41
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
42
|
+
NSDate* modified = [[NSFileManager defaultManager] attributesOfItemAtPath:plistPath
|
|
43
|
+
error:nil][NSFileModificationDate];
|
|
44
|
+
// The whole file predates this boot, so none of its entries — including the last one — can
|
|
45
|
+
// belong to it (confirmed empirically: a reboot leaves the previous boot's entry in place until
|
|
46
|
+
// backboardd rewrites the file, sometimes several seconds after boot completes — see CLAUDE.md).
|
|
47
|
+
// Treat exactly like "never rotated this boot" rather than trusting stale content.
|
|
48
|
+
if (lastBootedAt != nil && modified != nil && [modified compare:lastBootedAt] == NSOrderedAscending) {
|
|
49
|
+
handler(1);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
NSDictionary* plist = [NSDictionary dictionaryWithContentsOfFile:plistPath];
|
|
53
|
+
NSNumber* graphicsOrientation = CurrentGraphicsOrientation(plist);
|
|
54
|
+
handler(graphicsOrientation != nil ? TranslateGraphicsOrientation(graphicsOrientation.integerValue) : 1);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
} // namespace coresim
|
|
@@ -18,6 +18,11 @@ NSArray<NSDictionary*>* ListDisplays(id device, NSError** error);
|
|
|
18
18
|
// object itself (e.g. StartVideoRecording's `screen` argument).
|
|
19
19
|
id ResolveCaptureDisplay(id device, NSString* displayId, NSError** error);
|
|
20
20
|
|
|
21
|
+
// The device-wide "capture service" port (real protocol: SimScreenCaptureService — see CLAUDE.md),
|
|
22
|
+
// distinct from a display descriptor (see ResolveCaptureDisplay above). Found by scanning ioPorts
|
|
23
|
+
// since no header exists. Throws NativeSimUnavailableError (not NSError**) if absent.
|
|
24
|
+
id ResolveScreenCaptureService(id device, NSError** error);
|
|
25
|
+
|
|
21
26
|
// The descriptor's current framebuffer as an `IOSurfaceRef` (bridge-cast the returned `id`).
|
|
22
27
|
// Returns nil if not available yet (e.g. connection just dropped) — not an error, since that can
|
|
23
28
|
// legitimately happen from one call to the next on a live device.
|
|
@@ -152,6 +152,27 @@ id ResolveCaptureDisplay(id device, NSString* displayId, NSError** error) {
|
|
|
152
152
|
return ResolveDisplayDescriptor(candidates, displayId, error);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// The real protocol is SimScreenCaptureService (see CLAUDE.md) — found by scanning ioPorts for
|
|
156
|
+
// whichever descriptor responds to startRecordingFromScreen:..., since no header exists for it.
|
|
157
|
+
id ResolveScreenCaptureService(id device, NSError** error) {
|
|
158
|
+
static const std::string kStartRecordingSelector =
|
|
159
|
+
"startRecordingFromScreen:maskPolicy:assetWriterOutputSettings:outputFile:completionQueue:completionHandler:";
|
|
160
|
+
id ioClient = IdGetter(device, "io");
|
|
161
|
+
if (ioClient == nil) {
|
|
162
|
+
*error = MakeError(11, @"Device has no IO client available — is it booted?");
|
|
163
|
+
return nil;
|
|
164
|
+
}
|
|
165
|
+
NSArray* ports = IdGetter(ioClient, "ioPorts");
|
|
166
|
+
SEL selector = NSSelectorFromString(@(kStartRecordingSelector.c_str()));
|
|
167
|
+
for (id port in ports) {
|
|
168
|
+
id descriptor = IdGetter(port, "descriptor");
|
|
169
|
+
if (descriptor != nil && [descriptor respondsToSelector:selector]) {
|
|
170
|
+
return descriptor;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
throw NativeSimUnavailableError("selector", kStartRecordingSelector, CoreSimulatorFrameworkVersion());
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
id CurrentDisplaySurface(id descriptor) { return RenderableSurface(descriptor); }
|
|
156
177
|
|
|
157
178
|
NSArray<NSDictionary*>* ListDisplays(id device, NSError** error) {
|
|
@@ -8,50 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
namespace coresim {
|
|
10
10
|
|
|
11
|
-
namespace {
|
|
12
|
-
|
|
13
|
-
NSString* const kVideoRecordingErrorDomain = @"io.appium.coresim.VideoRecording";
|
|
14
|
-
|
|
15
|
-
NSError* MakeError(NSInteger code, NSString* message) {
|
|
16
|
-
return [NSError errorWithDomain:kVideoRecordingErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
id IdGetter(id target, const std::string& selectorName) {
|
|
20
|
-
RequireSelector(target, selectorName);
|
|
21
|
-
SEL selector = SelectorNamed(selectorName);
|
|
22
|
-
return SafeInvoke([&] {
|
|
23
|
-
using Fn = id (*)(id, SEL);
|
|
24
|
-
return ((Fn)objc_msgSend)(target, selector);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// The device-wide "capture service" port (real protocol: SimScreenCaptureService — see CLAUDE.md),
|
|
29
|
-
// distinct from the display descriptor passed as `screen` below. Found by scanning ioPorts since
|
|
30
|
-
// no header exists. Throws NativeSimUnavailableError (not NSError**) if absent.
|
|
31
|
-
id ResolveVideoCaptureService(id device, NSError** error) {
|
|
32
|
-
static const std::string kStartRecordingSelector =
|
|
33
|
-
"startRecordingFromScreen:maskPolicy:assetWriterOutputSettings:outputFile:completionQueue:completionHandler:";
|
|
34
|
-
id ioClient = IdGetter(device, "io");
|
|
35
|
-
if (ioClient == nil) {
|
|
36
|
-
*error = MakeError(1, @"Device has no IO client available — is it booted?");
|
|
37
|
-
return nil;
|
|
38
|
-
}
|
|
39
|
-
NSArray* ports = IdGetter(ioClient, "ioPorts");
|
|
40
|
-
SEL selector = NSSelectorFromString(@(kStartRecordingSelector.c_str()));
|
|
41
|
-
for (id port in ports) {
|
|
42
|
-
id descriptor = IdGetter(port, "descriptor");
|
|
43
|
-
if (descriptor != nil && [descriptor respondsToSelector:selector]) {
|
|
44
|
-
return descriptor;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
throw NativeSimUnavailableError("selector", kStartRecordingSelector, CoreSimulatorFrameworkVersion());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
} // namespace
|
|
51
|
-
|
|
52
11
|
BOOL StartVideoRecording(id device, NSString* displayId, VideoMaskPolicy mask, NSDictionary* assetWriterOutputSettings,
|
|
53
12
|
NSString* outputFile, dispatch_queue_t queue, void (^handler)(NSError*), NSError** error) {
|
|
54
|
-
id captureService =
|
|
13
|
+
id captureService = ResolveScreenCaptureService(device, error);
|
|
55
14
|
if (captureService == nil) {
|
|
56
15
|
return NO;
|
|
57
16
|
}
|
|
@@ -71,7 +30,7 @@ BOOL StartVideoRecording(id device, NSString* displayId, VideoMaskPolicy mask, N
|
|
|
71
30
|
}
|
|
72
31
|
|
|
73
32
|
BOOL StopVideoRecording(id device, dispatch_queue_t queue, void (^handler)(NSError*), NSError** error) {
|
|
74
|
-
id captureService =
|
|
33
|
+
id captureService = ResolveScreenCaptureService(device, error);
|
|
75
34
|
if (captureService == nil) {
|
|
76
35
|
return NO;
|
|
77
36
|
}
|
|
@@ -17,6 +17,13 @@ struct VideoEncoderOptions {
|
|
|
17
17
|
NSString* displayId = nil;
|
|
18
18
|
double fps = 60.0;
|
|
19
19
|
int bitrate = 4000000;
|
|
20
|
+
|
|
21
|
+
// Freezes the encoded frame size at whatever it is on the first frame, forever — a rotation
|
|
22
|
+
// afterward letterboxes into that size instead of resizing the session to match. Internal-only
|
|
23
|
+
// (not JS-configurable): AVRecordingSession forces this on, since its AVAssetWriter track's
|
|
24
|
+
// dimensions are fixed for the file's life (see CLAUDE.md); a live stream has no such constraint
|
|
25
|
+
// and keeps the default (false), matching its own decoded dimensions to the actual rotation.
|
|
26
|
+
bool fixedFrameSize = false;
|
|
20
27
|
};
|
|
21
28
|
|
|
22
29
|
// Polls the live display IOSurface (sim_screenshot.h) on a serial queue and encodes changed
|