Skip to content

Commit 4437ff0

Browse files
lcompleteclaude
andcommitted
feat(tauri): add backend-driven Tauri update check and improve server auto-update scheduling
- Add check_tauri_update Tauri command that fetches the latest.json manifest from GitHub releases and delegates to the updater plugin, bypassing the hardcoded endpoint limitation - Persist server JAR release version in a sidecar version file so the displayed version survives JAR upgrades that strip metadata - Replace one-shot server auto-update flag with a proper interval scheduler (24h, persisted via localStorage) - Move update status alerts inline with their respective settings sections; show current app/JAR versions next to each heading - Fix Maven release build to pass -Drevision so the packaged JAR carries the correct version string Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c23f1ac commit 4437ff0

4 files changed

Lines changed: 350 additions & 101 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ jobs:
9898
- name: Build with Maven
9999
run: |
100100
cd app/server
101-
mvn clean package
101+
mvn -Drevision=${{ steps.get_version.outputs.version-without-v }} clean package
102102
env:
103103
CI: false
104104

app/tauri/src-tauri/src/lib.rs

Lines changed: 168 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use reqwest::StatusCode;
1+
use reqwest::{StatusCode, Url};
22
use std::fs::File;
33
use std::io::{BufRead, BufReader};
44
#[cfg(target_os = "windows")]
@@ -12,21 +12,25 @@ use tauri::{
1212
command,
1313
menu::{MenuBuilder, MenuItemBuilder},
1414
tray::{MouseButton, MouseButtonState, TrayIconEvent},
15-
AppHandle, Manager, RunEvent, WindowEvent,
15+
AppHandle, Manager, ResourceId, RunEvent, Webview, WindowEvent,
1616
};
1717
use tauri_plugin_autostart::MacosLauncher;
18+
use tauri_plugin_updater::UpdaterExt;
1819

1920
#[macro_use]
2021
extern crate lazy_static;
2122

2223
#[cfg(target_os = "windows")]
2324
const CREATE_NO_WINDOW: u32 = 0x08000000;
2425
const SERVER_JAR_FILE_NAME: &str = "huntly-server.jar";
26+
const SERVER_JAR_VERSION_FILE_NAME: &str = "huntly-server.version";
2527
const SERVER_JAR_RESOURCE_PATH: &str = "server_bin/huntly-server.jar";
2628
const SERVER_JAR_DATA_DIR: &str = "server_bin";
2729
const GITHUB_RELEASES_API: &str =
2830
"https://api.github.com/repos/lcomplete/huntly/releases?per_page=100";
2931
const GITHUB_USER_AGENT: &str = "Huntly-Tauri";
32+
const TAURI_RELEASE_TAG_PREFIX: &str = "tauri/v";
33+
const TAURI_UPDATE_MANIFEST_FILE_NAME: &str = "latest.json";
3034

