use std::fs;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use corundum_framework_platform::dirs::AppDirs;
use corundum_framework_platform::package;
use semver::Version;
use velopack::VelopackApp;
use crate::backend::{self, Located};
use crate::services::Services;
#[cfg(windows)]
use crate::cli_path;
use crate::cli_path::CliSpec;
const FIRSTRUN: &str = "VELOPACK_FIRSTRUN";
const RESTART: &str = "VELOPACK_RESTART";
static RESTARTED: AtomicBool = AtomicBool::new(false);
const KEPT_VERSION: &str = "package-version";
#[derive(Clone, Debug)]
pub struct PackageSpec {
pub app_name: &'static str,
pub on_installed: Option<fn()>,
pub on_uninstalling: Option<fn()>,
pub before_apply: Option<fn() -> Result<(), String>>,
pub cli: Option<CliSpec>,
pub services: Services,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Start {
NotPackaged,
FirstStart { version: Version },
Updated { from: Version, to: Version },
Same { version: Version },
}
pub fn start(spec: &PackageSpec) -> Start {
let firstrun = std::env::var_os(FIRSTRUN).is_some();
RESTARTED.store(std::env::var_os(RESTART).is_some(), Ordering::Relaxed);
let app = VelopackApp::build().set_auto_apply_on_startup(false);
let located = backend::locate();
let app = match &located {
Ok(Located {
config: Some(config),
..
}) => app.set_locator(config.clone()),
_ => app,
};
#[cfg(windows)]
let app = windows_hooks(app, spec);
let mut app = app;
app.run();
#[cfg(feature = "service")]
if let Some(dirs) = AppDirs::new(spec.app_name) {
crate::services::restart_stopped(
spec,
dirs.data(),
&corundum_framework_service::Environment::detect(),
&corundum_framework_service::SystemManager,
);
}
if package::current().is_none() {
return Start::NotPackaged;
}
let now = match located {
Ok(Located { version, .. }) => version,
Err(e) => {
tracing::warn!("in a package, but its version could not be read: {e}");
return Start::NotPackaged;
}
};
let kept_path = AppDirs::new(spec.app_name).map(|dirs| dirs.data().join(KEPT_VERSION));
let kept = kept_path
.as_deref()
.and_then(|path| fs::read(path).ok())
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned());
let start = classify(kept.as_deref(), firstrun, &now);
match &kept_path {
Some(path) => {
if let Err(e) = keep(path, &now) {
tracing::warn!("could not keep the version in {}: {e}", path.display());
}
}
None => tracing::warn!("no data directory to keep the version in"),
}
#[cfg(not(windows))]
if matches!(start, Start::FirstStart { .. })
&& let Some(on_installed) = spec.on_installed
{
on_installed();
}
start
}
pub(crate) fn restarted() -> bool {
RESTARTED.load(Ordering::Relaxed)
}
#[cfg(windows)]
fn windows_hooks<'a>(app: VelopackApp<'a>, spec: &PackageSpec) -> VelopackApp<'a> {
let on_installed = spec.on_installed;
let on_uninstalling = spec.on_uninstalling;
let cli = spec.cli;
#[cfg(feature = "service")]
let services = spec.clone();
app.on_after_install_fast_callback(move |_| {
if let Some(cli) = cli
&& let Err(e) = cli_path::install(&cli)
{
tracing::warn!("could not put {} on PATH: {e}", cli.command);
}
if let Some(on_installed) = on_installed {
on_installed();
}
})
.on_before_uninstall_fast_callback(move |_| {
#[cfg(feature = "service")]
crate::services::uninstall_all(
&services,
&corundum_framework_service::Environment::detect(),
&corundum_framework_service::SystemManager,
);
if let Some(on_uninstalling) = on_uninstalling {
on_uninstalling();
}
if let Some(cli) = cli
&& let Err(e) = cli_path::uninstall(&cli)
{
tracing::warn!("could not take {} off PATH: {e}", cli.command);
}
})
}
fn keep(path: &Path, version: &Version) -> std::io::Result<()> {
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
}
fs::write(path, version.to_string())
}
pub(crate) fn classify(kept: Option<&str>, firstrun: bool, now: &Version) -> Start {
match kept.and_then(|kept| Version::parse(kept.trim()).ok()) {
Some(from) if !firstrun => {
if from == *now {
Start::Same {
version: now.clone(),
}
} else {
Start::Updated {
from,
to: now.clone(),
}
}
}
_ => Start::FirstStart {
version: now.clone(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(s: &str) -> Version {
Version::parse(s).unwrap()
}
#[test]
fn a_start_is_told_from_the_kept_version_the_variable_and_the_package() {
let first = |now: &str| Start::FirstStart { version: v(now) };
let same = |now: &str| Start::Same { version: v(now) };
let updated = |from: &str, to: &str| Start::Updated {
from: v(from),
to: v(to),
};
let cases: &[(Option<&str>, bool, &str, Start)] = &[
(None, false, "1.0.0", first("1.0.0")),
(Some("1.0.0"), false, "1.0.0", same("1.0.0")),
(Some("1.0.0"), false, "1.1.0", updated("1.0.0", "1.1.0")),
(Some("1.1.0"), false, "1.0.0", updated("1.1.0", "1.0.0")),
(Some("1.0.0"), true, "1.0.0", first("1.0.0")),
(Some(""), false, "1.0.0", first("1.0.0")),
(Some("garbage"), false, "1.0.0", first("1.0.0")),
(Some(" 1.0.0\n"), false, "1.0.0", same("1.0.0")),
];
for (kept, firstrun, now, want) in cases {
assert_eq!(
classify(*kept, *firstrun, &v(now)),
*want,
"kept {kept:?}, firstrun {firstrun}, now {now}"
);
}
}
}