@stim-cli/server 1.11.0 → 1.12.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/README.md +82 -5
- package/dist/{protocol-CmlG7X7_.mjs → protocol-BElQc2vm.mjs} +159 -5
- package/dist/protocol.d.mts +109 -4
- package/dist/protocol.mjs +2 -2
- package/dist/protocol.schema.json +399 -2
- package/dist/stim-frames/EmulatorPosture.swift +73 -0
- package/dist/stim-frames/EmulatorRotation.swift +59 -0
- package/dist/stim-frames/InputMessages.swift +56 -6
- package/dist/stim-frames/ScreenshotMessages.swift +27 -4
- package/dist/stim-frames/SimulatorHID.swift +256 -21
- package/dist/stim-frames/SimulatorRotation.swift +64 -0
- package/dist/stim-frames/main.swift +261 -27
- package/dist/stim-frames/sim-fold.entitlements +8 -0
- package/dist/stim-frames/sim-fold.m +47 -0
- package/dist/stim-server.mjs +1011 -155
- package/package.json +3 -3
|
@@ -6,6 +6,10 @@ public struct EmulatorFrame: Sendable {
|
|
|
6
6
|
public var height: Int
|
|
7
7
|
/// The Rotation.SkinRotation the emulator turned the image upright by.
|
|
8
8
|
public var rotation: Int
|
|
9
|
+
/// The display's own size while a foldable is folded; the frame already
|
|
10
|
+
/// shows only that display. Touches address its pixels, not the unfolded
|
|
11
|
+
/// display's.
|
|
12
|
+
public var folded: (width: Int, height: Int)?
|
|
9
13
|
public var rgba: Data
|
|
10
14
|
}
|
|
11
15
|
|
|
@@ -25,7 +29,7 @@ enum ScreenshotMessages {
|
|
|
25
29
|
|
|
26
30
|
static func frame(fromImage bytes: Data) -> EmulatorFrame? {
|
|
27
31
|
var reader = ProtoReader(bytes)
|
|
28
|
-
var size: (width: Int, height: Int, rotation: Int)?
|
|
32
|
+
var size: (width: Int, height: Int, rotation: Int, folded: (width: Int, height: Int)?)?
|
|
29
33
|
var image: Data?
|
|
30
34
|
while let (field, value) = reader.next() {
|
|
31
35
|
switch (field, value) {
|
|
@@ -37,26 +41,45 @@ enum ScreenshotMessages {
|
|
|
37
41
|
guard let size, let image, size.width > 0, size.height > 0,
|
|
38
42
|
image.count == size.width * size.height * 4
|
|
39
43
|
else { return nil }
|
|
40
|
-
return EmulatorFrame(
|
|
44
|
+
return EmulatorFrame(
|
|
45
|
+
width: size.width, height: size.height, rotation: size.rotation, folded: size.folded, rgba: image)
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
private static func rgbaSize(_ bytes: Data) -> (
|
|
48
|
+
private static func rgbaSize(_ bytes: Data) -> (
|
|
49
|
+
width: Int, height: Int, rotation: Int, folded: (width: Int, height: Int)?
|
|
50
|
+
)? {
|
|
44
51
|
var reader = ProtoReader(bytes)
|
|
45
52
|
var format: UInt64 = 0
|
|
46
53
|
var width = 0
|
|
47
54
|
var height = 0
|
|
48
55
|
var rotation = 0
|
|
56
|
+
var folded: (width: Int, height: Int)?
|
|
49
57
|
while let (field, value) = reader.next() {
|
|
50
58
|
switch (field, value) {
|
|
51
59
|
case (1, .varint(let v)): format = v
|
|
52
60
|
case (2, .bytes(let nested)): rotation = skinRotation(nested)
|
|
53
61
|
case (3, .varint(let v)): width = Int(v)
|
|
54
62
|
case (4, .varint(let v)): height = Int(v)
|
|
63
|
+
case (7, .bytes(let nested)): folded = foldedSize(nested)
|
|
55
64
|
default: break
|
|
56
65
|
}
|
|
57
66
|
}
|
|
58
67
|
guard format == rgba8888 else { return nil }
|
|
59
|
-
return (width, height, rotation)
|
|
68
|
+
return (width, height, rotation, folded)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private static func foldedSize(_ bytes: Data) -> (width: Int, height: Int)? {
|
|
72
|
+
var reader = ProtoReader(bytes)
|
|
73
|
+
var width = 0
|
|
74
|
+
var height = 0
|
|
75
|
+
while let (field, value) = reader.next() {
|
|
76
|
+
switch (field, value) {
|
|
77
|
+
case (1, .varint(let v)): width = Int(v)
|
|
78
|
+
case (2, .varint(let v)): height = Int(v)
|
|
79
|
+
default: break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return width > 0 && height > 0 ? (width, height) : nil
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
private static func skinRotation(_ bytes: Data) -> Int {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import ObjectiveC
|
|
3
|
+
import XPC
|
|
3
4
|
|
|
4
|
-
// SimulatorKit's HID client and Indigo message builders are private. These
|
|
5
|
-
// signatures match the assertion strings and ObjC type encodings that Xcode 27
|
|
6
|
-
// ships. Touch points are fractions of the screen when the size is 1x1, and
|
|
7
|
-
// 0x32 is the Indigo target of the main screen's digitizer.
|
|
8
5
|
private typealias AllocFn = @convention(c) (AnyClass, Selector) -> Unmanaged<AnyObject>
|
|
9
6
|
private typealias InitFn = @convention(c) (
|
|
10
7
|
Unmanaged<AnyObject>, Selector, AnyObject, UnsafeMutablePointer<Unmanaged<NSError>?>?
|
|
@@ -12,17 +9,19 @@ private typealias InitFn = @convention(c) (
|
|
|
12
9
|
private typealias SendFn = @convention(c) (
|
|
13
10
|
AnyObject, Selector, UnsafeMutableRawPointer, Bool, DispatchQueue?, (@convention(block) (NSError?) -> Void)?
|
|
14
11
|
) -> Void
|
|
15
|
-
|
|
12
|
+
typealias MouseMessageFn = @convention(c) (
|
|
16
13
|
UnsafePointer<CGPoint>, UnsafePointer<CGPoint>?, UInt32, UInt, CGSize, UInt32
|
|
17
14
|
) -> UnsafeMutableRawPointer?
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
typealias UsageForKeyCodeFn = @convention(c) (UInt32) -> UInt32
|
|
16
|
+
typealias ButtonMessageFn = @convention(c) (UInt32, UInt32, UInt32) -> UnsafeMutableRawPointer?
|
|
17
|
+
typealias HIDMessageFn = @convention(c) (UInt32, UInt32, UInt32, UInt32) -> UnsafeMutableRawPointer?
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
enum SimulatorKit {
|
|
22
20
|
static let handle = dlopen(CoreSimulator.simulatorKitPath(CoreSimulator.developerDir), RTLD_NOW)
|
|
23
21
|
static let mouseMessage = symbol("IndigoHIDMessageForMouseNSEvent", MouseMessageFn.self)
|
|
24
|
-
static let keyboardMessage = symbol("IndigoHIDMessageForKeyboardArbitrary", KeyboardMessageFn.self)
|
|
25
22
|
static let usageForKeyCode = symbol("hidUsageForCGKeyCode", UsageForKeyCodeFn.self)
|
|
23
|
+
static let buttonMessage = symbol("IndigoHIDMessageForButton", ButtonMessageFn.self)
|
|
24
|
+
static let hidMessage = symbol("IndigoHIDMessageForHIDArbitrary", HIDMessageFn.self)
|
|
26
25
|
|
|
27
26
|
static func symbol<T>(_ name: String, _ type: T.Type) -> T? {
|
|
28
27
|
guard let handle, let pointer = dlsym(handle, name) else { return nil }
|
|
@@ -30,6 +29,27 @@ private enum SimulatorKit {
|
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
// Indigo event sources for IndigoHIDMessageForButton, and the HID consumer
|
|
33
|
+
// usages CoreDevice takes for the same buttons; SimulatorKit exports no names
|
|
34
|
+
// for them.
|
|
35
|
+
enum SimulatorButton {
|
|
36
|
+
case home, lock
|
|
37
|
+
|
|
38
|
+
var indigoSource: UInt32 {
|
|
39
|
+
switch self {
|
|
40
|
+
case .home: return 0x0
|
|
41
|
+
case .lock: return 0xbb8
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var consumerUsage: UInt64 {
|
|
46
|
+
switch self {
|
|
47
|
+
case .home: return 0x40
|
|
48
|
+
case .lock: return 0x30
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
enum TouchPhase {
|
|
34
54
|
case down, move, up
|
|
35
55
|
|
|
@@ -42,8 +62,195 @@ enum TouchPhase {
|
|
|
42
62
|
}
|
|
43
63
|
}
|
|
44
64
|
|
|
65
|
+
/// Input for one booted simulator. It uses the simulator's CoreDevice HID
|
|
66
|
+
/// service when the runtime has one, and SimulatorKit's legacy HID client
|
|
67
|
+
/// otherwise. `isConnected` turns false once the CoreDevice connection closes,
|
|
68
|
+
/// and a few seconds after falling back to the legacy client, so a service
|
|
69
|
+
/// that was not up yet is tried again; make a new instance then.
|
|
45
70
|
final class SimulatorHID {
|
|
71
|
+
private let transport: Transport
|
|
72
|
+
private let legacyUntil = Date().addingTimeInterval(10)
|
|
73
|
+
|
|
74
|
+
private enum Transport {
|
|
75
|
+
case coreDevice(CoreDeviceHID)
|
|
76
|
+
case legacy(LegacyHID)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
init?(udid: String) {
|
|
80
|
+
guard let device = CoreSimulator.device(udid: udid) else { return nil }
|
|
81
|
+
if let coreDevice = CoreDeviceHID(device: device) {
|
|
82
|
+
transport = .coreDevice(coreDevice)
|
|
83
|
+
} else if let legacy = LegacyHID(device: device) {
|
|
84
|
+
transport = .legacy(legacy)
|
|
85
|
+
} else {
|
|
86
|
+
return nil
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var isConnected: Bool {
|
|
91
|
+
if case .coreDevice(let coreDevice) = transport { return coreDevice.isConnected }
|
|
92
|
+
return Date() < legacyUntil
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// `point` is a fraction of the screen in its native orientation, origin top-left.
|
|
96
|
+
func touch(_ phase: TouchPhase, at point: CGPoint, screenID: UInt32 = 1) {
|
|
97
|
+
switch transport {
|
|
98
|
+
case .coreDevice(let coreDevice): coreDevice.touch(phase, at: point, screenID: screenID)
|
|
99
|
+
case .legacy(let legacy): legacy.touch(phase, at: point, screenID: screenID)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Presses or releases a key, named by its macOS virtual key code.
|
|
104
|
+
func hardwareKey(code: UInt16, down: Bool) {
|
|
105
|
+
guard let usage = SimulatorKit.usageForKeyCode?(UInt32(code)), usage != 0 else { return }
|
|
106
|
+
switch transport {
|
|
107
|
+
case .coreDevice(let coreDevice): coreDevice.key(usage: UInt64(usage), down: down)
|
|
108
|
+
case .legacy(let legacy): legacy.key(usage: usage, down: down)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func button(_ button: SimulatorButton, down: Bool) {
|
|
113
|
+
switch transport {
|
|
114
|
+
case .coreDevice(let coreDevice): coreDevice.button(usage: button.consumerUsage, down: down)
|
|
115
|
+
case .legacy(let legacy): legacy.button(source: button.indigoSource, down: down)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Once a CoreDevice client such as Device Hub starts dtuhidd in the simulator,
|
|
121
|
+
// the guest ignores SimulatorKit's legacy HID client until it reboots; dtuhidd's
|
|
122
|
+
// own service takes input in both states. The message shapes follow dtuhidd's
|
|
123
|
+
// IndigoHIDServer as Siniulator uses it
|
|
124
|
+
// (github.com/kmagiera/Siniulator, Sources/Siniulator/Input.swift).
|
|
125
|
+
private final class CoreDeviceHID {
|
|
126
|
+
private static let feature = "com.apple.coredevice.feature.remote.hid.digitizer"
|
|
127
|
+
private static let lookupSelector = NSSelectorFromString("lookup:error:")
|
|
128
|
+
|
|
129
|
+
private typealias LookupFn = @convention(c) (
|
|
130
|
+
AnyObject, Selector, NSString, UnsafeMutablePointer<Unmanaged<NSError>?>?
|
|
131
|
+
) -> mach_port_t
|
|
132
|
+
private typealias EndpointFn = @convention(c) (mach_port_t, UInt64, UInt64) -> xpc_object_t?
|
|
133
|
+
private typealias EnableFn = @convention(c) (xpc_connection_t) -> Void
|
|
134
|
+
|
|
135
|
+
private let connection: xpc_connection_t
|
|
136
|
+
private let lock = NSLock()
|
|
137
|
+
private var closed = false
|
|
138
|
+
private var pending: [xpc_object_t]? = []
|
|
139
|
+
|
|
140
|
+
init?(device: NSObject) {
|
|
141
|
+
let process = dlopen(nil, RTLD_NOW)
|
|
142
|
+
guard device.responds(to: Self.lookupSelector),
|
|
143
|
+
let createEndpoint = dlsym(process, "xpc_endpoint_create_mach_port_4sim"),
|
|
144
|
+
let enableSimToHost = dlsym(process, "xpc_connection_enable_sim2host_4sim")
|
|
145
|
+
else { return nil }
|
|
146
|
+
let lookup = unsafeBitCast(device.method(for: Self.lookupSelector), to: LookupFn.self)
|
|
147
|
+
let port = lookup(device, Self.lookupSelector, Self.feature as NSString, nil)
|
|
148
|
+
guard port != 0, let endpoint = unsafeBitCast(createEndpoint, to: EndpointFn.self)(port, 0, 0) else { return nil }
|
|
149
|
+
connection = xpc_connection_create_from_endpoint(endpoint)
|
|
150
|
+
unsafeBitCast(enableSimToHost, to: EnableFn.self)(connection)
|
|
151
|
+
let queue = DispatchQueue(label: "stim.simulator-hid")
|
|
152
|
+
xpc_connection_set_target_queue(connection, queue)
|
|
153
|
+
xpc_connection_set_event_handler(connection) { [weak self] event in
|
|
154
|
+
guard xpc_get_type(event) == XPC_TYPE_ERROR, let self else { return }
|
|
155
|
+
self.lock.lock()
|
|
156
|
+
self.closed = true
|
|
157
|
+
self.pending = nil
|
|
158
|
+
self.lock.unlock()
|
|
159
|
+
}
|
|
160
|
+
xpc_connection_resume(connection)
|
|
161
|
+
// dtuhidd drops events that arrive before it has answered the activating
|
|
162
|
+
// barrier; a barrier left unanswered fails the connection.
|
|
163
|
+
let activation = message("IndigoKeyboardButtonEvent", dictionary(["usageCode": 0, "state": 2]), barrier: true)
|
|
164
|
+
xpc_connection_send_message_with_reply(connection, activation, queue) { [weak self] reply in
|
|
165
|
+
guard xpc_get_type(reply) != XPC_TYPE_ERROR, let self else { return }
|
|
166
|
+
self.lock.lock()
|
|
167
|
+
let queued = self.pending ?? []
|
|
168
|
+
self.pending = nil
|
|
169
|
+
for message in queued { xpc_connection_send_message(self.connection, message) }
|
|
170
|
+
self.lock.unlock()
|
|
171
|
+
}
|
|
172
|
+
queue.asyncAfter(deadline: .now() + 5) { [weak self] in
|
|
173
|
+
guard let self else { return }
|
|
174
|
+
self.lock.lock()
|
|
175
|
+
if self.pending != nil {
|
|
176
|
+
self.closed = true
|
|
177
|
+
self.pending = nil
|
|
178
|
+
}
|
|
179
|
+
self.lock.unlock()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
deinit {
|
|
184
|
+
xpc_connection_cancel(connection)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
var isConnected: Bool {
|
|
188
|
+
lock.lock()
|
|
189
|
+
defer { lock.unlock() }
|
|
190
|
+
return !closed
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// dtuhidd's DigitizerTarget is 0 for the main screen and the screen ID for any other display.
|
|
194
|
+
func touch(_ phase: TouchPhase, at point: CGPoint, screenID: UInt32) {
|
|
195
|
+
let contact = xpc_dictionary_create(nil, nil, 0)
|
|
196
|
+
xpc_dictionary_set_double(contact, "x", point.x)
|
|
197
|
+
xpc_dictionary_set_double(contact, "y", point.y)
|
|
198
|
+
let payload = dictionary([
|
|
199
|
+
"eventType": phase == .down ? 0 : phase == .move ? 1 : 2, "edge": 0, "target": UInt64(screenID == 1 ? 0 : screenID),
|
|
200
|
+
])
|
|
201
|
+
xpc_dictionary_set_value(payload, "pointOne", contact)
|
|
202
|
+
send("IndigoDigitizerEvent", payload)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func key(usage: UInt64, down: Bool) {
|
|
206
|
+
send("IndigoKeyboardButtonEvent", ["usageCode": usage, "state": down ? 1 : 2])
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func button(usage: UInt64, down: Bool) {
|
|
210
|
+
send("IndigoButtonEvent", ["usagePage": 0x0c, "usageCode": usage, "state": down ? 1 : 2])
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func send(_ type: String, _ values: [String: UInt64]) {
|
|
214
|
+
send(type, dictionary(values))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private func send(_ type: String, _ payload: xpc_object_t) {
|
|
218
|
+
let message = message(type, payload, barrier: false)
|
|
219
|
+
lock.lock()
|
|
220
|
+
defer { lock.unlock() }
|
|
221
|
+
guard !closed else { return }
|
|
222
|
+
if pending != nil {
|
|
223
|
+
pending?.append(message)
|
|
224
|
+
} else {
|
|
225
|
+
xpc_connection_send_message(connection, message)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private func message(_ type: String, _ payload: xpc_object_t, barrier: Bool) -> xpc_object_t {
|
|
230
|
+
let message = xpc_dictionary_create(nil, nil, 0)
|
|
231
|
+
xpc_dictionary_set_string(message, "messageType", type)
|
|
232
|
+
xpc_dictionary_set_bool(message, "isBarrier", barrier)
|
|
233
|
+
xpc_dictionary_set_string(message, "featureIdentifier", Self.feature)
|
|
234
|
+
xpc_dictionary_set_value(message, "payload", payload)
|
|
235
|
+
return message
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func dictionary(_ values: [String: UInt64]) -> xpc_object_t {
|
|
239
|
+
let object = xpc_dictionary_create(nil, nil, 0)
|
|
240
|
+
for (key, value) in values { xpc_dictionary_set_uint64(object, key, value) }
|
|
241
|
+
return object
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// SimulatorKit's HID client and Indigo message builders are private. These
|
|
246
|
+
// signatures match the assertion strings and ObjC type encodings that Xcode 27
|
|
247
|
+
// ships. Touch points are fractions of the screen when the size is 1x1, and
|
|
248
|
+
// 0x32 is the Indigo target of the main screen's digitizer. Keys and buttons
|
|
249
|
+
// also go to 0x32: the keyboard (0x64) and button (0x33) targets stop working
|
|
250
|
+
// once the guest suppresses those services.
|
|
251
|
+
private final class LegacyHID {
|
|
46
252
|
private static let mainScreenTarget: UInt32 = 0x32
|
|
253
|
+
private static let keyboardPage: UInt32 = 0x07
|
|
47
254
|
private static let keyDown: UInt32 = 1
|
|
48
255
|
private static let keyUp: UInt32 = 2
|
|
49
256
|
private static let sendSelector = NSSelectorFromString("sendWithMessage:freeWhenDone:completionQueue:completion:")
|
|
@@ -51,11 +258,10 @@ final class SimulatorHID {
|
|
|
51
258
|
private let client: AnyObject
|
|
52
259
|
private let send: SendFn
|
|
53
260
|
|
|
54
|
-
init?(
|
|
261
|
+
init?(device: NSObject) {
|
|
55
262
|
let allocSelector = NSSelectorFromString("alloc")
|
|
56
263
|
let initSelector = NSSelectorFromString("initWithDevice:error:")
|
|
57
264
|
guard SimulatorKit.handle != nil,
|
|
58
|
-
let device = CoreSimulator.device(udid: udid),
|
|
59
265
|
let cls = NSClassFromString("_TtC12SimulatorKit24SimDeviceLegacyHIDClient"),
|
|
60
266
|
let metaclass = object_getClass(cls),
|
|
61
267
|
class_respondsToSelector(metaclass, allocSelector),
|
|
@@ -73,13 +279,8 @@ final class SimulatorHID {
|
|
|
73
279
|
// The simulator's SimulatorHID addresses a display's digitizer by its screen
|
|
74
280
|
// ID with bit 30 set, and backboardd aborts on a target it has no service
|
|
75
281
|
// for. The main display keeps its legacy target.
|
|
76
|
-
|
|
77
|
-
screenID == 1 ? mainScreenTarget : 0x4000_0000 | screenID
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/// `point` is a fraction of the screen, origin top-left.
|
|
81
|
-
func touch(_ phase: TouchPhase, at point: CGPoint, screenID: UInt32 = 1) {
|
|
82
|
-
let target = Self.digitizerTarget(screenID: screenID)
|
|
282
|
+
func touch(_ phase: TouchPhase, at point: CGPoint, screenID: UInt32) {
|
|
283
|
+
let target = screenID == 1 ? Self.mainScreenTarget : 0x4000_0000 | screenID
|
|
83
284
|
var point = point
|
|
84
285
|
// The builder returns nil for a drag that arrives within 16 ms of the previous message.
|
|
85
286
|
guard let message = SimulatorKit.mouseMessage?(
|
|
@@ -88,9 +289,14 @@ final class SimulatorHID {
|
|
|
88
289
|
deliver(message)
|
|
89
290
|
}
|
|
90
291
|
|
|
91
|
-
func key(
|
|
92
|
-
guard let
|
|
93
|
-
|
|
292
|
+
func key(usage: UInt32, down: Bool) {
|
|
293
|
+
guard let message = SimulatorKit.hidMessage?(Self.mainScreenTarget, Self.keyboardPage, usage, down ? Self.keyDown : Self.keyUp)
|
|
294
|
+
else { return }
|
|
295
|
+
deliver(message)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func button(source: UInt32, down: Bool) {
|
|
299
|
+
guard let message = SimulatorKit.buttonMessage?(source, down ? Self.keyDown : Self.keyUp, Self.mainScreenTarget)
|
|
94
300
|
else { return }
|
|
95
301
|
deliver(message)
|
|
96
302
|
}
|
|
@@ -99,3 +305,32 @@ final class SimulatorHID {
|
|
|
99
305
|
send(client, Self.sendSelector, message, true, nil, nil)
|
|
100
306
|
}
|
|
101
307
|
}
|
|
308
|
+
|
|
309
|
+
/// Maps a fraction of the upright screen, origin top-left, to a fraction of
|
|
310
|
+
/// the framebuffer in its native portrait orientation, which is what the
|
|
311
|
+
/// simulator's digitizer expects. `orientation` is a UIInterfaceOrientation.
|
|
312
|
+
func nativeScreenPoint(_ point: CGPoint, orientation: UInt32) -> CGPoint {
|
|
313
|
+
switch orientation {
|
|
314
|
+
case 2: return CGPoint(x: 1 - point.x, y: 1 - point.y)
|
|
315
|
+
case 3: return CGPoint(x: point.y, y: 1 - point.x)
|
|
316
|
+
case 4: return CGPoint(x: 1 - point.y, y: point.x)
|
|
317
|
+
default: return point
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Whether a grid of samples across a BGRA framebuffer is all black. A lit
|
|
322
|
+
/// screen shows at least a status bar, so a dark app still has non-black pixels.
|
|
323
|
+
func isBlack(_ surface: IOSurface) -> Bool {
|
|
324
|
+
surface.lock(options: .readOnly, seed: nil)
|
|
325
|
+
defer { surface.unlock(options: .readOnly, seed: nil) }
|
|
326
|
+
let bytes = surface.baseAddress.assumingMemoryBound(to: UInt8.self)
|
|
327
|
+
let rowStep = max(surface.height / 64, 1)
|
|
328
|
+
let columnStep = max(surface.width / 48, 1)
|
|
329
|
+
for y in stride(from: 0, to: surface.height, by: rowStep) {
|
|
330
|
+
for x in stride(from: 0, to: surface.width, by: columnStep) {
|
|
331
|
+
let offset = y * surface.bytesPerRow + x * 4
|
|
332
|
+
if bytes[offset] | bytes[offset + 1] | bytes[offset + 2] != 0 { return false }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return true
|
|
336
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import Darwin
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Turns a booted simulator a quarter turn, as Simulator.app's Rotate Left and
|
|
5
|
+
/// Rotate Right do. Returns false when the simulator cannot be reached.
|
|
6
|
+
public enum SimulatorRotation {
|
|
7
|
+
private static var sent: [String: UInt32] = [:]
|
|
8
|
+
|
|
9
|
+
public static func rotate(udid: String, clockwise: Bool) -> Bool {
|
|
10
|
+
guard let device = CoreSimulator.device(udid: udid) else { return false }
|
|
11
|
+
let current = sent[udid] ?? deviceOrientation(interface: CoreSimulator.displays(udid: udid).first?
|
|
12
|
+
.screenProperties?.uiOrientation ?? 1)
|
|
13
|
+
let next = quarterTurn(from: current, clockwise: clockwise)
|
|
14
|
+
guard send(orientation: next, to: device) else { return false }
|
|
15
|
+
sent[udid] = next
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// GraphicsServices' GSEvent wire format for PurpleWorkspacePort, as Simulator.app
|
|
20
|
+
// sends it: a 108-byte mach message with id 0x7B carrying event type 50
|
|
21
|
+
// (device orientation changed) with the host flag 0x20000, a 4-byte record at
|
|
22
|
+
// 0x48 and the UIDeviceOrientation at 0x4C. facebook/idb documents the layout
|
|
23
|
+
// in PrivateHeaders/SimulatorApp/GSEvent.h.
|
|
24
|
+
private static func send(orientation: UInt32, to device: NSObject) -> Bool {
|
|
25
|
+
typealias Lookup = @convention(c) (AnyObject, Selector, NSString, UnsafeMutablePointer<NSError?>?) -> mach_port_t
|
|
26
|
+
let selector = NSSelectorFromString("lookup:error:")
|
|
27
|
+
guard device.responds(to: selector) else { return false }
|
|
28
|
+
let lookup = unsafeBitCast(device.method(for: selector), to: Lookup.self)
|
|
29
|
+
let port = lookup(device, selector, "PurpleWorkspacePort", nil)
|
|
30
|
+
guard port != 0 else { return false }
|
|
31
|
+
var message = [UInt8](repeating: 0, count: 112)
|
|
32
|
+
func write(_ value: UInt32, at offset: Int) {
|
|
33
|
+
withUnsafeBytes(of: value.littleEndian) { message.replaceSubrange(offset..<offset + 4, with: $0) }
|
|
34
|
+
}
|
|
35
|
+
write(0x13, at: 0x00)
|
|
36
|
+
write(108, at: 0x04)
|
|
37
|
+
write(port, at: 0x08)
|
|
38
|
+
write(0x7B, at: 0x14)
|
|
39
|
+
write(50 | 0x20000, at: 0x18)
|
|
40
|
+
write(4, at: 0x48)
|
|
41
|
+
write(orientation, at: 0x4C)
|
|
42
|
+
return message.withUnsafeMutableBytes { bytes in
|
|
43
|
+
let header = bytes.baseAddress!.assumingMemoryBound(to: mach_msg_header_t.self)
|
|
44
|
+
return mach_msg(header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, header.pointee.msgh_size, 0, 0, 2000, 0)
|
|
45
|
+
} == KERN_SUCCESS
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// The UIDeviceOrientation that shows a UIInterfaceOrientation upright.
|
|
50
|
+
func deviceOrientation(interface: UInt32) -> UInt32 {
|
|
51
|
+
switch interface {
|
|
52
|
+
case 3: return 4
|
|
53
|
+
case 4: return 3
|
|
54
|
+
case 2: return 2
|
|
55
|
+
default: return 1
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// The UIDeviceOrientation a quarter turn from `orientation`.
|
|
60
|
+
func quarterTurn(from orientation: UInt32, clockwise: Bool) -> UInt32 {
|
|
61
|
+
let counterclockwise: [UInt32] = [1, 3, 2, 4]
|
|
62
|
+
let index = counterclockwise.firstIndex(of: orientation) ?? 0
|
|
63
|
+
return counterclockwise[(index + (clockwise ? 3 : 1)) % 4]
|
|
64
|
+
}
|