Skip to main content

yt_dlp/executor/
mod.rs

1//! Command execution module.
2//!
3//! This module provides tools for executing commands with timeout support,
4//! and long-running streaming processes controllable via cancellation tokens.
5
6pub 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/// Represents a command executor.
20///
21/// # Example
22///
23/// ```rust,no_run
24/// # use yt_dlp::utils;
25/// # use std::path::PathBuf;
26/// # use std::time::Duration;
27/// # use yt_dlp::executor::Executor;
28/// # #[tokio::main]
29/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
30/// let args = vec!["--update"];
31///
32/// let executor = Executor::new(
33///     PathBuf::from("yt-dlp"),
34///     utils::to_owned(args),
35///     Duration::from_secs(30),
36/// );
37///
38/// let output = executor.execute().await?;
39/// println!("Output: {}", output.stdout);
40///
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Debug, Clone, PartialEq)]
45pub struct Executor {
46    /// The path to the command executable.
47    executable_path: PathBuf,
48    /// The timeout for the process.
49    timeout: Duration,
50    /// The arguments to pass to the command.
51    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    /// Creates a new Executor.
68    ///
69    /// # Arguments
70    ///
71    /// * `executable_path` - Path to the executable
72    /// * `args` - Arguments to pass to the command
73    /// * `timeout` - Timeout for the command
74    ///
75    /// # Returns
76    ///
77    /// A new Executor instance
78    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    /// Returns the executable path.
101    ///
102    /// # Returns
103    ///
104    /// Reference to the executable path
105    pub fn executable_path(&self) -> &PathBuf {
106        &self.executable_path
107    }
108
109    /// Returns the arguments.
110    ///
111    /// # Returns
112    ///
113    /// Slice of command arguments
114    pub fn args(&self) -> &[String] {
115        &self.args
116    }
117
118    /// Returns the timeout.
119    ///
120    /// # Returns
121    ///
122    /// Timeout duration for command execution
123    pub fn timeout(&self) -> Duration {
124        self.timeout
125    }
126
127    /// Executes the command and returns the output.
128    ///
129    /// # Returns
130    ///
131    /// ProcessOutput containing stdout, stderr, and exit code
132    ///
133    /// # Errors
134    ///
135    /// This function will return an error if the command could not be executed, or if the process timed out.
136    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    /// Executes the command and redirects stdout to a file.
165    ///
166    /// # Arguments
167    ///
168    /// * `output_path` - The path where stdout will be written
169    ///
170    /// # Returns
171    ///
172    /// ProcessOutput containing stderr and exit code (stdout is written to file)
173    ///
174    /// # Errors
175    ///
176    /// This function will return an error if the command could not be executed, if the process timed out,
177    /// or if the output file could not be created.
178    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    /// Spawns the command as a long-running process without timeout.
212    ///
213    /// Returns a [`StreamingProcess`] handle that can be stopped gracefully
214    /// via stdin `q` (for FFmpeg) or killed. Intended for live recording.
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the process could not be spawned.
219    #[cfg(feature = "live-recording")]
220    // LCOV_EXCL_START — requires real ffmpeg binary on PATH
221    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    // LCOV_EXCL_STOP
252}
253
254/// A long-running child process controllable via stdin or kill.
255///
256/// Used for FFmpeg-based live recording where the process runs indefinitely
257/// until explicitly stopped.
258#[cfg(feature = "live-recording")]
259pub struct StreamingProcess {
260    child: tokio::process::Child,
261}
262
263#[cfg(feature = "live-recording")]
264impl StreamingProcess {
265    /// Sends `q` to stdin to trigger a graceful FFmpeg quit, then waits for exit.
266    ///
267    /// # Errors
268    ///
269    /// Returns an error if writing to stdin or waiting fails.
270    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            // Ignore write errors (process may have already exited)
275            let _ = stdin.write_all(b"q").await;
276            let _ = stdin.flush().await;
277        }
278
279        self.wait().await
280    }
281
282    /// Forcefully kills the process.
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if the kill signal cannot be sent.
287    pub async fn kill(&mut self) -> Result<()> {
288        tracing::warn!("Killing streaming process");
289        self.child.kill().await?;
290        Ok(())
291    }
292
293    /// Waits for the process to exit and collects output.
294    ///
295    /// # Errors
296    ///
297    /// Returns an error if waiting for the process fails.
298    pub async fn wait(&mut self) -> Result<ProcessOutput> {
299        // Read stderr before waiting (stdout may be large for recordings)
300        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        // Prevent orphaned FFmpeg processes: send SIGKILL on drop
326        let _ = self.child.start_kill();
327    }
328}