Files
luma/Sources/LumaCore/VirtualMachines/HostProcessLookup.swift
Ole André Vadla Ravnås 761ee194fb core: Run the Android emulator on Windows
The emulator's control channel was the only thing pulling grpc-swift
in, and with it swift-nio-ssl, which does not build on Windows at all
-- it has no branch for the platform and its PosixPort reaches for
mlock, lstat and readlink. Nothing here wants TLS: the endpoint is
plaintext on the loopback, and only seven calls are ever made.

So the wire is spoken directly. GRPCConnection opens one HTTP/2
stream per call over swift-nio-http2 and frames length-prefixed
protobuf into it; EmulatorControlClient names the seven. That drops
grpc-swift-2, grpc-swift-nio-transport, grpc-swift-protobuf and
swift-nio-ssl from the graph, along with 7840 lines of generated
service stubs. The generated messages stay.

The rest is what running it on Windows turned up:

  - AVD discovery split on "\n" and trimmed only .whitespaces, so
    every name kept a trailing \r and every AVD was dropped.
  - An AVD config spells its paths with backslashes, which URL keeps
    as ordinary characters rather than separators.
  - emulator and adb want .exe, and the SDK sits under LOCALAPPDATA.
  - Port reservation and process lookup move into HostPort and
    HostProcessLookup, which QemuMachine's two copies also wanted.
  - The launcher re-execs into QEMU there, so the discovery ini is
    named after that process; the one naming this launch's debug port
    is taken when the launcher's own is absent.

Frames still arrive through a mapping rather than the socket:
CreateFileMapping backs the same file:// handle the emulator is
handed elsewhere.

A guest without vsock is refused outright on Windows, where the
virtio-serial fallback would ask QEMU for a QMP UNIX socket and be
told "unix sockets are not available on windows".

LUMA_EMULATOR_DISPLAY_ONLY drives launch, control and streaming
without a guest agent. Against a Pixel_2 AVD it reports a 1080x1920
first frame and nine more in three seconds.
2026-09-17 13:13:47 +02:00

140 lines
5.1 KiB
Swift

import Foundation
#if canImport(WinSDK)
import WinSDK
#elseif canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#endif
#if os(Windows) || os(macOS) || os(Linux)
#if os(Windows)
typealias HostProcessID = DWORD
#else
typealias HostProcessID = pid_t
#endif
enum HostProcessLookup {
static func firstProcess(named prefix: String, commandLineContaining needle: String) -> HostProcessID? {
for candidate in processes() where candidate.name.hasPrefix(prefix) {
if let arguments = commandLine(of: candidate.id), arguments.contains(needle) {
return candidate.id
}
}
return nil
}
static func isAlive(_ id: HostProcessID) -> Bool {
#if os(Windows)
guard let handle = OpenProcess(DWORD(PROCESS_QUERY_LIMITED_INFORMATION), false, id) else { return false }
defer { CloseHandle(handle) }
var code: DWORD = 0
guard GetExitCodeProcess(handle, &code) else { return false }
return Int32(code) == STILL_ACTIVE
#else
return kill(id, 0) == 0
#endif
}
static func terminate(_ id: HostProcessID, force: Bool) {
#if os(Windows)
guard let handle = OpenProcess(DWORD(PROCESS_TERMINATE), false, id) else { return }
defer { CloseHandle(handle) }
TerminateProcess(handle, 1)
#else
kill(id, force ? SIGKILL : SIGTERM)
#endif
}
#if os(Windows)
private static func processes() -> [(id: HostProcessID, name: String)] {
let snapshot = CreateToolhelp32Snapshot(DWORD(TH32CS_SNAPPROCESS), 0)
guard snapshot != INVALID_HANDLE_VALUE else { return [] }
defer { CloseHandle(snapshot) }
var entry = PROCESSENTRY32W()
entry.dwSize = DWORD(MemoryLayout<PROCESSENTRY32W>.size)
guard Process32FirstW(snapshot, &entry) else { return [] }
var found: [(id: HostProcessID, name: String)] = []
repeat {
let name = withUnsafeBytes(of: entry.szExeFile) { raw in
String(decodingCString: raw.bindMemory(to: WCHAR.self).baseAddress!, as: UTF16.self)
}
found.append((entry.th32ProcessID, name))
} while Process32NextW(snapshot, &entry)
return found
}
private static func commandLine(of id: HostProcessID) -> String? {
let query = """
(Get-CimInstance Win32_Process -Filter 'ProcessId = \(id)').CommandLine
"""
return runPowerShell(query)
}
private static func runPowerShell(_ command: String) -> String? {
let process = ChildProcess()
process.executableURL = URL(fileURLWithPath: "C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe")
process.arguments = ["-NoProfile", "-NonInteractive", "-Command", command]
let output = Pipe()
process.standardOutput = output
process.standardError = FileHandle.nullDevice
do {
try process.run()
} catch {
return nil
}
let data = output.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
return String(data: data, encoding: .utf8)
}
#elseif os(macOS)
private static func processes() -> [(id: HostProcessID, name: String)] {
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0]
var size = 0
if sysctl(&mib, 4, nil, &size, nil, 0) != 0 { return [] }
let count = size / MemoryLayout<kinfo_proc>.stride
var procs = [kinfo_proc](repeating: kinfo_proc(), count: count)
if sysctl(&mib, 4, &procs, &size, nil, 0) != 0 { return [] }
return procs.map { entry in
var entry = entry
let name = withUnsafeBytes(of: &entry.kp_proc.p_comm) { raw in
String(cString: raw.bindMemory(to: CChar.self).baseAddress!)
}
return (entry.kp_proc.p_pid, name)
}
}
private static func commandLine(of id: HostProcessID) -> String? {
var mib: [Int32] = [CTL_KERN, KERN_PROCARGS2, id]
var size = 0
if sysctl(&mib, 3, nil, &size, nil, 0) != 0 { return nil }
var buffer = [UInt8](repeating: 0, count: size)
if sysctl(&mib, 3, &buffer, &size, nil, 0) != 0 { return nil }
let bytes = buffer.prefix(size).map { $0 == 0 ? UInt8(ascii: " ") : $0 }
return String(decoding: bytes, as: UTF8.self)
}
#else
private static func processes() -> [(id: HostProcessID, name: String)] {
guard let entries = try? FileManager.default.contentsOfDirectory(atPath: "/proc") else { return [] }
return entries.compactMap { entry in
guard let id = pid_t(entry),
let name = try? String(contentsOfFile: "/proc/\(entry)/comm", encoding: .utf8)
else { return nil }
return (id, name.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
private static func commandLine(of id: HostProcessID) -> String? {
guard let raw = FileManager.default.contents(atPath: "/proc/\(id)/cmdline") else { return nil }
return String(decoding: raw.map { $0 == 0 ? UInt8(ascii: " ") : $0 }, as: UTF8.self)
}
#endif
}
#endif