use anyhow::{Context, Result, bail};
use clap::Args;
use std::io::{Read, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use crate::cmd::{Ctx, deployment_name};
use crate::output;
use crate::{ExecRequest, ShellEvent, ShellExit, ShellOptions};
#[derive(Args, Debug)]
pub struct ExecArgs {
#[arg(value_name = "RESOURCE")]
pub resource: String,
#[arg(value_name = "COMMAND", trailing_var_arg = true, required = true)]
pub command: Vec<String>,
#[arg(long, value_name = "DIR")]
pub cwd: Option<String>,
#[arg(long = "env", short = 'e', value_name = "KEY=VALUE")]
pub env: Vec<String>,
#[arg(long, value_name = "SECS", default_value = "60")]
pub timeout: u64,
#[arg(long)]
pub no_wake: bool,
}
pub fn exec(ctx: &Ctx, args: &ExecArgs) -> Result<()> {
let id = deployment_name(&args.resource)?;
let mut req = ExecRequest::new(args.command.join(" ")).timeout_secs(args.timeout);
if let Some(cwd) = &args.cwd {
req = req.cwd(cwd);
}
if args.no_wake {
req = req.no_wake();
}
for pair in &args.env {
let (k, v) = pair
.split_once('=')
.with_context(|| format!("--env expects KEY=VALUE, got {pair:?}"))?;
req = req.env(k, v);
}
let result = ctx.client.exec(&id, &req)?;
if ctx.out.is_machine() {
let rendered = serde_json::json!({
"sandbox_id": result.sandbox_id,
"exit_code": result.exit_code,
"stdout": result.stdout,
"stderr": result.stderr,
"output": result.output,
});
return output::emit(&rendered, ctx.out, &[]);
}
print!("{}", result.stdout);
eprint!("{}", result.stderr);
let _ = std::io::stdout().flush();
if !result.ok() {
std::process::exit(result.exit_code);
}
Ok(())
}
#[derive(Args, Debug)]
pub struct ShellArgs {
#[arg(value_name = "RESOURCE")]
pub resource: String,
#[arg(long, value_name = "DIR")]
pub cwd: Option<String>,
#[arg(long)]
pub no_wake: bool,
#[arg(long, value_name = "SANDBOX_ID")]
pub vm: Option<String>,
}
const TICK: Duration = Duration::from_millis(20);
pub fn shell(ctx: &Ctx, args: &ShellArgs) -> Result<()> {
let id = deployment_name(&args.resource)?;
let (cols, rows) = terminal_size().unwrap_or((80, 24));
let mut opts = ShellOptions::default().size(cols, rows);
if let Some(cwd) = &args.cwd {
opts = opts.cwd(cwd);
}
if args.no_wake {
opts = opts.no_wake();
}
if let Some(vm) = &args.vm {
opts = opts.vm(vm);
}
let mut session = ctx.client.shell(&id, &opts)?;
eprintln!("connected to {} ({id})", session.sandbox_id());
let _raw = RawMode::enter()?;
watch_winch();
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
std::thread::spawn(move || {
let mut stdin = std::io::stdin();
let mut buf = [0u8; 4096];
while let Ok(n) = stdin.read(&mut buf) {
if n == 0 || tx.send(buf[..n].to_vec()).is_err() {
break;
}
}
});
let mut last_size = (cols, rows);
let mut stdout = std::io::stdout();
loop {
match session.next_timeout(TICK) {
Some(ShellEvent::Output(bytes)) => {
let _ = stdout.write_all(&bytes);
let _ = stdout.flush();
}
Some(ShellEvent::Error(msg)) => {
let _ = write!(stdout, "\r\nshell error: {msg}\r\n");
let _ = stdout.flush();
}
None if session.exit().is_some() => break,
None => {}
}
if RESIZED.swap(false, Ordering::Relaxed)
&& let Some(size) = terminal_size()
&& size != last_size
{
last_size = size;
let _ = session.resize(size.0, size.1);
}
for chunk in rx.try_iter() {
if session.write(&chunk).is_err() {
break;
}
}
}
let exit = session
.exit()
.cloned()
.unwrap_or(ShellExit { code: 0, error: None });
let _ = session.close();
drop(_raw);
if let Some(err) = &exit.error {
eprintln!("shell ended abnormally: {err}");
if exit.code == 0 {
std::process::exit(1);
}
}
if exit.code != 0 {
std::process::exit(exit.code);
}
Ok(())
}
struct RawMode {
original: Option<libc::termios>,
}
impl RawMode {
fn enter() -> Result<Self> {
if unsafe { libc::isatty(libc::STDIN_FILENO) } != 1 {
return Ok(Self { original: None });
}
let mut term: libc::termios = unsafe { std::mem::zeroed() };
if unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut term) } != 0 {
bail!("could not read the terminal settings");
}
let original = term;
unsafe { libc::cfmakeraw(&mut term) };
if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &term) } != 0 {
bail!("could not put the terminal in raw mode");
}
Ok(Self {
original: Some(original),
})
}
}
impl Drop for RawMode {
fn drop(&mut self) {
if let Some(original) = self.original {
unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &original) };
}
}
}
static RESIZED: AtomicBool = AtomicBool::new(false);
extern "C" fn on_winch(_: libc::c_int) {
RESIZED.store(true, Ordering::Relaxed);
}
fn watch_winch() {
let handler: extern "C" fn(libc::c_int) = on_winch;
unsafe { libc::signal(libc::SIGWINCH, handler as usize as libc::sighandler_t) };
RESIZED.store(true, Ordering::Relaxed);
}
fn terminal_size() -> Option<(u16, u16)> {
let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
(rc == 0 && ws.ws_col > 0).then_some((ws.ws_col, ws.ws_row.max(1)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_resize_flag_latches_and_is_consumed_once() {
RESIZED.store(false, Ordering::Relaxed);
on_winch(libc::SIGWINCH);
assert!(RESIZED.swap(false, Ordering::Relaxed), "the signal sets it");
assert!(
!RESIZED.swap(false, Ordering::Relaxed),
"a burst of signals during a drag is one resize, not many"
);
}
#[test]
fn raw_mode_is_a_no_op_when_stdin_is_not_a_terminal() {
if unsafe { libc::isatty(libc::STDIN_FILENO) } != 1 {
let guard = RawMode::enter().unwrap();
assert!(guard.original.is_none(), "nothing to restore, nothing stored");
}
}
}