Skip to main content

yt_dlp/executor/
process.rs

1//! Process execution and output handling.
2
3#[cfg(target_os = "windows")]
4use std::os::windows::process::CommandExt;
5use std::path::PathBuf;
6use std::time::Duration;
7
8use crate::error::{Error, Result};
9
10/// Represents the output of a process.
11#[derive(Debug, Clone, PartialEq)]
12pub struct ProcessOutput {
13    /// The stdout of the process.
14    pub stdout: String,
15    /// The stderr of the process.
16    pub stderr: String,
17    /// The exit code of the process.
18    pub code: i32,
19}
20
21impl std::fmt::Display for ProcessOutput {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(
24            f,
25            "ProcessOutput(code={}, stdout_len={}, stderr_len={})",
26            self.code,
27            self.stdout.len(),
28            self.stderr.len()
29        )
30    }
31}
32
33/// Executes a command with the given arguments and timeout.
34///
35/// # Arguments
36///
37/// * `executable_path` - Path to the executable
38/// * `args` - Arguments to pass to the command
39/// * `timeout` - Maximum duration to wait for the process
40///
41/// # Errors
42///
43/// Returns an error if the command fails, times out, or cannot be executed
44pub async fn execute_command(
45    executable_path: impl Into<PathBuf>,
46    args: &[String],
47    timeout: Duration,
48) -> Result<ProcessOutput> {
49    execute_command_internal(executable_path, args, timeout, None).await
50}
51
52/// Executes a command and redirects stdout to a file.
53///
54/// # Arguments
55///
56/// * `executable_path` - Path to the executable
57/// * `args` - Arguments to pass to the command
58/// * `timeout` - Maximum duration to wait for the process
59/// * `output_path` - Path to the file where stdout will be written
60///
61/// # Errors
62///
63/// Returns an error if the command fails, times out, or cannot be executed
64pub async fn execute_command_to_file(
65    executable_path: impl Into<PathBuf>,
66    args: &[String],
67    timeout: Duration,
68    output_path: impl Into<PathBuf>,
69) -> Result<ProcessOutput> {
70    execute_command_internal(executable_path, args, timeout, Some(output_path.into())).await
71}
72
73/// Internal command execution with optional file output
74///
75/// # Arguments
76///
77/// * `executable_path` - Path to the executable
78/// * `args` - Arguments to pass to the command
79/// * `timeout` - Maximum duration to wait for the process
80/// * `output_path` - Optional path to redirect stdout to a file
81///
82/// # Returns
83///
84/// ProcessOutput containing stdout (if not redirected), stderr, and exit code
85///
86/// # Errors
87///
88/// Returns an error if the command fails, times out, or cannot be executed
89// LCOV_EXCL_START — requires real yt-dlp/ffmpeg binary on PATH
90async fn execute_command_internal(
91    executable_path: impl Into<PathBuf>,
92    args: &[String],
93    timeout: Duration,
94    output_path: Option<PathBuf>,
95) -> Result<ProcessOutput> {
96    let executable_path: PathBuf = executable_path.into();
97
98    tracing::debug!(
99        executable = ?executable_path,
100        arg_count = args.len(),
101        timeout_secs = timeout.as_secs(),
102        output_to_file = output_path.is_some(),
103        output_path = ?output_path,
104        "⚙️ Starting command execution"
105    );
106
107    let mut command = tokio::process::Command::new(&executable_path);
108
109    // Configure stdout: either pipe (memory) or file
110    if let Some(path) = &output_path {
111        let file = std::fs::File::create(path)?;
112        command.stdout(std::process::Stdio::from(file));
113    } else {
114        command.stdout(std::process::Stdio::piped());
115    }
116
117    command.stderr(std::process::Stdio::piped());
118
119    #[cfg(target_os = "windows")]
120    command.creation_flags(0x08000000);
121
122    command.args(args);
123
124    tracing::debug!(
125        executable = ?executable_path,
126        "⚙️ Spawning child process"
127    );
128
129    let mut child = command.spawn()?;
130
131    tracing::debug!(
132        executable = ?executable_path,
133        pid = ?child.id(),
134        "✅ Child process spawned"
135    );
136
137    // Read streams asynchronously
138    let stdout_task = if output_path.is_none() {
139        let stdout = child
140            .stdout
141            .take()
142            .ok_or_else(|| Error::io("capture stdout", std::io::Error::other("stdout stream not available")))?;
143
144        Some(tokio::spawn(read_stream(stdout)))
145    } else {
146        None
147    };
148
149    let stderr = child
150        .stderr
151        .take()
152        .ok_or_else(|| Error::io("capture stderr", std::io::Error::other("stderr stream not available")))?;
153
154    let stderr_task = tokio::spawn(read_stream(stderr));
155
156    tracing::debug!(
157        executable = ?executable_path,
158        timeout_secs = timeout.as_secs(),
159        "⚙️ Waiting for process to complete"
160    );
161
162    // Wait for the process to finish with timeout
163    let exit_status = match tokio::time::timeout(timeout, child.wait()).await {
164        Ok(result) => result?,
165        Err(_) => {
166            tracing::warn!(
167                executable = ?executable_path,
168                timeout_secs = timeout.as_secs(),
169                "⚙️ Process timed out, killing it"
170            );
171
172            if let Err(e) = child.kill().await {
173                tracing::error!(
174                    executable = ?executable_path,
175                    error = %e,
176                    "⚙️ Failed to kill process after timeout"
177                );
178            } else if let Err(e) = child.wait().await {
179                tracing::error!(
180                    executable = ?executable_path,
181                    error = %e,
182                    "⚙️ Failed to wait for process after kill"
183                );
184            }
185
186            return Err(Error::Timeout {
187                operation: format!("executing command: {}", executable_path.display()),
188                duration: timeout,
189            });
190        }
191    };
192
193    tracing::debug!(
194        executable = ?executable_path,
195        exit_code = exit_status.code().unwrap_or(-1),
196        success = exit_status.success(),
197        "⚙️ Process completed"
198    );
199
200    // Read stderr stream
201    let stderr_result = match stderr_task.await {
202        Ok(Ok(buffer)) => buffer,
203        Ok(Err(e)) => return Err(Error::io("reading command stderr", e)),
204        Err(e) => return Err(Error::runtime("reading command stderr task", e)),
205    };
206
207    let stdout_result = if let Some(task) = stdout_task {
208        match task.await {
209            Ok(Ok(buffer)) => buffer,
210            Ok(Err(e)) => return Err(Error::io("reading command stdout", e)),
211            Err(e) => return Err(Error::runtime("reading command stdout task", e)),
212        }
213    } else {
214        Vec::new()
215    };
216
217    // Convert the buffers to Strings (lossy to avoid errors on non-UTF8 output)
218    let stdout = String::from_utf8_lossy(&stdout_result).to_string();
219    let stderr = String::from_utf8_lossy(&stderr_result).to_string();
220    let code = exit_status.code().unwrap_or(-1);
221
222    tracing::debug!(
223        executable = ?executable_path,
224        exit_code = code,
225        stdout_len = stdout.len(),
226        stderr_len = stderr.len(),
227        "⚙️ Command output captured"
228    );
229
230    if exit_status.success() {
231        tracing::debug!(
232            executable = ?executable_path,
233            exit_code = code,
234            "✅ Command execution succeeded"
235        );
236
237        return Ok(ProcessOutput { stdout, stderr, code });
238    }
239
240    tracing::warn!(
241        executable = ?executable_path,
242        exit_code = code,
243        stderr_preview = if stderr.len() > 100 {
244            &stderr[..100]
245        } else {
246            &stderr
247        },
248        "⚙️ Command execution failed"
249    );
250
251    Err(Error::CommandFailed {
252        command: executable_path.display().to_string(),
253        exit_code: code,
254        stderr,
255    })
256}
257// LCOV_EXCL_STOP
258
259/// Helper function to read a stream into a buffer
260///
261/// # Arguments
262///
263/// * `stream` - An async readable stream (stdout or stderr)
264///
265/// # Returns
266///
267/// A vector of bytes containing all data read from the stream
268///
269/// # Errors
270///
271/// Returns an IO error if reading fails
272async fn read_stream<R>(mut stream: R) -> std::io::Result<Vec<u8>>
273where
274    R: tokio::io::AsyncRead + Unpin + Send + 'static,
275{
276    let mut buffer = Vec::new();
277    let bytes_read = tokio::io::copy(&mut tokio::io::BufReader::new(&mut stream), &mut buffer).await?;
278
279    tracing::trace!(bytes_read, "Stream read completed");
280    Ok(buffer)
281}