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.
140 lines
5.1 KiB
Swift
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
|