yt_dlp/executor/
process.rs1#[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#[derive(Debug, Clone, PartialEq)]
12pub struct ProcessOutput {
13 pub stdout: String,
15 pub stderr: String,
17 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
33pub 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
52pub 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
73async 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 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 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 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 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 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}
257async 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}