3135
lazy_static! {
3236
static ref SPRING_BOOT_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
@@ -89,6 +93,17 @@ struct ServerUpdateInfo {
8993
release_url: Option<String>,
9094
}
9195

96+
#[derive(serde::Serialize)]
97+
#[serde(rename_all = "camelCase")]
98+
struct TauriUpdateMetadata {
99+
rid: ResourceId,
100+
current_version: String,
101+
version: String,
102+
date: Option<String>,
103+
body: Option<String>,
104+
raw_json: serde_json::Value,
105+
}
106+
92107
#[derive(Clone)]
93108
struct ServerRelease {
94109
version: String,
@@ -219,7 +234,7 @@ fn collect_server_info(app: &AppHandle) -> ServerInfo {
219234
let jar_path_buf = active_server_jar_path(app);
220235

221236
let java_version = java_path_buf.as_ref().and_then(read_java_version);
222-
let jar_version = jar_path_buf.as_ref().and_then(read_server_jar_version);
237+
let jar_version = current_server_jar_version(app);
223238

224239
ServerInfo {
225240
jar_version,
@@ -271,6 +286,52 @@ fn writable_server_jar_path(app: &AppHandle) -> Result<PathBuf, String> {
271286
Ok(server_bin_dir.join(SERVER_JAR_FILE_NAME))
272287
}
273288

289+
fn writable_server_jar_version_path(app: &AppHandle) -> Result<PathBuf, String> {
290+
let app_data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
291+
let server_bin_dir = app_data_dir.join(SERVER_JAR_DATA_DIR);
292+
std::fs::create_dir_all(&server_bin_dir).map_err(|e| e.to_string())?;
293+
Ok(server_bin_dir.join(SERVER_JAR_VERSION_FILE_NAME))
294+
}
295+
296+
fn current_server_jar_version(app: &AppHandle) -> Option<String> {
297+
let jar_path = active_server_jar_path(app)?;
298+
if is_writable_server_jar_path(app, &jar_path) {
299+
return read_server_jar_release_version(app).or_else(|| read_server_jar_version(&jar_path));
300+
}
301+
302+
read_server_jar_version(&jar_path)
303+
}
304+
305+
fn is_writable_server_jar_path(app: &AppHandle, jar_path: &Path) -> bool {
306+
app.path()
307+
.app_data_dir()
308+
.map(|app_data_dir| {
309+
jar_path
310+
== app_data_dir
311+
.join(SERVER_JAR_DATA_DIR)
312+
.join(SERVER_JAR_FILE_NAME)
313+
})
314+
.unwrap_or(false)
315+
}
316+
317+
fn read_server_jar_release_version(app: &AppHandle) -> Option<String> {
318+
let version_path = app
319+
.path()
320+
.app_data_dir()
321+
.ok()?
322+
.join(SERVER_JAR_DATA_DIR)
323+
.join(SERVER_JAR_VERSION_FILE_NAME);
324+
let version = std::fs::read_to_string(version_path)
325+
.ok()?
326+
.trim()
327+
.to_string();
328+
if version.is_empty() {
329+
None
330+
} else {
331+
Some(version)
332+
}
333+
}
334+
274335
fn read_java_version(java_path: &PathBuf) -> Option<String> {
275336
let output = Command::new(java_path).arg("-version").output().ok()?;
276337
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -315,8 +376,7 @@ fn read_server_jar_version(jar_path: &PathBuf) -> Option<String> {
315376

316377
#[command]
317378
async fn check_server_update(app: AppHandle) -> Result<ServerUpdateInfo, String> {
318-
let current_version =
319-
active_server_jar_path(&app).and_then(|path| read_server_jar_version(&path));
379+
let current_version = current_server_jar_version(&app);
320380
let release = fetch_latest_server_release().await?;
321381
let available = current_version
322382
.as_deref()
@@ -342,8 +402,7 @@ async fn install_server_update(app: AppHandle) -> Result<ServerInfo, String> {
342402
return Err("Server bundle updates are disabled by HUNTLY_NO_SERVER_JAR.".to_string());
343403
}
344404

345-
let current_version =
346-
active_server_jar_path(&app).and_then(|path| read_server_jar_version(&path));
405+
let current_version = current_server_jar_version(&app);
347406
let release = fetch_latest_server_release().await?;
348407
let available = current_version
349408
.as_deref()
@@ -369,12 +428,15 @@ async fn install_server_update(app: AppHandle) -> Result<ServerInfo, String> {
369428
let dest_path = writable_server_jar_path(&app)?;
370429
let temp_path = dest_path.with_extension("jar.download");
371430
let backup_path = dest_path.with_extension("jar.bak");
431+
let version_path = writable_server_jar_version_path(&app)?;
372432

373433
std::fs::write(&temp_path, bytes.as_ref()).map_err(|e| e.to_string())?;
374434

375435
let downloaded_version = read_server_jar_version(&temp_path)
376436
.ok_or_else(|| "Downloaded server JAR does not include version metadata.".to_string())?;
377-
if normalize_version(&downloaded_version) != normalize_version(&release.version) {
437+
if normalize_version(&downloaded_version) != normalize_version(&release.version)
438+
&& !is_server_jar_asset_for_version(&release.asset.name, &release.version)
439+
{
378440
let _ = std::fs::remove_file(&temp_path);
379441
return Err(format!(
380442
"Downloaded server JAR version {} does not match release version {}.",
@@ -401,12 +463,102 @@ async fn install_server_update(app: AppHandle) -> Result<ServerInfo, String> {
401463
let _ = std::fs::remove_file(&backup_path);
402464
}
403465

466+
std::fs::write(&version_path, format!("{}\n", release.version)).map_err(|e| e.to_string())?;
467+
404468
Ok(collect_server_info(&app))
405469
}
406470

471+
#[command]
472+
async fn check_tauri_update(webview: Webview) -> Result<Option<TauriUpdateMetadata>, String> {
473+
let manifest_url = match fetch_latest_tauri_update_manifest_url().await? {
474+
Some(manifest_url) => manifest_url,
475+
None => return Ok(None),
476+
};
477+
478+
let endpoint = Url::parse(&manifest_url).map_err(|e| e.to_string())?;
479+
let updater = webview
480+
.updater_builder()
481+
.endpoints(vec![endpoint])
482+
.map_err(|e| e.to_string())?
483+
.build()
484+
.map_err(|e| e.to_string())?;
485+
486+
let update = match updater.check().await {
487+
Ok(update) => update,
488+
Err(tauri_plugin_updater::Error::ReleaseNotFound) => return Ok(None),
489+
Err(e) => return Err(e.to_string()),
490+
};
491+
492+
if let Some(update) = update {
493+
let current_version = update.current_version.clone();
494+
let version = update.version.clone();
495+
let date = update
496+
.raw_json
497+
.get("pub_date")
498+
.and_then(|value| value.as_str())
499+
.map(ToString::to_string);
500+
let body = update.body.clone();
501+
let raw_json = update.raw_json.clone();
502+
let rid = webview.resources_table().add(update);
503+
504+
Ok(Some(TauriUpdateMetadata {
505+
rid,
506+
current_version,
507+
version,
508+
date,
509+
body,
510+
raw_json,
511+
}))
512+
} else {
513+
Ok(None)
514+
}
515+
}
516+
517+
async fn fetch_latest_tauri_update_manifest_url() -> Result<Option<String>, String> {
518+
let releases = fetch_github_releases().await?;
519+
Ok(releases.into_iter().find_map(tauri_update_manifest_url))
520+
}
521+
522+
fn tauri_update_manifest_url(release: GithubRelease) -> Option<String> {
523+
if release.draft || release.prerelease || tauri_release_version(&release.tag_name).is_none() {
524+
return None;
525+
}
526+
527+
release
528+
.assets
529+
.into_iter()
530+
.find(|asset| asset.name == TAURI_UPDATE_MANIFEST_FILE_NAME)
531+
.map(|asset| asset.browser_download_url)
532+
}
533+
534+
fn tauri_release_version(tag_name: &str) -> Option<String> {
535+
let version = tag_name.strip_prefix(TAURI_RELEASE_TAG_PREFIX)?;
536+
let version_parts: Vec<&str> = version.split('.').collect();
537+
if version_parts.len() < 3
538+
|| !version_parts.iter().take(3).all(|part| {
539+
part.chars()
540+
.next()
541+
.map(|ch| ch.is_ascii_digit())
542+
.unwrap_or(false)
543+
})
544+
{
545+
return None;
546+
}
547+
Some(version.to_string())
548+
}
549+
407550
async fn fetch_latest_server_release() -> Result<ServerRelease, String> {
551+
let releases = fetch_github_releases().await?;
552+
553+
releases
554+
.into_iter()
555+
.find_map(main_server_release)
556+
.ok_or_else(|| "No main Huntly release with a server JAR asset was found.".to_string())
557+
}
558+
559+
async fn fetch_github_releases() -> Result<Vec<GithubRelease>, String> {
408560
let client = github_client()?;
409-
let releases: Vec<GithubRelease> = client
561+
client
410562
.get(GITHUB_RELEASES_API)
411563
.send()
412564
.await
@@ -415,12 +567,7 @@ async fn fetch_latest_server_release() -> Result<ServerRelease, String> {
415567
.map_err(|e| e.to_string())?
416568
.json()
417569
.await
418-
.map_err(|e| e.to_string())?;
419-
420-
releases
421-
.into_iter()
422-
.find_map(main_server_release)
423-
.ok_or_else(|| "No main Huntly release with a server JAR asset was found.".to_string())
570+
.map_err(|e| e.to_string())
424571
}
425572

426573
fn github_client() -> Result<reqwest::Client, String> {
@@ -471,9 +618,11 @@ fn main_release_version(tag_name: &str) -> Option<String> {
471618
}
472619

473620
fn is_server_jar_asset(asset_name: &str, version: &str) -> bool {
474-
asset_name == SERVER_JAR_FILE_NAME
475-
|| asset_name == format!("huntly-server-{}.jar", version)
476-
|| (asset_name.starts_with("huntly-server-") && asset_name.ends_with(".jar"))
621+
asset_name == SERVER_JAR_FILE_NAME || is_server_jar_asset_for_version(asset_name, version)
622+
}
623+
624+
fn is_server_jar_asset_for_version(asset_name: &str, version: &str) -> bool {
625+
asset_name == format!("huntly-server-{}.jar", normalize_version(version))
477626
}
478627

479628
fn normalize_version(version: &str) -> String {
@@ -906,6 +1055,7 @@ pub fn run() {
9061055
read_settings,
9071056
has_server_jar,
9081057
get_server_info,
1058+
check_tauri_update,
9091059
check_server_update,
9101060
install_server_update,
9111061
set_tray_visible,

0 commit comments

Comments
 (0)