Skip to main content

self_update_extras/
silence.rs

1//! Silence wrapper: redirects the wrapped update's stdout while it runs.
2//!
3//! This is intended for fully headless updates that run underneath a parent
4//! process which is monitoring the child's standard I/O for other purposes,
5//! such as a stdio-based MCP server where fd 1 carries the JSON-RPC protocol
6//! stream. Any output the wrapped [`ReleaseUpdate`] writes to file descriptor
7//! 1 would corrupt that stream, so it is diverted for the duration of the
8//! update and fd 1 is restored before returning.
9
10use self_update::Status;
11use self_update::errors::{Error, Result};
12use self_update::update::{Release, ReleaseUpdate, UpdateStatus};
13
14/// Where the wrapped update's standard output (fd 1) is sent while it runs.
15///
16/// The redirect is applied at the file-descriptor level and therefore also
17/// captures output from child processes and native libraries, not just the
18/// current process's buffered stdout.
19///
20/// Named `Sink` rather than "target" to avoid colliding with
21/// [`ReleaseUpdate::target`], which reports the platform target triple.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum Sink {
24    /// Fold stdout into the process's standard error (fd 2).
25    ///
26    /// Output is preserved as out-of-band diagnostics. This is the
27    /// default.
28    #[default]
29    Stderr,
30    /// Discard stdout entirely, redirecting it to the `/dev/null` device on
31    /// Unix or the `NUL` device on Windows.
32    ///
33    /// Use this for a truly hidden update whose output should vanish.
34    Null,
35}
36
37/// Builder for a [`silence`](crate::silence) [`Update`].
38///
39/// Configure the inner [`ReleaseUpdate`] backend and, optionally, the redirect
40/// [`Sink`], then call [`build`](Self::build) to produce a
41/// `Box<dyn ReleaseUpdate>` that diverts fd 1 while the wrapped update runs.
42#[derive(Default)]
43pub struct UpdateBuilder {
44    release_update: Option<Box<dyn ReleaseUpdate>>,
45    sink: Option<Sink>,
46}
47
48impl UpdateBuilder {
49    /// Initialize a new builder.
50    pub fn new() -> Self {
51        Default::default()
52    }
53
54    /// Set the release update implementation to wrap. Required.
55    pub fn release_update(&mut self, release_update: Box<dyn ReleaseUpdate>) -> &mut Self {
56        self.release_update = Some(release_update);
57        self
58    }
59
60    /// Set where fd 1 is redirected while the update runs. Defaults to
61    /// [`Sink::Stderr`].
62    pub fn sink(&mut self, sink: Sink) -> &mut Self {
63        self.sink = Some(sink);
64        self
65    }
66
67    /// Confirm config and create a ready-to-use `Update`.
68    ///
69    /// * Errors:
70    ///     * Config - `release_update` was not provided
71    pub fn build(&mut self) -> Result<Box<dyn ReleaseUpdate>> {
72        let inner = self
73            .release_update
74            .take()
75            .ok_or_else(|| Error::Config("`release_update` required".to_owned()))?;
76
77        Ok(Box::new(Update {
78            inner,
79            sink: self.sink.unwrap_or_default(),
80        }))
81    }
82}
83
84/// Wraps a [`ReleaseUpdate`] and redirects file descriptor 1 while it runs.
85///
86/// Before delegating to the inner update, fd 1 is pointed at the configured
87/// [`Sink`]; once the update returns (successfully or not) the original fd 1
88/// is restored. Because the redirect happens at the descriptor level, output
89/// from spawned child processes and native code is diverted too.
90///
91/// # Platform behavior
92///
93/// - **Unix**: fd 1 is duplicated and swapped via `dup`/`dup2`.
94/// - **Windows**: the process's standard-output handle is swapped via
95///   `GetStdHandle`/`SetStdHandle` (pointed at the `NUL` device or the
96///   standard-error handle). Rust's `std` re-queries `GetStdHandle` on every
97///   write, so its output honors the swap for the duration of the update.
98/// - **Other platforms**: redirection is unsupported, so the wrapped update
99///   runs unchanged (no redirect is applied).
100///
101/// # Metadata
102///
103/// All metadata methods (including `no_confirm`, `show_output`, and
104/// `show_download_progress`) delegate to the wrapped update. Because a backend
105/// reads its own configuration when it runs, overriding those on the wrapper
106/// would not change the behavior of an arbitrarily deep composition, so this
107/// wrapper relies solely on the fd redirect to keep the update quiet.
108pub struct Update {
109    inner: Box<dyn ReleaseUpdate>,
110    sink: Sink,
111}
112
113impl Update {
114    /// Initialize a new `Update` builder.
115    pub fn configure() -> UpdateBuilder {
116        UpdateBuilder::new()
117    }
118
119    /// The [`Sink`] fd 1 is redirected to while the wrapped update runs.
120    pub fn sink(&self) -> Sink {
121        self.sink
122    }
123}
124
125impl ReleaseUpdate for Update {
126    fn get_latest_release(&self) -> Result<Release> {
127        self.inner.get_latest_release()
128    }
129
130    fn get_latest_releases(&self, current_version: &str) -> Result<Vec<Release>> {
131        self.inner.get_latest_releases(current_version)
132    }
133
134    fn get_release_version(&self, ver: &str) -> Result<Release> {
135        self.inner.get_release_version(ver)
136    }
137
138    fn current_version(&self) -> String {
139        self.inner.current_version()
140    }
141
142    fn target(&self) -> String {
143        self.inner.target()
144    }
145
146    fn target_version(&self) -> Option<String> {
147        self.inner.target_version()
148    }
149
150    fn bin_name(&self) -> String {
151        self.inner.bin_name()
152    }
153
154    fn bin_install_path(&self) -> std::path::PathBuf {
155        self.inner.bin_install_path()
156    }
157
158    fn bin_path_in_archive(&self) -> String {
159        self.inner.bin_path_in_archive()
160    }
161
162    fn show_download_progress(&self) -> bool {
163        self.inner.show_download_progress()
164    }
165
166    fn show_output(&self) -> bool {
167        self.inner.show_output()
168    }
169
170    fn no_confirm(&self) -> bool {
171        self.inner.no_confirm()
172    }
173
174    fn progress_template(&self) -> String {
175        self.inner.progress_template()
176    }
177
178    fn progress_chars(&self) -> String {
179        self.inner.progress_chars()
180    }
181
182    fn auth_token(&self) -> Option<String> {
183        self.inner.auth_token()
184    }
185
186    fn update(&self) -> Result<Status> {
187        let current_version = self.current_version();
188        self.update_extended()
189            .map(|s| s.into_status(current_version))
190    }
191
192    fn update_extended(&self) -> Result<UpdateStatus> {
193        // The guard restores stdout when it drops at the end of this scope,
194        // covering every early return from the inner update.
195        #[cfg(any(unix, windows))]
196        let _redirect = StdoutRedirect::new(self.sink)?;
197        #[cfg(not(any(unix, windows)))]
198        let _ = self.sink;
199
200        self.inner.update_extended()
201    }
202}
203
204/// RAII guard that redirects fd 1 on construction and restores it on drop.
205#[cfg(unix)]
206struct StdoutRedirect {
207    saved: std::os::fd::OwnedFd,
208}
209
210#[cfg(unix)]
211impl StdoutRedirect {
212    fn new(sink: Sink) -> Result<Self> {
213        use std::io::Write;
214        use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
215
216        // Flush buffered stdout so pending output reaches the real stdout
217        // before fd 1 is diverted.
218        let _ = std::io::stdout().flush();
219
220        // Save the current stdout so it can be restored on drop.
221        // SAFETY: `dup` returns a fresh descriptor we take exclusive ownership
222        // of, or a negative value on error (handled below).
223        let saved = unsafe {
224            let fd = libc::dup(libc::STDOUT_FILENO);
225            if fd < 0 {
226                return Err(os_error("duplicating stdout"));
227            }
228            OwnedFd::from_raw_fd(fd)
229        };
230
231        let result = match sink {
232            // SAFETY: fd 1 and fd 2 are valid; `dup2` points fd 1 at fd 2's
233            // open file description.
234            Sink::Stderr => unsafe { libc::dup2(libc::STDERR_FILENO, libc::STDOUT_FILENO) },
235            Sink::Null => {
236                // SAFETY: opening "/dev/null" yields an owned descriptor that
237                // is closed as soon as fd 1 has been pointed at it.
238                let null = unsafe {
239                    let fd = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
240                    if fd < 0 {
241                        return Err(os_error("opening /dev/null"));
242                    }
243                    OwnedFd::from_raw_fd(fd)
244                };
245                // SAFETY: both descriptors are valid for the duration of the call.
246                unsafe { libc::dup2(null.as_raw_fd(), libc::STDOUT_FILENO) }
247            }
248        };
249
250        if result < 0 {
251            return Err(os_error("redirecting stdout"));
252        }
253
254        Ok(Self { saved })
255    }
256}
257
258#[cfg(unix)]
259impl Drop for StdoutRedirect {
260    fn drop(&mut self) {
261        use std::io::Write;
262        use std::os::fd::AsRawFd;
263
264        // Flush anything the wrapped update buffered into the diverted stdout,
265        // then restore fd 1 to its original destination.
266        let _ = std::io::stdout().flush();
267        // SAFETY: `saved` is a valid descriptor referring to the original stdout.
268        unsafe {
269            libc::dup2(self.saved.as_raw_fd(), libc::STDOUT_FILENO);
270        }
271    }
272}
273
274/// Minimal Win32 bindings for swapping the standard-output handle. Declared
275/// locally (mirroring the crate's use of raw `libc` on Unix) to avoid pulling
276/// in a Windows API crate for a handful of stable calls.
277#[cfg(windows)]
278#[allow(non_snake_case)]
279mod win {
280    use std::ffi::c_void;
281
282    pub type Handle = *mut c_void;
283
284    pub const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
285    pub const STD_ERROR_HANDLE: u32 = -12i32 as u32;
286    pub const INVALID_HANDLE_VALUE: Handle = -1isize as Handle;
287    pub const GENERIC_WRITE: u32 = 0x4000_0000;
288    pub const FILE_SHARE_READ: u32 = 0x0000_0001;
289    pub const FILE_SHARE_WRITE: u32 = 0x0000_0002;
290    pub const OPEN_EXISTING: u32 = 3;
291    pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
292
293    unsafe extern "system" {
294        pub fn GetStdHandle(nStdHandle: u32) -> Handle;
295        pub fn SetStdHandle(nStdHandle: u32, hHandle: Handle) -> i32;
296        pub fn CreateFileW(
297            lpFileName: *const u16,
298            dwDesiredAccess: u32,
299            dwShareMode: u32,
300            lpSecurityAttributes: *mut c_void,
301            dwCreationDisposition: u32,
302            dwFlagsAndAttributes: u32,
303            hTemplateFile: Handle,
304        ) -> Handle;
305        pub fn CloseHandle(hObject: Handle) -> i32;
306    }
307}
308
309/// RAII guard that swaps the standard-output handle on construction and
310/// restores it on drop.
311#[cfg(windows)]
312struct StdoutRedirect {
313    saved: win::Handle,
314    /// A handle we opened ourselves (the `NUL` device) that must be closed on
315    /// drop, or null when the sink borrows an existing handle (stderr).
316    opened: win::Handle,
317}
318
319#[cfg(windows)]
320impl StdoutRedirect {
321    fn new(sink: Sink) -> Result<Self> {
322        use std::io::Write;
323
324        // Flush buffered stdout so pending output reaches the real stdout
325        // before the handle is swapped.
326        let _ = std::io::stdout().flush();
327
328        // SAFETY: FFI call returning the current stdout handle.
329        let saved = unsafe { win::GetStdHandle(win::STD_OUTPUT_HANDLE) };
330        if saved == win::INVALID_HANDLE_VALUE {
331            return Err(os_error("querying stdout handle"));
332        }
333
334        let (target, opened) = match sink {
335            Sink::Stderr => {
336                // SAFETY: FFI call returning the current stderr handle.
337                let err = unsafe { win::GetStdHandle(win::STD_ERROR_HANDLE) };
338                if err == win::INVALID_HANDLE_VALUE {
339                    return Err(os_error("querying stderr handle"));
340                }
341                (err, std::ptr::null_mut())
342            }
343            Sink::Null => {
344                // "NUL" as a NUL-terminated wide string.
345                let name = [b'N' as u16, b'U' as u16, b'L' as u16, 0];
346                // SAFETY: opens the null device for writing; validated below.
347                let handle = unsafe {
348                    win::CreateFileW(
349                        name.as_ptr(),
350                        win::GENERIC_WRITE,
351                        win::FILE_SHARE_READ | win::FILE_SHARE_WRITE,
352                        std::ptr::null_mut(),
353                        win::OPEN_EXISTING,
354                        win::FILE_ATTRIBUTE_NORMAL,
355                        std::ptr::null_mut(),
356                    )
357                };
358                if handle == win::INVALID_HANDLE_VALUE {
359                    return Err(os_error("opening NUL device"));
360                }
361                (handle, handle)
362            }
363        };
364
365        // SAFETY: redirects the process's stdout handle to `target`.
366        if unsafe { win::SetStdHandle(win::STD_OUTPUT_HANDLE, target) } == 0 {
367            if !opened.is_null() {
368                // SAFETY: release the handle we opened before erroring out.
369                unsafe { win::CloseHandle(opened) };
370            }
371            return Err(os_error("redirecting stdout handle"));
372        }
373
374        Ok(Self { saved, opened })
375    }
376}
377
378#[cfg(windows)]
379impl Drop for StdoutRedirect {
380    fn drop(&mut self) {
381        use std::io::Write;
382
383        // Flush anything the wrapped update buffered into the diverted stdout,
384        // then restore the original handle.
385        let _ = std::io::stdout().flush();
386        // SAFETY: `saved` is the valid original stdout handle; `opened`, if
387        // non-null, is the `NUL` handle we allocated in `new`.
388        unsafe {
389            win::SetStdHandle(win::STD_OUTPUT_HANDLE, self.saved);
390            if !self.opened.is_null() {
391                win::CloseHandle(self.opened);
392            }
393        }
394    }
395}
396
397#[cfg(any(unix, windows))]
398fn os_error(context: &str) -> Error {
399    Error::Release(format!("{context}: {}", std::io::Error::last_os_error()))
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use crate::test_support::MockRelease;
406
407    /// Build a concrete `Update` for white-box tests. `build()` returns a
408    /// `Box<dyn ReleaseUpdate>`, which hides the concrete type and its fields.
409    fn concrete(mock: MockRelease, sink: Sink) -> Update {
410        Update {
411            inner: Box::new(mock),
412            sink,
413        }
414    }
415
416    #[test]
417    fn build_requires_a_release_update() {
418        assert!(Update::configure().build().is_err());
419    }
420
421    #[test]
422    fn sink_defaults_to_stderr() {
423        assert_eq!(Sink::default(), Sink::Stderr);
424        let updater = concrete(MockRelease::new("mock-silence-default"), Sink::default());
425        assert_eq!(updater.sink(), Sink::Stderr);
426    }
427
428    #[test]
429    fn sink_getter_reports_the_configured_sink() {
430        let updater = concrete(MockRelease::new("mock-silence-getter"), Sink::Null);
431        assert_eq!(updater.sink(), Sink::Null);
432    }
433
434    #[test]
435    fn builder_sets_the_sink() {
436        // `build` erases the concrete type, so exercise the builder plumbing
437        // by driving an update and confirming the inner backend still runs.
438        let mock = MockRelease::new("mock-silence-builder");
439        let calls = mock.call_counter();
440        let updater = Update::configure()
441            .release_update(Box::new(mock))
442            .sink(Sink::Null)
443            .build()
444            .unwrap();
445
446        let status = updater.update().unwrap();
447
448        assert!(matches!(status, Status::UpToDate(_)));
449        assert_eq!(calls.get(), 1);
450    }
451
452    #[test]
453    fn metadata_methods_delegate_to_inner() {
454        let updater = concrete(MockRelease::new("mock-silence-forward"), Sink::Stderr);
455
456        assert_eq!(updater.current_version(), "1.0.0");
457        assert_eq!(updater.target(), "test-target");
458        assert_eq!(updater.target_version(), None);
459        assert_eq!(updater.bin_name(), "mock-silence-forward");
460        assert_eq!(
461            updater.bin_install_path(),
462            std::env::temp_dir().join("mock-silence-forward")
463        );
464        assert_eq!(updater.bin_path_in_archive(), "mock-silence-forward");
465        assert!(!updater.show_download_progress());
466        assert!(!updater.show_output());
467        assert!(updater.no_confirm());
468        assert_eq!(updater.progress_template(), "");
469        assert_eq!(updater.progress_chars(), "");
470        assert_eq!(updater.auth_token(), None);
471        assert!(updater.get_latest_release().is_ok());
472        assert!(updater.get_latest_releases("1.0.0").is_ok());
473        assert!(updater.get_release_version("1.0.0").is_ok());
474    }
475
476    // The descriptor-level tests below replace the process's fd 1 (and fd 2)
477    // with observation files, so they must run serially and always restore the
478    // saved descriptors before asserting to avoid leaving stdout broken.
479    #[cfg(unix)]
480    mod fd {
481        use super::*;
482        use std::io::{Read, Seek, SeekFrom};
483        use std::os::fd::AsRawFd;
484
485        fn scratch(name: &str) -> (std::fs::File, std::path::PathBuf) {
486            let path = std::env::temp_dir().join(name);
487            let _ = std::fs::remove_file(&path);
488            let file = std::fs::OpenOptions::new()
489                .create(true)
490                .read(true)
491                .write(true)
492                .truncate(true)
493                .open(&path)
494                .unwrap();
495            (file, path)
496        }
497
498        fn read_all(file: &mut std::fs::File) -> String {
499            file.seek(SeekFrom::Start(0)).unwrap();
500            let mut buf = String::new();
501            file.read_to_string(&mut buf).unwrap();
502            buf
503        }
504
505        // These tests swap the process-global fd 1, so the test harness's own
506        // `test ... ok` progress lines can interleave into the observation
507        // files. Assertions therefore check for the presence/absence of the
508        // mock's distinctive markers rather than exact file contents.
509
510        #[test]
511        #[serial_test::serial(silence_stdout_fd)]
512        fn null_sink_discards_stdout() {
513            let (mut out, out_path) = scratch("mock-silence-null.out");
514
515            // SAFETY: standard descriptor juggling; `saved` is restored below.
516            let saved = unsafe { libc::dup(libc::STDOUT_FILENO) };
517            assert!(saved >= 0);
518            unsafe { libc::dup2(out.as_raw_fd(), libc::STDOUT_FILENO) };
519
520            let updater = concrete(
521                MockRelease::new("mock-silence-null").print_on_update("LEAK"),
522                Sink::Null,
523            );
524            let _ = updater.update();
525
526            unsafe {
527                libc::dup2(saved, libc::STDOUT_FILENO);
528                libc::close(saved);
529            }
530
531            assert!(
532                !read_all(&mut out).contains("LEAK"),
533                "stdout should have been discarded to /dev/null"
534            );
535            let _ = std::fs::remove_file(&out_path);
536        }
537
538        #[test]
539        #[serial_test::serial(silence_stdout_fd)]
540        fn stderr_sink_folds_stdout_into_stderr() {
541            let (mut out, out_path) = scratch("mock-silence-stderr.out");
542            let (mut err, err_path) = scratch("mock-silence-stderr.err");
543
544            let saved_out = unsafe { libc::dup(libc::STDOUT_FILENO) };
545            let saved_err = unsafe { libc::dup(libc::STDERR_FILENO) };
546            assert!(saved_out >= 0 && saved_err >= 0);
547            unsafe {
548                libc::dup2(out.as_raw_fd(), libc::STDOUT_FILENO);
549                libc::dup2(err.as_raw_fd(), libc::STDERR_FILENO);
550            }
551
552            let updater = concrete(
553                MockRelease::new("mock-silence-stderr").print_on_update("HELLO"),
554                Sink::Stderr,
555            );
556            let _ = updater.update();
557
558            unsafe {
559                libc::dup2(saved_out, libc::STDOUT_FILENO);
560                libc::dup2(saved_err, libc::STDERR_FILENO);
561                libc::close(saved_out);
562                libc::close(saved_err);
563            }
564
565            assert!(
566                !read_all(&mut out).contains("HELLO"),
567                "stdout should not have carried the wrapped update's output"
568            );
569            assert!(
570                read_all(&mut err).contains("HELLO"),
571                "stdout should have been folded into stderr"
572            );
573            let _ = std::fs::remove_file(&out_path);
574            let _ = std::fs::remove_file(&err_path);
575        }
576
577        #[test]
578        #[serial_test::serial(silence_stdout_fd)]
579        fn stdout_is_restored_after_update() {
580            let (mut out, out_path) = scratch("mock-silence-restore.out");
581
582            let saved = unsafe { libc::dup(libc::STDOUT_FILENO) };
583            assert!(saved >= 0);
584            unsafe { libc::dup2(out.as_raw_fd(), libc::STDOUT_FILENO) };
585
586            let updater = concrete(
587                MockRelease::new("mock-silence-restore").print_on_update("DURING"),
588                Sink::Null,
589            );
590            let _ = updater.update();
591
592            // Writing after the update must land in the original destination,
593            // proving fd 1 was restored.
594            let marker = b"AFTER";
595            unsafe {
596                libc::write(
597                    libc::STDOUT_FILENO,
598                    marker.as_ptr() as *const libc::c_void,
599                    marker.len(),
600                );
601                libc::dup2(saved, libc::STDOUT_FILENO);
602                libc::close(saved);
603            }
604
605            let observed = read_all(&mut out);
606            assert!(
607                observed.contains("AFTER"),
608                "fd 1 must be restored so later writes reach the original stdout"
609            );
610            assert!(
611                !observed.contains("DURING"),
612                "the update's output must not have reached the original stdout"
613            );
614            let _ = std::fs::remove_file(&out_path);
615        }
616    }
617
618    // The Windows counterparts swap the process-global standard-output (and
619    // standard-error) handles, so they must run serially and restore the saved
620    // handles before asserting. As with the Unix tests, the harness's progress
621    // lines can interleave into the observation files, so assertions check for
622    // the presence/absence of the mock's markers rather than exact contents.
623    #[cfg(windows)]
624    mod handle {
625        use super::*;
626        use crate::silence::win;
627        use std::io::{Read, Seek, SeekFrom};
628        use std::os::windows::io::AsRawHandle;
629
630        fn scratch(name: &str) -> (std::fs::File, std::path::PathBuf) {
631            let path = std::env::temp_dir().join(name);
632            let _ = std::fs::remove_file(&path);
633            let file = std::fs::OpenOptions::new()
634                .create(true)
635                .read(true)
636                .write(true)
637                .truncate(true)
638                .open(&path)
639                .unwrap();
640            (file, path)
641        }
642
643        fn read_all(file: &mut std::fs::File) -> String {
644            file.seek(SeekFrom::Start(0)).unwrap();
645            let mut buf = String::new();
646            file.read_to_string(&mut buf).unwrap();
647            buf
648        }
649
650        #[test]
651        #[serial_test::serial(silence_stdout_fd)]
652        fn null_sink_discards_stdout() {
653            let (mut out, out_path) = scratch("mock-silence-null-win.out");
654
655            // SAFETY: standard handle juggling; `saved` is restored below.
656            let saved = unsafe { win::GetStdHandle(win::STD_OUTPUT_HANDLE) };
657            unsafe {
658                win::SetStdHandle(win::STD_OUTPUT_HANDLE, out.as_raw_handle() as win::Handle)
659            };
660
661            let updater = concrete(
662                MockRelease::new("mock-silence-null-win").print_on_update("LEAK"),
663                Sink::Null,
664            );
665            let _ = updater.update();
666
667            unsafe { win::SetStdHandle(win::STD_OUTPUT_HANDLE, saved) };
668
669            assert!(
670                !read_all(&mut out).contains("LEAK"),
671                "stdout should have been discarded to the NUL device"
672            );
673            let _ = std::fs::remove_file(&out_path);
674        }
675
676        #[test]
677        #[serial_test::serial(silence_stdout_fd)]
678        fn stderr_sink_folds_stdout_into_stderr() {
679            let (mut out, out_path) = scratch("mock-silence-stderr-win.out");
680            let (mut err, err_path) = scratch("mock-silence-stderr-win.err");
681
682            let saved_out = unsafe { win::GetStdHandle(win::STD_OUTPUT_HANDLE) };
683            let saved_err = unsafe { win::GetStdHandle(win::STD_ERROR_HANDLE) };
684            unsafe {
685                win::SetStdHandle(win::STD_OUTPUT_HANDLE, out.as_raw_handle() as win::Handle);
686                win::SetStdHandle(win::STD_ERROR_HANDLE, err.as_raw_handle() as win::Handle);
687            }
688
689            let updater = concrete(
690                MockRelease::new("mock-silence-stderr-win").print_on_update("HELLO"),
691                Sink::Stderr,
692            );
693            let _ = updater.update();
694
695            unsafe {
696                win::SetStdHandle(win::STD_OUTPUT_HANDLE, saved_out);
697                win::SetStdHandle(win::STD_ERROR_HANDLE, saved_err);
698            }
699
700            assert!(
701                !read_all(&mut out).contains("HELLO"),
702                "stdout should not have carried the wrapped update's output"
703            );
704            assert!(
705                read_all(&mut err).contains("HELLO"),
706                "stdout should have been folded into stderr"
707            );
708            let _ = std::fs::remove_file(&out_path);
709            let _ = std::fs::remove_file(&err_path);
710        }
711
712        #[test]
713        #[serial_test::serial(silence_stdout_fd)]
714        fn stdout_is_restored_after_update() {
715            let (mut out, out_path) = scratch("mock-silence-restore-win.out");
716
717            let saved = unsafe { win::GetStdHandle(win::STD_OUTPUT_HANDLE) };
718            let observation = out.as_raw_handle() as win::Handle;
719            unsafe { win::SetStdHandle(win::STD_OUTPUT_HANDLE, observation) };
720
721            let updater = concrete(
722                MockRelease::new("mock-silence-restore-win").print_on_update("DURING"),
723                Sink::Null,
724            );
725            let _ = updater.update();
726
727            // The wrapper must have restored our observation handle as stdout.
728            let after = unsafe { win::GetStdHandle(win::STD_OUTPUT_HANDLE) };
729            unsafe { win::SetStdHandle(win::STD_OUTPUT_HANDLE, saved) };
730
731            assert_eq!(
732                after, observation,
733                "the standard-output handle must be restored after the update"
734            );
735            assert!(
736                !read_all(&mut out).contains("DURING"),
737                "the update's output must not have reached the original stdout"
738            );
739            let _ = std::fs::remove_file(&out_path);
740        }
741    }
742}