pub mod ffmpeg;
pub mod process;
use std::path::PathBuf;
use std::time::Duration;
pub use ffmpeg::{FfmpegArgs, run_ffmpeg_with_tempfile};
pub use process::{ProcessOutput, execute_command};
#[cfg(feature = "live-recording")]
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::error::Result;
#[derive(Debug, Clone, PartialEq)]
pub struct Executor {
executable_path: PathBuf,
timeout: Duration,
args: Vec<String>,
}
impl std::fmt::Display for Executor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Executor(path={}, args={}, timeout={}s)",
self.executable_path.display(),
self.args.len(),
self.timeout.as_secs()
)
}
}
impl Executor {
pub fn new<I, S>(executable_path: impl Into<PathBuf>, args: I, timeout: Duration) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let executable_path = executable_path.into();
let args: Vec<String> = args.into_iter().map(Into::into).collect();
tracing::debug!(
executable = ?executable_path,
arg_count = args.len(),
timeout_secs = timeout.as_secs(),
"🔧 Creating new Executor"
);
Self {
executable_path,
args,
timeout,
}
}
pub fn executable_path(&self) -> &PathBuf {
&self.executable_path
}
pub fn args(&self) -> &[String] {
&self.args
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub async fn execute(&self) -> Result<ProcessOutput> {
tracing::debug!(
executable = ?self.executable_path,
arg_count = self.args.len(),
timeout_secs = self.timeout.as_secs(),
"⚙️ Executing command"
);
let result = execute_command(&self.executable_path, &self.args, self.timeout).await;
match &result {
Ok(output) => tracing::debug!(
executable = ?self.executable_path,
exit_code = output.code,
stdout_len = output.stdout.len(),
stderr_len = output.stderr.len(),
"✅ Command execution completed"
),
Err(e) => tracing::warn!(
executable = ?self.executable_path,
error = %e,
"⚙️ Command execution failed"
),
}
result
}
pub async fn execute_to_file(&self, output_path: impl Into<PathBuf>) -> Result<ProcessOutput> {
let output_path = output_path.into();
tracing::debug!(
executable = ?self.executable_path,
arg_count = self.args.len(),
output_path = ?output_path,
timeout_secs = self.timeout.as_secs(),
"⚙️ Executing command to file"
);
let result =
process::execute_command_to_file(&self.executable_path, &self.args, self.timeout, &output_path).await;
match &result {
Ok(output) => tracing::debug!(
executable = ?self.executable_path,
output_path = ?output_path,
exit_code = output.code,
stderr_len = output.stderr.len(),
"✅ Command execution to file completed"
),
Err(e) => tracing::warn!(
executable = ?self.executable_path,
output_path = ?output_path,
error = %e,
"⚙️ Command execution to file failed"
),
}
result
}
#[cfg(feature = "live-recording")]
pub async fn execute_streaming(&self) -> Result<StreamingProcess> {
tracing::debug!(
executable = ?self.executable_path,
arg_count = self.args.len(),
"📥 Spawning long-running streaming process"
);
let mut command = tokio::process::Command::new(&self.executable_path);
command.stdin(std::process::Stdio::piped());
command.stdout(std::process::Stdio::piped());
command.stderr(std::process::Stdio::piped());
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
command.creation_flags(0x08000000);
}
command.args(&self.args);
let child = command.spawn()?;
tracing::debug!(
executable = ?self.executable_path,
pid = ?child.id(),
"✅ Streaming process spawned"
);
Ok(StreamingProcess { child })
}
}
#[cfg(feature = "live-recording")]
pub struct StreamingProcess {
child: tokio::process::Child,
}
#[cfg(feature = "live-recording")]
impl StreamingProcess {
pub async fn stop(&mut self) -> Result<ProcessOutput> {
tracing::info!("📥 Stopping streaming process gracefully (stdin q)");
if let Some(stdin) = self.child.stdin.as_mut() {
let _ = stdin.write_all(b"q").await;
let _ = stdin.flush().await;
}
self.wait().await
}
pub async fn kill(&mut self) -> Result<()> {
tracing::warn!("Killing streaming process");
self.child.kill().await?;
Ok(())
}
pub async fn wait(&mut self) -> Result<ProcessOutput> {
let mut stderr_buf = String::new();
if let Some(stderr) = self.child.stderr.take() {
let mut reader = tokio::io::BufReader::new(stderr);
let _ = reader.read_to_string(&mut stderr_buf).await;
}
let status = self.child.wait().await?;
let code = status.code().unwrap_or(-1);
tracing::debug!(
exit_code = code,
stderr_len = stderr_buf.len(),
"📥 Streaming process exited"
);
Ok(ProcessOutput {
stdout: String::new(),
stderr: stderr_buf,
code,
})
}
}
#[cfg(feature = "live-recording")]
impl Drop for StreamingProcess {
fn drop(&mut self) {
let _ = self.child.start_kill();
}
}