Swift Pseudoterminal

I'm creating a CLI library for Swift and I'm trying to provide an abstraction around opening a Pseudoterminal.

The reason I can't use Process for this is that I'd like an option similar to php's passthru where all output (including request for credentials, full output of git commands, etc) gets forwarded to the terminal the Swift script is running in.

I've actually got a rudimentary version working on macOS (thanks ChatGPT!) but am having some trouble getting it working on Linux.

import Foundation

#if os(Linux)
import Glibc

func posix_openpt(_ oflag: Int32) -> Int32 {
    return open("/dev/ptmx", oflag)
}

// ERROR: Always results in an EIO error.
func grantpt(_ fd: Int32) -> Int32 {
    return ioctl(fd, UInt(TIOCGPTPEER), (PTM_RDWR | O_NOCTTY | O_CLOEXEC))
}

func unlockpt(_ fd: Int32) -> Int32 {
    var unlock: Int32 = 0
    return ioctl(fd, UInt(TIOCSPTLCK), &unlock)
}

let TIOCGPTPEER: Int32 = 0x5441
let TIOCSPTLCK: Int32 = 0x40045431
let PTM_RDWR: Int32 = 0x0002

func ptsname(_ fd: Int32) -> String? {
    var ptyNumber: Int32 = 0
    while true {
        let ptyName = "/dev/pts/\(ptyNumber)"
        if access(ptyName, F_OK) == 0 {
            return ptyName
        }
        ptyNumber += 1
    }
}

#else
import Darwin
#endif

passthru("sudo echo 'hello!'")
passthru("git clone https://github.com/alchemy-swift/alchemy-examples")

func passthru(_ command: String) {
    let masterFD = posix_openpt(O_RDWR | O_NOCTTY)
    if masterFD == -1 {
        print("Failed to open PTY")
        return
    }

    if grantpt(masterFD) == -1 {
        print("Failed to grantpt.")
        return
    }

    if unlockpt(masterFD) == -1 {
        print("Failed to unlockpt")
        return
    }

    #if os(Linux)
    let slaveName = ptsname(masterFD)!
    #else
    let slaveName = ptsname(masterFD)
    if slaveName == nil {
        print("Failed to get slave PTY name")
        return
    }
    #endif

    var pid = pid_t()
    
    #if os(Linux)
    var fileActions: posix_spawn_file_actions_t = posix_spawn_file_actions_t()
    #else
    var fileActions: posix_spawn_file_actions_t?
    #endif
    
    posix_spawn_file_actions_init(&fileActions)
    posix_spawn_file_actions_addopen(&fileActions, STDIN_FILENO, slaveName, O_RDWR, 0)
    posix_spawn_file_actions_adddup2(&fileActions, STDIN_FILENO, STDOUT_FILENO)
    posix_spawn_file_actions_adddup2(&fileActions, STDIN_FILENO, STDERR_FILENO)

    let argv = ["/bin/sh", "-c", command, nil].map { $0.flatMap { strdup($0) } }

    if posix_spawn(&pid, argv[0]!, &fileActions, nil, argv, environ) != 0 {
        print("Failed to spawn process")
        return
    }

    let masterHandle = FileHandle(fileDescriptor: masterFD, closeOnDealloc: true)

    while true {
        let data = masterHandle.availableData
        if data.isEmpty {
            break
        }

        if let str = String(data: data, encoding: .utf8) {
            print(str, terminator: "")
        }
    }

    var status = Int32()
    waitpid(pid, &status, 0)

    posix_spawn_file_actions_destroy(&fileActions)
    argv.forEach { free($0) }
}

Has anyone done this before or know of an example of Pseudoterminals/PTY running through Swift on Linux?

I'd appreciate any tips or pointers :sweat_smile: