Skip to main content

node_headless/
update.rs

1//! Update-command and automatic-update policy.
2
3use std::{ffi::OsString, process::ExitCode};
4
5use wowlab_node::{UpdateError, check_for_update, install_update};
6
7use crate::VERSION;
8
9#[derive(Debug, thiserror::Error)]
10pub(crate) enum CommandError {
11    #[error("failed to check for updates")]
12    Check(#[source] UpdateError),
13    #[error("failed to install update")]
14    Install(#[source] UpdateError),
15}
16
17#[derive(Debug, thiserror::Error)]
18enum RestartError {
19    #[error("failed to get current executable path: {0}")]
20    CurrentExecutable(#[source] std::io::Error),
21    #[error("failed to launch replacement process: {0}")]
22    Launch(#[source] std::io::Error),
23}
24
25pub(crate) enum AutoUpdateAction {
26    Continue,
27    Exit(ExitCode),
28}
29
30impl CommandError {
31    pub(crate) fn log(&self) {
32        match self {
33            Self::Check(error) => tracing::error!(%error, "Failed to check for updates"),
34            Self::Install(error) => tracing::error!(%error, "Update failed"),
35        }
36    }
37}
38
39pub(crate) fn run_command(check: bool) -> Result<(), CommandError> {
40    if check {
41        let available = match check_for_update(VERSION) {
42            Ok(available) => available,
43            Err(error) => return Err(CommandError::Check(error)),
44        };
45
46        if let Some(version) = available {
47            tracing::info!(current = VERSION, latest = %version, "Update available");
48            tracing::info!("Run `node-headless update` to install");
49        } else {
50            tracing::info!(version = VERSION, "Already on latest version");
51        }
52    } else {
53        tracing::info!("Updating node-headless...");
54        let updated = match install_update("node-headless", VERSION) {
55            Ok(updated) => updated,
56            Err(error) => return Err(CommandError::Install(error)),
57        };
58
59        if updated {
60            tracing::info!("Updated successfully. Please restart.");
61        } else {
62            tracing::info!("Already on latest version.");
63        }
64    }
65
66    Ok(())
67}
68
69pub(crate) fn check_and_apply() -> AutoUpdateAction {
70    match check_for_update(VERSION) {
71        Ok(Some(new_version)) => {
72            tracing::info!(current = VERSION, latest = %new_version, "Update available");
73            tracing::info!("Downloading...");
74
75            match install_update("node-headless", VERSION) {
76                Ok(true) => {
77                    tracing::info!("Update installed. Restarting...");
78
79                    match restart() {
80                        Ok(code) => return AutoUpdateAction::Exit(code),
81                        Err(error) => {
82                            tracing::error!(%error, "Failed to restart");
83
84                            return AutoUpdateAction::Exit(ExitCode::FAILURE);
85                        }
86                    }
87                }
88                Ok(false) => {}
89                Err(error) => {
90                    tracing::warn!(%error, "Auto-update failed");
91                    tracing::warn!(
92                        "Continuing with current version. Run `node-headless update` manually."
93                    );
94                }
95            }
96        }
97        Ok(None) => {}
98        Err(error) => tracing::debug!(%error, "Update check failed"),
99    }
100
101    AutoUpdateAction::Continue
102}
103
104fn restart() -> Result<ExitCode, RestartError> {
105    let executable = match std::env::current_exe() {
106        Ok(executable) => executable,
107        Err(error) => return Err(RestartError::CurrentExecutable(error)),
108    };
109    let arguments = restart_arguments(std::env::args_os().skip(1));
110
111    #[cfg(unix)]
112    {
113        use std::os::unix::process::CommandExt;
114        let error = std::process::Command::new(&executable)
115            .args(&arguments)
116            .exec();
117
118        Err(RestartError::Launch(error))
119    }
120
121    #[cfg(windows)]
122    {
123        match std::process::Command::new(&executable)
124            .args(&arguments)
125            .spawn()
126        {
127            Ok(_) => Ok(ExitCode::SUCCESS),
128            Err(error) => Err(RestartError::Launch(error)),
129        }
130    }
131
132    #[cfg(not(any(unix, windows)))]
133    {
134        tracing::error!("Restart not supported on this platform. Please restart manually.");
135
136        Ok(ExitCode::SUCCESS)
137    }
138}
139
140fn restart_arguments(arguments: impl IntoIterator<Item = OsString>) -> Vec<OsString> {
141    let mut arguments: Vec<OsString> = arguments.into_iter().collect();
142
143    arguments.push(OsString::from("--no-update"));
144
145    arguments
146}
147
148#[cfg(test)]
149mod tests {
150    use googletest::prelude::*;
151
152    use super::*;
153
154    #[gtest]
155    fn restart_preserves_arguments_and_appends_update_guard() -> Result<()> {
156        let arguments = [OsString::from("run"), OsString::from("--verbose")];
157
158        let restarted = restart_arguments(arguments);
159
160        verify_eq!(
161            restarted,
162            [
163                OsString::from("run"),
164                OsString::from("--verbose"),
165                OsString::from("--no-update")
166            ]
167        )
168    }
169}