fix(agents): import standalone JRE offline packages

This commit is contained in:
zipg authored and GitHub committed 2026-09-15 18:09:01 +08:00
1 parent 2fd8f42246
commit 168f854f8a
20 files changed
+306 -13

No files matched your search

+127
View File
@@ -1065,6 +1065,11 @@ pub async fn import_agents_from_zip(
pub fn inspect_offline_package(package_path: &Path) -> Result<OfflineImportPlan, String> {
if is_tar_zstd_package(package_path) {
if let Some(info) = tar_zstd_jre_package_info(package_path)? {
let staging = tempfile::tempdir().map_err(|error| error.to_string())?;
extract_and_validate_standalone_jre(package_path, staging.path(), &info)?;
return Ok(OfflineImportPlan { driver_keys: Vec::new(), includes_jre: true });
}
inspect_tar_zstd_driver_package(package_path)
} else {
inspect_offline_zip(package_path)
@@ -1077,6 +1082,9 @@ pub async fn import_agents_from_package(
progress: impl Fn(AgentProgressEvent),
) -> Result<OfflineImportResult, String> {
if is_tar_zstd_package(package_path) {
if let Some(info) = tar_zstd_jre_package_info(package_path)? {
return import_tar_zstd_jre_package(am, package_path, &info, &progress).await;
}
import_tar_zstd_driver_package(am, package_path, |event| {
progress(AgentProgressEvent {
operation_id: None,
@@ -2467,6 +2475,125 @@ pub struct OfflineImportResult {
pub drivers_skipped: Vec<String>,
}
#[derive(Debug)]
struct TarZstdJrePackageInfo {
key: String,
version: String,
}
fn tar_zstd_jre_package_info(package_path: &Path) -> Result<Option<TarZstdJrePackageInfo>, String> {
let file = std::fs::File::open(package_path).map_err(|error| error.to_string())?;
let decoder = zstd::stream::read::Decoder::new(file).map_err(|error| error.to_string())?;
let mut archive = tar::Archive::new(decoder);
let mut release = None;
let mut has_registry = false;
let mut invalid_jre_entry = false;
for entry in archive.entries().map_err(|error| error.to_string())? {
let mut entry = entry.map_err(|error| error.to_string())?;
let name = safe_archive_entry_name(&entry.path().map_err(|error| error.to_string())?)?;
has_registry |= name == "agent-registry.json";
invalid_jre_entry |= !(name == "dbx-jre" || name.starts_with("dbx-jre/"))
|| !(entry.header().entry_type().is_file()
|| entry.header().entry_type().is_dir()
|| (entry.header().entry_type().is_symlink() && standalone_jre_link_is_safe(&entry, &name)?));
if name == "dbx-jre/release" {
if release.is_some() || !entry.header().entry_type().is_file() || entry.size() > 64 * 1024 {
return Err("Invalid offline JRE release metadata".to_string());
}
let mut text = String::new();
entry.read_to_string(&mut text).map_err(|error| error.to_string())?;
release = Some(text);
}
}
if has_registry {
return Ok(None);
}
let Some(release) = release else { return Ok(None) };
if invalid_jre_entry {
return Err("Offline JRE package contains an unexpected or non-regular entry".to_string());
}
let version = release
.lines()
.find_map(|line| line.strip_prefix("JAVA_VERSION=").map(|value| value.trim().trim_matches('"')))
.ok_or("Offline JRE package is missing JAVA_VERSION")?;
let key = version.split('.').next().unwrap_or("");
if key.is_empty() || !key.bytes().all(|byte| byte.is_ascii_digit()) || key == "0" {
return Err("Invalid offline JRE JAVA_VERSION".to_string());
}
validate_offline_identifier(version, "JRE version")?;
Ok(Some(TarZstdJrePackageInfo { key: key.to_string(), version: version.to_string() }))
}
fn standalone_jre_link_is_safe<R: Read>(entry: &tar::Entry<'_, R>, name: &str) -> Result<bool, String> {
let Some(target) = entry.link_name().map_err(|error| error.to_string())? else { return Ok(false) };
let mut parts: Vec<_> = name.split('/').collect();
parts.pop();
for component in target.components() {
match component {
std::path::Component::Normal(value) => {
let Some(value) = value.to_str() else { return Ok(false) };
parts.push(value);
}
std::path::Component::CurDir => {}
std::path::Component::ParentDir if parts.len() > 1 => {
parts.pop();
}
_ => return Ok(false),
}
}
Ok(parts.first() == Some(&"dbx-jre"))
}
fn extract_and_validate_standalone_jre(
package_path: &Path,
staging: &Path,
info: &TarZstdJrePackageInfo,
) -> Result<(), String> {
extract_jre_archive(package_path, staging, Some(ArtifactFormat::TarZstd))?;
let java = staging.join("bin").join(if cfg!(windows) { "java.exe" } else { "java" });
if !java.is_file() {
return Err(format!(
"Offline JRE {} package does not contain a Java executable for {}",
info.key,
AgentManager::current_platform()
));
}
validate_native_agent_binary(&java).map_err(|_| {
format!("Offline JRE {} package does not support platform: {}", info.key, AgentManager::current_platform())
})?;
mark_executable(&java)
}
async fn import_tar_zstd_jre_package(
am: &AgentManager,
package_path: &Path,
info: &TarZstdJrePackageInfo,
progress: &impl Fn(AgentProgressEvent),
) -> Result<OfflineImportResult, String> {
let _installation_guard = am.installation_operation_lock.write().await;
std::fs::create_dir_all(am.base_dir()).map_err(|error| error.to_string())?;
let staging = tempfile::Builder::new()
.prefix(".jre-offline-import-")
.tempdir_in(am.base_dir())
.map_err(|error| error.to_string())?;
progress(AgentProgressEvent::step("jre-extract"));
extract_and_validate_standalone_jre(package_path, staging.path(), info)?;
// Validate before stopping active daemons or replacing a working runtime.
am.stop_daemons().await;
let pending_cleanup = replace_imported_jre_dir(staging.path(), &am.jre_dir(&info.key))?;
am.mutate_state(|state| {
state.jre_versions.insert(info.key.clone(), info.version.clone());
if let Some(path) = pending_cleanup {
state.pending_jre_cleanup.push(path);
}
})?;
Ok(OfflineImportResult {
jre_installed: vec![info.key.clone()],
drivers_installed: Vec::new(),
drivers_skipped: Vec::new(),
})
}
#[derive(Debug, Clone)]
pub struct OfflineImportPlan {
pub driver_keys: Vec<String>,
+100
View File
@@ -14,6 +14,106 @@ fn test_manager(name: &str) -> AgentManager {
AgentManager::new_with_base_dir(dir)
}
fn write_standalone_jre(path: &std::path::Path, version: &str, java: Option<&[u8]>) {
let encoder = zstd::stream::write::Encoder::new(std::fs::File::create(path).unwrap(), 0).unwrap();
let mut archive = tar::Builder::new(encoder);
let mut append = |name: &str, bytes: &[u8]| {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o755);
header.set_cksum();
archive.append_data(&mut header, name, bytes).unwrap();
};
append("dbx-jre/release", format!("JAVA_VERSION=\"{version}\"\n").as_bytes());
if let Some(java) = java {
append(if cfg!(windows) { "dbx-jre/bin/java.exe" } else { "dbx-jre/bin/java" }, java);
}
archive.into_inner().unwrap().finish().unwrap();
}
#[tokio::test]
async fn standalone_jre_import_works_without_registry_and_with_renamed_upload() {
let dir = tempfile::tempdir().unwrap();
let package = dir.path().join("agent-offline-upload.tar.zst");
let binary = current_platform_native_binary();
write_standalone_jre(&package, "21.0.12.1", Some(&binary));
let plan = inspect_offline_package(&package).unwrap();
assert!(plan.includes_jre);
assert!(plan.driver_keys.is_empty());
let manager = AgentManager::new_with_base_dir(dir.path().join("agents"));
let result = import_agents_from_package(&manager, &package, |_| {}).await.unwrap();
assert_eq!(result.jre_installed, vec!["21"]);
assert!(result.drivers_installed.is_empty());
assert!(manager.is_jre_installed("21"));
assert_eq!(manager.load_state().jre_versions["21"], "21.0.12.1");
}
#[tokio::test]
async fn standalone_jre_invalid_package_preserves_existing_runtime() {
let dir = tempfile::tempdir().unwrap();
let manager = AgentManager::new_with_base_dir(dir.path().join("agents"));
let package = dir.path().join("upload.tar.zst");
write_standalone_jre(&package, "21.0.12.1", Some(&current_platform_native_binary()));
import_agents_from_package(&manager, &package, |_| {}).await.unwrap();
for (version, java) in
[("21.0.13", None), ("../../outside", Some(b"bad".as_slice())), ("21.0.13", Some(b"bad".as_slice()))]
{
write_standalone_jre(&package, version, java);
assert!(inspect_offline_package(&package).is_err());
assert!(import_agents_from_package(&manager, &package, |_| {}).await.is_err());
assert_eq!(manager.load_state().jre_versions["21"], "21.0.12.1");
assert!(manager.is_jre_installed("21"));
}
write_standalone_jre(&package, "21.0.13", Some(&current_platform_native_binary()));
import_agents_from_package(&manager, &package, |_| {}).await.unwrap();
assert_eq!(manager.load_state().jre_versions["21"], "21.0.13");
}
#[test]
fn standalone_jre_rejects_wrong_cpu_architecture() {
let dir = tempfile::tempdir().unwrap();
let package = dir.path().join("upload.tar.zst");
write_standalone_jre(&package, "21.0.12.1", Some(&native_binary_for_arch(!cfg!(target_arch = "aarch64"))));
assert!(inspect_offline_package(&package).unwrap_err().contains("does not support platform"));
}
#[test]
fn standalone_jre_rejects_links_outside_archive() {
let dir = tempfile::tempdir().unwrap();
let package = dir.path().join("upload.tar.zst");
let encoder = zstd::stream::write::Encoder::new(std::fs::File::create(&package).unwrap(), 0).unwrap();
let mut archive = tar::Builder::new(encoder);
let release = b"JAVA_VERSION=\"21.0.12.1\"\n";
let mut header = tar::Header::new_gnu();
header.set_size(release.len() as u64);
header.set_mode(0o644);
header.set_cksum();
archive.append_data(&mut header, "dbx-jre/release", release.as_slice()).unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Symlink);
header.set_size(0);
header.set_mode(0o777);
archive.append_link(&mut header, "dbx-jre/lib/escape", "../../outside").unwrap();
archive.into_inner().unwrap().finish().unwrap();
assert!(inspect_offline_package(&package).unwrap_err().contains("non-regular entry"));
assert!(!dir.path().join("outside").exists());
}
#[tokio::test]
#[ignore = "requires DBX_TEST_JRE_PACKAGE containing an official package for the current platform"]
async fn standalone_jre_official_package_runs_java_after_import() {
let package = std::path::PathBuf::from(std::env::var("DBX_TEST_JRE_PACKAGE").unwrap());
let dir = tempfile::tempdir().unwrap();
let manager = AgentManager::new_with_base_dir(dir.path().join("agents"));
assert!(inspect_offline_package(&package).unwrap().includes_jre);
let result = import_agents_from_package(&manager, &package, |_| {}).await.unwrap();
let key = &result.jre_installed[0];
let java = manager.jre_dir(key).join("bin").join(if cfg!(windows) { "java.exe" } else { "java" });
let output = std::process::Command::new(java).arg("-version").output().unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
println!("{}", String::from_utf8_lossy(&output.stderr));
}
fn registry_with_driver(db_type: &str, version: &str, jre: &str) -> AgentRegistry {
let mut drivers = std::collections::HashMap::new();
drivers.insert(
+4 -1
View File
@@ -304,7 +304,10 @@ pub async fn import_agents_from_zip(
let result = result?;
send_progress_event(&tx, AgentProgressEvent::step("done").with_operation_id(&operation_id));
return Ok(Json(serde_json::json!({ "count": result.drivers_installed.len() as u32 })));
return Ok(Json(serde_json::json!({
"count": result.drivers_installed.len() as u32,
"jreCount": result.jre_installed.len(),
})));
}
Err(AppError::from("No file uploaded".to_string()))