use std::io::SeekFrom;
use std::process::Stdio;
use std::time::Duration;
use anyhow::{Context, Result, anyhow};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use tokio::sync::mpsc;
use crate::protocol::{Frame, FrameReader, FrameType};
async fn write_frame(
frame: &Frame,
log: &mut tokio::fs::File,
stdout: &mut tokio::io::Stdout,
stdout_ok: &mut bool,
) -> Result<bool> {
let encoded = frame.encode();
if log.write_all(&encoded).await.is_err() || log.flush().await.is_err() {
return Ok(false);
}
if *stdout_ok && stdout.write_all(&encoded).await.is_err() {
*stdout_ok = false;
}
Ok(true)
}
pub async fn worker(command: &str) -> Result<()> {
unsafe {
libc::signal(libc::SIGHUP, libc::SIG_IGN);
}
let pid = std::process::id();
let home = dirs::home_dir().context("cannot determine home directory")?;
let log_dir = home.join(".rexec").join("logs");
std::fs::create_dir_all(&log_dir)
.with_context(|| format!("creating {}", log_dir.display()))?;
let log_path = log_dir.join(format!("{}.log", pid));
let mut log_file = tokio::fs::File::create(&log_path)
.await
.with_context(|| format!("creating {}", log_path.display()))?;
let mut stdout = tokio::io::stdout();
let mut stdout_ok = true;
let mut child = tokio::process::Command::new("sh")
.arg("-c")
.arg(command)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("failed to spawn child process")?;
let mut child_stdout = child.stdout.take().unwrap();
let mut child_stderr = child.stderr.take().unwrap();
write_frame(&Frame::started(pid), &mut log_file, &mut stdout, &mut stdout_ok).await?;
let (frame_tx, mut frame_rx) = mpsc::channel::<Frame>(256);
let tx_out = frame_tx.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 8192];
loop {
match child_stdout.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => {
if tx_out.send(Frame::stdout(buf[..n].to_vec())).await.is_err() {
break;
}
}
}
}
});
let tx_err = frame_tx.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 8192];
loop {
match child_stderr.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => {
if tx_err.send(Frame::stderr(buf[..n].to_vec())).await.is_err() {
break;
}
}
}
}
});
drop(frame_tx);
while let Some(frame) = frame_rx.recv().await {
let _ = write_frame(&frame, &mut log_file, &mut stdout, &mut stdout_ok).await;
}
let status = child.wait().await?;
let exit_code = status.code().unwrap_or(-1);
let _ = write_frame(
&Frame::exited(exit_code),
&mut log_file,
&mut stdout,
&mut stdout_ok,
)
.await;
let _ = log_file.flush().await;
let _ = stdout.flush().await;
if exit_code == 0 {
let _ = tokio::fs::remove_file(&log_path).await;
}
Ok(())
}
pub async fn attach(pid: u32, offset: u64) -> Result<()> {
let home = dirs::home_dir().context("cannot determine home directory")?;
let log_path = home
.join(".rexec")
.join("logs")
.join(format!("{}.log", pid));
if !log_path.exists() {
let alive = unsafe { libc::kill(pid as i32, 0) == 0 };
if !alive {
let mut stdout = tokio::io::stdout();
let frame = Frame::exited(0);
stdout.write_all(&frame.encode()).await?;
stdout.flush().await?;
return Ok(());
}
return Err(anyhow!("log file not found for PID {} and process is alive", pid));
}
let mut stdout = tokio::io::stdout();
let mut offset = offset;
let mut reader = FrameReader::new();
let mut file = tokio::fs::File::open(&log_path).await?;
file.seek(SeekFrom::Start(offset)).await?;
loop {
let file_size = match file.metadata().await {
Ok(m) => m.len(),
Err(_) => {
let frame = Frame::exited(-1);
stdout.write_all(&frame.encode()).await?;
stdout.flush().await?;
return Ok(());
}
};
if file_size > offset {
file.seek(SeekFrom::Start(offset)).await?;
let to_read = (file_size - offset) as usize;
let read_size = to_read.min(65536);
let mut buf = vec![0u8; read_size];
let n = match file.read(&mut buf).await {
Ok(0) => 0,
Ok(n) => n,
Err(_) => 0, };
if n > 0 {
offset += n as u64;
stdout.write_all(&buf[..n]).await?;
stdout.flush().await?;
reader.push(&buf[..n]);
while let Some(frame) = reader.next_frame() {
if frame.frame_type == FrameType::Exited {
return Ok(()); }
}
}
}
let alive = unsafe { libc::kill(pid as i32, 0) == 0 };
if !alive && file_size <= offset {
tokio::time::sleep(Duration::from_millis(200)).await;
let new_size = file.metadata().await.map(|m| m.len()).unwrap_or(0);
if new_size <= offset {
let frame = Frame::exited(-1);
stdout.write_all(&frame.encode()).await?;
stdout.flush().await?;
return Ok(());
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}