containerization-framework 0.3.0

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
//===----------------------------------------------------------------------===//
// Descriptors as the streams Containerization reads and writes.
//
// `LinuxProcessConfiguration` takes a `ReaderStream` and `Writer`s; Rust passes
// descriptors. Each is duplicated here, so the stream owns its copy and the
// caller keeps theirs.
//===----------------------------------------------------------------------===//

import Containerization
import Foundation
import Synchronization

/// A duplicate of `descriptor`, closed with the handle.
func duplicate(_ descriptor: Int32) throws -> FileHandle {
  let copy = dup(descriptor)

  guard copy >= 0 else {
    throw BridgeError.malformed("descriptor", String(descriptor))
  }

  return FileHandle(fileDescriptor: copy, closeOnDealloc: true)
}

/// A descriptor the guest reads, streamed until it reaches end of file.
///
/// Unchecked: `FileHandle` is not `Sendable`, and only the readability handler
/// touches it, on one queue at a time.
final class FileReader: ReaderStream, @unchecked Sendable {
  private let handle: FileHandle

  init(_ handle: FileHandle) {
    self.handle = handle
  }

  func stream() -> AsyncStream<Data> {
    .init { continuation in
      handle.readabilityHandler = { handle in
        let data = handle.availableData

        // Empty means end of file. Containerization closes the guest's
        // stdin once the stream finishes.
        guard !data.isEmpty else {
          handle.readabilityHandler = nil
          continuation.finish()
          return
        }

        continuation.yield(data)
      }
    }
  }
}

/// A descriptor the guest writes. Locked so stdout and stderr chunks never
/// interleave mid-write when they share one.
final class FileWriter: Writer, Sendable {
  private let handle: Mutex<FileHandle>

  init(_ handle: FileHandle) {
    self.handle = Mutex(handle)
  }

  func write(_ data: Data) throws {
    try handle.withLock { try $0.write(contentsOf: data) }
  }

  func close() throws {
    try handle.withLock { try $0.close() }
  }
}