Isolate multi-VM setup with deterministic device targeting (#119)

This commit is contained in:
Lakr
2026-03-06 12:47:30 +08:00
committed by GitHub
parent 5c2bce03dd
commit e08850a45e
11 changed files with 252 additions and 59 deletions

View File

@@ -544,7 +544,7 @@ class VPhoneControl {
let timeoutSeconds = max(Int(timeout.rounded()), 1)
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + timeout) { [weak self] in
guard let self else { return }
guard let pending = self.removePending(id: id) else { return }
guard let pending = removePending(id: id) else { return }
pending.handler(.failure(ControlError.requestTimedOut(type: type, seconds: timeoutSeconds)))
}
}

View File

@@ -195,7 +195,7 @@ class VPhoneFileBrowserModel {
transferName = nil
await refresh()
// Set error after refresh so refresh() doesn't clear it before the alert fires.
if let e = uploadError { self.error = e }
if let e = uploadError { error = e }
}
func createNewFolder(name: String) async {

View File

@@ -13,6 +13,9 @@ import Virtualization
/// Minimum host OS for PV=3: macOS 15.0 (Sequoia)
///
enum VPhoneHardware {
/// Fixed CPID for the current vphone hardware descriptor.
static let udidChipID: UInt32 = 0xFE01
static func createModel() throws -> VZMacHardwareModel {
// platformVersion=3, boardID=0x90, ISA=2 matches vresearch101
let desc = Dynamic._VZMacHardwareModelDescriptor()

View File

@@ -46,7 +46,9 @@ class VPhoneLocationProvider: NSObject {
private var replayTask: Task<Void, Never>?
private var replayName: String?
var isReplaying: Bool { replayTask != nil }
var isReplaying: Bool {
replayTask != nil
}
init(control: VPhoneControl) {
self.control = control
@@ -135,7 +137,7 @@ class VPhoneLocationProvider: NSObject {
var index = 0
while !Task.isCancelled {
let point = points[index]
self.sendSimulatedLocation(
sendSimulatedLocation(
latitude: point.latitude,
longitude: point.longitude,
altitude: point.altitude,

View File

@@ -27,6 +27,12 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
var kernelDebugPort: Int = 5909
}
private struct DeviceIdentity {
let cpidHex: String
let ecidHex: String
let udid: String
}
init(options: Options) throws {
// --- Hardware model (PV=3) ---
let hwModel = try VPhoneHardware.createModel()
@@ -36,17 +42,34 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
let platform = VZMacPlatformConfiguration()
// Persist machineIdentifier for stable ECID
let machineIdentifier: VZMacMachineIdentifier
if let savedData = try? Data(contentsOf: options.machineIDURL),
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData)
{
platform.machineIdentifier = savedID
machineIdentifier = savedID
print("[vphone] Loaded machineIdentifier (ECID stable)")
} else {
let newID = VZMacMachineIdentifier()
platform.machineIdentifier = newID
machineIdentifier = newID
try newID.dataRepresentation.write(to: options.machineIDURL)
print("[vphone] Created new machineIdentifier -> \(options.machineIDURL.lastPathComponent)")
}
platform.machineIdentifier = machineIdentifier
if let identity = Self.resolveDeviceIdentity(machineIdentifier: machineIdentifier) {
print("[vphone] ECID: 0x\(identity.ecidHex)")
print("[vphone] Predicted UDID: \(identity.udid)")
do {
let outputURL = try Self.writeUDIDPrediction(
identity: identity, machineIDURL: options.machineIDURL
)
print("[vphone] Wrote UDID prediction: \(outputURL.path)")
} catch {
print("[vphone] Warning: failed to write udid-prediction.txt: \(error)")
}
} else {
print("[vphone] Warning: failed to resolve ECID from machineIdentifier")
}
let auxStorage = try VZMacAuxiliaryStorage(
creatingStorageAt: options.nvramURL,
@@ -204,6 +227,39 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
}
}
private static func resolveDeviceIdentity(machineIdentifier: VZMacMachineIdentifier)
-> DeviceIdentity?
{
let ecidValue: UInt64? = if let ecid = Dynamic(machineIdentifier)._ECID.asUInt64 {
ecid
} else if let ecidNumber = Dynamic(machineIdentifier)._ECID.asObject as? NSNumber {
ecidNumber.uint64Value
} else {
nil
}
guard let ecidValue else { return nil }
let cpidHex = String(format: "%08X", VPhoneHardware.udidChipID)
let ecidHex = String(format: "%016llX", ecidValue)
let udid = "\(cpidHex)-\(ecidHex)"
return DeviceIdentity(cpidHex: cpidHex, ecidHex: ecidHex, udid: udid)
}
private static func writeUDIDPrediction(identity: DeviceIdentity, machineIDURL: URL) throws -> URL {
let outputURL = machineIDURL.deletingLastPathComponent().appendingPathComponent(
"udid-prediction.txt"
)
let content = """
UDID=\(identity.udid)
CPID=0x\(identity.cpidHex)
ECID=0x\(identity.ecidHex)
MACHINE_IDENTIFIER=\(machineIDURL.lastPathComponent)
"""
try content.write(to: outputURL, atomically: true, encoding: .utf8)
return outputURL
}
// MARK: - Battery
/// Update the synthetic battery charge and connectivity at runtime.