1use 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}