mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 10:29:52 +08:00
🐛 fix(driver-manager): 修复驱动 revision 错配与安装卡住
- 修复 revision 生成文件变更未触发 driver-agent 构建的问题 - 检测脚本自身变更时保守触发全量 driver-agent 构建 - 调整 dev 构建驱动安装策略为发布包优先、源码构建兜底 - 为驱动总包下载和源码构建增加超时边界 - 为驱动管理安装流程增加前端看门狗并补充回归测试
This commit is contained in:
@@ -201,4 +201,33 @@ describe('DriverManagerModal toolbar actions', () => {
|
||||
expect(importDirButtonAfter.props.disabled).toBeFalsy();
|
||||
expect(installAllButtonAfter.props.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('releases install action when the driver install watchdog expires', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<DriverManagerModal open onClose={vi.fn()} />);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const installButton = findButton(renderer!, '安装启用');
|
||||
await act(async () => {
|
||||
installButton.props.onClick();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(findButton(renderer!, '安装启用').props.disabled).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(12 * 60 * 1000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(findButton(renderer!, '安装启用').props.disabled).toBeFalsy();
|
||||
expect(messageApi.error).toHaveBeenCalledWith(expect.stringContaining('超过 12 分钟仍未完成'));
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,6 +123,7 @@ const buildVersionOptionKey = (option: DriverVersionOption) => `${option.version
|
||||
const buildVersionSizeLoadingKey = (driverType: string, optionKey: string) => `${driverType}@@${optionKey}`;
|
||||
const DRIVER_STATUS_CACHE_TTL_MS = 60 * 1000;
|
||||
const DRIVER_NETWORK_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const DRIVER_INSTALL_WATCHDOG_MS = 12 * 60 * 1000;
|
||||
const normalizeDriverSearchText = (value: string) => String(value || '').trim().toLowerCase();
|
||||
const isSlimBuildInstallUnavailable = (row: DriverStatusRow) => (row.message || '').includes('精简构建') && !row.packageInstalled;
|
||||
const resolveDriverBatchActionLabel = (actionKind: DriverBatchActionKind) => {
|
||||
@@ -703,6 +704,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
percent: 0,
|
||||
});
|
||||
appendOperationLog(row.type, '[START] 开始自动安装');
|
||||
let watchdogId: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
let versionOptions = versionMap[row.type] || [];
|
||||
if (versionOptions.length === 0) {
|
||||
@@ -716,7 +718,18 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
const selectedVersion = selectedOption?.version || row.pinnedVersion || '';
|
||||
const selectedDownloadURL = selectedOption?.downloadUrl || row.defaultDownloadUrl || '';
|
||||
|
||||
const result = await DownloadDriverPackage(row.type, selectedVersion, selectedDownloadURL, downloadDir);
|
||||
const installWatchdog = new Promise<never>((_, reject) => {
|
||||
watchdogId = setTimeout(() => {
|
||||
reject(new Error(`安装 ${row.name} 超过 ${Math.round(DRIVER_INSTALL_WATCHDOG_MS / 60000)} 分钟仍未完成。后台任务可能仍在下载或构建,请稍后刷新状态;如多次出现,请检查代理或改用本地驱动包导入。`));
|
||||
}, DRIVER_INSTALL_WATCHDOG_MS);
|
||||
});
|
||||
const result = await Promise.race([
|
||||
DownloadDriverPackage(row.type, selectedVersion, selectedDownloadURL, downloadDir),
|
||||
installWatchdog,
|
||||
]);
|
||||
if (watchdogId) {
|
||||
clearTimeout(watchdogId);
|
||||
}
|
||||
if (!result?.success) {
|
||||
const errText = result?.message || `安装 ${row.name} 失败`;
|
||||
appendOperationLog(row.type, `[ERROR] ${errText}`);
|
||||
@@ -734,7 +747,22 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
await refreshStatus(false);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errText = error instanceof Error ? error.message : String(error || `安装 ${row.name} 失败`);
|
||||
appendOperationLog(row.type, `[ERROR] ${errText}`);
|
||||
updateDriverProgress(row.type, {
|
||||
status: 'error',
|
||||
message: errText,
|
||||
percent: 0,
|
||||
});
|
||||
if (!actionOptions?.silentToast) {
|
||||
message.error(errText);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (watchdogId) {
|
||||
clearTimeout(watchdogId);
|
||||
}
|
||||
setActionState({ driverType: '', kind: '' });
|
||||
}
|
||||
}, [appendOperationLog, downloadDir, loadVersionOptions, refreshStatus, selectedVersionMap, updateDriverProgress, versionMap]);
|
||||
|
||||
@@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -301,7 +302,7 @@ const (
|
||||
optionalDriverBundleAssetName = "GoNavi-DriverAgents.zip"
|
||||
duckDBWindowsDriverZipAssetName = "duckdb-driver.zip"
|
||||
optionalDriverBundleIndexAssetName = "GoNavi-DriverAgents-Index.json"
|
||||
optionalDriverBundleDownloadTimeout = 45 * time.Minute
|
||||
optionalDriverBundleDownloadTimeout = 15 * time.Minute
|
||||
optionalDriverBundleCacheMaxAge = 7 * 24 * time.Hour
|
||||
optionalDriverBundleCacheMaxFiles = 4
|
||||
driverManifestCacheTTL = 5 * time.Minute
|
||||
@@ -373,6 +374,8 @@ var (
|
||||
errLocalDriverDirScanLimit = errors.New("local_driver_directory_scan_limit_exceeded")
|
||||
)
|
||||
|
||||
var optionalDriverSourceBuildTimeout = 8 * time.Minute
|
||||
|
||||
var validateOptionalDriverAgentExecutableFunc = db.ValidateOptionalDriverAgentExecutable
|
||||
|
||||
type driverVersionWarmupState struct {
|
||||
@@ -3829,10 +3832,15 @@ func buildOptionalDriverAgentFromSource(definition driverDefinition, executableP
|
||||
env = prependPathEnv(env, duckDBLibDir)
|
||||
}
|
||||
buildArgs = append(buildArgs, "-o", executablePath, "./cmd/optional-driver-agent")
|
||||
cmd := exec.Command(goPath, buildArgs...)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), optionalDriverSourceBuildTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, goPath, buildArgs...)
|
||||
cmd.Dir = projectRoot
|
||||
cmd.Env = env
|
||||
output, buildErr := cmd.CombinedOutput()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", fmt.Errorf("构建 %s 驱动代理超时(超过 %s),请优先使用预编译驱动包或本地驱动包导入", displayName, optionalDriverSourceBuildTimeout)
|
||||
}
|
||||
if buildErr != nil {
|
||||
return "", fmt.Errorf("构建 %s 驱动代理失败:%v,输出:%s", displayName, buildErr, strings.TrimSpace(string(output)))
|
||||
}
|
||||
@@ -3978,26 +3986,15 @@ func shouldPreferSourceBuildBeforeDownload(driverType string, selectedVersion st
|
||||
|
||||
func shouldPreferSourceBuildBeforeDownloadForBuildType(buildType string, driverType string, selectedVersion string) bool {
|
||||
_ = selectedVersion
|
||||
if shouldPreferDevelopmentDriverAgentSourceBuild(buildType, driverType) {
|
||||
return true
|
||||
}
|
||||
_ = buildType
|
||||
return shouldUseDuckDBWindowsDynamicLibrary(driverType)
|
||||
}
|
||||
|
||||
func shouldRequireSourceBuildBeforeDownloadForBuildType(buildType string, driverType string, selectedVersion string) bool {
|
||||
_ = selectedVersion
|
||||
if normalizeDriverType(driverType) == "duckdb" {
|
||||
return false
|
||||
}
|
||||
return shouldPreferDevelopmentDriverAgentSourceBuild(buildType, driverType)
|
||||
}
|
||||
|
||||
func shouldPreferDevelopmentDriverAgentSourceBuild(buildType string, driverType string) bool {
|
||||
normalizedBuildType := strings.ToLower(strings.TrimSpace(buildType))
|
||||
if normalizedBuildType != "dev" && normalizedBuildType != "development" {
|
||||
return false
|
||||
}
|
||||
return db.IsOptionalGoDriver(driverType)
|
||||
_ = buildType
|
||||
_ = driverType
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldSkipReusableAgentCandidate(driverType string, selectedVersion string) bool {
|
||||
|
||||
@@ -630,11 +630,11 @@ func TestShouldPreferSourceBuildBeforeDownloadDoesNotPreferKingbase(t *testing.T
|
||||
}
|
||||
|
||||
func TestShouldPreferSourceBuildBeforeDownloadForDevelopmentBuild(t *testing.T) {
|
||||
if !shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "mariadb", "1.9.3") {
|
||||
t.Fatal("expected development build to prefer local driver-agent source build")
|
||||
if shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "mariadb", "1.9.3") {
|
||||
t.Fatal("expected development release build to prefer published MariaDB driver-agent before source fallback")
|
||||
}
|
||||
if !shouldPreferSourceBuildBeforeDownloadForBuildType("development", "clickhouse", "2.43.1") {
|
||||
t.Fatal("expected development build alias to prefer local driver-agent source build")
|
||||
if shouldPreferSourceBuildBeforeDownloadForBuildType("development", "clickhouse", "2.43.1") && !shouldUseDuckDBWindowsDynamicLibrary("clickhouse") {
|
||||
t.Fatal("expected development build alias not to prefer source build for ClickHouse")
|
||||
}
|
||||
if shouldPreferSourceBuildBeforeDownloadForBuildType("production", "mariadb", "1.9.3") {
|
||||
t.Fatal("expected production build not to prefer source build for MariaDB")
|
||||
@@ -648,11 +648,15 @@ func TestShouldRequireSourceBuildBeforeDownloadForDevelopmentBuild(t *testing.T)
|
||||
if shouldRequireSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
|
||||
t.Fatal("expected development build to allow DuckDB release bundle fallback after local build failure")
|
||||
}
|
||||
if !shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
|
||||
t.Fatal("expected development build to still prefer local DuckDB driver-agent source build before bundle fallback")
|
||||
if shouldUseDuckDBWindowsDynamicLibrary("duckdb") {
|
||||
if !shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
|
||||
t.Fatal("expected DuckDB Windows dynamic-library install to prefer local source build before bundle fallback")
|
||||
}
|
||||
} else if shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
|
||||
t.Fatal("expected development build not to prefer DuckDB source build on non-Windows dynamic-library platforms")
|
||||
}
|
||||
if !shouldRequireSourceBuildBeforeDownloadForBuildType("development", "mariadb", "1.9.3") {
|
||||
t.Fatal("expected development build alias to require local driver-agent source build")
|
||||
if shouldRequireSourceBuildBeforeDownloadForBuildType("development", "mariadb", "1.9.3") {
|
||||
t.Fatal("expected development build alias to allow published MariaDB driver-agent fallback")
|
||||
}
|
||||
if shouldRequireSourceBuildBeforeDownloadForBuildType("production", "duckdb", "2.5.6") {
|
||||
t.Fatal("expected production build to allow DuckDB release bundle fallback")
|
||||
@@ -662,6 +666,15 @@ func TestShouldRequireSourceBuildBeforeDownloadForDevelopmentBuild(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalDriverInstallTimeoutsStayBounded(t *testing.T) {
|
||||
if optionalDriverBundleDownloadTimeout > 15*time.Minute {
|
||||
t.Fatalf("driver bundle download timeout should stay bounded, got %s", optionalDriverBundleDownloadTimeout)
|
||||
}
|
||||
if optionalDriverSourceBuildTimeout > 8*time.Minute {
|
||||
t.Fatalf("driver source build timeout should stay bounded, got %s", optionalDriverSourceBuildTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDuckDBWindowsCGOToolchainBinFromCandidates(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
writeSelfExecutable(t, filepath.Join(binDir, "gcc.exe"))
|
||||
|
||||
@@ -320,9 +320,31 @@ add_all_forced_drivers() {
|
||||
done
|
||||
}
|
||||
|
||||
revision_file_changed_drivers() {
|
||||
local line driver emitted_seen
|
||||
emitted_seen="|"
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
+++*|---*|@@*)
|
||||
continue
|
||||
;;
|
||||
+*|-*)
|
||||
if [[ "$line" =~ \"([^\"]+)\"[[:space:]]*:[[:space:]]*\"src-[^\"]+\" ]]; then
|
||||
driver="$(normalize_driver "${BASH_REMATCH[1]}")" || continue
|
||||
if [[ "$emitted_seen" == *"|$driver|"* ]]; then
|
||||
continue
|
||||
fi
|
||||
printf '%s\n' "$driver"
|
||||
emitted_seen="${emitted_seen}${driver}|"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < <(git diff --unified=0 "$base_commit" "$head_commit" -- internal/db/driver_agent_revisions_gen.go)
|
||||
}
|
||||
|
||||
is_ignored_driver_agent_source_file() {
|
||||
case "$1" in
|
||||
*_test.go|frontend/*|internal/app/*|internal/db/driver_agent_revisions_gen.go)
|
||||
*_test.go|frontend/*|internal/app/*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
@@ -469,6 +491,15 @@ declare -a forced_changed_drivers=()
|
||||
forced_driver_seen="|"
|
||||
for file in "${!changed_file_set[@]}"; do
|
||||
case "$file" in
|
||||
internal/db/driver_agent_revisions_gen.go)
|
||||
revision_delta="$(revision_file_changed_drivers)"
|
||||
if [[ -z "$revision_delta" ]]; then
|
||||
echo "检测到 driver-agent revision 文件存在无法归因的变更;保守构建全部 driver-agent:$file" >&2
|
||||
all_drivers_csv
|
||||
exit 0
|
||||
fi
|
||||
add_forced_drivers_from_tokens "$revision_delta"
|
||||
;;
|
||||
go.mod|go.sum|build-driver-agents.sh|tools/generate-driver-agent-revisions.sh)
|
||||
set +e
|
||||
shared_delta="$(shared_file_driver_delta "$file")"
|
||||
@@ -487,7 +518,9 @@ for file in "${!changed_file_set[@]}"; do
|
||||
exit 0
|
||||
;;
|
||||
tools/detect-changed-driver-agents.sh)
|
||||
# This script only selects CI work; it is not embedded in driver-agent binaries.
|
||||
echo "检测到 driver-agent 变更检测脚本更新;保守构建全部 driver-agent:$file" >&2
|
||||
all_drivers_csv
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
72
tools/detect-changed-driver-agents.test.sh
Executable file
72
tools/detect-changed-driver-agents.test.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${BASH_VERSINFO[0]:-0}" -lt 4 ]]; then
|
||||
echo "skip: detect-changed-driver-agents.sh requires Bash 4+ for associative arrays; current bash is ${BASH_VERSION:-unknown}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-detect-driver-revisions.XXXXXX")"
|
||||
tmpdir_script=""
|
||||
cleanup() {
|
||||
rm -rf "$tmpdir"
|
||||
if [[ -n "$tmpdir_script" ]]; then
|
||||
rm -rf "$tmpdir_script"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
git init -q "$tmpdir"
|
||||
mkdir -p "$tmpdir/tools"
|
||||
cp tools/detect-changed-driver-agents.sh "$tmpdir/tools/detect-changed-driver-agents.sh"
|
||||
mkdir -p "$tmpdir/internal/db"
|
||||
cat >"$tmpdir/internal/db/driver_agent_revisions_gen.go" <<'GOEOF'
|
||||
package db
|
||||
|
||||
func init() {
|
||||
optionalDriverAgentRevisions = map[string]string{
|
||||
"mariadb": "src-old-mariadb",
|
||||
"clickhouse": "src-old-clickhouse",
|
||||
}
|
||||
}
|
||||
GOEOF
|
||||
|
||||
(
|
||||
cd "$tmpdir"
|
||||
git add .
|
||||
git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m initial
|
||||
base="$(git rev-parse HEAD)"
|
||||
perl -0pi -e 's/src-old-clickhouse/src-new-clickhouse/' internal/db/driver_agent_revisions_gen.go
|
||||
git add internal/db/driver_agent_revisions_gen.go
|
||||
git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m 'update clickhouse revision'
|
||||
actual="$(bash ./tools/detect-changed-driver-agents.sh --base "$base" --head HEAD)"
|
||||
if [[ "$actual" != "clickhouse" ]]; then
|
||||
echo "expected clickhouse revision-only change to trigger clickhouse build, got: ${actual:-<empty>}" >&2
|
||||
exit 1
|
||||
fi
|
||||
)
|
||||
|
||||
tmpdir_script="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-detect-script-change.XXXXXX")"
|
||||
git init -q "$tmpdir_script"
|
||||
mkdir -p "$tmpdir_script/tools"
|
||||
cp tools/detect-changed-driver-agents.sh "$tmpdir_script/tools/detect-changed-driver-agents.sh"
|
||||
(
|
||||
cd "$tmpdir_script"
|
||||
git add .
|
||||
git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m initial
|
||||
base="$(git rev-parse HEAD)"
|
||||
printf '\n# test change\n' >> tools/detect-changed-driver-agents.sh
|
||||
git add tools/detect-changed-driver-agents.sh
|
||||
git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m 'update detection script'
|
||||
actual="$(bash ./tools/detect-changed-driver-agents.sh --base "$base" --head HEAD 2>/dev/null)"
|
||||
if [[ "$actual" != *"mariadb"* || "$actual" != *"clickhouse"* || "$actual" != *"elasticsearch"* ]]; then
|
||||
echo "expected detection script change to trigger all driver builds, got: ${actual:-<empty>}" >&2
|
||||
exit 1
|
||||
fi
|
||||
)
|
||||
|
||||
echo "detect-changed-driver-agents revision test passed"
|
||||
Reference in New Issue
Block a user