@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.
@@ -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
 
@@ -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 = ResolveVideoCaptureService(device, error);
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 = ResolveVideoCaptureService(device, error);
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