Merge pull request #252 from dmackdev/show-stderr-with-os-pipe

feat: show `stderr` output from code execution
This commit is contained in:
Matias Fontanini 2024-05-26 07:58:19 -07:00 committed by GitHub
commit a515212abe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 86 additions and 11 deletions

11
Cargo.lock generated
View File

@ -796,6 +796,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_pipe"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@ -881,6 +891,7 @@ dependencies = [
"itertools",
"merge-struct",
"once_cell",
"os_pipe",
"rand",
"rstest",
"schemars",

View File

@ -33,6 +33,7 @@ tempfile = "3.10"
console = "0.15.8"
thiserror = "1"
unicode-width = "0.1"
os_pipe = "1.1.5"
[dependencies.syntect]
version = "5.2"

View File

@ -13,6 +13,7 @@ This presentation shows how to:
* Left-align code blocks.
* Have code blocks without background.
* Execute code snippets.
```rust
pub struct Greeter {
@ -74,3 +75,35 @@ fn main() {
println!("{greeting}");
}
```
<!-- end_slide -->
Code execution
===
Run commands from the presentation and display their output dynamically.
```bash +exec
for i in $(seq 1 5)
do
echo "hi $i"
sleep 0.5
done
```
<!-- end_slide -->
Code execution - `stderr`
===
Output from `stderr` will also be shown as output.
```bash +exec
echo "This is a successful command"
sleep 0.5
echo "This message redirects to stderr" >&2
sleep 0.5
echo "This is a successful command again"
sleep 0.5
man # Missing argument
```

View File

@ -3,9 +3,9 @@
use crate::markdown::elements::{Code, CodeLanguage};
use std::{
io::{self, BufRead, BufReader, Write},
process::{self, ChildStdout, Stdio},
process::{self, Stdio},
sync::{Arc, Mutex},
thread::{self},
thread,
};
use tempfile::NamedTempFile;
@ -31,17 +31,19 @@ impl CodeExecuter {
let mut output_file = NamedTempFile::new().map_err(CodeExecuteError::TempFile)?;
output_file.write_all(code.as_bytes()).map_err(CodeExecuteError::TempFile)?;
output_file.flush().map_err(CodeExecuteError::TempFile)?;
let (reader, writer) = os_pipe::pipe().map_err(CodeExecuteError::Pipe)?;
let writer_clone = writer.try_clone().map_err(CodeExecuteError::Pipe)?;
let process_handle = process::Command::new("/usr/bin/env")
.arg(interpreter)
.arg(output_file.path())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdout(writer)
.stderr(writer_clone)
.spawn()
.map_err(CodeExecuteError::SpawnProcess)?;
let state: Arc<Mutex<ExecutionState>> = Default::default();
let reader_handle = ProcessReader::spawn(process_handle, state.clone(), output_file);
let reader_handle = ProcessReader::spawn(process_handle, state.clone(), output_file, reader);
let handle = ExecutionHandle { state, reader_handle };
Ok(handle)
}
@ -61,6 +63,9 @@ pub(crate) enum CodeExecuteError {
#[error("error spawning process: {0}")]
SpawnProcess(io::Error),
#[error("error creating pipe: {0}")]
Pipe(io::Error),
}
/// A handle for the execution of a piece of code.
@ -84,6 +89,7 @@ struct ProcessReader {
state: Arc<Mutex<ExecutionState>>,
#[allow(dead_code)]
file_handle: NamedTempFile,
reader: os_pipe::PipeReader,
}
impl ProcessReader {
@ -91,15 +97,14 @@ impl ProcessReader {
handle: process::Child,
state: Arc<Mutex<ExecutionState>>,
file_handle: NamedTempFile,
reader: os_pipe::PipeReader,
) -> thread::JoinHandle<()> {
let reader = Self { handle, state, file_handle };
let reader = Self { handle, state, file_handle, reader };
thread::spawn(|| reader.run())
}
fn run(mut self) {
let stdout = self.handle.stdout.take().expect("no stdout");
let stdout = BufReader::new(stdout);
let _ = Self::process_output(self.state.clone(), stdout);
let _ = Self::process_output(self.state.clone(), self.reader);
let success = match self.handle.wait() {
Ok(code) => code.success(),
_ => false,
@ -111,8 +116,9 @@ impl ProcessReader {
self.state.lock().unwrap().status = status;
}
fn process_output(state: Arc<Mutex<ExecutionState>>, stdout: BufReader<ChildStdout>) -> io::Result<()> {
for line in stdout.lines() {
fn process_output(state: Arc<Mutex<ExecutionState>>, reader: os_pipe::PipeReader) -> io::Result<()> {
let reader = BufReader::new(reader);
for line in reader.lines() {
let line = line?;
// TODO: consider not locking per line...
state.lock().unwrap().output.push(line);
@ -183,4 +189,28 @@ echo 'bye'"
let result = CodeExecuter::execute(&code);
assert!(result.is_err());
}
#[test]
fn shell_code_execution_captures_stderr() {
let contents = r"
echo 'This message redirects to stderr' >&2
echo 'hello world'
"
.into();
let code = Code {
contents,
language: CodeLanguage::Shell("sh".into()),
attributes: CodeAttributes { execute: true, ..Default::default() },
};
let handle = CodeExecuter::execute(&code).expect("execution failed");
let state = loop {
let state = handle.state();
if state.status.is_finished() {
break state;
}
};
let expected_lines = vec!["This message redirects to stderr", "hello world"];
assert_eq!(state.output, expected_lines);
}
}