containerization-framework 0.2.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.
//
// `Terminal` is already both a `ReaderStream` and a `Writer`, so it needs none
// of this. These cover every other stream: a build step's log, a caller's
// stdin, stdout and stderr, and the stdout of a process reading a terminal.
//===----------------------------------------------------------------------===//

import Containerization
import Foundation
import Synchronization

/// A descriptor as a handle, which neither closes nor is closed by the
/// descriptor; whoever wraps it says when it closes. Negative is no descriptor:
/// a stream the guest process leaves unattached.
func handle(_ descriptor: Int32) -> FileHandle? {
    descriptor < 0 ? nil : FileHandle(fileDescriptor: descriptor, closeOnDealloc: false)
}

/// 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)
            }
        }
    }

    /// Drops the handler and closes the descriptor, so a process that exits
    /// with input unread leaves nothing reading.
    func close() {
        handle.readabilityHandler = nil
        try? handle.close()
    }
}

/// 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>
    /// Whether `close()` closes the handle. This process's own stderr outlives
    /// every writer that logs to it.
    private let owned: Bool

    init(_ handle: FileHandle, owned: Bool = false) {
        self.handle = Mutex(handle)
        self.owned = owned
    }

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

    func line(_ text: String) {
        try? write(Data("\(text)\n".utf8))
    }

    func close() throws {
        guard owned else { return }

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