try to replicate the original more

This commit is contained in:
Michael Zhang 2023-01-10 01:43:18 -06:00
parent eb6b76f3ff
commit c555b2e006
6 changed files with 203 additions and 65 deletions

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
/target /target
/output.cast output.cast
.direnv .direnv
/logs /logs

View file

View file

@ -8,17 +8,17 @@ use serde::ser::{Serialize, SerializeSeq, Serializer};
#[derive(Serialize)] #[derive(Serialize)]
pub struct Header { pub struct Header {
version: u32, pub version: u32,
width: u32, pub width: u32,
height: u32, pub height: u32,
timestamp: Option<u32>, pub timestamp: Option<u32>,
duration: Option<f64>, pub duration: Option<f64>,
idle_time_limit: Option<f64>, pub idle_time_limit: Option<f64>,
command: Option<String>, pub command: Option<String>,
title: Option<String>, pub title: Option<String>,
env: Option<HashMap<String, String>>, pub env: Option<HashMap<String, String>>,
theme: Option<Theme>, pub theme: Option<Theme>,
} }
#[derive(Serialize)] #[derive(Serialize)]

View file

@ -1,4 +1,6 @@
#[macro_use] #[macro_use]
extern crate anyhow;
#[macro_use]
extern crate serde_derive; extern crate serde_derive;
#[macro_use] #[macro_use]
extern crate tracing; extern crate tracing;

View file

