mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 04:59:05 +08:00
Fix: Replace emoji and non-ASCII characters (#177)
This commit is contained in:
424
research/VPhoneVirtualMachineRefactored.swift
Normal file
424
research/VPhoneVirtualMachineRefactored.swift
Normal file
@@ -0,0 +1,424 @@
|
||||
import Dynamic
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode.
|
||||
@MainActor
|
||||
class VPhoneVirtualMachineRefactored: NSObject, VZVirtualMachineDelegate {
|
||||
let virtualMachine: VZVirtualMachine
|
||||
/// ECID hex string resolved from machineIdentifier (e.g. "0x0012345678ABCDEF").
|
||||
let ecidHex: String?
|
||||
/// Read handle for VM serial output.
|
||||
private var serialOutputReadHandle: FileHandle?
|
||||
/// Synthetic battery source for runtime charge/connectivity updates.
|
||||
private var batterySource: AnyObject?
|
||||
|
||||
struct Configuration {
|
||||
var romURL: URL
|
||||
var nvramURL: URL
|
||||
var machineIDURL: URL
|
||||
var diskURL: URL
|
||||
var cpuCount: Int = 8
|
||||
var memorySize: UInt64 = 8 * 1024 * 1024 * 1024
|
||||
var sepStorageURL: URL
|
||||
var sepRomURL: URL
|
||||
var screenConfiguration: ScreenConfiguration = .default
|
||||
var kernelDebugPort: Int?
|
||||
}
|
||||
|
||||
struct ScreenConfiguration {
|
||||
let width: Int
|
||||
let height: Int
|
||||
let pixelsPerInch: Int
|
||||
let scale: Double
|
||||
|
||||
static let `default` = ScreenConfiguration(
|
||||
width: 1290,
|
||||
height: 2796,
|
||||
pixelsPerInch: 460,
|
||||
scale: 3.0
|
||||
)
|
||||
}
|
||||
|
||||
private struct DeviceIdentity {
|
||||
let cpidHex: String
|
||||
let ecidHex: String
|
||||
let udid: String
|
||||
}
|
||||
|
||||
// MARK: - Battery Connectivity States
|
||||
|
||||
private enum BatteryConnectivity {
|
||||
static let charging = 1
|
||||
static let disconnected = 2
|
||||
}
|
||||
|
||||
init(options: Configuration) throws {
|
||||
// Create hardware model
|
||||
let hardwareModel = try VPhoneHardware.createModel()
|
||||
print("[vphone] PV=3 hardware model: isSupported = true")
|
||||
|
||||
// Configure platform
|
||||
let platform = try configurePlatform(
|
||||
machineIDURL: options.machineIDURL,
|
||||
nvramURL: options.nvramURL,
|
||||
hardwareModel: hardwareModel
|
||||
)
|
||||
|
||||
// Resolve device identity
|
||||
if let machineIdentifier = platform.machineIdentifier {
|
||||
ecidHex = Self.resolveDeviceIdentity(machineIdentifier: machineIdentifier)?.ecidHex
|
||||
} else {
|
||||
ecidHex = nil
|
||||
}
|
||||
|
||||
// Create bootloader
|
||||
let bootloader = createBootloader(romURL: options.romURL)
|
||||
|
||||
// Build VM configuration
|
||||
let config = buildConfiguration(
|
||||
options: options,
|
||||
hardwareModel: hardwareModel,
|
||||
platform: platform,
|
||||
bootloader: bootloader
|
||||
)
|
||||
|
||||
// Validate configuration
|
||||
try config.validate()
|
||||
print("[vphone] Configuration validated")
|
||||
|
||||
virtualMachine = VZVirtualMachine(configuration: config)
|
||||
super.init()
|
||||
virtualMachine.delegate = self
|
||||
|
||||
// Setup serial output forwarding
|
||||
if let readHandle = serialOutputReadHandle {
|
||||
readHandle.readabilityHandler = { handle in
|
||||
let data = handle.availableData
|
||||
guard !data.isEmpty else { return }
|
||||
FileHandle.standardOutput.write(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Platform Configuration
|
||||
|
||||
private func configurePlatform(
|
||||
machineIDURL: URL,
|
||||
nvramURL: URL,
|
||||
hardwareModel: VZMacHardwareModel
|
||||
) throws -> VZMacPlatformConfiguration {
|
||||
let platform = VZMacPlatformConfiguration()
|
||||
platform.hardwareModel = hardwareModel
|
||||
|
||||
// Load or create machine identifier
|
||||
let machineIdentifier = loadOrCreateMachineIdentifier(at: machineIDURL)
|
||||
platform.machineIdentifier = machineIdentifier
|
||||
|
||||
// Create auxiliary storage (NVRAM)
|
||||
let auxiliaryStorage = try VZMacAuxiliaryStorage(
|
||||
creatingStorageAt: nvramURL,
|
||||
hardwareModel: hardwareModel,
|
||||
options: .allowOverwrite
|
||||
)
|
||||
platform.auxiliaryStorage = auxiliaryStorage
|
||||
|
||||
// Configure boot args for serial output
|
||||
setBootArgsSerialOutput(auxiliaryStorage)
|
||||
|
||||
return platform
|
||||
}
|
||||
|
||||
private func loadOrCreateMachineIdentifier(at url: URL) -> VZMacMachineIdentifier {
|
||||
if let savedData = try? Data(contentsOf: url),
|
||||
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData)
|
||||
{
|
||||
print("[vphone] Loaded machineIdentifier (ECID stable)")
|
||||
return savedID
|
||||
}
|
||||
|
||||
let newID = VZMacMachineIdentifier()
|
||||
try? newID.dataRepresentation.write(to: url)
|
||||
print("[vphone] Created new machineIdentifier -> \(url.lastPathComponent)")
|
||||
return newID
|
||||
}
|
||||
|
||||
private func setBootArgsSerialOutput(_ auxiliaryStorage: VZMacAuxiliaryStorage) {
|
||||
let bootArgs = "serial=3 debug=0x104c04"
|
||||
guard let bootArgsData = bootArgs.data(using: .utf8) else { return }
|
||||
|
||||
let success = Dynamic(auxiliaryStorage)
|
||||
._setDataValue(bootArgsData, forNVRAMVariableNamed: "boot-args", error: nil)
|
||||
.asBool ?? false
|
||||
|
||||
if success {
|
||||
print("[vphone] NVRAM boot-args: \(bootArgs)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Bootloader
|
||||
|
||||
private func createBootloader(romURL: URL) -> VZMacOSBootLoader {
|
||||
let bootloader = VZMacOSBootLoader()
|
||||
Dynamic(bootloader)._setROMURL(romURL)
|
||||
return bootloader
|
||||
}
|
||||
|
||||
// MARK: - Configuration Builder
|
||||
|
||||
private func buildConfiguration(
|
||||
options: Configuration,
|
||||
hardwareModel _: VZMacHardwareModel,
|
||||
platform: VZMacPlatformConfiguration,
|
||||
bootloader: VZMacOSBootLoader
|
||||
) -> VZVirtualMachineConfiguration {
|
||||
let config = VZVirtualMachineConfiguration()
|
||||
config.bootLoader = bootloader
|
||||
config.platform = platform
|
||||
config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
|
||||
config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
|
||||
|
||||
// Configure each subsystem
|
||||
configureDisplay(&config, screen: options.screenConfiguration)
|
||||
configureAudio(&config)
|
||||
configureStorage(&config, diskURL: options.diskURL)
|
||||
configureNetwork(&config)
|
||||
configureSerialPort(&config)
|
||||
configureInputDevices(&config)
|
||||
configureSocketDevice(&config)
|
||||
configureBattery(&config)
|
||||
configureDebugStub(&config, port: options.kernelDebugPort)
|
||||
configureSEP(&config, options: options)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
private func configureDisplay(_ config: inout VZVirtualMachineConfiguration, screen: ScreenConfiguration) {
|
||||
let graphicsConfiguration = VZMacGraphicsDeviceConfiguration()
|
||||
let displayConfiguration = VZMacGraphicsDisplayConfiguration(
|
||||
widthInPixels: screen.width,
|
||||
heightInPixels: screen.height,
|
||||
pixelsPerInch: screen.pixelsPerInch
|
||||
)
|
||||
graphicsConfiguration.displays = [displayConfiguration]
|
||||
config.graphicsDevices = [graphicsConfiguration]
|
||||
}
|
||||
|
||||
private func configureAudio(_ config: inout VZVirtualMachineConfiguration) {
|
||||
let soundDevice = VZVirtioSoundDeviceConfiguration()
|
||||
let inputStream = VZVirtioSoundDeviceInputStreamConfiguration()
|
||||
inputStream.source = VZHostAudioInputStreamSource()
|
||||
|
||||
let outputStream = VZVirtioSoundDeviceOutputStreamConfiguration()
|
||||
outputStream.sink = VZHostAudioOutputStreamSink()
|
||||
|
||||
soundDevice.streams = [inputStream, outputStream]
|
||||
config.audioDevices = [soundDevice]
|
||||
}
|
||||
|
||||
private func configureStorage(_ config: inout VZVirtualMachineConfiguration, diskURL: URL) {
|
||||
guard FileManager.default.fileExists(atPath: diskURL.path) else {
|
||||
print("[vphone] Warning: Disk image not found at \(diskURL.path)")
|
||||
return
|
||||
}
|
||||
|
||||
let attachment = try? VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
|
||||
let storageDevice = VZVirtioBlockDeviceConfiguration(attachment: attachment!)
|
||||
config.storageDevices = [storageDevice]
|
||||
}
|
||||
|
||||
private func configureNetwork(_ config: inout VZVirtualMachineConfiguration) {
|
||||
let networkDevice = VZVirtioNetworkDeviceConfiguration()
|
||||
networkDevice.attachment = VZNATNetworkDeviceAttachment()
|
||||
config.networkDevices = [networkDevice]
|
||||
}
|
||||
|
||||
private func configureSerialPort(_ config: inout VZVirtualMachineConfiguration) {
|
||||
guard let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject as? VZSerialPortConfiguration else {
|
||||
return
|
||||
}
|
||||
|
||||
let inputPipe = Pipe()
|
||||
let outputPipe = Pipe()
|
||||
|
||||
serialPort.attachment = VZFileHandleSerialPortAttachment(
|
||||
fileHandleForReading: inputPipe.fileHandleForReading,
|
||||
fileHandleForWriting: outputPipe.fileHandleForWriting
|
||||
)
|
||||
|
||||
// Forward host stdin → VM serial input
|
||||
forwardStandardInput(to: inputPipe.fileHandleForWriting)
|
||||
serialOutputReadHandle = outputPipe.fileHandleForReading
|
||||
|
||||
config.serialPorts = [serialPort]
|
||||
print("[vphone] PL011 serial port attached (interactive)")
|
||||
}
|
||||
|
||||
private func forwardStandardInput(to writeHandle: FileHandle) {
|
||||
let stdinFD = FileHandle.standardInput.fileDescriptor
|
||||
DispatchQueue.global(qos: .userInteractive).async {
|
||||
var buffer = [UInt8](repeating: 0, count: 4096)
|
||||
while true {
|
||||
let bytesRead = read(stdinFD, &buffer, buffer.count)
|
||||
guard bytesRead > 0 else { break }
|
||||
writeHandle.write(Data(buffer[..<bytesRead]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func configureInputDevices(_ config: inout VZVirtualMachineConfiguration) {
|
||||
// Multi-touch screen
|
||||
if let touchScreen = Dynamic._VZUSBTouchScreenConfiguration().asObject {
|
||||
Dynamic(config)._setMultiTouchDevices([touchScreen])
|
||||
print("[vphone] USB touch screen configured")
|
||||
}
|
||||
|
||||
// Keyboard
|
||||
config.keyboards = [VZUSBKeyboardConfiguration()]
|
||||
}
|
||||
|
||||
private func configureSocketDevice(_ config: inout VZVirtualMachineConfiguration) {
|
||||
config.socketDevices = [VZVirtioSocketDeviceConfiguration()]
|
||||
}
|
||||
|
||||
private func configureBattery(_ config: inout VZVirtualMachineConfiguration) {
|
||||
let batterySource = Dynamic._VZMacSyntheticBatterySource()
|
||||
batterySource.setCharge(100.0)
|
||||
batterySource.setConnectivity(BatteryConnectivity.charging)
|
||||
|
||||
let batteryConfiguration = Dynamic._VZMacBatteryPowerSourceDeviceConfiguration()
|
||||
batteryConfiguration.setSource(batterySource.asObject)
|
||||
|
||||
guard let batteryObject = batteryConfiguration.asObject else { return }
|
||||
|
||||
Dynamic(config)._setPowerSourceDevices([batteryObject])
|
||||
self.batterySource = batterySource.asObject as AnyObject?
|
||||
print("[vphone] Synthetic battery configured (100%, charging)")
|
||||
}
|
||||
|
||||
private func configureDebugStub(_ config: inout VZVirtualMachineConfiguration, port: Int?) {
|
||||
if let port {
|
||||
guard (6000 ... 65535).contains(port) else {
|
||||
print("[vphone] Warning: Invalid kernel debug port \(port), using system-assigned")
|
||||
configureDefaultDebugStub(&config)
|
||||
return
|
||||
}
|
||||
|
||||
if let debugStub = Dynamic._VZGDBDebugStubConfiguration(port: port).asObject {
|
||||
Dynamic(config)._setDebugStub(debugStub)
|
||||
print("[vphone] Kernel GDB debug stub: tcp://127.0.0.1:\(port)")
|
||||
} else {
|
||||
configureDefaultDebugStub(&config)
|
||||
}
|
||||
} else {
|
||||
configureDefaultDebugStub(&config)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureDefaultDebugStub(_ config: inout VZVirtualMachineConfiguration) {
|
||||
let debugStub = Dynamic._VZGDBDebugStubConfiguration().asObject
|
||||
Dynamic(config)._setDebugStub(debugStub)
|
||||
print("[vphone] Kernel GDB debug stub enabled (system-assigned port)")
|
||||
}
|
||||
|
||||
private func configureSEP(_ config: inout VZVirtualMachineConfiguration, options: Configuration) {
|
||||
let sepConfiguration = Dynamic._VZSEPCoprocessorConfiguration(storageURL: options.sepStorageURL)
|
||||
sepConfiguration.setRomBinaryURL(options.sepRomURL)
|
||||
sepConfiguration.setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
|
||||
|
||||
guard let sepObject = sepConfiguration.asObject else { return }
|
||||
|
||||
Dynamic(config)._setCoprocessors([sepObject])
|
||||
print("[vphone] SEP coprocessor enabled (storage: \(options.sepStorageURL.path))")
|
||||
}
|
||||
|
||||
// MARK: - Device Identity
|
||||
|
||||
private static func resolveDeviceIdentity(machineIdentifier: VZMacMachineIdentifier) -> DeviceIdentity? {
|
||||
let ecidValue = extractECID(from: machineIdentifier)
|
||||
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 extractECID(from machineIdentifier: VZMacMachineIdentifier) -> UInt64? {
|
||||
if let ecid = Dynamic(machineIdentifier)._ECID.asUInt64 {
|
||||
return ecid
|
||||
} else if let ecidNumber = Dynamic(machineIdentifier)._ECID.asObject as? NSNumber {
|
||||
return ecidNumber.uint64Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Battery
|
||||
|
||||
/// Update the synthetic battery charge and connectivity at runtime.
|
||||
func updateBattery(charge: Double, isCharging: Bool) {
|
||||
guard let source = batterySource else { return }
|
||||
Dynamic(source).setCharge(charge)
|
||||
Dynamic(source).setConnectivity(isCharging ? BatteryConnectivity.charging : BatteryConnectivity.disconnected)
|
||||
}
|
||||
|
||||
// MARK: - Start
|
||||
|
||||
@MainActor
|
||||
func start(forceDFU: Bool) async throws {
|
||||
let startOptions = VZMacOSVirtualMachineStartOptions()
|
||||
Dynamic(startOptions)._setForceDFU(forceDFU)
|
||||
Dynamic(startOptions)._setStopInIBootStage1(false)
|
||||
Dynamic(startOptions)._setStopInIBootStage2(false)
|
||||
|
||||
print("[vphone] Starting\(forceDFU ? " DFU" : "")...")
|
||||
|
||||
nonisolated(unsafe) let vm = virtualMachine
|
||||
try await vm.start(options: startOptions)
|
||||
|
||||
if forceDFU {
|
||||
print("[vphone] VM started in DFU mode — connect with irecovery")
|
||||
} else {
|
||||
print("[vphone] VM started — booting normally")
|
||||
}
|
||||
|
||||
logDebugStubPortIfNeeded(vm)
|
||||
}
|
||||
|
||||
private func logDebugStubPortIfNeeded(_ vm: VZVirtualMachine) {
|
||||
guard ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 26 else {
|
||||
print("[vphone] Kernel GDB debug stub port query requires macOS 26+, skipped")
|
||||
return
|
||||
}
|
||||
|
||||
guard let debugStub = Dynamic(vm)._configuration._debugStub.asAnyObject,
|
||||
let port = Dynamic(debugStub).port.asInt,
|
||||
port > 0
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
print("[vphone] Kernel GDB debug stub listening on tcp://127.0.0.1:\(port)")
|
||||
}
|
||||
|
||||
// MARK: - Delegate
|
||||
|
||||
nonisolated func guestDidStop(_: VZVirtualMachine) {
|
||||
print("[vphone] Guest stopped")
|
||||
exit(EXIT_SUCCESS)
|
||||
}
|
||||
|
||||
nonisolated func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) {
|
||||
print("[vphone] Stopped with error: \(error)")
|
||||
exit(EXIT_FAILURE)
|
||||
}
|
||||
|
||||
nonisolated func virtualMachine(
|
||||
_: VZVirtualMachine,
|
||||
networkDevice _: VZNetworkDevice,
|
||||
attachmentWasDisconnectedWithError error: Error
|
||||
) {
|
||||
print("[vphone] Network error: \(error)")
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
|
||||
signal(SIGINT, SIG_IGN)
|
||||
let src = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
|
||||
src.setEventHandler {
|
||||
print("\n[vphone] SIGINT — shutting down")
|
||||
print("\n[vphone] SIGINT - shutting down")
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
src.activate()
|
||||
|
||||
@@ -607,7 +607,7 @@ class VPhoneControl {
|
||||
continue
|
||||
}
|
||||
|
||||
// No pending request — handle as before (fire-and-forget)
|
||||
// No pending request - handle as before (fire-and-forget)
|
||||
switch type {
|
||||
case "ok":
|
||||
let detail = msg["msg"] as? String ?? ""
|
||||
|
||||
@@ -123,7 +123,7 @@ class VPhoneKeyHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ASCII → Apple VK Code (US Layout)
|
||||
// MARK: - ASCII -> Apple VK Code (US Layout)
|
||||
|
||||
private func asciiToVK(_ char: Character) -> (UInt16, Bool)? {
|
||||
switch char {
|
||||
|
||||
@@ -81,7 +81,7 @@ class VPhoneKeychainBrowserModel {
|
||||
|
||||
func refresh() async {
|
||||
guard control.isConnected else {
|
||||
error = "Waiting for vphoned connection…"
|
||||
error = "Waiting for vphoned connection..."
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
|
||||
@@ -57,21 +57,21 @@ struct VPhoneKeychainBrowserView: View {
|
||||
.width(min: 60, ideal: 80, max: 100)
|
||||
|
||||
TableColumn("Account", value: \.account) { item in
|
||||
Text(item.account.isEmpty ? "—" : item.account)
|
||||
Text(item.account.isEmpty ? "-" : item.account)
|
||||
.lineLimit(1)
|
||||
.help(item.account)
|
||||
}
|
||||
.width(min: 80, ideal: 150, max: .infinity)
|
||||
|
||||
TableColumn("Service", value: \.service) { item in
|
||||
Text(item.service.isEmpty ? "—" : item.service)
|
||||
Text(item.service.isEmpty ? "-" : item.service)
|
||||
.lineLimit(1)
|
||||
.help(item.service)
|
||||
}
|
||||
.width(min: 80, ideal: 150, max: .infinity)
|
||||
|
||||
TableColumn("Access Group", value: \.accessGroup) { item in
|
||||
Text(item.accessGroup.isEmpty ? "—" : item.accessGroup)
|
||||
Text(item.accessGroup.isEmpty ? "-" : item.accessGroup)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.lineLimit(1)
|
||||
.help(item.accessGroup)
|
||||
@@ -79,7 +79,7 @@ struct VPhoneKeychainBrowserView: View {
|
||||
.width(min: 80, ideal: 160, max: .infinity)
|
||||
|
||||
TableColumn("Protection", value: \.protection) { item in
|
||||
Text(item.protection.isEmpty ? "—" : item.protection)
|
||||
Text(item.protection.isEmpty ? "-" : item.protection)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.lineLimit(1)
|
||||
.help(item.protectionDescription)
|
||||
|
||||
@@ -38,7 +38,7 @@ struct VPhoneKeychainItem: Identifiable, Hashable {
|
||||
}
|
||||
|
||||
var displayValue: String {
|
||||
if value.isEmpty { return "—" }
|
||||
if value.isEmpty { return "-" }
|
||||
if valueEncoding == "base64" {
|
||||
return "[\(ByteCountFormatter.string(fromByteCount: Int64(valueSize), countStyle: .file)) binary]"
|
||||
}
|
||||
@@ -73,7 +73,7 @@ struct VPhoneKeychainItem: Identifiable, Hashable {
|
||||
if let created {
|
||||
return Self.dateFormatter.string(from: created)
|
||||
}
|
||||
return "—"
|
||||
return "-"
|
||||
}
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import Foundation
|
||||
/// Uses macOS CoreLocation to track the Mac's real location and forwards
|
||||
/// every update to the guest. Call `startForwarding()` when the guest
|
||||
/// reports "location" capability. Safe to call multiple times (e.g.
|
||||
/// after vphoned reconnects) — re-sends the last known position.
|
||||
/// after vphoned reconnects) - re-sends the last known position.
|
||||
@MainActor
|
||||
class VPhoneLocationProvider: NSObject {
|
||||
struct ReplayPoint {
|
||||
@@ -244,7 +244,7 @@ private class LocationDelegateProxy: NSObject, CLLocationManagerDelegate {
|
||||
guard let location = locations.last else { return }
|
||||
let c = location.coordinate
|
||||
print(
|
||||
"[location] got location: \(String(format: "%.6f,%.6f", c.latitude, c.longitude)) (±\(String(format: "%.0f", location.horizontalAccuracy))m)"
|
||||
"[location] got location: \(String(format: "%.6f,%.6f", c.latitude, c.longitude)) (+/-\(String(format: "%.0f", location.horizontalAccuracy))m)"
|
||||
)
|
||||
handler(location)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ struct VPhoneRemoteFile: Identifiable, Hashable {
|
||||
}
|
||||
|
||||
var displaySize: String {
|
||||
if isDirectory || isSymbolicLink { return "—" }
|
||||
if isDirectory || isSymbolicLink { return "-" }
|
||||
return ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file)
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class VPhoneScreenRecorder {
|
||||
}
|
||||
|
||||
print(
|
||||
"[record] started — \(url.lastPathComponent) (\(width)x\(height), source: \(captureModeDescription))"
|
||||
"[record] started - \(url.lastPathComponent) (\(width)x\(height), source: \(captureModeDescription))"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class VPhoneScreenRecorder {
|
||||
didLogCaptureFailure = false
|
||||
|
||||
if let url {
|
||||
print("[record] saved — \(url.path)")
|
||||
print("[record] saved - \(url.path)")
|
||||
}
|
||||
return url
|
||||
}
|
||||
@@ -153,7 +153,7 @@ class VPhoneScreenRecorder {
|
||||
|
||||
let url = screenshotOutputURL()
|
||||
try pngData.write(to: url, options: .atomic)
|
||||
print("[record] screenshot saved — \(url.path)")
|
||||
print("[record] screenshot saved - \(url.path)")
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ class VPhoneScreenRecorder {
|
||||
|
||||
let cfObject = imageObject as CFTypeRef
|
||||
if CFGetTypeID(cfObject) == CGImage.typeID {
|
||||
// CGImage is a CF type, not a Swift class — unsafeDowncast cannot be used here.
|
||||
// CGImage is a CF type, not a Swift class - unsafeDowncast cannot be used here.
|
||||
return (cfObject as! CGImage) // swiftlint:disable:this force_cast
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
|
||||
net.attachment = VZNATNetworkDeviceAttachment()
|
||||
config.networkDevices = [net]
|
||||
|
||||
// Serial port (PL011 UART — pipes for input/output with boot detection)
|
||||
// Serial port (PL011 UART - pipes for input/output with boot detection)
|
||||
if let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject
|
||||
as? VZSerialPortConfiguration
|
||||
{
|
||||
@@ -188,7 +188,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
|
||||
fileHandleForWriting: outputPipe.fileHandleForWriting
|
||||
)
|
||||
|
||||
// Forward host stdin → VM serial input
|
||||
// Forward host stdin -> VM serial input
|
||||
let writeHandle = inputPipe.fileHandleForWriting
|
||||
let stdinFD = FileHandle.standardInput.fileDescriptor
|
||||
DispatchQueue.global(qos: .userInteractive).async {
|
||||
@@ -214,10 +214,10 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
|
||||
|
||||
config.keyboards = [VZUSBKeyboardConfiguration()]
|
||||
|
||||
// Vsock (host ↔ guest control channel, no IP/TCP involved)
|
||||
// Vsock (host <-> guest control channel, no IP/TCP involved)
|
||||
config.socketDevices = [VZVirtioSocketDeviceConfiguration()]
|
||||
|
||||
// Power source (synthetic battery — guest sees full charge, charging)
|
||||
// Power source (synthetic battery - guest sees full charge, charging)
|
||||
let source = Dynamic._VZMacSyntheticBatterySource()
|
||||
source.setCharge(100.0)
|
||||
source.setConnectivity(1) // 1=charging, 2=disconnected
|
||||
@@ -265,7 +265,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
|
||||
super.init()
|
||||
virtualMachine.delegate = self
|
||||
|
||||
// Forward VM serial output → host stdout
|
||||
// Forward VM serial output -> host stdout
|
||||
if let readHandle = serialOutputReadHandle {
|
||||
readHandle.readabilityHandler = { handle in
|
||||
let data = handle.availableData
|
||||
@@ -328,9 +328,9 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
|
||||
nonisolated(unsafe) let vm = virtualMachine
|
||||
try await vm.start(options: opts)
|
||||
if forceDFU {
|
||||
print("[vphone] VM started in DFU mode — connect with irecovery")
|
||||
print("[vphone] VM started in DFU mode - connect with irecovery")
|
||||
} else {
|
||||
print("[vphone] VM started — booting normally")
|
||||
print("[vphone] VM started - booting normally")
|
||||
}
|
||||
|
||||
// Print auto-assigned debug stub port after VM starts (private API, macOS 26+ only)
|
||||
|
||||
@@ -43,7 +43,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate {
|
||||
|
||||
window.isReleasedWhenClosed = false
|
||||
window.contentAspectRatio = windowSize
|
||||
window.title = "VPHONE ⏳"
|
||||
window.title = "VPHONE [loading]"
|
||||
window.subtitle = ecid ?? ""
|
||||
window.contentView = vmView
|
||||
if let ecid {
|
||||
@@ -75,7 +75,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate {
|
||||
[weak self, weak window] _ in
|
||||
Task { @MainActor in
|
||||
guard let self, let window, let control = self.control else { return }
|
||||
window.title = control.isConnected ? "VPHONE 🔗" : "VPHONE ⛓️💥"
|
||||
window.title = control.isConnected ? "VPHONE [connected]" : "VPHONE [disconnected]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user