mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-31 23:25:17 +00:00
Merge pull request #252 from dmackdev/show-stderr-with-os-pipe
feat: show `stderr` output from code execution
This commit is contained in:
commit
a515212abe
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
```
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user