From 87541cbe656a8c80053bacf80def44152251dcb0 Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Mon, 11 Aug 2025 12:33:30 -0400 Subject: [PATCH] Agent: Add and implement writeFile (#259) This change adds in a new rpc that we probably should swap to for the underlying implementation of a couple rpcs we currently have. This new rpc simply writes to an existing or new file some data. This is useful for guest (and container) setup for things like /etc/hosts, /etc/resolv.conf, writing to one-off cgroup files and so on. Currently I've just implemented subtree_control toggling for the root cg in standardSetup with this new change, but in the future I'd like to swap the implementations of our /etc/hosts and /etc/resolv.conf logic with this. --- Sources/Containerization/Agent/Vminitd.swift | 23 +++ .../SandboxContext/SandboxContext.grpc.swift | 100 +++++++++++ .../SandboxContext/SandboxContext.pb.swift | 170 ++++++++++++++++++ .../SandboxContext/SandboxContext.proto | 16 ++ .../VirtualMachineAgent.swift | 13 +- vminitd/Sources/vminitd/Server+GRPC.swift | 59 ++++++ 6 files changed, 380 insertions(+), 1 deletion(-) diff --git a/Sources/Containerization/Agent/Vminitd.swift b/Sources/Containerization/Agent/Vminitd.swift index 53db0c59..bfa005cb 100644 --- a/Sources/Containerization/Agent/Vminitd.swift +++ b/Sources/Containerization/Agent/Vminitd.swift @@ -64,6 +64,29 @@ extension Vminitd: VirtualMachineAgent { for mount in mounts { try await self.mount(mount) } + + // Setup root cg subtree_control. + let data = "+memory +pids +io +cpu +cpuset".data(using: .utf8)! + try await writeFile( + path: "/sys/fs/cgroup/cgroup.subtree_control", + data: data, + flags: .init(), + mode: 0 + ) + } + + public func writeFile(path: String, data: Data, flags: WriteFileFlags, mode: UInt32) async throws { + _ = try await client.writeFile( + .with { + $0.path = path + $0.mode = mode + $0.data = data + $0.flags = .with { + $0.append = flags.append + $0.createIfMissing = flags.create + $0.createParentDirs = flags.createParentDirectories + } + }) } /// Mount a filesystem in the sandbox's environment. diff --git a/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift b/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift index 44ac6f48..4896f21f 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift @@ -74,6 +74,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtoc callOptions: CallOptions? ) -> UnaryCall + func writeFile( + _ request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, + callOptions: CallOptions? + ) -> UnaryCall + func createProcess( _ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, callOptions: CallOptions? @@ -309,6 +314,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtocol { ) } + /// Write data to an existing or new file. + /// + /// - Parameters: + /// - request: Request to send to WriteFile. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func writeFile( + _ request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.writeFile.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeWriteFileInterceptors() ?? [] + ) + } + /// Create a new process inside the container. /// /// - Parameters: @@ -720,6 +743,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientP callOptions: CallOptions? ) -> GRPCAsyncUnaryCall + func makeWriteFileCall( + _ request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + func makeCreateProcessCall( _ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, callOptions: CallOptions? @@ -912,6 +940,18 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco ) } + public func makeWriteFileCall( + _ request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.writeFile.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeWriteFileInterceptors() ?? [] + ) + } + public func makeCreateProcessCall( _ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, callOptions: CallOptions? = nil @@ -1215,6 +1255,18 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco ) } + public func writeFile( + _ request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, + callOptions: CallOptions? = nil + ) async throws -> Com_Apple_Containerization_Sandbox_V3_WriteFileResponse { + return try await self.performAsyncUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.writeFile.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeWriteFileInterceptors() ?? [] + ) + } + public func createProcess( _ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, callOptions: CallOptions? = nil @@ -1463,6 +1515,9 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientInterc /// - Returns: Interceptors to use when invoking 'setupEmulator'. func makeSetupEmulatorInterceptors() -> [ClientInterceptor] + /// - Returns: Interceptors to use when invoking 'writeFile'. + func makeWriteFileInterceptors() -> [ClientInterceptor] + /// - Returns: Interceptors to use when invoking 'createProcess'. func makeCreateProcessInterceptors() -> [ClientInterceptor] @@ -1528,6 +1583,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata { Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.sysctl, 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.createProcess, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.deleteProcess, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.startProcess, @@ -1597,6 +1653,12 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata { type: GRPCCallType.unary ) + public static let writeFile = GRPCMethodDescriptor( + name: "WriteFile", + path: "/com.apple.containerization.sandbox.v3.SandboxContext/WriteFile", + type: GRPCCallType.unary + ) + public static let createProcess = GRPCMethodDescriptor( name: "CreateProcess", path: "/com.apple.containerization.sandbox.v3.SandboxContext/CreateProcess", @@ -1731,6 +1793,9 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider: Ca /// Set up an emulator in the guest for a specific binary format. func setupEmulator(request: Com_Apple_Containerization_Sandbox_V3_SetupEmulatorRequest, context: StatusOnlyCallContext) -> EventLoopFuture + /// Write data to an existing or new file. + func writeFile(request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, context: StatusOnlyCallContext) -> EventLoopFuture + /// Create a new process inside the container. func createProcess(request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, context: StatusOnlyCallContext) -> EventLoopFuture @@ -1868,6 +1933,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider { userFunction: self.setupEmulator(request:context:) ) + case "WriteFile": + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeWriteFileInterceptors() ?? [], + userFunction: self.writeFile(request:context:) + ) + case "CreateProcess": return UnaryServerHandler( context: context, @@ -2083,6 +2157,12 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvide context: GRPCAsyncServerCallContext ) async throws -> Com_Apple_Containerization_Sandbox_V3_SetupEmulatorResponse + /// Write data to an existing or new file. + func writeFile( + request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Com_Apple_Containerization_Sandbox_V3_WriteFileResponse + /// Create a new process inside the container. func createProcess( request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, @@ -2278,6 +2358,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvider { wrapping: { try await self.setupEmulator(request: $0, context: $1) } ) + case "WriteFile": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeWriteFileInterceptors() ?? [], + wrapping: { try await self.writeFile(request: $0, context: $1) } + ) + case "CreateProcess": return GRPCAsyncServerHandler( context: context, @@ -2471,6 +2560,10 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextServerInterc /// Defaults to calling `self.makeInterceptors()`. func makeSetupEmulatorInterceptors() -> [ServerInterceptor] + /// - Returns: Interceptors to use when handling 'writeFile'. + /// Defaults to calling `self.makeInterceptors()`. + func makeWriteFileInterceptors() -> [ServerInterceptor] + /// - Returns: Interceptors to use when handling 'createProcess'. /// Defaults to calling `self.makeInterceptors()`. func makeCreateProcessInterceptors() -> [ServerInterceptor] @@ -2553,6 +2646,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata { Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.sysctl, 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.createProcess, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.deleteProcess, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.startProcess, @@ -2622,6 +2716,12 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata { type: GRPCCallType.unary ) + public static let writeFile = GRPCMethodDescriptor( + name: "WriteFile", + path: "/com.apple.containerization.sandbox.v3.SandboxContext/WriteFile", + type: GRPCCallType.unary + ) + public static let createProcess = GRPCMethodDescriptor( name: "CreateProcess", path: "/com.apple.containerization.sandbox.v3.SandboxContext/CreateProcess", diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index 3a72fc83..90ea72f4 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -682,6 +682,59 @@ public struct Com_Apple_Containerization_Sandbox_V3_MkdirResponse: Sendable { public init() {} } +public struct Com_Apple_Containerization_Sandbox_V3_WriteFileRequest: @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 path: String = String() + + public var data: Data = Data() + + public var mode: UInt32 = 0 + + public var flags: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest.WriteFileFlags { + get {return _flags ?? Com_Apple_Containerization_Sandbox_V3_WriteFileRequest.WriteFileFlags()} + set {_flags = newValue} + } + /// Returns true if `flags` has been explicitly set. + public var hasFlags: Bool {return self._flags != nil} + /// Clears the value of `flags`. Subsequent reads from it will return its default value. + public mutating func clearFlags() {self._flags = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public struct WriteFileFlags: 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 createParentDirs: Bool = false + + public var append: Bool = false + + public var createIfMissing: Bool = false + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + + public init() {} + + fileprivate var _flags: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest.WriteFileFlags? = nil +} + +public struct Com_Apple_Containerization_Sandbox_V3_WriteFileResponse: 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_IpLinkSetRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -2152,6 +2205,123 @@ extension Com_Apple_Containerization_Sandbox_V3_MkdirResponse: SwiftProtobuf.Mes } } +extension Com_Apple_Containerization_Sandbox_V3_WriteFileRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".WriteFileRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "path"), + 2: .same(proto: "data"), + 3: .same(proto: "mode"), + 4: .same(proto: "flags"), + ] + + public mutating func decodeMessage(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.decodeSingularBytesField(value: &self.data) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.mode) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._flags) }() + default: break + } + } + } + + public func traverse(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 + if !self.path.isEmpty { + try visitor.visitSingularStringField(value: self.path, fieldNumber: 1) + } + if !self.data.isEmpty { + try visitor.visitSingularBytesField(value: self.data, fieldNumber: 2) + } + if self.mode != 0 { + try visitor.visitSingularUInt32Field(value: self.mode, fieldNumber: 3) + } + try { if let v = self._flags { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, rhs: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest) -> Bool { + if lhs.path != rhs.path {return false} + if lhs.data != rhs.data {return false} + if lhs.mode != rhs.mode {return false} + if lhs._flags != rhs._flags {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Apple_Containerization_Sandbox_V3_WriteFileRequest.WriteFileFlags: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Com_Apple_Containerization_Sandbox_V3_WriteFileRequest.protoMessageName + ".WriteFileFlags" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "create_parent_dirs"), + 2: .same(proto: "append"), + 3: .standard(proto: "create_if_missing"), + ] + + public mutating func decodeMessage(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.decodeSingularBoolField(value: &self.createParentDirs) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.append) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.createIfMissing) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.createParentDirs != false { + try visitor.visitSingularBoolField(value: self.createParentDirs, fieldNumber: 1) + } + if self.append != false { + try visitor.visitSingularBoolField(value: self.append, fieldNumber: 2) + } + if self.createIfMissing != false { + try visitor.visitSingularBoolField(value: self.createIfMissing, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest.WriteFileFlags, rhs: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest.WriteFileFlags) -> Bool { + if lhs.createParentDirs != rhs.createParentDirs {return false} + if lhs.append != rhs.append {return false} + if lhs.createIfMissing != rhs.createIfMissing {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Apple_Containerization_Sandbox_V3_WriteFileResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".WriteFileResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_WriteFileResponse, rhs: Com_Apple_Containerization_Sandbox_V3_WriteFileResponse) -> Bool { + 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 = [ diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index d771710c..4cdc76b4 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -20,6 +20,8 @@ service SandboxContext { rpc SetTime(SetTimeRequest) returns (SetTimeResponse); // Set up an emulator in the guest for a specific binary format. rpc SetupEmulator(SetupEmulatorRequest) returns (SetupEmulatorResponse); + // Write data to an existing or new file. + rpc WriteFile(WriteFileRequest) returns (WriteFileResponse); // Create a new process inside the container. rpc CreateProcess(CreateProcessRequest) returns (CreateProcessResponse); @@ -201,6 +203,20 @@ message MkdirRequest { message MkdirResponse {} +message WriteFileRequest { + message WriteFileFlags { + bool create_parent_dirs = 1; + bool append = 2; + bool create_if_missing = 3; + } + string path = 1; + bytes data = 2; + uint32 mode = 3; + WriteFileFlags flags = 4; +} + +message WriteFileResponse {} + message IpLinkSetRequest { string interface = 1; bool up = 2; diff --git a/Sources/Containerization/VirtualMachineAgent.swift b/Sources/Containerization/VirtualMachineAgent.swift index 0d9a8d40..ec6029de 100644 --- a/Sources/Containerization/VirtualMachineAgent.swift +++ b/Sources/Containerization/VirtualMachineAgent.swift @@ -18,6 +18,12 @@ import ContainerizationError import ContainerizationOCI import Foundation +public struct WriteFileFlags { + public var createParentDirectories = false + public var append = false + public var create = false +} + /// A protocol for the agent running inside a virtual machine. If an operation isn't /// supported the implementation MUST return a ContainerizationError with a code of /// `.unsupported`. @@ -28,7 +34,7 @@ public protocol VirtualMachineAgent: Sendable { /// Close any resources held by the agent. func close() async throws - // POSIX + // POSIX-y func getenv(key: String) async throws -> String func setenv(key: String, value: String) async throws func mount(_ mount: ContainerizationOCI.Mount) async throws @@ -36,6 +42,7 @@ public protocol VirtualMachineAgent: Sendable { func mkdir(path: String, all: Bool, perms: UInt32) async throws @discardableResult func kill(pid: Int32, signal: Int32) async throws -> Int32 + func writeFile(path: String, data: Data, flags: WriteFileFlags, mode: UInt32) async throws // Process lifecycle func createProcess( @@ -71,4 +78,8 @@ extension VirtualMachineAgent { public func configureHosts(config: Hosts, location: String) async throws { throw ContainerizationError(.unsupported, message: "configureHosts") } + + public func writeFile(path: String, data: Data, flags: WriteFileFlags, mode: UInt32) async throws { + throw ContainerizationError(.unsupported, message: "writeFile") + } } diff --git a/vminitd/Sources/vminitd/Server+GRPC.swift b/vminitd/Sources/vminitd/Server+GRPC.swift index b9b71f10..9daf23e5 100644 --- a/vminitd/Sources/vminitd/Server+GRPC.swift +++ b/vminitd/Sources/vminitd/Server+GRPC.swift @@ -245,6 +245,65 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid return .init() } + func writeFile(request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, context: GRPC.GRPCAsyncServerCallContext) + async throws -> Com_Apple_Containerization_Sandbox_V3_WriteFileResponse + { + log.debug( + "writeFile", + metadata: [ + "path": "\(request.path)", + "mode": "\(request.mode)", + "dataSize": "\(request.data.count)", + ]) + + do { + if request.flags.createParentDirs { + let fileURL = URL(fileURLWithPath: request.path) + let parentDir = fileURL.deletingLastPathComponent() + try FileManager.default.createDirectory( + at: parentDir, + withIntermediateDirectories: true + ) + } + + var flags = O_WRONLY + if request.flags.createIfMissing { + flags |= O_CREAT + } + if request.flags.append { + flags |= O_APPEND + } + + let mode = request.mode > 0 ? mode_t(request.mode) : mode_t(0644) + let fd = open(request.path, flags, mode) + guard fd != -1 else { + let error = swiftErrno("open") + throw GRPCStatus( + code: .internalError, + message: "writeFile: failed to open file: \(error)" + ) + } + + let fh = FileHandle(fileDescriptor: fd, closeOnDealloc: true) + try fh.write(contentsOf: request.data) + } catch { + log.error( + "writeFile", + metadata: [ + "error": "\(error)" + ]) + if error is GRPCStatus { + throw error + } + throw GRPCStatus( + code: .internalError, + message: "writeFile: \(error)" + ) + } + + return .init() + } + func mount(request: Com_Apple_Containerization_Sandbox_V3_MountRequest, context: GRPC.GRPCAsyncServerCallContext) async throws -> Com_Apple_Containerization_Sandbox_V3_MountResponse {