use dashmap::DashMap;
use http::Uri as HttpUri;
use lingxia_platform::Platform;
use lingxia_platform::traits::app_runtime::AppRuntime;
use std::collections::{HashMap, VecDeque};
use std::fs;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
use tokio::sync::oneshot;
use tokio::time;
use self::navbar::NavigationBarState;
use crate::app::load_app_config;
use crate::cache::LxAppCache;
use crate::error::LxAppError;
use crate::executor::LxAppExecutor;
use crate::lxapp::page_config::OrientationConfig;
use crate::page::Page;
use crate::startup::LxAppStartupOptions;
use crate::update::UpdateManager;
use crate::{error, info, warn};
use security::NetworkSecurity;
pub mod config;
use config::LxAppConfig;
mod content;
pub(crate) mod metadata;
pub use metadata::ReleaseType;
pub mod navbar;
pub mod page_config;
mod popup;
mod scheme;
mod security;
pub mod tabbar;
pub mod uri;
pub(crate) mod version;
use crate::lifecycle::AppServiceEvent;
use lingxia_webview::WebTag;
use lingxia_webview::runtime::destroy_webview;
pub use popup::PopupMode;
pub(crate) use popup::WEB_POPUP_PATH;
pub(crate) const LINGXIA_DIR: &str = "lingxia";
pub(crate) const LXAPPS_DIR: &str = "lxapps";
pub(crate) const PLUGINS_DIR: &str = "plugins";
pub(crate) const STORAGE_DIR: &str = "storage";
pub(crate) const USER_DATA_DIR: &str = "userdata";
pub(crate) const USER_CACHE_DIR: &str = "usercache";
const LXAPPS_DB_FILE: &str = "lxapps.redb";
const DEFAULT_VERSION: &str = "0.0.1";
const LXAPP_STACK_MAX: usize = 5;
const PAGE_STACK_MAX: usize = 10;
const VIEW_CALL_TIMEOUT: Duration = Duration::from_secs(15);
static NUM_WORKERS: OnceLock<usize> = OnceLock::new();
pub fn set_num_workers(n: usize) {
let n = n.max(1);
if NUM_WORKERS.set(n).is_err() {
warn!("set_num_workers: value already set, ignoring");
}
}
fn get_num_workers() -> usize {
NUM_WORKERS.get().copied().unwrap_or(LXAPP_STACK_MAX)
}
#[cfg(target_os = "macos")]
static HOME_LXAPP_DEV_PATH: OnceLock<PathBuf> = OnceLock::new();
#[cfg(target_os = "macos")]
pub fn set_home_lxapp_dev_path(path: &str) -> bool {
let path = PathBuf::from(path);
if !path.exists() {
warn!(
"set_home_lxapp_dev_path: path does not exist: {}",
path.display()
);
return false;
}
if !path.join("lxapp.json").exists() {
warn!(
"set_home_lxapp_dev_path: lxapp.json not found in: {}",
path.display()
);
return false;
}
match HOME_LXAPP_DEV_PATH.set(path.clone()) {
Ok(()) => {
info!("Home lxapp dev path set to: {}", path.display());
true
}
Err(_) => {
warn!("set_home_lxapp_dev_path: path already set");
false
}
}
}
#[cfg(not(target_os = "macos"))]
pub fn set_home_lxapp_dev_path(_path: &str) -> bool {
false
}
#[cfg(target_os = "macos")]
fn get_home_lxapp_dev_path() -> Option<&'static PathBuf> {
HOME_LXAPP_DEV_PATH.get()
}
#[cfg(not(target_os = "macos"))]
fn get_home_lxapp_dev_path() -> Option<&'static PathBuf> {
None
}
pub struct LxApps {
lxapps: DashMap<String, Arc<LxApp>>,
lxapp_stack: Mutex<VecDeque<String>>,
runtime: Arc<Platform>,
pub(crate) executor: Arc<LxAppExecutor>,
pending_destroy: Mutex<HashMap<String, oneshot::Sender<()>>>,
}
impl LxApps {
fn new(runtime: Platform, executor: Arc<LxAppExecutor>, capacity: usize) -> Self {
info!("LxApps manager initialized with {} workers", capacity);
let runtime = Arc::new(runtime);
Self {
lxapps: DashMap::new(),
runtime,
executor,
lxapp_stack: Mutex::new(VecDeque::with_capacity(capacity)),
pending_destroy: Mutex::new(HashMap::new()),
}
}
pub(crate) fn ensure_lxapp(&self, appid: String, release_type: ReleaseType) -> Arc<LxApp> {
let has_pending_update = metadata::downloaded_get(&appid, release_type)
.map(|opt| opt.is_some())
.unwrap_or(false);
if has_pending_update {
self.destroy_lxapp(&appid);
if let Err(e) =
UpdateManager::apply_downloaded_update(self.runtime.clone(), &appid, release_type)
{
error!(
"Failed to apply downloaded update before opening app: {}",
e
)
.with_appid(appid.clone());
}
} else if let Some(app_arc) = self.lxapps.get(&appid) {
return app_arc.clone();
}
let new_lxapp = Arc::new(LxApp::new(
appid.clone(),
self.runtime.clone(),
self.executor.clone(),
release_type,
));
self.lxapps.insert(appid, new_lxapp.clone());
new_lxapp
}
fn destroy_lxapp_with_options(&self, appid: &str, skip_hide: bool) {
if let Some(app_arc) = self.lxapps.get(appid) {
let _ = app_arc.shutdown_with_options(skip_hide);
}
self.remove_from_stack(appid);
self.lxapps.remove(appid);
}
fn destroy_lxapp(&self, appid: &str) {
self.destroy_lxapp_with_options(appid, false);
}
fn recreate_lxapp(&self, appid: String, release_type: ReleaseType) -> Arc<LxApp> {
self.destroy_lxapp_with_options(&appid, true);
self.ensure_lxapp(appid, release_type)
}
fn evict_lru_lxapp(&self) {
let appid_to_destroy = {
if let Ok(stack) = self.lxapp_stack.lock() {
stack.front().cloned()
} else {
None
}
};
if let Some(appid_to_destroy) = appid_to_destroy {
if let Some(app_arc) = self.lxapps.get(&appid_to_destroy)
&& app_arc.is_home_lxapp
{
warn!("Cannot evict the home lxapp").with_appid(appid_to_destroy);
return;
}
info!("Evicting least recently used lxapp").with_appid(appid_to_destroy.clone());
self.destroy_lxapp(&appid_to_destroy);
}
}
pub(crate) fn schedule_delayed_destroy(self: &Arc<Self>, appid: String) {
if let Ok(mut map) = self.pending_destroy.lock()
&& let Some(cancel) = map.remove(&appid)
{
let _ = cancel.send(());
let (tx, rx) = oneshot::channel();
map.insert(appid.clone(), tx);
let mgr_weak = Arc::downgrade(self);
let task_appid = appid.clone();
let _ = rong::bg::spawn(async move {
let sleep = time::sleep(Duration::from_secs(1800));
tokio::pin!(rx);
tokio::pin!(sleep);
tokio::select! {
_ = &mut sleep => {},
_ = &mut rx => return, }
if let Some(mgr) = mgr_weak.upgrade() {
info!("Delayed destroy triggered after inactivity")
.with_appid(task_appid.clone());
mgr.destroy_lxapp(&task_appid);
if let Ok(mut guard) = mgr.pending_destroy.lock() {
guard.remove(&task_appid);
}
}
});
}
}
pub(crate) fn cancel_delayed_destroy(&self, appid: &str) {
if let Ok(mut map) = self.pending_destroy.lock()
&& let Some(cancel) = map.remove(appid)
{
let _ = cancel.send(());
}
}
pub(crate) fn push_lxapp_stack(&self, appid: String) {
let max = get_num_workers();
if let Ok(mut stack) = self.lxapp_stack.lock() {
if stack.len() < max {
stack.push_back(appid);
} else {
warn!(
"LxApp navigation stack is full (capacity: {}). Cannot push app: {}",
max, appid
);
}
}
}
fn peek_lxapp_stack(&self) -> Option<String> {
if let Ok(stack) = self.lxapp_stack.lock() {
stack.back().cloned()
} else {
None
}
}
pub(crate) fn remove_from_stack(&self, appid: &str) {
if let Ok(mut stack) = self.lxapp_stack.lock() {
stack.retain(|id| id != appid);
}
}
fn is_lxapp_stack_full(&self) -> bool {
let max = get_num_workers();
if let Ok(stack) = self.lxapp_stack.lock() {
stack.len() >= max
} else {
true
}
}
}
pub(crate) struct LxAppState {
pub(crate) pages: Mutex<HashMap<String, Page>>,
pub(crate) page_stack: Mutex<VecDeque<String>>,
pub(crate) last_active_time: Instant,
network_security: NetworkSecurity,
pub tabbar: Option<tabbar::TabBar>,
pub(crate) startup_options: LxAppStartupOptions,
pub(crate) current_popup: Option<popup::ActivePopup>,
pub(crate) orientation_override: Option<OrientationConfig>,
}
impl LxAppState {
fn new() -> Self {
Self {
pages: Mutex::new(HashMap::new()),
page_stack: Mutex::new(VecDeque::with_capacity(PAGE_STACK_MAX)),
last_active_time: Instant::now(),
network_security: NetworkSecurity::new(),
tabbar: None,
startup_options: LxAppStartupOptions::default(),
current_popup: None,
orientation_override: None,
}
}
}
pub struct LxApp {
pub appid: String,
pub runtime: Arc<Platform>,
pub lxapp_dir: PathBuf,
pub storage_file_path: PathBuf,
pub user_data_dir: PathBuf,
pub user_cache_dir: PathBuf,
pub fingermark: String,
pub is_home_lxapp: bool,
pub(crate) release_type: ReleaseType,
pub(crate) config: LxAppConfig,
pub(crate) executor: Arc<LxAppExecutor>,
home_update_check_dispatched: AtomicBool,
pending_restart_request: AtomicBool,
pub(crate) session: LxAppSession,
pub(crate) state: Mutex<LxAppState>,
cache: Option<LxAppCache>,
}
pub(crate) type LxAppSessionId = u64;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub(crate) enum LxAppSessionStatus {
Closed = 0,
Opening = 1,
Opened = 2,
Closing = 3,
Restarting = 4,
}
pub(crate) struct LxAppSession {
pub(crate) id: LxAppSessionId,
status: AtomicU8,
}
impl LxAppSession {
pub(crate) fn new() -> Self {
use std::sync::atomic::AtomicU64;
static SESSION_SEQ: AtomicU64 = AtomicU64::new(1);
let id = SESSION_SEQ.fetch_add(1, Ordering::Relaxed);
Self {
id,
status: AtomicU8::new(LxAppSessionStatus::Closed as u8),
}
}
pub(crate) fn status(&self) -> LxAppSessionStatus {
match self.status.load(Ordering::SeqCst) {
1 => LxAppSessionStatus::Opening,
2 => LxAppSessionStatus::Opened,
3 => LxAppSessionStatus::Closing,
4 => LxAppSessionStatus::Restarting,
_ => LxAppSessionStatus::Closed,
}
}
pub(crate) fn set_status(&self, s: LxAppSessionStatus) {
self.status.store(s as u8, Ordering::SeqCst);
}
pub(crate) fn cas_status(&self, from: LxAppSessionStatus, to: LxAppSessionStatus) -> bool {
let current = self.status.load(Ordering::SeqCst);
if current == from as u8 {
self.status.store(to as u8, Ordering::SeqCst);
true
} else {
false
}
}
}
impl LxApp {
pub(crate) fn clone_arc(&self) -> Arc<LxApp> {
crate::lxapp::get(self.appid.clone())
}
pub(crate) fn status(&self) -> LxAppSessionStatus {
self.session.status()
}
pub fn session_id(&self) -> LxAppSessionId {
self.session.id
}
pub fn release_type(&self) -> ReleaseType {
self.release_type
}
pub(crate) fn set_status(&self, s: LxAppSessionStatus) {
self.session.set_status(s);
}
pub(crate) fn cas_status(&self, from: LxAppSessionStatus, to: LxAppSessionStatus) -> bool {
self.session.cas_status(from, to)
}
pub(crate) fn trigger_home_update_check_once(&self) {
if !self.is_home_lxapp {
return;
}
if self
.home_update_check_dispatched
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
UpdateManager::spawn_release_lxapp_update_check(self.appid.clone());
}
}
pub(crate) fn has_pending_restart_request(&self) -> bool {
self.pending_restart_request.load(Ordering::SeqCst)
}
pub fn shutdown_with_options(&self, skip_hide: bool) -> Result<(), LxAppError> {
self.set_status(LxAppSessionStatus::Closing);
crate::key_event::clear(&self.appid, self.session.id);
if !skip_hide {
let _ = self
.runtime
.hide_lxapp(self.appid.clone(), self.session.id)
.map_err(LxAppError::from);
}
let page_paths: Vec<String> = {
let state = self.state.lock().unwrap();
state.pages.lock().unwrap().keys().cloned().collect()
};
crate::appservice::view_call::cancel_view_calls_for_pages(
&page_paths,
"Page removed while waiting for view response",
);
if let Ok(state) = self.state.lock() {
for (_k, page) in state.pages.lock().unwrap().iter() {
page.detach_webview();
}
}
if let Ok(state) = self.state.lock() {
state.pages.lock().unwrap().clear();
}
for p in &page_paths {
destroy_webview(&WebTag::new(&self.appid, p, Some(self.session.id)));
}
let _ = self.clear_page_stack();
if let Ok(mut state) = self.state.lock() {
if let Some(popup::ActivePopup::WebPage) = state.current_popup.take() {
destroy_webview(&WebTag::new(
&self.appid,
popup::WEB_POPUP_PATH,
Some(self.session.id),
));
}
}
let _ = self.executor.terminate_app_svc(self.clone_arc());
Ok(())
}
pub fn shutdown(&self) -> Result<(), LxAppError> {
self.shutdown_with_options(false)
}
fn _new(
appid: String,
runtime: Arc<Platform>,
executor: Arc<LxAppExecutor>,
release_type: ReleaseType,
) -> Self {
let session = LxAppSession::new();
Self {
appid,
runtime,
lxapp_dir: PathBuf::new(),
storage_file_path: PathBuf::new(),
user_data_dir: PathBuf::new(),
user_cache_dir: PathBuf::new(),
fingermark: String::new(),
is_home_lxapp: false,
release_type,
config: LxAppConfig::default(),
executor,
home_update_check_dispatched: AtomicBool::new(false),
pending_restart_request: AtomicBool::new(false),
session,
state: Mutex::new(LxAppState::new()),
cache: None,
}
}
pub(crate) fn new(
appid: String,
runtime: Arc<Platform>,
executor: Arc<LxAppExecutor>,
release_type: ReleaseType,
) -> Self {
let mut app = Self::_new(appid, runtime, executor, release_type);
if let Err(e) = app.setup() {
error!("Setup failed: {}", e).with_appid(&app.appid);
}
app
}
fn new_as_home(appid: String, runtime: Arc<Platform>, executor: Arc<LxAppExecutor>) -> Self {
let mut app = Self::_new(appid, runtime, executor, ReleaseType::Release);
app.is_home_lxapp = true;
if let Err(e) = app.setup() {
error!("Setup failed for home app: {}", e).with_appid(&app.appid);
}
app
}
fn initialize_paths(&mut self) -> Result<(), LxAppError> {
let meta = metadata::get(&self.appid, self.release_type).ok().flatten();
self.fingermark = meta
.as_ref()
.map(|record| record.fingermark.clone())
.unwrap_or_else(|| lxapp_fingermark(&self.appid, self.release_type));
let dir_name = self.fingermark.clone();
let base_dir = self
.runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(LXAPPS_DIR);
self.lxapp_dir = base_dir.join(&dir_name);
if self.is_home_lxapp {
if let Some(dev_path) = get_home_lxapp_dev_path() {
info!("Using dev path for home lxapp: {}", dev_path.display());
self.lxapp_dir = dev_path.clone();
}
}
self.storage_file_path = self
.runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(STORAGE_DIR)
.join(format!("{}.redb", self.fingermark));
let userdata_base_dir = self
.runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(USER_DATA_DIR);
self.user_data_dir = userdata_base_dir.join(&dir_name);
if !self.user_data_dir.exists() {
std::fs::create_dir_all(&self.user_data_dir).map_err(|e| {
LxAppError::IoError(format!("Failed to create user data directory: {}", e))
})?;
}
let cache_base_dir = self
.runtime
.app_cache_dir()
.join(LINGXIA_DIR)
.join(LXAPPS_DIR)
.join(USER_CACHE_DIR);
self.user_cache_dir = cache_base_dir.join(&dir_name);
if !self.user_cache_dir.exists() {
std::fs::create_dir_all(&self.user_cache_dir).map_err(|e| {
LxAppError::IoError(format!("Failed to create cache directory: {}", e))
})?;
}
Ok(())
}
pub fn load_config(&mut self) -> Result<(), LxAppError> {
let lxapp_json_path = self.lxapp_dir.join("lxapp.json");
info!(
" [{}] Loading lxapp.json from: {}",
self.appid,
lxapp_json_path.display()
);
self.read_json("lxapp.json").map(|app_json| {
self.config = LxAppConfig::from_value(app_json)
.map_err(|e| LxAppError::InvalidJsonFile(format!("lxapp.json: {}", e)))?;
if let Some(tabbar_config) = &self.config.tabBar {
let mut state = self.state.lock().unwrap();
state.tabbar = Some(tabbar_config.with_absolute_paths(&self.lxapp_dir));
}
Ok(())
})?
}
fn setup(&mut self) -> Result<(), LxAppError> {
self.initialize_paths()?;
self.load_config()?;
self.cache = Some(
LxAppCache::new(self.user_cache_dir.clone())
.map_err(|e| LxAppError::IoError(e.to_string()))?,
);
Ok(())
}
pub fn cache(&self) -> Result<&LxAppCache, LxAppError> {
self.cache
.as_ref()
.ok_or_else(|| LxAppError::IoError("cache not initialized".to_string()))
}
pub fn current_version(&self) -> String {
metadata::get(&self.appid, self.release_type)
.ok()
.flatten()
.map(|record| record.version_string())
.filter(|version| !version.is_empty())
.unwrap_or_else(|| DEFAULT_VERSION.to_string())
}
pub fn get_app_orientation(&self) -> OrientationConfig {
let state = self.state.lock().unwrap();
state.orientation_override.unwrap_or_default()
}
pub fn set_app_orientation(&self, orientation: OrientationConfig) {
let orientation = OrientationConfig::normalize(orientation.mode, orientation.rotation);
let mut state = self.state.lock().unwrap();
state.orientation_override = Some(orientation);
}
pub fn get_page_orientation(&self, path: &str) -> OrientationConfig {
let app_orientation = self.get_app_orientation();
let page_override = self
.get_page(path)
.and_then(|page| page.get_orientation_override())
.unwrap_or_default();
page_override.apply(app_orientation)
}
fn read_bytes(&self, relative_path: &str) -> Result<Vec<u8>, LxAppError> {
let file_path = match crate::plugin::resolve_plugin_resource_path_from_internal_path(
&self.runtime,
&self.config.plugins,
relative_path,
)? {
Some(path) => path,
None => self.lxapp_dir.join(relative_path),
};
fs::read(&file_path).map_err(|e| {
LxAppError::ResourceNotFound(format!(
"{}:{} (resolved: {})",
relative_path,
e,
file_path.display()
))
})
}
pub fn resolve_accessible_path(&self, path: &str) -> Result<PathBuf, LxAppError> {
let path = path.trim();
if path.is_empty() {
return Err(LxAppError::ResourceNotFound("empty path".to_string()));
}
if path.starts_with("lx://") {
let lx_uri = uri::LxUri::from_str(path)
.map_err(|e| LxAppError::InvalidParameter(format!("invalid lx uri: {}", e)))?;
return self.resolve_lx_user_uri(&lx_uri);
}
if path.split('/').any(|s| s == "..") {
return Err(LxAppError::ResourceNotFound(
"directory traversal not allowed".to_string(),
));
}
let path_ref = Path::new(path);
if !path_ref.is_absolute() && !path.contains(':') {
let rel = path.trim_start_matches('/');
return Ok(self.lxapp_dir.join(rel));
}
let trusted_roots = [
(&self.lxapp_dir, "app bundle"),
(&self.user_data_dir, "user data"),
(&self.user_cache_dir, "user cache"),
];
for (root, _name) in trusted_roots {
if !root.as_os_str().is_empty() && path_ref.starts_with(root) {
return Ok(path_ref.to_path_buf());
}
}
for root in [&self.user_data_dir, &self.user_cache_dir] {
if let Some(parent) = root.parent() {
if path_ref.starts_with(parent) {
return Ok(path_ref.to_path_buf());
}
}
}
Err(LxAppError::ResourceNotFound(format!(
"Access denied: {}",
path
)))
}
pub fn to_uri(&self, path: &Path) -> Option<uri::LxUri> {
uri::try_convert_path_to_uri(path, self)
}
fn resolve_lx_user_uri(&self, lx_uri: &uri::LxUri) -> Result<PathBuf, LxAppError> {
let uri = HttpUri::from_str(lx_uri.as_str())
.map_err(|_| LxAppError::InvalidParameter("invalid lx uri".to_string()))?;
if uri.scheme_str() != Some(uri::LX_SCHEME) {
return Err(LxAppError::InvalidParameter(
"invalid lx uri scheme".to_string(),
));
}
match uri.host() {
Some(uri::HOST_USER_CACHE) | Some(uri::HOST_USER_DATA) => {
let base_dir = match uri.host() {
Some(uri::HOST_USER_CACHE) => &self.user_cache_dir,
Some(uri::HOST_USER_DATA) => &self.user_data_dir,
_ => unreachable!(),
};
let decoded_path = uri::decode_lx_path(uri.path());
let rel = decoded_path.trim_matches('/');
if rel.is_empty() {
return Err(LxAppError::ResourceNotFound(lx_uri.as_str().to_string()));
}
if uri::has_invalid_segment(rel) || rel.contains(':') || rel.contains('\\') {
return Err(LxAppError::ResourceNotFound(lx_uri.as_str().to_string()));
}
Ok(base_dir.join(rel))
}
Some(uri::HOST_LXAPP) => {
let decoded_path = uri::decode_lx_path(uri.path());
let raw = decoded_path.trim_start_matches('/');
let (appid, rest) = raw
.split_once('/')
.ok_or_else(|| LxAppError::ResourceNotFound(lx_uri.as_str().to_string()))?;
if appid != self.appid.as_str() {
return Err(LxAppError::ResourceNotFound(lx_uri.as_str().to_string()));
}
let rel = rest.trim_matches('/');
if rel.is_empty() {
return Err(LxAppError::ResourceNotFound(lx_uri.as_str().to_string()));
}
if uri::has_invalid_segment(rel) || rel.contains(':') || rel.contains('\\') {
return Err(LxAppError::ResourceNotFound(lx_uri.as_str().to_string()));
}
Ok(self.lxapp_dir.join(rel))
}
_ => Err(LxAppError::ResourceNotFound(format!(
"unsupported lx uri host: {}",
lx_uri.as_str()
))),
}
}
fn read_text(&self, relative_path: &str) -> Result<String, LxAppError> {
self.read_bytes(relative_path)
.map(|content| String::from_utf8_lossy(&content).to_string())
}
pub(crate) fn read_json(&self, relative_path: &str) -> Result<serde_json::Value, LxAppError> {
self.read_text(relative_path).and_then(|content| {
serde_json::from_str(&content)
.map_err(|_| LxAppError::InvalidJsonFile(relative_path.to_string()))
})
}
pub fn is_opened(&self) -> bool {
matches!(self.status(), LxAppSessionStatus::Opened)
}
pub fn is_domain_allowed(&self, domain: &str) -> bool {
self.state
.lock()
.unwrap()
.network_security
.is_domain_allowed(domain)
}
pub fn get_page(&self, path: &str) -> Option<Page> {
self.state
.lock()
.unwrap()
.pages
.lock()
.unwrap()
.get(path)
.cloned()
}
pub(crate) fn register_page(&self, page: Page) {
let state = self.state.lock().unwrap();
state
.pages
.lock()
.unwrap()
.entry(page.path())
.or_insert(page);
}
pub fn is_pull_down_refresh_enabled(&self, path: &str) -> bool {
self.get_page(path)
.map(|page| page.is_pull_down_refresh_enabled())
.unwrap_or(false)
}
pub fn get_navbar_state(&self, path: &str) -> NavigationBarState {
self.get_page(path)
.and_then(|page| page.get_navbar_state())
.unwrap_or_default()
}
pub(crate) fn open(&self, options: LxAppStartupOptions) -> Result<(), LxAppError> {
let mut startup_options = options;
let raw_url = if startup_options.path.is_empty() {
self.config.get_initial_route()
} else {
startup_options.path.clone()
};
let resolved = crate::route::resolve_route(self, &raw_url).unwrap_or_else(|e| {
error!("Failed to resolve startup url '{}': {}", raw_url, e)
.with_appid(self.appid.clone());
crate::route::ResolvedRoute {
original: raw_url.clone(),
query: None,
target: crate::route::RouteTarget::Normal {
path: raw_url.clone(),
},
}
});
startup_options.path = resolved.internal_path();
if startup_options.query.is_empty()
&& let Some(query) = resolved.query.clone()
{
startup_options.query = query;
}
self.state.lock().unwrap().startup_options = startup_options.clone();
if let Err(e) = self.executor.create_app_svc(self.clone_arc()) {
error!("Failed to trigger app service: {}", e).with_appid(self.appid.clone());
}
let page = self.get_or_create_page(&startup_options.path);
page.set_query(startup_options.query.clone());
self.runtime.show_lxapp(
self.appid.clone(),
startup_options.path,
self.session.id,
startup_options.presentation,
startup_options.panel_id,
)?;
Ok(())
}
pub fn navigate_to(
&self,
appid: String,
options: LxAppStartupOptions,
) -> Result<(), LxAppError> {
if let Some(manager) = get_lxapps_manager() {
manager.cancel_delayed_destroy(&appid);
if manager.is_lxapp_stack_full() {
warn!(
"LxApp navigation stack is full (capacity: {}). Cannot navigate to app: {}",
get_num_workers(),
appid
);
return Ok(());
}
let app = manager.ensure_lxapp(appid.clone(), options.release_type);
app.open(options)?;
}
Ok(())
}
pub fn navigate_back(&self) -> Result<(), LxAppError> {
self.runtime
.hide_lxapp(self.appid.clone(), self.session.id)?;
Ok(())
}
pub fn restart(&self) -> Result<(), LxAppError> {
let from_session = self.session.id;
let current_status = self.status();
match current_status {
LxAppSessionStatus::Opening
| LxAppSessionStatus::Closed
| LxAppSessionStatus::Closing => {
self.pending_restart_request.store(true, Ordering::SeqCst);
return Ok(());
}
LxAppSessionStatus::Opened => {}
LxAppSessionStatus::Restarting => return Ok(()),
}
if !self.cas_status(LxAppSessionStatus::Opened, LxAppSessionStatus::Restarting) {
let current = self.status();
if current == LxAppSessionStatus::Opening {
self.pending_restart_request.store(true, Ordering::SeqCst);
}
return Ok(());
}
self.pending_restart_request.store(false, Ordering::SeqCst);
if let Err(e) = self.runtime.hide_lxapp(self.appid.clone(), from_session) {
error!(
"Restart transition: failed to request close for session {}: {}",
from_session, e
)
.with_appid(self.appid.clone());
}
let relaunch_path = self.config.get_initial_route();
let appid = self.appid.clone();
let release_type = self.release_type;
let _ = rong::bg::spawn(async move {
let wait_deadline = Instant::now() + Duration::from_millis(1500);
loop {
let Some(current) = crate::lxapp::try_get(&appid) else {
break;
};
if current.session_id() != from_session {
return;
}
if current.status() == LxAppSessionStatus::Closed {
break;
}
if Instant::now() >= wait_deadline {
warn!(
"Restart transition: close wait timeout for session {}, forcing recreate",
from_session
)
.with_appid(appid.clone());
break;
}
time::sleep(Duration::from_millis(20)).await;
}
if let Some(manager) = get_lxapps_manager() {
let new_app = manager.recreate_lxapp(appid.clone(), release_type);
let options =
LxAppStartupOptions::new(&relaunch_path).set_release_type(release_type);
if let Err(e) = new_app.open(options) {
error!("Failed to start lxapp after restart: {}", e);
}
}
});
Ok(())
}
pub fn get_lxapp_info(&self) -> config::LxAppInfo {
self.config.get_lxapp_info(self.release_type.as_str())
}
pub fn find_page_path(&self, path: &str) -> Option<String> {
find_matching_page_path(&self.config.pages, path).map(|s| s.to_string())
}
pub fn ensure_page_exists(&self, url: &str) -> Result<(), LxAppError> {
let resolved = crate::route::resolve_route(self, url)?;
self.ensure_resolved_route_exists(&resolved)
}
fn ensure_resolved_route_exists(
&self,
resolved: &crate::route::ResolvedRoute,
) -> Result<(), LxAppError> {
match &resolved.target {
crate::route::RouteTarget::Normal { path } => {
if self.is_configured_page(path) {
Ok(())
} else {
Err(LxAppError::ResourceNotFound(path.clone()))
}
}
crate::route::RouteTarget::Plugin { name, path } => {
if self.is_plugin_page_configured(name, path, &resolved.original) {
Ok(())
} else {
Err(LxAppError::ResourceNotFound(format!(
"plugin/{}/{}",
name, path
)))
}
}
}
}
fn is_configured_page(&self, path: &str) -> bool {
!path.trim_start_matches('/').is_empty()
&& find_matching_page_path(&self.config.pages, path).is_some()
}
fn is_plugin_page_configured(
&self,
plugin_name: &str,
resolved_page_path: &str,
original_url: &str,
) -> bool {
let plugin_cfg = match self.config.plugins.get(plugin_name) {
Some(cfg) => cfg,
None => return false,
};
let requested_path = extract_plugin_page_path(original_url)
.unwrap_or_else(|| resolved_page_path.to_string());
if !plugin_cfg.pages.is_empty() {
return plugin_page_map_contains(
&plugin_cfg.pages,
&requested_path,
resolved_page_path,
);
}
if let Some(pages) =
crate::plugin::load_plugin_manifest_pages(&self.runtime, plugin_name, plugin_cfg)
{
return plugin_page_map_contains(&pages, &requested_path, resolved_page_path);
}
true
}
pub fn get_or_create_page(&self, url: &str) -> Page {
let resolved = crate::route::resolve_route(self, url).unwrap_or_else(|e| {
error!("Failed to resolve page url '{}': {}", url, e).with_appid(self.appid.clone());
let (path, query) = crate::startup::split_path_query(url);
crate::route::ResolvedRoute {
original: url.to_string(),
query,
target: crate::route::RouteTarget::Normal { path },
}
});
let path = resolved.internal_path();
let query = resolved.query;
{
let state = self.state.lock().unwrap();
if let Some(page) = state.pages.lock().unwrap().get(&path) {
if let Some(query) = query.clone() {
page.set_query(query);
}
return page.clone();
}
}
let appid = self.appid.clone();
let lxapp_arc = self.clone_arc();
let page = Page::new(appid.clone(), path.to_string(), self, move |page| {
let lxapp_arc = lxapp_arc.clone();
let page_clone = page.clone();
async move {
let (ack_tx, ack_rx) = oneshot::channel();
if let Err(e) = lxapp_arc.executor.create_page_svc_with_ack(
lxapp_arc.clone(),
page_clone.path(),
ack_tx,
) {
return Err(e.to_string());
}
ack_rx
.await
.map_err(|e| format!("Page service creation ack failed: {}", e))?;
page_clone
.load_html()
.map_err(|e| format!("Failed to load HTML for page: {}", e))
}
});
{
let state = self.state.lock().unwrap();
state
.pages
.lock()
.unwrap()
.insert(path.clone(), page.clone());
}
self.evict_inactive_pages_if_needed();
if let Some(query) = query {
page.set_query(query);
}
page
}
fn should_evict_pages(&self) -> bool {
let state = self.state.lock().unwrap();
let page_count = state.pages.lock().unwrap().len();
let max_allowed = if let Some(ref tabbar) = state.tabbar {
tabbar.list.len() + PAGE_STACK_MAX
} else {
PAGE_STACK_MAX
};
page_count > max_allowed
}
fn evict_inactive_pages_if_needed(&self) {
if !self.should_evict_pages() {
return;
}
let state = self.state.lock().unwrap();
let mut pages = state.pages.lock().unwrap();
let current_page = state.page_stack.lock().unwrap().back().cloned();
let mut oldest_time: Option<Instant> = None;
let mut oldest_path: Option<String> = None;
for (path, page) in pages.iter() {
if Some(path) == current_page.as_ref() {
continue; }
if page.is_tabbar_page() {
info!("Skipping tabbar page for eviction: {}", path).with_appid(self.appid.clone());
continue;
}
if let Some(last_active) = page.get_last_active_time()
&& oldest_time.is_none_or(|old| last_active < old)
{
oldest_time = Some(last_active);
oldest_path = Some(path.clone());
}
}
if let Some(path) = oldest_path.clone() {
let _ = self
.executor
.terminate_page_svc(self.clone_arc(), path.clone())
.map_err(|e| {
warn!("Failed to request page termination: {}", e)
.with_appid(self.appid.clone())
.with_path(path.clone())
});
if let Some(_removed_page) = pages.remove(&path) {
info!("Evicted inactive page: {}", path).with_appid(self.appid.clone());
} else {
warn!("Failed to evict page (not found): {}", path).with_appid(self.appid.clone());
}
}
}
pub(crate) fn is_page_stack_full(&self) -> bool {
self.get_page_stack_size() >= PAGE_STACK_MAX
}
pub(crate) fn clear_page_stack(&self) -> Result<(), LxAppError> {
let state = self.state.lock().unwrap();
state.page_stack.lock().unwrap().clear();
Ok(())
}
pub(crate) fn push_to_page_stack(&self, path: &str) -> Result<(), LxAppError> {
let state = self.state.lock().unwrap();
let mut stack = state.page_stack.lock().unwrap();
if stack.len() >= PAGE_STACK_MAX {
return Ok(());
}
stack.push_back(path.to_string());
Ok(())
}
pub(crate) fn pop_from_page_stack(&self) -> Option<String> {
let state = self.state.lock().unwrap();
state.page_stack.lock().unwrap().pop_back()
}
pub(crate) fn remove_pages(&self, paths: &[String]) {
crate::appservice::view_call::cancel_view_calls_for_pages(
paths,
"Page removed while waiting for view response",
);
let lxapp = self.clone_arc();
for path in paths {
let _ = self
.executor
.terminate_page_svc(lxapp.clone(), path.clone())
.map_err(|e| {
warn!("Failed to request page termination: {}", e)
.with_appid(self.appid.clone())
.with_path(path.clone())
});
}
if let Ok(state) = self.state.lock() {
let mut pages = state.pages.lock().unwrap();
for path in paths {
pages.remove(path);
}
}
}
pub(crate) fn get_page_stack_size(&self) -> usize {
self.state.lock().unwrap().page_stack.lock().unwrap().len()
}
pub fn get_page_stack(&self) -> Vec<String> {
self.state
.lock()
.unwrap()
.page_stack
.lock()
.unwrap()
.iter()
.cloned()
.collect()
}
pub fn peek_current_page(&self) -> Option<String> {
self.state
.lock()
.unwrap()
.page_stack
.lock()
.unwrap()
.back()
.cloned()
}
}
pub(crate) fn lxapp_fingermark(lxappid: &str, release_type: ReleaseType) -> String {
let device_fp = resolve_or_cache_device_fingerprint();
let combined = format!("{}|{}|{}", lxappid, release_type.as_str(), device_fp);
let mut hasher = DefaultHasher::new();
combined.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
impl LxApp {
pub async fn call_current_page_view(
&self,
method: &str,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value, LxAppError> {
let path = self
.peek_current_page()
.ok_or_else(|| LxAppError::WebView("No current page".to_string()))?;
let page = self
.get_page(&path)
.ok_or_else(|| LxAppError::WebView(format!("Page not found: {}", path)))?;
let pending = crate::appservice::view_call::call_view(&page, method, params)?;
match time::timeout(VIEW_CALL_TIMEOUT, pending.rx).await {
Ok(Ok(result)) => result.map_err(|rpc_err| LxAppError::RongJSHost {
code: rpc_err.code,
message: rpc_err
.message
.unwrap_or_else(|| "View call failed".to_string()),
data: rpc_err.data,
}),
Ok(Err(_)) => Err(LxAppError::ChannelError(
"View call channel closed".to_string(),
)),
Err(_) => {
crate::appservice::view_call::cancel_view_call(
&pending.id,
Some(format!("View call timed out after {:?}", VIEW_CALL_TIMEOUT)),
);
Err(LxAppError::Bridge(format!(
"{}: View call timed out after {:?}",
crate::appservice::bridge::BRIDGE_TIMEOUT,
VIEW_CALL_TIMEOUT
)))
}
}
}
pub fn appservice_notify(
&self,
event: AppServiceEvent,
payload_json: Option<String>,
) -> Result<(), LxAppError> {
self.executor
.call_app_service_event(self.clone_arc(), event, payload_json)
}
}
impl Drop for LxApp {
fn drop(&mut self) {
if self.is_home_lxapp {
return;
}
info!("Dropping LxApp").with_appid(self.appid.clone());
}
}
fn prepare_directory_structure(runtime: Arc<Platform>) -> Result<(), LxAppError> {
let data_dir = runtime.app_data_dir();
let cache_dir = runtime.app_cache_dir();
let dirs = [
data_dir.join(LINGXIA_DIR).join(LXAPPS_DIR),
data_dir.join(LINGXIA_DIR).join(PLUGINS_DIR),
data_dir.join(LINGXIA_DIR).join(USER_DATA_DIR),
data_dir.join(LINGXIA_DIR).join(STORAGE_DIR),
cache_dir.join(LINGXIA_DIR).join(LXAPPS_DIR),
];
for dir in &dirs {
fs::create_dir_all(dir)?;
}
let metadata_path = data_dir.join(LINGXIA_DIR).join(LXAPPS_DB_FILE);
metadata::init(metadata_path)
}
fn spawn_cache_cleanup(runtime: Arc<Platform>) {
let max_bytes = crate::app::cache_max_size_bytes();
let max_age = crate::app::cache_max_age();
if max_bytes == 0 && max_age.is_zero() {
info!("Cache cleanup disabled (cacheMaxSizeMB=0 and cacheMaxAgeDays=0)");
return;
}
let _ = rong::bg::spawn(async move {
let cache_base_dir = runtime
.app_cache_dir()
.join(LINGXIA_DIR)
.join(LXAPPS_DIR)
.join(USER_CACHE_DIR);
if let Ok(entries) = fs::read_dir(&cache_base_dir) {
for entry in entries.flatten() {
let path = entry.path();
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() && !file_type.is_symlink() {
crate::cache::cleanup_cache_dir(&path, max_bytes, max_age);
}
}
}
});
}
static LXAPPS_MANAGER: OnceLock<Arc<LxApps>> = OnceLock::new();
static RUNTIME: OnceLock<Arc<Platform>> = OnceLock::new();
static DEVICE_FINGERPRINT: OnceLock<String> = OnceLock::new();
fn resolve_or_cache_device_fingerprint() -> String {
if let Some(fp) = DEVICE_FINGERPRINT.get() {
return fp.clone();
}
let fp = match crate::provider::get_provider().get_fingerprint() {
Ok(fp) => fp,
Err(e) => {
warn!("Device Fingerprint unavailable: {}", e);
String::new()
}
};
let _ = DEVICE_FINGERPRINT.set(fp.clone());
fp
}
pub fn init(runtime: Platform) -> Option<String> {
std::panic::set_hook(Box::new(|panic_info| {
let location = panic_info
.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
.unwrap_or_else(|| "unknown location".to_string());
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"unknown panic message".to_string()
};
error!("RUST PANIC: {} at {}", message, location);
}));
crate::host::register_all();
match rong::bg::handle() {
Some(handle) => lingxia_platform::init(handle),
None => {
error!("rong::bg not started; using fallback threads");
}
}
let runtime_arc = Arc::new(runtime.clone());
let _ = RUNTIME.set(runtime_arc.clone());
let _ = resolve_or_cache_device_fingerprint();
if let Err(e) = prepare_directory_structure(runtime_arc.clone()) {
error!("Failed to prepare directory structure: {}", e);
return None;
}
match load_app_config(runtime_arc.clone()) {
Ok(config) => {
let home_lxapp_appid = config.home_lxapp_appid.clone();
let home_lxapp_version = &config.home_lxapp_version;
let app_version = config.product_version.clone();
let stored_app_version = match metadata::app_version_get() {
Ok(version) => version,
Err(e) => {
warn!("Failed to read host app version metadata: {}", e);
None
}
};
let app_version_changed = stored_app_version
.as_deref()
.map_or(true, |version| version != app_version);
if app_version_changed {
if let Err(e) = crate::update::UpdateManager::install_from_assets(
runtime_arc.clone(),
&home_lxapp_appid,
home_lxapp_version,
) {
error!("Failed to install home LxApp: {}", e);
return None;
}
} else {
let has_pending_home_update =
metadata::downloaded_get(&home_lxapp_appid, ReleaseType::Release)
.map(|record| record.is_some())
.unwrap_or(false);
if has_pending_home_update {
match crate::update::UpdateManager::apply_downloaded_update(
runtime_arc.clone(),
&home_lxapp_appid,
ReleaseType::Release,
) {
Ok(()) => {
info!("Applied pending home lxapp update before startup")
.with_appid(home_lxapp_appid.clone());
}
Err(e) => {
warn!("Failed to apply pending home lxapp update: {}", e)
.with_appid(home_lxapp_appid.clone());
}
}
}
}
if let Err(e) = metadata::app_version_set(&app_version) {
warn!("Failed to persist host app version: {}", e);
}
crate::browser::preload_builtin_browser_assets(runtime_arc.clone());
let num_workers = get_num_workers();
let executor = LxAppExecutor::init(num_workers);
let lxapps_manager = Arc::new(LxApps::new(runtime, executor.clone(), num_workers));
if LXAPPS_MANAGER.set(lxapps_manager.clone()).is_err() {
error!("LxApps manager singleton had been initialized by another instance");
return None;
}
let home_lxapp = LxApp::new_as_home(
home_lxapp_appid.clone(),
runtime_arc.clone(),
executor.clone(),
);
let initial_route = home_lxapp.config.get_initial_route();
home_lxapp.state.lock().unwrap().startup_options.path = initial_route;
let home_lxapp_arc = Arc::new(home_lxapp);
lxapps_manager
.lxapps
.insert(home_lxapp_appid.clone(), home_lxapp_arc.clone());
if let Err(e) = home_lxapp_arc
.executor
.create_app_svc(home_lxapp_arc.clone())
{
error!("Failed to trigger home app service: {}", e)
.with_appid(home_lxapp_appid.clone());
}
info!("LxApps initialized successfully");
spawn_cache_cleanup(runtime_arc.clone());
UpdateManager::spawn_app_update_flow(runtime_arc.clone(), Some(app_version.clone()));
Some(home_lxapp_appid)
}
Err(e) => {
let error_message = match e {
LxAppError::InvalidParameter(msg) => {
format!("Configuration validation failed: {}", msg)
}
LxAppError::InvalidJsonFile(msg) => {
format!("Invalid app.json file: {}", msg)
}
LxAppError::IoError(msg) => {
format!("I/O error while reading configuration: {}", msg)
}
_ => format!("Failed to load app configuration: {}", e),
};
error!("{}", error_message);
None
}
}
}
pub(crate) fn get_lxapps_manager() -> Option<Arc<LxApps>> {
LXAPPS_MANAGER.get().cloned()
}
pub fn get_platform() -> Option<Arc<Platform>> {
RUNTIME
.get()
.cloned()
.or_else(|| LXAPPS_MANAGER.get().map(|manager| manager.runtime.clone()))
}
pub fn get_locale() -> String {
RUNTIME
.get()
.map(|runtime| runtime.get_system_locale().to_string())
.unwrap_or_else(|| "en-US".to_string())
}
pub fn try_get(appid: &str) -> Option<Arc<LxApp>> {
LXAPPS_MANAGER
.get()
.and_then(|manager| manager.lxapps.get(appid).map(|lxapp| lxapp.clone()))
}
pub(crate) fn get(appid: String) -> Arc<LxApp> {
try_get(&appid).expect("LxApp not found")
}
pub fn on_low_memory() {
if let Some(manager) = LXAPPS_MANAGER.get() {
info!("on_low_memory triggered, evicting least recently used app.");
manager.evict_lru_lxapp();
}
}
pub fn get_current_lxapp() -> (String, String, u64) {
if let Some(manager) = LXAPPS_MANAGER.get()
&& let Some(current_appid) = manager.peek_lxapp_stack()
&& let Some(lxapp) = manager.lxapps.get(¤t_appid)
{
let current_path = lxapp.peek_current_page().unwrap_or_default();
let current_session = lxapp.session_id();
info!(
"Peek {}:{} (session={}) from lxapp stack",
current_appid, current_path, current_session
);
return (current_appid, current_path, current_session);
}
(String::new(), String::new(), 0)
}
pub fn is_pull_down_refresh_enabled(appid: &str, path: &str) -> bool {
try_get(appid)
.map(|lxapp| lxapp.is_pull_down_refresh_enabled(path))
.unwrap_or(false)
}
pub fn is_lxapp_open(lxappid: &str) -> bool {
if let Some(manager) = LXAPPS_MANAGER.get()
&& let Some(app) = manager.lxapps.get(lxappid)
{
return app.is_opened();
}
false
}
fn normalize_page_path(path: &str) -> &str {
path.trim_start_matches('/')
}
fn strip_extension(path: &str) -> &str {
for ext in [".tsx", ".jsx", ".vue"] {
if let Some(p) = path.strip_suffix(ext) {
return p;
}
}
path
}
fn find_matching_page_path<'a>(pages: &'a [String], path: &str) -> Option<&'a str> {
let path = normalize_page_path(path);
let path_no_ext = strip_extension(path);
pages
.iter()
.find(|p| {
let p = normalize_page_path(p);
p == path || strip_extension(p) == path_no_ext
})
.map(|s| s.as_str())
}
fn extract_plugin_page_path(url: &str) -> Option<String> {
let (path, _) = crate::startup::split_path_query(url);
crate::plugin::parse_plugin_url(&path)
.or_else(|| crate::plugin::parse_plugin_page_path(&path))
.map(|(_, page_path)| page_path)
}
fn plugin_page_map_contains(
pages: &std::collections::BTreeMap<String, String>,
requested_path: &str,
resolved_path: &str,
) -> bool {
let requested = normalize_page_path(requested_path);
let resolved = normalize_page_path(resolved_path);
pages.iter().any(|(key, value)| {
let key = normalize_page_path(key);
let value = normalize_page_path(value);
key == requested || value == requested || key == resolved || value == resolved
})
}