///|
/// Handle to a spawned PTY plus its event-loop registration.
///
/// On Unix the PTY master fd is used for both reading and writing, so
/// `read_io` and `write_io` are the same `RawFd` wrapper around the same
/// `IoHandle`. The two fields exist so that reader and writer code paths
/// can use intent-revealing names. On Windows they point at the separate
/// ConPTY input/output pipe HANDLEs.
struct Pty {
handle : PtyHandle
read_io : @raw_fd.RawFd
write_io : @raw_fd.RawFd
spawned : PtySpawn
}
///|
#cfg(platform="windows")
priv struct PtySpawn(@async.Task[Int])
///|
#cfg(not(platform="windows"))
priv struct PtySpawn(@process.Process)
///|
fn raise_last_os_error(context : String) -> Unit raise @os_error.OSError {
@os_error.check_errno(context)
// Fallback for defensive callers that returned -1 without setting errno.
raise @os_error.OSError(0, context~)
}
///|
fn check_pty_status(
ret : Int,
context : String,
) -> Unit raise @os_error.OSError {
if ret < 0 {
raise_last_os_error(context)
}
}
///|
fn raise_invalid_argument(context : String) -> Unit raise @os_error.OSError {
// This is for validation failures found in MoonBit before any native call
// can set errno/GetLastError. The C shim installs the platform-specific
// invalid-argument code, then we report it through the normal os_error path.
check_pty_status(pty_set_invalid_argument(), context)
}
///|
/// Spawn a new PTY running the given program.
///
/// `argv[0]` is the program to execute (resolved via PATH by `execvp`),
/// and `argv[1..]` are its arguments.
///
/// `cols` and `rows` default to 80x24 if not specified.
///
/// Must be called from inside an async event-loop context — the PTY's
/// master fd is registered with the event loop on construction so that
/// reads and writes can suspend instead of blocking the thread.
///
/// Raises `@os_error.OSError` wrapping `errno` (Unix) or `GetLastError()`
/// (Windows) if the underlying syscall fails.
#as_free_fn
#deprecated("Use @pty.spawn directly")
pub async fn[X] Pty::spawn(
group : @async.TaskGroup[X],
argv : Array[String],
cols? : Int = 80,
rows? : Int = 24,
no_wait? : Bool,
) -> Pty {
if argv.is_empty() {
raise_invalid_argument("pty.spawn: empty argv")
}
let spawn_payload = build_spawn_payload(argv)
let handle = pty_new()
let spawned = spawn_handle(group, handle, spawn_payload, cols, rows, no_wait?) catch {
err => {
pty_close(handle)
raise err
}
}
// Transfer fd ownership from the C PTY handle to `RawFd`.
let read_io = @raw_fd.RawFd::RawFd(pty_take_read_fd(handle)) catch {
err => {
pty_close(handle)
raise err
}
}
let write_io = register_write_io(handle, read_io)
{ handle, read_io, write_io, spawned }
}
///|
#cfg(platform="windows")
fn StringBuilder::write_windows_arg(
builder : StringBuilder,
arg : StringView,
) -> Unit {
let need_quote = if arg.length() == 0 {
true
} else {
for char in arg {
if char is (' ' | '\t' | '"') {
break true
}
} nobreak {
false
}
}
if !need_quote {
builder.write_stringview(arg)
return
}
let mut segment_start = 0
let mut index = 0
let mut trailing_backslash = 0
fn flush(skip : Int) {
if index > segment_start {
builder.write_stringview(arg[segment_start:index - trailing_backslash])
if trailing_backslash > 0 {
builder.write_string(String::make(trailing_backslash * 2, '\\'))
}
}
segment_start = index + skip
}
builder.write_char('"')
while index < arg.length() {
match arg.code_unit_at(index) {
'"' => {
flush(1)
builder.write_string("\\\"")
trailing_backslash = 0
}
'\\' => trailing_backslash += 1
_ => trailing_backslash = 0
}
index += 1
}
flush(0)
builder.write_char('"')
}
///|
#cfg(platform="windows")
fn build_spawn_payload(argv : Array[String]) -> Bytes {
let command_line = StringBuilder::new()
for i, arg in argv {
if i > 0 {
command_line.write_char(' ')
}
command_line.write_windows_arg(arg[:])
}
@utf8.encode(command_line.to_string()[:])
}
///|
#cfg(not(platform="windows"))
fn build_spawn_payload(argv : Array[String]) -> Bytes {
// Flatten argv into a single null-separated byte buffer:
// "arg0\0arg1\0...argN\0"
// The Unix helper walks the buffer by null byte, using the buffer's own
// length to know when to stop.
let buf = Buffer()
for arg in argv {
buf.write_bytes(@utf8.encode(arg[:]))
buf.write_byte(b'\x00')
}
buf.contents()
}
///|
#cfg(platform="windows")
fn[X] spawn_handle(
group : @async.TaskGroup[X],
handle : PtyHandle,
command_line : Bytes,
cols : Int,
rows : Int,
no_wait? : Bool,
) -> PtySpawn raise @os_error.OSError {
check_pty_status(
pty_spawn_windows(handle, command_line, cols, rows),
"pty.spawn",
)
let pid = pty_child_pid(handle)
if pid <= 0 {
raise_invalid_argument("pty.spawn: child pid")
}
let wait_task = group.spawn(no_wait?, () => {
@process.wait_pid(pid) catch {
_ if @async.is_being_cancelled() =>
@async.protect_from_cancel(() => {
pty_kill_pid_windows(pid)
@process.wait_pid(pid)
})
err => raise err
}
})
PtySpawn(wait_task)
}
///|
#cfg(not(platform="windows"))
async fn[X] spawn_handle(
group : @async.TaskGroup[X],
handle : PtyHandle,
argv_flat : Bytes,
cols : Int,
rows : Int,
no_wait? : Bool,
) -> PtySpawn {
check_pty_status(pty_open(handle, cols, rows), "pty.spawn")
let mut keep_handle = false
defer (if !keep_handle { pty_close(handle) })
let (argv_read, argv_write) = @pipe.pipe()
defer argv_read.close()
defer argv_write.close()
let (err_read, err_write) = @pipe.pipe()
defer err_read.close()
defer err_write.close()
let (slave_read, slave_write) = @pipe.pipe()
defer slave_read.close()
defer slave_write.close()
let bind_err = pty_bind_slave_to_fd(handle, slave_write.fd())
check_pty_status(bind_err, "pty.spawn: bind slave")
let self_cmd = @utf8.decode_lossy(pty_self_executable())
if self_cmd.is_empty() {
raise_invalid_argument("pty.spawn: self executable")
}
let self_args = parse_self_args(pty_self_args_flat())
let child = @process.spawn(
group,
self_cmd,
self_args,
stdin=argv_read,
stdout=slave_write,
stderr=err_write,
extra_env={ "MOONBIT_PTY_EXEC": "stdio" },
no_wait?,
)
pty_set_child_pid(handle, child.pid)
argv_read.close()
err_write.close()
slave_read.close()
slave_write.close()
let argv_write_result = try? argv_write.write(argv_flat)
argv_write.close()
let child_err = pty_decode_child_error(err_read.read_all().binary())
err_read.close()
check_pty_status(child_err, "pty.spawn")
match argv_write_result {
Ok(_) => ()
Err(err) => raise err
}
keep_handle = true
PtySpawn(child)
}
///|
#cfg(not(platform="windows"))
fn parse_self_args(flat : Bytes) -> Array[String] {
let args : Array[String] = []
let mut start = 0
let mut index = 0
for i in 0.. 0 {
args.push(@utf8.decode_lossy(flat[start:i]))
}
index += 1
start = i + 1
}
}
args
}
///|
/// On Unix read and write share the same PTY master fd.
#cfg(not(platform="windows"))
fn register_write_io(
_handle : PtyHandle,
read_io : @raw_fd.RawFd,
) -> @raw_fd.RawFd {
read_io
}
///|
/// On Windows ConPTY has a separate input pipe HANDLE.
#cfg(platform="windows")
async fn register_write_io(
handle : PtyHandle,
read_io : @raw_fd.RawFd,
) -> @raw_fd.RawFd {
@raw_fd.RawFd::RawFd(pty_take_write_fd_windows(handle)) catch {
err => {
read_io.close()
pty_close(handle)
raise err
}
}
}
///|
/// Get the async reader for this PTY.
///
/// The returned `RawFd` is owned by the `Pty`; do not call `close()`
/// or `detach()` on it. Use `Pty::close` to release the registration.
pub fn Pty::reader(self : Pty) -> @raw_fd.RawFd {
self.read_io
}
///|
/// Write data to the PTY stdin.
///
/// Suspends the current coroutine via the async event loop until all
/// bytes are written, so a stuck child that stops draining its input
/// buffer won't block other concurrent tasks in the runtime.
///
/// Raises `@os_error.OSError` if the underlying write fails.
pub async fn Pty::write(self : Pty, data : Bytes) -> Unit {
let total = data.length()
let mut offset = 0
while offset < total {
let n = self.write_io.write(data, offset~, len=total - offset)
if n <= 0 {
raise_invalid_argument("Pty::write: short write")
}
offset += n
}
}
///|
/// Resize the PTY window.
///
/// Raises `@os_error.OSError` if the underlying syscall fails.
pub fn Pty::resize(
self : Pty,
cols~ : Int,
rows~ : Int,
) -> Unit raise @os_error.OSError {
check_pty_status(pty_resize(self.handle, cols, rows), "Pty::resize")
}
///|
/// Get the spawned child PID when available.
pub fn Pty::pid(self : Pty) -> Int {
pty_child_pid(self.handle)
}
///|
/// Wait for the spawned child process to exit and return its exit code.
///
/// This is backed by `moonbitlang/async/process`, so waiting/reaping lives
/// in the async process layer rather than in `Pty::close`.
#cfg(not(platform="windows"))
pub async fn Pty::wait(self : Pty) -> Int {
let PtySpawn(process) = self.spawned
process.wait()
}
///|
/// Wait for the spawned child process to exit and return its exit code.
///
/// This is backed by `moonbitlang/async/process`, so waiting/reaping lives
/// in the async process layer rather than in `Pty::close`.
#cfg(platform="windows")
pub async fn Pty::wait(self : Pty) -> Int {
let PtySpawn(task) = self.spawned
task.wait()
}
///|
/// Request cancellation of the child process.
#cfg(not(platform="windows"))
pub fn Pty::cancel(self : Pty) -> Unit {
let PtySpawn(process) = self.spawned
process.cancel()
}
///|
/// Request cancellation of the child process.
#cfg(platform="windows")
pub fn Pty::cancel(self : Pty) -> Unit {
let pid = pty_child_pid(self.handle)
if pid > 0 {
pty_kill_pid_windows(pid)
}
}
///|
/// Close the PTY, releasing all resources.
///
/// Closes the owned `RawFd` wrapper(s) and then tears down the underlying
/// OS PTY handle.
///
/// Resource lifetime is explicit: call this when the PTY is no longer needed.
/// The native handle finalizer does not close OS resources.
///
/// Safe to call multiple times because the underlying close paths are
/// idempotent.
#cfg(not(platform="windows"))
pub fn Pty::close(self : Pty) -> Unit {
self.cancel()
self.read_io.close()
pty_close(self.handle)
}
///|
/// Close the PTY, releasing all resources.
///
/// Closes the owned `RawFd` wrapper(s) and then tears down the underlying
/// OS PTY handle.
///
/// Resource lifetime is explicit: call this when the PTY is no longer needed.
/// The native handle finalizer does not close OS resources.
///
/// Safe to call multiple times because the underlying close paths are
/// idempotent.
#cfg(platform="windows")
pub fn Pty::close(self : Pty) -> Unit {
self.cancel()
self.read_io.close()
self.write_io.close()
pty_close(self.handle)
}