pistonite_cu/process/builder.rs
1use std::ffi::OsStr;
2use std::path::{Path, PathBuf};
3use std::process::ExitStatus;
4
5use tokio::process::{Child as TokioChild, Command as TokioCommand};
6
7use super::{Child, ChildIo, Config, Preset};
8
9use crate::{Context as _, co, pio, str::PathExtension as _};
10
11/// A [`Command`] to be built
12pub type CommandBuilder = Command<(), (), ()>;
13
14/// Builder for spawning a child process.
15///
16/// You can use the `command()` function on `Path` or `PathBuf`
17/// to create a `CommandBuilder`.
18///
19/// ```rust,no_run
20/// # use pistonite_cu as cu;
21/// use std::path::Path;
22///
23/// use cu::pre::*;
24///
25/// # fn main() -> cu::Result<()> {
26/// let path = Path::new("ls");
27/// path.command().all_null().wait_nz()?;
28/// # Ok(()) }
29/// ```
30///
31/// # Configuration
32/// Configuring a `cu::Command` is much like configuring `Command`
33/// from `std` or `tokio`. Notable features are:
34/// - Use [`cu::args`](crate::args) and [`cu::envs`](crate::envs) macro
35/// to enable adding multiple args or envs of different type in the same call.
36/// Without the macro, `args`/`envs` only accept iterators that yield the same types.
37/// - [`current_dir()`](Self::current_dir) will be normalized based on current process, before the child is spawned.
38/// `std` recommends this, but not enforced. Here it's always done if set
39/// - [`name()`](Self::name) sets an identifier for the process to be passed to IO handlers.
40/// See below for more info about IO.
41///
42/// # Input-Output (IO)
43/// IO is the most important part of dealing with child processes. After all, you would
44/// want some information out of the process that you spawned.
45///
46/// Unlike `Command` in the standard library or `tokio`, `CommandBuilder` does not have
47/// "default" IO behavior. You cannot spawn the child until you configured how you'd like
48/// `stdout`, `stderr`, and `stdin` to behave.
49///
50/// See [`pio`] (Process IO) for configuration IO.
51///
52/// # Spawning
53/// See [`spawn`](Self::spawn) and [`co_spawn`](Self::co_spawn).
54///
55pub struct Command<Out, Err, In> {
56 /// Inner command
57 command: TokioCommand,
58 /// buffered current_dir to be set on the command before spawning
59 current_dir: Option<PathBuf>,
60 /// Name which maybe used by IO to print
61 name: Option<String>,
62 stdout: Out,
63 stderr: Err,
64 stdin: In,
65}
66
67impl CommandBuilder {
68 /// Start building a command. If the arg is a `Path` or `PathBuf`,
69 /// you can also call `.command()` on it (remember to import prelude)
70 ///
71 /// ```rust
72 /// # use pistonite_cu as cu;
73 /// use cu::pre::*;
74 ///
75 /// # fn main() -> cu::Result<()> {
76 /// let command = cu::CommandBuilder::new("git");
77 /// // or:
78 /// let command = std::path::Path::new("git").command();
79 /// // even better, find the executable in PATH using the crate's
80 /// // binary registry
81 /// let command = cu::which("git")?.command();
82 /// # Ok(()) }
83 /// ```
84 pub fn new(bin: impl AsRef<OsStr>) -> Self {
85 Self {
86 command: TokioCommand::new(bin),
87 name: None,
88 current_dir: None,
89 stdout: (),
90 stderr: (),
91 stdin: (),
92 }
93 }
94}
95
96// expose functions from tokio/std
97#[rustfmt::skip]
98impl<Out, Err, In> Command<Out, Err, In> {
99 /// Add one argument
100 ///
101 /// Use [`Self::args`] or [`args`](crate::args) macro to add more than one arguments
102 /// in one call.
103 #[inline(always)]
104 pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Self { self.command.arg(arg); self }
105
106 /// Add multiple arguments
107 ///
108 /// This only accepts iterators of a single type, which forces
109 /// you to call `.as_ref()` if the input has multiple types.
110 /// To workaround this, use the [`args`](crate::args) shorthand.
111 #[inline(always)]
112 pub fn args<I: IntoIterator<Item=S>, S:AsRef<OsStr>>(mut self, args: I) -> Self { self.command.args(args); self }
113
114
115 /// Clear the environment variables, which will prevent
116 /// inheriting environment variables from the parent (this process)
117 #[inline(always)]
118 pub fn env_clear(mut self) -> Self { self.command.env_clear(); self }
119
120 /// Remove an environment variable
121 #[inline(always)]
122 pub fn env_remove(mut self, env: impl AsRef<OsStr>) -> Self { self.command.env_remove(env); self }
123
124 /// Set a single environment variable
125 #[inline(always)]
126 pub fn env(mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) -> Self { self.command.env(k, v); self }
127
128 /// Set multiple environment variables
129 ///
130 /// This only accepts iterators of a single type, which forces
131 /// you to call `.as_ref()` if the input has multiple types.
132 /// To workaround this, use the [`envs`](crate::envs) macro shorthand.
133 #[inline(always)]
134 pub fn envs<I: IntoIterator<Item=(K,V)>,K:AsRef<OsStr>,V:AsRef<OsStr>>(mut self, envs: I) -> Self { self.command.envs(envs); self }
135
136 /// Add more configuration. See [`args!`](crate::args) and [`envs!`](crate::envs).
137 #[inline(always)]
138 #[allow(clippy::should_implement_trait)]
139 pub fn add(mut self, config: impl Config) -> Self {
140 config.configure(&mut self.command);
141 self
142 }
143
144 /// Set the current working directory for the child process.
145 ///
146 /// Unlike `std`/`tokio` implementation, where canonicalizing the current
147 /// dir is recommended, we always canonicalize the input here based on this
148 /// process before spawning the child.
149 #[inline(always)]
150 pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
151 self.current_dir = Some(dir.into());
152 self
153 }
154
155 /// Set the name of this command, which maybe used by output config
156 /// to print in the terminal
157 #[inline(always)]
158 pub fn name(mut self, name: impl Into<String>) -> Self {
159 self.name = Some(name.into());
160 self
161 }
162
163 /// Configure child's standard input stream
164 #[inline(always)]
165 pub fn stdin<T: pio::ChildInConfig>(self, config:T) -> Command<Out, Err, T> {
166 Command {
167 command: self.command,
168 current_dir: self.current_dir,
169 name: self.name,
170 stdout: self.stdout,
171 stderr: self.stderr,
172 stdin: config,
173 }
174 }
175
176 /// Configure child's standard input stream as [`pio::null()`]
177 #[inline(always)]
178 pub fn stdin_null(self) -> Command<Out, Err, pio::Null> {
179 self.stdin(pio::null())
180 }
181
182 /// Configure child's standard input stream to inherit from parent ([`pio::inherit()`])
183 #[inline(always)]
184 pub fn stdin_inherit(self) -> Command<Out, Err, pio::Inherit> {
185 self.stdin(pio::inherit())
186 }
187
188 /// Configure child's standard output stream
189 #[inline(always)]
190 pub fn stdout<T: pio::ChildOutConfig>(self, config: T) -> Command<T, Err, In> {
191 Command {
192 command: self.command,
193 current_dir: self.current_dir,
194 name: self.name,
195 stdout: config,
196 stderr: self.stderr,
197 stdin: self.stdin
198 }
199 }
200
201 /// Configure child's standard output stream to inherit from parent ([`pio::inherit()`])
202 #[inline(always)]
203 pub fn stdout_inherit(self) -> Command<pio::Inherit, Err, In> {
204 self.stdout(pio::inherit())
205 }
206
207 /// Configure child's standard error stream
208 #[inline(always)]
209 pub fn stderr<T: pio::ChildOutConfig>(self, config: T) -> Command<Out, T, In> {
210 Command {
211 command: self.command,
212 current_dir: self.current_dir,
213 name: self.name,
214 stdout: self.stdout,
215 stderr: config,
216 stdin: self.stdin
217 }
218 }
219
220 /// Configure child's standard output stream to inherit from parent ([`pio::inherit()`])
221 #[inline(always)]
222 pub fn stderr_inherit(self) -> Command<Out, pio::Inherit, In> {
223 self.stderr(pio::inherit())
224 }
225
226 /// Configure child's both standard output and standard error with the same config
227 #[inline(always)]
228 pub fn stdoe<T: pio::ChildOutConfig + Clone>(self, config: T) -> Command<T, T, In> {
229 Command {
230 command: self.command,
231 current_dir: self.current_dir,
232 name: self.name,
233 stdout: config.clone(),
234 stderr: config,
235 stdin: self.stdin
236 }
237 }
238
239 /// Configure child's standard output stream as [`pio::null()`]
240 #[inline(always)]
241 pub fn stdout_null(self) -> Command<pio::Null, Err, In> {
242 self.stdout(pio::null())
243 }
244
245 /// Configure child's standard error stream as [`pio::null()`]
246 #[inline(always)]
247 pub fn stderr_null(self) -> Command<Out, pio::Null, In> {
248 self.stderr(pio::null())
249 }
250
251 /// Configure both child's standard output and error streams as [`pio::null()`]
252 #[inline(always)]
253 pub fn stdoe_null(self) -> Command<pio::Null, pio::Null, In> {
254 self.stdoe(pio::null())
255 }
256
257 /// Configure both child's standard input and error streams as [`pio::null()`]
258 #[inline(always)]
259 pub fn stdie_null(self) -> Command<Out, pio::Null, pio::Null> {
260 self.stdin_null().stderr_null()
261 }
262
263 /// Configure both child's standard input and output streams as [`pio::null()`]
264 #[inline(always)]
265 pub fn stdio_null(self) -> Command<pio::Null, Err, pio::Null> {
266 self.stdin_null().stdout_null()
267 }
268
269 /// Configure all standard input, output and error streams as [`pio::null()`]
270 #[inline(always)]
271 pub fn all_null(self) -> Command<pio::Null, pio::Null, pio::Null> {
272 self.stdin_null().stdout_null().stderr_null()
273 }
274
275 /// Configure all standard input, output and error streams as [`pio::inherit()`]
276 #[inline(always)]
277 pub fn all_inherit(self) -> Command<pio::Inherit, pio::Inherit, pio::Inherit> {
278 self.stdin_inherit().stdout_inherit().stderr_inherit()
279 }
280
281 /// Apply a preset for the child command's input and output.
282 ///
283 /// See [`pio`] for available IO presets.
284 #[inline(always)]
285 pub fn preset<P: Preset>(self, preset: P) -> P::Output {
286 preset.configure(self)
287 }
288
289 /// Spawn the child and run the child's IO tasks in the background. **Note that
290 /// you have to configure all 3 of `stdin`, `stdout` and `stderr`, before you can spawn the
291 /// child. Otherwise, you will get a compile error that `spawn` is not defined**
292 ///
293 /// # Blocking
294 /// Use [`co_spawn`](Self::co_spawn) if you are within an async context.
295 ///
296 /// # Return Value
297 /// The return type shown above is a placeholder. The actual return
298 /// value depends on the configuration for `stdout` and `stderr`,
299 /// specifically, whether they have a handle to access the output.
300 /// For example [`pio::inherit()`] or [`pio::spinner()`] don't
301 /// have handles, because the output is directly printed,
302 /// whereas [`pio::pipe()`] or [`pio::string()`] has handles for reading the output.
303 ///
304 /// - If both `stdout` and `stderr` don't have handle, then the output is [`Child`]
305 /// ```rust
306 /// # use pistonite_cu as cu;
307 /// use cu::pre::*; // The prelude import is required for `spawn`
308 ///
309 /// # fn main() -> cu::Result<()> {
310 /// let child = cu::which("echo")?.command()
311 /// .arg("Hello, world!")
312 /// .stdout(cu::lv::I) // print stdout as info level log messages
313 /// .stdie_null()
314 /// .spawn()?;
315 /// # Ok(()) }
316 /// ```
317 /// - If only `stdout` has a handle but not `stderr`, then the output is a 2-tuple ([`Child`],
318 /// `Stdout), where `Stdout` is the respective handle type for the configured output.
319 /// ```rust
320 /// # use pistonite_cu as cu;
321 /// use cu::pre::*; // The prelude import is required for `spawn`
322 ///
323 /// # fn main() -> cu::Result<()> {
324 /// let (child, lines) = cu::which("bash")?.command()
325 /// .args(["-c", r#"for i in {1..5}; do echo "Line $i"; sleep 1; done"# ])
326 /// .stdout(cu::pio::lines())
327 /// .stdie_null()
328 /// .spawn()?;
329 /// # Ok(()) }
330 /// ```
331 /// - Otherwise, the output is a 3-tuple ([`Child`], `Stdout`, `Stderr`),
332 /// where `Stdout` and `Stderr` are the respective handle type for the output configured,
333 /// and `Stdout` might be `()` if it does not have a handle
334 /// ```rust
335 /// # use pistonite_cu as cu;
336 /// use cu::pre::*; // The prelude import is required for `spawn`
337 ///
338 /// # fn main() -> cu::Result<()> {
339 /// let (child, lines, err) = cu::which("bash")?.command()
340 /// .args(["-c", r#"for i in {1..5}; do echo "Line $i"; sleep 1; done"# ])
341 /// .stdout(cu::pio::lines())
342 /// .stderr(cu::pio::string())
343 /// .stdin_null()
344 /// .spawn()?;
345 /// # Ok(()) }
346 /// ```
347 #[cfg(doc)]
348 pub fn spawn(self) -> crate::Result<EitherOf<Child, (Child, Out, Err)>> {
349 panic!("this is a placeholder for documetnation, see below for implementation")
350 }
351
352
353 /// Spawn the child and run the child's IO tasks in the background. **Note that
354 /// you have to configure all 3 of `stdin`, `stdout` and `stderr`, before you can spawn the
355 /// child. Otherwise, you will get a compile error that `co_spawn` is not defined**
356 ///
357 /// Note that the child's IO are still drived by the background context.
358 /// This is to prevent errors where blocking the current-thread runtime unexpectedly
359 /// block the child's IO, even when the child's IO tasks are running on the background
360 /// thread.
361 ///
362 /// # Return value
363 /// See [`spawn`](Self::spawn)
364 #[cfg(doc)]
365 pub async fn co_spawn(self) -> crate::Result<EitherOf<Child, (Child, Out, Err)>> {
366 panic!("this is a placeholder for documetnation, see below for implementation")
367 }
368}
369#[cfg(doc)]
370struct EitherOf<A, B>(A, B);
371
372/// **Implementation only applies if the child has no output handles (see [`spawn`](Command::spawn) for more
373/// details)**
374impl<
375 Out: pio::ChildOutConfig<__Null = pio::__OCNull>,
376 Err: pio::ChildOutConfig<__Null = pio::__OCNull>,
377 In: pio::ChildInConfig,
378> Command<Out, Err, In>
379{
380 /// Spawn and wait for child to exit with non-zero status code.
381 ///
382 /// This is equivalent to calling [`spawn()`](Self::spawn), then
383 /// [`wait_nz()`](Child::wait_nz) on the [`Child`].
384 #[inline(always)]
385 pub fn wait_nz(self) -> crate::Result<()> {
386 self.spawn()?.wait_nz()
387 }
388 /// Spawn and wait for child to exit, returning its [`ExitStatus`] code
389 ///
390 /// This is equivalent to calling [`spawn()`](Self::spawn), then
391 /// [`wait()`](Child::wait) on the [`Child`].
392 #[inline(always)]
393 pub fn wait(self) -> crate::Result<ExitStatus> {
394 self.spawn()?.wait()
395 }
396
397 /// Spawn and wait for child to exit with non-zero status code, using
398 /// the current tokio runtime context.
399 ///
400 /// This is equivalent to calling [`co_spawn()`](Self::co_spawn), then
401 /// [`co_wait_nz()`](Child::co_wait_nz) on the [`Child`].
402 #[inline(always)]
403 pub async fn co_wait_nz(self) -> crate::Result<()> {
404 self.co_spawn().await?.co_wait_nz().await
405 }
406 /// Spawn and wait for child to exit, returning its [`ExitStatus`] code,
407 /// using the current tokio runtime context.
408 ///
409 /// This is equivalent to calling [`co_spawn()`](Self::co_spawn), then
410 /// [`co_wait()`](Child::co_wait) on the [`Child`].
411 #[inline(always)]
412 pub async fn co_wait(self) -> crate::Result<ExitStatus> {
413 self.co_spawn().await?.co_wait().await
414 }
415}
416
417/// This trait allows implementing different return types for [`Command::spawn`] based on the configured IO.
418pub trait Spawn<Target>
419where
420 Target: Send + 'static,
421{
422 fn spawn(self) -> crate::Result<Target>;
423 fn co_spawn(self) -> crate::BoxedFuture<crate::Result<Target>>;
424}
425
426#[cfg(not(doc))]
427macro_rules! Spawned {
428 () => {
429 $crate::Child
430 };
431 (Out) => {
432 (
433 $crate::Child,
434 <Out::Task as $crate::process::pio::ChildOutTask>::Output,
435 )
436 };
437 ($A:ident, $B:ident) => {
438 (
439 $crate::Child,
440 <$A::Task as $crate::process::pio::ChildOutTask>::Output,
441 <$B::Task as $crate::process::pio::ChildOutTask>::Output,
442 )
443 };
444}
445
446#[rustfmt::skip]
447#[cfg(not(doc))]
448impl< Out: pio::ChildOutConfig<__Null=pio::__OCNull>, Err: pio::ChildOutConfig<__Null=pio::__OCNull>, In: pio::ChildInConfig> Spawn<Spawned![]> for Command<Out, Err, In> {
449 #[inline(always)]
450 fn spawn(self) -> crate::Result<Spawned![]> {
451 spawn_internal(self).map(|x| x.0)
452 }
453 #[inline(always)]
454 fn co_spawn(self) -> crate::BoxedFuture<crate::Result<Spawned![]>> {
455 Box::pin(async move {
456 co_spawn_internal(self).await.map(|x| x.0)
457 })
458 }
459}
460
461#[rustfmt::skip]
462#[cfg(not(doc))]
463impl< Out: pio::ChildOutConfig<__Null=pio::__OCNonNull>, Err: pio::ChildOutConfig<__Null=pio::__OCNull>, In: pio::ChildInConfig> Spawn<Spawned![Out]> for Command<Out, Err, In> {
464 #[inline(always)]
465 fn spawn(self) -> crate::Result<Spawned![Out]> {
466 spawn_internal(self).map(|(c,o,_)| (c,o))
467 }
468 #[inline(always)]
469 fn co_spawn(self) -> crate::BoxedFuture<crate::Result<Spawned![Out]>> {
470 Box::pin(async move {
471 co_spawn_internal(self).await.map(|(c,o,_)| (c,o))
472 })
473 }
474}
475
476#[rustfmt::skip]
477#[cfg(not(doc))]
478impl< Out: pio::ChildOutConfig, Err: pio::ChildOutConfig<__Null=pio::__OCNonNull>, In: pio::ChildInConfig> Spawn<Spawned![Out, Err]> for Command<Out, Err, In> {
479 #[inline(always)]
480 fn spawn(self) -> crate::Result<Spawned![Out, Err]> {
481 spawn_internal(self)
482 }
483 #[inline(always)]
484 fn co_spawn(self) -> crate::BoxedFuture<crate::Result<Spawned![Out, Err]>> {
485 Box::pin(co_spawn_internal(self))
486 }
487}
488
489/// handle the actual spawning
490#[allow(clippy::type_complexity)]
491fn spawn_internal<Out: pio::ChildOutConfig, Err: pio::ChildOutConfig, In: pio::ChildInConfig>(
492 mut self_: Command<Out, Err, In>,
493) -> crate::Result<(
494 Child,
495 <Out::Task as pio::ChildOutTask>::Output,
496 <Err::Task as pio::ChildOutTask>::Output,
497)> {
498 pre_spawn(&mut self_)?;
499
500 // self.command.spawn() must be called on the background runtime,
501 // because the IO will be attached to the active runtime context
502 // if we call .spawn() on the current-thread runtime, then blocking
503 // the current-thread runtime will also block the child's IO
504 co::spawn(async move {
505 let child = self_.command.spawn().context("failed to spawn command")?;
506 post_spawn(self_, child)
507 })
508 .join()?
509}
510/// handle the actual spawning
511#[allow(clippy::type_complexity)]
512async fn co_spawn_internal<
513 Out: pio::ChildOutConfig,
514 Err: pio::ChildOutConfig,
515 In: pio::ChildInConfig,
516>(
517 mut self_: Command<Out, Err, In>,
518) -> crate::Result<(
519 Child,
520 <Out::Task as pio::ChildOutTask>::Output,
521 <Err::Task as pio::ChildOutTask>::Output,
522)> {
523 pre_spawn(&mut self_)?;
524
525 // self.command.spawn() must be called on the background runtime,
526 // because the IO will be attached to the active runtime context
527 // if we call .spawn() on the current-thread runtime, then blocking
528 // the current-thread runtime will also block the child's IO
529 co::spawn(async move {
530 let child = self_.command.spawn().context("failed to spawn command")?;
531 post_spawn(self_, child)
532 })
533 .co_join()
534 .await?
535}
536
537fn pre_spawn<Out: pio::ChildOutConfig, Err: pio::ChildOutConfig, In: pio::ChildInConfig>(
538 self_: &mut Command<Out, Err, In>,
539) -> crate::Result<()> {
540 use std::fmt::Write as _;
541 let mut trace = String::new();
542
543 // build the trace message
544 let log_enabled = crate::lv::T.enabled();
545 if log_enabled {
546 let command = self_.command.as_std();
547 let _ = write!(
548 &mut trace,
549 "spawning '{}', args: [",
550 command.get_program().display()
551 );
552 let mut args = command.get_args();
553 if let Some(a) = args.next() {
554 let arg = a.display().to_string().replace('\'', "\\'");
555 let _ = write!(&mut trace, "'{arg}'");
556 }
557 for arg in args {
558 let arg = arg.display().to_string().replace('\'', "\\'");
559 let _ = write!(&mut trace, ", '{arg}'");
560 }
561 let _ = write!(&mut trace, "]");
562 }
563
564 // configure final things on the Command
565 if let Some(cd) = &self_.current_dir {
566 let cd = cd.normalize_exists().with_context(|| {
567 if log_enabled {
568 crate::trace!("error while {trace}");
569 }
570 crate::error!("cannot canonicalize current_dir: {}", cd.display());
571 "cannot canonicalize current_dir while spawning child"
572 })?;
573 if log_enabled {
574 let _ = write!(&mut trace, ", current_dir: '{}'", cd.display());
575 }
576 self_.command.current_dir(cd);
577 }
578
579 if log_enabled {
580 match &self_.name {
581 Some(name) => crate::trace!("[{name}] {trace}"),
582 _ => crate::trace!("{trace}"),
583 }
584 }
585 // configure IO
586 self_.stdout.configure_stdout(&mut self_.command);
587 self_.stderr.configure_stderr(&mut self_.command);
588 self_
589 .stdin
590 .configure_stdin(&mut self_.command)
591 .context("failed to configure child stdin")?;
592 Ok(())
593}
594
595#[allow(clippy::type_complexity)]
596fn post_spawn<Out: pio::ChildOutConfig, Err: pio::ChildOutConfig, In: pio::ChildInConfig>(
597 self_: Command<Out, Err, In>,
598 mut child: TokioChild,
599) -> crate::Result<(
600 Child,
601 <Out::Task as pio::ChildOutTask>::Output,
602 <Err::Task as pio::ChildOutTask>::Output,
603)> {
604 let name = self_.name;
605
606 let stdout = self_
607 .stdout
608 .take(&mut child, name.as_deref(), true)
609 .context("failed to take child stdout")?;
610 let stderr = self_
611 .stderr
612 .take(&mut child, name.as_deref(), false)
613 .context("failed to take child stderr")?;
614 let stdin = self_
615 .stdin
616 .take(&mut child)
617 .context("failed to take child stdin")?;
618
619 let name = match name {
620 Some(x) => format!("[{x}]"),
621 None => {
622 let command = self_.command.as_std();
623 let program = Path::new(command.get_program())
624 .file_name()
625 .map(|x| x.display().to_string());
626 match program {
627 Some(x) => format!("program '{x}'"),
628 None => "child process".to_string(),
629 }
630 }
631 };
632
633 // get the IO tasks
634 use pio::ChildInTask as _;
635 use pio::ChildOutTask as _;
636 let (stdout_future, stdout) = stdout.run();
637 let (stderr_future, stderr) = stderr.run();
638 let stdin_future = stdin.run();
639
640 let io = ChildIo::start(stdin_future, stdout_future, stderr_future);
641
642 Ok((
643 Child {
644 name,
645 inner: child,
646 io,
647 },
648 stdout,
649 stderr,
650 ))
651}