Skip to main content

self_update_extras/
restart.rs

1//! Restart wrapper: re-executes the process after a successful update.
2
3use self_update::Status;
4use self_update::errors::{Error, Result};
5use self_update::update::{Release, ReleaseUpdate, UpdateStatus};
6
7/// Default guard environment variable name.
8const DEFAULT_GUARD_ENV: &str = "RESTART_GUARD";
9
10/// Builder for a restart [`Update`].
11///
12/// Configure the inner [`ReleaseUpdate`] backend and an optional guard
13/// environment variable, then call [`build`](Self::build) to produce a
14/// `Box<dyn ReleaseUpdate>` that re-executes the process after a successful
15/// update.
16#[derive(Default)]
17pub struct UpdateBuilder {
18    release_update: Option<Box<dyn ReleaseUpdate>>,
19    guard_env: Option<String>,
20}
21
22impl UpdateBuilder {
23    /// Initialize a new builder.
24    pub fn new() -> Self {
25        Default::default()
26    }
27
28    /// Set the release update implementation to wrap. Required.
29    pub fn release_update(&mut self, release_update: Box<dyn ReleaseUpdate>) -> &mut Self {
30        self.release_update = Some(release_update);
31        self
32    }
33
34    /// Set the guard environment variable name used to prevent restart loops.
35    /// Defaults to `RESTART_GUARD`.
36    pub fn guard_env(&mut self, env: &str) -> &mut Self {
37        self.guard_env = Some(env.to_owned());
38        self
39    }
40
41    /// Confirm config and create a ready-to-use `Update`.
42    ///
43    /// * Errors:
44    ///     * Config - `release_update` was not provided
45    pub fn build(&mut self) -> Result<Box<dyn ReleaseUpdate>> {
46        let inner = self
47            .release_update
48            .take()
49            .ok_or_else(|| Error::Config("`release_update` required".to_owned()))?;
50
51        Ok(Box::new(Update {
52            inner,
53            guard_env: self
54                .guard_env
55                .clone()
56                .unwrap_or_else(|| DEFAULT_GUARD_ENV.to_owned()),
57        }))
58    }
59}
60
61/// Wraps a [`ReleaseUpdate`] and restarts the process with the freshly
62/// installed binary after a successful update.
63///
64/// After `update()` returns `Status::Updated`, the process is re-executed
65/// using the new binary. A guard environment variable prevents restart loops:
66/// once the restarted process detects the guard, it returns `UpToDate` instead
67/// of restarting again.
68///
69/// # Platform behavior
70///
71/// - **Unix**: Replaces the current process image via `exec` (never returns on success).
72/// - **Windows**: Spawns the new binary and exits with the child's status code.
73///
74/// # Guard environment variable
75///
76/// Defaults to `RESTART_GUARD` but can be customized via
77/// [`UpdateBuilder::guard_env`](UpdateBuilder::guard_env). The value is set to
78/// `"1"` during re-execution.
79pub struct Update {
80    inner: Box<dyn ReleaseUpdate>,
81    guard_env: String,
82}
83
84impl Update {
85    /// Initialize a new `Update` builder.
86    pub fn configure() -> UpdateBuilder {
87        UpdateBuilder::new()
88    }
89
90    /// Restart the process using the freshly installed executable.
91    ///
92    /// On Unix the current process image is replaced via `exec`, so this never
93    /// returns on success. Windows has no `exec`, so the new binary is spawned
94    /// and the current process exits with the child's status code.
95    fn restart(&self) -> Result<()> {
96        let args: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect();
97        let mut command = std::process::Command::new(self.inner.bin_install_path());
98        command.args(&args).env(self.guard_env.clone(), "1");
99
100        #[cfg(unix)]
101        {
102            use std::os::unix::process::CommandExt;
103            let err = command.exec();
104            Err(Error::Release(format!(
105                "re-executing updated binary: {}",
106                err
107            )))
108        }
109
110        #[cfg(windows)]
111        {
112            let status = command
113                .status()
114                .map_err(|e| Error::Release(format!("re-executing updated binary: {}", e)))?;
115            std::process::exit(status.code().unwrap_or(0));
116        }
117    }
118}
119
120impl ReleaseUpdate for Update {
121    fn get_latest_release(&self) -> Result<Release> {
122        self.inner.get_latest_release()
123    }
124
125    fn get_latest_releases(&self, current_version: &str) -> Result<Vec<Release>> {
126        self.inner.get_latest_releases(current_version)
127    }
128
129    fn get_release_version(&self, ver: &str) -> Result<Release> {
130        self.inner.get_release_version(ver)
131    }
132
133    fn current_version(&self) -> String {
134        self.inner.current_version()
135    }
136
137    fn target(&self) -> String {
138        self.inner.target()
139    }
140
141    fn target_version(&self) -> Option<String> {
142        self.inner.target_version()
143    }
144
145    fn bin_name(&self) -> String {
146        self.inner.bin_name()
147    }
148
149    fn bin_install_path(&self) -> std::path::PathBuf {
150        self.inner.bin_install_path()
151    }
152
153    fn bin_path_in_archive(&self) -> String {
154        self.inner.bin_path_in_archive()
155    }
156
157    fn show_download_progress(&self) -> bool {
158        self.inner.show_download_progress()
159    }
160
161    fn show_output(&self) -> bool {
162        self.inner.show_output()
163    }
164
165    fn no_confirm(&self) -> bool {
166        self.inner.no_confirm()
167    }
168
169    fn progress_template(&self) -> String {
170        self.inner.progress_template()
171    }
172
173    fn progress_chars(&self) -> String {
174        self.inner.progress_chars()
175    }
176
177    fn auth_token(&self) -> Option<String> {
178        self.inner.auth_token()
179    }
180
181    fn update(&self) -> Result<Status> {
182        // Already restarted after an update: nothing left to do.
183        if std::env::var_os(&self.guard_env).is_some() {
184            return Ok(Status::UpToDate(self.current_version()));
185        }
186
187        let status = self.inner.update()?;
188        if let Status::Updated(_) = status {
189            self.restart()?;
190        }
191        Ok(status)
192    }
193
194    fn update_extended(&self) -> Result<UpdateStatus> {
195        // Already restarted after an update: nothing left to do.
196        if std::env::var_os(&self.guard_env).is_some() {
197            return Ok(UpdateStatus::UpToDate);
198        }
199
200        let status = self.inner.update_extended()?;
201        if let UpdateStatus::Updated(_) = status {
202            self.restart()?;
203        }
204        Ok(status)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::test_support::MockRelease;
212
213    // These tests mutate a process-global environment variable, so they run
214    // serially. The mock only reports `Updated` when the guard is set, which
215    // means `inner.update` is short-circuited before any real re-exec occurs.
216
217    #[test]
218    #[serial_test::serial(env_guard)]
219    fn update_is_skipped_when_guard_env_is_set() {
220        let guard = "AUTO_UPDATE_TEST_GUARD_SET";
221        unsafe { std::env::set_var(guard, "1") };
222
223        let mock = MockRelease::new("mock-guard-set").report_updated(true);
224        let calls = mock.call_counter();
225        let updater = Update::configure()
226            .release_update(Box::new(mock))
227            .guard_env(guard)
228            .build()
229            .unwrap();
230
231        let status = updater.update().unwrap();
232
233        assert!(matches!(status, Status::UpToDate(_)));
234        assert_eq!(
235            calls.get(),
236            0,
237            "inner update must be skipped when the guard is set"
238        );
239
240        unsafe { std::env::remove_var(guard) };
241    }
242
243    #[test]
244    #[serial_test::serial(env_guard)]
245    fn update_extended_is_skipped_when_guard_env_is_set() {
246        let guard = "AUTO_UPDATE_TEST_GUARD_EXT";
247        unsafe { std::env::set_var(guard, "1") };
248
249        let mock = MockRelease::new("mock-guard-ext");
250        let calls = mock.call_counter();
251        let updater = Update::configure()
252            .release_update(Box::new(mock))
253            .guard_env(guard)
254            .build()
255            .unwrap();
256
257        let status = updater.update_extended().unwrap();
258
259        assert!(matches!(status, UpdateStatus::UpToDate));
260        assert_eq!(calls.get(), 0);
261
262        unsafe { std::env::remove_var(guard) };
263    }
264
265    #[test]
266    #[serial_test::serial(env_guard)]
267    fn update_runs_inner_when_guard_env_is_unset() {
268        let guard = "AUTO_UPDATE_TEST_GUARD_UNSET";
269        unsafe { std::env::remove_var(guard) };
270
271        // Mock reports `UpToDate`, so no restart (process re-exec) is triggered.
272        let mock = MockRelease::new("mock-guard-unset");
273        let calls = mock.call_counter();
274        let updater = Update::configure()
275            .release_update(Box::new(mock))
276            .guard_env(guard)
277            .build()
278            .unwrap();
279
280        let status = updater.update().unwrap();
281
282        assert!(matches!(status, Status::UpToDate(_)));
283        assert_eq!(calls.get(), 1);
284    }
285
286    #[test]
287    fn build_requires_a_release_update() {
288        assert!(Update::configure().build().is_err());
289    }
290
291    #[test]
292    fn metadata_methods_delegate_to_inner() {
293        let updater = Update::configure()
294            .release_update(Box::new(MockRelease::new("mock-forward")))
295            .build()
296            .unwrap();
297
298        assert_eq!(updater.current_version(), "1.0.0");
299        assert_eq!(updater.target(), "test-target");
300        assert_eq!(updater.target_version(), None);
301        assert_eq!(updater.bin_name(), "mock-forward");
302        assert_eq!(
303            updater.bin_install_path(),
304            std::env::temp_dir().join("mock-forward")
305        );
306        assert_eq!(updater.bin_path_in_archive(), "mock-forward");
307        assert!(!updater.show_download_progress());
308        assert!(!updater.show_output());
309        assert!(updater.no_confirm());
310        assert_eq!(updater.progress_template(), "");
311        assert_eq!(updater.progress_chars(), "");
312        assert_eq!(updater.auth_token(), None);
313        assert!(updater.get_latest_release().is_ok());
314        assert!(updater.get_latest_releases("1.0.0").is_ok());
315        assert!(updater.get_release_version("1.0.0").is_ok());
316    }
317}