From 46c8fd32691c75b9bea7f6f4fb94ba23f9bb99ba Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Mon, 27 Apr 2026 12:17:12 -0700 Subject: [PATCH] LinuxContainer/LinuxPod: Expose withVirtualMachineInstance (#701) --- Sources/Containerization/LinuxContainer.swift | 32 +++++++++++++++++++ Sources/Containerization/LinuxPod.swift | 14 ++++++++ .../VZVirtualMachineInstance.swift | 28 ++++++++-------- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 24924cb9..e2540274 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -246,6 +246,24 @@ public final class LinuxContainer: Container, Sendable { mutating func setErrored(error: Swift.Error) { self = .errored(error) } + + func vm(_ operation: String) throws -> any VirtualMachineInstance { + switch self { + case .created(let state): + return state.vm + case .started(let state): + return state.vm + case .paused(let state): + return state.vm + case .errored(let err): + throw err + default: + throw ContainerizationError( + .invalidState, + message: "failed to \(operation): container must be created, running, or paused" + ) + } + } } private let vmm: VirtualMachineManager @@ -936,6 +954,20 @@ extension LinuxContainer { } } + /// Provides scoped access to the underlying virtual machine instance. + /// + /// Most users should prefer the higher level APIs on ``LinuxContainer`` + /// directly. This is intended for advanced use cases that need to interact + /// with the virtual machine outside of the container abstraction. + public func withVirtualMachineInstance( + _ fn: @Sendable (any VirtualMachineInstance) async throws -> T + ) async throws -> T { + let vm = try await self.state.withLock { state in + try state.vm("withVirtualMachineInstance") + } + return try await fn(vm) + } + /// Close the containers standard input to signal no more input is /// arriving. public func closeStdin() async throws { diff --git a/Sources/Containerization/LinuxPod.swift b/Sources/Containerization/LinuxPod.swift index 035e1a8d..67ec76d7 100644 --- a/Sources/Containerization/LinuxPod.swift +++ b/Sources/Containerization/LinuxPod.swift @@ -861,6 +861,20 @@ extension LinuxPod { } } + /// Provides scoped access to the underlying virtual machine instance. + /// + /// Most users should prefer the higher level APIs on ``LinuxPod`` + /// directly. This is intended for advanced use cases that need to interact + /// with the virtual machine outside of the pod abstraction. + public func withVirtualMachineInstance( + _ fn: @Sendable (any VirtualMachineInstance) async throws -> T + ) async throws -> T { + let vm = try await self.state.withLock { state in + try state.phase.createdState("withVirtualMachineInstance").vm + } + return try await fn(vm) + } + /// Close a container's standard input to signal no more input is arriving. public func closeContainerStdin(_ containerID: String) async throws { try await self.state.withLock { state in diff --git a/Sources/Containerization/VZVirtualMachineInstance.swift b/Sources/Containerization/VZVirtualMachineInstance.swift index 7e580aa2..85d411ae 100644 --- a/Sources/Containerization/VZVirtualMachineInstance.swift +++ b/Sources/Containerization/VZVirtualMachineInstance.swift @@ -25,8 +25,8 @@ import NIOPosix import Synchronization import Virtualization -struct VZVirtualMachineInstance: Sendable { - typealias Agent = Vminitd +public final class VZVirtualMachineInstance: Sendable { + public typealias Agent = Vminitd /// Attached mounts on the virtual machine, organized by metadata ID. public let mounts: [String: [AttachedFilesystem]] @@ -58,7 +58,7 @@ struct VZVirtualMachineInstance: Sendable { /// Destination for the virtual machine's boot logs. public var bootLog: BootLog? - init() { + public init() { self.cpus = 4 self.memoryInBytes = 1024.mib() self.rosetta = false @@ -77,7 +77,7 @@ struct VZVirtualMachineInstance: Sendable { private let timeSyncer: TimeSyncer private let logger: Logger? - public init( + public convenience init( group: EventLoopGroup? = nil, logger: Logger? = nil, with: (inout Configuration) throws -> Void @@ -111,7 +111,7 @@ struct VZVirtualMachineInstance: Sendable { } extension VZVirtualMachineInstance: VirtualMachineInstance { - func start() async throws { + public func start() async throws { try await lock.withLock { _ in guard self.state == .stopped else { throw ContainerizationError( @@ -145,7 +145,7 @@ extension VZVirtualMachineInstance: VirtualMachineInstance { } } - func stop() async throws { + public func stop() async throws { try await lock.withLock { connections in // NOTE: We should record HOW the vm stopped eventually. If the vm exited // unexpectedly virtualization framework offers you a way to store @@ -168,14 +168,14 @@ extension VZVirtualMachineInstance: VirtualMachineInstance { // NOTE: Investigate what is the "right" way to handle already vended vsock // connections for pause and resume. - func pause() async throws { + public func pause() async throws { try await lock.withLock { _ in await self.timeSyncer.pause() try await self.vm.pause(queue: self.queue) } } - func resume() async throws { + public func resume() async throws { try await lock.withLock { _ in try await self.vm.resume(queue: self.queue) await self.timeSyncer.resume() @@ -185,8 +185,8 @@ extension VZVirtualMachineInstance: VirtualMachineInstance { public func dialAgent() async throws -> Vminitd { try await lock.withLock { _ in do { - let conn = try await vm.connect( - queue: queue, + let conn = try await self.vm.connect( + queue: self.queue, port: Vminitd.port ) let handle = try conn.dupHandle() @@ -204,11 +204,11 @@ extension VZVirtualMachineInstance: VirtualMachineInstance { } } - func dial(_ port: UInt32) async throws -> FileHandle { + public func dial(_ port: UInt32) async throws -> FileHandle { try await lock.withLock { _ in do { - let conn = try await vm.connect( - queue: queue, + let conn = try await self.vm.connect( + queue: self.queue, port: port ) return try conn.dupHandle() @@ -225,7 +225,7 @@ extension VZVirtualMachineInstance: VirtualMachineInstance { } } - func listen(_ port: UInt32) throws -> VsockListener { + public func listen(_ port: UInt32) throws -> VsockListener { let stream = VsockListener(port: port, stopListen: self.stopListen) let listener = VZVirtioSocketListener() listener.delegate = stream