1use self_update::Status;
11use self_update::errors::{Error, Result};
12use self_update::update::{Release, ReleaseUpdate, UpdateStatus};
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum Sink {
24 #[default]
29 Stderr,
30 Null,
35}
36
37#[derive(Default)]
43pub struct UpdateBuilder {
44 release_update: Option<Box<dyn ReleaseUpdate>>,
45 sink: Option<Sink>,
46}
47
48impl UpdateBuilder {
49 pub fn new() -> Self {
51 Default::default()
52 }
53
54 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 pub fn sink(&mut self, sink: Sink) -> &mut Self {
63 self.sink = Some(sink);
64 self
65 }
66
67 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
84pub struct Update {
109 inner: Box<dyn ReleaseUpdate>,
110 sink: Sink,
111}
112
113impl Update {
114 pub fn configure() -> UpdateBuilder {
116 UpdateBuilder::new()
117 }
118
119 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 #[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#[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 let _ = std::io::stdout().flush();
219
220 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 Sink::Stderr => unsafe { libc::dup2(libc::STDERR_FILENO, libc::STDOUT_FILENO) },
235 Sink::Null => {
236 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 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 let _ = std::io::stdout().flush();
267 unsafe {
269 libc::dup2(self.saved.as_raw_fd(), libc::STDOUT_FILENO);
270 }
271 }
272}
273
274#[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#[cfg(windows)]
312struct StdoutRedirect {
313 saved: win::Handle,
314 opened: win::Handle,
317}
318
319#[cfg(windows)]
320impl StdoutRedirect {
321 fn new(sink: Sink) -> Result<Self> {
322 use std::io::Write;
323
324 let _ = std::io::stdout().flush();
327
328 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 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 let name = [b'N' as u16, b'U' as u16, b'L' as u16, 0];
346 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 if unsafe { win::SetStdHandle(win::STD_OUTPUT_HANDLE, target) } == 0 {
367 if !opened.is_null() {
368 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 let _ = std::io::stdout().flush();
386 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 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 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 #[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 #[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 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 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 #[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 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 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}