Skip to content
2 changes: 2 additions & 0 deletions crates/wasi/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ wasmtime::component::bindgen!({
"wasi:io/error": wasmtime_wasi_io::bindings::wasi::io::error,
"wasi:filesystem/types.descriptor": crate::filesystem::Descriptor,
"wasi:filesystem/types.directory-entry-stream": crate::filesystem::DirectoryEntryStream,
"wasi:cli/terminal-input.terminal-input": crate::cli::TerminalInput,
"wasi:cli/terminal-output.terminal-output": crate::cli::TerminalOutput,
},
require_store_data_send: true,
});
83 changes: 66 additions & 17 deletions crates/wasi/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,41 +86,90 @@ impl stderr::Host for WasiCtxView<'_> {
}
}

// The process stream is a pipe to the launcher, which does the terminal's
// line discipline itself: the guest is not attached to a terminal, and says
// so, the way a program with redirected stdio does.
/// A guest's handle on its terminal input. The interface has no methods: holding
/// one *is* the answer to "is stdin a terminal?".
pub struct TerminalInput;

/// A guest's handle on a terminal output, likewise only its own answer.
pub struct TerminalOutput;

// The launcher does the line discipline: a terminal's stream arrives a line at
// a time, already edited and echoed. Whether its far end is a terminal is the
// embedder's declaration (`WasiCtx::with_terminal`): Kore's shell declares its
// interactive relay one, and a launcher that captures the output does not, so
// the guest is told the truth either way. All three streams share the one
// process stream, so they answer together.
impl terminal_input::Host for WasiCtxView<'_> {}
impl terminal_input::HostTerminalInput for WasiCtxView<'_> {
fn drop(&mut self, r: Resource<terminal_input::TerminalInput>) -> wasmtime::Result<()> {
fn drop(&mut self, r: Resource<TerminalInput>) -> wasmtime::Result<()> {
self.table.delete(r)?;
Ok(())
}
}
impl terminal_output::Host for WasiCtxView<'_> {}
impl terminal_output::HostTerminalOutput for WasiCtxView<'_> {
fn drop(&mut self, r: Resource<terminal_output::TerminalOutput>) -> wasmtime::Result<()> {
fn drop(&mut self, r: Resource<TerminalOutput>) -> wasmtime::Result<()> {
self.table.delete(r)?;
Ok(())
}
}
impl terminal_stdin::Host for WasiCtxView<'_> {
fn get_terminal_stdin(
&mut self,
) -> wasmtime::Result<Option<Resource<terminal_input::TerminalInput>>> {
Ok(None)
fn get_terminal_stdin(&mut self) -> wasmtime::Result<Option<Resource<TerminalInput>>> {
if !self.ctx.terminal {
return Ok(None);
}
Ok(Some(self.table.push(TerminalInput)?))
}
}
impl terminal_stdout::Host for WasiCtxView<'_> {
fn get_terminal_stdout(
&mut self,
) -> wasmtime::Result<Option<Resource<terminal_output::TerminalOutput>>> {
Ok(None)
fn get_terminal_stdout(&mut self) -> wasmtime::Result<Option<Resource<TerminalOutput>>> {
self.terminal_output()
}
}
impl terminal_stderr::Host for WasiCtxView<'_> {
fn get_terminal_stderr(
&mut self,
) -> wasmtime::Result<Option<Resource<terminal_output::TerminalOutput>>> {
Ok(None)
fn get_terminal_stderr(&mut self) -> wasmtime::Result<Option<Resource<TerminalOutput>>> {
self.terminal_output()
}
}

impl WasiCtxView<'_> {
/// stdout's or stderr's terminal, when the streams are a terminal's.
fn terminal_output(&mut self) -> wasmtime::Result<Option<Resource<TerminalOutput>>> {
if !self.ctx.terminal {
return Ok(None);
}
Ok(Some(self.table.push(TerminalOutput)?))
}
}

#[cfg(test)]
mod tests {
use alloc::vec::Vec;

use super::*;
use crate::{Wasi, WasiCtx, WasiView};

/// Each standard stream answers "is this a terminal?" exactly as the
/// embedder declared, and a terminal handle the guest drops leaves the
/// table.
#[test]
fn the_guest_is_told_the_terminal_its_embedder_declared() {
for terminal in [false, true] {
let mut wasi = Wasi::new(WasiCtx::new(Vec::new()).with_terminal(terminal));
let mut view = wasi.wasi();
let stdin = terminal_stdin::Host::get_terminal_stdin(&mut view).unwrap();
let stdout = terminal_stdout::Host::get_terminal_stdout(&mut view).unwrap();
let stderr = terminal_stderr::Host::get_terminal_stderr(&mut view).unwrap();
assert_eq!(
(stdin.is_some(), stdout.is_some(), stderr.is_some()),
(terminal, terminal, terminal)
);
if let (Some(stdin), Some(stdout), Some(stderr)) = (stdin, stdout, stderr) {
terminal_input::HostTerminalInput::drop(&mut view, stdin).unwrap();
terminal_output::HostTerminalOutput::drop(&mut view, stdout).unwrap();
terminal_output::HostTerminalOutput::drop(&mut view, stderr).unwrap();
}
assert!(wasi.table.is_empty());
}
}
}
Loading
Loading