Files
Ole André Vadla Ravnås 11c9de137d emulator: Prewarm backend before listing AVDs
availability() reads the AVD cache that only reloadAVDs()
fills, so the check found no AVD until prewarm() ran. Call
prewarm() first.
2026-09-30 15:12:13 +02:00

169 lines
6.7 KiB
Swift

import Foundation
import Frida
import LumaCore
// Drives the whole Android-emulator path end to end: launch an AVD through the backend, hand its
// reported stub to frida-core, inject the barebone agent, and run one script. Env:
// LUMA_BAREBONE_AGENT (required) path to the linux-arm64 barebone agent binary
// LUMA_EMULATOR_AVD (optional) AVD name; defaults to the first discovered
// LUMA_EMULATOR_SCRIPT (optional) agent source; defaults to send(1 + 1)
func fail(_ message: String) -> Never {
FileHandle.standardError.write(Data("FAIL: \(message)\n".utf8))
exit(1)
}
let started = Date()
func note(_ message: String) {
let elapsed = String(format: "%6.1fs", Date().timeIntervalSince(started))
FileHandle.standardError.write(Data("[check \(elapsed)] \(message)\n".utf8))
}
@MainActor
func checkDisplay(_ machine: any VirtualMachine) async throws {
guard case .frames(let source)? = machine.display else { fail("machine published no frame source") }
let deadline = Date().addingTimeInterval(60)
while source.revision == 0, Date() < deadline {
try await Task.sleep(for: .milliseconds(100))
}
guard let frame = source.frame, source.revision > 0 else {
fail("no frame arrived within 60s")
}
note("first frame \(frame.width)x\(frame.height) stride \(frame.stride)")
let settled = source.revision
try await Task.sleep(for: .seconds(3))
guard source.revision > settled else { fail("the stream stalled after its first frame") }
note("OK — \(source.revision - settled) further frames in 3s")
}
@MainActor
func run() async throws {
let env = ProcessInfo.processInfo.environment
let displayOnly = env["LUMA_EMULATOR_DISPLAY_ONLY"] != nil
guard let agentPath = env["LUMA_BAREBONE_AGENT"] ?? (displayOnly ? "" : nil) else {
fail("set LUMA_BAREBONE_AGENT to the barebone agent binary")
}
let code = env["LUMA_EMULATOR_SCRIPT"] ?? "send(1 + 1);"
func avds(of template: VirtualMachineTemplate) -> [String] {
guard case .choice(let options, _) = template.parameters.first?.kind else { return [] }
return options.map(\.id)
}
let backend = AndroidEmulatorBackend()
await backend.prewarm()
let templates = backend.templates
let wanted = env["LUMA_EMULATOR_AVD"]
guard let template = templates.first(where: { wanted.map(avds(of:)($0).contains) ?? false })
?? templates.first
else { fail("no Android emulator template") }
let availability = backend.availability(for: template)
guard availability.isAvailable else { fail(availability.reason ?? "backend unavailable") }
let avdName = wanted ?? avds(of: template).first ?? ""
guard !avdName.isEmpty else { fail("no AVD to launch") }
note("launching AVD \(avdName) (\(template.architecture.rawValue))…")
let request = VirtualMachineLaunchRequest(
id: UUID(),
template: template,
name: "emulator-check",
parameters: [AndroidEmulatorBackend.avdParameterID: .text(avdName)],
agentPath: nil,
storageDirectory: FileManager.default.temporaryDirectory,
resumesFromReadySnapshot: false
)
let machine = try await backend.launch(request)
note("emulator launched; stub reported")
if displayOnly {
try await checkDisplay(machine)
await machine.shutDown()
return
}
do {
guard case .androidEmulator(let host, let port, let pid)? = machine.debugStub else { fail("machine did not report an androidEmulator stub") }
guard case .linuxImage(let kernel)? = machine.kernelSymbols else { fail("machine has no kernel image") }
let transport: BareboneInjectingTransportConfig
switch machine.agentTransport {
case .pipeVsock(let socketPath)?:
transport = BareboneVsockPipeTransportConfig(socketPath: socketPath.path)
note("transport pipe-vsock")
case .hostlink(let qmp, let bus, let fabric)?:
let fabricConfig: Frida.BareboneHostlinkFabric
switch fabric {
case .ports: fabricConfig = BareboneHostlinkPortsFabric()
case .ecam(let base): fabricConfig = BareboneHostlinkEcamFabric(ecam: base)
case .mmio: fabricConfig = BareboneHostlinkMmioFabric()
}
transport = BareboneHostlinkTransportConfig(qmp: qmp, bus: bus, fabric: fabricConfig)
note("transport hostlink virtio-pci")
default:
fail("machine did not report a supported transport")
}
note("stub host=\(host) port=\(port) pid=\(pid)")
note("kernel \(kernel.path)")
let agentData = try Data(contentsOf: URL(fileURLWithPath: agentPath))
let kernelSymbols = try LinuxKernelImage.open(path: kernel.path)
note("kernel names vsock: \(kernelSymbols.hasSymbol(name: "vsock"))")
let config = BareboneConfig(
connection: BareboneConnectionConfig(host: host, port: UInt(port), pid: pid, flavor: .androidEmulator),
agent: BareboneInjectedAgentConfig(
image: [UInt8](agentData),
transport: transport
),
image: BareboneLinuxKernelConfig(kernel: kernelSymbols),
kernel: .linux
)
note("connecting frida-core (parse kallsyms + instrument + inject)…")
let manager = DeviceManager()
let device = try await manager.addBareboneDevice(config: config, id: "emulator-check", name: "emulator-check")
note("device added")
let session = try await device.attach(pid: 0)
note("session attached")
let script = try await session.createScript(source: code)
// Subscribe before load: the script's send() fires as it loads, so a consumer started
// afterwards can miss it. A detached task begins draining events first, then load runs.
let firstMessage = Task { () -> String? in
for await event in script.events {
if case .message(let message, _) = event {
return "\(message)"
}
}
return nil
}
note("script created; loading…")
try await script.load()
let timeout = Task { () -> String? in
try? await Task.sleep(nanoseconds: 60_000_000_000)
firstMessage.cancel()
return nil
}
let message = await firstMessage.value
timeout.cancel()
note("MESSAGE \(message ?? "<none — timed out>")")
try? await script.unload()
try? await manager.close()
note("OK — end to end works")
}
await machine.shutDown()
}
do {
try await run()
} catch {
if ProcessInfo.processInfo.environment["FRIDA_BAREBONE_DUMP_TMO"] != nil {
try? await Task.sleep(nanoseconds: 6_000_000_000)
}
fail("\(error)")
}