diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
index a0680e1b..57c0c58c 100644
--- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx
+++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
@@ -364,10 +364,19 @@ vi.mock('./DataGrid', () => ({
}));
vi.mock('./LogPanel', () => ({
- default: ({ variant, executionError }: { variant?: string; executionError?: string }) => (
+ default: ({
+ variant,
+ executionError,
+ onDiagnoseExecutionError,
+ }: {
+ variant?: string;
+ executionError?: string;
+ onDiagnoseExecutionError?: () => void;
+ }) => (
SQL 执行日志
- {executionError ? ` ${executionError}` : ''}
+ {executionError ? ` 执行失败 ${executionError}` : ''}
+ {onDiagnoseExecutionError ? : null}
),
}));
@@ -2514,6 +2523,7 @@ describe('QueryEditor external SQL save', () => {
vi.useFakeTimers();
try {
storeState.aiPanelVisible = true;
+ storeState.appearance.uiVersion = 'v2';
let renderer!: ReactTestRenderer;
await act(async () => {
@@ -2554,6 +2564,8 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
+ expect(textContent(renderer.toJSON())).toContain('SQL 执行日志');
+
await act(async () => {
findButton(renderer, 'AI diagnose').props.onClick();
vi.runAllTimers();
@@ -4573,6 +4585,7 @@ describe('QueryEditor external SQL save', () => {
});
it('shows Chinese semantic meaning for SQL execution errors', async () => {
+ storeState.appearance.uiVersion = 'v2';
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: false,
message: 'pq: syntax error at or near "from"',
@@ -4592,6 +4605,7 @@ describe('QueryEditor external SQL save', () => {
});
const pageText = textContent(renderer!.root);
+ expect(pageText).toContain('SQL 执行日志');
expect(pageText).toContain('执行失败');
expect(pageText).toContain('中文语义:SQL 语法错误');
expect(pageText).toContain('处理建议:');
@@ -7014,7 +7028,7 @@ describe('QueryEditor external SQL save', () => {
it('keeps query result tabs compact, centered, and readable in v2 UI', () => {
const source = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8');
- const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8');
+ const css = readV2ThemeCss();
expect(source).toContain('.query-result-tabs .ant-tabs-tab {');
expect(source).toContain('width: auto !important;');
@@ -7037,7 +7051,7 @@ describe('QueryEditor external SQL save', () => {
const resultsPanelSource = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8');
const transactionSettingsSource = readFileSync(new URL('./QueryEditorTransactionSettings.tsx', import.meta.url), 'utf8');
const transactionToolbarSource = readFileSync(new URL('./QueryEditorTransactionToolbar.tsx', import.meta.url), 'utf8');
- const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8');
+ const css = readV2ThemeCss();
expect(source).toContain('QueryEditorToolbar');
expect(toolbarSource).toContain('gn-v2-query-toolbar-selects');
diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx
index fadc7aa3..9d77890f 100644
--- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx
+++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx
@@ -5,7 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { readV2ThemeCss } from '../test/readV2ThemeCss';
import { setCurrentLanguage } from '../i18n';
+import { catalogs } from '../i18n/catalog';
import type { SavedQuery, TabData } from '../types';
+import { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
import QueryEditor, {
@@ -356,6 +358,15 @@ vi.mock('./DataGrid', () => ({
GONAVI_ROW_KEY: '__gonavi_row_key__',
}));
+vi.mock('./LogPanel', () => ({
+ default: ({ variant, executionError }: { variant?: string; executionError?: string }) => (
+
+ SQL 执行日志
+ {executionError ? ` ${executionError}` : ''}
+
+ ),
+}));
+
vi.mock('@ant-design/icons', () => {
const Icon = () => ;
return {
@@ -460,10 +471,12 @@ vi.mock('antd', () => {
};
});
-const textContent = (node: any): string =>
- (node.children || [])
- .map((item: any) => (typeof item === 'string' ? item : textContent(item)))
- .join('');
+const textContent = (node: any): string => {
+ if (node === null || node === undefined) return '';
+ if (typeof node === 'string' || typeof node === 'number') return String(node);
+ if (Array.isArray(node)) return node.map((item) => textContent(item)).join('');
+ return textContent(node.children || []);
+};
const findButton = (renderer: ReactTestRenderer, text: string) =>
renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0];
@@ -1303,9 +1316,13 @@ describe('QueryEditor external SQL save', () => {
renderer = create();
});
+ await act(async () => {
+ findButton(renderer!, 'Show results panel').props.onClick();
+ });
+
const rendered = textContent(renderer!.toJSON());
- expect(rendered).toContain('Awaiting SQL execution');
- expect(rendered).toContain('Run a query to display results below in the new data grid.');
+ expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.title']);
+ expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.description']);
expect(rendered).not.toContain('等待执行 SQL');
expect(rendered).not.toContain('运行查询后,结果会在下方以新版数据网格展示。');
});
@@ -1401,7 +1418,7 @@ describe('QueryEditor external SQL save', () => {
editorState.position = { lineNumber: 3, column: 1 };
await act(async () => {
- const runButton = findButton(renderer!, 'Run');
+ const runButton = findButton(renderer!, '运行');
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
await runButton.props.onClick();
});
@@ -1441,7 +1458,7 @@ describe('QueryEditor external SQL save', () => {
};
await act(async () => {
- const runButton = findButton(renderer!, 'Run');
+ const runButton = findButton(renderer!, '运行');
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
await runButton.props.onClick();
});
@@ -1483,7 +1500,7 @@ describe('QueryEditor external SQL save', () => {
};
await act(async () => {
- const runButton = findButton(renderer!, '运行');
+ const runButton = findButton(renderer!, 'Run');
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
await runButton.props.onClick();
});
@@ -1492,7 +1509,7 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
- expect(textContent(renderer!.toJSON())).toContain('结果 1');
+ expect(textContent(renderer!.toJSON())).toContain('Result 1');
backendApp.DBQueryMulti.mockClear();
messageApi.info.mockClear();
@@ -1510,7 +1527,7 @@ describe('QueryEditor external SQL save', () => {
});
await act(async () => {
- const runButton = findButton(renderer!, '运行');
+ const runButton = findButton(renderer!, 'Run');
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
await runButton.props.onClick();
});
@@ -1646,7 +1663,8 @@ describe('QueryEditor external SQL save', () => {
});
const rendered = textContent(renderer.toJSON());
- expect(rendered).toContain('Statement 2 failed: driver exploded');
+ expect(rendered).toContain('Statement 2 failed:');
+ expect(rendered).toContain('Raw error: driver exploded');
expect(rendered).not.toContain('第 2 条语句执行失败:driver exploded');
});
@@ -1718,11 +1736,16 @@ describe('QueryEditor external SQL save', () => {
expect(messageApi.success).not.toHaveBeenCalledWith('已执行完成,生成 2 个结果集。');
});
- it('shows the non-Mongo zero-result success toast in English', async () => {
+ it('renders the non-Mongo zero-row transactional result in English', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
const query = 'update users set active = 1 where 1 = 0;';
- backendApp.DBQueryMulti.mockResolvedValueOnce({ success: true, data: [] });
+ backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({
+ success: true,
+ transactionId: 'tx-zero-rows',
+ transactionPending: true,
+ data: [{ columns: ['affectedRows'], rows: [{ affectedRows: 0 }], statementIndex: 1 }],
+ });
let renderer!: ReactTestRenderer;
await act(async () => {
@@ -1747,11 +1770,16 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
+ const rendered = textContent(renderer.toJSON());
expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled();
- expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(1);
- expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('update users set active = 1 where 1 = 0');
- expect(messageApi.success).toHaveBeenCalledWith('Execution succeeded.');
- expect(messageApi.success).not.toHaveBeenCalledWith('执行成功。');
+ expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
+ expect(backendApp.DBQueryMultiTransactional).toHaveBeenCalledTimes(1);
+ expect(String(backendApp.DBQueryMultiTransactional.mock.calls[0][2])).toContain('update users set active = 1 where 1 = 0');
+ expect(rendered).toContain(catalogs['en-US']['query_editor.result.execution_success']);
+ expect(rendered).toContain(catalogs['en-US']['query_editor.result.affected_rows'].replace('{{count}}', '0'));
+ expect(rendered).not.toContain('执行成功');
+ expect(rendered).not.toContain('影响行数:0');
+ expect(messageApi.success).not.toHaveBeenCalledWith('Execution succeeded.');
});
it('shows the wrapped execution failure toast in English while preserving raw error detail', async () => {
@@ -1786,7 +1814,7 @@ describe('QueryEditor external SQL save', () => {
expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled();
expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(1);
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('select 1');
- expect(messageApi.error).toHaveBeenCalledWith('Query execution failed: driver exploded');
+ expect(messageApi.error).toHaveBeenCalledWith(`Query execution failed: ${formatSqlExecutionError('driver exploded')}`);
expect(messageApi.error).not.toHaveBeenCalledWith('Error executing query: driver exploded');
});
});
@@ -1841,7 +1869,7 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
- expect(messageApi.error).toHaveBeenCalledWith('Refresh failed: network down');
+ expect(messageApi.error).toHaveBeenCalledWith(`Refresh failed: ${formatSqlExecutionError('network down')}`);
expect(messageApi.error).not.toHaveBeenCalledWith('刷新失败: network down');
});
@@ -1867,7 +1895,7 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
- expect(messageApi.error).toHaveBeenCalledWith('Refresh failed: socket closed');
+ expect(messageApi.error).toHaveBeenCalledWith(`Refresh failed: ${formatSqlExecutionError('socket closed')}`);
expect(messageApi.error).not.toHaveBeenCalledWith('刷新失败: socket closed');
});
});
@@ -2432,7 +2460,6 @@ describe('QueryEditor external SQL save', () => {
const modalSource = readFileSync(new URL('./SnippetSettingsModal.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
- expect(modalSource).toContain('片段语法说明(可编辑)');
expect(modalSource).toContain('data-sql-snippet-syntax-help-editor="true"');
expect(modalSource).toContain("defaultActiveKey={['snippet-help']}");
expect(modalSource).toContain('footer={null}');
@@ -2441,7 +2468,7 @@ describe('QueryEditor external SQL save', () => {
expect(modalSource).toContain("size=\"large\"");
expect(modalSource).toContain('minWidth: 96');
expect(modalSource).toContain('syntaxHelp');
- expect(modalSource).toContain('占位符语法参考');
+ expect(modalSource).toContain("t('snippet_settings.syntax_reference.label')");
expect(source).toContain('s.syntaxHelp || s.description || s.body');
});
diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go
index d0504f37..d2539d1c 100644
--- a/internal/app/methods_driver.go
+++ b/internal/app/methods_driver.go
@@ -4846,1354 +4846,3 @@ func prepareDuckDBWindowsDynamicLibraryForBuild() (string, func(), error) {
return workDir, cleanup, nil
}
-
-func optionalDriverPublicTypeName(driverType string) string {
- switch normalizeDriverType(driverType) {
- case "diros":
- return "doris"
- default:
- return normalizeDriverType(driverType)
- }
-}
-
-func optionalDriverExecutableBaseNameForType(typeName string) string {
- base := strings.TrimSpace(typeName)
- if base == "" {
- base = "unknown"
- }
- name := fmt.Sprintf("%s-driver-agent", base)
- if stdRuntime.GOOS == "windows" {
- return name + ".exe"
- }
- return name
-}
-
-func optionalDriverReleaseAssetNameForType(typeName string, goos string, goarch string) string {
- base := strings.TrimSpace(typeName)
- if base == "" {
- base = "unknown"
- }
- name := fmt.Sprintf("%s-driver-agent-%s-%s", base, goos, goarch)
- if strings.EqualFold(goos, "windows") {
- return name + ".exe"
- }
- return name
-}
-
-func optionalDriverNameStemCandidates(driverType string, selectedVersion string) []string {
- candidates := make([]string, 0, 3)
- seen := make(map[string]struct{}, 3)
- appendStem := func(stem string) {
- trimmed := strings.TrimSpace(stem)
- if trimmed == "" {
- return
- }
- if _, ok := seen[trimmed]; ok {
- return
- }
- seen[trimmed] = struct{}{}
- candidates = append(candidates, trimmed)
- }
-
- base := fmt.Sprintf("%s-driver-agent", optionalDriverPublicTypeName(driverType))
- if normalizeDriverType(driverType) == "mongodb" {
- switch resolveMongoDriverMajorFromVersion(selectedVersion) {
- case 1:
- appendStem(base + "-v1")
- case 2:
- appendStem(base + "-v2")
- appendStem(base)
- default:
- appendStem(base)
- }
- return candidates
- }
-
- appendStem(base)
- return candidates
-}
-
-func optionalDriverExecutableBaseNamesForVersion(driverType string, selectedVersion string) []string {
- names := make([]string, 0, 2)
- seen := make(map[string]struct{}, 2)
- appendName := func(stem string) {
- name := strings.TrimSpace(stem)
- if strings.TrimSpace(name) == "" {
- return
- }
- if stdRuntime.GOOS == "windows" {
- name += ".exe"
- }
- if _, ok := seen[name]; ok {
- return
- }
- seen[name] = struct{}{}
- names = append(names, name)
- }
-
- for _, stem := range optionalDriverNameStemCandidates(driverType, selectedVersion) {
- appendName(stem)
- }
- return names
-}
-
-func optionalDriverExecutableBaseNames(driverType string) []string {
- return optionalDriverExecutableBaseNamesForVersion(driverType, "")
-}
-
-func optionalDriverReleaseAssetNamesForVersion(driverType string, selectedVersion string) []string {
- names := make([]string, 0, 2)
- seen := make(map[string]struct{}, 2)
- appendName := func(stem string) {
- trimmedStem := strings.TrimSpace(stem)
- if trimmedStem == "" {
- return
- }
- name := fmt.Sprintf("%s-%s-%s", trimmedStem, stdRuntime.GOOS, stdRuntime.GOARCH)
- if strings.EqualFold(stdRuntime.GOOS, "windows") {
- name += ".exe"
- }
- if strings.TrimSpace(name) == "" {
- return
- }
- if _, ok := seen[name]; ok {
- return
- }
- seen[name] = struct{}{}
- names = append(names, name)
- }
-
- for _, stem := range optionalDriverNameStemCandidates(driverType, selectedVersion) {
- appendName(stem)
- }
- return names
-}
-
-func optionalDriverReleaseAssetNames(driverType string) []string {
- return optionalDriverReleaseAssetNamesForVersion(driverType, "")
-}
-
-func optionalDriverExecutableBaseName(driverType string) string {
- names := optionalDriverExecutableBaseNames(driverType)
- if len(names) == 0 {
- return optionalDriverExecutableBaseNameForType("")
- }
- return names[0]
-}
-
-func optionalDriverReleaseAssetName(driverType string) string {
- names := optionalDriverReleaseAssetNames(driverType)
- if len(names) == 0 {
- return optionalDriverReleaseAssetNameForType("", stdRuntime.GOOS, stdRuntime.GOARCH)
- }
- return names[0]
-}
-
-func optionalDriverReleaseAssetNameForVersion(driverType string, selectedVersion string) string {
- names := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
- if len(names) == 0 {
- return optionalDriverReleaseAssetNameForType("", stdRuntime.GOOS, stdRuntime.GOARCH)
- }
- return names[0]
-}
-
-func currentDriverReleaseTag() string {
- currentVersion := normalizeVersion(getCurrentVersion())
- if currentVersion == "" || currentVersion == "0.0.0" {
- return ""
- }
- if isDevelopmentDriverReleaseVersion(currentVersion) {
- return driverReleaseDevTag
- }
- return "v" + currentVersion
-}
-
-func isDevelopmentDriverReleaseVersion(version string) bool {
- normalized := strings.ToLower(strings.TrimSpace(normalizeVersion(version)))
- if normalized == "" || normalized == "0.0.0" {
- return false
- }
- if strings.HasPrefix(normalized, "dev-") {
- return true
- }
- for _, marker := range []string{"-dev", "-test", "-local", "-snapshot"} {
- if strings.Contains(normalized, marker) {
- return true
- }
- }
- return false
-}
-
-func driverReleaseDownloadURL(tag string, assetName string) string {
- tagName := strings.TrimSpace(tag)
- asset := strings.TrimSpace(assetName)
- if tagName == "" || asset == "" {
- return ""
- }
- return fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", driverReleaseRepo, url.PathEscape(tagName), url.PathEscape(asset))
-}
-
-func driverReleaseLatestDownloadURL(assetName string) string {
- asset := strings.TrimSpace(assetName)
- if asset == "" {
- return ""
- }
- return fmt.Sprintf("https://github.com/%s/releases/latest/download/%s", driverReleaseRepo, url.PathEscape(asset))
-}
-
-func findReleaseAssetByName(release *githubRelease, assetNames []string) (githubAsset, bool) {
- if release == nil || len(release.Assets) == 0 || len(assetNames) == 0 {
- return githubAsset{}, false
- }
- for _, expected := range assetNames {
- trimmed := strings.TrimSpace(expected)
- if trimmed == "" {
- continue
- }
- for _, asset := range release.Assets {
- if strings.EqualFold(strings.TrimSpace(asset.Name), trimmed) {
- return asset, true
- }
- }
- }
- return githubAsset{}, false
-}
-
-func driverReleaseAssetAPIURL(asset githubAsset) string {
- urlText := strings.TrimSpace(asset.URL)
- if urlText != "" {
- name := strings.TrimSpace(asset.Name)
- if name == "" {
- return urlText
- }
- parsed, err := url.Parse(urlText)
- if err != nil {
- return urlText
- }
- parsed.Fragment = name
- return parsed.String()
- }
- urlText = strings.TrimSpace(asset.BrowserDownloadURL)
- if urlText == "" {
- return ""
- }
- return urlText
-}
-
-func optionalDriverBundlePlatformDir(goos string) string {
- switch strings.ToLower(strings.TrimSpace(goos)) {
- case "windows":
- return "Windows"
- case "darwin":
- return "MacOS"
- case "linux":
- return "Linux"
- default:
- return "Unknown"
- }
-}
-
-func optionalDriverBundleEntryPathsForVersion(driverType string, selectedVersion string) []string {
- platformDir := optionalDriverBundlePlatformDir(stdRuntime.GOOS)
- assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
- result := make([]string, 0, len(assetNames))
- seen := make(map[string]struct{}, len(assetNames))
- for _, assetName := range assetNames {
- entry := filepath.ToSlash(filepath.Join(platformDir, assetName))
- if _, ok := seen[entry]; ok {
- continue
- }
- seen[entry] = struct{}{}
- result = append(result, entry)
- }
- return result
-}
-
-func optionalDriverBundleEntryPaths(driverType string) []string {
- return optionalDriverBundleEntryPathsForVersion(driverType, "")
-}
-
-func optionalDriverBundleEntryPathForVersion(driverType string, selectedVersion string) string {
- paths := optionalDriverBundleEntryPathsForVersion(driverType, selectedVersion)
- if len(paths) == 0 {
- return filepath.ToSlash(filepath.Join(optionalDriverBundlePlatformDir(stdRuntime.GOOS), optionalDriverReleaseAssetNameForVersion(driverType, selectedVersion)))
- }
- return paths[0]
-}
-
-func optionalDriverBundleEntryPath(driverType string) string {
- return optionalDriverBundleEntryPathForVersion(driverType, "")
-}
-
-func resolveOptionalDriverAssetSize(sizeByAsset map[string]int64, driverType string) int64 {
- if len(sizeByAsset) == 0 {
- return 0
- }
- for _, assetName := range optionalDriverReleaseAssetNames(driverType) {
- sizeBytes := sizeByAsset[assetName]
- if sizeBytes > 0 {
- return sizeBytes
- }
- }
- return 0
-}
-
-func resolveOptionalDriverAssetSizeForVersion(sizeByAsset map[string]int64, driverType string, version string) int64 {
- if len(sizeByAsset) == 0 {
- return 0
- }
- for _, assetName := range optionalDriverReleaseAssetNamesForVersion(driverType, version) {
- sizeBytes := sizeByAsset[assetName]
- if sizeBytes > 0 {
- return sizeBytes
- }
- }
- return 0
-}
-
-func resolveOptionalDriverBundleDownloadURLs() []string {
- candidates := make([]string, 0, 2)
- seen := make(map[string]struct{}, 2)
- appendURL := func(value string) {
- trimmed := strings.TrimSpace(value)
- if trimmed == "" {
- return
- }
- if _, ok := seen[trimmed]; ok {
- return
- }
- seen[trimmed] = struct{}{}
- candidates = append(candidates, trimmed)
- }
-
- if tag := currentDriverReleaseTag(); tag != "" {
- if release, err := fetchReleaseByTag(tag); err == nil {
- if asset, ok := findReleaseAssetByName(release, []string{optionalDriverBundleAssetName}); ok {
- appendURL(driverReleaseAssetAPIURL(asset))
- }
- }
- appendURL(driverReleaseDownloadURL(tag, optionalDriverBundleAssetName))
- }
- if release, err := fetchLatestReleaseForDriverAssets(); err == nil {
- if asset, ok := findReleaseAssetByName(release, []string{optionalDriverBundleAssetName}); ok {
- appendURL(driverReleaseAssetAPIURL(asset))
- }
- }
- appendURL(driverReleaseLatestDownloadURL(optionalDriverBundleAssetName))
- return candidates
-}
-
-func optionalDriverBundleCacheDir() (string, error) {
- cacheDir := filepath.Join(os.TempDir(), "gonavi-driver-bundle-cache")
- if err := os.MkdirAll(cacheDir, 0o755); err != nil {
- return "", err
- }
- return cacheDir, nil
-}
-
-func optionalDriverBundleCachePath(bundleURL string) (string, error) {
- cacheDir, err := optionalDriverBundleCacheDir()
- if err != nil {
- return "", err
- }
- sum := sha256.Sum256([]byte(strings.TrimSpace(bundleURL)))
- return filepath.Join(cacheDir, hex.EncodeToString(sum[:])+".zip"), nil
-}
-
-func cleanupOptionalDriverBundleCache(keepPaths ...string) {
- cacheDir, err := optionalDriverBundleCacheDir()
- if err != nil {
- return
- }
-
- keep := make(map[string]struct{}, len(keepPaths)+4)
- for _, path := range keepPaths {
- if strings.TrimSpace(path) != "" {
- keep[filepath.Clean(path)] = struct{}{}
- }
- }
- optionalDriverBundleDownloadMu.Lock()
- for _, state := range optionalDriverBundleDownloads {
- if state != nil && strings.TrimSpace(state.path) != "" {
- keep[filepath.Clean(state.path)] = struct{}{}
- }
- }
- optionalDriverBundleDownloadMu.Unlock()
-
- type cacheFile struct {
- path string
- modTime time.Time
- }
- cacheFiles := make([]cacheFile, 0)
- now := time.Now()
- entries, err := os.ReadDir(cacheDir)
- if err != nil {
- return
- }
- for _, entry := range entries {
- if entry.IsDir() {
- continue
- }
- path := filepath.Join(cacheDir, entry.Name())
- cleanPath := filepath.Clean(path)
- if _, ok := keep[cleanPath]; ok {
- continue
- }
- info, statErr := entry.Info()
- if statErr != nil {
- continue
- }
- name := strings.ToLower(strings.TrimSpace(entry.Name()))
- if strings.HasSuffix(name, ".tmp") {
- if now.Sub(info.ModTime()) > 24*time.Hour {
- _ = os.Remove(path)
- }
- continue
- }
- if !strings.HasSuffix(name, ".zip") {
- continue
- }
- if now.Sub(info.ModTime()) > optionalDriverBundleCacheMaxAge {
- _ = os.Remove(path)
- continue
- }
- cacheFiles = append(cacheFiles, cacheFile{path: path, modTime: info.ModTime()})
- }
- if len(cacheFiles) <= optionalDriverBundleCacheMaxFiles {
- return
- }
- sort.Slice(cacheFiles, func(i, j int) bool {
- return cacheFiles[i].modTime.After(cacheFiles[j].modTime)
- })
- for _, item := range cacheFiles[optionalDriverBundleCacheMaxFiles:] {
- _ = os.Remove(item.path)
- }
-}
-
-func downloadOptionalDriverBundleToCache(bundleURL string, onProgress func(downloaded, total int64)) (string, error) {
- cachePath, err := optionalDriverBundleCachePath(bundleURL)
- if err != nil {
- return "", err
- }
- tempPath := cachePath + fmt.Sprintf(".%d.tmp", time.Now().UnixNano())
- _ = os.Remove(tempPath)
- if _, err := downloadFileWithHashWithTimeout(bundleURL, tempPath, onProgress, optionalDriverBundleDownloadTimeout); err != nil {
- _ = os.Remove(tempPath)
- return "", err
- }
- if err := os.Remove(cachePath); err != nil && !os.IsNotExist(err) {
- _ = os.Remove(tempPath)
- return "", err
- }
- if err := os.Rename(tempPath, cachePath); err != nil {
- _ = os.Remove(tempPath)
- return "", err
- }
- reader, err := zip.OpenReader(cachePath)
- if err != nil {
- _ = os.Remove(cachePath)
- return "", fmt.Errorf("open driver bundle failed: %w", err)
- }
- if err := reader.Close(); err != nil {
- _ = os.Remove(cachePath)
- return "", fmt.Errorf("close driver bundle failed: %w", err)
- }
- cleanupOptionalDriverBundleCache(cachePath)
- return cachePath, nil
-}
-
-func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloaded, total int64), onWaiting func()) (string, error) {
- trimmedURL := strings.TrimSpace(bundleURL)
- if trimmedURL == "" {
- return "", newLocalizedDriverBackendError("driver_manager.backend.error.bundle_url_empty", nil, nil)
- }
-
- for {
- optionalDriverBundleDownloadMu.Lock()
- state, ok := optionalDriverBundleDownloads[trimmedURL]
- if ok {
- if state.finished {
- path := strings.TrimSpace(state.path)
- err := state.err
- if err == nil && path != "" && fileExists(path) {
- optionalDriverBundleDownloadMu.Unlock()
- return path, nil
- }
- delete(optionalDriverBundleDownloads, trimmedURL)
- optionalDriverBundleDownloadMu.Unlock()
- continue
- }
- done := state.done
- optionalDriverBundleDownloadMu.Unlock()
- if onWaiting != nil {
- onWaiting()
- }
- <-done
- optionalDriverBundleDownloadMu.Lock()
- path := strings.TrimSpace(state.path)
- err := state.err
- if err == nil && path != "" && fileExists(path) {
- optionalDriverBundleDownloadMu.Unlock()
- return path, nil
- }
- if current, exists := optionalDriverBundleDownloads[trimmedURL]; exists && current == state {
- delete(optionalDriverBundleDownloads, trimmedURL)
- }
- optionalDriverBundleDownloadMu.Unlock()
- if err == nil {
- err = fmt.Errorf("driver bundle cache file is unavailable")
- }
- return "", err
- }
-
- state = &optionalDriverBundleDownloadState{done: make(chan struct{})}
- optionalDriverBundleDownloads[trimmedURL] = state
- optionalDriverBundleDownloadMu.Unlock()
-
- path, err := downloadOptionalDriverBundleToCache(trimmedURL, onProgress)
- optionalDriverBundleDownloadMu.Lock()
- state.path = path
- state.err = err
- state.finished = true
- if err != nil {
- delete(optionalDriverBundleDownloads, trimmedURL)
- }
- close(state.done)
- optionalDriverBundleDownloadMu.Unlock()
-
- if err != nil {
- return "", err
- }
- return path, nil
- }
-}
-
-func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL string, selectedVersion string) []string {
- candidates := make([]string, 0, 3)
- seen := make(map[string]struct{}, 3)
- driverType := normalizeDriverType(definition.Type)
- appendURL := func(value string) {
- trimmed := strings.TrimSpace(value)
- if trimmed == "" {
- return
- }
- if _, ok := seen[trimmed]; ok {
- return
- }
- seen[trimmed] = struct{}{}
- candidates = append(candidates, trimmed)
- }
-
- restrictToExplicitArtifact := shouldRestrictToExplicitVersionArtifact(definition, selectedVersion)
- appendPublishedURLs := func() {
- if tag := currentDriverReleaseTag(); tag != "" {
- if publishedURL, ok := resolvePublishedDriverDownloadURLForTag(definition, selectedVersion, tag); ok {
- appendURL(publishedURL)
- }
- }
- if publishedURL, ok := resolveLatestPublishedDriverDownloadURLForVersion(definition, selectedVersion); ok {
- appendURL(publishedURL)
- }
- }
-
- if !restrictToExplicitArtifact && shouldPreferPublishedOptionalDriverDownloads(driverType) {
- appendPublishedURLs()
- }
-
- if parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil {
- switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
- case "http", "https":
- appendURL(parsed.String())
- }
- }
- if restrictToExplicitArtifact {
- return candidates
- }
-
- if !shouldPreferPublishedOptionalDriverDownloads(driverType) {
- appendPublishedURLs()
- }
- return candidates
-}
-
-func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targetPath string) (string, bool) {
- driverType := normalizeDriverType(definition.Type)
- targetAbs, _ := filepath.Abs(targetPath)
- candidates := resolveOptionalDriverAgentCandidatePaths(definition)
- for _, candidate := range candidates {
- candidate = strings.TrimSpace(candidate)
- if candidate == "" {
- continue
- }
- absPath, err := filepath.Abs(candidate)
- if err != nil || absPath == "" {
- continue
- }
- if targetAbs != "" && absPath == targetAbs {
- continue
- }
- info, statErr := os.Stat(absPath)
- if statErr != nil || info.IsDir() {
- continue
- }
- if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, absPath); validateErr != nil {
- continue
- }
- if !isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType, absPath) {
- continue
- }
- return absPath, true
- }
- return "", false
-}
-
-func isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType string, executablePath string) bool {
- expected := strings.TrimSpace(db.OptionalDriverAgentRevision(driverType))
- if expected == "" {
- return true
- }
- actual, current, err := optionalDriverAgentRevisionCurrent(driverType, executablePath)
- displayName := resolveDriverDisplayName(driverDefinition{Type: driverType})
- if err != nil {
- logger.Warnf("可复用 %s 驱动代理候选版本元数据不可用,仍允许安装:path=%s err=%v;建议在驱动管理中重装", displayName, executablePath, err)
- return true
- }
- if !current {
- actualLabel := strings.TrimSpace(actual)
- if actualLabel == "" {
- actualLabel = "空"
- }
- logger.Warnf("可复用 %s 驱动代理候选 revision 不匹配,仍允许安装:path=%s actual=%s expected=%s;建议在驱动管理中重装", displayName, executablePath, actualLabel, expected)
- return true
- }
- return true
-}
-
-func resolveOptionalDriverAgentCandidatePaths(definition driverDefinition) []string {
- driverType := normalizeDriverType(definition.Type)
- names := optionalDriverExecutableBaseNames(driverType)
- assetNames := optionalDriverReleaseAssetNames(driverType)
- pathTypeNames := make([]string, 0, 2)
- seenPathType := make(map[string]struct{}, 2)
- appendPathType := func(typeName string) {
- trimmed := strings.TrimSpace(typeName)
- if trimmed == "" {
- return
- }
- if _, ok := seenPathType[trimmed]; ok {
- return
- }
- seenPathType[trimmed] = struct{}{}
- pathTypeNames = append(pathTypeNames, trimmed)
- }
- appendPathType(optionalDriverPublicTypeName(driverType))
-
- candidates := make([]string, 0, 12)
- appendPath := func(pathText string) {
- trimmed := strings.TrimSpace(pathText)
- if trimmed != "" {
- candidates = append(candidates, trimmed)
- }
- }
-
- if exePath, err := os.Executable(); err == nil && strings.TrimSpace(exePath) != "" {
- resolved := exePath
- if evalPath, evalErr := filepath.EvalSymlinks(exePath); evalErr == nil && strings.TrimSpace(evalPath) != "" {
- resolved = evalPath
- }
- exeDir := filepath.Dir(resolved)
- for _, name := range names {
- appendPath(filepath.Join(exeDir, name))
- }
- for _, assetName := range assetNames {
- appendPath(filepath.Join(exeDir, assetName))
- }
- for _, typeName := range pathTypeNames {
- for _, name := range names {
- appendPath(filepath.Join(exeDir, "drivers", typeName, name))
- }
- for _, assetName := range assetNames {
- appendPath(filepath.Join(exeDir, "drivers", typeName, assetName))
- }
- }
-
- resourcesDir := filepath.Clean(filepath.Join(exeDir, "..", "Resources"))
- for _, typeName := range pathTypeNames {
- for _, name := range names {
- appendPath(filepath.Join(resourcesDir, "drivers", typeName, name))
- }
- for _, assetName := range assetNames {
- appendPath(filepath.Join(resourcesDir, "drivers", typeName, assetName))
- }
- }
- }
- if wd, err := os.Getwd(); err == nil && strings.TrimSpace(wd) != "" {
- for _, assetName := range assetNames {
- appendPath(filepath.Join(wd, "dist", assetName))
- appendPath(filepath.Join(wd, assetName))
- }
- }
-
- unique := make([]string, 0, len(candidates))
- seen := make(map[string]struct{}, len(candidates))
- for _, item := range candidates {
- if _, ok := seen[item]; ok {
- continue
- }
- seen[item] = struct{}{}
- unique = append(unique, item)
- }
- return unique
-}
-
-func resolveDriverDisplayName(definition driverDefinition) string {
- if strings.TrimSpace(definition.Name) != "" {
- return strings.TrimSpace(definition.Name)
- }
- if strings.TrimSpace(definition.Type) != "" {
- return strings.TrimSpace(definition.Type)
- }
- return defaultAppText("driver_manager.backend.driver_fallback_name", nil)
-}
-
-func activateOptionalDriverAgentBinary(driverType string, installPath string, runtimePath string) error {
- source := strings.TrimSpace(installPath)
- target := strings.TrimSpace(runtimePath)
- if source == "" || target == "" {
- return fmt.Errorf("agent path is empty")
- }
- if source == target {
- return nil
- }
-
- absSource := source
- absTarget := target
- if value, err := filepath.Abs(source); err == nil && strings.TrimSpace(value) != "" {
- absSource = value
- }
- if value, err := filepath.Abs(target); err == nil && strings.TrimSpace(value) != "" {
- absTarget = value
- }
- if strings.EqualFold(absSource, absTarget) {
- return nil
- }
- if err := copyAgentBinary(source, target); err != nil {
- return err
- }
- return copyOptionalDriverSupportFilesFromDirectory(driverType, filepath.Dir(source), filepath.Dir(target))
-}
-
-func copyAgentBinary(sourcePath, targetPath string) error {
- src, err := os.Open(sourcePath)
- if err != nil {
- return err
- }
- defer src.Close()
-
- tempPath := targetPath + ".tmp"
- _ = os.Remove(tempPath)
- dst, err := os.Create(tempPath)
- if err != nil {
- return err
- }
- if _, err := io.Copy(dst, src); err != nil {
- dst.Close()
- _ = os.Remove(tempPath)
- return err
- }
- if err := dst.Sync(); err != nil {
- dst.Close()
- _ = os.Remove(tempPath)
- return err
- }
- if err := dst.Close(); err != nil {
- _ = os.Remove(tempPath)
- return err
- }
- if chmodErr := os.Chmod(tempPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
- _ = os.Remove(tempPath)
- return chmodErr
- }
- if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
- _ = os.Remove(tempPath)
- return err
- }
- if chmodErr := os.Chmod(targetPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
- return chmodErr
- }
- return nil
-}
-
-func extractZipFileToPath(file *zip.File, targetPath string) error {
- if file == nil {
- return newLocalizedDriverBackendError("driver_manager.backend.error.zip_entry_empty", nil, nil)
- }
- src, err := file.Open()
- if err != nil {
- return err
- }
- defer src.Close()
- tempPath := targetPath + ".tmp"
- _ = os.Remove(tempPath)
- if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
- return err
- }
- dst, err := os.Create(tempPath)
- if err != nil {
- return err
- }
- if _, err := io.Copy(dst, src); err != nil {
- dst.Close()
- _ = os.Remove(tempPath)
- return err
- }
- if err := dst.Sync(); err != nil {
- dst.Close()
- _ = os.Remove(tempPath)
- return err
- }
- if err := dst.Close(); err != nil {
- _ = os.Remove(tempPath)
- return err
- }
- if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
- _ = os.Remove(tempPath)
- return err
- }
- return nil
-}
-
-func copyOptionalDriverSupportFile(sourcePath, targetPath string) error {
- src, err := os.Open(sourcePath)
- if err != nil {
- return err
- }
- defer src.Close()
-
- tempPath := targetPath + ".tmp"
- _ = os.Remove(tempPath)
- if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
- return err
- }
- dst, err := os.Create(tempPath)
- if err != nil {
- return err
- }
- if _, err := io.Copy(dst, src); err != nil {
- dst.Close()
- _ = os.Remove(tempPath)
- return err
- }
- if err := dst.Sync(); err != nil {
- dst.Close()
- _ = os.Remove(tempPath)
- return err
- }
- if err := dst.Close(); err != nil {
- _ = os.Remove(tempPath)
- return err
- }
- if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
- _ = os.Remove(tempPath)
- return err
- }
- return nil
-}
-
-func renameTempFileOverTarget(tempPath, targetPath string) error {
- if err := os.Rename(tempPath, targetPath); err == nil {
- return nil
- } else {
- firstErr := err
- if removeErr := os.Remove(targetPath); removeErr != nil && !os.IsNotExist(removeErr) {
- return firstErr
- }
- if retryErr := os.Rename(tempPath, targetPath); retryErr != nil {
- return retryErr
- }
- return nil
- }
-}
-
-func copyOptionalDriverSupportFilesFromDirectory(driverType string, sourceDir string, targetDir string) error {
- names := optionalDriverSupportFileNames(driverType)
- if len(names) == 0 {
- return nil
- }
- sourceRoot := strings.TrimSpace(sourceDir)
- targetRoot := strings.TrimSpace(targetDir)
- if sourceRoot == "" || targetRoot == "" {
- return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_directory_empty", nil, nil)
- }
- for _, name := range names {
- sourcePath := filepath.Join(sourceRoot, name)
- targetPath := filepath.Join(targetRoot, name)
- if err := copyOptionalDriverSupportFile(sourcePath, targetPath); err != nil {
- return newLocalizedDriverBackendError("driver_manager.backend.error.copy_runtime_dependency_entry_failed", map[string]any{"name": name}, err)
- }
- }
- return nil
-}
-
-func findOptionalDriverSupportFileInZip(files []*zip.File, agentEntryName string, supportName string) *zip.File {
- normalizedAgent := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(agentEntryName), "./"))
- agentDir := filepath.ToSlash(filepath.Dir(normalizedAgent))
- if agentDir == "." {
- agentDir = ""
- }
- candidatePaths := []string{}
- if agentDir != "" {
- candidatePaths = append(candidatePaths, filepath.ToSlash(filepath.Join(agentDir, supportName)))
- }
- candidatePaths = append(candidatePaths, supportName)
-
- for _, candidate := range candidatePaths {
- for _, file := range files {
- name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
- if name == candidate {
- return file
- }
- }
- for _, file := range files {
- name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
- if strings.EqualFold(name, candidate) {
- return file
- }
- }
- }
- for _, file := range files {
- name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
- if strings.EqualFold(filepath.Base(name), supportName) {
- return file
- }
- }
- return nil
-}
-
-func extractOptionalDriverSupportFilesFromZip(files []*zip.File, driverType string, agentEntryName string, targetDir string) error {
- names := optionalDriverSupportFileNames(driverType)
- if len(names) == 0 {
- return nil
- }
- targetRoot := strings.TrimSpace(targetDir)
- if targetRoot == "" {
- return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_target_directory_empty", nil, nil)
- }
- for _, name := range names {
- entry := findOptionalDriverSupportFileInZip(files, agentEntryName, name)
- if entry == nil {
- return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_entry_missing", map[string]any{"name": name}, nil)
- }
- if err := extractZipFileToPath(entry, filepath.Join(targetRoot, name)); err != nil {
- return newLocalizedDriverBackendError("driver_manager.backend.error.extract_runtime_dependency_failed", map[string]any{"name": name}, err)
- }
- }
- return nil
-}
-
-func scaleProgress(downloaded, total, start, end int64) (int64, int64) {
- if end <= start {
- return end, 100
- }
- if total <= 0 {
- return start, 100
- }
- if downloaded < 0 {
- downloaded = 0
- }
- if downloaded > total {
- downloaded = total
- }
- span := end - start
- return start + ((downloaded * span) / total), 100
-}
-
-func preloadOptionalDriverPackageSizes(definitions []driverDefinition) map[string]int64 {
- result := make(map[string]int64)
- if len(definitions) == 0 {
- return result
- }
-
- needed := make([]string, 0, len(definitions))
- for _, definition := range definitions {
- normalizedType := normalizeDriverType(definition.Type)
- if normalizedType == "" || definition.BuiltIn {
- continue
- }
- if !db.IsOptionalGoDriver(normalizedType) {
- continue
- }
- if !db.IsOptionalGoDriverBuildIncluded(normalizedType) {
- continue
- }
- needed = append(needed, normalizedType)
- }
- if len(needed) == 0 {
- return result
- }
-
- tag := currentDriverReleaseTag()
-
- fillFromSizes := func(sizeByAsset map[string]int64, driverTypes []string) []string {
- missing := make([]string, 0, len(driverTypes))
- for _, driverType := range driverTypes {
- sizeBytes := resolveOptionalDriverAssetSize(sizeByAsset, driverType)
- if sizeBytes > 0 {
- result[driverType] = sizeBytes
- continue
- }
- missing = append(missing, driverType)
- }
- return missing
- }
-
- pending := needed
- if tag != "" {
- if sizeByAsset, _, err := loadReleaseAssetSizesCached("tag:"+tag, func() (*githubRelease, error) {
- return fetchReleaseByTag(tag)
- }); err == nil {
- pending = fillFromSizes(sizeByAsset, pending)
- }
- }
- if len(pending) == 0 {
- return result
- }
- if sizeByAsset, _, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets); err == nil {
- _ = fillFromSizes(sizeByAsset, pending)
- }
- return result
-}
-
-func loadReleaseAssetSizesCached(cacheKey string, fetch func() (*githubRelease, error)) (map[string]int64, map[string]bool, error) {
- key := strings.TrimSpace(cacheKey)
- if key == "" {
- return nil, nil, newLocalizedDriverBackendError("driver_manager.backend.error.cache_key_empty", nil, nil)
- }
-
- driverReleaseSizeMu.RLock()
- cached, ok := driverReleaseSizeMap[key]
- driverReleaseSizeMu.RUnlock()
- if ok {
- ttl := driverReleaseAssetSizeCacheTTL
- if strings.TrimSpace(cached.Err) != "" {
- ttl = driverReleaseAssetSizeErrorCacheTTL
- }
- if time.Since(cached.LoadedAt) < ttl {
- if strings.TrimSpace(cached.Err) != "" {
- return nil, nil, errors.New(strings.TrimSpace(cached.Err))
- }
- return cached.SizeByKey, cached.PublishedAssets, nil
- }
- }
-
- release, err := fetch()
- entry := driverReleaseAssetSizeCacheEntry{
- LoadedAt: time.Now(),
- SizeByKey: map[string]int64{},
- PublishedAssets: map[string]bool{},
- }
- if err != nil {
- entry.Err = err.Error()
- } else {
- entry.SizeByKey = buildReleaseAssetSizeMap(release)
- entry.PublishedAssets = buildReleaseAssetNameMap(release)
- if indexSizes, indexErr := fetchDriverBundleAssetSizeIndex(release); indexErr == nil {
- for name, size := range indexSizes {
- trimmedName := strings.TrimSpace(name)
- if trimmedName == "" || size <= 0 {
- continue
- }
- entry.SizeByKey[trimmedName] = size
- }
- }
- }
-
- driverReleaseSizeMu.Lock()
- driverReleaseSizeMap[key] = entry
- driverReleaseSizeMu.Unlock()
-
- if err != nil {
- return nil, nil, err
- }
- return entry.SizeByKey, entry.PublishedAssets, nil
-}
-
-func readReleaseAssetSizesFromCache(cacheKey string) (map[string]int64, map[string]bool, bool) {
- key := strings.TrimSpace(cacheKey)
- if key == "" {
- return nil, nil, false
- }
-
- driverReleaseSizeMu.RLock()
- cached, ok := driverReleaseSizeMap[key]
- driverReleaseSizeMu.RUnlock()
- if !ok {
- return nil, nil, false
- }
-
- ttl := driverReleaseAssetSizeCacheTTL
- if strings.TrimSpace(cached.Err) != "" {
- ttl = driverReleaseAssetSizeErrorCacheTTL
- }
- if time.Since(cached.LoadedAt) >= ttl {
- return nil, nil, false
- }
- if strings.TrimSpace(cached.Err) != "" {
- return nil, nil, false
- }
- return cached.SizeByKey, cached.PublishedAssets, true
-}
-
-func buildReleaseAssetSizeMap(release *githubRelease) map[string]int64 {
- sizes := make(map[string]int64)
- if release == nil {
- return sizes
- }
- for _, asset := range release.Assets {
- name := strings.TrimSpace(asset.Name)
- if name == "" || asset.Size <= 0 {
- continue
- }
- sizes[name] = asset.Size
- }
- return sizes
-}
-
-func buildReleaseAssetNameMap(release *githubRelease) map[string]bool {
- names := make(map[string]bool)
- if release == nil {
- return names
- }
- for _, asset := range release.Assets {
- name := strings.TrimSpace(asset.Name)
- if name == "" {
- continue
- }
- names[name] = true
- }
- return names
-}
-
-func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, error) {
- if release == nil {
- return nil, newLocalizedDriverBackendError("driver_manager.backend.error.release_empty", nil, nil)
- }
- indexURL := ""
- for _, asset := range release.Assets {
- if strings.EqualFold(strings.TrimSpace(asset.Name), optionalDriverBundleIndexAssetName) {
- indexURL = strings.TrimSpace(asset.BrowserDownloadURL)
- break
- }
- }
- if indexURL == "" {
- return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_asset_missing", nil, nil)
- }
-
- client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout)
- req, err := http.NewRequest(http.MethodGet, indexURL, nil)
- if err != nil {
- return nil, err
- }
- req.Header.Set("User-Agent", "GoNavi-DriverManager")
- req.Header.Set("Accept", "application/json")
-
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, newLocalizedDriverBackendError(
- "driver_manager.backend.error.bundle_index_fetch_failed",
- nil,
- fmt.Errorf("HTTP %d", resp.StatusCode),
- )
- }
-
- limited := io.LimitReader(resp.Body, driverBundleIndexMaxSize)
- decoder := json.NewDecoder(limited)
- var index driverBundleAssetIndex
- if err := decoder.Decode(&index); err != nil {
- return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_parse_failed", nil, err)
- }
- if len(index.Assets) == 0 {
- return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_empty", nil, nil)
- }
- return index.Assets, nil
-}
-
-func fetchLatestReleaseForDriverAssets() (*githubRelease, error) {
- return fetchDriverReleaseByURL(driverReleaseLatestAPIURL)
-}
-
-func resolveLatestPublishedDriverDownloadURL(definition driverDefinition) (string, bool) {
- return resolveLatestPublishedDriverDownloadURLForVersion(definition, "")
-}
-
-func resolveLatestPublishedDriverDownloadURLForVersion(definition driverDefinition, selectedVersion string) (string, bool) {
- driverType := normalizeDriverType(definition.Type)
- if driverType == "" {
- return "", false
- }
- if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
- if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok {
- if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
- if release, err := fetchLatestReleaseForDriverAssets(); err == nil {
- if asset, found := findReleaseAssetByName(release, []string{duckDBWindowsDriverZipAssetName}); found {
- return driverReleaseAssetAPIURL(asset), true
- }
- }
- return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true
- }
- return "", false
- }
-
- sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets)
- if err != nil {
- return "", false
- }
- if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
- if release, relErr := fetchLatestReleaseForDriverAssets(); relErr == nil {
- if asset, found := findReleaseAssetByName(release, []string{duckDBWindowsDriverZipAssetName}); found {
- return driverReleaseAssetAPIURL(asset), true
- }
- }
- return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true
- }
- return "", false
- }
-
- assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
- if len(assetNames) == 0 {
- return "", false
- }
-
- if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok {
- for _, assetName := range assetNames {
- if publishedAssets[assetName] && sizeByAsset[assetName] > 0 {
- if release, err := fetchLatestReleaseForDriverAssets(); err == nil {
- if asset, found := findReleaseAssetByName(release, []string{assetName}); found {
- return driverReleaseAssetAPIURL(asset), true
- }
- }
- return driverReleaseLatestDownloadURL(assetName), true
- }
- }
- return "", false
- }
-
- sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets)
- if err != nil {
- return "", false
- }
- for _, assetName := range assetNames {
- if publishedAssets[assetName] && sizeByAsset[assetName] > 0 {
- if release, relErr := fetchLatestReleaseForDriverAssets(); relErr == nil {
- if asset, found := findReleaseAssetByName(release, []string{assetName}); found {
- return driverReleaseAssetAPIURL(asset), true
- }
- }
- return driverReleaseLatestDownloadURL(assetName), true
- }
- }
- return "", false
-}
-
-func fetchReleaseByTag(tag string) (*githubRelease, error) {
- tagName := strings.TrimSpace(tag)
- if tagName == "" {
- return nil, newLocalizedDriverBackendError("driver_manager.backend.error.tag_empty", nil, nil)
- }
- apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", driverReleaseRepo, url.PathEscape(tagName))
- return fetchDriverReleaseByURL(apiURL)
-}
-
-func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) {
- urlText := strings.TrimSpace(apiURL)
- if urlText == "" {
- return nil, newLocalizedDriverBackendError("driver_manager.backend.error.api_url_empty", nil, nil)
- }
-
- client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout)
- req, err := http.NewRequest(http.MethodGet, urlText, nil)
- if err != nil {
- return nil, err
- }
- req.Header.Set("User-Agent", "GoNavi-DriverManager")
- req.Header.Set("Accept", "application/vnd.github+json")
-
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, newLocalizedDriverBackendError(
- "driver_manager.backend.error.release_info_fetch_failed",
- nil,
- fmt.Errorf("HTTP %d", resp.StatusCode),
- )
- }
-
- var release githubRelease
- if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
- return nil, err
- }
- return &release, nil
-}
-
-func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriverPackage, packageMetaExists bool, packageSizeBytesMap map[string]int64, text func(string, map[string]any) string) string {
- if definition.BuiltIn {
- return driverManagerLocalizedText(text, "driver_manager.package_size.built_in", nil, "Built-in")
- }
-
- normalizedType := normalizeDriverType(definition.Type)
- if packageMetaExists {
- sizeBytes := readInstalledPackageSizeBytes(pkg)
- if sizeBytes > 0 {
- return formatSizeMB(sizeBytes)
- }
- }
- if sizeBytes, ok := packageSizeBytesMap[normalizedType]; ok && sizeBytes > 0 {
- return formatSizeMB(sizeBytes)
- }
-
- if !db.IsOptionalGoDriverBuildIncluded(normalizedType) {
- return driverManagerLocalizedText(text, "driver_manager.package_size.pending_release", nil, "Pending release")
- }
- return "-"
-}
-
-func driverManagerLocalizedText(text func(string, map[string]any) string, key string, params map[string]any, fallback string) string {
- if text == nil {
- return fallback
- }
- localized := text(key, params)
- if localized == "" {
- return fallback
- }
- return localized
-}
-
-func readInstalledPackageSizeBytes(pkg installedDriverPackage) int64 {
- pathText := strings.TrimSpace(pkg.ExecutablePath)
- if pathText == "" {
- pathText = strings.TrimSpace(pkg.FilePath)
- }
- if pathText == "" {
- return 0
- }
- info, err := os.Stat(pathText)
- if err != nil || info.IsDir() {
- return 0
- }
- return info.Size()
-}
-
-func formatSizeMB(sizeBytes int64) string {
- if sizeBytes <= 0 {
- return "-"
- }
- sizeMB := float64(sizeBytes) / (1024 * 1024)
- return fmt.Sprintf("%.2f MB", sizeMB)
-}
diff --git a/internal/app/methods_driver_assets.go b/internal/app/methods_driver_assets.go
index 37a9ff10..9627e46b 100644
--- a/internal/app/methods_driver_assets.go
+++ b/internal/app/methods_driver_assets.go
@@ -466,11 +466,11 @@ func downloadOptionalDriverBundleToCache(bundleURL string, onProgress func(downl
reader, err := zip.OpenReader(cachePath)
if err != nil {
_ = os.Remove(cachePath)
- return "", fmt.Errorf("打开驱动总包失败:%w", err)
+ return "", fmt.Errorf("open driver bundle failed: %w", err)
}
if err := reader.Close(); err != nil {
_ = os.Remove(cachePath)
- return "", fmt.Errorf("关闭驱动总包失败:%w", err)
+ return "", fmt.Errorf("close driver bundle failed: %w", err)
}
cleanupOptionalDriverBundleCache(cachePath)
return cachePath, nil
@@ -479,7 +479,7 @@ func downloadOptionalDriverBundleToCache(bundleURL string, onProgress func(downl
func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloaded, total int64), onWaiting func()) (string, error) {
trimmedURL := strings.TrimSpace(bundleURL)
if trimmedURL == "" {
- return "", fmt.Errorf("驱动总包下载地址为空")
+ return "", newLocalizedDriverBackendError("driver_manager.backend.error.bundle_url_empty", nil, nil)
}
for {
@@ -515,7 +515,7 @@ func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloade
}
optionalDriverBundleDownloadMu.Unlock()
if err == nil {
- err = fmt.Errorf("驱动总包缓存文件不可用")
+ err = fmt.Errorf("driver bundle cache file is unavailable")
}
return "", err
}
@@ -727,7 +727,7 @@ func resolveDriverDisplayName(definition driverDefinition) string {
if strings.TrimSpace(definition.Type) != "" {
return strings.TrimSpace(definition.Type)
}
- return "未知"
+ return defaultAppText("driver_manager.backend.driver_fallback_name", nil)
}
func activateOptionalDriverAgentBinary(driverType string, installPath string, runtimePath string) error {
@@ -800,7 +800,7 @@ func copyAgentBinary(sourcePath, targetPath string) error {
func extractZipFileToPath(file *zip.File, targetPath string) error {
if file == nil {
- return fmt.Errorf("zip 条目为空")
+ return newLocalizedDriverBackendError("driver_manager.backend.error.zip_entry_empty", nil, nil)
}
src, err := file.Open()
if err != nil {
@@ -897,13 +897,13 @@ func copyOptionalDriverSupportFilesFromDirectory(driverType string, sourceDir st
sourceRoot := strings.TrimSpace(sourceDir)
targetRoot := strings.TrimSpace(targetDir)
if sourceRoot == "" || targetRoot == "" {
- return fmt.Errorf("运行时依赖目录为空")
+ return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_directory_empty", nil, nil)
}
for _, name := range names {
sourcePath := filepath.Join(sourceRoot, name)
targetPath := filepath.Join(targetRoot, name)
if err := copyOptionalDriverSupportFile(sourcePath, targetPath); err != nil {
- return fmt.Errorf("复制 %s 失败:%w", name, err)
+ return newLocalizedDriverBackendError("driver_manager.backend.error.copy_runtime_dependency_entry_failed", map[string]any{"name": name}, err)
}
}
return nil
@@ -951,15 +951,15 @@ func extractOptionalDriverSupportFilesFromZip(files []*zip.File, driverType stri
}
targetRoot := strings.TrimSpace(targetDir)
if targetRoot == "" {
- return fmt.Errorf("运行时依赖目标目录为空")
+ return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_target_directory_empty", nil, nil)
}
for _, name := range names {
entry := findOptionalDriverSupportFileInZip(files, agentEntryName, name)
if entry == nil {
- return fmt.Errorf("驱动包缺少运行时依赖:%s", name)
+ return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_entry_missing", map[string]any{"name": name}, nil)
}
if err := extractZipFileToPath(entry, filepath.Join(targetRoot, name)); err != nil {
- return fmt.Errorf("解压运行时依赖 %s 失败:%w", name, err)
+ return newLocalizedDriverBackendError("driver_manager.backend.error.extract_runtime_dependency_failed", map[string]any{"name": name}, err)
}
}
return nil
@@ -1041,7 +1041,7 @@ func preloadOptionalDriverPackageSizes(definitions []driverDefinition) map[strin
func loadReleaseAssetSizesCached(cacheKey string, fetch func() (*githubRelease, error)) (map[string]int64, map[string]bool, error) {
key := strings.TrimSpace(cacheKey)
if key == "" {
- return nil, nil, fmt.Errorf("缓存 key 为空")
+ return nil, nil, newLocalizedDriverBackendError("driver_manager.backend.error.cache_key_empty", nil, nil)
}
driverReleaseSizeMu.RLock()
@@ -1150,7 +1150,7 @@ func buildReleaseAssetNameMap(release *githubRelease) map[string]bool {
func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, error) {
if release == nil {
- return nil, fmt.Errorf("release 为空")
+ return nil, newLocalizedDriverBackendError("driver_manager.backend.error.release_empty", nil, nil)
}
indexURL := ""
for _, asset := range release.Assets {
@@ -1160,7 +1160,7 @@ func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64,
}
}
if indexURL == "" {
- return nil, fmt.Errorf("未找到驱动总包索引资产")
+ return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_asset_missing", nil, nil)
}
client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout)
@@ -1177,17 +1177,21 @@ func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64,
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("拉取驱动总包索引失败:HTTP %d", resp.StatusCode)
+ return nil, newLocalizedDriverBackendError(
+ "driver_manager.backend.error.bundle_index_fetch_failed",
+ nil,
+ fmt.Errorf("HTTP %d", resp.StatusCode),
+ )
}
limited := io.LimitReader(resp.Body, driverBundleIndexMaxSize)
decoder := json.NewDecoder(limited)
var index driverBundleAssetIndex
if err := decoder.Decode(&index); err != nil {
- return nil, fmt.Errorf("解析驱动总包索引失败:%w", err)
+ return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_parse_failed", nil, err)
}
if len(index.Assets) == 0 {
- return nil, fmt.Errorf("驱动总包索引为空")
+ return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_empty", nil, nil)
}
return index.Assets, nil
}
@@ -1272,7 +1276,7 @@ func resolveLatestPublishedDriverDownloadURLForVersion(definition driverDefiniti
func fetchReleaseByTag(tag string) (*githubRelease, error) {
tagName := strings.TrimSpace(tag)
if tagName == "" {
- return nil, fmt.Errorf("Tag 为空")
+ return nil, newLocalizedDriverBackendError("driver_manager.backend.error.tag_empty", nil, nil)
}
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", driverReleaseRepo, url.PathEscape(tagName))
return fetchDriverReleaseByURL(apiURL)
@@ -1281,7 +1285,7 @@ func fetchReleaseByTag(tag string) (*githubRelease, error) {
func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) {
urlText := strings.TrimSpace(apiURL)
if urlText == "" {
- return nil, fmt.Errorf("API 地址为空")
+ return nil, newLocalizedDriverBackendError("driver_manager.backend.error.api_url_empty", nil, nil)
}
client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout)
@@ -1298,7 +1302,11 @@ func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) {
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("拉取 Release 信息失败:HTTP %d", resp.StatusCode)
+ return nil, newLocalizedDriverBackendError(
+ "driver_manager.backend.error.release_info_fetch_failed",
+ nil,
+ fmt.Errorf("HTTP %d", resp.StatusCode),
+ )
}
var release githubRelease
@@ -1308,9 +1316,9 @@ func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) {
return &release, nil
}
-func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriverPackage, packageMetaExists bool, packageSizeBytesMap map[string]int64) string {
+func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriverPackage, packageMetaExists bool, packageSizeBytesMap map[string]int64, text func(string, map[string]any) string) string {
if definition.BuiltIn {
- return "内置"
+ return driverManagerLocalizedText(text, "driver_manager.package_size.built_in", nil, "Built-in")
}
normalizedType := normalizeDriverType(definition.Type)
@@ -1325,11 +1333,22 @@ func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriv
}
if !db.IsOptionalGoDriverBuildIncluded(normalizedType) {
- return "待发布"
+ return driverManagerLocalizedText(text, "driver_manager.package_size.pending_release", nil, "Pending release")
}
return "-"
}
+func driverManagerLocalizedText(text func(string, map[string]any) string, key string, params map[string]any, fallback string) string {
+ if text == nil {
+ return fallback
+ }
+ localized := text(key, params)
+ if localized == "" {
+ return fallback
+ }
+ return localized
+}
+
func readInstalledPackageSizeBytes(pkg installedDriverPackage) int64 {
pathText := strings.TrimSpace(pkg.ExecutablePath)
if pathText == "" {
diff --git a/internal/app/methods_driver_i18n_test.go b/internal/app/methods_driver_i18n_test.go
index ea9a3fe6..8d868d29 100644
--- a/internal/app/methods_driver_i18n_test.go
+++ b/internal/app/methods_driver_i18n_test.go
@@ -20,7 +20,7 @@ func methodsDriverFunctionSource(t *testing.T, source string, signature string)
t.Helper()
start := strings.Index(source, signature)
if start < 0 {
- t.Fatalf("methods_driver.go missing function signature %q", signature)
+ t.Fatalf("methods driver source missing function signature %q", signature)
}
rest := source[start+len(signature):]
end := strings.Index(rest, "\nfunc ")
@@ -39,11 +39,7 @@ func (timeoutDriverNetworkError) Temporary() bool { return true }
var _ net.Error = timeoutDriverNetworkError{}
func TestMethodsDriverNetworkBackendMessagesUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
checks := map[string]struct {
rawMessages []string
@@ -116,11 +112,7 @@ func TestMethodsDriverNetworkBackendCatalogKeysExist(t *testing.T) {
}
func TestMethodsDriverReleaseHelpersUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
checks := map[string]struct {
rawMessages []string
@@ -188,11 +180,7 @@ func TestMethodsDriverReleaseHelpersUseLocalizedText(t *testing.T) {
}
func TestMethodsDriverInstallActionDetailsUseEnglishInternalWrappers(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
checks := map[string]struct {
rawMessages []string
@@ -388,6 +376,7 @@ func TestLoadReleaseAssetSizesCachedUsesCurrentLanguageForEmptyCacheKey(t *testi
func TestFetchDriverBundleAssetSizeIndexUsesCurrentLanguageForStructuredErrors(t *testing.T) {
app := NewApp()
app.SetLanguage(string(i18n.LanguageEnUS))
+ disableGlobalProxyForTest(t)
makeRelease := func(downloadURL string) *githubRelease {
return &githubRelease{
@@ -486,6 +475,7 @@ func TestFetchReleaseByTagUsesCurrentLanguageForEmptyTag(t *testing.T) {
func TestFetchDriverReleaseByURLUsesCurrentLanguageForStructuredErrors(t *testing.T) {
app := NewApp()
app.SetLanguage(string(i18n.LanguageEnUS))
+ disableGlobalProxyForTest(t)
_, err := fetchDriverReleaseByURL(" ")
if err == nil {
@@ -517,11 +507,7 @@ func TestFetchDriverReleaseByURLUsesCurrentLanguageForStructuredErrors(t *testin
}
func TestMethodsDriverVersionOptionErrorsUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
functionSource := methodsDriverFunctionSource(t, source, "func resolveDriverVersionOptions")
for _, rawMessage := range []string{
@@ -628,11 +614,7 @@ func TestResolveDriverVersionOptionsUsesCurrentLanguageForStructuredErrors(t *te
}
func TestMethodsDriverModuleVersionFetchErrorsUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
functionSource := methodsDriverFunctionSource(t, source, "func fetchGoModuleVersionMetas(modulePath string)")
for _, rawMessage := range []string{
@@ -699,11 +681,7 @@ func TestFetchGoModuleVersionMetasUsesCurrentLanguageForEmptyModulePath(t *testi
}
func TestMethodsDriverBundleAcquireErrorsUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
functionSource := methodsDriverFunctionSource(t, source, "func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloaded, total int64), onWaiting func()) (string, error)")
if strings.Contains(functionSource, `fmt.Errorf("驱动总包下载地址为空")`) {
@@ -734,11 +712,7 @@ func TestAcquireOptionalDriverBundlePathUsesCurrentLanguageForEmptyURL(t *testin
}
func TestMethodsDriverManifestErrorsUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
checks := map[string]struct {
rawMessages []string
@@ -852,6 +826,7 @@ func TestResolveDriverRepositoryURLUsesCurrentLanguageForStructuredErrors(t *tes
func TestGetDriverStatusListUsesCurrentLanguageForManifestErrors(t *testing.T) {
app := NewApp()
app.SetLanguage(string(i18n.LanguageEnUS))
+ disableGlobalProxyForTest(t)
t.Cleanup(func() {
app.SetLanguage(string(i18n.LanguageZhCN))
})
@@ -948,11 +923,7 @@ func TestGetDriverStatusListUsesCurrentLanguageForManifestErrors(t *testing.T) {
}
func TestMethodsDriverUnsupportedVersionErrorsUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
functionSource := methodsDriverFunctionSource(t, source, "func (a *App) localizeDriverSelectionError")
for _, key := range []string{
@@ -966,11 +937,7 @@ func TestMethodsDriverUnsupportedVersionErrorsUseLocalizedText(t *testing.T) {
}
func TestMethodsDriverUnsupportedVersionErrorTypesUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
checks := map[string]struct {
rawMessages []string
@@ -1136,11 +1103,7 @@ func TestLocalizeDriverSelectionErrorUsesCurrentLanguageForUnsupportedVersions(t
}
func TestMethodsDriverRuntimeReasonCompatibilityUsesLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
checks := map[string]struct {
rawMessages []string
@@ -1335,11 +1298,7 @@ func TestResolveDriverDisplayNameUsesCurrentLanguageFallbackName(t *testing.T) {
}
func TestMethodsDriverUpdateStatusUsesLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
checks := map[string]struct {
rawMessages []string
@@ -1460,11 +1419,7 @@ func TestOptionalDriverPackageUpdateStatusUsesCurrentLanguageForMongoCompatibili
}
func TestMethodsDriverSourceBuildHelperErrorsUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
checks := map[string]struct {
rawMessages []string
diff --git a/internal/app/methods_driver_local_import_test.go b/internal/app/methods_driver_local_import_test.go
index 53e938b1..dea64b65 100644
--- a/internal/app/methods_driver_local_import_test.go
+++ b/internal/app/methods_driver_local_import_test.go
@@ -75,11 +75,7 @@ func TestInstallLocalDriverPackageUsesCurrentLanguageForEmptyLocalPath(t *testin
}
func TestLocalDriverImportErrorsUseI18nWrappers(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
functionNames := []string{
"(*App) InstallLocalDriverPackage",
@@ -243,11 +239,7 @@ func TestDriverOperationFailureDetailUsesCurrentLanguageForRuntimeDependencyWrap
}
func TestInstallLocalDriverPackageProgressUsesLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
start := strings.Index(source, "func (a *App) InstallLocalDriverPackage")
if start < 0 {
t.Fatal("methods_driver.go missing InstallLocalDriverPackage")
diff --git a/internal/app/methods_driver_test_helpers_test.go b/internal/app/methods_driver_test_helpers_test.go
new file mode 100644
index 00000000..bbfdb9e6
--- /dev/null
+++ b/internal/app/methods_driver_test_helpers_test.go
@@ -0,0 +1,37 @@
+package app
+
+import (
+ "os"
+ "strings"
+ "testing"
+)
+
+func methodsDriverSource(t *testing.T) string {
+ t.Helper()
+
+ paths := []string{
+ "methods_driver.go",
+ "methods_driver_assets.go",
+ }
+ parts := make([]string, 0, len(paths))
+ for _, path := range paths {
+ content, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("read %s: %v", path, err)
+ }
+ parts = append(parts, string(content))
+ }
+ return strings.Join(parts, "\n\n")
+}
+
+func disableGlobalProxyForTest(t *testing.T) {
+ t.Helper()
+
+ proxySnapshot := currentGlobalProxyConfig()
+ if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil {
+ t.Fatalf("disable global proxy failed: %v", err)
+ }
+ t.Cleanup(func() {
+ _, _ = setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy)
+ })
+}
diff --git a/internal/app/methods_driver_version_test.go b/internal/app/methods_driver_version_test.go
index 15510c37..1280a0e4 100644
--- a/internal/app/methods_driver_version_test.go
+++ b/internal/app/methods_driver_version_test.go
@@ -180,11 +180,7 @@ func TestOptionalDriverDownloadZipURLAcceptsAssetAPIFragment(t *testing.T) {
}
func TestOptionalDriverAgentDownloadAndBuildErrorsUseI18nWrappers(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
functionNames := []string{
"ensureOptionalDriverAgentBinary",
@@ -257,11 +253,7 @@ func TestOptionalDriverAgentDownloadAndBuildErrorsUseI18nWrappers(t *testing.T)
}
func TestInstallOptionalDriverAgentPackageUsesLocalizedNamedHashFailure(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
start := strings.Index(source, "func installOptionalDriverAgentPackage")
if start < 0 {
@@ -627,11 +619,7 @@ func TestVerifyInstalledOptionalDriverAgentRevisionLocalizesRevisionMismatch(t *
}
func TestVerifyInstalledOptionalDriverAgentRevisionUsesI18nWrappers(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
start := strings.Index(source, "func verifyInstalledOptionalDriverAgentRevision")
if start < 0 {
t.Fatal("methods_driver.go missing verifyInstalledOptionalDriverAgentRevision")
@@ -999,11 +987,7 @@ func zhCNDriverProgressText(t *testing.T) func(string, map[string]any) string {
}
func TestOptionalDriverAgentProgressMessagesUseLocalizedText(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
rawMessages := []string{
`"准备安装 %s 驱动代理`,
@@ -1317,11 +1301,7 @@ func TestResolveDriverPackageSizeTextUsesLocalizedStatusText(t *testing.T) {
}
func TestDriverVersionDisplayLabelsAndPackageSizeStatusesUseI18nKeys(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
if !strings.Contains(source, "resolveDriverVersionOptions(definition, repositoryURL, a.appText)") {
t.Fatal("expected GetDriverVersionList to pass app localizer into resolveDriverVersionOptions")
@@ -2068,11 +2048,7 @@ func resetOptionalDriverBundleDownloadCacheForTest(t *testing.T) {
}
func TestOptionalDriverBundleCacheHelpersDoNotContainLegacyChineseWrappers(t *testing.T) {
- sourceBytes, err := os.ReadFile("methods_driver.go")
- if err != nil {
- t.Fatalf("read methods_driver.go: %v", err)
- }
- source := string(sourceBytes)
+ source := methodsDriverSource(t)
functionNames := []string{
"downloadOptionalDriverBundleToCache",