|
1 | 1 | import ArgumentParser |
2 | 2 | import Darwin |
3 | 3 | import Foundation |
| 4 | +import ImageIO |
4 | 5 | import TritonKitShared |
5 | 6 |
|
6 | 7 | func runSimpleHostCommand( |
@@ -89,6 +90,87 @@ func runHostCommandCapturingStdoutArtifact( |
89 | 90 | } |
90 | 91 | } |
91 | 92 |
|
| 93 | +func runHostSimulatorScreenshotCommand( |
| 94 | + simulator: String, |
| 95 | + command: TKHostCommand, |
| 96 | + outputPath: String, |
| 97 | + outputFormat: ClientOutputFormat |
| 98 | +) throws { |
| 99 | + do { |
| 100 | + let result = try runHostCommand(command) |
| 101 | + let stderr = result.stderr.trimmingCharacters(in: .whitespacesAndNewlines) |
| 102 | + let size = imagePixelSize(path: outputPath) |
| 103 | + let output = HostSimulatorScreenshotOutput( |
| 104 | + ok: true, |
| 105 | + action: "sim.screenshot", |
| 106 | + runtimeScope: "host-simulator", |
| 107 | + target: "sim:\(simulator)", |
| 108 | + tool: command.executable, |
| 109 | + exitCode: result.exitCode, |
| 110 | + riskLevel: command.riskLevel.rawValue, |
| 111 | + sourceCommand: result.sourceCommand, |
| 112 | + stdoutTruncated: result.stdoutTruncated, |
| 113 | + stderrTruncated: result.stderrTruncated, |
| 114 | + stderr: stderr.isEmpty ? nil : stderr, |
| 115 | + artifact: outputPath, |
| 116 | + pixelWidth: size?.width, |
| 117 | + pixelHeight: size?.height, |
| 118 | + display: parseSimctlScreenshotDisplayMetadata(stderr: result.stderr), |
| 119 | + orientationPolicy: "raw-framebuffer", |
| 120 | + orientationNote: "simctl io screenshot writes the simulator display framebuffer as provided by CoreSimulator. Triton reports this as raw framebuffer orientation and does not rotate iPad screenshots yet.", |
| 121 | + note: "Host-side simulator screenshot was written with raw framebuffer orientation metadata." |
| 122 | + ) |
| 123 | + switch outputFormat { |
| 124 | + case .json: |
| 125 | + print(try encodeJSON(output)) |
| 126 | + case .text: |
| 127 | + print(outputPath) |
| 128 | + print(output.orientationNote) |
| 129 | + } |
| 130 | + } catch { |
| 131 | + try failHostCommand(error, outputFormat: outputFormat) |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +func parseSimctlScreenshotDisplayMetadata(stderr: String) -> HostSimulatorScreenshotDisplayMetadata { |
| 136 | + let line = stderr |
| 137 | + .split(whereSeparator: \.isNewline) |
| 138 | + .map(String.init) |
| 139 | + .first { $0.contains("Defaulting to display:") || $0.contains("display:") } |
| 140 | + guard let line else { |
| 141 | + return HostSimulatorScreenshotDisplayMetadata(rawLine: nil, displayID: nil, screenID: nil, name: nil) |
| 142 | + } |
| 143 | + |
| 144 | + let displayID = value(after: "display:", before: "(", in: line) |
| 145 | + let screenID = value(after: "screenID:", before: ",", in: line) |
| 146 | + let name = value(after: "name:", before: ")", in: line) |
| 147 | + return HostSimulatorScreenshotDisplayMetadata( |
| 148 | + rawLine: line, |
| 149 | + displayID: displayID, |
| 150 | + screenID: screenID, |
| 151 | + name: name |
| 152 | + ) |
| 153 | +} |
| 154 | + |
| 155 | +func imagePixelSize(path: String) -> (width: Int, height: Int)? { |
| 156 | + guard let source = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, nil), |
| 157 | + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], |
| 158 | + let width = properties[kCGImagePropertyPixelWidth] as? Int, |
| 159 | + let height = properties[kCGImagePropertyPixelHeight] as? Int |
| 160 | + else { |
| 161 | + return nil |
| 162 | + } |
| 163 | + return (width, height) |
| 164 | +} |
| 165 | + |
| 166 | +private func value(after marker: String, before terminator: String, in line: String) -> String? { |
| 167 | + guard let markerRange = line.range(of: marker) else { return nil } |
| 168 | + let tail = line[markerRange.upperBound...] |
| 169 | + let end = tail.range(of: terminator)?.lowerBound ?? tail.endIndex |
| 170 | + let value = tail[..<end].trimmingCharacters(in: .whitespacesAndNewlines) |
| 171 | + return value.isEmpty ? nil : value |
| 172 | +} |
| 173 | + |
92 | 174 | private struct HostPipeDrainResult { |
93 | 175 | let data: Data |
94 | 176 | let bytes: Int |
|
0 commit comments