try to replicate the original more
This commit is contained in:
parent
eb6b76f3ff
commit
c555b2e006
6 changed files with 203 additions and 65 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
||||||
/target
|
/target
|
||||||
/output.cast
|
output.cast
|
||||||
.direnv
|
.direnv
|
||||||
/logs
|
/logs
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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;
|
||||||
|
|
54
src/main.rs
54
src/main.rs
|
@ -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(())
|
||||||
|
|
190
src/recorder.rs
190
src/recorder.rs
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue