diff --git a/tty-fwd/Cargo.lock b/tty-fwd/Cargo.lock index 07d03f48..8bf30393 100644 --- a/tty-fwd/Cargo.lock +++ b/tty-fwd/Cargo.lock @@ -31,6 +31,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "getrandom" version = "0.3.3" @@ -55,6 +71,12 @@ version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "memchr" version = "2.7.5" @@ -73,6 +95,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -97,6 +125,19 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "ryu" version = "1.0.20" @@ -165,6 +206,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "tty-fwd" version = "0.4.0" @@ -175,6 +229,7 @@ dependencies = [ "serde", "serde_json", "signal-hook", + "tempfile", "uuid", ] @@ -202,6 +257,79 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/tty-fwd/Cargo.toml b/tty-fwd/Cargo.toml index 64241744..1fd41639 100644 --- a/tty-fwd/Cargo.toml +++ b/tty-fwd/Cargo.toml @@ -19,4 +19,5 @@ nix = { version = "0.29.0", default-features = false, features = ["fs", "process serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" signal-hook = { version = "0.3.14", default-features = false } +tempfile = "3.20.0" uuid = { version = "1.17.0", features = ["v4"], default-features = false } diff --git a/tty-fwd/src/main.rs b/tty-fwd/src/main.rs index d216e38a..321bb8a8 100644 --- a/tty-fwd/src/main.rs +++ b/tty-fwd/src/main.rs @@ -38,18 +38,45 @@ fn list_sessions(control_path: &Path) -> Result<(), anyhow::Error> { let stdin_path = path.join("stdin"); if session_json_path.exists() { - let status = if stream_out_path.exists() && stdin_path.exists() { - "running" + let session_data = if let Ok(content) = fs::read_to_string(&session_json_path) { + if let Ok(session_info) = serde_json::from_str::(&content) { + serde_json::json!({ + "cmdline": session_info.cmdline, + "name": session_info.name, + "cwd": session_info.cwd, + "pid": session_info.pid, + "status": session_info.status, + "exit_code": session_info.exit_code, + "stream-out": stream_out_path.to_string_lossy(), + "stdin": stdin_path.to_string_lossy() + }) + } else { + // Fallback to old behavior if JSON parsing fails + let status = if stream_out_path.exists() && stdin_path.exists() { + "running" + } else { + "stopped" + }; + serde_json::json!({ + "status": status, + "stream-out": stream_out_path.to_string_lossy(), + "stdin": stdin_path.to_string_lossy() + }) + } } else { - "stopped" + // Fallback to old behavior if file reading fails + let status = if stream_out_path.exists() && stdin_path.exists() { + "running" + } else { + "stopped" + }; + serde_json::json!({ + "status": status, + "stream-out": stream_out_path.to_string_lossy(), + "stdin": stdin_path.to_string_lossy() + }) }; - let session_data = serde_json::json!({ - "status": status, - "stream-out": stream_out_path.to_string_lossy(), - "stdin": stdin_path.to_string_lossy() - }); - sessions.insert(session_id.to_string(), session_data); } } @@ -121,10 +148,13 @@ fn main() -> Result<(), anyhow::Error> { .collect(), name: session_name.unwrap_or(executable_name), cwd: current_dir, + pid: None, + status: "starting".to_string(), + exit_code: None, }; let session_info_path = session_path.join("session.json"); let session_info_str = serde_json::to_string(&session_info)?; - fs::write(session_info_path, session_info_str)?; + fs::write(&session_info_path, session_info_str)?; // Set up stream-out and stdin paths let stream_out_path = session_path.join("stream-out"); @@ -134,7 +164,8 @@ fn main() -> Result<(), anyhow::Error> { let mut tty_spawn = TtySpawn::new_cmdline(cmdline.iter().map(|s| s.as_os_str())); tty_spawn .stdout_path(&stream_out_path, true)? - .stdin_path(&stdin_path)?; + .stdin_path(&stdin_path)? + .session_json_path(&session_info_path); // Spawn the process let exit_code = tty_spawn.spawn()?; diff --git a/tty-fwd/src/protocol.rs b/tty-fwd/src/protocol.rs index 8aad430b..771c1cee 100644 --- a/tty-fwd/src/protocol.rs +++ b/tty-fwd/src/protocol.rs @@ -1,11 +1,16 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct SessionInfo { pub cmdline: Vec, pub name: String, pub cwd: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub pid: Option, + pub status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub exit_code: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/tty-fwd/src/tty_spawn.rs b/tty-fwd/src/tty_spawn.rs index c92917de..c5a40ac3 100644 --- a/tty-fwd/src/tty_spawn.rs +++ b/tty-fwd/src/tty_spawn.rs @@ -2,12 +2,15 @@ use std::ffi::{CString, OsStr, OsString}; use std::fs::File; use std::os::fd::{AsFd, BorrowedFd, IntoRawFd, OwnedFd}; use std::os::unix::prelude::{AsRawFd, OpenOptionsExt, OsStrExt}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::{env, io}; +use tempfile::NamedTempFile; -use crate::protocol::{AsciinemaEvent, AsciinemaEventType, AsciinemaHeader, StreamWriter}; +use crate::protocol::{ + AsciinemaEvent, AsciinemaEventType, AsciinemaHeader, SessionInfo, StreamWriter, +}; use nix::errno::Errno; use nix::libc::{login_tty, O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, VEOF}; @@ -41,6 +44,7 @@ impl TtySpawn { no_echo: false, no_pager: false, no_raw: false, + session_json_path: None, }), } } @@ -191,6 +195,12 @@ impl TtySpawn { self } + /// Sets the session JSON path for status updates. + pub fn session_json_path>(&mut self, path: P) -> &mut Self { + self.options_mut().session_json_path = Some(path.as_ref().to_path_buf()); + self + } + /// Spawns the application in the TTY. pub fn spawn(&mut self) -> Result { Ok(spawn( @@ -212,6 +222,36 @@ struct SpawnOptions { no_echo: bool, no_pager: bool, no_raw: bool, + session_json_path: Option, +} + +/// Updates the session status in the JSON file +fn update_session_status( + session_json_path: &Path, + pid: Option, + status: &str, + exit_code: Option, +) -> Result<(), io::Error> { + if let Ok(content) = std::fs::read_to_string(session_json_path) { + if let Ok(mut session_info) = serde_json::from_str::(&content) { + if let Some(pid) = pid { + session_info.pid = Some(pid); + } + session_info.status = status.to_string(); + if let Some(code) = exit_code { + session_info.exit_code = Some(code); + } + let updated_content = serde_json::to_string(&session_info)?; + + // Write to temporary file first, then move to final location + let temp_file = NamedTempFile::new_in( + session_json_path.parent().unwrap_or_else(|| Path::new(".")), + )?; + std::fs::write(temp_file.path(), updated_content)?; + temp_file.persist(session_json_path)?; + } + } + Ok(()) } /// Spawns a process in a PTY in a manor similar to `script` @@ -290,7 +330,18 @@ fn spawn(mut opts: SpawnOptions) -> Result { } else { None }; - return communication_loop( + + // Update session status to running with PID + if let Some(ref session_json_path) = opts.session_json_path { + let _ = update_session_status( + session_json_path, + Some(child.as_raw() as u32), + "running", + None, + ); + } + + let exit_code = communication_loop( pty.master, child, term_attrs.is_some(), @@ -298,7 +349,14 @@ fn spawn(mut opts: SpawnOptions) -> Result { opts.stdin_file.as_mut(), stderr_pty, !opts.no_flush, - ); + )?; + + // Update session status to exited with exit code + if let Some(ref session_json_path) = opts.session_json_path { + let _ = update_session_status(session_json_path, None, "exited", Some(exit_code)); + } + + return Ok(exit_code); } // set the pagers to `cat` if it's disabled.