corundum_framework_package/
start.rs1use std::fs;
2use std::path::Path;
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use corundum_framework_platform::dirs::AppDirs;
6use corundum_framework_platform::package;
7use semver::Version;
8use velopack::VelopackApp;
9
10use crate::backend::{self, Located};
11use crate::services::Services;
12
13#[cfg(windows)]
14use crate::cli_path;
15use crate::cli_path::CliSpec;
16
17const FIRSTRUN: &str = "VELOPACK_FIRSTRUN";
20
21const RESTART: &str = "VELOPACK_RESTART";
24
25static RESTARTED: AtomicBool = AtomicBool::new(false);
27
28const KEPT_VERSION: &str = "package-version";
30
31#[derive(Clone, Debug)]
33pub struct PackageSpec {
34 pub app_name: &'static str,
36 pub on_installed: Option<fn()>,
39 pub on_uninstalling: Option<fn()>,
42 pub before_apply: Option<fn() -> Result<(), String>>,
45 pub cli: Option<CliSpec>,
47 pub services: Services,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum Start {
56 NotPackaged,
58 FirstStart { version: Version },
60 Updated { from: Version, to: Version },
62 Same { version: Version },
64}
65
66pub fn start(spec: &PackageSpec) -> Start {
71 let firstrun = std::env::var_os(FIRSTRUN).is_some();
72 RESTARTED.store(std::env::var_os(RESTART).is_some(), Ordering::Relaxed);
73 let app = VelopackApp::build().set_auto_apply_on_startup(false);
76 let located = backend::locate();
79 let app = match &located {
80 Ok(Located {
81 config: Some(config),
82 ..
83 }) => app.set_locator(config.clone()),
84 _ => app,
85 };
86 #[cfg(windows)]
87 let app = windows_hooks(app, spec);
88 let mut app = app;
89 app.run();
90
91 #[cfg(feature = "service")]
94 if let Some(dirs) = AppDirs::new(spec.app_name) {
95 crate::services::restart_stopped(
96 spec,
97 dirs.data(),
98 &corundum_framework_service::Environment::detect(),
99 &corundum_framework_service::SystemManager,
100 );
101 }
102
103 if package::current().is_none() {
104 return Start::NotPackaged;
105 }
106 let now = match located {
107 Ok(Located { version, .. }) => version,
108 Err(e) => {
109 tracing::warn!("in a package, but its version could not be read: {e}");
110 return Start::NotPackaged;
111 }
112 };
113 let kept_path = AppDirs::new(spec.app_name).map(|dirs| dirs.data().join(KEPT_VERSION));
114 let kept = kept_path
117 .as_deref()
118 .and_then(|path| fs::read(path).ok())
119 .map(|bytes| String::from_utf8_lossy(&bytes).into_owned());
120 let start = classify(kept.as_deref(), firstrun, &now);
121 match &kept_path {
122 Some(path) => {
123 if let Err(e) = keep(path, &now) {
124 tracing::warn!("could not keep the version in {}: {e}", path.display());
125 }
126 }
127 None => tracing::warn!("no data directory to keep the version in"),
128 }
129 #[cfg(not(windows))]
132 if matches!(start, Start::FirstStart { .. })
133 && let Some(on_installed) = spec.on_installed
134 {
135 on_installed();
136 }
137 start
138}
139
140pub(crate) fn restarted() -> bool {
144 RESTARTED.load(Ordering::Relaxed)
145}
146
147#[cfg(windows)]
151fn windows_hooks<'a>(app: VelopackApp<'a>, spec: &PackageSpec) -> VelopackApp<'a> {
152 let on_installed = spec.on_installed;
153 let on_uninstalling = spec.on_uninstalling;
154 let cli = spec.cli;
155 #[cfg(feature = "service")]
156 let services = spec.clone();
157 app.on_after_install_fast_callback(move |_| {
159 if let Some(cli) = cli
160 && let Err(e) = cli_path::install(&cli)
161 {
162 tracing::warn!("could not put {} on PATH: {e}", cli.command);
163 }
164 if let Some(on_installed) = on_installed {
165 on_installed();
166 }
167 })
168 .on_before_uninstall_fast_callback(move |_| {
169 #[cfg(feature = "service")]
172 crate::services::uninstall_all(
173 &services,
174 &corundum_framework_service::Environment::detect(),
175 &corundum_framework_service::SystemManager,
176 );
177 if let Some(on_uninstalling) = on_uninstalling {
178 on_uninstalling();
179 }
180 if let Some(cli) = cli
181 && let Err(e) = cli_path::uninstall(&cli)
182 {
183 tracing::warn!("could not take {} off PATH: {e}", cli.command);
184 }
185 })
186}
187
188fn keep(path: &Path, version: &Version) -> std::io::Result<()> {
190 if let Some(dir) = path.parent() {
191 fs::create_dir_all(dir)?;
192 }
193 fs::write(path, version.to_string())
194}
195
196pub(crate) fn classify(kept: Option<&str>, firstrun: bool, now: &Version) -> Start {
200 match kept.and_then(|kept| Version::parse(kept.trim()).ok()) {
201 Some(from) if !firstrun => {
202 if from == *now {
203 Start::Same {
204 version: now.clone(),
205 }
206 } else {
207 Start::Updated {
208 from,
209 to: now.clone(),
210 }
211 }
212 }
213 _ => Start::FirstStart {
214 version: now.clone(),
215 },
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn v(s: &str) -> Version {
224 Version::parse(s).unwrap()
225 }
226
227 #[test]
228 fn a_start_is_told_from_the_kept_version_the_variable_and_the_package() {
229 let first = |now: &str| Start::FirstStart { version: v(now) };
230 let same = |now: &str| Start::Same { version: v(now) };
231 let updated = |from: &str, to: &str| Start::Updated {
232 from: v(from),
233 to: v(to),
234 };
235 let cases: &[(Option<&str>, bool, &str, Start)] = &[
236 (None, false, "1.0.0", first("1.0.0")),
237 (Some("1.0.0"), false, "1.0.0", same("1.0.0")),
238 (Some("1.0.0"), false, "1.1.0", updated("1.0.0", "1.1.0")),
239 (Some("1.1.0"), false, "1.0.0", updated("1.1.0", "1.0.0")),
240 (Some("1.0.0"), true, "1.0.0", first("1.0.0")),
241 (Some(""), false, "1.0.0", first("1.0.0")),
242 (Some("garbage"), false, "1.0.0", first("1.0.0")),
243 (Some(" 1.0.0\n"), false, "1.0.0", same("1.0.0")),
244 ];
245 for (kept, firstrun, now, want) in cases {
246 assert_eq!(
247 classify(*kept, *firstrun, &v(now)),
248 *want,
249 "kept {kept:?}, firstrun {firstrun}, now {now}"
250 );
251 }
252 }
253}