apple: release the virtual machine when it fails to start

No delegate callback follows a failed start, restore, or install, so the
VZVirtualMachine created for the attempt was kept until the next start and
the serial port PTYs stayed open and advertised for a stopped VM. With vmnet
networking this also kept the network and its subnet reserved with nothing
running. Drop the virtual machine and close the serial ports in that case,
unless the guest is still alive and will report its own stop.

Assisted-by: Claude:claude-fable-5-1
This commit is contained in:
osy
2026-09-18 13:39:50 +02:00
parent 31b431f8b5
commit fe74b1d8cc
+37 -9
View File
@@ -235,6 +235,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
if #available(macOS 27, *) {
await stopUsbPassthrough()
}
await releaseFailedAppleVM()
state = .stopped
try? await deleteSnapshot()
throw error
@@ -537,7 +538,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
let serialPort = UTMSerialPort(portNamed: name, readFileHandle: slaveTtyHandle, writeFileHandle: slaveTtyHandle, terminalFileHandle: terminalTtyHandle)
config.serials[i].interface = serialPort
}
let vzConfig = try config.appleVZConfiguration(forValidation: false)
let vzConfig = try config.appleVZConfiguration()
var usbControllerDelegate: AnyObject?
if #available(macOS 27, *), hasUsbRedirection {
if isRestoring {
@@ -566,6 +567,39 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
}
}
@MainActor private func closeSerialPorts() {
for i in config.serials.indices {
if let serialPort = config.serials[i].interface {
serialPort.close()
config.serials[i].interface = nil
config.serials[i].fileHandleForReading = nil
config.serials[i].fileHandleForWriting = nil
}
}
}
/// Release what `createAppleVM` set up when the virtual machine did not come up.
///
/// No delegate callback follows a failed start, so without this the virtual machine would hold on
/// to its devices (and any network they reserve) and the serial ports would stay open.
private func releaseFailedAppleVM() async {
let isReleased = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
vmQueue.async { [self] in
if let apple = apple, apple.state != .stopped && apple.state != .error {
// the guest is alive, everything is released once it stops
continuation.resume(returning: false)
return
}
apple = nil
snapshotUnsupportedError = nil
continuation.resume(returning: true)
}
}
if isReleased {
await closeSerialPorts()
}
}
private func updateSharedDirectories(with newShares: [UTMAppleConfigurationSharedDirectory], tag: String) {
guard let fsConfig = apple?.directorySharingDevices.first(where: { device in
if let device = device as? VZVirtioFileSystemDevice {
@@ -622,6 +656,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
progressObserver = nil
installProgress = nil
await stopAccesingResources()
await releaseFailedAppleVM()
delegate?.virtualMachine(self, didCompleteInstallation: false)
state = .stopped
let error = error as NSError
@@ -754,14 +789,7 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
if #available(macOS 27, *) {
stopUsbPassthrough()
}
for i in config.serials.indices {
if let serialPort = config.serials[i].interface {
serialPort.close()
config.serials[i].interface = nil
config.serials[i].fileHandleForReading = nil
config.serials[i].fileHandleForWriting = nil
}
}
closeSerialPorts()
}
try? saveScreenshot()
state = .stopped