Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ extension ConnectedDevice: Device {
}

var type: DeviceType {
guard let product = product else {
guard let product else {
// In case the product name is not available, fall back to checking the serial.
return serial.contains("emulator") ? .simulator : .device
}

return product.contains("sdk_gphone") ? .simulator : .device
return product.hasPrefix("sdk_") ? .simulator : .device
}

var connection: Connection {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension ShellCommand where Self == EmulatorCommand {
}

enum EmulatorCommand {
case startDevice(name: String)
case startDevice(name: String, reportConsolePort: Int)
}

extension EmulatorCommand: ShellCommand {
Expand All @@ -26,8 +26,20 @@ extension EmulatorCommand: ShellCommand {

var arguments: [String] {
switch self {
case .startDevice(let name):
return ["-avd", name, "&"]
case .startDevice(let name, let reportConsolePort):
return [
"-avd",
name,
"-report-console",
"tcp:\(reportConsolePort)",
">/dev/null",
"2>&1",
"&",
"nc",
"-l",
String(reportConsolePort),
"</dev/null"
]
}
}
}
21 changes: 7 additions & 14 deletions TophatModules/Sources/AndroidDeviceKit/ProxyVirtualDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ private extension ProxyVirtualDevice {
self.connectedDevice = connectedDevice
}

func update() async {
let connectedDevices = Adb.listDevices()
let virtualDeviceNameMappings = await connectedDevices.mappedToVirtualDeviceNames()
self.connectedDevice = virtualDeviceNameMappings.connectedDevice(for: virtualDevice)
func update(serial: String) {
self.connectedDevice = Adb.listDevices().first { $0.serial == serial }
}
}
}
Expand Down Expand Up @@ -73,22 +71,17 @@ extension ProxyVirtualDevice: Device {

func boot() async throws {
do {
try Emulator.start(name: name)
let serial = try await Emulator.start(name: name)
try Adb.wait(forSerial: serial)

await connectedDeviceStore.update(serial: serial)
} catch {
throw DeviceError.failedToBoot
}

await connectedDeviceStore.update()

guard let connectedDevice = await connectedDeviceStore.connectedDevice else {
guard await connectedDeviceStore.connectedDevice != nil else {
throw await DeviceError.deviceNotAvailable(state: state)
}

do {
try Adb.wait(for: connectedDevice)
} catch {
throw DeviceError.failedToBoot
}
}

func openLogs() async throws {
Expand Down
8 changes: 2 additions & 6 deletions TophatModules/Sources/AndroidDeviceKit/Utilities/Adb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,8 @@ struct Adb {
return value
}

static func wait(for device: ConnectedDevice) throws {
try run(command: .adb(.waitForDevice(serial: device.serial)), log: log)

// Artificially give Emulator time to communicate with adb
// TODO: Figure out how Android Studio does it without sleeping
sleep(3)
static func wait(forSerial serial: String) throws {
try run(command: .adb(.waitForDevice(serial: serial)), log: log)
}

private static func firstLine(of command: ShellCommand) throws -> String? {
Expand Down
64 changes: 59 additions & 5 deletions TophatModules/Sources/AndroidDeviceKit/Utilities/Emulator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,67 @@
//

import Foundation
import Network
import ShellKit

struct Emulator {
static func start(name: String) throws {
try run(command: .emulator(.startDevice(name: name)), log: log)
// Artificially give Emulator time to communicate with adb
// TODO: Figure out how Android Studio does it without sleeping
sleep(5)
private static let portSequence = PortSequence()

/// - returns: The serial of the device that was booted.
static func start(name: String) async throws -> String {
let output = try await portSequence.withAvailablePort { reportConsolePort in
try run(
command: .emulator(.startDevice(name: name, reportConsolePort: reportConsolePort)),
log: log
)
}

guard let consolePort = Int(output.trimmingCharacters(in: .whitespacesAndNewlines)) else {
throw EmulatorError.failedToReadPort
}

return "emulator-\(consolePort)"
}
}

enum EmulatorError: Error {
case failedToReadPort
case noAvailablePort
}

private actor PortSequence {
private let startPort: UInt16 = 49152
private let maxAttempts = 20

private var inFlight: Set<UInt16> = []

func withAvailablePort<T>(_ body: (Int) async throws -> T) async throws -> T {
guard let port = reservePort() else {
throw EmulatorError.noAvailablePort
}

defer { inFlight.remove(port) }
return try await body(Int(port))
}

private func reservePort() -> UInt16? {
for attempt in 0..<maxAttempts {
let port = startPort + UInt16(attempt)

if !inFlight.contains(port), isAvailable(port) {
inFlight.insert(port)
return port
}
}

return nil
}

private func isAvailable(_ port: UInt16) -> Bool {
guard let listener = try? NWListener(using: .tcp, on: NWEndpoint.Port(rawValue: port)!) else {
return false
}
listener.cancel()
return true
}
}
Loading