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