@ -1,12 +1,13 @@
#[macro_use] #[macro_use]
extern crate tracing; extern crate tracing;
use std::fs::File; use std::process::Command;
use std::thread;
use std::{fs::File, io::Write};
use anyhow::Result; use anyhow::Result;
use clap::{ArgAction, Parser}; use clap::{ArgAction, Parser};
use liveterm::recorder::Terminal; use liveterm::{asciicast::Header, recorder::Terminal};
use tokio::process::Command;
use tracing::Level; use tracing::Level;
#[derive(Parser)] #[derive(Parser)]
@ -26,12 +27,53 @@ enum Subcommand {
} }
fn record() -> Result<()> { fn record() -> Result<()> {
let _file = File::create("output.cast")?; let mut file = File::create("output.cast")?;
let mut command = Command::new("/usr/bin/env"); let mut command = Command::new("zsh");
command.arg("bash"); // command.arg("bash");
command.env("TERM", "xterm-256color");
// Write header
// TODO: Clean this up
let header = {
let command_str = format!("{:?}", command);
let env = command
.get_envs()
.into_iter()
.filter_map(|(a, b)| b.map(|b| (a, b)))
.map(|(a, b)| {
(
a.to_string_lossy().to_string(),
b.to_string_lossy().to_string(),
)
})
.collect();
Header {
version: 2,
width: 30,
height: 30,
timestamp: None,
duration: None,
idle_time_limit: None,
command: Some(command_str),
title: None,
env: Some(env),
theme: None,
}
};
serde_json::to_writer(&file, &header)?;
file.write(b"\n")?;
let (pty, rx) = Terminal::setup(command)?; let (pty, rx) = Terminal::setup(command)?;
thread::spawn(move || {
for event in rx.into_iter() {
serde_json::to_writer(&file, &event);
file.write(b"\n");
}
});
pty.wait_until_complete()?; pty.wait_until_complete()?;
Ok(()) Ok(())

View file

@ -1,40 +1,66 @@
//! Setting up a recorder //! Setting up a recorder
use std::os::unix::prelude::RawFd; use std::env;
use std::ffi::CString;
use std::os::unix::prelude::{AsRawFd, OsStrExt, RawFd};
use std::process::{Command, Stdio};
use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::mpsc::{self, Receiver, Sender};
use std::time::Instant;
use anyhow::Result; use anyhow::{Context, Result};
use libc::{STDIN_FILENO, STDOUT_FILENO}; use libc::{O_RDWR, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
use nix::fcntl::{open, OFlag};
use nix::pty::{openpty, OpenptyResult}; use nix::pty::{openpty, OpenptyResult};
use nix::sys::select::{select, FdSet}; use nix::sys::select::{select, FdSet};
use nix::unistd::{dup, dup2, fsync, read, write}; use nix::sys::stat::Mode;
use tokio::process::Command; use nix::unistd::{
close, dup, dup2, execvp, execvpe, fork, fsync, read, setsid, ttyname, write,
ForkResult,
};
use crate::asciicast::Event; use crate::asciicast::{Event, EventKind};
pub struct Terminal { 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>, event_tx: Sender<Event>,
command: Command,
} }
impl Terminal { impl Terminal {
pub fn setup(mut command: Command) -> Result<(Self, Receiver<Event>)> { pub fn setup(command: Command) -> Result<(Self, Receiver<Event>)> {
command.env("TERM", "xterm-256color");
// Set up channels // Set up channels
let (event_tx, event_rx) = mpsc::channel::<Event>(); let (event_tx, event_rx) = mpsc::channel::<Event>();
let term = Terminal { event_tx, command };
Ok((term, event_rx))
}
pub fn wait_until_complete(self) -> Result<()> {
self.run()?;
Ok(())
}
fn run(self) -> Result<()> {
// References:
// - https://github.com/python/cpython/blob/main/Lib/pty.py
// - https://github.com/asciinema/asciinema/blob/develop/asciinema/pty_.py
// TODO: Investigate cross-platformness
if cfg!(target_os = "windows") {
panic!("Not supported on windows.");
}
let mut command = self.command;
// Merge environment
for (key, val) in env::vars() {
command.env(key, val);
}
// Open a pty // Open a pty
let pty = openpty(None, None)?; let pty = openpty(None, None)?;
let parent_stdout_dup = dup(STDOUT_FILENO)?; let parent_stdout_dup = dup(STDOUT_FILENO)?;
info!(parent_stdout_dup, "Duplicated parent process' stdout."); info!(parent_stdout_dup, "Duplicated parent process' stdout.");
dup2(pty.slave, STDOUT_FILENO)?; // dup2(pty.slave, STDOUT_FILENO)?;
info!( info!(
redirected_to = pty.slave, redirected_to = pty.slave,
"Redirected parent process' stdout to new pty." "Redirected parent process' stdout to new pty."
@ -43,71 +69,139 @@ impl Terminal {
eprintln!("Starting child process..."); eprintln!("Starting child process...");
// Spawn the child // Spawn the child
let child = command.spawn()?; let child_pid = match unsafe { fork() } {
let _child_pid = child.id(); Ok(ForkResult::Parent { child }) => {
info!(
child_pid = child.as_raw(),
"Continuing execution in parent process.",
);
let term = Terminal { close(pty.slave)?;
parent_stdout_dup,
pty, child
event_tx, }
Ok(ForkResult::Child) => {
setsid()?;
close(pty.master)?;
// Duplicate all stdio descriptors to slave
dup2(pty.slave, STDIN_FILENO)?;
dup2(pty.slave, STDOUT_FILENO)?;
dup2(pty.slave, STDERR_FILENO)?;
if pty.slave > STDERR_FILENO {
close(pty.slave)?;
}
let tty_path = ttyname(STDOUT_FILENO)?;
let tmp_fd = open(&tty_path, OFlag::O_RDWR, Mode::S_IRWXU)?;
close(tmp_fd)?;
exec_command(command)?;
unreachable!()
}
Err(err) => bail!("Could not fork: {}", err),
}; };
Ok((term, event_rx))
}
pub fn wait_until_complete(self) -> Result<()> { // Set up fd sets and buffers for reading
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 read_fd_set = FdSet::new();
let mut buf = [0; 1024]; let mut buf = [0; 1024];
// Set up recording function
let start_time = Instant::now();
let record = |now: Instant, output: bool, data: &[u8]| -> Result<()> {
let elapsed = (now - start_time).as_secs_f64();
let event_kind = (match output {
true => EventKind::Output,
false => EventKind::Input,
})(data.to_vec());
let event = Event(elapsed, event_kind);
self.event_tx.send(event)?;
Ok(())
};
info!("Starting read loop..."); info!("Starting read loop...");
loop { loop {
// Set up fdsets // Set up fdsets
// asciinema does not capture stdin by default // asciinema does not capture stdin by default
read_fd_set.clear(); read_fd_set.clear();
read_fd_set.insert(self.pty.master); read_fd_set.insert(pty.master);
read_fd_set.insert(STDIN_FILENO); read_fd_set.insert(STDIN_FILENO);
// TODO: Signal file descriptor // TODO: Signal file descriptor
select(None, &mut read_fd_set, None, None, None)?; select(None, &mut read_fd_set, None, None, None).context("Select")?;
// Master is ready for read, which means child process stdout has data // Master is ready for read, which means child process stdout has data
// (child process stdout) -> (pty.slave) -> (pty.master) // (child process stdout) -> (pty.slave) -> (pty.master)
if read_fd_set.contains(self.pty.master) { if read_fd_set.contains(pty.master) {
let bytes_read = read(self.pty.master, &mut buf)?; let bytes_read =
read(pty.master, &mut buf).context("Read from master")?;
if bytes_read == 0 { if bytes_read == 0 {
info!("Read 0 bytes, exiting the read loop."); info!("Read 0 bytes, exiting the read loop.");
break; break;
} }
let now = Instant::now();
let data = &buf[..bytes_read]; let data = &buf[..bytes_read];
debug!(bytes_read, "Read data from master."); debug!(bytes_read, "Read data from master.");
// We should take this and rewrite this to the current stdout // We should take this and rewrite this to the current stdout
write(STDOUT_FILENO, data)?; write(STDOUT_FILENO, data).context("Write to current STDOUT")?;
}
// 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)?; // Also, record this
record(now, true, data).context("Record data")?;
}
// Stdin is ready for read, which means input from the user
if read_fd_set.contains(STDIN_FILENO) {
let bytes_read =
read(STDIN_FILENO, &mut buf).context("Read from current STDIN")?;
let data = &buf[..bytes_read];
debug!(bytes_read, "Read data from current STDIN.");
let now = Instant::now();
write(pty.master, data).context("Write to master")?;
// Also, record this
record(now, false, data).context("Record data")?;
} }
} }
// Restore stdout
fsync(STDOUT_FILENO)?;
dup2(parent_stdout_dup, STDOUT_FILENO)?;
Ok(()) Ok(())
} }
} }
fn exec_command(command: Command) -> Result<()> {
let program = CString::new(command.get_program().as_bytes())?;
let args = command
.get_args()
.into_iter()
.map(|osstr| CString::new(osstr.as_bytes()).map_err(|e| e.into()))
.collect::<Result<Vec<_>>>()?;
let env = command
.get_envs()
.into_iter()
.filter_map(|(key, val)| {
let val = match val {
Some(val) => val,
None => return None,
};
let mut key = key.to_os_string();
let equals = String::from("=");
key.extend([equals.as_ref(), val]);
Some(CString::new(key.as_bytes()).map_err(|e| e.into()))
})
.collect::<Result<Vec<_>>>()?;
execvpe(&program, &args, &env)?;
unreachable!()
}
/* /*
pub fn record(args: &[&str], writer: impl Writer + Send) -> Result<()> { pub fn record(args: &[&str], writer: impl Writer + Send) -> Result<()> {
// Setup // Setup