From 851157ba8c91e557edfa38b9aaa9416f48e8e3f0 Mon Sep 17 00:00:00 2001 From: Kyle Hubert Date: Sat, 22 Feb 2025 17:47:00 -0500 Subject: [PATCH] When using Apple Virtualization notify guest OS of wake up events Open a Virtio Socket for the guest OS to receive a wakeup command if it connects. This could be the basis for more of a command structure, like the QEMU guest agent. We are only using this to send a wakeup command, however. This is predominantly useful for the guest OS to resync time against the hardware clock. Otherwise, using network time syncs relies on a timer, which can take betweem 5 - 30 minutes before its next scheduled sync. Also, network time syncs can sometimes fail with too much drift to resync the local system clock. Yet, while ntp has a skew of 1000s before failing, modern systemd Linux distros have switched to system-timesyncd.service, so the author is unsure what max drift is with this package. --- Build.xcconfig | 2 +- ...MDisplayAppleDisplayWindowController.swift | 25 +++++++++ Services/UTMAppleVirtualMachine.swift | 56 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/Build.xcconfig b/Build.xcconfig index 187356f09..b738e3ffc 100644 --- a/Build.xcconfig +++ b/Build.xcconfig @@ -18,7 +18,7 @@ // https://help.apple.com/xcode/#/dev745c5c974 MARKETING_VERSION = 4.6.4 -CURRENT_PROJECT_VERSION = 107 +CURRENT_PROJECT_VERSION = 108 // Codesigning settings defined optionally, see Documentation/iOSDevelopment.md #include? "CodeSigning.xcconfig" diff --git a/Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift b/Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift index 8f0bd104d..d5f3e79c0 100644 --- a/Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift +++ b/Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift @@ -234,3 +234,28 @@ class VMDisplayAppleDisplayWindowController: VMDisplayAppleWindowController { isReadyToSaveResolution = false } } + +extension VMDisplayAppleWindowController { + @objc override func didWake(_ notification: NSNotification) { + Task { + if #available(macOS 12, *) { + if let appleVM = self.appleVM as? UTMAppleVirtualMachine, + let connection = appleVM.socketConnection { + let message = "wakeup" + if let data = message.data(using: .utf8) { + let fd = connection.fileDescriptor + if fd != -1 { + data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in + let bytes = buffer.bindMemory(to: UInt8.self).baseAddress! + let result = write(fd, bytes, data.count) + if result == -1 { + NSLog("Failed to write wakeup command to socket: \(String(cString: strerror(errno)))") + } + } + } + } + } + } + } + } +} diff --git a/Services/UTMAppleVirtualMachine.swift b/Services/UTMAppleVirtualMachine.swift index df3153c54..9c950ce75 100644 --- a/Services/UTMAppleVirtualMachine.swift +++ b/Services/UTMAppleVirtualMachine.swift @@ -118,6 +118,16 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { private var removableDrives: [String: Any] = [:] + var socketConnection: VZVirtioSocketConnection? + + @available(macOS 12, *) + private class SocketState { + var delegate: UTMSocketDelegate? + var listener: VZVirtioSocketListener? + } + + private var socketState: Any? // Will store SocketState when available + @MainActor required init(packageUrl: URL, configuration: UTMAppleConfiguration, isShortcut: Bool = false) throws { self.isScopedAccess = packageUrl.startAccessingSecurityScopedResource() // load configuration @@ -192,6 +202,33 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { if #available(macOS 15, *) { try await attachExternalDrives() } + + // Add VZVirtioSocketListener call + if #available(macOS 12, *) { + vmQueue.async { [weak self] in + guard let self = self else { return } + + // Wait until state is .started using a polling loop on vmQueue + while self.state != .started { + Thread.sleep(forTimeInterval: 0.1) + } + + if let virtioSocket = self.apple?.socketDevices.first as? VZVirtioSocketDevice { + let localListener = VZVirtioSocketListener() + let delegate = UTMSocketDelegate(owner: self) + localListener.delegate = delegate + virtioSocket.setSocketListener(localListener, forPort: 43218) + + if socketState == nil { + socketState = SocketState() + } + let state = socketState as! SocketState + state.listener = localListener + state.delegate = delegate + } + } + } + if #available(macOS 12, *) { Task { @MainActor in let tag = config.shareDirectoryTag @@ -503,6 +540,10 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { config.serials[i].interface = serialPort } let vzConfig = try config.appleVZConfiguration() + if #available(macOS 11, *) { + let socketDevice = VZVirtioSocketDeviceConfiguration() + vzConfig.socketDevices = [socketDevice] + } vmQueue.async { [self] in apple = VZVirtualMachine(configuration: vzConfig, queue: vmQueue) apple!.delegate = self @@ -987,3 +1028,18 @@ extension UTMAppleVirtualMachine { } } } + +@available(macOS 12, *) +class UTMSocketDelegate: NSObject, VZVirtioSocketListenerDelegate { + weak var owner: UTMAppleVirtualMachine? + + init(owner: UTMAppleVirtualMachine) { + self.owner = owner + super.init() + } + + func listener(_ listener: VZVirtioSocketListener, shouldAcceptNewConnection connection: VZVirtioSocketConnection, from socketDevice: VZVirtioSocketDevice) -> Bool { + owner?.socketConnection = connection + return true + } +}