mirror of
https://github.com/apple/container.git
synced 2026-11-04 04:53:25 +00:00
Agent: Add single file copying (#463)
Add a streaming rpc to copy single files in and out. This can easily be extended to support copying directories, but we need the guest agent to be able to tar/untar, and we'll need to figure out what that model is.
This commit is contained in:
1 parent
fb95398fb5
commit
44ecb87f8d
9 files changed
+1165
-8
No files matched your search
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// 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.
|
||||
@@ -830,6 +830,59 @@ extension LinuxContainer {
|
||||
try await relayManager.start(port: port, socket: socket)
|
||||
try await relayAgent.relaySocket(port: port, configuration: socket)
|
||||
}
|
||||
|
||||
/// Default chunk size for file transfers (1MiB).
|
||||
public static let defaultCopyChunkSize = 1024 * 1024
|
||||
|
||||
/// Copy a file from the host into the container.
|
||||
public func copyIn(
|
||||
from source: URL,
|
||||
to destination: URL,
|
||||
mode: UInt32 = 0o644,
|
||||
createParents: Bool = true,
|
||||
chunkSize: Int = defaultCopyChunkSize,
|
||||
progress: ProgressHandler? = nil
|
||||
) async throws {
|
||||
try await self.state.withLock {
|
||||
let state = try $0.startedState("copyIn")
|
||||
|
||||
let guestPath = URL(filePath: self.root).appending(path: destination.path)
|
||||
try await state.vm.withAgent { agent in
|
||||
try await agent.copyIn(
|
||||
from: source,
|
||||
to: guestPath,
|
||||
mode: mode,
|
||||
createParents: createParents,
|
||||
chunkSize: chunkSize,
|
||||
progress: progress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a file from the container to the host.
|
||||
public func copyOut(
|
||||
from source: URL,
|
||||
to destination: URL,
|
||||
createParents: Bool = true,
|
||||
chunkSize: Int = defaultCopyChunkSize,
|
||||
progress: ProgressHandler? = nil
|
||||
) async throws {
|
||||
try await self.state.withLock {
|
||||
let state = try $0.startedState("copyOut")
|
||||
|
||||
let guestPath = URL(filePath: self.root).appending(path: source.path)
|
||||
try await state.vm.withAgent { agent in
|
||||
try await agent.copyOut(
|
||||
from: guestPath,
|
||||
to: destination,
|
||||
createParents: createParents,
|
||||
chunkSize: chunkSize,
|
||||
progress: progress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VirtualMachineInstance {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// 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.
|
||||
@@ -79,6 +79,16 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtoc
|
||||
callOptions: CallOptions?
|
||||
) -> UnaryCall<Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>
|
||||
|
||||
func copyIn(
|
||||
callOptions: CallOptions?
|
||||
) -> ClientStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse>
|
||||
|
||||
func copyOut(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
|
||||
callOptions: CallOptions?,
|
||||
handler: @escaping (Com_Apple_Containerization_Sandbox_V3_CopyOutChunk) -> Void
|
||||
) -> ServerStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>
|
||||
|
||||
func createProcess(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
|
||||
callOptions: CallOptions?
|
||||
@@ -337,6 +347,45 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtocol {
|
||||
)
|
||||
}
|
||||
|
||||
/// Copy a file from the host into the guest.
|
||||
///
|
||||
/// Callers should use the `send` method on the returned object to send messages
|
||||
/// to the server. The caller should send an `.end` after the final message has been sent.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - callOptions: Call options.
|
||||
/// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response.
|
||||
public func copyIn(
|
||||
callOptions: CallOptions? = nil
|
||||
) -> ClientStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse> {
|
||||
return self.makeClientStreamingCall(
|
||||
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn.path,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: self.interceptors?.makeCopyInInterceptors() ?? []
|
||||
)
|
||||
}
|
||||
|
||||
/// Copy a file from the guest to the host.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - request: Request to send to CopyOut.
|
||||
/// - callOptions: Call options.
|
||||
/// - handler: A closure called when each response is received from the server.
|
||||
/// - Returns: A `ServerStreamingCall` with futures for the metadata and status.
|
||||
public func copyOut(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
|
||||
callOptions: CallOptions? = nil,
|
||||
handler: @escaping (Com_Apple_Containerization_Sandbox_V3_CopyOutChunk) -> Void
|
||||
) -> ServerStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk> {
|
||||
return self.makeServerStreamingCall(
|
||||
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyOut.path,
|
||||
request: request,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? [],
|
||||
handler: handler
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new process inside the container.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -771,6 +820,15 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientP
|
||||
callOptions: CallOptions?
|
||||
) -> GRPCAsyncUnaryCall<Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>
|
||||
|
||||
func makeCopyInCall(
|
||||
callOptions: CallOptions?
|
||||
) -> GRPCAsyncClientStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse>
|
||||
|
||||
func makeCopyOutCall(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
|
||||
callOptions: CallOptions?
|
||||
) -> GRPCAsyncServerStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>
|
||||
|
||||
func makeCreateProcessCall(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
|
||||
callOptions: CallOptions?
|
||||
@@ -980,6 +1038,28 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco
|
||||
)
|
||||
}
|
||||
|
||||
public func makeCopyInCall(
|
||||
callOptions: CallOptions? = nil
|
||||
) -> GRPCAsyncClientStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse> {
|
||||
return self.makeAsyncClientStreamingCall(
|
||||
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn.path,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: self.interceptors?.makeCopyInInterceptors() ?? []
|
||||
)
|
||||
}
|
||||
|
||||
public func makeCopyOutCall(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
|
||||
callOptions: CallOptions? = nil
|
||||
) -> GRPCAsyncServerStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk> {
|
||||
return self.makeAsyncServerStreamingCall(
|
||||
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyOut.path,
|
||||
request: request,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? []
|
||||
)
|
||||
}
|
||||
|
||||
public func makeCreateProcessCall(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
|
||||
callOptions: CallOptions? = nil
|
||||
@@ -1307,6 +1387,42 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco
|
||||
)
|
||||
}
|
||||
|
||||
public func copyIn<RequestStream>(
|
||||
_ requests: RequestStream,
|
||||
callOptions: CallOptions? = nil
|
||||
) async throws -> Com_Apple_Containerization_Sandbox_V3_CopyInResponse where RequestStream: Sequence, RequestStream.Element == Com_Apple_Containerization_Sandbox_V3_CopyInChunk {
|
||||
return try await self.performAsyncClientStreamingCall(
|
||||
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn.path,
|
||||
requests: requests,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: self.interceptors?.makeCopyInInterceptors() ?? []
|
||||
)
|
||||
}
|
||||
|
||||
public func copyIn<RequestStream>(
|
||||
_ requests: RequestStream,
|
||||
callOptions: CallOptions? = nil
|
||||
) async throws -> Com_Apple_Containerization_Sandbox_V3_CopyInResponse where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Com_Apple_Containerization_Sandbox_V3_CopyInChunk {
|
||||
return try await self.performAsyncClientStreamingCall(
|
||||
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn.path,
|
||||
requests: requests,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: self.interceptors?.makeCopyInInterceptors() ?? []
|
||||
)
|
||||
}
|
||||
|
||||
public func copyOut(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
|
||||
callOptions: CallOptions? = nil
|
||||
) -> GRPCAsyncResponseStream<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk> {
|
||||
return self.performAsyncServerStreamingCall(
|
||||
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyOut.path,
|
||||
request: request,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? []
|
||||
)
|
||||
}
|
||||
|
||||
public func createProcess(
|
||||
_ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
|
||||
callOptions: CallOptions? = nil
|
||||
@@ -1570,6 +1686,12 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientInterc
|
||||
/// - Returns: Interceptors to use when invoking 'writeFile'.
|
||||
func makeWriteFileInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>]
|
||||
|
||||
/// - Returns: Interceptors to use when invoking 'copyIn'.
|
||||
func makeCopyInInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse>]
|
||||
|
||||
/// - Returns: Interceptors to use when invoking 'copyOut'.
|
||||
func makeCopyOutInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>]
|
||||
|
||||
/// - Returns: Interceptors to use when invoking 'createProcess'.
|
||||
func makeCreateProcessInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, Com_Apple_Containerization_Sandbox_V3_CreateProcessResponse>]
|
||||
|
||||
@@ -1639,6 +1761,8 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata {
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.setTime,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.setupEmulator,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.writeFile,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyOut,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.createProcess,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.deleteProcess,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.startProcess,
|
||||
@@ -1715,6 +1839,18 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata {
|
||||
type: GRPCCallType.unary
|
||||
)
|
||||
|
||||
public static let copyIn = GRPCMethodDescriptor(
|
||||
name: "CopyIn",
|
||||
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CopyIn",
|
||||
type: GRPCCallType.clientStreaming
|
||||
)
|
||||
|
||||
public static let copyOut = GRPCMethodDescriptor(
|
||||
name: "CopyOut",
|
||||
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CopyOut",
|
||||
type: GRPCCallType.serverStreaming
|
||||
)
|
||||
|
||||
public static let createProcess = GRPCMethodDescriptor(
|
||||
name: "CreateProcess",
|
||||
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CreateProcess",
|
||||
@@ -1858,6 +1994,12 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider: Ca
|
||||
/// Write data to an existing or new file.
|
||||
func writeFile(request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>
|
||||
|
||||
/// Copy a file from the host into the guest.
|
||||
func copyIn(context: UnaryResponseCallContext<Com_Apple_Containerization_Sandbox_V3_CopyInResponse>) -> EventLoopFuture<(StreamEvent<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>) -> Void>
|
||||
|
||||
/// Copy a file from the guest to the host.
|
||||
func copyOut(request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, context: StreamingResponseCallContext<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>) -> EventLoopFuture<GRPCStatus>
|
||||
|
||||
/// Create a new process inside the container.
|
||||
func createProcess(request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_CreateProcessResponse>
|
||||
|
||||
@@ -2007,6 +2149,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider {
|
||||
userFunction: self.writeFile(request:context:)
|
||||
)
|
||||
|
||||
case "CopyIn":
|
||||
return ClientStreamingServerHandler(
|
||||
context: context,
|
||||
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>(),
|
||||
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_CopyInResponse>(),
|
||||
interceptors: self.interceptors?.makeCopyInInterceptors() ?? [],
|
||||
observerFactory: self.copyIn(context:)
|
||||
)
|
||||
|
||||
case "CopyOut":
|
||||
return ServerStreamingServerHandler(
|
||||
context: context,
|
||||
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest>(),
|
||||
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>(),
|
||||
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? [],
|
||||
userFunction: self.copyOut(request:context:)
|
||||
)
|
||||
|
||||
case "CreateProcess":
|
||||
return UnaryServerHandler(
|
||||
context: context,
|
||||
@@ -2237,6 +2397,19 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvide
|
||||
context: GRPCAsyncServerCallContext
|
||||
) async throws -> Com_Apple_Containerization_Sandbox_V3_WriteFileResponse
|
||||
|
||||
/// Copy a file from the host into the guest.
|
||||
func copyIn(
|
||||
requestStream: GRPCAsyncRequestStream<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>,
|
||||
context: GRPCAsyncServerCallContext
|
||||
) async throws -> Com_Apple_Containerization_Sandbox_V3_CopyInResponse
|
||||
|
||||
/// Copy a file from the guest to the host.
|
||||
func copyOut(
|
||||
request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
|
||||
responseStream: GRPCAsyncResponseStreamWriter<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>,
|
||||
context: GRPCAsyncServerCallContext
|
||||
) async throws
|
||||
|
||||
/// Create a new process inside the container.
|
||||
func createProcess(
|
||||
request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
|
||||
@@ -2447,6 +2620,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvider {
|
||||
wrapping: { try await self.writeFile(request: $0, context: $1) }
|
||||
)
|
||||
|
||||
case "CopyIn":
|
||||
return GRPCAsyncServerHandler(
|
||||
context: context,
|
||||
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>(),
|
||||
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_CopyInResponse>(),
|
||||
interceptors: self.interceptors?.makeCopyInInterceptors() ?? [],
|
||||
wrapping: { try await self.copyIn(requestStream: $0, context: $1) }
|
||||
)
|
||||
|
||||
case "CopyOut":
|
||||
return GRPCAsyncServerHandler(
|
||||
context: context,
|
||||
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest>(),
|
||||
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>(),
|
||||
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? [],
|
||||
wrapping: { try await self.copyOut(request: $0, responseStream: $1, context: $2) }
|
||||
)
|
||||
|
||||
case "CreateProcess":
|
||||
return GRPCAsyncServerHandler(
|
||||
context: context,
|
||||
@@ -2653,6 +2844,14 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextServerInterc
|
||||
/// Defaults to calling `self.makeInterceptors()`.
|
||||
func makeWriteFileInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>]
|
||||
|
||||
/// - Returns: Interceptors to use when handling 'copyIn'.
|
||||
/// Defaults to calling `self.makeInterceptors()`.
|
||||
func makeCopyInInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse>]
|
||||
|
||||
/// - Returns: Interceptors to use when handling 'copyOut'.
|
||||
/// Defaults to calling `self.makeInterceptors()`.
|
||||
func makeCopyOutInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>]
|
||||
|
||||
/// - Returns: Interceptors to use when handling 'createProcess'.
|
||||
/// Defaults to calling `self.makeInterceptors()`.
|
||||
func makeCreateProcessInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, Com_Apple_Containerization_Sandbox_V3_CreateProcessResponse>]
|
||||
@@ -2740,6 +2939,8 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata {
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.setTime,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.setupEmulator,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.writeFile,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.copyIn,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.copyOut,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.createProcess,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.deleteProcess,
|
||||
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.startProcess,
|
||||
@@ -2816,6 +3017,18 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata {
|
||||
type: GRPCCallType.unary
|
||||
)
|
||||
|
||||
public static let copyIn = GRPCMethodDescriptor(
|
||||
name: "CopyIn",
|
||||
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CopyIn",
|
||||
type: GRPCCallType.clientStreaming
|
||||
)
|
||||
|
||||
public static let copyOut = GRPCMethodDescriptor(
|
||||
name: "CopyOut",
|
||||
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CopyOut",
|
||||
type: GRPCCallType.serverStreaming
|
||||
)
|
||||
|
||||
public static let createProcess = GRPCMethodDescriptor(
|
||||
name: "CreateProcess",
|
||||
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CreateProcess",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// 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.
|
||||
@@ -756,6 +756,137 @@ public struct Com_Apple_Containerization_Sandbox_V3_WriteFileResponse: Sendable
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Com_Apple_Containerization_Sandbox_V3_CopyInChunk: @unchecked Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
public var content: Com_Apple_Containerization_Sandbox_V3_CopyInChunk.OneOf_Content? = nil
|
||||
|
||||
/// Initialization message (must be first).
|
||||
public var init_p: Com_Apple_Containerization_Sandbox_V3_CopyInInit {
|
||||
get {
|
||||
if case .init_p(let v)? = content {return v}
|
||||
return Com_Apple_Containerization_Sandbox_V3_CopyInInit()
|
||||
}
|
||||
set {content = .init_p(newValue)}
|
||||
}
|
||||
|
||||
/// File data chunk.
|
||||
public var data: Data {
|
||||
get {
|
||||
if case .data(let v)? = content {return v}
|
||||
return Data()
|
||||
}
|
||||
set {content = .data(newValue)}
|
||||
}
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public enum OneOf_Content: Equatable, @unchecked Sendable {
|
||||
/// Initialization message (must be first).
|
||||
case init_p(Com_Apple_Containerization_Sandbox_V3_CopyInInit)
|
||||
/// File data chunk.
|
||||
case data(Data)
|
||||
|
||||
}
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// Destination path in the guest.
|
||||
public var path: String = String()
|
||||
|
||||
/// File mode (defaults to 0644 if not set).
|
||||
public var mode: UInt32 = 0
|
||||
|
||||
/// Create parent directories if they don't exist.
|
||||
public var createParents: Bool = false
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Com_Apple_Containerization_Sandbox_V3_CopyInResponse: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// Source path in the guest.
|
||||
public var path: String = String()
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Com_Apple_Containerization_Sandbox_V3_CopyOutChunk: @unchecked Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
public var content: Com_Apple_Containerization_Sandbox_V3_CopyOutChunk.OneOf_Content? = nil
|
||||
|
||||
/// Initialization message with metadata (first chunk).
|
||||
public var init_p: Com_Apple_Containerization_Sandbox_V3_CopyOutInit {
|
||||
get {
|
||||
if case .init_p(let v)? = content {return v}
|
||||
return Com_Apple_Containerization_Sandbox_V3_CopyOutInit()
|
||||
}
|
||||
set {content = .init_p(newValue)}
|
||||
}
|
||||
|
||||
/// File data chunk.
|
||||
public var data: Data {
|
||||
get {
|
||||
if case .data(let v)? = content {return v}
|
||||
return Data()
|
||||
}
|
||||
set {content = .data(newValue)}
|
||||
}
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public enum OneOf_Content: Equatable, @unchecked Sendable {
|
||||
/// Initialization message with metadata (first chunk).
|
||||
case init_p(Com_Apple_Containerization_Sandbox_V3_CopyOutInit)
|
||||
/// File data chunk.
|
||||
case data(Data)
|
||||
|
||||
}
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Com_Apple_Containerization_Sandbox_V3_CopyOutInit: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// Total file size in bytes.
|
||||
public var totalSize: UInt64 = 0
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
@@ -2565,6 +2696,263 @@ extension Com_Apple_Containerization_Sandbox_V3_WriteFileResponse: SwiftProtobuf
|
||||
}
|
||||
}
|
||||
|
||||
extension Com_Apple_Containerization_Sandbox_V3_CopyInChunk: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = _protobuf_package + ".CopyInChunk"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "init"),
|
||||
2: .same(proto: "data"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try {
|
||||
var v: Com_Apple_Containerization_Sandbox_V3_CopyInInit?
|
||||
var hadOneofValue = false
|
||||
if let current = self.content {
|
||||
hadOneofValue = true
|
||||
if case .init_p(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.content = .init_p(v)
|
||||
}
|
||||
}()
|
||||
case 2: try {
|
||||
var v: Data?
|
||||
try decoder.decodeSingularBytesField(value: &v)
|
||||
if let v = v {
|
||||
if self.content != nil {try decoder.handleConflictingOneOf()}
|
||||
self.content = .data(v)
|
||||
}
|
||||
}()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
switch self.content {
|
||||
case .init_p?: try {
|
||||
guard case .init_p(let v)? = self.content else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
|
||||
}()
|
||||
case .data?: try {
|
||||
guard case .data(let v)? = self.content else { preconditionFailure() }
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
|
||||
}()
|
||||
case nil: break
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyInChunk, rhs: Com_Apple_Containerization_Sandbox_V3_CopyInChunk) -> Bool {
|
||||
if lhs.content != rhs.content {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = _protobuf_package + ".CopyInInit"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "path"),
|
||||
2: .same(proto: "mode"),
|
||||
3: .standard(proto: "create_parents"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularStringField(value: &self.path) }()
|
||||
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.mode) }()
|
||||
case 3: try { try decoder.decodeSingularBoolField(value: &self.createParents) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if !self.path.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
|
||||
}
|
||||
if self.mode != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.mode, fieldNumber: 2)
|
||||
}
|
||||
if self.createParents != false {
|
||||
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyInInit, rhs: Com_Apple_Containerization_Sandbox_V3_CopyInInit) -> Bool {
|
||||
if lhs.path != rhs.path {return false}
|
||||
if lhs.mode != rhs.mode {return false}
|
||||
if lhs.createParents != rhs.createParents {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension Com_Apple_Containerization_Sandbox_V3_CopyInResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = _protobuf_package + ".CopyInResponse"
|
||||
public static let _protobuf_nameMap = SwiftProtobuf._NameMap()
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
// Load everything into unknown fields
|
||||
while try decoder.nextFieldNumber() != nil {}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyInResponse, rhs: Com_Apple_Containerization_Sandbox_V3_CopyInResponse) -> Bool {
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "path"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularStringField(value: &self.path) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if !self.path.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, rhs: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest) -> Bool {
|
||||
if lhs.path != rhs.path {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension Com_Apple_Containerization_Sandbox_V3_CopyOutChunk: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = _protobuf_package + ".CopyOutChunk"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "init"),
|
||||
2: .same(proto: "data"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try {
|
||||
var v: Com_Apple_Containerization_Sandbox_V3_CopyOutInit?
|
||||
var hadOneofValue = false
|
||||
if let current = self.content {
|
||||
hadOneofValue = true
|
||||
if case .init_p(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.content = .init_p(v)
|
||||
}
|
||||
}()
|
||||
case 2: try {
|
||||
var v: Data?
|
||||
try decoder.decodeSingularBytesField(value: &v)
|
||||
if let v = v {
|
||||
if self.content != nil {try decoder.handleConflictingOneOf()}
|
||||
self.content = .data(v)
|
||||
}
|
||||
}()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
switch self.content {
|
||||
case .init_p?: try {
|
||||
guard case .init_p(let v)? = self.content else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
|
||||
}()
|
||||
case .data?: try {
|
||||
guard case .data(let v)? = self.content else { preconditionFailure() }
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
|
||||
}()
|
||||
case nil: break
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyOutChunk, rhs: Com_Apple_Containerization_Sandbox_V3_CopyOutChunk) -> Bool {
|
||||
if lhs.content != rhs.content {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension Com_Apple_Containerization_Sandbox_V3_CopyOutInit: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = _protobuf_package + ".CopyOutInit"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .standard(proto: "total_size"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularUInt64Field(value: &self.totalSize) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.totalSize != 0 {
|
||||
try visitor.visitSingularUInt64Field(value: self.totalSize, fieldNumber: 1)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyOutInit, rhs: Com_Apple_Containerization_Sandbox_V3_CopyOutInit) -> Bool {
|
||||
if lhs.totalSize != rhs.totalSize {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = _protobuf_package + ".IpLinkSetRequest"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
|
||||
@@ -24,6 +24,10 @@ service SandboxContext {
|
||||
rpc SetupEmulator(SetupEmulatorRequest) returns (SetupEmulatorResponse);
|
||||
// Write data to an existing or new file.
|
||||
rpc WriteFile(WriteFileRequest) returns (WriteFileResponse);
|
||||
// Copy a file from the host into the guest.
|
||||
rpc CopyIn(stream CopyInChunk) returns (CopyInResponse);
|
||||
// Copy a file from the guest to the host.
|
||||
rpc CopyOut(CopyOutRequest) returns (stream CopyOutChunk);
|
||||
|
||||
// Create a new process inside the container.
|
||||
rpc CreateProcess(CreateProcessRequest) returns (CreateProcessResponse);
|
||||
@@ -225,6 +229,45 @@ message WriteFileRequest {
|
||||
|
||||
message WriteFileResponse {}
|
||||
|
||||
message CopyInChunk {
|
||||
oneof content {
|
||||
// Initialization message (must be first).
|
||||
CopyInInit init = 1;
|
||||
// File data chunk.
|
||||
bytes data = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message CopyInInit {
|
||||
// Destination path in the guest.
|
||||
string path = 1;
|
||||
// File mode (defaults to 0644 if not set).
|
||||
uint32 mode = 2;
|
||||
// Create parent directories if they don't exist.
|
||||
bool create_parents = 3;
|
||||
}
|
||||
|
||||
message CopyInResponse {}
|
||||
|
||||
message CopyOutRequest {
|
||||
// Source path in the guest.
|
||||
string path = 1;
|
||||
}
|
||||
|
||||
message CopyOutChunk {
|
||||
oneof content {
|
||||
// Initialization message with metadata (first chunk).
|
||||
CopyOutInit init = 1;
|
||||
// File data chunk.
|
||||
bytes data = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message CopyOutInit {
|
||||
// Total file size in bytes.
|
||||
uint64 total_size = 1;
|
||||
}
|
||||
|
||||
message IpLinkSetRequest {
|
||||
string interface = 1;
|
||||
bool up = 2;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// 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.
|
||||
@@ -46,6 +46,27 @@ public protocol VirtualMachineAgent: Sendable {
|
||||
func sync() async throws
|
||||
func writeFile(path: String, data: Data, flags: WriteFileFlags, mode: UInt32) async throws
|
||||
|
||||
// File transfer
|
||||
|
||||
/// Copy a file from the host into the guest.
|
||||
func copyIn(
|
||||
from source: URL,
|
||||
to destination: URL,
|
||||
mode: UInt32,
|
||||
createParents: Bool,
|
||||
chunkSize: Int,
|
||||
progress: ProgressHandler?
|
||||
) async throws
|
||||
|
||||
/// Copy a file from the guest to the host.
|
||||
func copyOut(
|
||||
from source: URL,
|
||||
to destination: URL,
|
||||
createParents: Bool,
|
||||
chunkSize: Int,
|
||||
progress: ProgressHandler?
|
||||
) async throws
|
||||
|
||||
// Process lifecycle
|
||||
func createProcess(
|
||||
id: String,
|
||||
@@ -96,4 +117,25 @@ extension VirtualMachineAgent {
|
||||
public func sync() async throws {
|
||||
throw ContainerizationError(.unsupported, message: "sync")
|
||||
}
|
||||
|
||||
public func copyIn(
|
||||
from source: URL,
|
||||
to destination: URL,
|
||||
mode: UInt32,
|
||||
createParents: Bool,
|
||||
chunkSize: Int,
|
||||
progress: ProgressHandler?
|
||||
) async throws {
|
||||
throw ContainerizationError(.unsupported, message: "copyIn")
|
||||
}
|
||||
|
||||
public func copyOut(
|
||||
from source: URL,
|
||||
to destination: URL,
|
||||
createParents: Bool,
|
||||
chunkSize: Int,
|
||||
progress: ProgressHandler?
|
||||
) async throws {
|
||||
throw ContainerizationError(.unsupported, message: "copyOut")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// 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.
|
||||
@@ -409,6 +409,96 @@ extension Vminitd {
|
||||
})
|
||||
return response.result
|
||||
}
|
||||
|
||||
/// Copy a file from the host into the guest.
|
||||
public func copyIn(
|
||||
from source: URL,
|
||||
to destination: URL,
|
||||
mode: UInt32,
|
||||
createParents: Bool,
|
||||
chunkSize: Int,
|
||||
progress: ProgressHandler?
|
||||
) async throws {
|
||||
let fileHandle = try FileHandle(forReadingFrom: source)
|
||||
defer { try? fileHandle.close() }
|
||||
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: source.path)
|
||||
guard let fileSize = attrs[.size] as? Int64 else {
|
||||
throw ContainerizationError(
|
||||
.invalidArgument,
|
||||
message: "copyIn: failed to get file size for '\(source.path)'"
|
||||
)
|
||||
}
|
||||
|
||||
await progress?([ProgressEvent(event: "add-total-size", value: fileSize)])
|
||||
|
||||
let call = client.makeCopyInCall()
|
||||
|
||||
try await call.requestStream.send(
|
||||
.with {
|
||||
$0.content = .init_p(
|
||||
.with {
|
||||
$0.path = destination.path
|
||||
$0.mode = mode
|
||||
$0.createParents = createParents
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
var totalSent: Int64 = 0
|
||||
while true {
|
||||
guard let data = try fileHandle.read(upToCount: chunkSize), !data.isEmpty else {
|
||||
break
|
||||
}
|
||||
try await call.requestStream.send(.with { $0.content = .data(data) })
|
||||
totalSent += Int64(data.count)
|
||||
await progress?([ProgressEvent(event: "add-size", value: Int64(data.count))])
|
||||
}
|
||||
|
||||
call.requestStream.finish()
|
||||
_ = try await call.response
|
||||
}
|
||||
|
||||
/// Copy a file from the guest to the host.
|
||||
public func copyOut(
|
||||
from source: URL,
|
||||
to destination: URL,
|
||||
createParents: Bool,
|
||||
chunkSize: Int,
|
||||
progress: ProgressHandler?
|
||||
) async throws {
|
||||
let request = Com_Apple_Containerization_Sandbox_V3_CopyOutRequest.with {
|
||||
$0.path = source.path
|
||||
}
|
||||
|
||||
if createParents {
|
||||
let parentDir = destination.deletingLastPathComponent()
|
||||
try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
let fd = open(destination.path, O_WRONLY | O_CREAT | O_TRUNC, 0o644)
|
||||
guard fd != -1 else {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
message: "copyOut: failed to open '\(destination.path)': \(String(cString: strerror(errno)))"
|
||||
)
|
||||
}
|
||||
let fileHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
|
||||
defer { try? fileHandle.close() }
|
||||
|
||||
let stream = client.copyOut(request)
|
||||
for try await chunk in stream {
|
||||
switch chunk.content {
|
||||
case .init_p(let initMsg):
|
||||
await progress?([ProgressEvent(event: "add-total-size", value: Int64(initMsg.totalSize))])
|
||||
case .data(let data):
|
||||
try fileHandle.write(contentsOf: data)
|
||||
await progress?([ProgressEvent(event: "add-size", value: Int64(data.count))])
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Hosts {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// 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.
|
||||
@@ -1436,4 +1436,181 @@ extension IntegrationSuite {
|
||||
throw IntegrationError.assert(msg: "container with CAP_CHOWN should succeed, got exit code \(status.exitCode)")
|
||||
}
|
||||
}
|
||||
|
||||
func testCopyIn() async throws {
|
||||
let id = "test-copy-in"
|
||||
|
||||
let bs = try await bootstrap(id)
|
||||
|
||||
// Create a temp file on the host with known content
|
||||
let testContent = "Hello from the host! This is a copyIn test."
|
||||
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
|
||||
.appendingPathComponent("test-input.txt")
|
||||
try testContent.write(to: hostFile, atomically: true, encoding: .utf8)
|
||||
|
||||
let buffer = BufferWriter()
|
||||
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
|
||||
config.process.arguments = ["sleep", "100"]
|
||||
config.bootLog = bs.bootLog
|
||||
}
|
||||
|
||||
do {
|
||||
try await container.create()
|
||||
try await container.start()
|
||||
|
||||
// Copy the file into the container
|
||||
try await container.copyIn(
|
||||
from: hostFile,
|
||||
to: URL(filePath: "/tmp/copied-file.txt")
|
||||
)
|
||||
|
||||
// Verify the file exists and has correct content
|
||||
let exec = try await container.exec("verify-copy") { config in
|
||||
config.arguments = ["cat", "/tmp/copied-file.txt"]
|
||||
config.stdout = buffer
|
||||
}
|
||||
|
||||
try await exec.start()
|
||||
let status = try await exec.wait()
|
||||
try await exec.delete()
|
||||
|
||||
guard status.exitCode == 0 else {
|
||||
throw IntegrationError.assert(msg: "cat command failed with status \(status)")
|
||||
}
|
||||
|
||||
guard let output = String(data: buffer.data, encoding: .utf8) else {
|
||||
throw IntegrationError.assert(msg: "failed to convert output to UTF8")
|
||||
}
|
||||
|
||||
guard output == testContent else {
|
||||
throw IntegrationError.assert(
|
||||
msg: "copied file content mismatch: expected '\(testContent)', got '\(output)'")
|
||||
}
|
||||
|
||||
try await container.kill(SIGKILL)
|
||||
try await container.wait()
|
||||
try await container.stop()
|
||||
} catch {
|
||||
try? await container.stop()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func testCopyOut() async throws {
|
||||
let id = "test-copy-out"
|
||||
|
||||
let bs = try await bootstrap(id)
|
||||
|
||||
let testContent = "Hello from the guest! This is a copyOut test."
|
||||
let hostDestination = FileManager.default.uniqueTemporaryDirectory(create: true)
|
||||
.appendingPathComponent("test-output.txt")
|
||||
|
||||
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
|
||||
config.process.arguments = ["sleep", "100"]
|
||||
config.bootLog = bs.bootLog
|
||||
}
|
||||
|
||||
do {
|
||||
try await container.create()
|
||||
try await container.start()
|
||||
|
||||
// Create a file inside the container
|
||||
let exec = try await container.exec("create-file") { config in
|
||||
config.arguments = ["sh", "-c", "echo -n '\(testContent)' > /tmp/guest-file.txt"]
|
||||
}
|
||||
|
||||
try await exec.start()
|
||||
let status = try await exec.wait()
|
||||
try await exec.delete()
|
||||
|
||||
guard status.exitCode == 0 else {
|
||||
throw IntegrationError.assert(msg: "failed to create file in guest, status \(status)")
|
||||
}
|
||||
|
||||
// Copy the file out of the container
|
||||
try await container.copyOut(
|
||||
from: URL(filePath: "/tmp/guest-file.txt"),
|
||||
to: hostDestination
|
||||
)
|
||||
|
||||
// Verify the file was copied correctly
|
||||
let copiedContent = try String(contentsOf: hostDestination, encoding: .utf8)
|
||||
|
||||
guard copiedContent == testContent else {
|
||||
throw IntegrationError.assert(
|
||||
msg: "copied file content mismatch: expected '\(testContent)', got '\(copiedContent)'")
|
||||
}
|
||||
|
||||
try await container.kill(SIGKILL)
|
||||
try await container.wait()
|
||||
try await container.stop()
|
||||
} catch {
|
||||
try? await container.stop()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func testCopyLargeFile() async throws {
|
||||
let id = "test-copy-large-file"
|
||||
|
||||
let bs = try await bootstrap(id)
|
||||
|
||||
// Create a 10MB file on the host with a repeating pattern
|
||||
let fileSize = 10 * 1024 * 1024
|
||||
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
|
||||
.appendingPathComponent("large-file.bin")
|
||||
|
||||
// Generate data with a repeating pattern
|
||||
let pattern = Data("ContainerizationCopyTest".utf8)
|
||||
var testData = Data(capacity: fileSize)
|
||||
while testData.count < fileSize {
|
||||
testData.append(pattern)
|
||||
}
|
||||
testData = testData.prefix(fileSize)
|
||||
try testData.write(to: hostFile)
|
||||
|
||||
let hostDestination = FileManager.default.uniqueTemporaryDirectory(create: true)
|
||||
.appendingPathComponent("large-file-out.bin")
|
||||
|
||||
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
|
||||
config.process.arguments = ["sleep", "100"]
|
||||
config.bootLog = bs.bootLog
|
||||
}
|
||||
|
||||
do {
|
||||
try await container.create()
|
||||
try await container.start()
|
||||
|
||||
// Copy large file into the container
|
||||
try await container.copyIn(
|
||||
from: hostFile,
|
||||
to: URL(filePath: "/tmp/large-file.bin")
|
||||
)
|
||||
|
||||
// Copy it back out
|
||||
try await container.copyOut(
|
||||
from: URL(filePath: "/tmp/large-file.bin"),
|
||||
to: hostDestination
|
||||
)
|
||||
|
||||
// Verify the content matches
|
||||
let copiedData = try Data(contentsOf: hostDestination)
|
||||
|
||||
guard copiedData.count == testData.count else {
|
||||
throw IntegrationError.assert(
|
||||
msg: "file size mismatch: expected \(testData.count), got \(copiedData.count)")
|
||||
}
|
||||
|
||||
guard copiedData == testData else {
|
||||
throw IntegrationError.assert(msg: "file content mismatch after round-trip copy")
|
||||
}
|
||||
|
||||
try await container.kill(SIGKILL)
|
||||
try await container.wait()
|
||||
try await container.stop()
|
||||
} catch {
|
||||
try? await container.stop()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// 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.
|
||||
@@ -305,6 +305,9 @@ struct IntegrationSuite: AsyncParsableCommand {
|
||||
Test("container capabilities OCI default", testCapabilitiesOCIDefault),
|
||||
Test("container capabilities all capabilities", testCapabilitiesAllCapabilities),
|
||||
Test("container capabilities file ownership", testCapabilitiesFileOwnership),
|
||||
Test("container copy in", testCopyIn),
|
||||
Test("container copy out", testCopyOut),
|
||||
Test("container copy large file", testCopyLargeFile),
|
||||
|
||||
// Pods
|
||||
Test("pod single container", testPodSingleContainer),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// 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.
|
||||
@@ -306,6 +306,154 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
|
||||
return .init()
|
||||
}
|
||||
|
||||
// Chunk size for streaming file transfers (1MB).
|
||||
private static let copyChunkSize = 1024 * 1024
|
||||
|
||||
func copyIn(
|
||||
requestStream: GRPCAsyncRequestStream<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>,
|
||||
context: GRPC.GRPCAsyncServerCallContext
|
||||
) async throws -> Com_Apple_Containerization_Sandbox_V3_CopyInResponse {
|
||||
var fileHandle: FileHandle?
|
||||
var path: String = ""
|
||||
var totalBytes: Int = 0
|
||||
|
||||
do {
|
||||
for try await chunk in requestStream {
|
||||
switch chunk.content {
|
||||
case .init_p(let initMsg):
|
||||
path = initMsg.path
|
||||
log.debug(
|
||||
"copyIn",
|
||||
metadata: [
|
||||
"path": "\(path)",
|
||||
"mode": "\(initMsg.mode)",
|
||||
"createParents": "\(initMsg.createParents)",
|
||||
])
|
||||
|
||||
if initMsg.createParents {
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
let parentDir = fileURL.deletingLastPathComponent()
|
||||
try FileManager.default.createDirectory(
|
||||
at: parentDir,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
}
|
||||
|
||||
let mode = initMsg.mode > 0 ? mode_t(initMsg.mode) : mode_t(0o644)
|
||||
let fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
|
||||
guard fd != -1 else {
|
||||
throw GRPCStatus(
|
||||
code: .internalError,
|
||||
message: "copyIn: failed to open file '\(path)': \(swiftErrno("open"))"
|
||||
)
|
||||
}
|
||||
fileHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
|
||||
case .data(let bytes):
|
||||
guard let fh = fileHandle else {
|
||||
throw GRPCStatus(
|
||||
code: .failedPrecondition,
|
||||
message: "copyIn: received data before init message"
|
||||
)
|
||||
}
|
||||
if !bytes.isEmpty {
|
||||
try fh.write(contentsOf: bytes)
|
||||
totalBytes += bytes.count
|
||||
}
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"copyIn complete",
|
||||
metadata: [
|
||||
"path": "\(path)",
|
||||
"totalBytes": "\(totalBytes)",
|
||||
])
|
||||
|
||||
return .init()
|
||||
} catch {
|
||||
log.error(
|
||||
"copyIn",
|
||||
metadata: [
|
||||
"path": "\(path)",
|
||||
"error": "\(error)",
|
||||
])
|
||||
if error is GRPCStatus {
|
||||
throw error
|
||||
}
|
||||
throw GRPCStatus(
|
||||
code: .internalError,
|
||||
message: "copyIn: \(error)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func copyOut(
|
||||
request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
|
||||
responseStream: GRPCAsyncResponseStreamWriter<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>,
|
||||
context: GRPC.GRPCAsyncServerCallContext
|
||||
) async throws {
|
||||
let path = request.path
|
||||
log.debug(
|
||||
"copyOut",
|
||||
metadata: [
|
||||
"path": "\(path)"
|
||||
])
|
||||
|
||||
do {
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: path)
|
||||
guard let fileSize = attrs[.size] as? UInt64 else {
|
||||
throw GRPCStatus(
|
||||
code: .internalError,
|
||||
message: "copyOut: failed to get file size for '\(path)'"
|
||||
)
|
||||
}
|
||||
|
||||
let fileHandle = try FileHandle(forReadingFrom: fileURL)
|
||||
defer { try? fileHandle.close() }
|
||||
|
||||
// Send init message with total size.
|
||||
try await responseStream.send(
|
||||
.with {
|
||||
$0.content = .init_p(.with { $0.totalSize = fileSize })
|
||||
}
|
||||
)
|
||||
|
||||
var totalSent: UInt64 = 0
|
||||
while true {
|
||||
guard let data = try fileHandle.read(upToCount: Self.copyChunkSize), !data.isEmpty else {
|
||||
break
|
||||
}
|
||||
|
||||
try await responseStream.send(.with { $0.content = .data(data) })
|
||||
totalSent += UInt64(data.count)
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"copyOut complete",
|
||||
metadata: [
|
||||
"path": "\(path)",
|
||||
"totalBytes": "\(totalSent)",
|
||||
])
|
||||
} catch {
|
||||
log.error(
|
||||
"copyOut",
|
||||
metadata: [
|
||||
"path": "\(path)",
|
||||
"error": "\(error)",
|
||||
])
|
||||
if error is GRPCStatus {
|
||||
throw error
|
||||
}
|
||||
throw GRPCStatus(
|
||||
code: .internalError,
|
||||
message: "copyOut: \(error)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func mount(request: Com_Apple_Containerization_Sandbox_V3_MountRequest, context: GRPC.GRPCAsyncServerCallContext)
|
||||
async throws -> Com_Apple_Containerization_Sandbox_V3_MountResponse
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user