use crate::archive;
use crate::error::LxAppError;
use crate::lxapp::metadata::{LxAppRecord, SemanticVersion};
use crate::lxapp::{
self, LINGXIA_DIR, LXAPPS_DIR, ReleaseType, STORAGE_DIR, USER_CACHE_DIR, USER_DATA_DIR,
lxapp_fingermark, metadata, version::Version,
};
use crate::provider::{LxAppUpdateQuery, UpdatePackageInfo, UpdateTarget};
use crate::publish_app_event;
use dashmap::DashMap;
use lingxia_messaging::{CallbackResult, get_callback, remove_callback};
use lingxia_platform::Platform;
use lingxia_platform::traits::app_runtime::AppRuntime;
use lingxia_platform::traits::update::UpdateService;
use rong_http::{self as service_executor, BodySink};
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::watch;
use tokio::time::sleep;
struct ProgressSink {
total_bytes: u64,
downloaded_bytes: u64,
last_reported_progress: i32,
runtime: Option<Arc<Platform>>,
}
impl ProgressSink {
fn new(total_bytes: u64, runtime: Option<Arc<Platform>>) -> Self {
Self {
total_bytes,
downloaded_bytes: 0,
last_reported_progress: 0,
runtime,
}
}
}
impl BodySink for ProgressSink {
fn write(&mut self, chunk: &[u8]) -> Result<(), String> {
self.downloaded_bytes += chunk.len() as u64;
if self.total_bytes > 0 {
let progress =
((self.downloaded_bytes as f64 / self.total_bytes as f64) * 100.0) as i32;
let progress = progress.min(100);
if progress > self.last_reported_progress {
self.last_reported_progress = progress;
if let Some(runtime) = &self.runtime {
let _ = runtime.update_download_progress(progress);
}
}
}
Ok(())
}
fn close(&mut self, result: &Result<(), String>) {
if result.is_ok() {
if let Some(runtime) = &self.runtime {
let _ = runtime.update_download_progress(100);
}
}
}
}
#[derive(Clone)]
pub struct UpdateManager {
lxapp: Arc<lxapp::LxApp>,
downloads_dir: PathBuf,
}
#[derive(Clone, Debug)]
pub struct DownloadedUpdateInfo {
pub version: String,
pub archive_path: PathBuf,
}
#[derive(Clone)]
pub enum OtaUpdateTarget {
App {
runtime: Arc<Platform>,
current_version: Option<String>,
},
LxApp {
target_appid: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum ForceUpdateDownloadState {
Downloading { version: String },
Completed,
Failed(String),
}
struct ForceUpdateDownloadTracker {
downloads: DashMap<String, watch::Sender<ForceUpdateDownloadState>>,
}
impl ForceUpdateDownloadTracker {
fn new() -> Self {
Self {
downloads: DashMap::new(),
}
}
fn try_start_download(
&self,
key: &str,
version: &str,
) -> Option<watch::Receiver<ForceUpdateDownloadState>> {
use dashmap::mapref::entry::Entry;
match self.downloads.entry(key.to_string()) {
Entry::Occupied(_) => None,
Entry::Vacant(entry) => {
let initial = ForceUpdateDownloadState::Downloading {
version: version.to_string(),
};
let (tx, rx) = watch::channel(initial);
entry.insert(tx);
Some(rx)
}
}
}
fn mark_completed(&self, key: &str) {
if let Some(entry) = self.downloads.get(key) {
let _ = entry.send(ForceUpdateDownloadState::Completed);
}
self.downloads.remove(key);
}
fn mark_failed(&self, key: &str, error: String) {
if let Some(entry) = self.downloads.get(key) {
let _ = entry.send(ForceUpdateDownloadState::Failed(error));
}
self.downloads.remove(key);
}
fn wait_for_download(&self, key: &str) -> Option<watch::Receiver<ForceUpdateDownloadState>> {
self.downloads.get(key).map(|entry| entry.subscribe())
}
fn state(&self, key: &str) -> Option<ForceUpdateDownloadState> {
self.downloads.get(key).map(|entry| entry.borrow().clone())
}
}
static FORCE_UPDATE_DOWNLOAD_TRACKER: OnceLock<ForceUpdateDownloadTracker> = OnceLock::new();
const UPDATE_CHECK_NEXT_AT_PREFIX: &str = "update_check_next_at:";
const APP_UPDATE_START_DELAY: Duration = Duration::from_secs(15);
const UPDATE_CHECK_COOLDOWN_SECS: i64 = 6 * 60 * 60;
fn force_update_tracker() -> &'static ForceUpdateDownloadTracker {
FORCE_UPDATE_DOWNLOAD_TRACKER.get_or_init(ForceUpdateDownloadTracker::new)
}
fn force_update_download_key(lxappid: &str, release_type: ReleaseType) -> String {
format!("{}@{}", lxappid, release_type.as_str())
}
fn unix_now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn update_check_next_at(target: &str) -> Option<i64> {
metadata::app_meta_get(&format!("{}{}", UPDATE_CHECK_NEXT_AT_PREFIX, target))
.ok()
.flatten()
.and_then(|s| s.parse::<i64>().ok())
}
fn set_update_check_next_at(target: &str, ts: i64) -> Result<(), LxAppError> {
metadata::app_meta_set(
&format!("{}{}", UPDATE_CHECK_NEXT_AT_PREFIX, target),
&ts.to_string(),
)
}
fn try_acquire_update_check_window(target: &str) -> bool {
let now = unix_now();
if let Some(next_check_at) = update_check_next_at(target)
&& now < next_check_at
{
crate::info!(
"Skip update check due to cooldown: target={} next_check_at={} now={}",
target,
next_check_at,
now
);
return false;
}
if let Err(err) = set_update_check_next_at(target, now + UPDATE_CHECK_COOLDOWN_SECS) {
crate::warn!(
"Failed to persist update-check cooldown for target {}: {}",
target,
err
);
}
true
}
fn ensure_runtime_version_compatible(
lxappid: &str,
pkg: &UpdatePackageInfo,
) -> Result<(), LxAppError> {
let Some(required_runtime_version) = pkg
.required_runtime_version
.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
else {
return Ok(());
};
let current = Version::parse(crate::SDK_RUNTIME_VERSION).map_err(|_| {
LxAppError::Runtime(format!(
"invalid SDK runtime version '{}'",
crate::SDK_RUNTIME_VERSION
))
})?;
let required = Version::parse(required_runtime_version).map_err(|_| {
LxAppError::UnsupportedOperation(format!(
"invalid minRuntimeVersion '{}' from update metadata for {}@{}",
required_runtime_version, lxappid, pkg.version
))
})?;
if current < required {
return Err(LxAppError::UnsupportedOperation(format!(
"LxApp '{}' update {} requires runtime >= {}, current SDK runtime is {}; update host app first",
lxappid, pkg.version, required, current
)));
}
Ok(())
}
pub fn is_force_update_downloading(lxappid: &str, release_type: ReleaseType) -> bool {
matches!(
force_update_tracker().state(&force_update_download_key(lxappid, release_type)),
Some(ForceUpdateDownloadState::Downloading { .. })
)
}
impl UpdateManager {
pub fn handle_ota_update(target: OtaUpdateTarget) {
match target {
OtaUpdateTarget::App {
runtime,
current_version,
} => {
Self::spawn_app_update_flow_internal(runtime, current_version, Duration::ZERO, true)
}
OtaUpdateTarget::LxApp { target_appid } => {
let release_type = ReleaseType::Release;
let context_lxapp = lxapp::try_get(&target_appid)
.filter(|app| app.release_type == release_type)
.or_else(|| {
crate::app::app_config()
.and_then(|config| lxapp::try_get(&config.home_lxapp_appid))
});
let Some(context_lxapp) = context_lxapp else {
crate::warn!(
"No available lxapp context for OTA-triggered update check: {}@{}",
target_appid,
release_type.as_str()
);
return;
};
let current_version = lxapp::try_get(&target_appid)
.filter(|app| app.release_type == release_type)
.map(|app| app.current_version());
Self::spawn_background_update_check_internal(
context_lxapp,
target_appid,
release_type,
current_version,
true,
);
}
}
}
fn spawn_app_update_flow_internal(
runtime: Arc<Platform>,
current_version: Option<String>,
start_delay: Duration,
bypass_cooldown: bool,
) {
let _ = rong::bg::spawn(async move {
if !start_delay.is_zero() {
sleep(start_delay).await;
}
if !bypass_cooldown && !try_acquire_update_check_window("app") {
return;
}
let result =
UpdateManager::check_and_install_app_update(runtime, current_version.as_deref())
.await;
if let Err(err) = result {
crate::warn!("App update flow failed: {}", err);
}
});
}
fn spawn_background_update_check_internal(
context_lxapp: Arc<lxapp::LxApp>,
target_appid: String,
release_type: ReleaseType,
current_version: Option<String>,
bypass_cooldown: bool,
) {
let update_check_target = format!("lxapp:{}@{}", target_appid, release_type.as_str());
let _ = rong::bg::spawn(async move {
if !bypass_cooldown && !try_acquire_update_check_window(&update_check_target) {
return;
}
let manager = UpdateManager::new(context_lxapp);
let current_version = current_version.or_else(|| {
manager
.installed_version(&target_appid, release_type)
.ok()
.flatten()
});
match manager
.check_latest_update(&target_appid, release_type, current_version.as_deref())
.await
{
Ok(Some(pkg)) => {
if !manager.should_update(&target_appid, release_type, &pkg.version) {
return;
}
if let Err(err) = ensure_runtime_version_compatible(&target_appid, &pkg) {
let payload = serde_json::json!({
"version": pkg.version,
"isForceUpdate": pkg.is_force_update,
"releaseType": release_type.as_str(),
"minRuntimeVersion": pkg.required_runtime_version,
"currentRuntimeVersion": crate::SDK_RUNTIME_VERSION,
"error": err.to_string(),
});
let _ = publish_app_event(
&target_appid,
"UpdateFailed",
Some(payload.to_string()),
);
return;
}
let already_downloaded_same = matches!(
manager.has_downloaded_update(&target_appid, release_type),
Ok(Some(info)) if info.version == pkg.version && info.archive_path.exists()
);
if already_downloaded_same {
crate::info!(
"Update package already downloaded; emitting UpdateReady directly (version={})",
pkg.version
)
.with_appid(target_appid.clone());
let payload = serde_json::json!({
"version": pkg.version,
"isForceUpdate": pkg.is_force_update,
"releaseType": release_type.as_str(),
});
let _ = publish_app_event(
&target_appid,
"UpdateReady",
Some(payload.to_string()),
);
return;
}
let download_res = manager
.download_archive_with_checksum(
&target_appid,
release_type,
&pkg.url,
&pkg.checksum_sha256,
&pkg.version,
)
.await;
if download_res.is_ok() {
let payload = serde_json::json!({
"version": pkg.version,
"isForceUpdate": pkg.is_force_update,
"releaseType": release_type.as_str(),
});
let _ = publish_app_event(
&target_appid,
"UpdateReady",
Some(payload.to_string()),
);
} else {
let payload = serde_json::json!({
"version": pkg.version,
"isForceUpdate": pkg.is_force_update,
"releaseType": release_type.as_str(),
"error": download_res.err().map(|e| e.to_string()).unwrap_or_else(|| "download failed".to_string()),
});
let _ = publish_app_event(
&target_appid,
"UpdateFailed",
Some(payload.to_string()),
);
}
}
Ok(None) => {}
Err(_) => {}
}
});
}
pub fn new(lxapp: Arc<lxapp::LxApp>) -> Self {
let downloads_dir = lxapp
.runtime
.app_cache_dir()
.join(LINGXIA_DIR)
.join(LXAPPS_DIR)
.join("download");
let _ = fs::create_dir_all(&downloads_dir);
Self {
lxapp,
downloads_dir,
}
}
pub(crate) fn apply_downloaded_update(
runtime: Arc<Platform>,
lxappid: &str,
release_type: ReleaseType,
) -> Result<(), LxAppError> {
let downloaded = match metadata::downloaded_get(lxappid, release_type)? {
Some(rec) => rec,
None => return Ok(()),
};
let archive_path = PathBuf::from(&downloaded.zip_path);
if !archive_path.exists() {
metadata::downloaded_remove(lxappid, release_type)?;
return Ok(());
}
let previous_path =
metadata::get(lxappid, release_type)?.map(|rec| PathBuf::from(rec.install_path));
let install_path =
Self::install_archive_to_dir(&runtime, lxappid, release_type, &archive_path)?;
Self::record_install_metadata(
lxappid,
release_type,
&downloaded.version.to_string(),
&install_path,
)?;
if let Some(prev) = previous_path
&& prev.exists()
&& prev != install_path
{
let _ = fs::remove_dir_all(&prev);
}
let _ = metadata::downloaded_remove(lxappid, release_type);
Ok(())
}
pub async fn check_update(
&self,
lxappid: &str,
release_type: ReleaseType,
query: LxAppUpdateQuery,
) -> Result<Option<UpdatePackageInfo>, LxAppError> {
let provider = crate::get_provider();
let target = UpdateTarget::LxApp {
id: lxappid.to_string(),
release_type,
query,
};
provider.check_update(target).await.map_err(|e| {
crate::error!("check_update failed: {}", e).with_appid(lxappid);
e.to_lxapp_error()
})
}
async fn check_latest_update(
&self,
lxappid: &str,
release_type: ReleaseType,
current_version: Option<&str>,
) -> Result<Option<UpdatePackageInfo>, LxAppError> {
self.check_update(
lxappid,
release_type,
LxAppUpdateQuery::Latest {
current_version: current_version.map(|v| v.to_string()),
},
)
.await
}
async fn check_exact_update(
&self,
lxappid: &str,
release_type: ReleaseType,
target_version: &str,
) -> Result<Option<UpdatePackageInfo>, LxAppError> {
self.check_update(
lxappid,
release_type,
LxAppUpdateQuery::TargetVersion(target_version.to_string()),
)
.await
}
pub async fn check_app_update(
current_version: Option<&str>,
) -> Result<Option<UpdatePackageInfo>, LxAppError> {
let provider = crate::get_provider();
let target = UpdateTarget::App {
current_version: current_version.map(|v| v.to_string()),
};
provider.check_update(target).await.map_err(|e| {
crate::error!("check_app_update failed: {}", e);
e.to_lxapp_error()
})
}
pub fn spawn_app_update_flow(runtime: Arc<Platform>, current_version: Option<String>) {
Self::spawn_app_update_flow_internal(
runtime,
current_version,
APP_UPDATE_START_DELAY,
false,
);
}
pub fn spawn_release_lxapp_update_check(target_appid: String) {
let release_type = ReleaseType::Release;
let Some(lxapp) = lxapp::try_get(&target_appid) else {
crate::warn!(
"LxApp '{}' not found for background update check",
target_appid
);
return;
};
if lxapp.release_type != release_type {
return;
}
UpdateManager::spawn_background_update_check_internal(
lxapp.clone(),
target_appid,
release_type,
Some(lxapp.current_version()),
false,
);
}
pub fn spawn_background_update_check(lxapp: Arc<lxapp::LxApp>) {
if lxapp.release_type != ReleaseType::Release {
return;
}
let target_appid = lxapp.appid.clone();
let release_type = lxapp.release_type;
UpdateManager::spawn_background_update_check_internal(
lxapp.clone(),
target_appid,
release_type,
Some(lxapp.current_version()),
false,
);
}
pub async fn check_and_install_app_update(
runtime: Arc<Platform>,
current_version: Option<&str>,
) -> Result<(), LxAppError> {
crate::info!(
"App update flow start: current_version={:?}",
current_version
);
let update = UpdateManager::check_app_update(current_version).await?;
let Some(pkg) = update else {
crate::info!("No app update available");
return Ok(());
};
crate::info!(
"App update available: version={} url={}",
pkg.version,
pkg.url
);
let update_info_json = {
let mut json_obj = serde_json::Map::new();
json_obj.insert("version".to_string(), serde_json::json!(&pkg.version));
json_obj.insert(
"isForceUpdate".to_string(),
serde_json::json!(pkg.is_force_update),
);
if let Some(size) = pkg.size {
json_obj.insert("size".to_string(), serde_json::json!(size));
}
if let Some(notes) = &pkg.release_notes {
json_obj.insert("releaseNotes".to_string(), serde_json::json!(notes));
}
Some(serde_json::to_string(&json_obj).unwrap_or_default())
};
let (callback_id, receiver) = get_callback();
if let Err(e) = runtime.show_update_prompt(callback_id, update_info_json.as_deref()) {
let _ = remove_callback(callback_id);
return Err(LxAppError::Runtime(format!(
"Failed to show update prompt: {}",
e
)));
}
let confirmed = match receiver.await {
Ok(CallbackResult::Success(data)) => serde_json::from_str::<Value>(&data)
.ok()
.and_then(|json| json.get("confirm").and_then(|v| v.as_bool()))
.unwrap_or(false),
Ok(CallbackResult::Error(_)) => false,
Err(_) => false,
};
if !confirmed && pkg.is_force_update {
return Err(LxAppError::Runtime(
"Forced app update was not confirmed".to_string(),
));
}
if !confirmed {
crate::info!("App update cancelled or deferred");
return Ok(());
}
crate::info!("App update confirmed, starting download");
let path = UpdateManager::download_app_update_with_checksum(
runtime.clone(),
&pkg.url,
&pkg.checksum_sha256,
&pkg.version,
)
.await?;
crate::info!("App update downloaded: {}", path.display());
runtime.install_update(&path).map_err(|e| {
LxAppError::Runtime(format!("Failed to request app update install: {}", e))
})?;
crate::info!("App update install requested");
Ok(())
}
pub fn should_update(
&self,
lxappid: &str,
release_type: ReleaseType,
server_version: &str,
) -> bool {
let installed = crate::lxapp::metadata::get(lxappid, release_type)
.ok()
.flatten()
.map(|rec| rec.version_string());
match installed {
Some(v) => v != server_version,
None => true,
}
}
pub fn has_downloaded_update(
&self,
lxappid: &str,
release_type: ReleaseType,
) -> Result<Option<DownloadedUpdateInfo>, LxAppError> {
Ok(
metadata::downloaded_get(lxappid, release_type)?.map(|rec| DownloadedUpdateInfo {
version: rec.version.to_version_string(),
archive_path: PathBuf::from(rec.zip_path),
}),
)
}
pub fn installed_version(
&self,
lxappid: &str,
release_type: ReleaseType,
) -> Result<Option<String>, LxAppError> {
Ok(metadata::get(lxappid, release_type)?.map(|rec| rec.version_string()))
}
pub fn is_installed(
&self,
lxappid: &str,
release_type: ReleaseType,
) -> Result<bool, LxAppError> {
let Some(record) = metadata::get(lxappid, release_type)? else {
return Ok(false);
};
let install_path_str = record.install_path.trim();
let install_path = Path::new(install_path_str);
let config_path = install_path.join("lxapp.json");
let is_valid =
!install_path_str.is_empty() && install_path.is_dir() && config_path.is_file();
if is_valid {
return Ok(true);
}
crate::warn!(
"Stale installed metadata detected (release_type={}, install_path={}); treating as not installed",
release_type,
record.install_path
)
.with_appid(lxappid);
let _ = metadata::remove(lxappid, release_type);
Ok(false)
}
pub fn install_from_assets(
runtime: Arc<Platform>,
lxappid: &str,
version: &str,
) -> Result<PathBuf, LxAppError> {
let dir_name = lxapp_fingermark(lxappid, ReleaseType::Release);
let destination = runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(LXAPPS_DIR)
.join(&dir_name);
if destination.exists() {
fs::remove_dir_all(&destination)?;
}
fs::create_dir_all(&destination)?;
for entry in runtime.asset_dir_iter(lxappid) {
let entry = entry?;
let rel_path = entry
.path
.strip_prefix(&format!("{}/", lxappid))
.unwrap_or(&entry.path);
let target = destination.join(rel_path);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
let mut reader = entry.reader;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
fs::write(&target, buffer)?;
}
Self::record_install_metadata(lxappid, ReleaseType::Release, version, &destination)?;
Ok(destination)
}
pub fn apply_update_archive(
&self,
lxappid: &str,
release_type: ReleaseType,
version: &str,
archive_path: &Path,
) -> Result<(), LxAppError> {
let previous_path =
metadata::get(lxappid, release_type)?.map(|rec| PathBuf::from(rec.install_path));
let install_path =
Self::install_archive_to_dir(&self.lxapp.runtime, lxappid, release_type, archive_path)?;
if let Err(e) = Self::record_install_metadata(lxappid, release_type, version, &install_path)
{
if let Err(cleanup_err) = fs::remove_dir_all(&install_path) {
crate::error!(
"Failed to rollback new installation at {}: {}",
install_path.display(),
cleanup_err
)
.with_appid(lxappid);
}
return Err(e);
}
if let Some(prev) = previous_path
&& prev.exists()
&& prev != install_path
&& let Err(e) = fs::remove_dir_all(&prev)
{
crate::warn!(
"Failed to remove old installation at {}: {}. Manual cleanup may be needed.",
prev.display(),
e
)
.with_appid(lxappid);
}
if let Err(e) = metadata::downloaded_remove(lxappid, release_type) {
crate::warn!(
"Failed to clean up download metadata and archive for {}:{:?}: {}",
lxappid,
release_type,
e
)
.with_appid(lxappid);
}
Ok(())
}
fn install_archive_to_dir(
runtime: &Arc<Platform>,
lxappid: &str,
release_type: ReleaseType,
archive_path: &Path,
) -> Result<PathBuf, LxAppError> {
let dir_name = lxapp_fingermark(lxappid, release_type);
let destination = runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(LXAPPS_DIR)
.join(dir_name);
archive::extract_tar_zst(archive_path, &destination)?;
Ok(destination)
}
fn uninstall_installed(
&self,
lxappid: &str,
release_type: ReleaseType,
) -> Result<(), LxAppError> {
if crate::lxapp::is_lxapp_open(lxappid) {
return Err(LxAppError::UnsupportedOperation(
"cannot uninstall an opened app".to_string(),
));
}
if let Some(rec) = metadata::get(lxappid, release_type)? {
let dir_name = rec.fingermark;
let pkg_dir = self
.lxapp
.runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(LXAPPS_DIR)
.join(&dir_name);
if pkg_dir.exists() {
fs::remove_dir_all(&pkg_dir)?;
}
let data_dir = self
.lxapp
.runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(USER_DATA_DIR)
.join(&dir_name);
if data_dir.exists() {
let _ = fs::remove_dir_all(&data_dir);
}
let cache_dir = self
.lxapp
.runtime
.app_cache_dir()
.join(LINGXIA_DIR)
.join(LXAPPS_DIR)
.join(USER_CACHE_DIR)
.join(&dir_name);
if cache_dir.exists() {
let _ = fs::remove_dir_all(&cache_dir);
}
}
if let Some(rec) = metadata::get(lxappid, release_type)? {
let storage_file = self
.lxapp
.runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(STORAGE_DIR)
.join(format!("{}.redb", rec.fingermark));
if storage_file.exists() {
let _ = fs::remove_file(&storage_file);
}
}
Ok(())
}
pub fn uninstall_all(&self, lxappid: &str) -> Result<(), LxAppError> {
if crate::lxapp::is_lxapp_open(lxappid) {
return Err(LxAppError::UnsupportedOperation(
"cannot uninstall an opened app".to_string(),
));
}
let _ = self.uninstall_installed(lxappid, ReleaseType::Release);
let _ = self.uninstall_installed(lxappid, ReleaseType::Preview);
let _ = self.uninstall_installed(lxappid, ReleaseType::Developer);
let _ = metadata::remove_all(lxappid);
Ok(())
}
pub async fn download_archive_with_checksum(
&self,
lxappid: &str,
release_type: ReleaseType,
url: &str,
checksum_sha256: &str,
version: &str,
) -> Result<PathBuf, LxAppError> {
let dest = self.dest_path_for_url(url);
if dest.exists() {
let _ = fs::remove_file(&dest);
}
let receiver =
service_executor::request_download(url.to_string(), dest.clone(), None, None)
.map_err(|e| LxAppError::IoError(format!("failed to start download: {}", e)))?;
match receiver
.await
.map_err(|_| LxAppError::IoError("download task cancelled".to_string()))?
{
Ok(()) => {
if !checksum_sha256.is_empty() {
if let Err(e) = archive::verify_sha256(&dest, checksum_sha256) {
let _ = fs::remove_file(&dest);
return Err(e);
}
}
if let Err(e) = metadata::downloaded_upsert(lxappid, release_type, version, &dest) {
let _ = fs::remove_file(&dest);
return Err(LxAppError::IoError(format!(
"failed to record downloaded update: {}",
e
)));
}
crate::info!(
"Recorded downloaded update: appid={}, release_type={}, version={}, archive={}",
lxappid,
release_type,
version,
dest.display()
)
.with_appid(lxappid);
Ok(dest)
}
Err(err) => {
let _ = fs::remove_file(&dest);
Err(LxAppError::IoError(format!("download failed: {}", err)))
}
}
}
fn dest_path_for_url(&self, url: &str) -> PathBuf {
let name = filename_from_url_or_hash(url);
self.downloads_dir.join(name)
}
pub async fn download_app_update_with_checksum(
runtime: Arc<Platform>,
url: &str,
checksum_sha256: &str,
version: &str,
) -> Result<PathBuf, LxAppError> {
crate::info!("App update download start: url={} version={}", url, version);
let dest_dir = runtime
.app_cache_dir()
.join(LINGXIA_DIR)
.join("app_updates");
let _ = fs::create_dir_all(&dest_dir);
let dest = dest_dir.join(app_update_filename(url, version));
crate::info!("App update download dest: {}", dest.display());
if dest.exists() {
if checksum_sha256.is_empty() {
if dest.metadata().map(|m| m.len()).unwrap_or(0) > 0 {
crate::info!("App update package already downloaded: {}", dest.display());
let _ = runtime.dismiss_download_progress();
return Ok(dest);
}
let _ = fs::remove_file(&dest);
}
if archive::verify_sha256(&dest, checksum_sha256).is_ok() {
crate::info!(
"App update package already downloaded and verified: {}",
dest.display()
);
let _ = runtime.dismiss_download_progress();
return Ok(dest);
}
let _ = fs::remove_file(&dest);
}
let file_size = get_content_length(url).await.unwrap_or(0);
if let Err(e) = runtime.show_download_progress() {
crate::warn!("Failed to show download progress: {}", e);
}
let sink: Option<Box<dyn BodySink>> = if file_size > 0 {
Some(Box::new(ProgressSink::new(
file_size,
Some(runtime.clone()),
)))
} else {
None
};
let receiver =
match service_executor::request_download(url.to_string(), dest.clone(), None, sink) {
Ok(receiver) => receiver,
Err(e) => {
let _ = runtime.dismiss_download_progress();
return Err(LxAppError::IoError(format!(
"failed to start download: {}",
e
)));
}
};
let result = match receiver
.await
.map_err(|_| LxAppError::IoError("download task cancelled".to_string()))?
{
Ok(()) => {
if !checksum_sha256.is_empty() {
if let Err(e) = archive::verify_sha256(&dest, checksum_sha256) {
let _ = fs::remove_file(&dest);
Err(e)
} else {
Ok(dest)
}
} else {
Ok(dest)
}
}
Err(err) => {
let _ = fs::remove_file(&dest);
Err(LxAppError::IoError(format!("download failed: {}", err)))
}
};
let _ = runtime.dismiss_download_progress();
result
}
fn hash_url(url: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
url.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
fn record_install_metadata(
lxappid: &str,
release_type: ReleaseType,
version: &str,
install_path: &Path,
) -> Result<(), LxAppError> {
let installed_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or_default();
let fingermark = lxapp_fingermark(lxappid, release_type);
let parsed_version = Version::parse(version).map_err(|_| {
LxAppError::InvalidParameter(format!("Invalid semantic version: {}", version))
})?;
let record = LxAppRecord::new(
lxappid,
release_type,
SemanticVersion::from_version(&parsed_version),
fingermark,
install_path.to_string_lossy().to_string(),
installed_at,
);
metadata::upsert(&record)
}
}
pub(crate) async fn ensure_first_install(
current_lxapp: &Arc<lxapp::LxApp>,
target_appid: &str,
release_type: ReleaseType,
) -> Result<(), LxAppError> {
if release_type != ReleaseType::Release {
return Ok(());
}
let manager = UpdateManager::new(current_lxapp.clone());
if manager.is_installed(target_appid, release_type)? {
return Ok(());
}
let pkg = manager
.check_latest_update(target_appid, release_type, None)
.await?
.ok_or_else(|| {
LxAppError::ResourceNotFound(format!(
"No package available for first install of {}",
target_appid
))
})?;
ensure_runtime_version_compatible(target_appid, &pkg)?;
let _archive = manager
.download_archive_with_checksum(
target_appid,
release_type,
&pkg.url,
&pkg.checksum_sha256,
&pkg.version,
)
.await?;
Ok(())
}
pub(crate) async fn ensure_target_version_ready(
current_lxapp: &Arc<lxapp::LxApp>,
target_appid: &str,
release_type: ReleaseType,
target_version: &str,
) -> Result<(), LxAppError> {
let target_version = target_version.trim();
if target_version.is_empty() {
return Err(LxAppError::InvalidParameter(
"targetVersion cannot be empty".to_string(),
));
}
let target_semver = Version::parse(target_version).map_err(|_| {
LxAppError::InvalidParameter(format!(
"targetVersion must be semantic version: {}",
target_version
))
})?;
let manager = UpdateManager::new(current_lxapp.clone());
let is_installed = manager.is_installed(target_appid, release_type)?;
let current_version = if is_installed {
manager.installed_version(target_appid, release_type)?
} else {
None
};
if release_type == ReleaseType::Release {
match manager
.check_latest_update(target_appid, release_type, current_version.as_deref())
.await
{
Ok(Some(pkg)) if pkg.is_force_update => {
let force_version = Version::parse(&pkg.version).map_err(|_| {
LxAppError::UnsupportedOperation(format!(
"invalid forced update version '{}' for {}",
pkg.version, target_appid
))
})?;
if target_semver < force_version {
return Err(LxAppError::UnsupportedOperation(format!(
"targetVersion {} is lower than required forced version {} for {} ({})",
target_version,
pkg.version,
target_appid,
release_type.as_str()
)));
}
}
Ok(_) => {}
Err(err) => {
crate::warn!(
"targetVersion force-update check failed (fail-open): {}",
err
)
.with_appid(target_appid.to_string());
}
}
}
if current_version.as_deref() == Some(target_version) {
return Ok(());
}
let pkg = manager
.check_exact_update(target_appid, release_type, target_version)
.await?
.ok_or_else(|| {
LxAppError::ResourceNotFound(format!(
"No package available for {}@{} ({})",
target_appid,
target_version,
release_type.as_str()
))
})?;
ensure_runtime_version_compatible(target_appid, &pkg)?;
let already_downloaded_same = matches!(
manager.has_downloaded_update(target_appid, release_type),
Ok(Some(info)) if info.version == pkg.version && info.archive_path.exists()
);
if already_downloaded_same {
return Ok(());
}
manager
.download_archive_with_checksum(
target_appid,
release_type,
&pkg.url,
&pkg.checksum_sha256,
&pkg.version,
)
.await?;
Ok(())
}
pub async fn ensure_force_update_for_installed(
current_lxapp: &Arc<lxapp::LxApp>,
target_appid: &str,
release_type: ReleaseType,
) -> Result<(), LxAppError> {
if release_type != ReleaseType::Release {
return Ok(());
}
let manager = UpdateManager::new(current_lxapp.clone());
if !manager.is_installed(target_appid, release_type)? {
return Ok(());
}
let current_version = manager.installed_version(target_appid, release_type)?;
let Some(current_version) = current_version else {
crate::warn!("Installed lxapp has no recorded version; skip force-update gating")
.with_appid(target_appid.to_string());
return Ok(());
};
let update = match manager
.check_latest_update(target_appid, release_type, Some(current_version.as_str()))
.await
{
Ok(update) => update,
Err(err) => {
crate::warn!("force-update check failed (fail-open): {}", err)
.with_appid(target_appid.to_string());
return Ok(());
}
};
let Some(pkg) = update else {
return Ok(());
};
if let Err(err) = ensure_runtime_version_compatible(target_appid, &pkg) {
if pkg.is_force_update {
return Err(err);
}
crate::warn!("optional update blocked by runtime version gate: {}", err)
.with_appid(target_appid.to_string());
return Ok(());
}
if !pkg.is_force_update || pkg.version == current_version {
return Ok(());
}
let already_downloaded_same = matches!(
manager.has_downloaded_update(target_appid, release_type),
Ok(Some(info)) if info.version == pkg.version && info.archive_path.exists()
);
if already_downloaded_same {
return Ok(());
}
let key = force_update_download_key(target_appid, release_type);
loop {
if let Some(mut rx) = force_update_tracker().try_start_download(&key, &pkg.version) {
let manager_bg = manager.clone();
let key_bg = key.clone();
let target_appid_bg = target_appid.to_string();
let url_bg = pkg.url.clone();
let checksum_bg = pkg.checksum_sha256.clone();
let version_bg = pkg.version.clone();
let _ = rong::bg::spawn(async move {
let result = manager_bg
.download_archive_with_checksum(
&target_appid_bg,
release_type,
&url_bg,
&checksum_bg,
&version_bg,
)
.await;
match result {
Ok(_) => force_update_tracker().mark_completed(&key_bg),
Err(err) => force_update_tracker().mark_failed(&key_bg, err.to_string()),
}
});
loop {
let state = { rx.borrow().clone() };
match state {
ForceUpdateDownloadState::Downloading { .. } => {
if rx.changed().await.is_err() {
break;
}
}
ForceUpdateDownloadState::Completed => return Ok(()),
ForceUpdateDownloadState::Failed(error) => {
return Err(LxAppError::IoError(format!(
"forced update package download failed: {}",
error
)));
}
}
}
}
if let Some(mut rx) = force_update_tracker().wait_for_download(&key) {
loop {
let state = { rx.borrow().clone() };
match state {
ForceUpdateDownloadState::Downloading { .. } => {
if rx.changed().await.is_err() {
break;
}
}
ForceUpdateDownloadState::Completed => return Ok(()),
ForceUpdateDownloadState::Failed(error) => {
return Err(LxAppError::IoError(format!(
"forced update package download failed: {}",
error
)));
}
}
}
}
let prepared = matches!(
manager.has_downloaded_update(target_appid, release_type),
Ok(Some(info)) if info.version == pkg.version && info.archive_path.exists()
);
if prepared {
return Ok(());
}
tokio::task::yield_now().await;
}
}
fn filename_from_url_or_hash(url: &str) -> String {
let main = url.split(&['?', '#'][..]).next().unwrap_or(url);
let seg = main.rsplit('/').next().unwrap_or(main);
if !seg.is_empty() && seg.contains('.') {
seg.to_string()
} else {
format!("{}.tar.zst", UpdateManager::hash_url(url))
}
}
fn app_update_filename(url: &str, version: &str) -> String {
let safe_version = version.replace(['/', '\\'], "_");
let main = url.split(&['?', '#'][..]).next().unwrap_or(url);
let seg = main.rsplit('/').next().unwrap_or(main);
if !seg.is_empty() && seg.contains('.') {
format!("app_{}_{}", safe_version, seg)
} else {
format!("app_{}_{}.apk", safe_version, UpdateManager::hash_url(url))
}
}
async fn get_content_length(url: &str) -> Result<u64, String> {
use http::Request;
use http_body_util::{BodyExt, Empty};
use std::io::Error;
let request = Request::builder()
.method("HEAD")
.uri(url)
.body(
Empty::<bytes::Bytes>::new()
.map_err(|_| Error::new(std::io::ErrorKind::Other, "body error"))
.boxed(),
)
.map_err(|e| format!("Failed to build HEAD request: {}", e))?;
let response = service_executor::send_request(request, 1024, None)
.await
.map_err(|e| format!("HEAD request failed: {}", e))?;
if let Some(content_length) = response
.headers
.get(http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
{
Ok(content_length)
} else {
Err("No Content-Length header".to_string())
}
}