merge: 同步 main 到 feat/home
This commit is contained in:
commit
0aa901ad48
43 files changed
+8621
-10862
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
# The actual version is set by the theseus-build workflow on tagging
|
||||
version = "1.0.2-beta11"
|
||||
version = "1.0.2-beta13"
|
||||
edition.workspace = true
|
||||
description = "Axolotl Launcher is a cross-platform Minecraft launcher"
|
||||
homepage = "https://www.ghs.red"
|
||||
|
||||
@@ -43,6 +43,8 @@ struct UpdatePreferences {
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
mod update_policy;
|
||||
#[cfg(feature = "updater")]
|
||||
mod updater_impl;
|
||||
#[cfg(not(feature = "updater"))]
|
||||
@@ -1024,7 +1026,8 @@ fn main() {
|
||||
s.restart_after_pending_update.load(Ordering::Relaxed)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if let Some((update, data)) = &*update_data.0.lock().unwrap()
|
||||
if let Some((update, data)) =
|
||||
&update_data.0.lock().unwrap().pending
|
||||
{
|
||||
fn set_changelog_toast(version: Option<String>) {
|
||||
let toast_result: theseus::Result<()> = tauri::async_runtime::block_on(async move {
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
use semver::Version;
|
||||
|
||||
pub fn requires_update(current: &Version, target: &Version) -> bool {
|
||||
current != target
|
||||
}
|
||||
|
||||
pub fn is_rollback(current: &Version, target: &Version) -> bool {
|
||||
requires_update(current, target) && !is_remote_newer(current, target)
|
||||
}
|
||||
|
||||
pub struct UpdateTarget<T> {
|
||||
target: Option<String>,
|
||||
check_revision: u64,
|
||||
pub pending: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for UpdateTarget<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: None,
|
||||
check_revision: 0,
|
||||
pending: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UpdateTarget<T> {
|
||||
pub fn begin_check(&mut self) -> u64 {
|
||||
self.check_revision = self.check_revision.wrapping_add(1);
|
||||
self.check_revision
|
||||
}
|
||||
|
||||
pub fn select_checked(
|
||||
&mut self,
|
||||
revision: u64,
|
||||
version: Option<&str>,
|
||||
) -> bool {
|
||||
if revision != self.check_revision {
|
||||
return false;
|
||||
}
|
||||
self.select(version);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn select(&mut self, version: Option<&str>) {
|
||||
if self.target.as_deref() != version {
|
||||
self.target = version.map(str::to_owned);
|
||||
self.pending = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_selected(&self, version: &str) -> bool {
|
||||
self.target.as_deref() == Some(version)
|
||||
}
|
||||
|
||||
pub fn enqueue(&mut self, version: &str, download: T) -> bool {
|
||||
if !self.is_selected(version) {
|
||||
return false;
|
||||
}
|
||||
self.pending = Some(download);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// semver compares prerelease identifiers lexically, so "beta10" < "beta9"
|
||||
// (because the character '1' sorts before '9'). Our release names use a
|
||||
// "betaN" scheme where N is a decimal counter, so a strict lexical compare
|
||||
// would wrongly report an older beta as the latest. Use a comparator that
|
||||
// treats the trailing digits of a prerelease identifier numerically.
|
||||
fn is_remote_newer(current: &Version, remote: &Version) -> bool {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
let core = (remote.major, remote.minor, remote.patch).cmp(&(
|
||||
current.major,
|
||||
current.minor,
|
||||
current.patch,
|
||||
));
|
||||
if core != Ordering::Equal {
|
||||
return core == Ordering::Greater;
|
||||
}
|
||||
|
||||
// Same x.y.z. A final release outranks any prerelease of that version.
|
||||
match (current.pre.is_empty(), remote.pre.is_empty()) {
|
||||
(true, true) => false,
|
||||
(true, false) => false,
|
||||
(false, true) => true,
|
||||
(false, false) => {
|
||||
compare_prerelease(remote.pre.as_str(), current.pre.as_str())
|
||||
== Ordering::Greater
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a prerelease identifier into its non-digit prefix and an optional
|
||||
/// trailing decimal number (so "beta10" -> ("beta", Some(10))).
|
||||
fn split_prerelease_identifier(segment: &str) -> (&str, Option<u64>) {
|
||||
let digit_start = segment
|
||||
.char_indices()
|
||||
.find(|(_, c)| c.is_ascii_digit())
|
||||
.map(|(i, _)| i);
|
||||
match digit_start {
|
||||
Some(i) => (&segment[..i], segment[i..].parse::<u64>().ok()),
|
||||
None => (segment, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two prerelease strings, treating a trailing run of ASCII digits
|
||||
/// as a decimal number (so "beta10" > "beta9"). All other segments fall back
|
||||
/// to a plain lexical comparison.
|
||||
fn compare_prerelease(left: &str, right: &str) -> std::cmp::Ordering {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
let left_segments: Vec<&str> = left.split('.').collect();
|
||||
let right_segments: Vec<&str> = right.split('.').collect();
|
||||
|
||||
for (l, r) in left_segments.iter().zip(right_segments.iter()) {
|
||||
let (l_prefix, l_num) = split_prerelease_identifier(l);
|
||||
let (r_prefix, r_num) = split_prerelease_identifier(r);
|
||||
|
||||
match (l_num, r_num) {
|
||||
(Some(ln), Some(rn)) if l_prefix == r_prefix => match ln.cmp(&rn) {
|
||||
Ordering::Equal => continue,
|
||||
other => return other,
|
||||
},
|
||||
_ => {
|
||||
let prefix_cmp = l_prefix.cmp(r_prefix);
|
||||
if prefix_cmp != Ordering::Equal {
|
||||
return prefix_cmp;
|
||||
}
|
||||
let num_cmp = l_num.cmp(&r_num);
|
||||
if num_cmp != Ordering::Equal {
|
||||
return num_cmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
left_segments.len().cmp(&right_segments.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use semver::Version;
|
||||
|
||||
fn version(value: &str) -> Version {
|
||||
Version::parse(value).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follows_the_server_target_in_both_directions() {
|
||||
for (current, target) in [
|
||||
("1.0.2-beta12", "1.0.2-beta9"),
|
||||
("1.0.2-beta9", "1.0.2-beta12"),
|
||||
("1.2.0", "1.1.0"),
|
||||
("1.1.0", "1.2.0"),
|
||||
("1.1.0", "1.1.0-beta1"),
|
||||
] {
|
||||
assert!(requires_update(&version(current), &version(target)));
|
||||
}
|
||||
assert!(!requires_update(
|
||||
&version("1.0.2-beta12"),
|
||||
&version("1.0.2-beta12")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_direction_uses_numeric_beta_suffixes() {
|
||||
assert!(is_rollback(
|
||||
&version("1.0.2-beta12"),
|
||||
&version("1.0.2-beta9")
|
||||
));
|
||||
assert!(!is_rollback(
|
||||
&version("1.0.2-beta9"),
|
||||
&version("1.0.2-beta12")
|
||||
));
|
||||
assert!(is_rollback(&version("1.2.0"), &version("1.1.0")));
|
||||
assert!(!is_rollback(&version("1.1.0"), &version("1.2.0")));
|
||||
assert!(!is_rollback(&version("1.2.0"), &version("1.2.0")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_the_target_discards_an_already_downloaded_update() {
|
||||
let mut state = UpdateTarget::default();
|
||||
state.select(Some("1.0.2-beta12"));
|
||||
assert!(state.enqueue("1.0.2-beta12", "new installer"));
|
||||
state.select(Some("1.0.2-beta9"));
|
||||
assert!(state.pending.is_none());
|
||||
assert!(state.enqueue("1.0.2-beta9", "rollback installer"));
|
||||
assert_eq!(state.pending, Some("rollback installer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stale_download_cannot_replace_the_selected_rollback() {
|
||||
let mut state = UpdateTarget::default();
|
||||
state.select(Some("1.0.2-beta12"));
|
||||
state.select(Some("1.0.2-beta9"));
|
||||
assert!(state.enqueue("1.0.2-beta9", "rollback installer"));
|
||||
assert!(!state.enqueue("1.0.2-beta12", "late installer"));
|
||||
assert_eq!(state.pending, Some("rollback installer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_checks_cannot_reauthorize_a_superseded_version() {
|
||||
let mut state = UpdateTarget::default();
|
||||
let old_check = state.begin_check();
|
||||
let rollback_check = state.begin_check();
|
||||
assert!(state.select_checked(rollback_check, Some("1.0.2-beta9")));
|
||||
assert!(state.enqueue("1.0.2-beta9", "rollback installer"));
|
||||
assert!(!state.select_checked(old_check, Some("1.0.2-beta12")));
|
||||
assert!(!state.enqueue("1.0.2-beta12", "late installer"));
|
||||
assert_eq!(state.pending, Some("rollback installer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_checks_preserve_the_matching_download_and_no_update_clears_it()
|
||||
{
|
||||
let mut state = UpdateTarget::default();
|
||||
state.select(Some("1.0.2-beta9"));
|
||||
state.enqueue("1.0.2-beta9", "rollback installer");
|
||||
state.select(Some("1.0.2-beta9"));
|
||||
assert_eq!(state.pending, Some("rollback installer"));
|
||||
state.select(None);
|
||||
assert!(state.pending.is_none());
|
||||
assert!(!state.enqueue("1.0.2-beta9", "late installer"));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::api::Result;
|
||||
use crate::update_policy::{UpdateTarget, is_rollback, requires_update};
|
||||
use futures::StreamExt;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -27,82 +28,6 @@ const STARLIGHT_UPDATE_BASE_URL: &str = "https://skin.starlight.cool/";
|
||||
const UPDATE_DOWNLOAD_TIMEOUT: std::time::Duration =
|
||||
std::time::Duration::from_secs(15 * 60);
|
||||
|
||||
// semver compares prerelease identifiers lexically, so "beta10" < "beta9"
|
||||
// (because the character '1' sorts before '9'). Our release names use a
|
||||
// "betaN" scheme where N is a decimal counter, so a strict lexical compare
|
||||
// would wrongly report an older beta as the latest. Use a comparator that
|
||||
// treats the trailing digits of a prerelease identifier numerically.
|
||||
fn is_remote_newer(current: &Version, remote: &Version) -> bool {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
let core = (remote.major, remote.minor, remote.patch).cmp(&(
|
||||
current.major,
|
||||
current.minor,
|
||||
current.patch,
|
||||
));
|
||||
if core != Ordering::Equal {
|
||||
return core == Ordering::Greater;
|
||||
}
|
||||
|
||||
// Same x.y.z. A final release outranks any prerelease of that version.
|
||||
match (current.pre.is_empty(), remote.pre.is_empty()) {
|
||||
(true, true) => false,
|
||||
(true, false) => false,
|
||||
(false, true) => true,
|
||||
(false, false) => {
|
||||
compare_prerelease(remote.pre.as_str(), current.pre.as_str())
|
||||
== Ordering::Greater
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a prerelease identifier into its non-digit prefix and an optional
|
||||
/// trailing decimal number (so "beta10" -> ("beta", Some(10))).
|
||||
fn split_prerelease_identifier(segment: &str) -> (&str, Option<u64>) {
|
||||
let digit_start = segment
|
||||
.char_indices()
|
||||
.find(|(_, c)| c.is_ascii_digit())
|
||||
.map(|(i, _)| i);
|
||||
match digit_start {
|
||||
Some(i) => (&segment[..i], segment[i..].parse::<u64>().ok()),
|
||||
None => (segment, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two prerelease strings, treating a trailing run of ASCII digits
|
||||
/// as a decimal number (so "beta10" > "beta9"). All other segments fall back
|
||||
/// to a plain lexical comparison.
|
||||
fn compare_prerelease(left: &str, right: &str) -> std::cmp::Ordering {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
let left_segments: Vec<&str> = left.split('.').collect();
|
||||
let right_segments: Vec<&str> = right.split('.').collect();
|
||||
|
||||
for (l, r) in left_segments.iter().zip(right_segments.iter()) {
|
||||
let (l_prefix, l_num) = split_prerelease_identifier(l);
|
||||
let (r_prefix, r_num) = split_prerelease_identifier(r);
|
||||
|
||||
match (l_num, r_num) {
|
||||
(Some(ln), Some(rn)) if l_prefix == r_prefix => match ln.cmp(&rn) {
|
||||
Ordering::Equal => continue,
|
||||
other => return other,
|
||||
},
|
||||
_ => {
|
||||
let prefix_cmp = l_prefix.cmp(r_prefix);
|
||||
if prefix_cmp != Ordering::Equal {
|
||||
return prefix_cmp;
|
||||
}
|
||||
let num_cmp = l_num.cmp(&r_num);
|
||||
if num_cmp != Ordering::Equal {
|
||||
return num_cmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
left_segments.len().cmp(&right_segments.len())
|
||||
}
|
||||
|
||||
// ── Shared types ─────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -115,11 +40,12 @@ pub struct UpdateMetadata {
|
||||
body: Option<String>,
|
||||
published_at: Option<String>,
|
||||
force_update: bool,
|
||||
rollback: bool,
|
||||
raw_json: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PendingUpdateData(pub Mutex<Option<(Arc<Update>, Vec<u8>)>>);
|
||||
pub struct PendingUpdateData(pub Mutex<UpdateTarget<(Arc<Update>, Vec<u8>)>>);
|
||||
|
||||
// ── Update Server API types ─────────────────────────────────────
|
||||
|
||||
@@ -307,7 +233,7 @@ async fn check_with_endpoints<R: Runtime>(
|
||||
.header("X-Axolotl-Platform", platform)?
|
||||
.header("X-Axolotl-Version", current_version)?
|
||||
.version_comparator(|current, release| {
|
||||
is_remote_newer(¤t, &release.version)
|
||||
requires_update(¤t, &release.version)
|
||||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -345,7 +271,20 @@ async fn check_with_updater<R: Runtime>(
|
||||
webview: &Webview<R>,
|
||||
channel: &str,
|
||||
) -> Result<Option<UpdateMetadata>> {
|
||||
let Some(mut update) = check_with_endpoints(webview, channel).await? else {
|
||||
let pending_data = webview.state::<PendingUpdateData>();
|
||||
let revision = pending_data.0.lock().unwrap().begin_check();
|
||||
let checked = check_with_endpoints(webview, channel).await?;
|
||||
if !pending_data.0.lock().unwrap().select_checked(
|
||||
revision,
|
||||
checked.as_ref().map(|update| update.version.as_str()),
|
||||
) {
|
||||
return Err(theseus::Error::from(theseus::ErrorKind::OtherError(
|
||||
"A newer launcher update check superseded this response"
|
||||
.to_string(),
|
||||
))
|
||||
.into());
|
||||
}
|
||||
let Some(mut update) = checked else {
|
||||
return Ok(None);
|
||||
};
|
||||
update.timeout = Some(UPDATE_DOWNLOAD_TIMEOUT);
|
||||
@@ -369,6 +308,10 @@ async fn check_with_updater<R: Runtime>(
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let metadata = UpdateMetadata {
|
||||
rollback: is_rollback(
|
||||
&Version::parse(&update.current_version).map_err(Error::from)?,
|
||||
&Version::parse(&update.version).map_err(Error::from)?,
|
||||
),
|
||||
rid: webview.resources_table().add(update.clone()),
|
||||
current_version: update.current_version.clone(),
|
||||
version: update.version.clone(),
|
||||
@@ -449,6 +392,12 @@ pub async fn enqueue_update_for_installation<R: Runtime>(
|
||||
|
||||
let update = webview.resources_table().get::<Update>(rid)?;
|
||||
|
||||
if !pending_data.0.lock().unwrap().is_selected(&update.version) {
|
||||
return Err(theseus::Error::from(theseus::ErrorKind::OtherError(
|
||||
"The server-selected launcher version changed; check for updates again".to_string(),
|
||||
)).into());
|
||||
}
|
||||
|
||||
let progress = init_loading(
|
||||
LoadingBarType::LauncherUpdate {
|
||||
version: update.version.clone(),
|
||||
@@ -565,11 +514,19 @@ pub async fn enqueue_update_for_installation<R: Runtime>(
|
||||
let download_duration = download_start.elapsed();
|
||||
tracing::info!("Downloaded update in {download_duration:?}");
|
||||
|
||||
pending_data
|
||||
let version = update.version.clone();
|
||||
if !pending_data
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace((update, update_data));
|
||||
.enqueue(&version, (update, update_data))
|
||||
{
|
||||
return Err(theseus::Error::from(theseus::ErrorKind::OtherError(
|
||||
"The server-selected launcher version changed during download"
|
||||
.to_string(),
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -577,7 +534,9 @@ pub async fn enqueue_update_for_installation<R: Runtime>(
|
||||
#[tauri::command]
|
||||
pub fn remove_enqueued_update<R: Runtime>(webview: Webview<R>) {
|
||||
let pending_data = webview.state::<PendingUpdateData>().inner();
|
||||
pending_data.0.lock().unwrap().take();
|
||||
let mut pending = pending_data.0.lock().unwrap();
|
||||
pending.begin_check();
|
||||
pending.select(None);
|
||||
}
|
||||
|
||||
// ── Debian / derivatives apt update ─────────────────────────────
|
||||
@@ -630,6 +589,7 @@ pub async fn install_apt_package(version: &str, data: &[u8]) -> Result<()> {
|
||||
.arg("apt")
|
||||
.arg("install")
|
||||
.arg("-y")
|
||||
.arg("--allow-downgrades")
|
||||
.arg(&install_path)
|
||||
.output()
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"icons/AxolotlAssets.car"
|
||||
],
|
||||
"windows": {
|
||||
"allowDowngrades": true,
|
||||
"nsis": {
|
||||
"installMode": "currentUser",
|
||||
"installerHooks": "./nsis/hooks.nsi"
|
||||
|
||||
Reference in new issue
Block a user