feat(agents): add versioned offline driver packages

This commit is contained in:
t8y2 2026-07-21 13:42:39 +08:00
parent 8d68a527a0
commit 79cd12d13a
29 changed files with 1372 additions and 162 deletions

View File

@ -298,6 +298,11 @@ jobs:
find artifacts -name 'dbx-jre-*.tar.gz' -exec cp {} release/ \;
ls -lh release/
- name: Add versions to agent artifact filenames
env:
MODULE_VERSIONS: ${{ needs.bump-versions.outputs.versions }}
run: python3 agents/scripts/version_agent_artifacts.py release "$MODULE_VERSIONS"
- name: Generate agent-registry.json
env:
MODULE_VERSIONS: ${{ needs.bump-versions.outputs.versions }}
@ -311,6 +316,10 @@ jobs:
echo "$MODULE_VERSIONS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('$name','${RELEASE_VERSION}'))"
}
module_names() {
echo "$MODULE_VERSIONS" | python3 -c 'import sys,json; print("\n".join(sorted(json.load(sys.stdin))))'
}
generate_jar_entry() {
local name="$1" label="$2" file="$3" jre_key="$4" version="$5" external_driver="$6" native_json="$7"
local sha256=$(sha256sum "$file" | cut -d' ' -f1)
@ -376,12 +385,13 @@ jobs:
}
generate_native_platforms() {
local name="$1"
local name="$1" version="$2"
local PLATFORMS=""
for f in release/dbx-agent-${name}-*; do
for p in macos-aarch64 macos-x64 linux-aarch64 linux-x64 windows-aarch64 windows-x64; do
extension=""
[[ "$p" == windows-* ]] && extension=".exe"
f="release/dbx-agent-${name}-${version}-${p}${extension}"
[ ! -f "$f" ] && continue
[[ "$f" != *.jar ]] || continue
p=$(basename "$f" | sed "s/dbx-agent-${name}-//; s/\\.exe$//")
sha256=$(sha256sum "$f" | cut -d' ' -f1)
size=$(stat -c%s "$f")
url="https://github.com/${REPO}/releases/download/${TAG}/$(basename $f)"
@ -401,25 +411,26 @@ jobs:
JRE21_PLATFORMS=$(generate_jre_platforms "21")
DRIVERS=""
for f in release/dbx-agent-*.jar; do
name=$(basename "$f" .jar | sed 's/dbx-agent-//')
for name in $(module_names); do
version=$(get_module_version "$name")
f="release/dbx-agent-${name}-${version}.jar"
[ -f "$f" ] || continue
label=$(unzip -p "$f" META-INF/MANIFEST.MF | awk -F': ' 'BEGIN{IGNORECASE=1} /^Agent-Label:/ {sub(/\r$/, "", $2); print $2; exit}' || echo "$name")
[ -z "$label" ] && label="$name"
external_driver=$(unzip -p "$f" META-INF/MANIFEST.MF | awk -F': ' 'BEGIN{IGNORECASE=1} /^Agent-External-Driver:/ {sub(/\r$/, "", $2); print tolower($2); exit}' || true)
[ "$external_driver" = "true" ] || external_driver="false"
jre_key=$(detect_jre_key "$name")
version=$(get_module_version "$name")
native_json=$(generate_native_platforms "$name")
native_json=$(generate_native_platforms "$name" "$version")
[ -n "$DRIVERS" ] && DRIVERS="${DRIVERS},"$'\n'
DRIVERS="${DRIVERS}$(generate_jar_entry "$name" "$label" "$f" "$jre_key" "$version" "$external_driver" "$native_json")"
done
for name in oracle xugu kingbase; do
[ -f "release/dbx-agent-${name}.jar" ] && continue
native_json=$(generate_native_platforms "$name")
version=$(get_module_version "$name")
[ -f "release/dbx-agent-${name}-${version}.jar" ] && continue
native_json=$(generate_native_platforms "$name" "$version")
[ -z "$native_json" ] && continue
label=$(native_only_label "$name")
jre_key=$(detect_jre_key "$name")
version=$(get_module_version "$name")
[ -n "$DRIVERS" ] && DRIVERS="${DRIVERS},"$'\n'
DRIVERS="${DRIVERS}$(generate_native_entry "$name" "$label" "$jre_key" "$version" "$native_json")"
done
@ -444,6 +455,9 @@ jobs:
echo "=== agent-registry.json ==="
cat release/agent-registry.json
- name: Build single-driver ZIP bundles
run: python3 agents/scripts/build_driver_zips.py release
- name: Build offline ZIP bundles
run: bash agents/scripts/build_offline_zip.sh release
@ -455,8 +469,12 @@ jobs:
run: |
NOTES=""
DRIVER_NAMES=()
for f in release/dbx-agent-*.jar; do
DRIVER_NAMES+=("$(basename "$f" .jar | sed 's/dbx-agent-//')")
module_names() {
echo "$MODULE_VERSIONS" | python3 -c 'import sys,json; print("\n".join(sorted(json.load(sys.stdin))))'
}
for name in $(module_names); do
version=$(echo "$MODULE_VERSIONS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('$name',''))")
[ -f "release/dbx-agent-${name}-${version}.jar" ] && DRIVER_NAMES+=("$name")
done
for name in oracle xugu kingbase; do
[ -f "release/dbx-agent-${name}.jar" ] && continue
@ -478,7 +496,7 @@ jobs:
new_ver=$(echo "$MODULE_VERSIONS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('$name',''))")
[ "$old_ver" = "$new_ver" ] && continue
jar_file="release/dbx-agent-${name}.jar"
jar_file="release/dbx-agent-${name}-${new_ver}.jar"
if [ -f "$jar_file" ]; then
label=$(unzip -p "$jar_file" META-INF/MANIFEST.MF | awk -F': ' 'BEGIN{IGNORECASE=1} /^Agent-Label:/ {sub(/\r$/, "", $2); print $2; exit}' || echo "$name")
[ -z "$label" ] && label="$name"
@ -531,6 +549,16 @@ jobs:
git push origin refs/tags/agents-latest --force
if gh release view agents-latest --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
# Versioned driver assets change names every release, so remove the
# previous set before uploading to keep agents-latest unambiguous.
gh release view agents-latest --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' |
while IFS= read -r asset; do
case "$asset" in
dbx-agent-*.jar|dbx-agent-*.zip|dbx-agent-oracle-*|dbx-agent-xugu-*|dbx-agent-kingbase-*)
gh release delete-asset agents-latest "$asset" --repo "$GITHUB_REPOSITORY" --yes
;;
esac
done
gh release upload agents-latest release/* --repo "$GITHUB_REPOSITORY" --clobber
gh release edit agents-latest \
--repo "$GITHUB_REPOSITORY" \
@ -563,11 +591,14 @@ jobs:
# Upload only changed driver JARs
UPLOADED=0
SKIPPED=0
for f in release/dbx-agent-*.jar; do
[ ! -f "$f" ] && continue
name=$(basename "$f" .jar | sed 's/dbx-agent-//')
module_names() {
echo "$MODULE_VERSIONS" | python3 -c 'import sys,json; print("\n".join(sorted(json.load(sys.stdin))))'
}
for name in $(module_names); do
old_ver=$(echo "$PREV_VERSIONS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('$name',''))" 2>/dev/null)
new_ver=$(echo "$MODULE_VERSIONS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('$name',''))" 2>/dev/null)
f="release/dbx-agent-${name}-${new_ver}.jar"
[ ! -f "$f" ] && continue
if [ "$old_ver" = "$new_ver" ] && [ -n "$old_ver" ]; then
echo "Skip $(basename $f) (unchanged $old_ver)"
SKIPPED=$((SKIPPED + 1))
@ -581,6 +612,7 @@ jobs:
done
echo "Drivers: $UPLOADED uploaded, $SKIPPED skipped"
# Upload versioned native binaries and all single-driver ZIP packages.
for f in release/dbx-agent-*; do
[ -f "$f" ] || continue
[[ "$f" != *.jar ]] || continue

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
import argparse
import copy
import json
import zipfile
from pathlib import Path
from urllib.parse import urlparse
def artifact_filename(url: str) -> str:
return Path(urlparse(url).path).name
def write_driver_zip(output: Path, registry: dict, source: Path) -> None:
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
archive.writestr("agent-registry.json", json.dumps(registry, ensure_ascii=False, indent=2) + "\n")
archive.write(source, f"drivers/{source.name}")
def build_driver_zips(release_dir: Path) -> list[Path]:
registry_path = release_dir / "agent-registry.json"
registry = json.loads(registry_path.read_text(encoding="utf-8"))
outputs: list[Path] = []
for driver_name, driver in registry.get("drivers", {}).items():
version = driver["version"]
jar_artifact = driver.get("jar")
if jar_artifact and jar_artifact.get("size", 0) > 0:
filename = artifact_filename(jar_artifact["url"])
source = release_dir / filename
if not source.is_file():
raise FileNotFoundError(f"Java agent artifact missing for {driver_name}: {source}")
package_driver = copy.deepcopy(driver)
package_driver.pop("native", None)
package_driver["jar"] = {"url": source.name, "size": source.stat().st_size}
package_registry = {"jres": {}, "drivers": {driver_name: package_driver}}
output = release_dir / f"dbx-agent-{driver_name}-{version}.zip"
write_driver_zip(output, package_registry, source)
outputs.append(output)
for platform, artifact in driver.get("native", {}).items():
filename = artifact_filename(artifact["url"])
source = release_dir / filename
if not source.is_file():
raise FileNotFoundError(f"Native agent artifact missing for {driver_name}/{platform}: {source}")
package_driver = copy.deepcopy(driver)
package_driver.pop("jar", None)
package_driver["native"] = {platform: {"url": source.name, "size": source.stat().st_size}}
package_registry = {"jres": {}, "drivers": {driver_name: package_driver}}
output = release_dir / f"dbx-agent-{driver_name}-{version}-{platform}.zip"
write_driver_zip(output, package_registry, source)
outputs.append(output)
return outputs
def main() -> None:
parser = argparse.ArgumentParser(description="Build offline ZIPs for individual DBX agents")
parser.add_argument("release_dir", type=Path)
args = parser.parse_args()
for path in build_driver_zips(args.release_dir):
print(f"Created {path.name} ({path.stat().st_size} bytes)")
if __name__ == "__main__":
main()

View File

@ -48,7 +48,9 @@ for platform in "${PLATFORMS[@]}"; do
for jar_file in "$RELEASE_DIR"/dbx-agent-*.jar; do
[ -f "$jar_file" ] || continue
# Kingbase is distributed only as a native agent; keep legacy JDBC builds out of offline bundles.
[ "$(basename "$jar_file")" = "dbx-agent-kingbase.jar" ] && continue
case "$(basename "$jar_file")" in
dbx-agent-kingbase.jar|dbx-agent-kingbase-*.jar) continue ;;
esac
cp "$jar_file" "$WORK/drivers/"
done

View File

@ -0,0 +1,103 @@
#!/usr/bin/env python3
import json
import subprocess
import tempfile
import unittest
import zipfile
from pathlib import Path
from build_driver_zips import build_driver_zips
from version_agent_artifacts import version_agent_artifacts
class DriverReleasePackagesTest(unittest.TestCase):
def test_builds_java_and_platform_specific_native_driver_zips(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
release_dir = Path(temp_dir)
native_source = release_dir / "dbx-agent-kingbase-windows-x64.exe"
native_source.write_bytes(b"MZtest-agent")
java_source = release_dir / "dbx-agent-h2.jar"
java_source.write_bytes(b"test-jar")
versions = {"h2": "0.2.5", "oracle": "0.1.10", "xugu": "0.1.20", "kingbase": "0.1.34"}
renamed = version_agent_artifacts(release_dir, versions)
versioned_java = release_dir / "dbx-agent-h2-0.2.5.jar"
versioned_native = release_dir / "dbx-agent-kingbase-0.1.34-windows-x64.exe"
self.assertEqual(renamed, [versioned_java, versioned_native])
registry = {
"jres": {"21": {"version": "21", "platforms": {}}},
"drivers": {
"h2": {
"version": "0.2.5",
"label": "H2",
"min_app_version": "0.6.0",
"jre": "21",
"jar": {"url": f"https://example.com/{versioned_java.name}", "size": versioned_java.stat().st_size},
},
"kingbase": {
"version": "0.1.34",
"label": "人大金仓 KingbaseES",
"min_app_version": "0.6.0",
"jre": "21",
"jar": {"url": "https://example.com/legacy-placeholder.jar", "size": 0},
"native": {
"windows-x64": {
"url": f"https://example.com/{versioned_native.name}",
"size": versioned_native.stat().st_size,
}
},
},
},
}
(release_dir / "agent-registry.json").write_text(json.dumps(registry), encoding="utf-8")
outputs = build_driver_zips(release_dir)
self.assertEqual(
outputs,
[
release_dir / "dbx-agent-h2-0.2.5.zip",
release_dir / "dbx-agent-kingbase-0.1.34-windows-x64.zip",
],
)
with zipfile.ZipFile(outputs[0]) as archive:
self.assertEqual(set(archive.namelist()), {"agent-registry.json", f"drivers/{versioned_java.name}"})
package_registry = json.loads(archive.read("agent-registry.json"))
self.assertEqual(set(package_registry["drivers"]), {"h2"})
self.assertNotIn("native", package_registry["drivers"]["h2"])
self.assertEqual(package_registry["drivers"]["h2"]["jar"], {"url": versioned_java.name, "size": 8})
with zipfile.ZipFile(outputs[1]) as archive:
self.assertEqual(set(archive.namelist()), {"agent-registry.json", f"drivers/{versioned_native.name}"})
package_registry = json.loads(archive.read("agent-registry.json"))
kingbase = package_registry["drivers"]["kingbase"]
self.assertNotIn("jar", kingbase)
self.assertEqual(set(kingbase["native"]), {"windows-x64"})
self.assertEqual(
kingbase["native"]["windows-x64"],
{"url": versioned_native.name, "size": versioned_native.stat().st_size},
)
def test_full_offline_bundle_includes_versioned_native_artifact(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
release_dir = Path(temp_dir)
filename = "dbx-agent-kingbase-0.1.34-windows-x64.exe"
(release_dir / filename).write_bytes(b"MZtest-agent")
(release_dir / "dbx-jre-21-windows-x64.tar.gz").write_bytes(b"test-jre")
(release_dir / "agent-registry.json").write_text('{"jres":{},"drivers":{}}', encoding="utf-8")
subprocess.run(
["bash", str(Path(__file__).with_name("build_offline_zip.sh")), str(release_dir)],
check=True,
capture_output=True,
text=True,
)
bundle = release_dir / "dbx-agents-offline-windows-x64.zip"
self.assertTrue(bundle.is_file())
with zipfile.ZipFile(bundle) as archive:
self.assertIn(f"drivers/{filename}", archive.namelist())
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,64 @@
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
NATIVE_DRIVERS = ("oracle", "xugu", "kingbase")
PLATFORMS = (
"macos-aarch64",
"macos-x64",
"linux-aarch64",
"linux-x64",
"windows-aarch64",
"windows-x64",
)
def rename_artifact(source: Path, target: Path) -> Path | None:
if not source.exists():
return None
if target.exists():
raise FileExistsError(f"Versioned agent artifact already exists: {target}")
source.rename(target)
return target
def version_agent_artifacts(release_dir: Path, versions: dict[str, str]) -> list[Path]:
renamed: list[Path] = []
for driver, version in sorted(versions.items()):
jar = rename_artifact(
release_dir / f"dbx-agent-{driver}.jar",
release_dir / f"dbx-agent-{driver}-{version}.jar",
)
if jar:
renamed.append(jar)
for driver in NATIVE_DRIVERS:
version = versions.get(driver)
if not version:
raise ValueError(f"Missing version for native driver: {driver}")
for platform in PLATFORMS:
extension = ".exe" if platform.startswith("windows-") else ""
artifact = rename_artifact(
release_dir / f"dbx-agent-{driver}-{platform}{extension}",
release_dir / f"dbx-agent-{driver}-{version}-{platform}{extension}",
)
if artifact:
renamed.append(artifact)
return renamed
def main() -> None:
parser = argparse.ArgumentParser(description="Add module versions to DBX agent release filenames")
parser.add_argument("release_dir", type=Path)
parser.add_argument("versions_json")
args = parser.parse_args()
versions = json.loads(args.versions_json)
for path in version_agent_artifacts(args.release_dir, versions):
print(f"Versioned {path.name}")
if __name__ == "__main__":
main()

View File

@ -21,6 +21,7 @@ import { formatRuntimeBytes, formatRuntimeCpu, formatRuntimeUptime, runtimeHealt
import { addDriverInstallQueue, driverInstallProgressPercent, isDriverInstallProgressTarget, removeDriverInstallQueue, takeNextDriverInstallQueue, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
import { PRESTOSQL_DRIVER_DB_TYPE, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/database/prestoSqlBuiltinDriver";
import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection";
const { t } = useI18n();
const { toast } = useToast();
@ -550,19 +551,38 @@ async function importOfflineZip() {
}
}
async function importDriverJar(dbType: string) {
async function importDriverFile(driver: AgentDriverInfo) {
const dbType = driver.db_type;
if (isPrestoSqlBuiltinDriver(dbType)) {
await importJdbcDrivers();
return;
}
const blockers = await api.checkAgentUpdateBlockers([dbType]);
if (blockers.length > 0) {
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
return;
}
const label = driverLabel(dbType);
if (isWeb) {
const file = await chooseWebFile(".jar");
if (!file) return;
try {
await api.importAgentJar(dbType, file);
const requiresJavaRuntime = driverRequiresJavaRuntime(driver);
const isWindows = navigator.userAgent.toLowerCase().includes("windows");
const installSelectedFile = async (selected: string | File) => {
if (isOfflineDriverPackage(selected)) {
const count = await api.importAgentsFromZip(selected);
await refreshAgents();
toast(t("driverStore.offlineImportSuccess", { count }));
} else {
await api.importAgentDriver(dbType, selected);
await refreshAgents();
toast(t("driverStore.driverImportSuccess", { label }));
}
};
if (isWeb) {
// Native release assets have no extension on macOS/Linux, so do not apply
// a browser accept filter that would make the correct file unselectable.
const file = await chooseWebFile(webDriverImportAccept(requiresJavaRuntime, isWindows));
if (!file) return;
try {
await installSelectedFile(file);
} catch (e: any) {
toast(t("driverStore.driverImportFailed", { label, error: e }));
}
@ -572,13 +592,11 @@ async function importDriverJar(dbType: string) {
const selected = await open({
title: t("driverStore.chooseDriverJar"),
multiple: false,
filters: [{ name: "JAR", extensions: ["jar"] }],
filters: requiresJavaRuntime ? [{ name: "Driver package or JAR", extensions: ["zip", "jar"] }] : isWindows ? [{ name: "Driver package or executable", extensions: ["zip", "exe"] }] : undefined,
});
if (typeof selected !== "string") return;
try {
await api.importAgentJar(dbType, selected);
await refreshAgents();
toast(t("driverStore.driverImportSuccess", { label }));
await installSelectedFile(selected);
} catch (e: any) {
toast(t("driverStore.driverImportFailed", { label, error: e }));
}
@ -1293,7 +1311,7 @@ watch(driverStoreTab, (tab) => {
class="h-7 w-7 rounded-[6px] text-xs text-muted-foreground"
:title="t('driverStore.importLocalJar')"
:disabled="upgradingAll || installing !== null"
@click="importDriverJar(driver.db_type)"
@click="importDriverFile(driver)"
>
<FileUp class="h-3.5 w-3.5" />
</Button>
@ -1359,7 +1377,7 @@ watch(driverStoreTab, (tab) => {
class="h-7 w-7 rounded-[6px] text-xs text-muted-foreground"
:title="t('driverStore.importLocalJar')"
:disabled="upgradingAll || installing !== null"
@click="importDriverJar(driver.db_type)"
@click="importDriverFile(driver)"
>
<FileUp class="h-3.5 w-3.5" />
</Button>

View File

@ -4075,7 +4075,7 @@ export default {
offlineImportFailed: "Offline import failed: {error}",
driverImportSuccess: "{label} driver imported",
driverImportFailed: "Failed to import {label} driver: {error}",
chooseDriverJar: "Choose driver JAR file",
chooseDriverJar: "Choose driver file",
jreReinstallSuccess: "JRE {jre} reinstalled",
jreReinstallFailed: "Failed to reinstall JRE {jre}: {error}",
jreUninstallSuccess: "JRE {jre} uninstalled",
@ -4109,7 +4109,7 @@ export default {
upgradingProgress: "Upgrading ({current}/{total})",
upgradeAll: "Upgrade all",
queued: "Queued",
importLocalJar: "Import local JAR",
importLocalJar: "Import local driver file",
updating: "Updating",
update: "Update",
localInstall: "Local install",

View File

@ -3841,7 +3841,7 @@ export default withEnglishFallback({
offlineImportFailed: "Error en la importación offline: {error}",
driverImportSuccess: "Driver {label} importado",
driverImportFailed: "Error al importar el driver {label}: {error}",
chooseDriverJar: "Seleccionar archivo JAR del driver",
chooseDriverJar: "Seleccionar archivo del driver",
jreReinstallSuccess: "JRE {jre} reinstalado",
jreReinstallFailed: "Error al reinstalar JRE {jre}: {error}",
jreUninstallSuccess: "JRE {jre} desinstalado",
@ -3875,7 +3875,7 @@ export default withEnglishFallback({
upgradingProgress: "Actualizando ({current}/{total})",
upgradeAll: "Actualizar todo",
queued: "En cola",
importLocalJar: "Importar JAR local",
importLocalJar: "Importar archivo de driver local",
updating: "Actualizando",
update: "Actualizar",
localInstall: "Instalación local",

View File

@ -3839,7 +3839,7 @@ export default withEnglishFallback({
offlineImportFailed: "Importazione offline non riuscita: {error}",
driverImportSuccess: "Driver {label} importato",
driverImportFailed: "Importazione del driver {label} non riuscita: {error}",
chooseDriverJar: "Scegli il file JAR del driver",
chooseDriverJar: "Scegli il file del driver",
jreReinstallSuccess: "JRE {jre} reinstallato",
jreReinstallFailed: "Reinstallazione del JRE {jre} non riuscita: {error}",
jreUninstallSuccess: "JRE {jre} disinstallato",
@ -3873,7 +3873,7 @@ export default withEnglishFallback({
upgradingProgress: "Aggiornamento in corso ({current}/{total})",
upgradeAll: "Aggiorna tutti",
queued: "In coda",
importLocalJar: "Importa JAR locale",
importLocalJar: "Importa file driver locale",
updating: "Aggiornamento in corso",
update: "Aggiorna",
localInstall: "Installazione locale",

View File

@ -3839,7 +3839,7 @@ export default withEnglishFallback({
offlineImportFailed: "オフラインインポートに失敗しました: {error}",
driverImportSuccess: "{label} ドライバーをインポートしました",
driverImportFailed: "{label} ドライバーのインポートに失敗しました: {error}",
chooseDriverJar: "ドライバーJARファイルを選択",
chooseDriverJar: "ドライバーファイルを選択",
jreReinstallSuccess: "JRE {jre} を再インストールしました",
jreReinstallFailed: "JRE {jre} の再インストールに失敗しました: {error}",
jreUninstallSuccess: "JRE {jre} をアンインストールしました",
@ -3873,7 +3873,7 @@ export default withEnglishFallback({
upgradingProgress: "アップグレード中 ({current}/{total})",
upgradeAll: "すべてアップグレード",
queued: "待機中",
importLocalJar: "ローカルJARをインポート",
importLocalJar: "ローカルドライバーファイルをインポート",
updating: "更新中",
update: "更新",
localInstall: "ローカルインストール",

View File

@ -3841,7 +3841,7 @@ export default withEnglishFallback({
offlineImportFailed: "Falha na importação offline: {error}",
driverImportSuccess: "Driver {label} importado",
driverImportFailed: "Falha ao importar o driver {label}: {error}",
chooseDriverJar: "Escolher arquivo JAR do driver",
chooseDriverJar: "Escolher arquivo do driver",
jreReinstallSuccess: "JRE {jre} reinstalada",
jreReinstallFailed: "Falha ao reinstalar a JRE {jre}: {error}",
jreUninstallSuccess: "JRE {jre} desinstalada",
@ -3875,7 +3875,7 @@ export default withEnglishFallback({
upgradingProgress: "Atualizando ({current}/{total})",
upgradeAll: "Atualizar todos",
queued: "Na fila",
importLocalJar: "Importar JAR local",
importLocalJar: "Importar arquivo de driver local",
updating: "Atualizando",
update: "Atualizar",
localInstall: "Instalação local",

View File

@ -4065,7 +4065,7 @@ export default withEnglishFallback({
offlineImportFailed: "离线导入失败: {error}",
driverImportSuccess: "{label} 驱动导入成功",
driverImportFailed: "{label} 驱动导入失败: {error}",
chooseDriverJar: "选择驱动 JAR 文件",
chooseDriverJar: "选择驱动文件",
jreReinstallSuccess: "JRE {jre} 重新安装成功",
jreReinstallFailed: "JRE {jre} 重新安装失败: {error}",
jreUninstallSuccess: "JRE {jre} 已卸载",
@ -4099,7 +4099,7 @@ export default withEnglishFallback({
upgradingProgress: "升级中 ({current}/{total})",
upgradeAll: "全部升级",
queued: "排队中",
importLocalJar: "导入本地 JAR",
importLocalJar: "导入本地驱动文件",
updating: "更新中",
update: "更新",
localInstall: "本地安装",

View File

@ -3645,7 +3645,7 @@ export default withEnglishFallback({
offlineImportFailed: "離線匯入失敗: {error}",
driverImportSuccess: "{label} 驅動程式匯入成功",
driverImportFailed: "{label} 驅動程式匯入失敗: {error}",
chooseDriverJar: "選擇驅動程式 JAR 檔案",
chooseDriverJar: "選擇驅動程式檔案",
jreReinstallSuccess: "JRE {jre} 重新安裝成功",
jreReinstallFailed: "JRE {jre} 重新安裝失敗: {error}",
jreUninstallSuccess: "JRE {jre} 已解除安裝",
@ -3679,7 +3679,7 @@ export default withEnglishFallback({
upgradingProgress: "更新中 ({current}/{total})",
upgradeAll: "全部更新",
queued: "排隊中",
importLocalJar: "匯入本機 JAR",
importLocalJar: "匯入本機驅動程式檔案",
updating: "更新中",
update: "更新",
localInstall: "本機安裝",

View File

@ -109,7 +109,8 @@ export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig");
export const setAgentJavaRuntimeConfig = forward("setAgentJavaRuntimeConfig");
export const invalidateAgentRegistryCache = forward("invalidateAgentRegistryCache");
export const importAgentsFromZip = forward("importAgentsFromZip");
export const importAgentJar = forward("importAgentJar");
export const importAgentDriver = forward("importAgentDriver");
export const importAgentJar = importAgentDriver;
export async function reinstallJre(jreKey?: string) {
const backend = await getBackend();
return backend.reinstallJre(jreKey, useSettingsStore().editorSettings.updateDownloadSource);

View File

@ -457,23 +457,25 @@ export async function importAgentsFromZip(fileOrPath: string | File): Promise<nu
return result.count;
}
export async function importAgentJar(dbType: string, pathOrFile: string | File): Promise<void> {
export async function importAgentDriver(dbType: string, pathOrFile: string | File): Promise<void> {
let blob: Blob;
let fileName: string;
if (pathOrFile instanceof File) {
blob = pathOrFile;
fileName = pathOrFile.name;
} else {
fileName = pathOrFile.split("/").pop() || "driver.jar";
fileName = pathOrFile.split("/").pop() || "agent";
blob = await (await fetch(pathOrFile)).blob();
}
const formData = new FormData();
formData.append("dbType", dbType);
formData.append("file", blob, fileName);
const uploadRes = await fetch(apiUrl("/api/agents/import-jar"), { method: "POST", body: formData });
const uploadRes = await fetch(apiUrl("/api/agents/import-driver"), { method: "POST", body: formData });
if (!uploadRes.ok) throw new Error(await uploadRes.text());
}
export const importAgentJar = importAgentDriver;
export async function reinstallJre(jreKey?: string, _source?: UpdateDownloadSource): Promise<void> {
await post("/api/agents/reinstall-jre", { jreKey });
}

View File

@ -1341,13 +1341,15 @@ export async function importAgentsFromZip(path: string | File): Promise<number>
return invoke("import_agents_from_zip", { path });
}
export async function importAgentJar(dbType: string, path: string | File): Promise<void> {
export async function importAgentDriver(dbType: string, path: string | File): Promise<void> {
if (typeof path !== "string") {
throw new Error("Desktop driver JAR import requires a local file path");
throw new Error("Desktop driver import requires a local file path");
}
return invoke("import_agent_jar_cmd", { dbType, path });
return invoke("import_agent_driver_cmd", { dbType, path });
}
export const importAgentJar = importAgentDriver;
export async function reinstallJre(jreKey?: string, source?: UpdateDownloadSource): Promise<void> {
return invoke("reinstall_jre", { jreKey, source });
}

View File

@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { isOfflineDriverPackage, webDriverImportAccept } from "./driverImportSelection";
describe("driver import selection", () => {
it("recognizes ZIP paths and uploaded files case-insensitively", () => {
expect(isOfflineDriverPackage("C:\\Downloads\\dbx-agent-h2-0.2.5.ZIP")).toBe(true);
expect(isOfflineDriverPackage({ name: "dbx-agent-kingbase-0.1.34-macos-aarch64.zip" })).toBe(true);
expect(isOfflineDriverPackage({ name: "dbx-agent-h2-0.2.5.jar" })).toBe(false);
});
it("allows ZIP alongside the platform raw artifact", () => {
expect(webDriverImportAccept(true, false)).toBe(".zip,.jar");
expect(webDriverImportAccept(false, true)).toBe(".zip,.exe");
expect(webDriverImportAccept(false, false)).toBe("");
});
});

View File

@ -0,0 +1,11 @@
export type DriverImportSelection = string | { name: string };
export function isOfflineDriverPackage(selection: DriverImportSelection): boolean {
const name = typeof selection === "string" ? selection : selection.name;
return name.toLowerCase().endsWith(".zip");
}
export function webDriverImportAccept(requiresJavaRuntime: boolean, isWindows: boolean): string {
if (requiresJavaRuntime) return ".zip,.jar";
return isWindows ? ".zip,.exe" : "";
}

View File

@ -1,5 +1,5 @@
use std::hash::{Hash, Hasher};
use std::io::Read;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::time::Duration;
@ -306,11 +306,13 @@ pub fn install_local_agent(am: &AgentManager, db_type: &str, source: PathBuf) ->
let jar_path = am.driver_jar_path(db_type);
let parent = jar_path.parent().ok_or_else(|| format!("Invalid driver path: {}", jar_path.display()))?;
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
std::fs::copy(&source, &jar_path).map_err(|e| format!("Failed to copy local agent jar: {e}"))?;
if !am.is_driver_jar_valid(db_type) {
std::fs::remove_file(&jar_path).ok();
let staging_path = parent.join(format!(".agent-jar-import-{}", uuid::Uuid::new_v4()));
std::fs::copy(&source, &staging_path).map_err(|e| format!("Failed to copy local agent jar: {e}"))?;
if !is_valid_agent_jar(&staging_path) {
std::fs::remove_file(&staging_path).ok();
return Err(format!("Local agent jar is invalid or corrupt: {}", source.display()));
}
replace_imported_agent_file(&staging_path, &jar_path)?;
let mut local_state = am.load_state();
local_state.installed_drivers.insert(
@ -324,6 +326,20 @@ pub fn install_local_agent(am: &AgentManager, db_type: &str, source: PathBuf) ->
am.save_state(&local_state)
}
fn is_valid_agent_jar(path: &Path) -> bool {
let Ok(file) = std::fs::File::open(path) else {
return false;
};
let Ok(mut archive) = zip::ZipArchive::new(file) else {
return false;
};
let Ok(mut manifest) = archive.by_name("META-INF/MANIFEST.MF") else {
return false;
};
let mut manifest_text = String::new();
manifest.read_to_string(&mut manifest_text).is_ok() && manifest_text.contains("Main-Class:")
}
pub async fn fetch_registry() -> Result<AgentRegistry, String> {
fetch_registry_from(DownloadSource::Official).await
}
@ -1228,6 +1244,25 @@ pub struct OfflineImportResult {
pub drivers_skipped: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OfflineImportPlan {
pub driver_keys: Vec<String>,
pub includes_jre: bool,
}
type OfflineDriverEntry = (String, String, bool);
pub fn inspect_offline_zip(zip_path: &Path) -> Result<OfflineImportPlan, String> {
let file = std::fs::File::open(zip_path).map_err(|e| format!("Failed to open ZIP file: {e}"))?;
let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("Invalid ZIP file: {e}"))?;
let registry = read_registry_from_zip(&mut archive)?;
let (jre_entries, driver_entries) = collect_offline_entries(&mut archive, &registry)?;
Ok(OfflineImportPlan {
driver_keys: driver_entries.into_iter().map(|(db_type, _, _)| db_type).collect(),
includes_jre: !jre_entries.is_empty(),
})
}
pub fn import_offline_zip(
am: &AgentManager,
zip_path: &Path,
@ -1244,36 +1279,13 @@ pub fn import_offline_zip(
let mut result =
OfflineImportResult { jre_installed: Vec::new(), drivers_installed: Vec::new(), drivers_skipped: Vec::new() };
let jre_entries: Vec<(String, String)> = (0..archive.len())
.filter_map(|i| {
let entry = archive.by_index(i).ok()?;
let name = entry.name().to_string();
if name.starts_with("jre/") && name.ends_with(".tar.gz") && name.contains(platform) {
let jre_key = extract_jre_key_from_filename(&name)?;
Some((jre_key, name))
} else {
None
}
})
.collect();
let driver_entries: Vec<(String, String, bool)> = (0..archive.len())
.filter_map(|i| {
let entry = archive.by_index(i).ok()?;
let name = entry.name().to_string();
if name.starts_with("drivers/") && name.ends_with(".jar") {
let db_type = extract_db_type_from_filename(&name)?;
Some((db_type, name, false))
} else if name.starts_with("drivers/") {
let db_type = db_type_for_native_offline_entry(&registry, platform, &name)?;
Some((db_type, name, true))
} else {
None
}
})
.collect();
let (jre_entries, driver_entries) = collect_offline_entries(&mut archive, &registry)?;
let total = (jre_entries.len() + driver_entries.len()) as u32;
if total == 0 {
return Err(format!("Offline package contains no drivers compatible with platform: {platform}"));
}
validate_offline_driver_entries(am, &mut archive, &driver_entries)?;
let mut current: u32 = 0;
for (jre_key, entry_name) in &jre_entries {
@ -1295,13 +1307,22 @@ pub fn import_offline_zip(
}
let jre_dir = am.jre_dir(jre_key);
// Daemons cannot be stopped from a sync function safely; the retry +
// Windows rename fallback in replace_old_jre_dir still handles a
// locked directory. Daemon shutdown for the foreground install paths
// happens in `reinstall_agent_jre` and `install_agent_driver_*`.
replace_old_jre_dir(am, &jre_dir)?;
extract_tar_gz(&tmp_archive, &jre_dir)?;
let staging_dir = am.base_dir().join(format!(".jre-offline-import-{}", uuid::Uuid::new_v4()));
if let Err(error) = extract_tar_gz(&tmp_archive, &staging_dir) {
std::fs::remove_dir_all(&staging_dir).ok();
std::fs::remove_file(&tmp_archive).ok();
return Err(error);
}
if !jre_dir_contains_java(&staging_dir) {
std::fs::remove_dir_all(&staging_dir).ok();
std::fs::remove_file(&tmp_archive).ok();
return Err(format!("Offline JRE archive does not contain a Java executable: {entry_name}"));
}
let pending_cleanup = replace_imported_jre_dir(&staging_dir, &jre_dir)?;
std::fs::remove_file(&tmp_archive).ok();
if let Some(path) = pending_cleanup {
local_state.pending_jre_cleanup.push(path);
}
if let Some(ver) = jre_version {
local_state.jre_versions.insert(jre_key.clone(), ver);
@ -1336,17 +1357,29 @@ pub fn import_offline_zip(
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let mut entry = archive.by_name(entry_name).map_err(|e| format!("Failed to read {entry_name}: {e}"))?;
let mut out = std::fs::File::create(&driver_path).map_err(|e| format!("Failed to write driver: {e}"))?;
let parent = driver_path.parent().ok_or_else(|| format!("Invalid driver path: {}", driver_path.display()))?;
let staging_path = parent.join(format!(".offline-agent-import-{}", uuid::Uuid::new_v4()));
let mut out = std::fs::File::create(&staging_path).map_err(|e| format!("Failed to write driver: {e}"))?;
std::io::copy(&mut entry, &mut out).map_err(|e| format!("Failed to copy driver: {e}"))?;
drop(out);
if *is_native {
mark_executable(&driver_path)?;
std::fs::remove_file(am.driver_jar_path(db_type)).ok();
if let Err(error) = validate_native_agent_binary(&staging_path) {
std::fs::remove_file(&staging_path).ok();
return Err(error);
}
mark_executable(&staging_path)?;
} else {
// Offline bundles are user-supplied; reject corrupt JARs before state says the driver is installed.
if !am.is_driver_jar_valid(db_type) {
std::fs::remove_file(&driver_path).ok();
// Validate the staged JAR before replacing a working driver so a
// corrupt offline package cannot destroy the previous installation.
if !is_valid_agent_jar(&staging_path) {
std::fs::remove_file(&staging_path).ok();
return Err(format!("Offline agent jar is invalid or corrupt: {entry_name}"));
}
}
replace_imported_agent_file(&staging_path, &driver_path)?;
if *is_native {
std::fs::remove_file(am.driver_jar_path(db_type)).ok();
} else {
std::fs::remove_file(am.driver_native_path(db_type)).ok();
}
@ -1365,6 +1398,88 @@ pub fn import_offline_zip(
Ok(result)
}
fn collect_offline_entries(
archive: &mut zip::ZipArchive<std::fs::File>,
registry: &AgentRegistry,
) -> Result<(Vec<(String, String)>, Vec<OfflineDriverEntry>), String> {
let platform = AgentManager::current_platform();
let mut jre_entries = Vec::new();
let mut drivers = std::collections::BTreeMap::<String, (String, bool)>::new();
for index in 0..archive.len() {
let entry = archive.by_index(index).map_err(|e| format!("Failed to inspect ZIP entry: {e}"))?;
let Some(path) = entry.enclosed_name() else {
return Err(format!("Offline package contains an unsafe path: {}", entry.name()));
};
let name = path.to_string_lossy().replace('\\', "/");
if name.starts_with("jre/") && name.ends_with(".tar.gz") && name.contains(platform) {
let jre_key = extract_jre_key_from_filename(&name)
.ok_or_else(|| format!("Invalid JRE filename in offline package: {name}"))?;
validate_offline_identifier(&jre_key, "JRE")?;
jre_entries.push((jre_key, name));
} else if name.starts_with("drivers/") && name.ends_with(".jar") {
let db_type = db_type_for_jar_offline_entry(registry, &name)
.or_else(|| extract_db_type_from_filename(&name))
.ok_or_else(|| format!("Unable to identify offline driver: {name}"))?;
validate_offline_driver_key(&db_type)?;
drivers.entry(db_type).or_insert((name, false));
} else if name.starts_with("drivers/") {
if let Some(db_type) = db_type_for_native_offline_entry(registry, platform, &name) {
validate_offline_driver_key(&db_type)?;
// Prefer the native artifact when a package contains both the
// platform executable and a Java fallback for the same driver.
drivers.insert(db_type, (name, true));
}
}
}
Ok((jre_entries, drivers.into_iter().map(|(db_type, (name, is_native))| (db_type, name, is_native)).collect()))
}
fn validate_offline_driver_entries(
am: &AgentManager,
archive: &mut zip::ZipArchive<std::fs::File>,
driver_entries: &[OfflineDriverEntry],
) -> Result<(), String> {
for (_, entry_name, is_native) in driver_entries {
let staging_path = am.base_dir().join(format!(".offline-agent-validation-{}", uuid::Uuid::new_v4()));
let result = (|| {
let mut entry = archive.by_name(entry_name).map_err(|e| format!("Failed to read {entry_name}: {e}"))?;
let mut out = std::fs::File::create(&staging_path).map_err(|e| format!("Failed to write driver: {e}"))?;
std::io::copy(&mut entry, &mut out).map_err(|e| format!("Failed to copy driver: {e}"))?;
drop(out);
if *is_native {
validate_native_agent_binary(&staging_path)
} else if is_valid_agent_jar(&staging_path) {
Ok(())
} else {
Err(format!("Offline agent jar is invalid or corrupt: {entry_name}"))
}
})();
std::fs::remove_file(&staging_path).ok();
result?;
}
Ok(())
}
fn validate_offline_driver_key(db_type: &str) -> Result<(), String> {
validate_offline_identifier(db_type, "driver")?;
if agent_catalog::label_for_key(db_type).is_none() {
return Err(format!("Offline package contains an unknown driver type: {db_type}"));
}
Ok(())
}
fn validate_offline_identifier(value: &str, kind: &str) -> Result<(), String> {
if value.is_empty()
|| matches!(value, "." | "..")
|| !value.chars().all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_'))
{
return Err(format!("Offline package contains an invalid {kind} identifier: {value}"));
}
Ok(())
}
fn read_registry_from_zip(archive: &mut zip::ZipArchive<std::fs::File>) -> Result<AgentRegistry, String> {
let mut entry = archive
.by_name("agent-registry.json")
@ -1403,6 +1518,15 @@ fn db_type_for_native_offline_entry(registry: &AgentRegistry, platform: &str, na
})
}
fn db_type_for_jar_offline_entry(registry: &AgentRegistry, name: &str) -> Option<String> {
let filename = name.rsplit('/').next()?;
registry.drivers.iter().find_map(|(db_type, driver)| {
let artifact = driver.jar.as_ref()?;
let artifact_filename = artifact.url.rsplit('/').next()?;
(artifact_filename == filename).then(|| db_type.clone())
})
}
fn extract_tar_gz(archive: &Path, dest: &Path) -> Result<(), String> {
std::fs::create_dir_all(dest).map_err(|e| e.to_string())?;
let status = crate::process::new_std_command("tar")
@ -1415,11 +1539,210 @@ fn extract_tar_gz(archive: &Path, dest: &Path) -> Result<(), String> {
Ok(())
}
pub fn import_agent_jar(am: &AgentManager, db_type: &str, jar_path: &Path) -> Result<(), String> {
if !jar_path.exists() {
return Err(format!("File not found: {}", jar_path.display()));
pub fn import_agent_driver(am: &AgentManager, db_type: &str, source_path: &Path) -> Result<(), String> {
if !source_path.is_file() {
return Err(format!("File not found: {}", source_path.display()));
}
install_local_agent(am, db_type, jar_path.to_path_buf())
if source_path.extension().is_some_and(|extension| extension.eq_ignore_ascii_case("jar")) {
install_local_agent(am, db_type, source_path.to_path_buf())?;
std::fs::remove_file(am.driver_native_path(db_type)).ok();
return Ok(());
}
validate_native_agent_binary(source_path)?;
let native_path = am.driver_native_path(db_type);
let parent = native_path.parent().ok_or_else(|| format!("Invalid driver path: {}", native_path.display()))?;
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
let staging_path = parent.join(format!(".agent-import-{}", uuid::Uuid::new_v4()));
std::fs::copy(source_path, &staging_path).map_err(|e| format!("Failed to copy native agent: {e}"))?;
mark_executable(&staging_path)?;
replace_imported_agent_file(&staging_path, &native_path)?;
std::fs::remove_file(am.driver_jar_path(db_type)).ok();
let mut local_state = am.load_state();
local_state.installed_drivers.insert(
db_type.to_string(),
InstalledDriver {
version: "0.1.0-local".to_string(),
installed_at: chrono::Utc::now().to_rfc3339(),
jre: DEFAULT_JRE_KEY.to_string(),
},
);
am.save_state(&local_state)
}
pub fn import_agent_jar(am: &AgentManager, db_type: &str, jar_path: &Path) -> Result<(), String> {
import_agent_driver(am, db_type, jar_path)
}
fn replace_imported_agent_file(staging_path: &Path, target_path: &Path) -> Result<(), String> {
let backup_path = target_path.with_file_name(format!(
".{}-backup-{}",
target_path.file_name().and_then(|name| name.to_str()).unwrap_or("agent"),
uuid::Uuid::new_v4()
));
let had_existing = target_path.exists();
if had_existing {
std::fs::rename(target_path, &backup_path).map_err(|e| format!("Failed to replace existing agent: {e}"))?;
}
if let Err(error) = std::fs::rename(staging_path, target_path) {
if had_existing {
let _ = std::fs::rename(&backup_path, target_path);
}
let _ = std::fs::remove_file(staging_path);
return Err(format!("Failed to install agent: {error}"));
}
if had_existing {
std::fs::remove_file(backup_path).ok();
}
Ok(())
}
fn replace_imported_jre_dir(staging_dir: &Path, target_dir: &Path) -> Result<Option<PathBuf>, String> {
let backup_dir = target_dir.with_file_name(format!(
".{}-backup-{}",
target_dir.file_name().and_then(|name| name.to_str()).unwrap_or("jre"),
uuid::Uuid::new_v4()
));
let had_existing = target_dir.exists();
if had_existing {
std::fs::rename(target_dir, &backup_dir).map_err(|error| {
let _ = std::fs::remove_dir_all(staging_dir);
format!("Failed to replace existing JRE: {error}")
})?;
}
if let Err(error) = std::fs::rename(staging_dir, target_dir) {
if had_existing {
let _ = std::fs::rename(&backup_dir, target_dir);
}
let _ = std::fs::remove_dir_all(staging_dir);
return Err(format!("Failed to install JRE: {error}"));
}
if had_existing && remove_jre_dir_with_retry(&backup_dir).is_err() {
// The new runtime is already installed. Keep the old directory for
// startup cleanup rather than turning a successful import into an error.
return Ok(Some(backup_dir));
}
Ok(None)
}
fn jre_dir_contains_java(path: &Path) -> bool {
let java_name = if cfg!(windows) { "java.exe" } else { "java" };
path.join("bin").join(java_name).is_file()
|| path.join("Contents").join("Home").join("bin").join(java_name).is_file()
}
fn validate_native_agent_binary(path: &Path) -> Result<(), String> {
let mut file = std::fs::File::open(path).map_err(|e| format!("Failed to read native agent: {e}"))?;
let mut magic = [0_u8; 4];
file.read_exact(&mut magic).map_err(|e| format!("Failed to read native agent header: {e}"))?;
let valid = if cfg!(target_os = "windows") {
is_windows_binary_for_current_arch(&mut file, &magic)
} else if cfg!(target_os = "linux") {
is_elf_binary_for_current_arch(&mut file, &magic)
} else if cfg!(target_os = "macos") {
is_macho_binary_for_current_arch(&mut file, &magic)
} else {
false
};
if valid {
Ok(())
} else {
Err(format!("The selected file is not a {} native agent for this platform", AgentManager::current_platform()))
}
}
fn is_elf_binary_for_current_arch(file: &mut std::fs::File, magic: &[u8; 4]) -> bool {
if magic != b"\x7fELF" || file.seek(SeekFrom::Start(4)).is_err() {
return false;
}
let mut header = [0_u8; 16];
if file.read_exact(&mut header).is_err() || header[0] != 2 {
return false;
}
let machine = match header[1] {
1 => u16::from_le_bytes([header[14], header[15]]),
2 => u16::from_be_bytes([header[14], header[15]]),
_ => return false,
};
(cfg!(target_arch = "x86_64") && machine == 62) || (cfg!(target_arch = "aarch64") && machine == 183)
}
fn is_macho_binary_for_current_arch(file: &mut std::fs::File, magic: &[u8; 4]) -> bool {
const CPU_TYPE_X86_64: u32 = 0x0100_0007;
const CPU_TYPE_ARM64: u32 = 0x0100_000c;
let expected = if cfg!(target_arch = "aarch64") { CPU_TYPE_ARM64 } else { CPU_TYPE_X86_64 };
let thin_endian = match magic {
[0xce, 0xfa, 0xed, 0xfe] | [0xcf, 0xfa, 0xed, 0xfe] => Some(true),
[0xfe, 0xed, 0xfa, 0xce] | [0xfe, 0xed, 0xfa, 0xcf] => Some(false),
_ => None,
};
if let Some(little_endian) = thin_endian {
if file.seek(SeekFrom::Start(4)).is_err() {
return false;
}
let mut cpu_type = [0_u8; 4];
if file.read_exact(&mut cpu_type).is_err() {
return false;
}
let cpu_type = if little_endian { u32::from_le_bytes(cpu_type) } else { u32::from_be_bytes(cpu_type) };
return cpu_type == expected;
}
let (little_endian, arch_size) = match magic {
[0xca, 0xfe, 0xba, 0xbe] => (false, 20_u64),
[0xbe, 0xba, 0xfe, 0xca] => (true, 20_u64),
[0xca, 0xfe, 0xba, 0xbf] => (false, 32_u64),
[0xbf, 0xba, 0xfe, 0xca] => (true, 32_u64),
_ => return false,
};
if file.seek(SeekFrom::Start(4)).is_err() {
return false;
}
let mut count = [0_u8; 4];
if file.read_exact(&mut count).is_err() {
return false;
}
let count = if little_endian { u32::from_le_bytes(count) } else { u32::from_be_bytes(count) };
// A real universal binary has only a handful of slices; cap the count so
// a malformed header cannot trigger unbounded seeks during import.
if count == 0 || count > 64 {
return false;
}
for index in 0..count {
if file.seek(SeekFrom::Start(8 + u64::from(index) * arch_size)).is_err() {
return false;
}
let mut cpu_type = [0_u8; 4];
if file.read_exact(&mut cpu_type).is_err() {
return false;
}
let cpu_type = if little_endian { u32::from_le_bytes(cpu_type) } else { u32::from_be_bytes(cpu_type) };
if cpu_type == expected {
return true;
}
}
false
}
fn is_windows_binary_for_current_arch(file: &mut std::fs::File, magic: &[u8; 4]) -> bool {
if &magic[..2] != b"MZ" || file.seek(SeekFrom::Start(0x3c)).is_err() {
return false;
}
let mut pe_offset = [0_u8; 4];
if file.read_exact(&mut pe_offset).is_err()
|| file.seek(SeekFrom::Start(u32::from_le_bytes(pe_offset) as u64)).is_err()
{
return false;
}
let mut pe_header = [0_u8; 6];
if file.read_exact(&mut pe_header).is_err() || &pe_header[..4] != b"PE\0\0" {
return false;
}
let machine = u16::from_le_bytes([pe_header[4], pe_header[5]]);
(cfg!(target_arch = "x86_64") && machine == 0x8664) || (cfg!(target_arch = "aarch64") && machine == 0xaa64)
}
// ──────────── Tests ────────────
@ -1449,6 +1772,31 @@ mod agent_download_url_tests {
assert_eq!(extract_jre_key_from_filename("jre/dbx-jre-21-macos-aarch64.tar.gz").as_deref(), Some("21"));
assert_eq!(extract_jre_key_from_filename("jre/jre-21-macos-aarch64.tar.gz").as_deref(), Some("21"));
}
#[test]
fn windows_native_header_validator_checks_cpu_architecture() {
let path = std::env::temp_dir().join(format!("dbx-agent-pe-test-{}", uuid::Uuid::new_v4()));
let expected_machine = if cfg!(target_arch = "aarch64") { 0xaa64_u16 } else { 0x8664_u16 };
let wrong_machine = if expected_machine == 0xaa64 { 0x8664_u16 } else { 0xaa64_u16 };
std::fs::write(&path, test_pe_binary(expected_machine)).unwrap();
let mut file = std::fs::File::open(&path).unwrap();
assert!(is_windows_binary_for_current_arch(&mut file, b"MZ\0\0"));
std::fs::write(&path, test_pe_binary(wrong_machine)).unwrap();
let mut file = std::fs::File::open(&path).unwrap();
assert!(!is_windows_binary_for_current_arch(&mut file, b"MZ\0\0"));
std::fs::remove_file(path).ok();
}
fn test_pe_binary(machine: u16) -> Vec<u8> {
let mut bytes = vec![0_u8; 0x48];
bytes[..2].copy_from_slice(b"MZ");
bytes[0x3c..0x40].copy_from_slice(&(0x40_u32).to_le_bytes());
bytes[0x40..0x44].copy_from_slice(b"PE\0\0");
bytes[0x44..0x46].copy_from_slice(&machine.to_le_bytes());
bytes
}
}
#[cfg(test)]

View File

@ -3,9 +3,9 @@ use dbx_core::agent_manager::{
JreInfo, DEFAULT_JRE_KEY,
};
use dbx_core::agent_service::{
build_agent_list, clear_agent_download_cache, github_url_to_r2_path, import_agent_jar, import_agents_from_zip,
is_app_version_compatible, jre_needs_install, local_agent_jar_candidates, replace_download, uninstall_agent_driver,
AgentProgressEvent,
build_agent_list, clear_agent_download_cache, github_url_to_r2_path, import_agent_driver, import_agent_jar,
import_agents_from_zip, inspect_offline_zip, is_app_version_compatible, jre_needs_install,
local_agent_jar_candidates, replace_download, uninstall_agent_driver, AgentProgressEvent,
};
fn test_manager(name: &str) -> AgentManager {
@ -406,6 +406,51 @@ fn local_jar_import_rejects_corrupt_jar() {
assert!(!manager.load_state().installed_drivers.contains_key("h2"));
}
#[test]
fn local_native_import_installs_current_platform_executable() {
let manager = test_manager("local-native-import");
let source = test_path("local-native-import-source").join(if cfg!(windows) {
"dbx-agent-kingbase-windows.exe"
} else {
"dbx-agent-kingbase"
});
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
std::fs::write(&source, current_platform_native_binary()).unwrap();
import_agent_driver(&manager, "kingbase", &source).unwrap();
assert_eq!(std::fs::read(manager.driver_native_path("kingbase")).unwrap(), std::fs::read(&source).unwrap());
assert!(!manager.driver_jar_path("kingbase").exists());
assert_eq!(manager.load_state().installed_drivers["kingbase"].version, "0.1.0-local");
}
#[test]
fn local_native_import_rejects_wrong_platform_binary() {
let manager = test_manager("local-native-import-invalid");
let source = test_path("local-native-import-invalid-source").join("dbx-agent-kingbase");
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
std::fs::write(&source, b"not-an-executable").unwrap();
let err = import_agent_driver(&manager, "kingbase", &source).unwrap_err();
assert!(err.contains(AgentManager::current_platform()));
assert!(!manager.driver_native_path("kingbase").exists());
assert!(!manager.load_state().installed_drivers.contains_key("kingbase"));
}
#[test]
fn local_native_import_rejects_wrong_arch_binary() {
let manager = test_manager("local-native-wrong-arch");
let native_path = test_path("local-native-wrong-arch-file").join("agent");
std::fs::create_dir_all(native_path.parent().unwrap()).unwrap();
std::fs::write(&native_path, native_binary_for_arch(!cfg!(target_arch = "aarch64"))).unwrap();
let err = import_agent_driver(&manager, "kingbase", &native_path).unwrap_err();
assert!(err.contains("not a"));
assert!(!manager.driver_native_path("kingbase").exists());
}
#[tokio::test]
async fn uninstall_driver_removes_artifact_and_state() {
let manager = test_manager("uninstall");
@ -511,6 +556,53 @@ fn offline_zip_import_installs_release_named_jre() {
assert!(events.lock().unwrap().iter().any(|event| event.step == "jre-extract"));
}
#[test]
fn offline_zip_import_preserves_existing_jre_when_archive_is_corrupt() {
let manager = test_manager("offline-corrupt-jre-preserves-existing");
let root = test_path("offline-corrupt-jre-preserves-existing-zip");
let valid_zip = root.join("valid.zip");
let corrupt_zip = root.join("corrupt.zip");
std::fs::create_dir_all(&root).unwrap();
write_offline_driver_zip_with_jre(&valid_zip, "h2", "0.2.0", "21.0.12");
import_agents_from_zip(&manager, &valid_zip, |_| {}).unwrap();
let java_path = manager.jre_java_path(DEFAULT_JRE_KEY);
let original_java = std::fs::read(&java_path).unwrap();
write_offline_driver_zip_with_jre_bytes(&corrupt_zip, "h2", "0.3.0", "21.0.13", b"not-a-tar-gz".to_vec());
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).unwrap_err();
assert!(err.contains("Failed to extract JRE archive"));
assert_eq!(std::fs::read(java_path).unwrap(), original_java);
assert_eq!(manager.load_state().jre_versions.get(DEFAULT_JRE_KEY).map(String::as_str), Some("21.0.12"));
}
#[test]
fn offline_zip_import_preserves_existing_jre_when_driver_is_corrupt() {
let manager = test_manager("offline-corrupt-driver-preserves-jre");
let root = test_path("offline-corrupt-driver-preserves-jre-zip");
let valid_zip = root.join("valid.zip");
let corrupt_zip = root.join("corrupt.zip");
std::fs::create_dir_all(&root).unwrap();
write_offline_driver_zip_with_jre(&valid_zip, "h2", "0.2.0", "21.0.12");
import_agents_from_zip(&manager, &valid_zip, |_| {}).unwrap();
let java_path = manager.jre_java_path(DEFAULT_JRE_KEY);
let original_java = std::fs::read(&java_path).unwrap();
write_offline_driver_zip_with_jre_and_jar_bytes(
&corrupt_zip,
"h2",
"0.3.0",
"21.0.13",
test_jre_archive_bytes(),
b"jar".to_vec(),
);
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).unwrap_err();
assert!(err.contains("invalid or corrupt"));
assert_eq!(std::fs::read(java_path).unwrap(), original_java);
assert_eq!(manager.load_state().jre_versions.get(DEFAULT_JRE_KEY).map(String::as_str), Some("21.0.12"));
}
#[test]
fn offline_zip_import_rejects_corrupt_jar() {
let manager = test_manager("offline-corrupt-driver");
@ -525,16 +617,284 @@ fn offline_zip_import_rejects_corrupt_jar() {
assert!(!manager.load_state().installed_drivers.contains_key("h2"));
}
#[test]
fn offline_zip_import_preserves_existing_driver_when_jar_is_corrupt() {
let manager = test_manager("offline-corrupt-driver-preserves-existing");
let root = test_path("offline-corrupt-driver-preserves-existing-zip");
let valid_zip = root.join("valid.zip");
let corrupt_zip = root.join("corrupt.zip");
std::fs::create_dir_all(&root).unwrap();
write_offline_driver_zip(&valid_zip, "h2", "0.2.0");
import_agents_from_zip(&manager, &valid_zip, |_| {}).unwrap();
let original = std::fs::read(manager.driver_jar_path("h2")).unwrap();
write_offline_driver_zip_with_jar(&corrupt_zip, "h2", "0.3.0", b"jar".to_vec());
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).unwrap_err();
assert!(err.contains("invalid or corrupt"));
assert_eq!(std::fs::read(manager.driver_jar_path("h2")).unwrap(), original);
assert_eq!(manager.load_state().installed_drivers["h2"].version, "0.2.0");
}
#[test]
fn offline_zip_import_keeps_legacy_unversioned_jar_compatibility() {
let manager = test_manager("offline-legacy-jar-zip");
let zip_path = test_path("offline-legacy-jar-zip").join("h2.zip");
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
let file = std::fs::File::create(&zip_path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
let jar = test_agent_jar_bytes();
let registry = serde_json::json!({
"drivers": {
"h2": {
"version": "0.1.9",
"label": "H2",
"min_app_version": "0.1.0",
"jre": DEFAULT_JRE_KEY,
"jar": { "url": "https://example.com/dbx-agent-h2.jar", "size": jar.len() }
}
}
});
zip.start_file("agent-registry.json", options).unwrap();
std::io::Write::write_all(&mut zip, registry.to_string().as_bytes()).unwrap();
zip.start_file("drivers/dbx-agent-h2.jar", options).unwrap();
std::io::Write::write_all(&mut zip, &jar).unwrap();
zip.finish().unwrap();
import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap();
assert_eq!(manager.load_state().installed_drivers["h2"].version, "0.1.9");
}
#[test]
fn offline_zip_import_installs_versioned_native_driver_package() {
let manager = test_manager("offline-native-driver-zip");
let zip_path = test_path("offline-native-driver-zip").join("kingbase.zip");
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
write_offline_native_driver_zip(&zip_path, "kingbase", "0.1.34");
let result = import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap();
assert_eq!(result.drivers_installed, vec!["kingbase"]);
assert_eq!(manager.load_state().installed_drivers["kingbase"].version, "0.1.34");
assert_eq!(std::fs::read(manager.driver_native_path("kingbase")).unwrap(), current_platform_native_binary());
assert!(!manager.driver_jar_path("kingbase").exists());
}
#[test]
fn offline_zip_import_rejects_native_driver_for_another_platform() {
let manager = test_manager("offline-wrong-platform-native-driver-zip");
let zip_path = test_path("offline-wrong-platform-native-driver-zip").join("kingbase.zip");
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
let other_platform = if AgentManager::current_platform() == "windows-x64" { "linux-x64" } else { "windows-x64" };
write_offline_native_driver_zip_for_platform(&zip_path, "kingbase", "0.1.34", other_platform);
let err = import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap_err();
assert!(err.contains("no drivers compatible"));
assert!(err.contains(AgentManager::current_platform()));
assert!(!manager.is_driver_installed("kingbase"));
}
#[test]
fn offline_zip_import_preserves_existing_native_driver_when_binary_is_invalid() {
let manager = test_manager("offline-invalid-native-preserves-existing");
let root = test_path("offline-invalid-native-preserves-existing-zip");
let valid_zip = root.join("valid.zip");
let invalid_zip = root.join("invalid.zip");
std::fs::create_dir_all(&root).unwrap();
write_offline_native_driver_zip(&valid_zip, "kingbase", "0.1.34");
import_agents_from_zip(&manager, &valid_zip, |_| {}).unwrap();
let original = std::fs::read(manager.driver_native_path("kingbase")).unwrap();
write_offline_native_driver_zip_with_bytes(
&invalid_zip,
"kingbase",
"0.1.35",
AgentManager::current_platform(),
b"not-a-native-agent".to_vec(),
);
let err = import_agents_from_zip(&manager, &invalid_zip, |_| {}).unwrap_err();
assert!(err.contains("not a"));
assert_eq!(std::fs::read(manager.driver_native_path("kingbase")).unwrap(), original);
assert_eq!(manager.load_state().installed_drivers["kingbase"].version, "0.1.34");
}
#[test]
fn offline_zip_inspection_reports_drivers_and_jre() {
let zip_path = test_path("offline-inspection").join("agents.zip");
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
write_offline_driver_zip_with_jre(&zip_path, "h2", "0.2.0", "21.0.12");
let plan = inspect_offline_zip(&zip_path).unwrap();
assert_eq!(plan.driver_keys, vec!["h2"]);
assert!(plan.includes_jre);
}
#[test]
fn offline_zip_import_rejects_unknown_driver_type() {
let zip_path = test_path("offline-unknown-driver").join("agents.zip");
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
write_offline_driver_zip_with_jar(&zip_path, "unknown-driver", "0.1.0", test_agent_jar_bytes());
let err = inspect_offline_zip(&zip_path).unwrap_err();
assert!(err.contains("unknown driver type"));
}
#[test]
fn offline_zip_import_rejects_unsafe_entry_path() {
let zip_path = test_path("offline-unsafe-entry").join("agents.zip");
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
let file = std::fs::File::create(&zip_path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("agent-registry.json", options).unwrap();
std::io::Write::write_all(&mut zip, br#"{"drivers":{}}"#).unwrap();
zip.start_file("../escape", options).unwrap();
std::io::Write::write_all(&mut zip, b"escape").unwrap();
zip.finish().unwrap();
let err = inspect_offline_zip(&zip_path).unwrap_err();
assert!(err.contains("unsafe path"));
}
#[test]
fn offline_zip_import_prefers_native_artifact_over_java_fallback() {
let manager = test_manager("offline-native-preferred");
let zip_path = test_path("offline-native-preferred-zip").join("kingbase.zip");
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
write_offline_hybrid_driver_zip(&zip_path, "kingbase", "0.1.34");
let plan = inspect_offline_zip(&zip_path).unwrap();
let result = import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap();
assert_eq!(plan.driver_keys, vec!["kingbase"]);
assert_eq!(result.drivers_installed, vec!["kingbase"]);
assert!(manager.driver_native_path("kingbase").exists());
assert!(!manager.driver_jar_path("kingbase").exists());
}
fn write_offline_driver_zip(path: &std::path::Path, db_type: &str, version: &str) {
write_offline_driver_zip_with_jar(path, db_type, version, test_agent_jar_bytes());
}
fn write_offline_driver_zip_with_jre(path: &std::path::Path, db_type: &str, version: &str, jre_version: &str) {
fn write_offline_native_driver_zip(path: &std::path::Path, db_type: &str, version: &str) {
write_offline_native_driver_zip_for_platform(path, db_type, version, AgentManager::current_platform());
}
fn write_offline_native_driver_zip_for_platform(path: &std::path::Path, db_type: &str, version: &str, platform: &str) {
write_offline_native_driver_zip_with_bytes(path, db_type, version, platform, current_platform_native_binary());
}
fn write_offline_native_driver_zip_with_bytes(
path: &std::path::Path,
db_type: &str,
version: &str,
platform: &str,
native: Vec<u8>,
) {
let file = std::fs::File::create(path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
let extension = if platform.starts_with("windows-") { ".exe" } else { "" };
let filename = format!("dbx-agent-{db_type}-{version}-{platform}{extension}");
let registry = serde_json::json!({
"jres": {},
"drivers": {
db_type: {
"version": version,
"label": db_type,
"min_app_version": "0.6.0",
"jre": DEFAULT_JRE_KEY,
"native": {
platform: {
"url": format!("https://example.com/{filename}"),
"size": native.len()
}
}
}
}
});
zip.start_file("agent-registry.json", options).unwrap();
std::io::Write::write_all(&mut zip, registry.to_string().as_bytes()).unwrap();
zip.start_file(format!("drivers/{filename}"), options).unwrap();
std::io::Write::write_all(&mut zip, &native).unwrap();
zip.finish().unwrap();
}
fn write_offline_hybrid_driver_zip(path: &std::path::Path, db_type: &str, version: &str) {
let file = std::fs::File::create(path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
let jar = test_agent_jar_bytes();
let jre_archive = test_jre_archive_bytes();
let native = current_platform_native_binary();
let platform = AgentManager::current_platform();
let extension = if platform.starts_with("windows-") { ".exe" } else { "" };
let jar_filename = format!("dbx-agent-{db_type}-{version}.jar");
let native_filename = format!("dbx-agent-{db_type}-{version}-{platform}{extension}");
let registry = serde_json::json!({
"jres": {},
"drivers": {
db_type: {
"version": version,
"label": db_type,
"min_app_version": "0.1.0",
"jre": DEFAULT_JRE_KEY,
"jar": { "url": format!("https://example.com/{jar_filename}"), "size": jar.len() },
"native": {
platform: { "url": format!("https://example.com/{native_filename}"), "size": native.len() }
}
}
}
});
zip.start_file("agent-registry.json", options).unwrap();
std::io::Write::write_all(&mut zip, registry.to_string().as_bytes()).unwrap();
zip.start_file(format!("drivers/{jar_filename}"), options).unwrap();
std::io::Write::write_all(&mut zip, &jar).unwrap();
zip.start_file(format!("drivers/{native_filename}"), options).unwrap();
std::io::Write::write_all(&mut zip, &native).unwrap();
zip.finish().unwrap();
}
fn write_offline_driver_zip_with_jre(path: &std::path::Path, db_type: &str, version: &str, jre_version: &str) {
write_offline_driver_zip_with_jre_bytes(path, db_type, version, jre_version, test_jre_archive_bytes());
}
fn write_offline_driver_zip_with_jre_bytes(
path: &std::path::Path,
db_type: &str,
version: &str,
jre_version: &str,
jre_archive: Vec<u8>,
) {
write_offline_driver_zip_with_jre_and_jar_bytes(
path,
db_type,
version,
jre_version,
jre_archive,
test_agent_jar_bytes(),
);
}
fn write_offline_driver_zip_with_jre_and_jar_bytes(
path: &std::path::Path,
db_type: &str,
version: &str,
jre_version: &str,
jre_archive: Vec<u8>,
jar: Vec<u8>,
) {
let file = std::fs::File::create(path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
let registry = serde_json::json!({
"jres": {
DEFAULT_JRE_KEY: {
@ -548,7 +908,7 @@ fn write_offline_driver_zip_with_jre(path: &std::path::Path, db_type: &str, vers
"label": db_type,
"min_app_version": "0.1.0",
"jre": DEFAULT_JRE_KEY,
"jar": { "url": format!("https://example.com/dbx-agent-{db_type}.jar"), "size": jar.len() }
"jar": { "url": format!("https://example.com/dbx-agent-{db_type}-{version}.jar"), "size": jar.len() }
}
}
});
@ -558,7 +918,7 @@ fn write_offline_driver_zip_with_jre(path: &std::path::Path, db_type: &str, vers
zip.start_file(format!("jre/dbx-jre-{DEFAULT_JRE_KEY}-{}.tar.gz", AgentManager::current_platform()), options)
.unwrap();
std::io::Write::write_all(&mut zip, &jre_archive).unwrap();
zip.start_file(format!("drivers/dbx-agent-{db_type}.jar"), options).unwrap();
zip.start_file(format!("drivers/dbx-agent-{db_type}-{version}.jar"), options).unwrap();
std::io::Write::write_all(&mut zip, &jar).unwrap();
zip.finish().unwrap();
}
@ -598,14 +958,14 @@ fn write_offline_driver_zip_with_jar(path: &std::path::Path, db_type: &str, vers
"label": db_type,
"min_app_version": "0.1.0",
"jre": DEFAULT_JRE_KEY,
"jar": { "url": format!("https://example.com/dbx-agent-{db_type}.jar"), "size": jar.len() }
"jar": { "url": format!("https://example.com/dbx-agent-{db_type}-{version}.jar"), "size": jar.len() }
}
}
});
zip.start_file("agent-registry.json", options).unwrap();
std::io::Write::write_all(&mut zip, registry.to_string().as_bytes()).unwrap();
zip.start_file(format!("drivers/dbx-agent-{db_type}.jar"), options).unwrap();
zip.start_file(format!("drivers/dbx-agent-{db_type}-{version}.jar"), options).unwrap();
std::io::Write::write_all(&mut zip, &jar).unwrap();
zip.finish().unwrap();
}
@ -623,3 +983,34 @@ fn test_agent_jar_bytes() -> Vec<u8> {
std::io::Write::write_all(&mut zip, b"Manifest-Version: 1.0\nMain-Class: com.dbx.agent.TestAgent\n\n").unwrap();
zip.finish().unwrap().into_inner()
}
fn current_platform_native_binary() -> Vec<u8> {
native_binary_for_arch(cfg!(target_arch = "aarch64"))
}
fn native_binary_for_arch(aarch64: bool) -> Vec<u8> {
if cfg!(windows) {
let mut bytes = vec![0_u8; 0x48];
bytes[..2].copy_from_slice(b"MZ");
bytes[0x3c..0x40].copy_from_slice(&(0x40_u32).to_le_bytes());
bytes[0x40..0x44].copy_from_slice(b"PE\0\0");
let machine = if aarch64 { 0xaa64_u16 } else { 0x8664_u16 };
bytes[0x44..0x46].copy_from_slice(&machine.to_le_bytes());
bytes
} else if cfg!(target_os = "linux") {
let mut bytes = vec![0_u8; 20];
bytes[..4].copy_from_slice(b"\x7fELF");
bytes[4] = 2;
bytes[5] = 1;
let machine = if aarch64 { 183_u16 } else { 62_u16 };
bytes[18..20].copy_from_slice(&machine.to_le_bytes());
bytes
} else if cfg!(target_os = "macos") {
let mut bytes = vec![0xcf, 0xfa, 0xed, 0xfe];
let cpu_type = if aarch64 { 0x0100_000c_u32 } else { 0x0100_0007_u32 };
bytes.extend_from_slice(&cpu_type.to_le_bytes());
bytes
} else {
Vec::new()
}
}

View File

@ -245,7 +245,8 @@ async fn main() {
.route("/agents/upgrade-all", post(routes::agents::upgrade_all_agents))
.route("/agents/uninstall", post(routes::agents::uninstall_agent))
.route("/agents/import-offline", post(routes::agents::import_agents_from_zip))
.route("/agents/import-jar", post(routes::agents::import_agent_jar))
.route("/agents/import-driver", post(routes::agents::import_agent_driver_file))
.route("/agents/import-jar", post(routes::agents::import_agent_driver_file))
.route(
"/agents/java-runtime",
get(routes::agents::get_agent_java_runtime_config).post(routes::agents::set_agent_java_runtime_config),

View File

@ -7,9 +7,10 @@ use dbx_core::agent_manager::{
AgentDriverInfo, AgentState, DriverStoreUsage, JavaRuntimeConfig, JavaRuntimeMode, DEFAULT_JRE_KEY,
};
use dbx_core::agent_service::{
build_agent_list, clear_agent_download_cache, fetch_registry,
import_agents_from_zip as import_agents_from_zip_core, install_agent_driver, invalidate_registry_cache,
reinstall_agent_jre, uninstall_agent_driver, uninstall_agent_jre, upgrade_all_agent_drivers, AgentProgressEvent,
build_agent_list, clear_agent_download_cache, fetch_registry, import_agent_driver,
import_agents_from_zip as import_agents_from_zip_core, inspect_offline_zip, install_agent_driver,
invalidate_registry_cache, reinstall_agent_jre, uninstall_agent_driver, uninstall_agent_jre,
upgrade_all_agent_drivers, AgentProgressEvent, OfflineImportPlan,
};
use dbx_core::driver_runtime::DriverRuntimeSummary;
use futures::Stream;
@ -177,18 +178,22 @@ pub async fn import_agents_from_zip(
}
let zip_path = tmp_dir.join(format!("agent-offline-{}.zip", uuid::Uuid::new_v4()));
let mut upload = tokio::fs::File::create(&zip_path).await.map_err(|err| AppError(err.to_string()))?;
let mut field = field;
while let Some(chunk) = field.chunk().await.map_err(|err| AppError(err.to_string()))? {
upload.write_all(&chunk).await.map_err(|err| AppError(err.to_string()))?;
}
upload.flush().await.map_err(|err| AppError(err.to_string()))?;
drop(upload);
let tx = progress_sender(&state, "global").await;
let result =
let result = async {
let mut upload = tokio::fs::File::create(&zip_path).await.map_err(|err| AppError(err.to_string()))?;
let mut field = field;
while let Some(chunk) = field.chunk().await.map_err(|err| AppError(err.to_string()))? {
upload.write_all(&chunk).await.map_err(|err| AppError(err.to_string()))?;
}
upload.flush().await.map_err(|err| AppError(err.to_string()))?;
drop(upload);
let plan = inspect_offline_zip(&zip_path).map_err(AppError)?;
ensure_no_offline_import_blockers(&state.app, &plan).await.map_err(AppError)?;
import_agents_from_zip_core(&state.app.agent_manager, &zip_path, |event| send_progress_event(&tx, event))
.map_err(AppError);
.map_err(AppError)
}
.await;
let _ = std::fs::remove_file(&zip_path);
let result = result?;
@ -199,37 +204,44 @@ pub async fn import_agents_from_zip(
Err(AppError("No file uploaded".to_string()))
}
pub async fn import_agent_jar(
pub async fn import_agent_driver_file(
State(state): State<Arc<WebState>>,
mut multipart: Multipart,
) -> Result<Json<serde_json::Value>, AppError> {
let mut db_type: Option<String> = None;
let mut jar_data: Option<Vec<u8>> = None;
let mut jar_name = String::new();
let mut driver_data: Option<Vec<u8>> = None;
let mut driver_name = String::new();
while let Ok(Some(field)) = multipart.next_field().await {
let name = field.name().unwrap_or("").to_string();
if name == "dbType" {
db_type = Some(field.text().await.map_err(|e| AppError(e.to_string()))?);
} else if name == "file" {
jar_name = field.file_name().unwrap_or("driver.jar").to_string();
if !jar_name.to_lowercase().ends_with(".jar") {
return Err(AppError("Only .jar files can be imported".to_string()));
}
jar_data = Some(field.bytes().await.map_err(|e| AppError(e.to_string()))?.to_vec());
driver_name = field.file_name().unwrap_or("agent").to_string();
driver_data = Some(field.bytes().await.map_err(|e| AppError(e.to_string()))?.to_vec());
}
}
let db_type = db_type.ok_or_else(|| AppError("Missing dbType field".to_string()))?;
let data = jar_data.ok_or_else(|| AppError("No file uploaded".to_string()))?;
let data = driver_data.ok_or_else(|| AppError("No file uploaded".to_string()))?;
let temp_dir = state.app.plugins.root_dir().join("jar_upload_tmp");
let temp_dir = state.app.plugins.root_dir().join("agent_upload_tmp");
std::fs::create_dir_all(&temp_dir).map_err(|e| AppError(e.to_string()))?;
let tmp_path = temp_dir.join(&jar_name);
let suffix = std::path::Path::new(&driver_name)
.extension()
.and_then(|extension| extension.to_str())
.map(|extension| format!(".{extension}"))
.unwrap_or_default();
let tmp_path = temp_dir.join(format!("agent-{}{}", uuid::Uuid::new_v4(), suffix));
std::fs::write(&tmp_path, &data).map_err(|e| AppError(e.to_string()))?;
dbx_core::agent_service::import_agent_jar(&state.app.agent_manager, &db_type, &tmp_path).map_err(AppError::from)?;
let result = async {
ensure_no_agent_update_blockers(&state.app, std::slice::from_ref(&db_type)).await.map_err(AppError)?;
import_agent_driver(&state.app.agent_manager, &db_type, &tmp_path).map_err(AppError::from)
}
.await;
let _ = std::fs::remove_file(&tmp_path);
result?;
Ok(Json(serde_json::json!({ "success": true })))
}
@ -302,3 +314,19 @@ async fn ensure_no_agent_update_blockers(
Err(format!("请先关闭以下数据库连接后再更新驱动: {}", blockers.join(", ")))
}
}
async fn ensure_no_offline_import_blockers(
state: &dbx_core::connection::AppState,
plan: &OfflineImportPlan,
) -> Result<(), String> {
let mut driver_keys = plan.driver_keys.clone();
if plan.includes_jre {
// Replacing a managed JRE affects every running Java Agent, so include
// all active runtimes in the same connection-aware update preflight.
driver_keys.extend(state.agent_manager.active_daemon_keys().await);
driver_keys.extend(state.active_agent_connection_driver_keys().await);
driver_keys.sort();
driver_keys.dedup();
}
ensure_no_agent_update_blockers(state, &driver_keys).await
}

View File

@ -17,9 +17,9 @@ const i18n = {
bundles: "Offline Bundles",
bundlesDesc: "Platform-specific ZIP packages that include the agent registry, database drivers, native agents, and the matching JRE.",
drivers: "Database Drivers",
driversDesc: "JDBC driver JAR files for each supported database type.",
driversDesc: "Single-driver offline ZIPs for Java agents. Install the matching JRE separately when it is not already available.",
nativeAgents: "Native Agents",
nativeAgentsDesc: "Go-based native agents for Oracle, KingBase, and XuguDB. Download the executable that matches the offline machine.",
nativeAgentsDesc: "Platform-specific offline ZIPs for Oracle, KingBase, and XuguDB. Import the ZIP directly in Driver Manager.",
jre: "Java Runtime (JRE)",
jreDesc: "JRE packages used by agent-based database drivers. Required for Oracle, SQL Server, and other agent-managed connections.",
loading: "Loading driver catalog...",
@ -56,9 +56,9 @@ const i18n = {
bundles: "整包下载",
bundlesDesc: "按平台提供的 ZIP 离线包,包含 Agent registry、数据库驱动、原生 Agent 和匹配的 JRE。",
drivers: "数据库驱动",
driversDesc: "每种支持的数据库类型对应的 JDBC 驱动 JAR 文件。",
driversDesc: "Java Agent 的单驱动离线 ZIP目标机器尚未安装 JRE 时需要另外安装一次对应 JRE。",
nativeAgents: "原生 Agent",
nativeAgentsDesc: "Oracle、人大金仓和虚谷使用 Go 原生 Agent请下载与内网机器平台匹配的可执行文件。",
nativeAgentsDesc: "Oracle、人大金仓和虚谷的按平台单驱动离线包,可直接在驱动管理中导入 ZIP。",
jre: "Java 运行时 (JRE)",
jreDesc: "Agent 驱动所需的 JRE 环境Oracle、SQL Server 等数据库通过 Agent 连接时需要。",
loading: "正在加载驱动列表...",

View File

@ -55,12 +55,22 @@ DBX 会定期检查驱动更新:
对于无法访问互联网的环境DBX 支持离线驱动安装:
1. 从另一台机器下载 JDBC 驱动 ZIP 包
1. 从另一台机器下载与目标操作系统和 CPU 架构匹配的 DBX 离线驱动 ZIP 包
2. 将其传输到离线机器
3. 在驱动商店中使用**从 ZIP 导入**安装驱动包
这适用于气隙网络、严格防火墙环境或企业中预先批准的驱动版本。
所有 Agent 都提供单驱动 ZIP可直接使用**导入离线包**安装。Java Agent 的单驱动包跨平台,但不重复携带 JREKingbase、Oracle、Xugu 等原生 Agent 的单驱动包按操作系统和 CPU 架构区分。
如果目标机器尚未安装 Java Agent 所需的托管 JRE请另外导入一次对应平台的 JRE或者直接使用包含全部驱动和 JRE 的完整离线包。
### Windows 导入 Kingbase
Windows x64 下载 `dbx-agent-kingbase-<version>-windows-x64.zip`Windows ARM64 下载 `dbx-agent-kingbase-<version>-windows-aarch64.zip`,然后在驱动管理中选择**导入离线包**。请勿导入 Linux 或 macOS 包。
ZIP 内包含版本信息和对应平台的原生 Agent。DBX 会将其安装为 `%USERPROFILE%\.dbx\agents\drivers\kingbase\agent.exe`,不需要手动解除浏览器下载文件的阻止状态或复制文件。
## 插件更新
对于由 [JDBC 插件](/cn/docs/plugins)支持的数据库当有新插件版本可用时DBX 会显示更新通知。插件更新遵循与内置 Agent 驱动相同的安装流程。

View File

@ -55,12 +55,22 @@ DBX checks for driver updates periodically:
For environments without internet access, DBX supports offline driver installation:
1. Download the JDBC driver ZIP bundle from another machine
1. Download the DBX offline driver ZIP matching the target OS and CPU architecture on another machine
2. Transfer it to the offline machine
3. In Driver Store, use **Import from ZIP** to install the driver bundle
This is useful for air-gapped networks, strict firewall environments, or pre-approved driver versions in enterprise settings.
Every agent provides a single-driver ZIP that can be installed with **Import offline package**. Java agent packages are platform-independent and do not duplicate the JRE, while native agents such as Kingbase, Oracle, and Xugu provide separate packages for each OS and CPU architecture.
If the target machine does not already have the managed JRE required by a Java agent, import the matching JRE package once or use the full offline bundle containing every driver and the JRE.
### Importing Kingbase on Windows
Download `dbx-agent-kingbase-<version>-windows-x64.zip` for Windows x64 or `dbx-agent-kingbase-<version>-windows-aarch64.zip` for Windows ARM64, then choose **Import offline package** in Driver Manager. Do not import a Linux or macOS package.
The ZIP contains the version metadata and native agent for that platform. DBX installs it as `%USERPROFILE%\.dbx\agents\drivers\kingbase\agent.exe`, without requiring users to unblock or manually copy the executable.
## Plugin Updates
For databases supported by the [JDBC Plugin](/en/docs/plugins), DBX shows update notices when a new plugin version is available. Plugin updates follow the same install flow as built-in agent drivers.

View File

@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { afterEach, test, vi } from "vitest";
import { buildAgentDownloadCatalog, buildNativeAgentEntries, downloadLinksFor, fetchAgentDownloadCatalog, formatSize } from "./agentRegistry";
import driverVersions from "../../agents/versions.json";
import { buildAgentDownloadCatalog, buildDriverEntries, buildNativeAgentEntries, downloadLinksFor, fetchAgentDownloadCatalog, formatSize } from "./agentRegistry";
afterEach(() => {
vi.restoreAllMocks();
@ -67,7 +68,26 @@ test("unknown fallback asset sizes render as unavailable", () => {
assert.equal(formatSize(0), "—");
});
test("KingBase release executables are listed as native agents", () => {
test("Java agent ZIPs are preferred over raw JARs", () => {
const accessVersion = driverVersions.access;
const entries = buildDriverEntries([
{
name: `dbx-agent-access-${accessVersion}.jar`,
browser_download_url: `https://example.com/dbx-agent-access-${accessVersion}.jar`,
size: 1024,
},
{
name: `dbx-agent-access-${accessVersion}.zip`,
browser_download_url: `https://example.com/dbx-agent-access-${accessVersion}.zip`,
size: 2048,
},
]);
assert.equal(entries[0]?.key, "access");
assert.equal(entries[0]?.jar.url, `https://example.com/dbx-agent-access-${accessVersion}.zip`);
});
test("KingBase native ZIPs are preferred over raw release executables", () => {
const entries = buildNativeAgentEntries([
{
name: "dbx-agent-kingbase-windows-x64.exe",
@ -75,22 +95,37 @@ test("KingBase release executables are listed as native agents", () => {
size: 1024,
},
{
name: "dbx-agent-kingbase-linux-x64",
browser_download_url: "https://example.com/dbx-agent-kingbase-linux-x64",
name: "dbx-agent-kingbase-0.1.34-windows-x64.exe",
browser_download_url: "https://example.com/dbx-agent-kingbase-0.1.34-windows-x64.exe",
size: 2048,
},
{
name: "dbx-agent-kingbase.jar",
browser_download_url: "https://example.com/dbx-agent-kingbase.jar",
name: "dbx-agent-kingbase-0.1.34-windows-x64.zip",
browser_download_url: "https://example.com/dbx-agent-kingbase-0.1.34-windows-x64.zip",
size: 4096,
},
{
name: "dbx-agent-kingbase-0.1.34-linux-x64.zip",
browser_download_url: "https://example.com/dbx-agent-kingbase-0.1.34-linux-x64.zip",
size: 3072,
},
]);
assert.deepEqual(
entries.map(({ key, platformKey, filename }) => ({ key, platformKey, filename })),
entries.map(({ key, version, platformKey, filename }) => ({ key, version, platformKey, filename })),
[
{ key: "kingbase", platformKey: "linux-x64", filename: "dbx-agent-kingbase-linux-x64" },
{ key: "kingbase", platformKey: "windows-x64", filename: "dbx-agent-kingbase-windows-x64.exe" },
{
key: "kingbase",
version: "0.1.34",
platformKey: "linux-x64",
filename: "dbx-agent-kingbase-0.1.34-linux-x64.zip",
},
{
key: "kingbase",
version: "0.1.34",
platformKey: "windows-x64",
filename: "dbx-agent-kingbase-0.1.34-windows-x64.zip",
},
],
);
});

View File

@ -28,6 +28,7 @@ interface AgentRegistryArtifact {
}
interface AgentRegistryDriver {
version?: string;
jar?: AgentRegistryArtifact;
native?: Record<string, AgentRegistryArtifact>;
}
@ -169,6 +170,11 @@ function releaseAssetName(url: string, fallback: string): string {
}
}
function siblingReleaseAssetUrl(url: string, filename: string): string {
const separator = url.lastIndexOf("/");
return separator >= 0 ? `${url.slice(0, separator + 1)}${filename}` : filename;
}
function githubReleaseAsset(name: string, size: number, tag = "agents-latest"): GitHubReleaseAsset {
return {
name,
@ -193,8 +199,25 @@ function registryReleaseAssets(registry: AgentRegistry): GitHubReleaseAsset[] {
for (const [driverKey, driver] of Object.entries(registry.drivers ?? {})) {
addArtifact(driver.jar, `dbx-agent-${driverKey}.jar`);
if (driver.version && driver.jar?.url && driver.jar.size > 0) {
const packageName = `dbx-agent-${driverKey}-${driver.version}.zip`;
assets.set(packageName, {
name: packageName,
size: 0,
browser_download_url: siblingReleaseAssetUrl(driver.jar.url, packageName),
});
}
for (const [platformKey, artifact] of Object.entries(driver.native ?? {})) {
addArtifact(artifact, `dbx-agent-${driverKey}-${platformKey}`);
const filename = releaseAssetName(artifact.url, "");
if (driver.version && filename.includes(`-${driver.version}-${platformKey}`)) {
const packageName = `${filename.replace(/\.exe$/, "")}.zip`;
assets.set(packageName, {
name: packageName,
size: 0,
browser_download_url: siblingReleaseAssetUrl(artifact.url, packageName),
});
}
}
}
@ -292,13 +315,17 @@ export function buildDriverEntries(assets: GitHubReleaseAsset[]): DriverDisplayE
return currentJavaDriverKeys
.map((key) => {
const asset = byName.get(`dbx-agent-${key}.jar`);
const version = driverVersionMap[key] ?? "";
const asset =
byName.get(`dbx-agent-${key}-${version}.zip`) ??
byName.get(`dbx-agent-${key}-${version}.jar`) ??
byName.get(`dbx-agent-${key}.jar`);
if (!asset) return null;
return {
key,
label: labelForDriver(key),
version: driverVersionMap[key] ?? "",
version,
minAppVersion: MIN_APP_VERSION,
jar: assetInfo(asset),
jre: "21",
@ -308,26 +335,39 @@ export function buildDriverEntries(assets: GitHubReleaseAsset[]): DriverDisplayE
}
export function buildNativeAgentEntries(assets: GitHubReleaseAsset[]): NativeAgentDisplayEntry[] {
const entries: NativeAgentDisplayEntry[] = [];
const entries = new Map<string, NativeAgentDisplayEntry & { packaged: boolean }>();
const platforms = "macos-aarch64|macos-x64|linux-aarch64|linux-x64|windows-aarch64|windows-x64";
for (const asset of assets) {
const match = /^dbx-agent-(.+?)-(macos-aarch64|macos-x64|linux-aarch64|linux-x64|windows-aarch64|windows-x64)(?:\.exe)?$/.exec(asset.name);
const packageMatch = new RegExp(`^dbx-agent-(oracle|kingbase|xugu)-(.+)-(${platforms})\\.zip$`).exec(asset.name);
const versionedMatch = new RegExp(`^dbx-agent-(oracle|kingbase|xugu)-(.+)-(${platforms})(?:\\.exe)?$`).exec(asset.name);
const legacyMatch = new RegExp(`^dbx-agent-(oracle|kingbase|xugu)-(${platforms})(?:\\.exe)?$`).exec(asset.name);
const match = packageMatch ?? versionedMatch ?? legacyMatch;
if (!match) continue;
const [, key, platformKey] = match;
const packaged = packageMatch !== null;
const key = match[1];
const version = legacyMatch ? (driverVersionMap[key] ?? "") : match[2];
const platformKey = legacyMatch ? match[2] : match[3];
if (!nativeDriverKeys.has(key)) continue;
entries.push({
const entryKey = `${key}:${platformKey}`;
const existing = entries.get(entryKey);
if (existing?.packaged && !packaged) continue;
entries.set(entryKey, {
key,
label: labelForDriver(key),
version: driverVersionMap[key] ?? "",
version,
platformKey,
platformLabel: platformLabels[platformKey] ?? platformKey,
filename: asset.name,
info: assetInfo(asset),
packaged,
});
}
return entries.sort((a, b) => a.label.localeCompare(b.label) || a.platformLabel.localeCompare(b.platformLabel));
return Array.from(entries.values())
.map(({ packaged: _, ...entry }) => entry)
.sort((a, b) => a.label.localeCompare(b.label) || a.platformLabel.localeCompare(b.platformLabel));
}
export function buildOfflineBundleEntries(assets: GitHubReleaseAsset[]): OfflineBundleEntry[] {

View File

@ -4,10 +4,10 @@ use tauri::{Emitter, State};
use dbx_core::agent_manager::{AgentDriverInfo, DriverStoreUsage, JavaRuntimeConfig, JavaRuntimeMode, DEFAULT_JRE_KEY};
use dbx_core::agent_service::{
build_agent_list, clear_agent_download_cache, fetch_registry_from, import_agent_jar,
import_agents_from_zip as import_agents_from_zip_core, install_agent_driver_from, invalidate_registry_cache,
reinstall_agent_jre_from, uninstall_agent_driver, uninstall_agent_jre, upgrade_all_agent_drivers_from,
AgentProgressEvent, UpgradeAllAgentDriversResult,
build_agent_list, clear_agent_download_cache, fetch_registry_from, import_agent_driver,
import_agents_from_zip as import_agents_from_zip_core, inspect_offline_zip, install_agent_driver_from,
invalidate_registry_cache, reinstall_agent_jre_from, uninstall_agent_driver, uninstall_agent_jre,
upgrade_all_agent_drivers_from, AgentProgressEvent, OfflineImportPlan, UpgradeAllAgentDriversResult,
};
use dbx_core::connection::AppState;
use dbx_core::driver_runtime::DriverRuntimeSummary;
@ -162,6 +162,8 @@ pub async fn import_agents_from_zip(
) -> Result<u32, String> {
let am = &state.agent_manager;
let zip_path = std::path::PathBuf::from(&path);
let plan = inspect_offline_zip(&zip_path)?;
ensure_no_offline_import_blockers(state.inner().as_ref(), &plan).await?;
let app_handle = app.clone();
let result = import_agents_from_zip_core(am, &zip_path, |event| emit_agent_progress(&app_handle, event))?;
let count = result.drivers_installed.len() as u32;
@ -169,13 +171,24 @@ pub async fn import_agents_from_zip(
Ok(count)
}
#[tauri::command]
pub async fn import_agent_driver_cmd(
state: State<'_, Arc<AppState>>,
db_type: String,
path: String,
) -> Result<(), String> {
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
import_agent_driver(&state.agent_manager, &db_type, std::path::Path::new(&path))
}
#[tauri::command]
pub async fn import_agent_jar_cmd(
state: State<'_, Arc<AppState>>,
db_type: String,
path: String,
) -> Result<(), String> {
import_agent_jar(&state.agent_manager, &db_type, std::path::Path::new(&path))
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
import_agent_driver(&state.agent_manager, &db_type, std::path::Path::new(&path))
}
#[tauri::command]
@ -206,6 +219,19 @@ async fn ensure_no_agent_update_blockers(state: &AppState, db_types: &[String])
Err(format!("请先关闭以下数据库连接后再更新驱动: {labels}"))
}
async fn ensure_no_offline_import_blockers(state: &AppState, plan: &OfflineImportPlan) -> Result<(), String> {
let mut driver_keys = plan.driver_keys.clone();
if plan.includes_jre {
// Replacing a managed JRE affects every running Java Agent, so include
// all active runtimes in the same connection-aware update preflight.
driver_keys.extend(state.agent_manager.active_daemon_keys().await);
driver_keys.extend(state.active_agent_connection_driver_keys().await);
driver_keys.sort();
driver_keys.dedup();
}
ensure_no_agent_update_blockers(state, &driver_keys).await
}
async fn agent_update_blockers(state: &AppState, db_types: &[String]) -> Vec<AgentUpdateBlocker> {
update_blockers_from_keys(state.active_agent_connection_driver_keys().await, db_types)
}

View File

@ -1471,6 +1471,7 @@ pub fn run() {
commands::agents::reinstall_jre,
commands::agents::invalidate_agent_registry_cache,
commands::agents::import_agents_from_zip,
commands::agents::import_agent_driver_cmd,
commands::agents::import_agent_jar_cmd,
commands::system_fonts::list_system_fonts,
commands::ssh_config::list_ssh_config_hosts,