1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 09:04:12 +02:00
haze/src/exec.rs
2026-02-27 22:52:44 +01:00

244 lines
6.3 KiB
Rust

use bollard::exec::{CreateExecOptions, ResizeExecOptions, StartExecResults};
use bollard::query_parameters::LogsOptions;
use bollard::Docker;
use futures_util::StreamExt;
use miette::{IntoDiagnostic, Report, Result, WrapErr};
use std::io::{stdout, Read, Stdin, Write};
use std::time::Duration;
use termion::raw::IntoRawMode;
use termion::{async_stdin, is_tty, terminal_size};
use tokio::io::AsyncWriteExt;
use tokio::task::spawn;
use tokio::time::sleep;
pub async fn exec_tty<S1: AsRef<str>, S2: Into<String>, Env: Into<String>>(
docker: &Docker,
container: S1,
user: &str,
cmd: Vec<S2>,
env: Vec<Env>,
) -> Result<ExitCode> {
let stdout = stdout();
if !is_tty(&stdout) {
return exec(docker, container, user, cmd, env, Some(stdout)).await;
}
let tty_size = terminal_size().into_diagnostic()?;
let cmd = cmd.into_iter().map(S2::into).collect();
let env = env.into_iter().map(Env::into).collect();
let config = CreateExecOptions {
cmd: Some(cmd),
user: Some(user.to_string()),
attach_stdout: Some(true),
attach_stderr: Some(true),
attach_stdin: Some(true),
tty: Some(true),
env: Some(env),
..Default::default()
};
let message = docker
.create_exec(container.as_ref(), config)
.await
.into_diagnostic()
.wrap_err("Failed to setup exec")?;
if let StartExecResults::Attached {
mut output,
mut input,
} = docker
.start_exec(&message.id, None)
.await
.into_diagnostic()
.wrap_err("Failed to start exec")?
{
docker
.resize_exec(
&message.id,
ResizeExecOptions {
height: tty_size.1,
width: tty_size.0,
},
)
.await
.ok();
// pipe stdin into the docker exec stream input
spawn(async move {
#[allow(clippy::unbuffered_bytes)]
let mut stdin = async_stdin().bytes();
loop {
if let Some(Ok(byte)) = stdin.next() {
input.write_all(&[byte]).await.ok();
} else {
sleep(Duration::from_nanos(10)).await;
}
}
});
// set stdout in raw mode so we can do tty stuff
let mut stdout = stdout.lock().into_raw_mode().into_diagnostic()?;
// pipe docker exec output into stdout
while let Some(Ok(output)) = output.next().await {
stdout
.write(output.into_bytes().as_ref())
.into_diagnostic()?;
stdout.flush().into_diagnostic()?;
}
} else {
unreachable!();
}
Ok(docker
.inspect_exec(&message.id)
.await
.into_diagnostic()?
.exit_code
.unwrap_or_default()
.into())
}
pub async fn exec<S1: AsRef<str>, S2: Into<String>, Env: Into<String>>(
docker: &Docker,
container: S1,
user: &str,
cmd: Vec<S2>,
env: Vec<Env>,
std_out: Option<impl Write>,
) -> Result<ExitCode> {
exec_io(
docker,
container,
user,
cmd,
env,
std_out,
Option::<Stdin>::None,
)
.await
}
pub async fn exec_io<S1: AsRef<str>, S2: Into<String>, Env: Into<String>>(
docker: &Docker,
container: S1,
user: &str,
cmd: Vec<S2>,
env: Vec<Env>,
mut std_out: Option<impl Write>,
std_in: Option<impl Read>,
) -> Result<ExitCode> {
let cmd = cmd.into_iter().map(S2::into).collect();
let env = env.into_iter().map(Env::into).collect();
let config = CreateExecOptions {
cmd: Some(cmd),
user: Some(user.to_string()),
attach_stdin: Some(std_in.is_some()),
attach_stdout: Some(true),
attach_stderr: Some(true),
env: Some(env),
tty: Some(false),
..Default::default()
};
let message = docker
.create_exec(container.as_ref(), config)
.await
.into_diagnostic()
.wrap_err("Failed to setup exec")?;
if let StartExecResults::Attached {
mut output,
mut input,
} = docker
.start_exec(&message.id, None)
.await
.into_diagnostic()
.wrap_err("Failed to start exec")?
{
if let Some(mut std_in) = std_in {
let mut buff = [0; 4 * 1024];
loop {
let bytes = std_in.read(&mut buff).into_diagnostic()?;
if bytes == 0 {
break;
}
input.write_all(&buff[0..bytes]).await.into_diagnostic()?;
}
input.shutdown().await.into_diagnostic()?;
}
while let Some(Ok(line)) = output.next().await {
if let Some(std_out) = &mut std_out {
write!(std_out, "{}", line).into_diagnostic()?;
}
}
} else {
unreachable!();
}
Ok(docker
.inspect_exec(&message.id)
.await
.into_diagnostic()?
.exit_code
.unwrap_or_default()
.into())
}
pub async fn container_logs(
docker: &Docker,
mut std_out: impl Write,
container: &str,
count: usize,
follow: bool,
) -> Result<()> {
let mut stream = docker.logs(
container,
Some(LogsOptions {
stdout: true,
stderr: true,
follow,
tail: format!("{}", count),
..Default::default()
}),
);
while let Some(line) = stream.next().await {
let line = line.into_diagnostic()?.to_string();
write!(&mut std_out, "{}", line).into_diagnostic()?;
}
Ok(())
}
pub struct ExitCode(i64);
impl ExitCode {
pub fn is_ok(&self) -> bool {
self.0 == 0
}
pub fn to_result(&self) -> Result<()> {
match self.0 {
0 => Ok(()),
code => Err(Report::msg(format!(
"Command failed with exit code {}",
code
))),
}
}
}
impl PartialEq<i64> for ExitCode {
fn eq(&self, other: &i64) -> bool {
&self.0 == other
}
}
impl From<i64> for ExitCode {
fn from(code: i64) -> Self {
ExitCode(code)
}
}
impl From<ExitCode> for std::process::ExitCode {
fn from(value: ExitCode) -> Self {
let code = u8::try_from(value.0).unwrap_or(1);
code.into()
}
}