Fix: Replace emoji and non-ASCII characters (#177)

This commit is contained in:
Luka
2026-03-10 11:30:01 +01:00
committed by GitHub
parent 6d11093152
commit 1eb9f627c3
12 changed files with 450 additions and 26 deletions

View 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)")
}
}

View File

@@ -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()

View File

@@ -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 ?? ""

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)

View File

@@ -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 = {

View File

@@ -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)
}

View File

@@ -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)
}

View 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
}

View File

@@ -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)

View File

@@ -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]"
}
}
}