I am trying to keep a shell (bash
/zsh
/etc
) running by using Process from the Foundation library.
I know how to use Pipe for standard input/output/error for other command line executables but it seems like the shell programs require that the standard input/output/error file be a terminal device file.
It looks like these are the files called /dev/ttys<3 digit number>
which are created for every new shell instance.
How can I create these files myself and use them in swift?
SOLUTION:
So this turned out to be a lot easier than I thought it would be.
Here are the steps needed to create a pair of master and slave pseudo-terminal FileHandle
objects:
Darwin
module.posix_openpt(int oflag)
to get a file descriptor for an available master pseudo-terminal device.grantpt(int filedes)
to establish ownership of corresponding slave pseudo-terminal device.unlockpt(int filedes)
to unlock the slave pseudo-terminal device.ptsname(int filedes)
to get path to slave pseudo-terminal device.FileHandle
objects using the file descriptor from step 2 and path from step 5.In steps 3 - 5 filedes
is the file descriptor returned by the call to posix_openpt(int oflag)
from step 1.
Use O_RDRW
for the oflag
parameter in step 1 for read-write permissions on the master pseudo-terminal device file.
Example code where we keep a bash
session alive and have it run the tty
command:
import Foundation
import Darwin
class A: NSObject {
var task: Process?
var slaveFile: FileHandle?
var masterFile: FileHandle?
override init() {
self.task = Process()
var masterFD: Int32 = 0
masterFD = posix_openpt(O_RDWR)
grantpt(masterFD)
unlockpt(masterFD)
self.masterFile = FileHandle.init(fileDescriptor: masterFD)
let slavePath = String.init(cString: ptsname(masterFD))
self.slaveFile = FileHandle.init(forUpdatingAtPath: slavePath)
self.task!.executableURL = URL(fileURLWithPath: "/bin/bash")
self.task!.arguments = ["-i"]
self.task!.standardOutput = slaveFile
self.task!.standardInput = slaveFile
self.task!.standardError = slaveFile
}
func run() {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else {
return
}
do {
try self.task!.run()
} catch {
print("Something went wrong.\n")
}
}
let data = self.masterFile!.availableData
let strData = String(data: data, encoding: String.Encoding.utf8)!
print("Output: "+strData)
self.masterFile!.write("tty\u{0D}".data(using: String.Encoding.utf8)!)
sleep(1)
let data1 = self.masterFile!.availableData
let strData1 = String(data: data1, encoding: String.Encoding.utf8)!
print("Output: "+strData1)
}
}
let a = A()
a.run()
An even simpler answer that is Playground friendly.
import PlaygroundSupport
import Foundation
import Darwin
// use socat to create psudo echo device
// socat -d -d -x pty,raw,nonblock,ispeed=115200,echo=0,link=/dev/ttys000 EXEC:/bin/cat
print("Started:\n")
var masterFile: FileHandle?
var masterFD: Int32 = 0
masterFD = open("/dev/ttys000",O_RDWR|O_NONBLOCK)
grantpt(masterFD)
unlockpt(masterFD)
masterFile = FileHandle.init(fileDescriptor: masterFD)
masterFile!.write("tty\u{0D}".data(using: String.Encoding.utf8)!)
sleep(1)
let data1 = masterFile!.availableData
let strData1 = String(data: data1, encoding: String.Encoding.utf8)!
print("Output: "+strData1)