use super::*;
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(USER_CACHE_DIR),
lingxia_transfer::dir(&runtime.app_data_dir()),
data_dir.join(LINGXIA_DIR).join(STORAGE_DIR),
cache_dir.join(LINGXIA_DIR).join(LXAPPS_DIR).join(TEMP_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 = lingxia_app_context::cache_max_size_bytes();
if max_bytes == 0 {
info!("Cache cleanup disabled (cacheMaxSizeMB=0)");
return;
}
std::mem::drop(crate::executor::spawn(async move {
let cache_base_dir = runtime
.app_data_dir()
.join(LINGXIA_DIR)
.join(USER_CACHE_DIR);
cleanup_cache_base_dir(&cache_base_dir, max_bytes);
}));
}
fn cleanup_cache_base_dir(cache_base_dir: &Path, max_bytes: u64) {
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() {
lingxia_service::storage::cleanup_cache_dir(&path, max_bytes);
}
}
}
}
fn installed_home_version(
appid: &str,
release_type: Channel,
) -> Result<Option<Version>, LxAppError> {
let Some(record) = metadata::get(appid, release_type)? else {
return Ok(None);
};
let install_path = Path::new(&record.install_path);
let manifest_path = install_path.join("lxapp.json");
if record.install_path.trim().is_empty() || !install_path.is_dir() || !manifest_path.is_file() {
let _ = metadata::remove(appid, release_type);
return Ok(None);
}
let manifest = fs::read_to_string(&manifest_path).map_err(LxAppError::from)?;
let manifest_json: serde_json::Value = serde_json::from_str(&manifest)
.map_err(|e| LxAppError::InvalidJsonFile(format!("{}: {}", manifest_path.display(), e)))?;
let config = LxAppConfig::from_value(manifest_json)
.map_err(|e| LxAppError::InvalidJsonFile(format!("{}: {}", manifest_path.display(), e)))?;
if config.get_initial_route().trim().is_empty() {
warn!(
"Installed home lxapp manifest has no pages: {}; reinstalling bundled home app",
manifest_path.display()
)
.with_appid(appid.to_string());
let _ = metadata::remove(appid, release_type);
return Ok(None);
}
Ok(Some(Version {
major: record.version.major,
minor: record.version.minor,
patch: record.version.patch,
}))
}
pub fn dev_session_active() -> bool {
let env_active = std::env::var("LINGXIA_DEV_WS_URL")
.map(|value| !value.trim().is_empty())
.unwrap_or(false);
if env_active {
return true;
}
lingxia_app_context::app_config()
.and_then(|config| config.dev_ws_url.as_deref())
.map(|value| !value.trim().is_empty())
.unwrap_or(false)
}
static RUNNER_HOST: AtomicBool = AtomicBool::new(false);
pub fn register_runner_host() {
RUNNER_HOST.store(true, Ordering::Release);
}
pub fn runner_active() -> bool {
runner_identity(
RUNNER_HOST.load(Ordering::Acquire),
std::env::var("LINGXIA_RUNNER").ok().as_deref(),
)
}
fn runner_identity(registered: bool, marker: Option<&str>) -> bool {
registered || marker.is_some_and(|value| !value.trim().is_empty())
}
#[cfg(test)]
mod runner_identity_tests {
use super::runner_identity;
#[test]
fn native_runner_identity_survives_missing_or_empty_environment() {
assert!(runner_identity(true, None));
assert!(runner_identity(true, Some("")));
assert!(runner_identity(true, Some(" ")));
assert!(runner_identity(false, Some("1")));
assert!(!runner_identity(false, None));
assert!(!runner_identity(false, Some("")));
}
}
pub fn init(runtime: Platform) -> Result<Option<String>, LxAppError> {
init_with_native_authority(runtime, None, None, Box::new(|_, _, _, _| Ok(())))
.map(|(home_app_id, _, _)| home_app_id)
}
type NativeAuthorityInstaller = dyn FnOnce(
crate::NativeControlPlaneAuthority,
crate::NativeControlPlaneAuthority,
crate::NativeControlPlaneAuthority,
crate::terminal_automation::TerminalAutomationAuthority,
) -> Result<(), LxAppError>
+ 'static;
#[unsafe(export_name = "lingxia_lxapp_platform_bootstrap_v1")]
pub(crate) extern "Rust" fn platform_bootstrap(
runtime: Platform,
app_grant_resolver: Option<Arc<crate::host::AppResourceGrantResolver>>,
devtools_grant_resolver: Option<Arc<crate::host::DevtoolsResourceGrantResolver>>,
install_authorities: Box<NativeAuthorityInstaller>,
) -> Result<
(
Option<String>,
crate::terminal_automation::TerminalAutomationAuthority,
crate::NativeControlPlaneAuthority,
),
LxAppError,
> {
init_with_native_authority(
runtime,
app_grant_resolver,
devtools_grant_resolver,
install_authorities,
)
}
fn init_with_native_authority(
runtime: Platform,
app_grant_resolver: Option<Arc<crate::host::AppResourceGrantResolver>>,
devtools_grant_resolver: Option<Arc<crate::host::DevtoolsResourceGrantResolver>>,
install_authorities: Box<NativeAuthorityInstaller>,
) -> Result<
(
Option<String>,
crate::terminal_automation::TerminalAutomationAuthority,
crate::NativeControlPlaneAuthority,
),
LxAppError,
> {
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_builtin_routes();
let runtime_arc = Arc::new(runtime.clone());
super::runtime_registry::set_runtime(runtime_arc.clone());
let native_token = crate::terminal_automation::NativeHostRuntimeToken::new(&runtime_arc);
crate::host::install_bootstrap_resource_grant_resolvers(
app_grant_resolver,
devtools_grant_resolver,
)
.map_err(|message| LxAppError::Runtime(message.to_string()))?;
let terminal_authority =
crate::terminal_automation::TerminalAutomationAuthority::for_native_runtime(&native_token);
install_authorities(
crate::NativeControlPlaneAuthority::for_native_runtime(&native_token),
crate::NativeControlPlaneAuthority::for_native_runtime(&native_token),
crate::NativeControlPlaneAuthority::for_native_runtime(&native_token),
terminal_authority.clone(),
)?;
let browser_registration_authority =
crate::NativeControlPlaneAuthority::for_native_runtime(&native_token);
if let Err(e) = prepare_directory_structure(runtime_arc.clone()) {
error!("Failed to prepare directory structure: {}", e);
return Err(e);
}
let num_workers = get_num_workers();
let executor = LxAppWorkers::init(num_workers);
let lxapps_manager = Arc::new(LxApps::new(runtime, executor.clone(), num_workers));
if let Err(e) = super::runtime_registry::set_lxapps_manager(lxapps_manager.clone()) {
error!("{}", e);
return Err(LxAppError::Runtime(e.to_string()));
}
let (Some(home_app_id), Some(home_app_version)) = (
lingxia_app_context::home_app_id(),
lingxia_app_context::home_app_version(),
) else {
info!("LxApps initialized without a home lxapp");
spawn_cache_cleanup(runtime_arc);
return Ok((None, terminal_authority, browser_registration_authority));
};
let home_app_id = home_app_id.to_string();
let bundled_home_version = match Version::parse(home_app_version) {
Ok(version) => version,
Err(e) => {
error!(
"Invalid bundled home lxapp version '{}': {}",
home_app_version, e
)
.with_appid(home_app_id.clone());
return Err(LxAppError::InvalidParameter(format!(
"invalid bundled home lxapp version '{home_app_version}': {e}"
)));
}
};
let home_channel = crate::default_channel();
let installed_home_version = match installed_home_version(&home_app_id, home_channel) {
Ok(version) => version,
Err(e) => {
warn!("Failed to inspect installed home lxapp version: {}", e)
.with_appid(home_app_id.clone());
None
}
};
let dev_session_active = dev_session_active();
let should_reinstall_home = dev_session_active
|| installed_home_version
.as_ref()
.map(|installed| installed < &bundled_home_version)
.unwrap_or(true);
let home_is_dev_sourced = matches!(
super::lxapp_bundle_source_for(&home_app_id),
Some(super::LxAppBundleSource::DevPath { .. })
);
if home_is_dev_sourced {
info!("Home lxapp is dev-sourced; serving from dev root, skipping bundled-asset install")
.with_appid(home_app_id.clone());
} else if should_reinstall_home {
let reason = if dev_session_active {
"dev session active; refreshing from bundled assets".to_string()
} else {
match installed_home_version {
None => "home lxapp is not installed or install is invalid".to_string(),
Some(installed) => format!(
"bundled version {} is newer than installed {}",
bundled_home_version, installed
),
}
};
info!("Installing home lxapp from bundled assets: {}", reason)
.with_appid(home_app_id.clone());
if let Err(e) = crate::update::UpdateManager::install_from_assets(
runtime_arc.clone(),
&home_app_id,
home_app_version,
) {
error!("Failed to install home LxApp: {}", e);
return Err(e);
}
} else {
let has_pending_home_update = metadata::downloaded_get(&home_app_id, home_channel)
.map(|record| record.is_some())
.unwrap_or(false);
if has_pending_home_update {
match crate::update::UpdateManager::apply_downloaded_update(
runtime_arc.clone(),
&home_app_id,
home_channel,
) {
Ok(()) => {
info!("Applied pending home lxapp update before startup")
.with_appid(home_app_id.clone());
}
Err(e) => {
warn!("Failed to apply pending home lxapp update: {}", e)
.with_appid(home_app_id.clone());
}
}
}
}
let home_app = match lxapps_manager.initialize_home_lxapp(home_app_id.clone()) {
Ok(app) => app,
Err(e) => {
error!("Failed to setup home LxApp: {}", e).with_appid(home_app_id.clone());
return Err(e);
}
};
if let Err(e) = home_app.executor.create_app_svc(home_app.clone()) {
error!("Failed to trigger home app service: {}", e).with_appid(home_app_id.clone());
}
info!("LxApps initialized successfully");
spawn_cache_cleanup(runtime_arc.clone());
Ok((
Some(home_app_id),
terminal_authority,
browser_registration_authority,
))
}