@stim-cli/server 1.10.1 → 1.11.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.
@@ -0,0 +1,141 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ /// Unary EmulatorController calls over one cleartext HTTP/2 connection to the
5
+ /// emulator's localhost gRPC port. Calls run one at a time, in order, because
6
+ /// the server may handle concurrent streams out of order and a touch must not
7
+ /// overtake the one before it. Response header blocks are never decoded; a
8
+ /// call ends on END_STREAM or RST_STREAM. Calls queued when the connection
9
+ /// fails are dropped, and the next call reconnects. `close` lets queued calls
10
+ /// finish first.
11
+ final class EmulatorInput {
12
+ private static let window: UInt32 = 1 << 30
13
+
14
+ private struct Call {
15
+ var method: String
16
+ var message: Data
17
+ var completion: ((Data?) -> Void)?
18
+ }
19
+
20
+ private let endpoint: EmulatorEndpoint
21
+ private let queue = DispatchQueue(label: "stim.emulator-input")
22
+ private var connection: NWConnection?
23
+ private var ready = false
24
+ private var inbox: [UInt8] = []
25
+ private var pending: [Call] = []
26
+ private var current: (stream: UInt32, call: Call, response: [UInt8])?
27
+ private var nextStream: UInt32 = 1
28
+ private var closing = false
29
+
30
+ init(endpoint: EmulatorEndpoint) {
31
+ self.endpoint = endpoint
32
+ }
33
+
34
+ func call(_ method: String, _ message: Data, completion: ((Data?) -> Void)? = nil) {
35
+ queue.async {
36
+ self.pending.append(Call(method: method, message: message, completion: completion))
37
+ self.connect()
38
+ self.sendNext()
39
+ }
40
+ }
41
+
42
+ func close() {
43
+ queue.async {
44
+ self.closing = true
45
+ self.closeIfIdle()
46
+ }
47
+ }
48
+
49
+ private func closeIfIdle() {
50
+ if closing, current == nil, pending.isEmpty { reset() }
51
+ }
52
+
53
+ private func connect() {
54
+ guard connection == nil else { return }
55
+ let connection = NWConnection(
56
+ host: "127.0.0.1", port: NWEndpoint.Port(integerLiteral: UInt16(endpoint.grpcPort)), using: .tcp)
57
+ self.connection = connection
58
+ connection.stateUpdateHandler = { [weak self, weak connection] state in
59
+ guard let self, let connection, connection === self.connection else { return }
60
+ switch state {
61
+ case .ready:
62
+ self.ready = true
63
+ self.send(GrpcFraming.preface(window: Self.window))
64
+ self.receive(on: connection)
65
+ self.sendNext()
66
+ case .waiting, .failed, .cancelled:
67
+ self.reset()
68
+ default:
69
+ break
70
+ }
71
+ }
72
+ connection.start(queue: queue)
73
+ }
74
+
75
+ private func sendNext() {
76
+ guard ready, current == nil, !pending.isEmpty else { return }
77
+ let call = pending.removeFirst()
78
+ let stream = nextStream
79
+ nextStream += 2
80
+ current = (stream, call, [])
81
+ send(GrpcFraming.request(method: call.method, message: call.message, endpoint: endpoint, stream: stream))
82
+ }
83
+
84
+ private func receive(on connection: NWConnection) {
85
+ connection.receive(minimumIncompleteLength: 1, maximumLength: 1 << 16) { [weak self] data, _, complete, error in
86
+ guard let self, connection === self.connection else { return }
87
+ var buffer = self.inbox + (data.map { [UInt8]($0) } ?? [])
88
+ self.inbox = []
89
+ GrpcFraming.drain(&buffer) { type, flags, stream, payload in
90
+ self.handle(type: type, flags: flags, stream: stream, payload: payload)
91
+ }
92
+ guard connection === self.connection else { return }
93
+ self.inbox = buffer
94
+ if complete || error != nil { self.reset() } else { self.receive(on: connection) }
95
+ }
96
+ }
97
+
98
+ private func handle(type: UInt8, flags: UInt8, stream: UInt32, payload: ArraySlice<UInt8>) {
99
+ if let reply = GrpcFraming.acknowledgement(type: type, flags: flags, payload: payload) {
100
+ send(reply)
101
+ return
102
+ }
103
+ if type == 7 { return reset() }
104
+ guard let current, stream == current.stream else { return }
105
+ switch type {
106
+ case 0:
107
+ var body = payload
108
+ if flags & 0x8 != 0, let pad = body.first { body = body.dropFirst().dropLast(Int(pad)) }
109
+ self.current?.response += body
110
+ if flags & 0x1 != 0 { complete() }
111
+ case 1 where flags & 0x1 != 0, 3:
112
+ complete()
113
+ default:
114
+ break
115
+ }
116
+ }
117
+
118
+ private func complete() {
119
+ guard let current else { return }
120
+ self.current = nil
121
+ current.call.completion?(current.response.count >= 5 ? Data(current.response.dropFirst(5)) : nil)
122
+ sendNext()
123
+ closeIfIdle()
124
+ }
125
+
126
+ private func send(_ bytes: [UInt8]) {
127
+ connection?.send(content: Data(bytes), completion: .idempotent)
128
+ }
129
+
130
+ private func reset() {
131
+ connection?.cancel()
132
+ connection = nil
133
+ ready = false
134
+ inbox = []
135
+ nextStream = 1
136
+ let dropped = (current.map { [$0.call] } ?? []) + pending
137
+ current = nil
138
+ pending = []
139
+ for call in dropped { call.completion?(nil) }
140
+ }
141
+ }
@@ -0,0 +1,85 @@
1
+ import Foundation
2
+
3
+ enum GrpcFraming {
4
+ static let service = "/android.emulation.control.EmulatorController/"
5
+
6
+ static func preface(window: UInt32) -> [UInt8] {
7
+ var out = Array("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".utf8)
8
+ var settings: [UInt8] = []
9
+ for (id, value) in [(UInt16(2), UInt32(0)), (4, window)] {
10
+ settings += [UInt8(id >> 8), UInt8(id & 0xff)] + bigEndian(value)
11
+ }
12
+ out += frame(type: 4, flags: 0, stream: 0, payload: settings)
13
+ out += frame(type: 8, flags: 0, stream: 0, payload: bigEndian(window))
14
+ return out
15
+ }
16
+
17
+ static func request(method: String, message: Data, endpoint: EmulatorEndpoint, stream: UInt32) -> [UInt8] {
18
+ var headers: [UInt8] = [0x83, 0x86]
19
+ headers += literal(index: 4, value: service + method)
20
+ headers += literal(index: 1, value: "127.0.0.1:\(endpoint.grpcPort)")
21
+ headers += literal(index: 31, value: "application/grpc")
22
+ headers += [0x00] + string("te") + string("trailers")
23
+ if let token = endpoint.token {
24
+ headers += literal(index: 23, value: "Bearer \(token)")
25
+ }
26
+ let body = [UInt8](message)
27
+ return frame(type: 1, flags: 0x4, stream: stream, payload: headers)
28
+ + frame(type: 0, flags: 0x1, stream: stream, payload: [0] + bigEndian(UInt32(body.count)) + body)
29
+ }
30
+
31
+ static func frame(type: UInt8, flags: UInt8, stream: UInt32, payload: [UInt8]) -> [UInt8] {
32
+ let length = UInt32(payload.count)
33
+ return [UInt8(length >> 16 & 0xff), UInt8(length >> 8 & 0xff), UInt8(length & 0xff), type, flags]
34
+ + bigEndian(stream) + payload
35
+ }
36
+
37
+ static func drain(_ inbox: inout [UInt8], _ handle: (UInt8, UInt8, UInt32, ArraySlice<UInt8>) -> Void) {
38
+ var offset = 0
39
+ while inbox.count - offset >= 9 {
40
+ let length = Int(inbox[offset]) << 16 | Int(inbox[offset + 1]) << 8 | Int(inbox[offset + 2])
41
+ guard inbox.count - offset >= 9 + length else { break }
42
+ let stream = inbox[(offset + 5)..<(offset + 9)].reduce(UInt32(0)) { $0 << 8 | UInt32($1) } & 0x7fff_ffff
43
+ handle(inbox[offset + 3], inbox[offset + 4], stream, inbox[(offset + 9)..<(offset + 9 + length)])
44
+ offset += 9 + length
45
+ }
46
+ inbox.removeFirst(offset)
47
+ }
48
+
49
+ static func acknowledgement(type: UInt8, flags: UInt8, payload: ArraySlice<UInt8>) -> [UInt8]? {
50
+ guard flags & 0x1 == 0 else { return nil }
51
+ switch type {
52
+ case 4: return frame(type: 4, flags: 0x1, stream: 0, payload: [])
53
+ case 6: return frame(type: 6, flags: 0x1, stream: 0, payload: Array(payload))
54
+ default: return nil
55
+ }
56
+ }
57
+
58
+ static func bigEndian(_ value: UInt32) -> [UInt8] {
59
+ [UInt8(value >> 24), UInt8(value >> 16 & 0xff), UInt8(value >> 8 & 0xff), UInt8(value & 0xff)]
60
+ }
61
+
62
+ // HPACK (RFC 7541) literal header field without indexing, name taken from
63
+ // the static table, value as a raw (non-Huffman) string.
64
+ private static func literal(index: Int, value: String) -> [UInt8] {
65
+ integer(index, prefixBits: 4, flags: 0x00) + string(value)
66
+ }
67
+
68
+ private static func string(_ value: String) -> [UInt8] {
69
+ let bytes = Array(value.utf8)
70
+ return integer(bytes.count, prefixBits: 7, flags: 0x00) + bytes
71
+ }
72
+
73
+ private static func integer(_ value: Int, prefixBits: Int, flags: UInt8) -> [UInt8] {
74
+ let limit = (1 << prefixBits) - 1
75
+ if value < limit { return [flags | UInt8(value)] }
76
+ var out = [flags | UInt8(limit)]
77
+ var rest = value - limit
78
+ while rest >= 0x80 {
79
+ out.append(UInt8(rest & 0x7f) | 0x80)
80
+ rest >>= 7
81
+ }
82
+ out.append(UInt8(rest))
83
+ return out
84
+ }
85
+ }
@@ -0,0 +1,73 @@
1
+ import Foundation
2
+
3
+ // Field numbers and enum values follow MouseEvent, KeyboardEvent and
4
+ // EmulatorStatus in the emulator's emulator_controller.proto (sdk/emulator/lib).
5
+ enum InputMessages {
6
+ static let macKeyCodeType: UInt64 = 4
7
+ static let keyup: UInt64 = 1
8
+
9
+ /// A MouseEvent on the main display. `pressed` sets the primary button; the
10
+ /// emulator turns it into a single touch on a touchscreen device.
11
+ static func mouse(x: Int, y: Int, pressed: Bool) -> Data {
12
+ message([(1, UInt64(max(x, 0))), (2, UInt64(max(y, 0))), (3, pressed ? 1 : 0)])
13
+ }
14
+
15
+ /// A KeyboardEvent the emulator types as a key press per character.
16
+ static func text(_ text: String) -> Data {
17
+ var out = Data()
18
+ appendBytes(field: 5, Data(text.utf8), to: &out)
19
+ return out
20
+ }
21
+
22
+ /// A KeyboardEvent for a physical key named by its macOS virtual key code,
23
+ /// which the emulator translates to an evdev code.
24
+ static func key(macKeyCode: UInt16, down: Bool) -> Data {
25
+ message([(1, macKeyCodeType), (2, down ? 0 : keyup), (3, UInt64(macKeyCode))])
26
+ }
27
+
28
+ /// The main display's size in pixels, read from `hw.lcd.width` and
29
+ /// `hw.lcd.height` in an EmulatorStatus's hardwareConfig.
30
+ static func displaySize(fromStatus bytes: Data) -> (width: Int, height: Int)? {
31
+ var reader = ProtoReader(bytes)
32
+ var width: Int?
33
+ var height: Int?
34
+ while let (field, value) = reader.next() {
35
+ guard field == 5, case .bytes(let list) = value else { continue }
36
+ var entries = ProtoReader(list)
37
+ while let (entryField, entryValue) = entries.next() {
38
+ guard entryField == 1, case .bytes(let entry) = entryValue, let (key, value) = pair(entry) else { continue }
39
+ if key == "hw.lcd.width" { width = Int(value) }
40
+ if key == "hw.lcd.height" { height = Int(value) }
41
+ }
42
+ }
43
+ guard let width, let height, width > 0, height > 0 else { return nil }
44
+ return (width, height)
45
+ }
46
+
47
+ private static func pair(_ bytes: Data) -> (String, String)? {
48
+ var reader = ProtoReader(bytes)
49
+ var key: String?
50
+ var value = ""
51
+ while let (field, item) = reader.next() {
52
+ guard case .bytes(let data) = item else { continue }
53
+ if field == 1 { key = String(decoding: data, as: UTF8.self) }
54
+ if field == 2 { value = String(decoding: data, as: UTF8.self) }
55
+ }
56
+ return key.map { ($0, value) }
57
+ }
58
+
59
+ private static func message(_ fields: [(Int, UInt64)]) -> Data {
60
+ var out = Data()
61
+ for (field, value) in fields where value != 0 {
62
+ ScreenshotMessages.appendVarint(UInt64(field << 3), to: &out)
63
+ ScreenshotMessages.appendVarint(value, to: &out)
64
+ }
65
+ return out
66
+ }
67
+
68
+ private static func appendBytes(field: Int, _ bytes: Data, to out: inout Data) {
69
+ ScreenshotMessages.appendVarint(UInt64(field << 3 | 2), to: &out)
70
+ ScreenshotMessages.appendVarint(UInt64(bytes.count), to: &out)
71
+ out += bytes
72
+ }
73
+ }
@@ -0,0 +1,129 @@
1
+ import Foundation
2
+
3
+ /// One RGBA8888 frame of an emulator display.
4
+ public struct EmulatorFrame: Sendable {
5
+ public var width: Int
6
+ public var height: Int
7
+ /// The Rotation.SkinRotation the emulator turned the image upright by.
8
+ public var rotation: Int
9
+ public var rgba: Data
10
+ }
11
+
12
+ // Field numbers follow ImageFormat and Image in the emulator's
13
+ // emulator_controller.proto (sdk/emulator/lib).
14
+ enum ScreenshotMessages {
15
+ static let rgba8888: UInt64 = 1
16
+
17
+ static func imageFormat(width: Int, height: Int) -> Data {
18
+ var out = Data()
19
+ for (field, value) in [(1, rgba8888), (3, UInt64(width)), (4, UInt64(height))] {
20
+ appendVarint(UInt64(field << 3), to: &out)
21
+ appendVarint(value, to: &out)
22
+ }
23
+ return out
24
+ }
25
+
26
+ static func frame(fromImage bytes: Data) -> EmulatorFrame? {
27
+ var reader = ProtoReader(bytes)
28
+ var size: (width: Int, height: Int, rotation: Int)?
29
+ var image: Data?
30
+ while let (field, value) = reader.next() {
31
+ switch (field, value) {
32
+ case (1, .bytes(let nested)): size = rgbaSize(nested)
33
+ case (4, .bytes(let data)): image = data
34
+ default: break
35
+ }
36
+ }
37
+ guard let size, let image, size.width > 0, size.height > 0,
38
+ image.count == size.width * size.height * 4
39
+ else { return nil }
40
+ return EmulatorFrame(width: size.width, height: size.height, rotation: size.rotation, rgba: image)
41
+ }
42
+
43
+ private static func rgbaSize(_ bytes: Data) -> (width: Int, height: Int, rotation: Int)? {
44
+ var reader = ProtoReader(bytes)
45
+ var format: UInt64 = 0
46
+ var width = 0
47
+ var height = 0
48
+ var rotation = 0
49
+ while let (field, value) = reader.next() {
50
+ switch (field, value) {
51
+ case (1, .varint(let v)): format = v
52
+ case (2, .bytes(let nested)): rotation = skinRotation(nested)
53
+ case (3, .varint(let v)): width = Int(v)
54
+ case (4, .varint(let v)): height = Int(v)
55
+ default: break
56
+ }
57
+ }
58
+ guard format == rgba8888 else { return nil }
59
+ return (width, height, rotation)
60
+ }
61
+
62
+ private static func skinRotation(_ bytes: Data) -> Int {
63
+ var reader = ProtoReader(bytes)
64
+ while let (field, value) = reader.next() {
65
+ if field == 1, case .varint(let v) = value { return Int(v & 3) }
66
+ }
67
+ return 0
68
+ }
69
+
70
+ static func appendVarint(_ value: UInt64, to out: inout Data) {
71
+ var v = value
72
+ while v >= 0x80 {
73
+ out.append(UInt8(v & 0x7f) | 0x80)
74
+ v >>= 7
75
+ }
76
+ out.append(UInt8(v))
77
+ }
78
+ }
79
+
80
+ struct ProtoReader {
81
+ enum Value {
82
+ case varint(UInt64)
83
+ case bytes(Data)
84
+ case fixed
85
+ }
86
+
87
+ private let data: Data
88
+ private var index: Data.Index
89
+
90
+ init(_ data: Data) {
91
+ self.data = data
92
+ index = data.startIndex
93
+ }
94
+
95
+ mutating func next() -> (Int, Value)? {
96
+ guard index < data.endIndex, let key = varint() else { return nil }
97
+ let field = Int(key >> 3)
98
+ switch key & 7 {
99
+ case 0:
100
+ guard let v = varint() else { return nil }
101
+ return (field, .varint(v))
102
+ case 1, 5:
103
+ let size = key & 7 == 1 ? 8 : 4
104
+ guard data.endIndex - index >= size else { return nil }
105
+ index += size
106
+ return (field, .fixed)
107
+ case 2:
108
+ guard let length = varint(), UInt64(data.endIndex - index) >= length else { return nil }
109
+ let end = index + Int(length)
110
+ defer { index = end }
111
+ return (field, .bytes(data[index..<end]))
112
+ default:
113
+ return nil
114
+ }
115
+ }
116
+
117
+ private mutating func varint() -> UInt64? {
118
+ var result: UInt64 = 0
119
+ var shift: UInt64 = 0
120
+ while index < data.endIndex, shift < 64 {
121
+ let byte = data[index]
122
+ index += 1
123
+ result |= UInt64(byte & 0x7f) << shift
124
+ if byte < 0x80 { return result }
125
+ shift += 7
126
+ }
127
+ return nil
128
+ }
129
+ }
@@ -0,0 +1,130 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ /// A single `streamScreenshot` call over cleartext HTTP/2 to the emulator's
5
+ /// localhost gRPC port. URLSession speaks HTTP/2 only over TLS, and the
6
+ /// emulator's endpoint is h2c, so this writes the frames itself. It never
7
+ /// decodes response header blocks: the call ends on END_STREAM, RST_STREAM,
8
+ /// GOAWAY, or a closed connection.
9
+ final class ScreenshotStream {
10
+ private static let window: UInt32 = 1 << 30
11
+
12
+ private let connection: NWConnection
13
+ private let queue = DispatchQueue(label: "stim.emulator-frames")
14
+ private let request: [UInt8]
15
+ private let onFrame: (EmulatorFrame) -> Void
16
+ private let onEnd: () -> Void
17
+ private var inbox: [UInt8] = []
18
+ private var messages: [UInt8] = []
19
+ private var unacknowledged: UInt32 = 0
20
+ private var ended = false
21
+
22
+ init(
23
+ endpoint: EmulatorEndpoint, width: Int, height: Int,
24
+ onFrame: @escaping (EmulatorFrame) -> Void, onEnd: @escaping () -> Void
25
+ ) {
26
+ connection = NWConnection(
27
+ host: "127.0.0.1", port: NWEndpoint.Port(integerLiteral: UInt16(endpoint.grpcPort)), using: .tcp)
28
+ request = Self.requestBytes(endpoint: endpoint, width: width, height: height)
29
+ self.onFrame = onFrame
30
+ self.onEnd = onEnd
31
+ }
32
+
33
+ func start() {
34
+ connection.stateUpdateHandler = { [weak self] state in
35
+ switch state {
36
+ case .ready:
37
+ guard let self else { return }
38
+ self.send(self.request)
39
+ self.receive()
40
+ case .failed, .cancelled:
41
+ self?.finish()
42
+ default:
43
+ break
44
+ }
45
+ }
46
+ connection.start(queue: queue)
47
+ }
48
+
49
+ func cancel() {
50
+ queue.async {
51
+ self.ended = true
52
+ self.connection.cancel()
53
+ }
54
+ }
55
+
56
+ static func requestBytes(endpoint: EmulatorEndpoint, width: Int, height: Int) -> [UInt8] {
57
+ GrpcFraming.preface(window: window)
58
+ + GrpcFraming.request(
59
+ method: "streamScreenshot", message: ScreenshotMessages.imageFormat(width: width, height: height),
60
+ endpoint: endpoint, stream: 1)
61
+ }
62
+
63
+ private func receive() {
64
+ connection.receive(minimumIncompleteLength: 1, maximumLength: 1 << 20) { [weak self] data, _, complete, error in
65
+ guard let self else { return }
66
+ if let data { self.inbox += data }
67
+ self.drainFrames()
68
+ if complete || error != nil { self.finish() } else if !self.ended { self.receive() }
69
+ }
70
+ }
71
+
72
+ private func drainFrames() {
73
+ GrpcFraming.drain(&inbox) { type, flags, _, payload in
74
+ if !ended { handle(type: type, flags: flags, payload: payload) }
75
+ }
76
+ }
77
+
78
+ private func handle(type: UInt8, flags: UInt8, payload: ArraySlice<UInt8>) {
79
+ switch type {
80
+ case 0:
81
+ var body = payload
82
+ if flags & 0x8 != 0, let pad = body.first {
83
+ body = body.dropFirst().dropLast(Int(pad))
84
+ }
85
+ messages += body
86
+ acknowledge(UInt32(payload.count))
87
+ drainMessages()
88
+ if flags & 0x1 != 0 { finish() }
89
+ case 1:
90
+ if flags & 0x1 != 0 { finish() }
91
+ case 3, 7:
92
+ finish()
93
+ default:
94
+ if let reply = GrpcFraming.acknowledgement(type: type, flags: flags, payload: payload) { send(reply) }
95
+ }
96
+ }
97
+
98
+ private func drainMessages() {
99
+ var offset = 0
100
+ while messages.count - offset >= 5 {
101
+ let length = messages[(offset + 1)..<(offset + 5)].reduce(0) { $0 << 8 | Int($1) }
102
+ guard messages.count - offset >= 5 + length else { break }
103
+ let bytes = Data(messages[(offset + 5)..<(offset + 5 + length)])
104
+ offset += 5 + length
105
+ if let frame = ScreenshotMessages.frame(fromImage: bytes) { onFrame(frame) }
106
+ }
107
+ messages.removeFirst(offset)
108
+ }
109
+
110
+ private func acknowledge(_ count: UInt32) {
111
+ unacknowledged += count
112
+ guard unacknowledged >= 1 << 24 else { return }
113
+ let increment = GrpcFraming.bigEndian(unacknowledged)
114
+ send(
115
+ GrpcFraming.frame(type: 8, flags: 0, stream: 0, payload: increment)
116
+ + GrpcFraming.frame(type: 8, flags: 0, stream: 1, payload: increment))
117
+ unacknowledged = 0
118
+ }
119
+
120
+ private func send(_ bytes: [UInt8]) {
121
+ connection.send(content: Data(bytes), completion: .idempotent)
122
+ }
123
+
124
+ private func finish() {
125
+ guard !ended else { return }
126
+ ended = true
127
+ connection.cancel()
128
+ onEnd()
129
+ }
130
+ }
@@ -0,0 +1,116 @@
1
+ import Foundation
2
+ import IOSurface
3
+ import ObjectiveC
4
+
5
+ // CoreSimulator and SimulatorKit are private Apple frameworks. These selectors
6
+ // match the SimDisplayIOSurfaceRenderable, SimDisplayRenderable, SimScreen and
7
+ // SimScreenProperties protocols that Xcode 27 ships; Apple can change them in
8
+ // any Xcode release.
9
+ @objc protocol SimDisplayIOSurfaceRenderable {
10
+ var framebufferSurface: IOSurface? { get }
11
+ @objc(registerCallbackWithUUID:ioSurfacesChangeCallback:)
12
+ func registerSurfacesCallback(_ uuid: NSUUID, _ callback: @escaping (AnyObject?) -> Void)
13
+ @objc(unregisterIOSurfacesChangeCallbackWithUUID:)
14
+ func unregisterSurfacesCallback(_ uuid: NSUUID)
15
+ }
16
+
17
+ @objc protocol SimDisplayRenderable {
18
+ @objc(registerCallbackWithUUID:damageRectanglesCallback:)
19
+ func registerDamageCallback(_ uuid: NSUUID, _ callback: @escaping (AnyObject?) -> Void)
20
+ @objc(unregisterDamageRectanglesCallbackWithUUID:)
21
+ func unregisterDamageCallback(_ uuid: NSUUID)
22
+ @objc(registerCallbackWithUUID:displayPropertiesChanged:)
23
+ func registerPropertiesCallback(_ uuid: NSUUID, _ callback: @escaping (AnyObject?) -> Void)
24
+ @objc(unregisterDisplayPropertiesChangedCallbackWithUUID:)
25
+ func unregisterPropertiesCallback(_ uuid: NSUUID)
26
+ }
27
+
28
+ @objc protocol SimScreen {
29
+ var screenProperties: SimScreenProperties? { get }
30
+ }
31
+
32
+ // uiOrientation is a UIInterfaceOrientation; the framebuffer stays in the
33
+ // display's native portrait orientation when the device rotates. screenType 0
34
+ // is a built-in display with a digitizer; TV out, CarPlay and resizable
35
+ // displays have other types.
36
+ @objc protocol SimScreenProperties {
37
+ var uiOrientation: UInt32 { get }
38
+ var screenID: UInt32 { get }
39
+ var screenType: UInt { get }
40
+ }
41
+
42
+ typealias SimDisplay = SimDisplayIOSurfaceRenderable & SimDisplayRenderable & SimScreen
43
+
44
+ public enum CoreSimulator {
45
+ /// The Xcode developer directory CoreSimulator loads SimulatorKit from.
46
+ /// Set it once at launch, before any view shows simulator frames.
47
+ public static var developerDir = defaultDeveloperDir
48
+
49
+ private static var defaultDeveloperDir: String {
50
+ ProcessInfo.processInfo.environment["DEVELOPER_DIR"] ?? "/Applications/Xcode.app/Contents/Developer"
51
+ }
52
+
53
+ /// The developer directory `xcode-select -p` reports, or `DEVELOPER_DIR`,
54
+ /// or `/Applications/Xcode.app`, when the selected one has no SimulatorKit.
55
+ public static func selectedDeveloperDir() -> String {
56
+ let process = Process()
57
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/xcode-select")
58
+ process.arguments = ["-p"]
59
+ let out = Pipe()
60
+ process.standardOutput = out
61
+ process.standardError = FileHandle.nullDevice
62
+ guard (try? process.run()) != nil else { return defaultDeveloperDir }
63
+ let path = String(decoding: out.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
64
+ .trimmingCharacters(in: .whitespacesAndNewlines)
65
+ return FileManager.default.fileExists(atPath: simulatorKitPath(path)) ? path : defaultDeveloperDir
66
+ }
67
+
68
+ static func simulatorKitPath(_ developerDir: String) -> String {
69
+ "\(developerDir)/../SharedFrameworks/SimulatorKit.framework/SimulatorKit"
70
+ }
71
+
72
+ static let deviceSet: NSObject? = {
73
+ guard dlopen("/Library/Developer/PrivateFrameworks/CoreSimulator.framework/CoreSimulator", RTLD_NOW) != nil
74
+ else { return nil }
75
+ dlopen(simulatorKitPath(developerDir), RTLD_NOW)
76
+ guard let contextClass = NSClassFromString("SimServiceContext") as? NSObject.Type,
77
+ let context = contextClass.perform(
78
+ NSSelectorFromString("sharedServiceContextForDeveloperDir:error:"), with: developerDir, with: nil
79
+ )?.takeUnretainedValue() as? NSObject
80
+ else { return nil }
81
+ return context.perform(NSSelectorFromString("defaultDeviceSetWithError:"), with: nil)?
82
+ .takeUnretainedValue() as? NSObject
83
+ }()
84
+
85
+ static func device(udid: String) -> NSObject? {
86
+ guard let devices = deviceSet?.value(forKey: "devices") as? [NSObject] else { return nil }
87
+ return devices.first { ($0.value(forKey: "UDID") as? NSUUID)?.uuidString == udid }
88
+ }
89
+
90
+ /// The screen IDs of the device's built-in displays that have a
91
+ /// framebuffer, main display first. An iPhone Duo has two; it is empty until
92
+ /// the device boots.
93
+ public static func screenIDs(udid: String) -> [UInt32] {
94
+ displays(udid: udid).compactMap { display in
95
+ display.screenProperties.flatMap { $0.screenType == 0 ? $0.screenID : nil }
96
+ }
97
+ }
98
+
99
+ static func displays(udid: String) -> [SimDisplay] {
100
+ guard let device = device(udid: udid),
101
+ let io = device.perform(NSSelectorFromString("io"))?.takeUnretainedValue() as? NSObject,
102
+ let ports = io.perform(NSSelectorFromString("ioPorts"))?.takeUnretainedValue() as? [NSObject],
103
+ let surfaceRenderable = objc_getProtocol("SimDisplayIOSurfaceRenderable"),
104
+ let renderable = objc_getProtocol("SimDisplayRenderable"),
105
+ let screen = objc_getProtocol("SimScreen")
106
+ else { return [] }
107
+ let displays = ports.compactMap { port -> SimDisplay? in
108
+ guard let descriptor = port.perform(NSSelectorFromString("descriptor"))?.takeUnretainedValue() as? NSObject,
109
+ descriptor.conforms(to: surfaceRenderable), descriptor.conforms(to: renderable), descriptor.conforms(to: screen)
110
+ else { return nil }
111
+ let display = unsafeBitCast(descriptor, to: SimDisplay.self)
112
+ return display.framebufferSurface == nil ? nil : display
113
+ }
114
+ return displays.sorted { ($0.screenProperties?.screenID ?? .max) < ($1.screenProperties?.screenID ?? .max) }
115
+ }
116
+ }