1pub mod ffmpeg;
7pub mod process;
8
9use std::path::PathBuf;
10use std::time::Duration;
11
12pub use ffmpeg::{FfmpegArgs, run_ffmpeg_with_tempfile};
13pub use process::{ProcessOutput, execute_command};
14#[cfg(feature = "live-recording")]
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16
17use crate::error::Result;
18
19#[derive(Debug, Clone, PartialEq)]
45pub struct Executor {
46 executable_path: PathBuf,
48 timeout: Duration,
50 args: Vec<String>,
52}
53
54impl std::fmt::Display for Executor {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 write!(
57 f,
58 "Executor(path={}, args={}, timeout={}s)",
59 self.executable_path.display(),
60 self.args.len(),
61 self.timeout.as_secs()
62 )
63 }
64}
65
66impl Executor {
67 pub fn new<I, S>(executable_path: impl Into<PathBuf>, args: I, timeout: Duration) -> Self
79 where
80 I: IntoIterator<Item = S>,
81 S: Into<String>,
82 {
83 let executable_path = executable_path.into();
84 let args: Vec<String> = args.into_iter().map(Into::into).collect();
85
86 tracing::debug!(
87 executable = ?executable_path,
88 arg_count = args.len(),
89 timeout_secs = timeout.as_secs(),
90 "🔧 Creating new Executor"
91 );
92
93 Self {
94 executable_path,
95 args,
96 timeout,
97 }
98 }
99
100 pub fn executable_path(&self) -> &PathBuf {
106 &self.executable_path
107 }
108
109 pub fn args(&self) -> &[String] {
115 &self.args
116 }
117
118 pub fn timeout(&self) -> Duration {
124 self.timeout
125 }
126
127 pub async fn execute(&self) -> Result<ProcessOutput> {
137 tracing::debug!(
138 executable = ?self.executable_path,
139 arg_count = self.args.len(),
140 timeout_secs = self.timeout.as_secs(),
141 "⚙️ Executing command"
142 );
143
144 let result = execute_command(&self.executable_path, &self.args, self.timeout).await;
145
146 match &result {
147 Ok(output) => tracing::debug!(
148 executable = ?self.executable_path,
149 exit_code = output.code,
150 stdout_len = output.stdout.len(),
151 stderr_len = output.stderr.len(),
152 "✅ Command execution completed"
153 ),
154 Err(e) => tracing::warn!(
155 executable = ?self.executable_path,
156 error = %e,
157 "⚙️ Command execution failed"
158 ),
159 }
160
161 result
162 }
163
164 pub async fn execute_to_file(&self, output_path: impl Into<PathBuf>) -> Result<ProcessOutput> {
179 let output_path = output_path.into();
180
181 tracing::debug!(
182 executable = ?self.executable_path,
183 arg_count = self.args.len(),
184 output_path = ?output_path,
185 timeout_secs = self.timeout.as_secs(),
186 "⚙️ Executing command to file"
187 );
188
189 let result =
190 process::execute_command_to_file(&self.executable_path, &self.args, self.timeout, &output_path).await;
191
192 match &result {
193 Ok(output) => tracing::debug!(
194 executable = ?self.executable_path,
195 output_path = ?output_path,
196 exit_code = output.code,
197 stderr_len = output.stderr.len(),
198 "✅ Command execution to file completed"
199 ),
200 Err(e) => tracing::warn!(
201 executable = ?self.executable_path,
202 output_path = ?output_path,
203 error = %e,
204 "⚙️ Command execution to file failed"
205 ),
206 }
207
208 result
209 }
210
211 #[cfg(feature = "live-recording")]
220 pub async fn execute_streaming(&self) -> Result<StreamingProcess> {
222 tracing::debug!(
223 executable = ?self.executable_path,
224 arg_count = self.args.len(),
225 "📥 Spawning long-running streaming process"
226 );
227
228 let mut command = tokio::process::Command::new(&self.executable_path);
229 command.stdin(std::process::Stdio::piped());
230 command.stdout(std::process::Stdio::piped());
231 command.stderr(std::process::Stdio::piped());
232
233 #[cfg(target_os = "windows")]
234 {
235 use std::os::windows::process::CommandExt;
236 command.creation_flags(0x08000000);
237 }
238
239 command.args(&self.args);
240
241 let child = command.spawn()?;
242
243 tracing::debug!(
244 executable = ?self.executable_path,
245 pid = ?child.id(),
246 "✅ Streaming process spawned"
247 );
248
249 Ok(StreamingProcess { child })
250 }
251 }
253
254#[cfg(feature = "live-recording")]
259pub struct StreamingProcess {
260 child: tokio::process::Child,
261}
262
263#[cfg(feature = "live-recording")]
264impl StreamingProcess {
265 pub async fn stop(&mut self) -> Result<ProcessOutput> {
271 tracing::info!("📥 Stopping streaming process gracefully (stdin q)");
272
273 if let Some(stdin) = self.child.stdin.as_mut() {
274 let _ = stdin.write_all(b"q").await;
276 let _ = stdin.flush().await;
277 }
278
279 self.wait().await
280 }
281
282 pub async fn kill(&mut self) -> Result<()> {
288 tracing::warn!("Killing streaming process");
289 self.child.kill().await?;
290 Ok(())
291 }
292
293 pub async fn wait(&mut self) -> Result<ProcessOutput> {
299 let mut stderr_buf = String::new();
301 if let Some(stderr) = self.child.stderr.take() {
302 let mut reader = tokio::io::BufReader::new(stderr);
303 let _ = reader.read_to_string(&mut stderr_buf).await;
304 }
305
306 let status = self.child.wait().await?;
307 let code = status.code().unwrap_or(-1);
308
309 tracing::debug!(
310 exit_code = code,
311 stderr_len = stderr_buf.len(),
312 "📥 Streaming process exited"
313 );
314
315 Ok(ProcessOutput {
316 stdout: String::new(),
317 stderr: stderr_buf,
318 code,
319 })
320 }
321}
322#[cfg(feature = "live-recording")]
323impl Drop for StreamingProcess {
324 fn drop(&mut self) {
325 let _ = self.child.start_kill();
327 }
328}