#[cfg(feature = "self-update")]
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, Sender};
#[cfg(feature = "self-update")]
use corundum_framework_update::Release;
use corundum_framework_update::{
InstallMethod, cargo_home, client, install_method, latest_release,
};
#[cfg(feature = "self-update")]
use semver::Version;
#[cfg(feature = "self-update")]
use super::panel::{AfterUpdate, UpdateTarget};
#[cfg(feature = "self-update")]
use super::state::RestartOutcome;
use super::state::{Event, has_asset};
use super::target::{DelegatedTarget, Target};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Job {
Check(usize),
Poll(usize),
Apply(usize),
#[cfg(feature = "self-update")]
Update(usize, Release, Option<Version>),
}
pub(crate) struct Worker {
pub(crate) jobs: Sender<Job>,
pub(crate) results: Receiver<(usize, Event)>,
}
pub(crate) fn spawn(targets: Arc<[Target]>, user_agent: String, ctx: egui::Context) -> Worker {
let (jobs, job_rx) = mpsc::channel();
let (result_tx, results) = mpsc::channel();
let spawned = std::thread::Builder::new()
.name("update worker".to_owned())
.spawn(move || {
let reply = |index: usize, event: Event| {
let _ = result_tx.send((index, event));
ctx.request_repaint();
};
run(&targets, &user_agent, &job_rx, reply);
});
if let Err(e) = spawned {
tracing::warn!("cannot start the update worker: {e}");
}
Worker { jobs, results }
}
fn run(targets: &[Target], user_agent: &str, jobs: &Receiver<Job>, reply: impl Fn(usize, Event)) {
let setup = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("no async runtime: {e}"))
.and_then(|runtime| {
client(user_agent)
.map(|client| (runtime, client))
.map_err(|e| e.to_string())
});
for job in jobs {
let index = job.index();
if let Some(package) = targets[index].package() {
for event in package_job(package, &job) {
reply(index, event);
}
continue;
}
let Some(target) = targets[index].binary() else {
continue;
};
match job {
Job::Check(index) => {
let found = match &setup {
Ok((runtime, client)) => runtime
.block_on(latest_release(client, &target.source))
.map_err(|e| e.to_string()),
Err(message) => Err(message.clone()),
};
let has_asset = match (&found, target.source.asset_base()) {
(Ok(release), Some(base)) => has_asset(release, base),
_ => false,
};
let method = method_of(&target.exe);
reply(
index,
Event::Checked {
found,
method,
has_asset,
},
);
}
#[cfg(feature = "self-update")]
Job::Update(index, release, current) => {
let installed = match &setup {
Ok((runtime, client)) => {
install(runtime, client, target, &release, current.as_ref())
}
Err(message) => Err(message.clone()),
};
let ok = installed.is_ok();
reply(index, Event::Downloaded(installed));
if ok {
reply(index, Event::Restarted(restart(target)));
}
}
Job::Poll(_) | Job::Apply(_) => {}
}
}
}
impl Job {
fn index(&self) -> usize {
match self {
Job::Check(index) | Job::Poll(index) | Job::Apply(index) => *index,
#[cfg(feature = "self-update")]
Job::Update(index, ..) => *index,
}
}
}
pub(crate) fn package_job(target: &dyn DelegatedTarget, job: &Job) -> Vec<Event> {
let status = || Event::Status(target.status());
match job {
Job::Check(_) => match target.check() {
Ok(()) => vec![status()],
Err(message) => vec![Event::Status(Err(message))],
},
Job::Poll(_) => vec![status()],
Job::Apply(_) => match target.apply() {
Ok(()) => vec![Event::Applied(Ok(())), status()],
Err(message) => vec![Event::Applied(Err(message))],
},
#[cfg(feature = "self-update")]
Job::Update(..) => Vec::new(),
}
}
fn resolved(exe: &Path) -> PathBuf {
exe.canonicalize().unwrap_or_else(|_| exe.to_path_buf())
}
fn method_of(exe: &Path) -> InstallMethod {
install_method(&resolved(exe), cargo_home().as_deref())
}
#[cfg(feature = "self-update")]
fn install(
runtime: &tokio::runtime::Runtime,
client: &reqwest::Client,
target: &UpdateTarget,
release: &Release,
current: Option<&Version>,
) -> Result<(), String> {
let exe = resolved(&target.exe);
let method = method_of(&exe);
if method != InstallMethod::Release {
return Err(format!(
"{} was not installed from a release",
exe.display()
));
}
if current.is_some_and(|current| release.version <= *current) {
return Err(format!(
"{} is not newer than the running version",
release.version
));
}
let bytes = runtime
.block_on(corundum_framework_update::download_asset(
client,
&target.source,
release,
))
.map_err(|e| e.to_string())?;
match target.after {
AfterUpdate::Relaunch => {
corundum_framework_update::install(&bytes, &exe, |new| self_replace::self_replace(new))
}
_ => corundum_framework_update::replace_file(&exe, &bytes),
}
.map_err(|e| e.to_string())
}
#[cfg(feature = "self-update")]
fn restart(target: &UpdateTarget) -> RestartOutcome {
match &target.after {
AfterUpdate::Relaunch => match relaunch(&target.exe) {
Ok(()) => RestartOutcome::Restarted,
Err(e) => RestartOutcome::Failed(format!("cannot start {}: {e}", target.exe.display())),
},
AfterUpdate::Restart(restart) => restart(),
AfterUpdate::Manual => RestartOutcome::ByHand,
}
}
#[cfg(feature = "self-update")]
pub(crate) fn relaunch(exe: &Path) -> io::Result<()> {
#[allow(clippy::zombie_processes)]
std::process::Command::new(exe)
.args(std::env::args_os().skip(1))
.spawn()
.map(drop)
}
#[cfg(all(test, feature = "self-update"))]
mod tests {
use super::*;
use corundum_framework_update::ReleaseSource;
#[test]
fn relaunching_a_missing_file_fails() {
let target = UpdateTarget {
label: "App".to_owned(),
current: None,
source: ReleaseSource::github("o", "r"),
exe: PathBuf::from("/nonexistent/app"),
after: AfterUpdate::Relaunch,
};
assert!(relaunch(&target.exe).is_err());
assert!(matches!(restart(&target), RestartOutcome::Failed(_)));
}
}
#[cfg(all(test, unix))]
mod resolve_tests {
use super::*;
#[test]
fn a_symlink_into_cargo_bin_is_not_a_release() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("cargo/bin");
std::fs::create_dir_all(&bin).unwrap();
std::fs::write(bin.join("app"), b"x").unwrap();
let link = dir.path().join("app");
std::os::unix::fs::symlink(bin.join("app"), &link).unwrap();
let home = dir.path().join("cargo").canonicalize().unwrap();
assert_eq!(install_method(&link, Some(&home)), InstallMethod::Release);
assert_eq!(
install_method(&resolved(&link), Some(&home)),
InstallMethod::Cargo
);
}
#[test]
fn an_unresolvable_path_stays() {
assert_eq!(
resolved(Path::new("/nonexistent/app")),
PathBuf::from("/nonexistent/app")
);
}
}