1use std::ffi::{OsStr, OsString};
8use std::fs::{File, OpenOptions};
9use std::io;
10#[cfg(unix)]
11use std::io::Write;
12use std::path::{Path, PathBuf};
13
14pub const STDOUT_FILE_ARG: &str = "--stdout-file";
16
17pub const STDERR_FILE_ARG: &str = "--stderr-file";
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct StreamRedirectConfig {
23 pub stdout_file: Option<PathBuf>,
25 pub stderr_file: Option<PathBuf>,
27}
28
29impl StreamRedirectConfig {
30 pub fn new(
32 stdout_file: Option<impl Into<PathBuf>>,
33 stderr_file: Option<impl Into<PathBuf>>,
34 ) -> io::Result<Option<Self>> {
35 let stdout_file = stdout_file.map(Into::into);
36 let stderr_file = stderr_file.map(Into::into);
37 validate_optional_file(STDOUT_FILE_ARG, stdout_file.as_deref())?;
38 validate_optional_file(STDERR_FILE_ARG, stderr_file.as_deref())?;
39 if stdout_file.is_none() && stderr_file.is_none() {
40 return Ok(None);
41 }
42 Ok(Some(Self {
43 stdout_file,
44 stderr_file,
45 }))
46 }
47}
48
49#[cfg_attr(not(unix), derive(Clone, Debug, PartialEq, Eq))]
54pub struct InstalledStreamRedirect {
55 pub stdout_file: Option<PathBuf>,
57 pub stderr_file: Option<PathBuf>,
59 #[cfg(unix)]
60 stdout_restore: Option<std::os::fd::OwnedFd>,
61 #[cfg(unix)]
62 stderr_restore: Option<std::os::fd::OwnedFd>,
63}
64
65#[cfg(unix)]
66impl std::fmt::Debug for InstalledStreamRedirect {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.debug_struct("InstalledStreamRedirect")
69 .field("stdout_file", &self.stdout_file)
70 .field("stderr_file", &self.stderr_file)
71 .finish_non_exhaustive()
72 }
73}
74
75#[cfg(unix)]
76impl Drop for InstalledStreamRedirect {
77 fn drop(&mut self) {
78 let _ = io::stdout().flush();
79
80 if let Some(stdout_restore) = &self.stdout_restore {
81 let _ = unix::redirect_fd(libc::STDOUT_FILENO, stdout_restore.as_raw_fd());
82 }
83 if let Some(stderr_restore) = &self.stderr_restore {
84 let _ = unix::redirect_fd(libc::STDERR_FILENO, stderr_restore.as_raw_fd());
85 }
86
87 self.stdout_restore.take();
88 self.stderr_restore.take();
89 unix::mark_uninstalled();
90 }
91}
92
93#[cfg(unix)]
94use std::os::fd::AsRawFd;
95
96pub fn config_from_raw_args<I, S>(args: I) -> io::Result<Option<StreamRedirectConfig>>
104where
105 I: IntoIterator<Item = S>,
106 S: Into<OsString>,
107{
108 let raw = parse_raw_args(args)?;
109 StreamRedirectConfig::new(raw.stdout_file, raw.stderr_file)
110}
111
112pub fn install_from_raw_args<I, S>(args: I) -> io::Result<Option<InstalledStreamRedirect>>
114where
115 I: IntoIterator<Item = S>,
116 S: Into<OsString>,
117{
118 match config_from_raw_args(args)? {
119 Some(config) => install(&config).map(Some),
120 None => Ok(None),
121 }
122}
123
124#[cfg(unix)]
126pub fn install(config: &StreamRedirectConfig) -> io::Result<InstalledStreamRedirect> {
127 unix::install(config)
128}
129
130#[cfg(not(unix))]
132pub fn install(config: &StreamRedirectConfig) -> io::Result<InstalledStreamRedirect> {
133 let _ = config;
134 Err(io::Error::new(
135 io::ErrorKind::Unsupported,
136 "stream redirection is only supported on Unix platforms",
137 ))
138}
139
140fn invalid_input(message: &str) -> io::Error {
141 io::Error::new(io::ErrorKind::InvalidInput, message)
142}
143
144fn validate_optional_file(arg_name: &str, path: Option<&Path>) -> io::Result<()> {
145 let Some(path) = path else {
146 return Ok(());
147 };
148 if path.as_os_str() == OsStr::new("") {
149 return Err(invalid_input(&format!("{arg_name} must not be empty")));
150 }
151 Ok(())
152}
153
154#[cfg_attr(not(unix), allow(dead_code))]
157fn open_append(path: &Path) -> io::Result<File> {
158 #[cfg(unix)]
159 {
160 use std::os::unix::fs::OpenOptionsExt;
161 match std::fs::symlink_metadata(path) {
162 Ok(metadata) => {
163 let file_type = metadata.file_type();
164 if file_type.is_symlink() {
165 return Err(io::Error::new(
166 io::ErrorKind::InvalidInput,
167 "stream redirection target must not be a symbolic link",
168 ));
169 }
170 if !file_type.is_file() {
171 return Err(io::Error::new(
172 io::ErrorKind::InvalidInput,
173 "stream redirection target must be a regular file",
174 ));
175 }
176 OpenOptions::new()
177 .append(true)
178 .custom_flags(libc::O_NOFOLLOW)
179 .open(path)
180 }
181 Err(err) if err.kind() == io::ErrorKind::NotFound => OpenOptions::new()
182 .append(true)
183 .create_new(true)
184 .mode(0o600)
185 .custom_flags(libc::O_NOFOLLOW)
186 .open(path),
187 Err(err) => Err(err),
188 }
189 }
190 #[cfg(not(unix))]
191 {
192 OpenOptions::new().create(true).append(true).open(path)
193 }
194}
195
196#[derive(Debug, Default)]
197struct RawStreamRedirectArgs {
198 stdout_file: Option<PathBuf>,
199 stderr_file: Option<PathBuf>,
200}
201
202fn parse_raw_args<I, S>(args: I) -> io::Result<RawStreamRedirectArgs>
203where
204 I: IntoIterator<Item = S>,
205 S: Into<OsString>,
206{
207 let mut parsed = RawStreamRedirectArgs::default();
208 let mut iter = args.into_iter().map(Into::into).peekable();
209
210 while let Some(arg) = iter.next() {
211 if arg == OsStr::new("--") {
212 break;
213 }
214 let Some(arg_str) = arg.to_str() else {
215 continue;
216 };
217
218 if arg_str == STDOUT_FILE_ARG {
219 parsed.stdout_file = Some(PathBuf::from(take_raw_value(&mut iter, STDOUT_FILE_ARG)?));
220 } else if let Some(value) = arg_str.strip_prefix("--stdout-file=") {
221 parsed.stdout_file = Some(PathBuf::from(value));
222 } else if arg_str == STDERR_FILE_ARG {
223 parsed.stderr_file = Some(PathBuf::from(take_raw_value(&mut iter, STDERR_FILE_ARG)?));
224 } else if let Some(value) = arg_str.strip_prefix("--stderr-file=") {
225 parsed.stderr_file = Some(PathBuf::from(value));
226 }
227 }
228
229 Ok(parsed)
230}
231
232fn take_raw_value<I>(iter: &mut std::iter::Peekable<I>, arg_name: &str) -> io::Result<OsString>
233where
234 I: Iterator<Item = OsString>,
235{
236 let Some(value) = iter.next() else {
237 return Err(invalid_input(&format!("{arg_name} requires a value")));
238 };
239 if value
240 .to_str()
241 .is_some_and(|value| value.starts_with("--") && value != "--")
242 {
243 return Err(invalid_input(&format!("{arg_name} requires a value")));
244 }
245 Ok(value)
246}
247
248#[cfg(unix)]
249mod unix {
250 use super::{InstalledStreamRedirect, PathBuf, StreamRedirectConfig, Write, io, open_append};
251 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
252 use std::sync::atomic::{AtomicBool, Ordering};
253
254 const STDOUT_FD: RawFd = libc::STDOUT_FILENO;
255 const STDERR_FD: RawFd = libc::STDERR_FILENO;
256
257 static INSTALLED: AtomicBool = AtomicBool::new(false);
258
259 pub(super) fn install(config: &StreamRedirectConfig) -> io::Result<InstalledStreamRedirect> {
260 INSTALLED
261 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
262 .map_err(|_| {
263 io::Error::new(
264 io::ErrorKind::AlreadyExists,
265 "stream redirection already installed",
266 )
267 })?;
268
269 match install_once(config) {
270 Ok(installed) => Ok(installed),
271 Err(err) => {
272 INSTALLED.store(false, Ordering::SeqCst);
273 Err(err)
274 }
275 }
276 }
277
278 fn install_once(config: &StreamRedirectConfig) -> io::Result<InstalledStreamRedirect> {
279 let mut stdout_target = prepare_target(STDOUT_FD, config.stdout_file.as_ref())?;
280 let mut stderr_target = prepare_target(STDERR_FD, config.stderr_file.as_ref())?;
281
282 let _ = io::stdout().flush();
283
284 if let Some(target) = &stdout_target {
285 redirect_fd(STDOUT_FD, target.file.as_raw_fd())?;
286 }
287 if let Some(target) = &stderr_target
288 && let Err(err) = redirect_fd(STDERR_FD, target.file.as_raw_fd())
289 {
290 if let Some(stdout_target) = &stdout_target {
291 let _ = redirect_fd(STDOUT_FD, stdout_target.restore.as_raw_fd());
292 }
293 return Err(err);
294 }
295
296 Ok(InstalledStreamRedirect {
297 stdout_file: config.stdout_file.clone(),
298 stderr_file: config.stderr_file.clone(),
299 stdout_restore: stdout_target.take().map(|target| target.restore),
300 stderr_restore: stderr_target.take().map(|target| target.restore),
301 })
302 }
303
304 struct PreparedTarget {
305 file: std::fs::File,
306 restore: OwnedFd,
307 }
308
309 fn prepare_target(
310 target_fd: RawFd,
311 path: Option<&PathBuf>,
312 ) -> io::Result<Option<PreparedTarget>> {
313 let Some(path) = path else {
314 return Ok(None);
315 };
316 let file = open_append(path)?;
317 let restore = dup_fd(target_fd)?;
318 Ok(Some(PreparedTarget { file, restore }))
319 }
320
321 fn dup_fd(fd: RawFd) -> io::Result<OwnedFd> {
322 let duped = unsafe { libc::dup(fd) };
323 if duped < 0 {
324 return Err(io::Error::last_os_error());
325 }
326 let owned = unsafe { OwnedFd::from_raw_fd(duped) };
327 set_cloexec(owned.as_raw_fd())?;
328 Ok(owned)
329 }
330
331 pub(super) fn redirect_fd(target_fd: RawFd, replacement_fd: RawFd) -> io::Result<()> {
332 let rc = unsafe { libc::dup2(replacement_fd, target_fd) };
333 if rc < 0 {
334 Err(io::Error::last_os_error())
335 } else {
336 Ok(())
337 }
338 }
339
340 pub(super) fn mark_uninstalled() {
341 INSTALLED.store(false, Ordering::SeqCst);
342 }
343
344 fn set_cloexec(fd: RawFd) -> io::Result<()> {
345 let current = unsafe { libc::fcntl(fd, libc::F_GETFD) };
346 if current < 0 {
347 return Err(io::Error::last_os_error());
348 }
349 let rc = unsafe { libc::fcntl(fd, libc::F_SETFD, current | libc::FD_CLOEXEC) };
350 if rc < 0 {
351 Err(io::Error::last_os_error())
352 } else {
353 Ok(())
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 #![allow(clippy::disallowed_methods)]
361 #![allow(clippy::expect_used)]
362
363 use super::*;
364 #[cfg(unix)]
365 use std::{
366 env, fs,
367 os::unix::fs::{PermissionsExt, symlink},
368 process::Command,
369 time::{SystemTime, UNIX_EPOCH},
370 };
371
372 #[cfg(unix)]
376 fn install_paths(
377 stdout_file_arg: Option<PathBuf>,
378 stderr_file_arg: Option<PathBuf>,
379 ) -> io::Result<Option<InstalledStreamRedirect>> {
380 match StreamRedirectConfig::new(stdout_file_arg, stderr_file_arg)? {
381 Some(config) => install(&config).map(Some),
382 None => Ok(None),
383 }
384 }
385
386 #[test]
387 fn config_builds_optional_paths() {
388 let config =
389 StreamRedirectConfig::new(Some("/tmp/afdata-out.jsonl"), Some("/tmp/afdata.err"))
390 .expect("valid config")
391 .expect("redirection should be enabled");
392 assert_eq!(
393 config.stdout_file,
394 Some(PathBuf::from("/tmp/afdata-out.jsonl"))
395 );
396 assert_eq!(config.stderr_file, Some(PathBuf::from("/tmp/afdata.err")));
397 }
398
399 #[test]
400 fn config_without_files_disables_redirection() {
401 let config = StreamRedirectConfig::new(None::<PathBuf>, None::<PathBuf>)
402 .expect("valid empty config");
403 assert_eq!(config, None);
404 }
405
406 #[test]
407 fn raw_args_support_space_separated_values() {
408 let config = config_from_raw_args([
409 "agent-cli",
410 "--stdout-file",
411 "/tmp/agent-cli.out",
412 "--stderr-file",
413 "/tmp/agent-cli.err",
414 "ping",
415 ])
416 .expect("valid raw args")
417 .expect("stream redirection should be enabled");
418 assert_eq!(
419 config.stdout_file,
420 Some(PathBuf::from("/tmp/agent-cli.out"))
421 );
422 assert_eq!(
423 config.stderr_file,
424 Some(PathBuf::from("/tmp/agent-cli.err"))
425 );
426 }
427
428 #[test]
429 fn raw_args_support_equals_values() {
430 let config = config_from_raw_args([
431 "agent-cli",
432 "--stdout-file=/tmp/agent-cli.out",
433 "--stderr-file=/tmp/agent-cli.err",
434 "ping",
435 ])
436 .expect("valid raw args")
437 .expect("stream redirection should be enabled");
438 assert_eq!(
439 config.stdout_file,
440 Some(PathBuf::from("/tmp/agent-cli.out"))
441 );
442 assert_eq!(
443 config.stderr_file,
444 Some(PathBuf::from("/tmp/agent-cli.err"))
445 );
446 }
447
448 #[test]
449 fn raw_args_accept_single_stream() {
450 let config = config_from_raw_args(["agent-cli", "--stderr-file", "/tmp/agent-cli.err"])
451 .expect("valid raw args")
452 .expect("stderr-only redirection should be enabled");
453 assert_eq!(config.stdout_file, None);
454 assert_eq!(
455 config.stderr_file,
456 Some(PathBuf::from("/tmp/agent-cli.err"))
457 );
458 }
459
460 #[test]
461 fn raw_args_reject_missing_values() {
462 assert!(config_from_raw_args(["agent-cli", "--stdout-file"]).is_err());
463 assert!(config_from_raw_args(["agent-cli", "--stderr-file", "--help"]).is_err());
464 }
465
466 #[test]
467 fn raw_args_disable_redirection_without_file_flags() {
468 assert_eq!(
469 config_from_raw_args(["agent-cli", "ping"]).expect("valid raw args without file flags"),
470 None
471 );
472 }
473
474 #[cfg(not(unix))]
475 #[test]
476 fn install_reports_unsupported_on_non_unix() {
477 let config = StreamRedirectConfig::new(Some("stdout.log"), None::<PathBuf>)
478 .expect("valid config")
479 .expect("redirection should be enabled");
480 let err = install(&config).expect_err("non-unix install must be unsupported");
481 assert_eq!(err.kind(), io::ErrorKind::Unsupported);
482 assert!(err.to_string().contains("only supported on Unix"));
483 }
484
485 #[cfg(unix)]
486 #[test]
487 fn install_redirects_stdout_and_stderr_in_child_process() {
488 let unique = SystemTime::now()
489 .duration_since(UNIX_EPOCH)
490 .expect("system clock should be after unix epoch")
491 .as_nanos();
492 let dir = env::temp_dir().join(format!(
493 "afdata-stream-redirect-{}-{unique}",
494 std::process::id()
495 ));
496 fs::create_dir_all(&dir).expect("create temp directory");
497 let stdout_file = dir.join("stdout.log");
498 let stderr_file = dir.join("stderr.log");
499 fs::write(&stdout_file, "existing stdout\n").expect("prewrite stdout file");
500
501 let status = Command::new(env::current_exe().expect("current test executable"))
502 .arg("--exact")
503 .arg("stream_redirect::tests::stream_redirect_child_writes_to_files")
504 .arg("--nocapture")
505 .env("AFDATA_STREAM_REDIRECT_CHILD", "1")
506 .env("AFDATA_STREAM_REDIRECT_STDOUT", &stdout_file)
507 .env("AFDATA_STREAM_REDIRECT_STDERR", &stderr_file)
508 .status()
509 .expect("run child test process");
510 assert!(status.success(), "child test process failed: {status}");
511
512 assert_eq!(
513 fs::read_to_string(&stdout_file).expect("read stdout file"),
514 "existing stdout\nstdout bytes\n"
515 );
516 assert_eq!(
517 fs::read_to_string(&stderr_file).expect("read stderr file"),
518 "stderr bytes\n"
519 );
520 assert_eq!(
521 fs::metadata(&stderr_file)
522 .expect("stderr metadata")
523 .permissions()
524 .mode()
525 & 0o777,
526 0o600
527 );
528 let _ = fs::remove_dir_all(dir);
529 }
530
531 #[cfg(unix)]
532 #[test]
533 fn install_rejects_symbolic_link_targets() {
534 let unique = SystemTime::now()
535 .duration_since(UNIX_EPOCH)
536 .expect("system clock should be after unix epoch")
537 .as_nanos();
538 let dir = env::temp_dir().join(format!(
539 "afdata-stream-redirect-symlink-{}-{unique}",
540 std::process::id()
541 ));
542 fs::create_dir_all(&dir).expect("create temp directory");
543 let real_file = dir.join("real.log");
544 let symlink_file = dir.join("stdout.log");
545 fs::write(&real_file, "").expect("create real file");
546 symlink(&real_file, &symlink_file).expect("create symlink");
547
548 let err = install_paths(Some(symlink_file), None::<PathBuf>)
549 .expect_err("symlink target must be rejected");
550 assert!(
551 err.to_string().contains("symbolic link")
552 || err.to_string().contains("Too many levels"),
553 "{err}"
554 );
555 let _ = fs::remove_dir_all(dir);
556 }
557
558 #[cfg(unix)]
559 #[test]
560 fn install_drop_flushes_and_restores_stdout_in_child_process() {
561 let unique = SystemTime::now()
562 .duration_since(UNIX_EPOCH)
563 .expect("system clock should be after unix epoch")
564 .as_nanos();
565 let dir = env::temp_dir().join(format!(
566 "afdata-stream-redirect-restore-{}-{unique}",
567 std::process::id()
568 ));
569 fs::create_dir_all(&dir).expect("create temp directory");
570 let stdout_file = dir.join("stdout.log");
571
572 let output = Command::new(env::current_exe().expect("current test executable"))
573 .arg("--exact")
574 .arg("stream_redirect::tests::stream_redirect_child_restores_stdout_after_drop")
575 .arg("--nocapture")
576 .env("AFDATA_STREAM_REDIRECT_RESTORE_CHILD", "1")
577 .env("AFDATA_STREAM_REDIRECT_STDOUT", &stdout_file)
578 .output()
579 .expect("run child test process");
580 assert!(
581 output.status.success(),
582 "child test process failed: status={} stderr={}",
583 output.status,
584 String::from_utf8_lossy(&output.stderr)
585 );
586
587 assert_eq!(
588 fs::read_to_string(&stdout_file).expect("read stdout file"),
589 "redirected before drop\n"
590 );
591 assert!(
592 String::from_utf8_lossy(&output.stdout).contains("stdout after restore\n"),
593 "restored stdout should reach parent capture: {}",
594 String::from_utf8_lossy(&output.stdout)
595 );
596 let _ = fs::remove_dir_all(dir);
597 }
598
599 #[cfg(unix)]
600 #[test]
601 fn install_reports_existing_redirect_and_recovers_after_drop_in_child_process() {
602 let unique = SystemTime::now()
603 .duration_since(UNIX_EPOCH)
604 .expect("system clock should be after unix epoch")
605 .as_nanos();
606 let dir = env::temp_dir().join(format!(
607 "afdata-stream-redirect-reinstall-{}-{unique}",
608 std::process::id()
609 ));
610 fs::create_dir_all(&dir).expect("create temp directory");
611 let stdout_file = dir.join("stdout.log");
612 let stderr_file = dir.join("stderr.log");
613
614 let output = Command::new(env::current_exe().expect("current test executable"))
615 .arg("--exact")
616 .arg("stream_redirect::tests::stream_redirect_child_reinstalls_after_drop")
617 .arg("--nocapture")
618 .env("AFDATA_STREAM_REDIRECT_REINSTALL_CHILD", "1")
619 .env("AFDATA_STREAM_REDIRECT_STDOUT", &stdout_file)
620 .env("AFDATA_STREAM_REDIRECT_STDERR", &stderr_file)
621 .output()
622 .expect("run child test process");
623 assert!(
624 output.status.success(),
625 "child test process failed: status={} stderr={}",
626 output.status,
627 String::from_utf8_lossy(&output.stderr)
628 );
629
630 assert_eq!(
631 fs::read_to_string(&stdout_file).expect("read stdout file"),
632 "first redirect still usable\n"
633 );
634 assert_eq!(
635 fs::read_to_string(&stderr_file).expect("read stderr file"),
636 "stderr after reinstall\n"
637 );
638 let _ = fs::remove_dir_all(dir);
639 }
640
641 #[cfg(unix)]
642 #[test]
643 fn stream_redirect_child_writes_to_files() {
644 if env::var_os("AFDATA_STREAM_REDIRECT_CHILD").is_none() {
645 return;
646 }
647 let stdout_file =
648 PathBuf::from(env::var_os("AFDATA_STREAM_REDIRECT_STDOUT").expect("stdout path"));
649 let stderr_file =
650 PathBuf::from(env::var_os("AFDATA_STREAM_REDIRECT_STDERR").expect("stderr path"));
651 let _redirect = install_paths(Some(stdout_file), Some(stderr_file))
652 .expect("install stream redirect")
653 .expect("stream redirect enabled");
654 io::stdout()
655 .write_all(b"stdout bytes\n")
656 .expect("write stdout bytes");
657 io::stderr()
658 .write_all(b"stderr bytes\n")
659 .expect("write stderr bytes");
660 }
661
662 #[cfg(unix)]
663 #[test]
664 fn stream_redirect_child_restores_stdout_after_drop() {
665 if env::var_os("AFDATA_STREAM_REDIRECT_RESTORE_CHILD").is_none() {
666 return;
667 }
668 let stdout_file =
669 PathBuf::from(env::var_os("AFDATA_STREAM_REDIRECT_STDOUT").expect("stdout path"));
670 let redirect = install_paths(Some(stdout_file), None::<PathBuf>)
671 .expect("install stream redirect")
672 .expect("stream redirect enabled");
673 io::stdout()
674 .write_all(b"redirected before drop\n")
675 .expect("write redirected stdout bytes");
676 drop(redirect);
677 io::stdout()
678 .write_all(b"stdout after restore\n")
679 .expect("write restored stdout bytes");
680 io::stdout().flush().expect("flush restored stdout");
681 }
682
683 #[cfg(unix)]
684 #[test]
685 fn stream_redirect_child_reinstalls_after_drop() {
686 if env::var_os("AFDATA_STREAM_REDIRECT_REINSTALL_CHILD").is_none() {
687 return;
688 }
689 let stdout_file =
690 PathBuf::from(env::var_os("AFDATA_STREAM_REDIRECT_STDOUT").expect("stdout path"));
691 let stderr_file =
692 PathBuf::from(env::var_os("AFDATA_STREAM_REDIRECT_STDERR").expect("stderr path"));
693 let first = install_paths(Some(stdout_file), None::<PathBuf>)
694 .expect("install first stream redirect")
695 .expect("first stream redirect enabled");
696 io::stdout()
697 .write_all(b"first redirect still usable\n")
698 .expect("write first redirected stdout bytes");
699
700 let err = install_paths(None::<PathBuf>, Some(stderr_file.clone()))
701 .expect_err("second install must report an active redirect");
702 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
703 assert!(
704 err.to_string().contains("already installed"),
705 "unexpected error message: {err}"
706 );
707
708 drop(first);
709 let second = install_paths(None::<PathBuf>, Some(stderr_file))
710 .expect("install second stream redirect after drop")
711 .expect("second stream redirect enabled");
712 io::stderr()
713 .write_all(b"stderr after reinstall\n")
714 .expect("write reinstalled stderr bytes");
715 drop(second);
716 }
717}