feat(agents): add versioned offline driver packages
This commit is contained in:
parent
8d68a527a0
commit
79cd12d13a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "ローカルインストール",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "本地安装",
|
||||
|
|
|
|||
|
|
@ -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: "本機安裝",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
});
|
||||
});
|
||||
|
|
@ -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" : "";
|
||||
}
|
||||
|
|
@ -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, ®istry)?;
|
||||
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(®istry, platform, &name)?;
|
||||
Some((db_type, name, true))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let (jre_entries, driver_entries) = collect_offline_entries(&mut archive, ®istry)?;
|
||||
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: "正在加载驱动列表...",
|
||||
|
|
|
|||
|
|
@ -55,12 +55,22 @@ DBX 会定期检查驱动更新:
|
|||
|
||||
对于无法访问互联网的环境,DBX 支持离线驱动安装:
|
||||
|
||||
1. 从另一台机器下载 JDBC 驱动 ZIP 包
|
||||
1. 从另一台机器下载与目标操作系统和 CPU 架构匹配的 DBX 离线驱动 ZIP 包
|
||||
2. 将其传输到离线机器
|
||||
3. 在驱动商店中使用**从 ZIP 导入**安装驱动包
|
||||
|
||||
这适用于气隙网络、严格防火墙环境或企业中预先批准的驱动版本。
|
||||
|
||||
所有 Agent 都提供单驱动 ZIP,可直接使用**导入离线包**安装。Java Agent 的单驱动包跨平台,但不重复携带 JRE;Kingbase、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 驱动相同的安装流程。
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue