1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.benchmark.darwin.gradle
18 
19 import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
20 import androidx.benchmark.darwin.gradle.xcode.SimulatorRuntimes
21 import java.io.ByteArrayInputStream
22 import java.io.ByteArrayOutputStream
23 import org.gradle.process.ExecOperations
24 
25 /** Controls XCode Simulator instances. */
26 class XCodeSimCtrl(private val execOperations: ExecOperations, private val destination: String) {
27 
28     private var destinationDesc: String? = null
29     private var deviceId: String? = null
30 
31     // A device type looks something like
32     // platform=iOS Simulator,name=iPhone 13,OS=15.2
33 
34     fun start(block: (destinationDesc: String) -> Unit) {
35         try {
36             val instance = boot(destination, execOperations)
37             destinationDesc = instance.destinationDesc
38             deviceId = instance.deviceId
39             block(destinationDesc!!)
40         } finally {
41             val id = deviceId
42             if (id != null) {
43                 shutDownAndDelete(execOperations, id)
44             }
45         }
46     }
47 
48     companion object {
49         private const val PLATFORM_KEY = "platform"
50         private const val NAME_KEY = "name"
51         private const val RUNTIME_KEY = "OS"
52         private const val IOS_SIMULATOR = "iOS Simulator"
53         private const val IPHONE_PRODUCT_FAMILY = "iPhone"
54 
55         /** Simulator metadata */
56         internal data class SimulatorInstance(
57             /* The full device descriptor. */
58             val destinationDesc: String,
59             /* The unique device UUID if we end up booting the device. */
60             val deviceId: String? = null
61         )
62 
63         internal fun boot(destination: String, execOperations: ExecOperations): SimulatorInstance {
64             val parsed = parse(destination)
65             return when (platform(parsed)) {
66                 // Simulator needs to be booted up.
67                 IOS_SIMULATOR -> bootSimulator(destination, parsed, execOperations)
68                 // For other destinations, we don't have much to do.
69                 else -> SimulatorInstance(destinationDesc = destination)
70             }
71         }
72 
73         private fun discoverSimulatorRuntimeVersion(execOperations: ExecOperations): String? {
74             val json =
75                 executeCommand(
76                     execOperations,
77                     listOf("xcrun", "simctl", "list", "runtimes", "--json")
78                 )
79             val simulatorRuntimes = GsonHelpers.gson().fromJson(json, SimulatorRuntimes::class.java)
80             // There is usually one version of the simulator runtime available per xcode version
81             val supported =
82                 simulatorRuntimes.runtimes.firstOrNull { runtime ->
83                     runtime.isAvailable &&
84                         runtime.supportedDeviceTypes.any { deviceType ->
85                             deviceType.productFamily == IPHONE_PRODUCT_FAMILY
86                         }
87                 }
88             return supported?.version
89         }
90 
91         private fun bootSimulator(
92             destination: String,
93             parsed: Map<String, String>,
94             execOperations: ExecOperations
95         ): SimulatorInstance {
96             val deviceName = deviceName(parsed)
97             val supported = discoverSimulatorRuntimeVersion(execOperations)
98             // While this is not strictly correct, these versions should be pretty close.
99             val runtimeVersion = supported ?: runtimeVersion(parsed)
100             check(deviceName != null && runtimeVersion != null) {
101                 "Invalid destination spec: $destination"
102             }
103             val deviceId =
104                 executeCommand(
105                     execOperations,
106                     listOf(
107                         "xcrun",
108                         "simctl",
109                         "create",
110                         deviceName, // Use the deviceName as the name
111                         deviceName,
112                         "iOS$runtimeVersion"
113                     )
114                 )
115             check(deviceId.isNotBlank()) {
116                 "Invalid device id for simulator: $deviceId (Destination: $destination)"
117             }
118             executeCommand(execOperations, listOf("xcrun", "simctl", "boot", deviceId))
119             // Return a simulator instance with the new descriptor + device id
120             return SimulatorInstance(destinationDesc = "id=$deviceId", deviceId = deviceId)
121         }
122 
123         internal fun shutDownAndDelete(execOperations: ExecOperations, deviceId: String) {
124             // Cleans up the instance of the simulator that was booted up.
125             executeCommand(execOperations, listOf("xcrun", "simctl", "shutdown", deviceId))
126             executeCommand(execOperations, listOf("xcrun", "simctl", "delete", deviceId))
127         }
128 
129         private fun executeCommand(execOperations: ExecOperations, args: List<String>): String {
130             val output = ByteArrayOutputStream()
131             output.use {
132                 execOperations.exec { spec ->
133                     spec.commandLine = args
134                     spec.standardOutput = output
135                 }
136                 val input = ByteArrayInputStream(output.toByteArray())
137                 return input.use {
138                     // Trimming is important here, otherwise ExecOperations encodes the string
139                     // with shell specific escape sequences which mangle the device
140                     input.reader().readText().trim()
141                 }
142             }
143         }
144 
145         private fun platform(parsed: Map<String, String>): String? {
146             return parsed[PLATFORM_KEY]
147         }
148 
149         private fun deviceName(parsed: Map<String, String>): String? {
150             return parsed[NAME_KEY]
151         }
152 
153         private fun runtimeVersion(parsed: Map<String, String>): String? {
154             return parsed[RUNTIME_KEY]
155         }
156 
157         private fun parse(destination: String): Map<String, String> {
158             return destination
159                 .splitToSequence(",")
160                 .map { split ->
161                     check(split.contains("=")) { "Invalid destination spec: $destination" }
162                     val (key, value) = split.split("=", limit = 2)
163                     key.trim() to value.trim()
164                 }
165                 .toMap()
166         }
167     }
168 }
169