Skip to main content

command_stream/development/
repl.rs

1use super::child::ManagedChild;
2use std::io;
3use std::path::PathBuf;
4use std::process::{ExitStatus, Stdio};
5use std::sync::atomic::{AtomicU64, Ordering};
6use tokio::process::Command;
7
8static NEXT_SESSION: AtomicU64 = AtomicU64::new(0);
9
10/// Real Rust evaluation via Evcxr, including persistent variables and async code.
11#[derive(Debug, Clone)]
12pub struct ReplOptions {
13    /// Evcxr executable; defaults to COMMAND_STREAM_EVCXR or `evcxr` on PATH.
14    pub executable: PathBuf,
15    /// Local crate directory. None loads the version of the invoking library.
16    pub crate_path: Option<PathBuf>,
17    /// Disable terminal editing when driving the REPL through a pipe.
18    pub disable_readline: bool,
19}
20
21impl Default for ReplOptions {
22    fn default() -> Self {
23        Self {
24            executable: std::env::var_os("COMMAND_STREAM_EVCXR")
25                .map(PathBuf::from)
26                .unwrap_or_else(|| "evcxr".into()),
27            crate_path: Some(PathBuf::from(env!("CARGO_MANIFEST_DIR")))
28                .filter(|path| path.join("Cargo.toml").is_file()),
29            disable_readline: false,
30        }
31    }
32}
33
34struct SessionDirectory(PathBuf);
35impl Drop for SessionDirectory {
36    fn drop(&mut self) {
37        let _ = std::fs::remove_dir_all(&self.0);
38    }
39}
40
41/// Owns the evaluator and its isolated startup configuration.
42pub struct RustRepl {
43    process: ManagedChild,
44    _session: SessionDirectory,
45}
46
47impl RustRepl {
48    pub async fn wait(&mut self) -> io::Result<ExitStatus> {
49        self.process.child.wait().await
50    }
51    pub async fn close(&mut self) -> io::Result<()> {
52        self.process.stop().await
53    }
54}
55
56/// Start an Evcxr session with the entire command-stream crate in scope.
57///
58/// Requires `rustup component add rust-src` and `cargo install --locked evcxr_repl`.
59/// Evcxr supplies Rust parsing, diagnostics, multiline editing and completion.
60/// Fully qualified macros such as `command_stream::s!` work without special parsing.
61pub fn start_repl(options: ReplOptions) -> io::Result<RustRepl> {
62    let path = std::env::temp_dir().join(format!(
63        ".command-stream-repl-{}-{}-{}",
64        std::process::id(),
65        std::time::SystemTime::now()
66            .duration_since(std::time::UNIX_EPOCH)
67            .unwrap_or_default()
68            .as_nanos(),
69        NEXT_SESSION.fetch_add(1, Ordering::Relaxed)
70    ));
71    std::fs::create_dir(&path)?;
72    let session = SessionDirectory(path);
73    let dependency = match options.crate_path {
74        Some(path) => format!(
75            "path = {}",
76            serde_json::to_string(&path.canonicalize()?.to_string_lossy())?
77        ),
78        None => format!("version = {:?}", env!("CARGO_PKG_VERSION")),
79    };
80    let facade = session.0.join("facade");
81    std::fs::create_dir(&facade)?;
82    std::fs::write(facade.join("Cargo.toml"),
83        format!("[package]\nname = \"command-stream-repl-context\"\nversion = \"0.0.0\"\nedition = \"2021\"\n[lib]\npath = \"lib.rs\"\ncrate-type = [\"dylib\"]\n[dependencies]\ncommand_stream = {{ package = \"command-stream\", {dependency} }}\ntokio = {{ version = \"1\", features = [\"full\"] }}\n"))?;
84    std::fs::write(
85        facade.join("lib.rs"),
86        "pub use command_stream::*;\npub use tokio;\n",
87    )?;
88    std::fs::write(
89        session.0.join("init.evcxr"),
90        // The dylib facade shares Tokio process/signal state across cells.
91        format!(":opt 0\n:dep command_stream = {{ package = \"command-stream-repl-context\", path = {} }}\n:dep tokio = {{ version = \"1\", features = [\"full\"] }}\n", serde_json::to_string(&facade.to_string_lossy())?),
92    )?;
93    std::fs::write(session.0.join("prelude.rs"), "use command_stream::*;\n")?;
94    let mut command = Command::new(options.executable);
95    command
96        .env("EVCXR_CONFIG_DIR", &session.0)
97        .stdin(Stdio::inherit())
98        .stdout(Stdio::inherit())
99        .stderr(Stdio::inherit());
100    if options.disable_readline {
101        command.arg("--disable-readline");
102    }
103    let process = ManagedChild::spawn(&mut command, !options.disable_readline).map_err(|error| {
104        io::Error::new(error.kind(), format!("Cannot start Rust REPL: {error}. Install Rust sources with `rustup component add rust-src` and the evaluator with `cargo install --locked evcxr_repl`, or set COMMAND_STREAM_EVCXR."))
105    })?;
106    Ok(RustRepl {
107        process,
108        _session: session,
109    })
110}