809 lines · 31.5 KB · Swift
RawDownload
#if canImport(Darwin)import Darwin#elseif canImport(Glibc)import Glibc#elseif canImport(Musl)import Musl#endifimport Foundation package final class SpawnedProcessGroup: @unchecked Sendable {    package enum LaunchError: LocalizedError {        case setupFailed(String)        case spawnFailed(String)         package var errorDescription: String? {            switch self {            case let .setupFailed(details):                "Failed to prepare process: \(details)"            case let .spawnFailed(details):                "Failed to launch process: \(details)"            }        }    }     private final class TerminationState: @unchecked Sendable {        private let condition = NSCondition()        private var exitObserved = false        private var reapRequested = false        private var status: Int32?         var hasObservedExit: Bool {            self.condition.withLock { self.exitObserved }        }         var value: Int32? {            self.condition.withLock { self.status }        }         func observeExit() {            self.condition.withLock {                self.exitObserved = true                self.condition.broadcast()            }        }         func requestReap() {            self.condition.withLock {                self.reapRequested = true                self.condition.broadcast()            }        }         func waitForReapRequest(timeout: TimeInterval) {            let deadline = Date().addingTimeInterval(timeout)            self.condition.lock()            while !self.reapRequested, self.condition.wait(until: deadline) {}            self.condition.unlock()        }         func resolve(_ status: Int32) {            self.condition.withLock {                guard self.status == nil else { return }                self.status = status                self.condition.broadcast()            }        }    }     private final class ProcessIdentityState: @unchecked Sendable {        private let lock = NSLock()        private var identities: Set<TTYProcessTreeTerminator.ProcessIdentity> = []         var snapshot: Set<TTYProcessTreeTerminator.ProcessIdentity> {            self.lock.withLock { self.identities }        }         func formUnion(_ identities: Set<TTYProcessTreeTerminator.ProcessIdentity>) {            self.lock.withLock {                self.identities.formUnion(identities)            }        }    }     private struct OutputPipeIdentity: Hashable {        #if canImport(Darwin)        let firstHandle: UInt64        let secondHandle: UInt64        #else        let inode: UInt64        #endif         static func resolve(fileDescriptor: Int32) -> OutputPipeIdentity? {            #if canImport(Darwin)            var info = pipe_fdinfo()            let byteCount = proc_pidfdinfo(                getpid(),                fileDescriptor,                PROC_PIDFDPIPEINFO,                &info,                Int32(MemoryLayout<pipe_fdinfo>.size))            guard byteCount == MemoryLayout<pipe_fdinfo>.size else { return nil }            let handles = [info.pipeinfo.pipe_handle, info.pipeinfo.pipe_peerhandle].sorted()            guard handles[0] != 0, handles[1] != 0 else { return nil }            return OutputPipeIdentity(firstHandle: handles[0], secondHandle: handles[1])            #else            var info = stat()            guard fstat(fileDescriptor, &info) == 0 else { return nil }            return OutputPipeIdentity(inode: UInt64(info.st_ino))            #endif        }         static func holderPIDs(for pipes: Set<OutputPipeIdentity>) -> Set<pid_t> {            guard !pipes.isEmpty else { return [] }            #if canImport(Darwin)            return Set(SpawnedProcessGroup.allProcessIDs().filter { self.process(pid: $0, holdsAny: pipes) })            #else            let targets = Set(pipes.map { "pipe:[\($0.inode)]" })            return Set(SpawnedProcessGroup.allProcessIDs().filter { pid in                let directory = "/proc/\(pid)/fd"                guard let descriptors = try? FileManager.default.contentsOfDirectory(atPath: directory) else {                    return false                }                return descriptors.contains { descriptor in                    let path = "\(directory)/\(descriptor)"                    guard let target = try? FileManager.default.destinationOfSymbolicLink(atPath: path) else {                        return false                    }                    return targets.contains(target)                }            })            #endif        }         #if canImport(Darwin)        private static func process(pid: pid_t, holdsAny pipes: Set<OutputPipeIdentity>) -> Bool {            let requiredBytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)            guard requiredBytes > 0 else { return false }            let stride = MemoryLayout<proc_fdinfo>.stride            var descriptors = [proc_fdinfo](                repeating: proc_fdinfo(),                count: Int(requiredBytes) / stride + 8)            let actualBytes = descriptors.withUnsafeMutableBytes { buffer in                proc_pidinfo(                    pid,                    PROC_PIDLISTFDS,                    0,                    buffer.baseAddress,                    Int32(buffer.count))            }            guard actualBytes > 0 else { return false }             for descriptor in descriptors.prefix(Int(actualBytes) / stride)                where descriptor.proc_fdtype == PROX_FDTYPE_PIPE            {                var info = pipe_fdinfo()                let byteCount = proc_pidfdinfo(                    pid,                    descriptor.proc_fd,                    PROC_PIDFDPIPEINFO,                    &info,                    Int32(MemoryLayout<pipe_fdinfo>.size))                guard byteCount == MemoryLayout<pipe_fdinfo>.size else { continue }                let handles = [info.pipeinfo.pipe_handle, info.pipeinfo.pipe_peerhandle].sorted()                let identity = OutputPipeIdentity(firstHandle: handles[0], secondHandle: handles[1])                if pipes.contains(identity) {                    return true                }            }            return false        }        #endif    }     private struct OutputTTYIdentity: Hashable {        let device: UInt64        let inode: UInt64        let rawDevice: UInt64         static func resolve(fileDescriptor: Int32) -> OutputTTYIdentity? {            var info = stat()            guard fstat(fileDescriptor, &info) == 0 else { return nil }            #if canImport(Darwin)            let device = SpawnedProcessGroup.darwinDeviceIdentifier(info.st_dev)            let rawDevice = SpawnedProcessGroup.darwinDeviceIdentifier(info.st_rdev)            #else            let device = UInt64(info.st_dev)            let rawDevice = UInt64(info.st_rdev)            #endif            return OutputTTYIdentity(                device: device,                inode: UInt64(info.st_ino),                rawDevice: rawDevice)        }         static func holderPIDs(for terminals: Set<OutputTTYIdentity>) -> Set<pid_t> {            guard !terminals.isEmpty else { return [] }            #if canImport(Darwin)            return Set(SpawnedProcessGroup.allProcessIDs().filter { self.process(pid: $0, holdsAny: terminals) })            #else            return Set(SpawnedProcessGroup.allProcessIDs().filter { pid in                let directory = "/proc/\(pid)/fd"                guard let descriptors = try? FileManager.default.contentsOfDirectory(atPath: directory) else {                    return false                }                return descriptors.contains { descriptor in                    var info = stat()                    let path = "\(directory)/\(descriptor)"                    guard path.withCString({ fstatat(AT_FDCWD, $0, &info, 0) }) == 0 else { return false }                    let identity = OutputTTYIdentity(                        device: UInt64(info.st_dev),                        inode: UInt64(info.st_ino),                        rawDevice: UInt64(info.st_rdev))                    return terminals.contains(identity)                }            })            #endif        }         #if canImport(Darwin)        private static func process(pid: pid_t, holdsAny terminals: Set<OutputTTYIdentity>) -> Bool {            let requiredBytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)            guard requiredBytes > 0 else { return false }            let stride = MemoryLayout<proc_fdinfo>.stride            var descriptors = [proc_fdinfo](                repeating: proc_fdinfo(),                count: Int(requiredBytes) / stride + 8)            let actualBytes = descriptors.withUnsafeMutableBytes { buffer in                proc_pidinfo(                    pid,                    PROC_PIDLISTFDS,                    0,                    buffer.baseAddress,                    Int32(buffer.count))            }            guard actualBytes > 0 else { return false }             for descriptor in descriptors.prefix(Int(actualBytes) / stride)                where descriptor.proc_fdtype == PROX_FDTYPE_VNODE            {                var info = vnode_fdinfo()                let byteCount = proc_pidfdinfo(                    pid,                    descriptor.proc_fd,                    PROC_PIDFDVNODEINFO,                    &info,                    Int32(MemoryLayout<vnode_fdinfo>.size))                guard byteCount == MemoryLayout<vnode_fdinfo>.size else { continue }                let stats = info.pvi.vi_stat                let identity = OutputTTYIdentity(                    device: UInt64(stats.vst_dev),                    inode: stats.vst_ino,                    rawDevice: UInt64(stats.vst_rdev))                if terminals.contains(identity) {                    return true                }            }            return false        }        #endif    }     private static func allProcessIDs() -> [pid_t] {        #if canImport(Darwin)        return self.processIDs(type: UInt32(PROC_ALL_PIDS), typeInfo: 0)        #else        guard let entries = try? FileManager.default.contentsOfDirectory(atPath: "/proc") else { return [] }        return entries.compactMap(pid_t.init)        #endif    }     #if canImport(Darwin)    /// Darwin exposes `stat` device IDs as signed values while vnode inspection uses the same bits unsigned.    package static func darwinDeviceIdentifier(_ value: Int32) -> UInt64 {        UInt64(UInt32(bitPattern: value))    }    #endif     private static func processIDs(inProcessGroup processGroup: pid_t) -> [pid_t] {        #if canImport(Darwin)        return self.processIDs(type: UInt32(PROC_PGRP_ONLY), typeInfo: UInt32(bitPattern: processGroup))        #else        return self.allProcessIDs().filter { getpgid($0) == processGroup }        #endif    }     #if canImport(Darwin)    private static func processIDs(type: UInt32, typeInfo: UInt32) -> [pid_t] {        let requiredBytes = proc_listpids(type, typeInfo, nil, 0)        guard requiredBytes > 0 else { return [] }        let stride = MemoryLayout<pid_t>.stride        var pids = [pid_t](repeating: 0, count: Int(requiredBytes) / stride + 32)        let actualBytes = pids.withUnsafeMutableBytes { buffer in            proc_listpids(                type,                typeInfo,                buffer.baseAddress,                Int32(buffer.count))        }        guard actualBytes > 0 else { return [] }        return Array(pids.prefix(Int(actualBytes) / stride)).filter { $0 > 0 }    }    #endif     package let pid: pid_t    package let processGroup: pid_t    private let termination = TerminationState()    private let observedProcessGroupMembers = ProcessIdentityState()    private let outputPipes: Set<OutputPipeIdentity>    private let outputTTYs: Set<OutputTTYIdentity>    private let rootIdentity: TTYProcessTreeTerminator.ProcessIdentity?     private init(        pid: pid_t,        outputPipes: Set<OutputPipeIdentity>,        outputTTYs: Set<OutputTTYIdentity> = [])    {        self.pid = pid        self.processGroup = pid        self.outputPipes = outputPipes        self.outputTTYs = outputTTYs        self.rootIdentity = TTYProcessTreeTerminator.processIdentity(for: pid)        self.startWaiter()    }     package static func launch(        binary: String,        arguments: [String],        environment: [String: String],        stdoutPipe: Pipe,        stderrPipe: Pipe) throws -> SpawnedProcessGroup    {        #if canImport(Darwin)        var fileActions: posix_spawn_file_actions_t?        #else        var fileActions = posix_spawn_file_actions_t()        #endif        guard posix_spawn_file_actions_init(&fileActions) == 0 else {            throw LaunchError.setupFailed("posix_spawn_file_actions_init")        }        defer { posix_spawn_file_actions_destroy(&fileActions) }         let stdoutRead = stdoutPipe.fileHandleForReading.fileDescriptor        let stdoutWrite = stdoutPipe.fileHandleForWriting.fileDescriptor        let stderrRead = stderrPipe.fileHandleForReading.fileDescriptor        let stderrWrite = stderrPipe.fileHandleForWriting.fileDescriptor        let outputPipes = Set(            [stdoutRead, stderrRead].compactMap(OutputPipeIdentity.resolve(fileDescriptor:)))        var fileActionResults = [            posix_spawn_file_actions_addopen(&fileActions, STDIN_FILENO, "/dev/null", O_RDONLY, 0),            posix_spawn_file_actions_adddup2(&fileActions, stdoutWrite, STDOUT_FILENO),            posix_spawn_file_actions_adddup2(&fileActions, stderrWrite, STDERR_FILENO),        ]        for descriptor in Self.pipeDescriptorsToClose([stdoutRead, stdoutWrite, stderrRead, stderrWrite]) {            fileActionResults.append(posix_spawn_file_actions_addclose(&fileActions, descriptor))        }        #if canImport(Glibc) || canImport(Musl)        do {            try PosixSpawnFileActionsCloseFrom.addCloseFrom(                &fileActions,                startingAt: STDERR_FILENO + 1)        } catch {            throw LaunchError.setupFailed(error.localizedDescription)        }        #endif        guard fileActionResults.allSatisfy({ $0 == 0 }) else {            throw LaunchError.setupFailed("posix_spawn file actions")        }         #if canImport(Darwin)        var attributes: posix_spawnattr_t?        #else        var attributes = posix_spawnattr_t()        #endif        guard posix_spawnattr_init(&attributes) == 0 else {            throw LaunchError.setupFailed("posix_spawnattr_init")        }        defer { posix_spawnattr_destroy(&attributes) }         #if canImport(Darwin)        let flags = POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_CLOEXEC_DEFAULT        #else        let flags = POSIX_SPAWN_SETPGROUP        #endif        guard posix_spawnattr_setflags(&attributes, Int16(flags)) == 0,              posix_spawnattr_setpgroup(&attributes, 0) == 0        else {            throw LaunchError.setupFailed("posix_spawn process group")        }         var cArguments: [UnsafeMutablePointer<CChar>?] = ([binary] + arguments).map { strdup($0) }        cArguments.append(nil)        defer {            for argument in cArguments {                free(argument)            }        }         var cEnvironment: [UnsafeMutablePointer<CChar>?] = environment.map { key, value in            strdup("\(key)=\(value)")        }        cEnvironment.append(nil)        defer {            for entry in cEnvironment {                free(entry)            }        }         var pid: pid_t = 0        let spawnResult = binary.withCString { path in            posix_spawn(&pid, path, &fileActions, &attributes, cArguments, cEnvironment)        }        stdoutPipe.fileHandleForWriting.closeFile()        stderrPipe.fileHandleForWriting.closeFile()        guard spawnResult == 0 else {            throw LaunchError.spawnFailed(String(cString: strerror(spawnResult)))        }        return SpawnedProcessGroup(pid: pid, outputPipes: outputPipes)    }     package static func launchPTY(        binary: String,        arguments: [String],        environment: [String: String],        workingDirectory: URL?,        fileDescriptors: (primary: Int32, secondary: Int32)) throws -> SpawnedProcessGroup    {        let primaryFD = fileDescriptors.primary        let secondaryFD = fileDescriptors.secondary        guard let outputTTY = OutputTTYIdentity.resolve(fileDescriptor: secondaryFD) else {            throw LaunchError.setupFailed("resolve PTY identity")        }        #if canImport(Darwin)        var fileActions: posix_spawn_file_actions_t?        #else        var fileActions = posix_spawn_file_actions_t()        #endif        guard posix_spawn_file_actions_init(&fileActions) == 0 else {            throw LaunchError.setupFailed("posix_spawn_file_actions_init")        }        defer { posix_spawn_file_actions_destroy(&fileActions) }         var fileActionResults = [            posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, STDIN_FILENO),            posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, STDOUT_FILENO),            posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, STDERR_FILENO),        ]        for descriptor in Self.pipeDescriptorsToClose([primaryFD, secondaryFD]) {            fileActionResults.append(posix_spawn_file_actions_addclose(&fileActions, descriptor))        }        if let workingDirectory {            fileActionResults.append(workingDirectory.path.withCString { path in                posix_spawn_file_actions_addchdir_np(&fileActions, path)            })        }        #if canImport(Glibc) || canImport(Musl)        do {            try PosixSpawnFileActionsCloseFrom.addCloseFrom(                &fileActions,                startingAt: STDERR_FILENO + 1)        } catch {            throw LaunchError.setupFailed(error.localizedDescription)        }        #endif        guard fileActionResults.allSatisfy({ $0 == 0 }) else {            throw LaunchError.setupFailed("posix_spawn PTY file actions")        }         #if canImport(Darwin)        var attributes: posix_spawnattr_t?        #else        var attributes = posix_spawnattr_t()        #endif        guard posix_spawnattr_init(&attributes) == 0 else {            throw LaunchError.setupFailed("posix_spawnattr_init")        }        defer { posix_spawnattr_destroy(&attributes) }         #if canImport(Darwin)        let flags = POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_CLOEXEC_DEFAULT        #else        let flags = POSIX_SPAWN_SETPGROUP        #endif        guard posix_spawnattr_setflags(&attributes, Int16(flags)) == 0,              posix_spawnattr_setpgroup(&attributes, 0) == 0        else {            throw LaunchError.setupFailed("posix_spawn PTY process group")        }         var cArguments: [UnsafeMutablePointer<CChar>?] = ([binary] + arguments).map { strdup($0) }        cArguments.append(nil)        defer {            for argument in cArguments {                free(argument)            }        }         var cEnvironment: [UnsafeMutablePointer<CChar>?] = environment.map { key, value in            strdup("\(key)=\(value)")        }        cEnvironment.append(nil)

Lines 1–500 of 809.