commit 059a409cb0386cb3ee734144df72695ea471fa5f Author: itycodes Date: Mon Oct 13 02:34:06 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..549d7d0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,324 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "stdgather" +version = "0.1.0" +dependencies = [ + "chrono", + "libc", + "nix", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6e04c76 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "stdgather" +version = "0.1.0" +edition = "2024" + +[dependencies] +chrono = "0.4.42" +libc = "0.2.176" +nix = { version = "0.30.1", features = ["fs", "poll", "process", "term"] } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2fbe819 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,225 @@ +use chrono::Local; +use libc::kill; +use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; +use nix::sys::wait::{WaitPidFlag, waitpid}; +use nix::unistd::{ForkResult, execvp, fork, setsid}; +use std::ffi::CString; +use std::fs::File; +use std::io::Read; +use std::io::Write; +use std::os::fd::{BorrowedFd, FromRawFd}; +use std::os::unix::io::AsRawFd; +use std::ptr; + +#[derive(Debug, Default)] +struct Ptys { + out_slave: libc::c_int, + out_master: libc::c_int, + err_slave: libc::c_int, + err_master: libc::c_int, +} + +#[derive(Debug, Clone, Copy)] +enum StdioStreamType { + StdOut, + StdErr, +} + +struct StdioStream<'a> { + var: StdioStreamType, + file: &'a File, + is_tty: bool, + buffer: &'a mut Vec, +} + +fn handle_data(mut stream: StdioStream) -> Result<(), ()> { + let mut buf = [0u8; 65536]; + let res = stream.file.read(&mut buf); + match res { + Ok(0) => Err(()), // EOF + Ok(n) => { + let bufc = &buf[..n]; + for c in bufc { + stream.buffer.push(*c); + if *c == 0x0a as u8 { + let format = String::from("%H:%M:%S.%6f") + + match (stream.is_tty, stream.var) { + (true, StdioStreamType::StdOut) => " \x1b[32m(O)\x1b[0m ", + (false, StdioStreamType::StdOut) => " (O) ", + (true, StdioStreamType::StdErr) => " \x1b[31m(E)\x1b[0m ", + (false, StdioStreamType::StdErr) => " (E) ", + }; + let now = Local::now().format(&format).to_string(); + std::io::stdout().write_all(now.as_bytes()).unwrap(); + std::io::stdout().write_all(&stream.buffer).unwrap(); + stream.buffer.clear(); + } + } + std::io::stdout().flush().unwrap(); + Ok(()) + } + Err(e) => { + eprintln!("Read error: {}", e); + Err(()) + } + } +} + +fn main() { + let mut ptys = Ptys::default(); + unsafe { + libc::openpty( + &mut ptys.out_slave, + &mut ptys.out_master, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ); + libc::openpty( + &mut ptys.err_slave, + &mut ptys.err_master, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ); + } + assert!(ptys.out_slave != 0); + assert!(ptys.out_master != 0); + assert!(ptys.err_slave != 0); + assert!(ptys.err_master != 0); + + let is_tty = unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 }; + + match unsafe { fork() } { + Ok(ForkResult::Child) => { + let cmd = CString::new("/bin/bash").unwrap(); + let bcmd = std::env::args().skip(1).collect::>().join(" "); + println!("Cmd: '{}'", bcmd); + + setsid().expect("Failed to create new session"); + + unsafe { libc::close(ptys.out_master.as_raw_fd()) }; + unsafe { + libc::dup2(ptys.out_slave.as_raw_fd(), libc::STDOUT_FILENO); + libc::dup2(ptys.err_slave.as_raw_fd(), libc::STDERR_FILENO); + // There's no real reason to dup the stdin. + // It's not being used anyway. + // ---------------------------------------------------------- + // libc::dup2(ptys.out_slave.as_raw_fd(), libc::STDIN_FILENO); + } + + unsafe { libc::close(ptys.out_slave.as_raw_fd()) }; + unsafe { libc::close(ptys.err_slave.as_raw_fd()) }; + + let args = [ + cmd.clone(), + CString::new("-c").unwrap(), + CString::new(bcmd).unwrap(), + ]; + execvp(&cmd, &args).expect("execvp failed"); + } + + Ok(ForkResult::Parent { child }) => { + let _ = child; + // TODO: The kernel seems to garbage collect the pty + // & the associated data when the only holder of the slave end dies, + // resulting in errors reading it. So we keep a copy around. + // I don't actually know if this is the correct way of doing things. + // But it fixed the issues, so... + // ----------------------------------------------------------------- + // unsafe { libc::close(slave.as_raw_fd()) }; + + let master_file_out = unsafe { File::from_raw_fd(ptys.out_master) }; + let master_file_err = unsafe { File::from_raw_fd(ptys.err_master) }; + + // In some racy cases, the read call might get stuck... + // Maybe it'd be better to switch to non-blocking reads instead of + // using poll. + // --------------------------------------------------------------- + // unsafe { + // let flags = fcntl(ptys.out_master, F_GETFL, 0); + // fcntl(ptys.out_master, F_SETFL, flags | O_NONBLOCK); + // } + + let mut poll_fds = [ + PollFd::new( + unsafe { BorrowedFd::borrow_raw(ptys.out_master) }, + PollFlags::POLLIN | PollFlags::POLLHUP, + ), + PollFd::new( + unsafe { BorrowedFd::borrow_raw(ptys.err_master) }, + PollFlags::POLLIN | PollFlags::POLLHUP, + ), + ]; + + let mut out_buf: Vec = Vec::new(); + let mut err_buf: Vec = Vec::new(); + + loop { + let res = poll(&mut poll_fds, PollTimeout::ZERO); + if res.is_err() { + eprintln!("Poll failed"); + std::process::exit(-1); + } + + let revents = poll_fds[0].revents(); + if let Some(revents) = revents { + if revents.contains(PollFlags::POLLHUP) { + break; + } + if revents.contains(PollFlags::POLLIN) { + match handle_data(StdioStream { + var: StdioStreamType::StdOut, + file: &master_file_out, + is_tty: is_tty, + buffer: &mut out_buf, + }) { + Ok(_) => {} + Err(_) => break, + } + } + } + + let revents = poll_fds[1].revents(); + if let Some(revents) = revents { + if revents.contains(PollFlags::POLLHUP) { + break; + } + if revents.contains(PollFlags::POLLIN) { + match handle_data(StdioStream { + var: StdioStreamType::StdErr, + file: &master_file_err, + is_tty: is_tty, + buffer: &mut err_buf, + }) { + Ok(_) => {} + Err(_) => break, + } + } + } + + if res.ok().unwrap() == 0 && unsafe { kill(child.as_raw(), 0) } != 0 { + // The poll might've timed out and in the short time between + // the poll timeout and the check if the process is alive, + // there might be pending data (unless the kernel deletes it...) + // I have no evidence supporting this theory, though. + // ------------------------------------------------------------- + // let mut buf = [0u8; 64]; + // match master_file.read(&mut buf) { + // Ok(e) => println!("Ok: {}", e), + // Err(_e) => {/*println!("Err: {}", e)*/} + // } + break; + } + + waitpid(child, Some(WaitPidFlag::WNOHANG)).ok(); + } + } + + Err(e) => { + eprintln!("Fork failed: {}", e); + std::process::exit(-1); + } + } + println!(""); +} diff --git a/vim/init.vim b/vim/init.vim new file mode 100644 index 0000000..f4daf7e --- /dev/null +++ b/vim/init.vim @@ -0,0 +1,5 @@ +augroup StdInputSyntax + autocmd! + autocmd StdinReadPost * + \ call timer_start(10, { -> execute('if getline(1) =~ "^Cmd: ''" | setfiletype stdgather | endif') }) +augroup END diff --git a/vim/stdgather.vim b/vim/stdgather.vim new file mode 100644 index 0000000..8414b67 --- /dev/null +++ b/vim/stdgather.vim @@ -0,0 +1,20 @@ +" syntax/stdgather.vim + +" Clear previous syntax +syntax clear + +" Match and highlight time format HH:MM:SS.microseconds in light blue +syntax match LogTime /\v\d{2}:\d{2}:\d{2}\.\d{6}/ +highlight LogTime ctermfg=LightBlue guifg=LightBlue + +" Match and highlight ' (O) ' in green +syntax match LogOK /\v\s\(O\)\s/ +highlight LogOK ctermfg=Green guifg=Green + +" Match and highlight ' (E) ' in red +syntax match LogError /\v\s\(E\)\s/ +highlight LogError ctermfg=Red guifg=Red + +" Match the first line if it starts with Cmd: ' and ends with a single quote +syntax match LogCmdLine /\%1l^Cmd: '.*'$/ +highlight LogCmdLine ctermfg=LightGrey guifg=DarkGrey