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
|
/target
|
||||||
/output.cast
|
/output.cast
|
||||||
.direnv
|
.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]
|
[package]
|
||||||
name = "asciinema"
|
name = "liveterm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Michael Zhang <iptq@protonmail.com>"]
|
authors = ["Michael Zhang <iptq@protonmail.com>"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.34"
|
anyhow = { version = "1.0.68", features = ["backtrace"] }
|
||||||
futures = "0.3.7"
|
chrono = "0.4.23"
|
||||||
libc = "0.2.80"
|
clap = { version = "4.0.32", features = ["derive"] }
|
||||||
nix = "0.19.0"
|
futures = "0.3.25"
|
||||||
parking_lot = "0.11.0"
|
libc = "0.2.139"
|
||||||
serde = "1.0.117"
|
log = "0.4.17"
|
||||||
serde_derive = "1.0.117"
|
nix = "0.26.1"
|
||||||
serde_json = "1.0.59"
|
parking_lot = "0.12.1"
|
||||||
signal-hook = "0.1.16"
|
serde = "1.0.152"
|
||||||
structopt = "0.3.20"
|
serde_derive = "1.0.152"
|
||||||
|
serde_json = "1.0.91"
|
||||||
|
signal-hook = "0.3.14"
|
||||||
termios = "0.3.3"
|
termios = "0.3.3"
|
||||||
thiserror = "1.0.22"
|
tokio = { version = "1.24.1", features = ["full"] }
|
||||||
tokio = { version = "0.3.3", features = ["full"] }
|
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||||
tokio-util = { version = "0.5.0", 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 std::collections::HashMap;
|
||||||
|
|
||||||
use serde::ser::{Serialize, SerializeSeq, Serializer};
|
use serde::ser::{Serialize, SerializeSeq, Serializer};
|
||||||
|
@ -24,14 +28,14 @@ pub struct Theme {
|
||||||
palette: String,
|
palette: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Event<'a>(pub f64, pub EventKind<'a>);
|
pub struct Event(pub f64, pub EventKind);
|
||||||
|
|
||||||
pub enum EventKind<'a> {
|
pub enum EventKind {
|
||||||
Output(&'a [u8]),
|
Output(Vec<u8>),
|
||||||
Input(&'a [u8]),
|
Input(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Serialize for Event<'a> {
|
impl Serialize for Event {
|
||||||
fn serialize<S: Serializer>(
|
fn serialize<S: Serializer>(
|
||||||
&self,
|
&self,
|
||||||
s: S,
|
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]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate tracing;
|
||||||
#[macro_use]
|
|
||||||
extern crate thiserror;
|
|
||||||
|
|
||||||
mod asciicast;
|
|
||||||
mod errors;
|
|
||||||
mod pty;
|
|
||||||
mod recorder;
|
|
||||||
mod term;
|
|
||||||
mod writer;
|
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
use anyhow::Result;
|
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)]
|
#[clap(long, short = 'v', action = ArgAction::Count, global = true)]
|
||||||
enum Command {
|
verbose: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
enum Subcommand {
|
||||||
/// Record terminal session.
|
/// Record terminal session.
|
||||||
#[structopt(name = "rec")]
|
#[structopt(name = "rec")]
|
||||||
Record,
|
Record,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record() -> Result<()> {
|
fn record() -> Result<()> {
|
||||||
let file = File::create("output.cast")?;
|
let _file = File::create("output.cast")?;
|
||||||
pty::record(&["sh", "-c", "/usr/bin/zsh"], FileWriter::new(file))?;
|
|
||||||
|
let mut command = Command::new("/usr/bin/env");
|
||||||
|
command.arg("bash");
|
||||||
|
|
||||||
|
let (pty, rx) = Terminal::setup(command)?;
|
||||||
|
pty.wait_until_complete()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
match Command::from_args() {
|
let opt = Opt::parse();
|
||||||
Command::Record => {
|
setup_logging(&opt)?;
|
||||||
|
|
||||||
|
match opt.subcommand {
|
||||||
|
Subcommand::Record => {
|
||||||
record()?;
|
record()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
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;
|
//! Setting up a recorder
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub struct RecordOptions {
|
use std::os::unix::prelude::RawFd;
|
||||||
pub path: PathBuf,
|
use std::sync::mpsc::{self, Receiver, Sender};
|
||||||
pub append: Option<bool>,
|
|
||||||
pub command: Option<String>,
|
use anyhow::Result;
|
||||||
pub capture_env: Option<Vec<String>>,
|
use libc::{STDIN_FILENO, STDOUT_FILENO};
|
||||||
pub title: Option<String>,
|
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) {
|
impl Terminal {
|
||||||
let _command = options
|
pub fn setup(mut command: Command) -> Result<(Self, Receiver<Event>)> {
|
||||||
.command
|
command.env("TERM", "xterm-256color");
|
||||||
.unwrap_or_else(|| env::var("SHELL").unwrap_or_else(|_| "sh".to_string()));
|
|
||||||
|
|
||||||
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 {
|
// Spawn the child
|
||||||
// version: 2,
|
let child = command.spawn()?;
|
||||||
// width,
|
let _child_pid = child.id();
|
||||||
// height,
|
|
||||||
|
|
||||||
// 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