Files

150 lines
5.6 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
#if os(macOS)
import Foundation
import Synchronization
@preconcurrency import Virtualization
/// A once-only escape hatch for stopping a VM when its normal lifecycle path is unavailable.
///
/// The handle deliberately retains only Virtualization's VM object, its required dispatch queue,
/// and caller-supplied generation metadata. Requesting a stop does not enter a container state
/// gate, an instance lifecycle lock, or a guest-agent/RPC path.
public final class EmergencyVMHandle: @unchecked Sendable {
private struct State {
enum Phase {
case ready
case requested
case stopped
}
var phase: Phase = .ready
var waiters: [UUID: CheckedContinuation<Bool, Never>] = [:]
}
/// Stable identity for this exact retained VM, independent of caller generation numbering.
public let id = UUID()
public let generation: UInt64
private nonisolated(unsafe) let virtualMachine: VZVirtualMachine
private let queue: DispatchQueue
private let state = Mutex(State())
init(virtualMachine: VZVirtualMachine, queue: DispatchQueue, generation: UInt64) {
self.virtualMachine = virtualMachine
self.queue = queue
self.generation = generation
}
/// Whether Virtualization has reported stop completion for this exact VM.
public var hasStopped: Bool {
state.withLock {
if case .stopped = $0.phase { return true }
return false
}
}
/// Enqueue a hard VM stop exactly once. Returns `true` only for the caller which requested it.
/// Completion is intentionally not awaited: this API is the independent request lane used when
/// normal lifecycle tasks may themselves be unable to make progress.
@discardableResult
public func requestStop() -> Bool {
let shouldRequest = state.withLock { state in
guard case .ready = state.phase else { return false }
state.phase = .requested
return true
}
guard shouldRequest else { return false }
queue.async { [self] in
if virtualMachine.state == .stopped {
finishStopped()
return
}
if virtualMachine.state == .stopping {
scheduleStopStateObservation()
return
}
virtualMachine.stop { [weak self] _ in
self?.scheduleStopStateObservation(immediate: true)
}
}
return true
}
/// Wait until Virtualization reports this exact VM stopped, but never beyond `deadline`.
/// `false` is an explicit force-retirement signal for the caller; the stop request itself stays
/// independent and lock-free even if the normal instance lifecycle lane is wedged.
public func waitUntilStopped(deadline: ContinuousClock.Instant) async -> Bool {
let waiterID = UUID()
return await withCheckedContinuation { continuation in
let alreadyStopped = state.withLock { state in
guard case .stopped = state.phase else {
state.waiters[waiterID] = continuation
return false
}
return true
}
if alreadyStopped {
continuation.resume(returning: true)
return
}
Task { [weak self] in
do {
try await ContinuousClock().sleep(until: deadline)
} catch {
// Cancellation is also a bounded failure for the waiting caller. The retained
// stop request continues independently on Virtualization's queue.
}
self?.finishWaiter(waiterID, stopped: false)
}
}
}
private func scheduleStopStateObservation(immediate: Bool = false) {
queue.asyncAfter(deadline: .now() + (immediate ? 0 : 0.05)) { [weak self] in
guard let self else { return }
guard case .requested = state.withLock({ $0.phase }) else { return }
if virtualMachine.state == .stopped {
finishStopped()
} else {
scheduleStopStateObservation()
}
}
}
private func finishStopped() {
let waiters = state.withLock { state -> [CheckedContinuation<Bool, Never>] in
guard case .stopped = state.phase else {
state.phase = .stopped
let waiters = Array(state.waiters.values)
state.waiters.removeAll()
return waiters
}
return []
}
for waiter in waiters { waiter.resume(returning: true) }
}
private func finishWaiter(_ id: UUID, stopped: Bool) {
let waiter = state.withLock { $0.waiters.removeValue(forKey: id) }
waiter?.resume(returning: stopped)
}
}
#endif