mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-16 19:58:03 +01:00
Fix recording in headless mode - when TTY is not available
This commit is contained in:
@@ -3,6 +3,7 @@ use crate::format::{asciicast, raw};
|
|||||||
use crate::locale;
|
use crate::locale;
|
||||||
use crate::pty;
|
use crate::pty;
|
||||||
use crate::recorder;
|
use crate::recorder;
|
||||||
|
use crate::tty;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -116,9 +117,17 @@ impl Cli {
|
|||||||
println!("asciinema: recording asciicast to {}", self.filename);
|
println!("asciinema: recording asciicast to {}", self.filename);
|
||||||
println!("asciinema: press <ctrl+d> or type \"exit\" when you're done");
|
println!("asciinema: press <ctrl+d> or type \"exit\" when you're done");
|
||||||
|
|
||||||
|
let tty: Box<dyn tty::Tty> = if let Ok(dev_tty) = tty::DevTty::open() {
|
||||||
|
Box::new(dev_tty)
|
||||||
|
} else {
|
||||||
|
println!("asciinema: TTY not available, recording in headless mode");
|
||||||
|
Box::new(tty::DevNull::open()?)
|
||||||
|
};
|
||||||
|
|
||||||
pty::exec(
|
pty::exec(
|
||||||
&exec_args,
|
&exec_args,
|
||||||
&exec_extra_env,
|
&exec_extra_env,
|
||||||
|
tty,
|
||||||
(self.cols, self.rows),
|
(self.cols, self.rows),
|
||||||
&mut recorder,
|
&mut recorder,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod locale;
|
|||||||
mod player;
|
mod player;
|
||||||
mod pty;
|
mod pty;
|
||||||
mod recorder;
|
mod recorder;
|
||||||
|
mod tty;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|||||||
66
src/pty.rs
66
src/pty.rs
@@ -1,4 +1,5 @@
|
|||||||
use crate::io::set_non_blocking;
|
use crate::io::set_non_blocking;
|
||||||
|
use crate::tty::Tty;
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use mio::unix::SourceFd;
|
use mio::unix::SourceFd;
|
||||||
use nix::{libc, pty, sys::signal, sys::wait, unistd, unistd::ForkResult};
|
use nix::{libc, pty, sys::signal, sys::wait, unistd, unistd::ForkResult};
|
||||||
@@ -7,11 +8,9 @@ use signal_hook_mio::v0_8::Signals;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::{CString, NulError};
|
use std::ffi::{CString, NulError};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::ops::Deref;
|
|
||||||
use std::os::fd::RawFd;
|
use std::os::fd::RawFd;
|
||||||
use std::os::unix::io::{AsRawFd, FromRawFd};
|
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
use termion::raw::IntoRawMode;
|
|
||||||
|
|
||||||
type ExtraEnv = HashMap<String, String>;
|
type ExtraEnv = HashMap<String, String>;
|
||||||
|
|
||||||
@@ -25,19 +24,19 @@ pub trait Recorder {
|
|||||||
pub fn exec<S: AsRef<str>, R: Recorder>(
|
pub fn exec<S: AsRef<str>, R: Recorder>(
|
||||||
args: &[S],
|
args: &[S],
|
||||||
extra_env: &ExtraEnv,
|
extra_env: &ExtraEnv,
|
||||||
|
tty: Box<dyn Tty>,
|
||||||
winsize_override: (Option<u16>, Option<u16>),
|
winsize_override: (Option<u16>, Option<u16>),
|
||||||
recorder: &mut R,
|
recorder: &mut R,
|
||||||
) -> anyhow::Result<i32> {
|
) -> anyhow::Result<i32> {
|
||||||
let tty = open_tty()?;
|
let winsize = get_tty_size(&*tty, winsize_override);
|
||||||
let winsize = get_tty_size(tty.as_raw_fd(), winsize_override);
|
|
||||||
recorder.start((winsize.ws_col, winsize.ws_row))?;
|
recorder.start((winsize.ws_col, winsize.ws_row))?;
|
||||||
let result = unsafe { pty::forkpty(Some(&winsize), None) }?;
|
let result = unsafe { pty::forkpty(Some(&winsize), None) }?;
|
||||||
|
|
||||||
match result.fork_result {
|
match result.fork_result {
|
||||||
ForkResult::Parent { child } => handle_parent(
|
ForkResult::Parent { child } => handle_parent(
|
||||||
result.master.as_raw_fd(),
|
result.master.as_raw_fd(),
|
||||||
tty,
|
|
||||||
child,
|
child,
|
||||||
|
tty,
|
||||||
winsize_override,
|
winsize_override,
|
||||||
recorder,
|
recorder,
|
||||||
),
|
),
|
||||||
@@ -51,12 +50,12 @@ pub fn exec<S: AsRef<str>, R: Recorder>(
|
|||||||
|
|
||||||
fn handle_parent<R: Recorder>(
|
fn handle_parent<R: Recorder>(
|
||||||
master_fd: RawFd,
|
master_fd: RawFd,
|
||||||
tty: fs::File,
|
|
||||||
child: unistd::Pid,
|
child: unistd::Pid,
|
||||||
|
tty: Box<dyn Tty>,
|
||||||
winsize_override: (Option<u16>, Option<u16>),
|
winsize_override: (Option<u16>, Option<u16>),
|
||||||
recorder: &mut R,
|
recorder: &mut R,
|
||||||
) -> anyhow::Result<i32> {
|
) -> anyhow::Result<i32> {
|
||||||
let copy_result = copy(master_fd, tty, child, winsize_override, recorder);
|
let copy_result = copy(master_fd, child, tty, winsize_override, recorder);
|
||||||
let wait_result = wait::waitpid(child, None);
|
let wait_result = wait::waitpid(child, None);
|
||||||
copy_result?;
|
copy_result?;
|
||||||
|
|
||||||
@@ -75,8 +74,8 @@ const BUF_SIZE: usize = 128 * 1024;
|
|||||||
|
|
||||||
fn copy<R: Recorder>(
|
fn copy<R: Recorder>(
|
||||||
master_fd: RawFd,
|
master_fd: RawFd,
|
||||||
tty: fs::File,
|
|
||||||
child: unistd::Pid,
|
child: unistd::Pid,
|
||||||
|
mut tty: Box<dyn Tty>,
|
||||||
winsize_override: (Option<u16>, Option<u16>),
|
winsize_override: (Option<u16>, Option<u16>),
|
||||||
recorder: &mut R,
|
recorder: &mut R,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@@ -84,7 +83,6 @@ fn copy<R: Recorder>(
|
|||||||
let mut poll = mio::Poll::new()?;
|
let mut poll = mio::Poll::new()?;
|
||||||
let mut events = mio::Events::with_capacity(128);
|
let mut events = mio::Events::with_capacity(128);
|
||||||
let mut master_source = SourceFd(&master_fd);
|
let mut master_source = SourceFd(&master_fd);
|
||||||
let mut tty = tty.into_raw_mode()?;
|
|
||||||
let tty_fd = tty.as_raw_fd();
|
let tty_fd = tty.as_raw_fd();
|
||||||
let mut tty_source = SourceFd(&tty_fd);
|
let mut tty_source = SourceFd(&tty_fd);
|
||||||
let mut signals = Signals::new([SIGWINCH, SIGINT, SIGTERM, SIGQUIT, SIGHUP])?;
|
let mut signals = Signals::new([SIGWINCH, SIGINT, SIGTERM, SIGQUIT, SIGHUP])?;
|
||||||
@@ -94,7 +92,6 @@ fn copy<R: Recorder>(
|
|||||||
let mut flush = false;
|
let mut flush = false;
|
||||||
|
|
||||||
set_non_blocking(&master_fd)?;
|
set_non_blocking(&master_fd)?;
|
||||||
set_non_blocking(&tty_fd)?;
|
|
||||||
|
|
||||||
poll.registry()
|
poll.registry()
|
||||||
.register(&mut master_source, MASTER, mio::Interest::READABLE)?;
|
.register(&mut master_source, MASTER, mio::Interest::READABLE)?;
|
||||||
@@ -147,11 +144,11 @@ fn copy<R: Recorder>(
|
|||||||
if event.is_read_closed() {
|
if event.is_read_closed() {
|
||||||
poll.registry().deregister(&mut master_source)?;
|
poll.registry().deregister(&mut master_source)?;
|
||||||
|
|
||||||
if !output.is_empty() {
|
if output.is_empty() {
|
||||||
flush = true;
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flush = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,19 +159,19 @@ fn copy<R: Recorder>(
|
|||||||
if left == 0 {
|
if left == 0 {
|
||||||
if flush {
|
if flush {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
|
||||||
poll.registry().reregister(
|
|
||||||
&mut tty_source,
|
|
||||||
TTY,
|
|
||||||
mio::Interest::READABLE,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
poll.registry().reregister(
|
||||||
|
&mut tty_source,
|
||||||
|
TTY,
|
||||||
|
mio::Interest::READABLE,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.is_readable() {
|
if event.is_readable() {
|
||||||
let offset = input.len();
|
let offset = input.len();
|
||||||
let read = read_all(&mut tty.deref(), &mut buf, &mut input)?;
|
let read = read_all(&mut tty, &mut buf, &mut input)?;
|
||||||
|
|
||||||
if read > 0 {
|
if read > 0 {
|
||||||
recorder.input(&input[offset..]);
|
recorder.input(&input[offset..]);
|
||||||
@@ -188,7 +185,7 @@ fn copy<R: Recorder>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if event.is_read_closed() {
|
if event.is_read_closed() {
|
||||||
poll.registry().deregister(&mut tty_source).unwrap();
|
poll.registry().deregister(&mut tty_source)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +194,7 @@ fn copy<R: Recorder>(
|
|||||||
for signal in signals.pending() {
|
for signal in signals.pending() {
|
||||||
match signal {
|
match signal {
|
||||||
SIGWINCH => {
|
SIGWINCH => {
|
||||||
let winsize = get_tty_size(tty_fd, winsize_override);
|
let winsize = get_tty_size(&*tty, winsize_override);
|
||||||
set_pty_size(master_fd, &winsize);
|
set_pty_size(master_fd, &winsize);
|
||||||
recorder.resize((winsize.ws_col, winsize.ws_row));
|
recorder.resize((winsize.ws_col, winsize.ws_row));
|
||||||
}
|
}
|
||||||
@@ -237,22 +234,11 @@ fn handle_child<S: AsRef<str>>(args: &[S], extra_env: &ExtraEnv) -> anyhow::Resu
|
|||||||
unsafe { libc::_exit(1) }
|
unsafe { libc::_exit(1) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_tty() -> io::Result<fs::File> {
|
fn get_tty_size<T: Tty + ?Sized>(
|
||||||
fs::OpenOptions::new()
|
tty: &T,
|
||||||
.read(true)
|
winsize_override: (Option<u16>, Option<u16>),
|
||||||
.write(true)
|
) -> pty::Winsize {
|
||||||
.open("/dev/tty")
|
let mut winsize = tty.get_size();
|
||||||
}
|
|
||||||
|
|
||||||
fn get_tty_size(tty_fd: i32, winsize_override: (Option<u16>, Option<u16>)) -> pty::Winsize {
|
|
||||||
let mut winsize = pty::Winsize {
|
|
||||||
ws_row: 24,
|
|
||||||
ws_col: 80,
|
|
||||||
ws_xpixel: 0,
|
|
||||||
ws_ypixel: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
unsafe { libc::ioctl(tty_fd, libc::TIOCGWINSZ, &mut winsize) };
|
|
||||||
|
|
||||||
if let Some(cols) = winsize_override.0 {
|
if let Some(cols) = winsize_override.0 {
|
||||||
winsize.ws_col = cols;
|
winsize.ws_col = cols;
|
||||||
@@ -327,6 +313,7 @@ fn write_all<W: Write>(sink: &mut W, data: &mut Vec<u8>) -> io::Result<usize> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::pty::ExtraEnv;
|
use crate::pty::ExtraEnv;
|
||||||
|
use crate::tty::DevNull;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct TestRecorder {
|
struct TestRecorder {
|
||||||
@@ -373,12 +360,13 @@ sys.stdout.write('bar');
|
|||||||
let result = super::exec(
|
let result = super::exec(
|
||||||
&["python3", "-c", code],
|
&["python3", "-c", code],
|
||||||
&ExtraEnv::new(),
|
&ExtraEnv::new(),
|
||||||
|
Box::new(DevNull::open().unwrap()),
|
||||||
(None, None),
|
(None, None),
|
||||||
&mut recorder,
|
&mut recorder,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(recorder.output(), vec!["foo", "bar"]);
|
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(recorder.output(), vec!["foo", "bar"]);
|
||||||
assert!(recorder.size.is_some());
|
assert!(recorder.size.is_some());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/tty.rs
Normal file
113
src/tty.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use mio::unix::pipe;
|
||||||
|
use nix::{libc, pty};
|
||||||
|
use std::{
|
||||||
|
fs, io,
|
||||||
|
os::fd::{AsRawFd, RawFd},
|
||||||
|
};
|
||||||
|
use termion::raw::{IntoRawMode, RawTerminal};
|
||||||
|
|
||||||
|
pub trait Tty: io::Write + io::Read + AsRawFd {
|
||||||
|
fn get_size(&self) -> pty::Winsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DevTty {
|
||||||
|
file: RawTerminal<fs::File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DevTty {
|
||||||
|
pub fn open() -> Result<Self> {
|
||||||
|
let file = fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open("/dev/tty")?
|
||||||
|
.into_raw_mode()?;
|
||||||
|
|
||||||
|
crate::io::set_non_blocking(&file.as_raw_fd())?;
|
||||||
|
|
||||||
|
Ok(Self { file })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tty for DevTty {
|
||||||
|
fn get_size(&self) -> pty::Winsize {
|
||||||
|
let mut winsize = pty::Winsize {
|
||||||
|
ws_row: 24,
|
||||||
|
ws_col: 80,
|
||||||
|
ws_xpixel: 0,
|
||||||
|
ws_ypixel: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe { libc::ioctl(self.file.as_raw_fd(), libc::TIOCGWINSZ, &mut winsize) };
|
||||||
|
|
||||||
|
winsize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Read for DevTty {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.file.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for DevTty {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.file.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.file.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRawFd for DevTty {
|
||||||
|
fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
|
||||||
|
self.file.as_raw_fd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DevNull {
|
||||||
|
tx: pipe::Sender,
|
||||||
|
_rx: pipe::Receiver,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DevNull {
|
||||||
|
pub fn open() -> Result<Self> {
|
||||||
|
let (tx, rx) = pipe::new()?;
|
||||||
|
|
||||||
|
Ok(Self { tx, _rx: rx })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tty for DevNull {
|
||||||
|
fn get_size(&self) -> pty::Winsize {
|
||||||
|
pty::Winsize {
|
||||||
|
ws_row: 24,
|
||||||
|
ws_col: 80,
|
||||||
|
ws_xpixel: 0,
|
||||||
|
ws_ypixel: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Read for DevNull {
|
||||||
|
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
panic!("read attempt from DevNull impl of Tty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for DevNull {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRawFd for DevNull {
|
||||||
|
fn as_raw_fd(&self) -> RawFd {
|
||||||
|
self.tx.as_raw_fd()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user