try to polish it up again
This commit is contained in:
parent
9108e7d28d
commit
436c46ba12
10 changed files with 1067 additions and 486 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
/output.cast
|
||||
.direnv
|
||||
/logs
|
||||
|
|
868
Cargo.lock
generated
868
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
34
Cargo.toml
34
Cargo.toml
|
@ -1,21 +1,25 @@
|
|||
[package]
|
||||
name = "asciinema"
|
||||
name = "liveterm"
|
||||
version = "0.1.0"
|
||||
authors = ["Michael Zhang <iptq@protonmail.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.34"
|
||||
futures = "0.3.7"
|
||||
libc = "0.2.80"
|
||||
nix = "0.19.0"
|
||||
parking_lot = "0.11.0"
|
||||
serde = "1.0.117"
|
||||
serde_derive = "1.0.117"
|
||||
serde_json = "1.0.59"
|
||||
signal-hook = "0.1.16"
|
||||
structopt = "0.3.20"
|
||||
anyhow = { version = "1.0.68", features = ["backtrace"] }
|
||||
chrono = "0.4.23"
|
||||
clap = { version = "4.0.32", features = ["derive"] }
|
||||
futures = "0.3.25"
|
||||
libc = "0.2.139"
|
||||
log = "0.4.17"
|
||||
nix = "0.26.1"
|
||||
parking_lot = "0.12.1"
|
||||
serde = "1.0.152"
|
||||
serde_derive = "1.0.152"
|
||||
serde_json = "1.0.91"
|
||||
signal-hook = "0.3.14"
|
||||
termios = "0.3.3"
|
||||
thiserror = "1.0.22"
|
||||
tokio = { version = "0.3.3", features = ["full"] }
|
||||
tokio-util = { version = "0.5.0", features = ["codec"] }
|
||||
tokio = { version = "1.24.1", features = ["full"] }
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-appender = "0.2.2"
|
||||
tracing-subscriber = "0.3.16"
|
||||
|
|
34
output.cast
34
output.cast
|
@ -1,34 +0,0 @@
|
|||
[0.101333215,"o","\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
|
||||
[0.101467546,"o","\u001b]2;michael@manjaro: ~/Projects/asciinema\u0007\u001b]1;..cts/asciinema\u0007"]
|
||||
[0.117281595,"o","\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1m\u001b[34m#\u001b[00m \u001b[36mmichael \u001b[37m@ \u001b[32mmanjaro \u001b[37min \u001b[1m\u001b[33m~/Projects/asciinema\u001b[00m \u001b[37mon\u001b[00m git:\u001b[36mmaster \u001b[31mx\u001b[00m \u001b[37m[3:21:36] \r\n\u001b[1m\u001b[31m$ \u001b[00m\u001b[K"]
|
||||
[0.117374656,"o","\u001b[?1h\u001b="]
|
||||
[0.117667658,"o","\u001b[?2004h"]
|
||||
[1.680256137,"i","w"]
|
||||
[1.6861166810000001,"o","\u001b[32mw\u001b[39m"]
|
||||
[1.7842199970000001,"i","h"]
|
||||
[1.7917789800000001,"o","\r\r\u001b[A\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1m\u001b[34m#\u001b[00m \u001b[36mmichael \u001b[37m@ \u001b[32mmanjaro \u001b[37min \u001b[1m\u001b[33m~/Projects/asciinema\u001b[00m \u001b[37mon\u001b[00m git:\u001b[36mmaster \u001b[31mx\u001b[00m \u001b[37m[3:21:38] \r\n\u001b[1m\u001b[31m$ \u001b[00mwh"]
|
||||
[1.792416114,"o","\b\b\u001b[1m\u001b[31mw\u001b[1m\u001b[31mh\u001b[0m\u001b[39m"]
|
||||
[1.8562032560000001,"i","a"]
|
||||
[1.857965656,"o","\b\b\u001b[1m\u001b[31mw\u001b[1m\u001b[31mh\u001b[1m\u001b[31ma\u001b[0m\u001b[39m"]
|
||||
[1.960175246,"i","t"]
|
||||
[1.968143781,"o","\r\r\u001b[A\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1m\u001b[34m#\u001b[00m \u001b[36mmichael \u001b[37m@ \u001b[32mmanjaro \u001b[37min \u001b[1m\u001b[33m~/Projects/asciinema\u001b[00m \u001b[37mon\u001b[00m git:\u001b[36mmaster \u001b[31mx\u001b[00m \u001b[37m[3:21:38] \r\n\u001b[1m\u001b[31m$ \u001b[00mwhat"]
|
||||
[1.9686859540000001,"o","\b\b\b\b\u001b[1m\u001b[31mw\u001b[1m\u001b[31mh\u001b[1m\u001b[31ma\u001b[1m\u001b[31mt\u001b[0m\u001b[39m"]
|
||||
[2.352188361,"i"," "]
|
||||
[2.35392467,"o"," "]
|
||||
[2.592205933,"i","u"]
|
||||
[2.593708682,"o","u"]
|
||||
[2.64813527,"i","p"]
|
||||
[2.6493960469999998,"o","p"]
|
||||
[3.232210615,"i","\r"]
|
||||
[3.232716968,"o","\u001b[?1l\u001b>"]
|
||||
[3.233917915,"o","\u001b[?2004l\r\r\n"]
|
||||
[3.23484062,"o","\u001b]2;what up\u0007\u001b]1;what\u0007"]
|
||||
[3.235505654,"o","zsh: command not found: what\r\n"]
|
||||
[3.235709995,"o","\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
|
||||
[3.235766455,"o","\u001b]2;michael@manjaro: ~/Projects/asciinema\u0007"]
|
||||
[3.235797955,"o","\u001b]1;..cts/asciinema\u0007"]
|
||||
[3.242549144,"o","\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1m\u001b[34m#\u001b[00m \u001b[36mmichael \u001b[37m@ \u001b[32mmanjaro \u001b[37min \u001b[1m\u001b[33m~/Projects/asciinema\u001b[00m \u001b[37mon\u001b[00m git:\u001b[36mmaster \u001b[31mx\u001b[00m \u001b[37m[3:21:39] C:\u001b[31m127\u001b[00m\r\n\u001b[1m\u001b[31m$ \u001b[00m\u001b[K"]
|
||||
[3.242670815,"o","\u001b[?1h\u001b="]
|
||||
[3.243217488,"o","\u001b[?2004h"]
|
||||
[3.800181468,"i","\u0004"]
|
||||
[3.800319309,"o","\u001b[?2004l\r\r\n"]
|
|
@ -1,3 +1,7 @@
|
|||
//! Data structures for representing the [asciicast v2] format
|
||||
//!
|
||||
//! [asciicast v2]: https://github.com/asciinema/asciinema/blob/develop/doc/asciicast-v2.md
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::ser::{Serialize, SerializeSeq, Serializer};
|
||||
|
@ -24,14 +28,14 @@ pub struct Theme {
|
|||
palette: String,
|
||||
}
|
||||
|
||||
pub struct Event<'a>(pub f64, pub EventKind<'a>);
|
||||
pub struct Event(pub f64, pub EventKind);
|
||||
|
||||
pub enum EventKind<'a> {
|
||||
Output(&'a [u8]),
|
||||
Input(&'a [u8]),
|
||||
pub enum EventKind {
|
||||
Output(Vec<u8>),
|
||||
Input(Vec<u8>),
|
||||
}
|
||||
|
||||
impl<'a> Serialize for Event<'a> {
|
||||
impl Serialize for Event {
|
||||
fn serialize<S: Serializer>(
|
||||
&self,
|
||||
s: S,
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("generic nix error: {0}")]
|
||||
Nix(#[from] nix::Error),
|
||||
|
||||
#[error("write(3) error (fd={1}, data={2:?}): {0}")]
|
||||
NixWrite(nix::Error, i32, Vec<u8>),
|
||||
|
||||
#[error("generic io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("generic serde_json error: {0}")]
|
||||
SerdeJson(#[from] serde_json::Error),
|
||||
}
|
10
src/lib.rs
Normal file
10
src/lib.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
pub mod asciicast;
|
||||
pub mod recorder;
|
||||
// pub mod recorder;
|
||||
// pub mod term;
|
||||
// pub mod writer;
|
63
src/main.rs
63
src/main.rs
|
@ -1,40 +1,67 @@
|
|||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate thiserror;
|
||||
|
||||
mod asciicast;
|
||||
mod errors;
|
||||
mod pty;
|
||||
mod recorder;
|
||||
mod term;
|
||||
mod writer;
|
||||
extern crate tracing;
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
use clap::{ArgAction, Parser};
|
||||
use liveterm::recorder::Terminal;
|
||||
use tokio::process::Command;
|
||||
use tracing::Level;
|
||||
|
||||
use crate::writer::FileWriter;
|
||||
#[derive(Parser)]
|
||||
struct Opt {
|
||||
#[clap(subcommand)]
|
||||
subcommand: Subcommand,
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum Command {
|
||||
#[clap(long, short = 'v', action = ArgAction::Count, global = true)]
|
||||
verbose: u8,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
enum Subcommand {
|
||||
/// Record terminal session.
|
||||
#[structopt(name = "rec")]
|
||||
Record,
|
||||
}
|
||||
|
||||
fn record() -> Result<()> {
|
||||
let file = File::create("output.cast")?;
|
||||
pty::record(&["sh", "-c", "/usr/bin/zsh"], FileWriter::new(file))?;
|
||||
let _file = File::create("output.cast")?;
|
||||
|
||||
let mut command = Command::new("/usr/bin/env");
|
||||
command.arg("bash");
|
||||
|
||||
let (pty, rx) = Terminal::setup(command)?;
|
||||
pty.wait_until_complete()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
match Command::from_args() {
|
||||
Command::Record => {
|
||||
let opt = Opt::parse();
|
||||
setup_logging(&opt)?;
|
||||
|
||||
match opt.subcommand {
|
||||
Subcommand::Record => {
|
||||
record()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_logging(opts: &Opt) -> Result<()> {
|
||||
let file_appender = tracing_appender::rolling::hourly("logs", "liveterm.log");
|
||||
let max_level = match opts.verbose {
|
||||
0 => Level::ERROR,
|
||||
1 => Level::WARN,
|
||||
2 => Level::INFO,
|
||||
3 => Level::DEBUG,
|
||||
_ => Level::TRACE,
|
||||
};
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(max_level)
|
||||
.with_writer(file_appender)
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
|
203
src/pty.rs
203
src/pty.rs
|
@ -1,203 +0,0 @@
|
|||
use std::env;
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::time::Instant;
|
||||
|
||||
use nix::{
|
||||
fcntl::{fcntl, FcntlArg, OFlag},
|
||||
ioctl_write_buf,
|
||||
pty::{forkpty, Winsize},
|
||||
sys::{
|
||||
select::{select, FdSet},
|
||||
termios::{tcsetattr, SetArg, Termios},
|
||||
wait::waitpid,
|
||||
},
|
||||
unistd::{execvpe, isatty, pipe, read, write, ForkResult},
|
||||
};
|
||||
use signal_hook::SigId;
|
||||
|
||||
use crate::errors::{Error, Result};
|
||||
use crate::writer::Writer;
|
||||
|
||||
const STDIN_FILENO: RawFd = 0;
|
||||
const STDOUT_FILENO: RawFd = 1;
|
||||
|
||||
pub fn record(args: &[&str], writer: impl Writer + Send) -> Result<()> {
|
||||
let forkpty_result = forkpty(None, None)?;
|
||||
let master_fd = forkpty_result.master;
|
||||
let start_time = Instant::now();
|
||||
|
||||
let _set_pty_size = || -> Result<()> {
|
||||
ioctl_write_buf!(helper_write, libc::TIOCGWINSZ, 104, Winsize);
|
||||
let winsize = Winsize {
|
||||
ws_row: 24,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
if isatty(STDOUT_FILENO)? {}
|
||||
unsafe { helper_write(master_fd, &[winsize]) }?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let write_stdout = |data: &[u8]| -> Result<()> {
|
||||
write(STDOUT_FILENO, &data)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut master_writer = writer.clone();
|
||||
let mut handle_master_read = move |data: &[u8]| -> Result<()> {
|
||||
let elapsed = (Instant::now() - start_time).as_secs_f64();
|
||||
master_writer.write_stdout(elapsed, data)?;
|
||||
write_stdout(data)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let write_master = |data: &[u8]| -> Result<()> {
|
||||
let mut offset = 0;
|
||||
while offset < data.len() {
|
||||
let len = write(master_fd, data)
|
||||
.map_err(|err| Error::NixWrite(err, master_fd, data.to_vec()))?;
|
||||
offset += len;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut stdin_writer = writer.clone();
|
||||
let mut handle_stdin_read = |data: &[u8]| -> Result<()> {
|
||||
write_master(data)?;
|
||||
let elapsed = (Instant::now() - start_time).as_secs_f64();
|
||||
stdin_writer.write_stdin(elapsed, data)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut copy = |signal_fd: RawFd| -> Result<()> {
|
||||
let mut fdset = FdSet::new();
|
||||
let mut buf = [0; 1024];
|
||||
|
||||
loop {
|
||||
fdset.clear();
|
||||
fdset.insert(master_fd);
|
||||
fdset.insert(STDIN_FILENO);
|
||||
fdset.insert(signal_fd);
|
||||
|
||||
select(None, &mut fdset, None, None, None)?;
|
||||
|
||||
if fdset.contains(master_fd) {
|
||||
let len = read(master_fd, &mut buf)?;
|
||||
if len == 0 {
|
||||
fdset.remove(master_fd);
|
||||
} else {
|
||||
handle_master_read(&buf[..len])?;
|
||||
}
|
||||
} else if fdset.contains(STDIN_FILENO) {
|
||||
let len = read(STDIN_FILENO, &mut buf)?;
|
||||
if len == 0 {
|
||||
fdset.remove(STDIN_FILENO);
|
||||
} else {
|
||||
handle_stdin_read(&buf[..len])?;
|
||||
}
|
||||
} else if fdset.contains(signal_fd) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let child_pid = match forkpty_result.fork_result {
|
||||
ForkResult::Parent { child } => child,
|
||||
ForkResult::Child => {
|
||||
let cstr_args: Vec<_> = args
|
||||
.into_iter()
|
||||
.map(|s| CString::new(*s).unwrap())
|
||||
.collect();
|
||||
let args: Vec<_> = cstr_args.iter().map(|s| s.as_ref()).collect();
|
||||
let cstr_env: Vec<_> = env::vars()
|
||||
.map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
|
||||
.collect();
|
||||
let env: Vec<_> = cstr_env.iter().map(|s| s.as_ref()).collect();
|
||||
execvpe(&args[0], &args, &env)?;
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
let (pipe_r, pipe_w) = pipe()?;
|
||||
let mut flags = fcntl(pipe_w, FcntlArg::F_GETFL)?;
|
||||
flags |= libc::O_NONBLOCK;
|
||||
fcntl(pipe_w, FcntlArg::F_SETFL(OFlag::from_bits(flags).unwrap()))?;
|
||||
|
||||
let old_handlers = set_signals(
|
||||
[
|
||||
signal_hook::SIGWINCH,
|
||||
signal_hook::SIGCHLD,
|
||||
signal_hook::SIGHUP,
|
||||
signal_hook::SIGTERM,
|
||||
signal_hook::SIGQUIT,
|
||||
]
|
||||
.iter()
|
||||
.map(|sig| (*sig, move || eprintln!("HIT SIGNAL {:?}", *sig))),
|
||||
)?;
|
||||
|
||||
// set_pty_size()?;
|
||||
|
||||
{
|
||||
let _term = RawTerm::init(STDIN_FILENO)?;
|
||||
copy(pipe_r)?;
|
||||
}
|
||||
unset_signals(old_handlers)?;
|
||||
|
||||
waitpid(child_pid, None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_signals<I, F>(signals_list: I) -> Result<Vec<SigId>, io::Error>
|
||||
where
|
||||
I: Iterator<Item = (i32, F)>,
|
||||
F: Fn() + Sync + Send + 'static,
|
||||
{
|
||||
let mut old_handlers = Vec::new();
|
||||
for (sig, handler) in signals_list {
|
||||
old_handlers.push(unsafe { signal_hook::register(sig, handler) }?);
|
||||
}
|
||||
Ok(old_handlers)
|
||||
}
|
||||
|
||||
fn unset_signals(handlers_list: Vec<SigId>) -> Result<(), io::Error> {
|
||||
for handler in handlers_list {
|
||||
signal_hook::unregister(handler);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct RawTerm(RawFd, Termios);
|
||||
|
||||
impl RawTerm {
|
||||
pub fn init(fd: RawFd) -> Result<Self> {
|
||||
use nix::sys::termios::*;
|
||||
let saved_mode = tcgetattr(fd)?;
|
||||
let mut mode = saved_mode.clone();
|
||||
mode.input_flags &= !(InputFlags::BRKINT
|
||||
| InputFlags::ICRNL
|
||||
| InputFlags::INPCK
|
||||
| InputFlags::ISTRIP
|
||||
| InputFlags::IXON);
|
||||
mode.output_flags &= !OutputFlags::OPOST;
|
||||
mode.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB);
|
||||
mode.control_flags |= ControlFlags::CS8;
|
||||
mode.local_flags &= !(LocalFlags::ECHO
|
||||
| LocalFlags::ICANON
|
||||
| LocalFlags::IEXTEN
|
||||
| LocalFlags::ISIG);
|
||||
mode.control_chars[libc::VMIN] = 1;
|
||||
mode.control_chars[libc::VTIME] = 0;
|
||||
tcsetattr(fd, SetArg::TCSAFLUSH, &mode)?;
|
||||
Ok(RawTerm(fd, saved_mode))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RawTerm {
|
||||
fn drop(&mut self) {
|
||||
tcsetattr(self.0, SetArg::TCSAFLUSH, &self.1).unwrap();
|
||||
}
|
||||
}
|
310
src/recorder.rs
310
src/recorder.rs
|
@ -1,30 +1,298 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
//! Setting up a recorder
|
||||
|
||||
pub struct RecordOptions {
|
||||
pub path: PathBuf,
|
||||
pub append: Option<bool>,
|
||||
pub command: Option<String>,
|
||||
pub capture_env: Option<Vec<String>>,
|
||||
pub title: Option<String>,
|
||||
use std::os::unix::prelude::RawFd;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
use anyhow::Result;
|
||||
use libc::{STDIN_FILENO, STDOUT_FILENO};
|
||||
use nix::pty::{openpty, OpenptyResult};
|
||||
use nix::sys::select::{select, FdSet};
|
||||
use nix::unistd::{dup, dup2, fsync, read, write};
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::asciicast::Event;
|
||||
|
||||
pub struct Terminal {
|
||||
/// The file descriptor that the parent process' original stdout got duplicated to.
|
||||
///
|
||||
/// This needs to be saved so we can restore it later.
|
||||
parent_stdout_dup: RawFd,
|
||||
|
||||
pty: OpenptyResult,
|
||||
|
||||
event_tx: Sender<Event>,
|
||||
}
|
||||
|
||||
pub fn record(options: RecordOptions) {
|
||||
let _command = options
|
||||
.command
|
||||
.unwrap_or_else(|| env::var("SHELL").unwrap_or_else(|_| "sh".to_string()));
|
||||
impl Terminal {
|
||||
pub fn setup(mut command: Command) -> Result<(Self, Receiver<Event>)> {
|
||||
command.env("TERM", "xterm-256color");
|
||||
|
||||
let _command_env = env::vars();
|
||||
// Set up channels
|
||||
let (event_tx, event_rx) = mpsc::channel::<Event>();
|
||||
|
||||
// let header_env = HashMap::new();
|
||||
// Open a pty
|
||||
let pty = openpty(None, None)?;
|
||||
let parent_stdout_dup = dup(STDOUT_FILENO)?;
|
||||
info!(parent_stdout_dup, "Duplicated parent process' stdout.");
|
||||
dup2(pty.slave, STDOUT_FILENO)?;
|
||||
info!(
|
||||
redirected_to = pty.slave,
|
||||
"Redirected parent process' stdout to new pty."
|
||||
);
|
||||
|
||||
// let (width, height) = term::get_size();
|
||||
eprintln!("Starting child process...");
|
||||
|
||||
// let header = Header {
|
||||
// version: 2,
|
||||
// width,
|
||||
// height,
|
||||
// Spawn the child
|
||||
let child = command.spawn()?;
|
||||
let _child_pid = child.id();
|
||||
|
||||
// title: options.title,
|
||||
// };
|
||||
let term = Terminal {
|
||||
parent_stdout_dup,
|
||||
pty,
|
||||
event_tx,
|
||||
};
|
||||
Ok((term, event_rx))
|
||||
}
|
||||
|
||||
pub fn wait_until_complete(self) -> Result<()> {
|
||||
self.run()?;
|
||||
|
||||
// Restore stdout
|
||||
fsync(STDOUT_FILENO)?;
|
||||
dup2(self.parent_stdout_dup, STDOUT_FILENO)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(&self) -> Result<()> {
|
||||
let mut read_fd_set = FdSet::new();
|
||||
let mut buf = [0; 1024];
|
||||
|
||||
info!("Starting read loop...");
|
||||
loop {
|
||||
// Set up fdsets
|
||||
// asciinema does not capture stdin by default
|
||||
read_fd_set.clear();
|
||||
read_fd_set.insert(self.pty.master);
|
||||
read_fd_set.insert(STDIN_FILENO);
|
||||
// TODO: Signal file descriptor
|
||||
|
||||
select(None, &mut read_fd_set, None, None, None)?;
|
||||
|
||||
// Master is ready for read, which means child process stdout has data
|
||||
// (child process stdout) -> (pty.slave) -> (pty.master)
|
||||
if read_fd_set.contains(self.pty.master) {
|
||||
let bytes_read = read(self.pty.master, &mut buf)?;
|
||||
if bytes_read == 0 {
|
||||
info!("Read 0 bytes, exiting the read loop.");
|
||||
break;
|
||||
}
|
||||
|
||||
let data = &buf[..bytes_read];
|
||||
debug!(bytes_read, "Read data from master.");
|
||||
|
||||
// We should take this and rewrite this to the current stdout
|
||||
write(STDOUT_FILENO, data)?;
|
||||
}
|
||||
// Stdin is ready for read, which means input from the user
|
||||
else if read_fd_set.contains(STDIN_FILENO) {
|
||||
let bytes_read = read(self.pty.master, &mut buf)?;
|
||||
let data = &buf[..bytes_read];
|
||||
debug!(bytes_read, "Read data from master.");
|
||||
|
||||
write(self.pty.master, data)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn record(args: &[&str], writer: impl Writer + Send) -> Result<()> {
|
||||
// Setup
|
||||
|
||||
let forkpty_result = forkpty(None, None)?;
|
||||
let master_fd = forkpty_result.master;
|
||||
let start_time = Instant::now();
|
||||
|
||||
let _set_pty_size = || -> Result<()> {
|
||||
ioctl_write_buf!(helper_write, libc::TIOCGWINSZ, 104, Winsize);
|
||||
let winsize = Winsize {
|
||||
ws_row: 24,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
if isatty(STDOUT_FILENO)? {}
|
||||
unsafe { helper_write(master_fd, &[winsize]) }?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let write_stdout = |data: &[u8]| -> Result<()> {
|
||||
write(STDOUT_FILENO, &data)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut master_writer = writer.clone();
|
||||
let mut handle_master_read = move |data: &[u8]| -> Result<()> {
|
||||
let elapsed = (Instant::now() - start_time).as_secs_f64();
|
||||
master_writer.write_stdout(elapsed, data)?;
|
||||
write_stdout(data)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let write_master = |data: &[u8]| -> Result<()> {
|
||||
let mut offset = 0;
|
||||
while offset < data.len() {
|
||||
let len = write(master_fd, data)
|
||||
.map_err(|err| Error::NixWrite(err, master_fd, data.to_vec()))?;
|
||||
offset += len;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut stdin_writer = writer.clone();
|
||||
let mut handle_stdin_read = |data: &[u8]| -> Result<()> {
|
||||
write_master(data)?;
|
||||
let elapsed = (Instant::now() - start_time).as_secs_f64();
|
||||
stdin_writer.write_stdin(elapsed, data)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// Copy fds?
|
||||
|
||||
let mut copy = |signal_fd: RawFd| -> Result<()> {
|
||||
let mut fdset = FdSet::new();
|
||||
let mut buf = [0; 1024];
|
||||
|
||||
loop {
|
||||
fdset.clear();
|
||||
fdset.insert(master_fd);
|
||||
fdset.insert(STDIN_FILENO);
|
||||
fdset.insert(signal_fd);
|
||||
|
||||
select(None, &mut fdset, None, None, None)?;
|
||||
|
||||
if fdset.contains(master_fd) {
|
||||
let len = read(master_fd, &mut buf)?;
|
||||
if len == 0 {
|
||||
fdset.remove(master_fd);
|
||||
} else {
|
||||
handle_master_read(&buf[..len])?;
|
||||
}
|
||||
} else if fdset.contains(STDIN_FILENO) {
|
||||
let len = read(STDIN_FILENO, &mut buf)?;
|
||||
if len == 0 {
|
||||
fdset.remove(STDIN_FILENO);
|
||||
} else {
|
||||
handle_stdin_read(&buf[..len])?;
|
||||
}
|
||||
} else if fdset.contains(signal_fd) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fork
|
||||
|
||||
let child_pid = match forkpty_result.fork_result {
|
||||
ForkResult::Parent { child } => child,
|
||||
ForkResult::Child => {
|
||||
let cstr_args: Vec<_> = args
|
||||
.into_iter()
|
||||
.map(|s| CString::new(*s).unwrap())
|
||||
.collect();
|
||||
let args: Vec<_> = cstr_args.iter().map(|s| s.as_ref()).collect();
|
||||
let cstr_env: Vec<_> = env::vars()
|
||||
.map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
|
||||
.collect();
|
||||
let env: Vec<_> = cstr_env.iter().map(|s| s.as_ref()).collect();
|
||||
execvpe(&args[0], &args, &env)?;
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
let (pipe_r, pipe_w) = pipe()?;
|
||||
let mut flags = fcntl(pipe_w, FcntlArg::F_GETFL)?;
|
||||
flags |= libc::O_NONBLOCK;
|
||||
fcntl(pipe_w, FcntlArg::F_SETFL(OFlag::from_bits(flags).unwrap()))?;
|
||||
|
||||
// Intercept signal handlers
|
||||
|
||||
let old_handlers = set_signals(
|
||||
[
|
||||
signal_hook::SIGWINCH,
|
||||
signal_hook::SIGCHLD,
|
||||
signal_hook::SIGHUP,
|
||||
signal_hook::SIGTERM,
|
||||
signal_hook::SIGQUIT,
|
||||
]
|
||||
.iter()
|
||||
.map(|sig| (*sig, move || eprintln!("HIT SIGNAL {:?}", *sig))),
|
||||
)?;
|
||||
|
||||
// set_pty_size()?;
|
||||
|
||||
{
|
||||
let _term = RawTerm::init(STDIN_FILENO)?;
|
||||
copy(pipe_r)?;
|
||||
}
|
||||
unset_signals(old_handlers)?;
|
||||
|
||||
waitpid(child_pid, None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_signals<I, F>(signals_list: I) -> Result<Vec<SigId>, io::Error>
|
||||
where
|
||||
I: Iterator<Item = (i32, F)>,
|
||||
F: Fn() + Sync + Send + 'static,
|
||||
{
|
||||
let mut old_handlers = Vec::new();
|
||||
for (sig, handler) in signals_list {
|
||||
old_handlers.push(unsafe { signal_hook::register(sig, handler) }?);
|
||||
}
|
||||
Ok(old_handlers)
|
||||
}
|
||||
|
||||
fn unset_signals(handlers_list: Vec<SigId>) -> Result<(), io::Error> {
|
||||
for handler in handlers_list {
|
||||
signal_hook::unregister(handler);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct RawTerm(RawFd, Termios);
|
||||
|
||||
impl RawTerm {
|
||||
pub fn init(fd: RawFd) -> Result<Self> {
|
||||
use nix::sys::termios::*;
|
||||
let saved_mode = tcgetattr(fd)?;
|
||||
let mut mode = saved_mode.clone();
|
||||
mode.input_flags &= !(InputFlags::BRKINT
|
||||
| InputFlags::ICRNL
|
||||
| InputFlags::INPCK
|
||||
| InputFlags::ISTRIP
|
||||
| InputFlags::IXON);
|
||||
mode.output_flags &= !OutputFlags::OPOST;
|
||||
mode.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB);
|
||||
mode.control_flags |= ControlFlags::CS8;
|
||||
mode.local_flags &= !(LocalFlags::ECHO
|
||||
| LocalFlags::ICANON
|
||||
| LocalFlags::IEXTEN
|
||||
| LocalFlags::ISIG);
|
||||
mode.control_chars[libc::VMIN] = 1;
|
||||
mode.control_chars[libc::VTIME] = 0;
|
||||
tcsetattr(fd, SetArg::TCSAFLUSH, &mode)?;
|
||||
Ok(RawTerm(fd, saved_mode))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RawTerm {
|
||||
fn drop(&mut self) {
|
||||
tcsetattr(self.0, SetArg::TCSAFLUSH, &self.1).unwrap();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue