diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml
deleted file mode 100644
index bba6b4bd..00000000
--- a/.github/workflows/release-winget.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: Publish to WinGet
-on:
- push:
- tags:
- - 'v*'
- workflow_dispatch:
- inputs:
- release_tag:
- required: true
- description: 'Tag of release you want to publish'
- type: string
-
-jobs:
- publish:
- runs-on: windows-2025-vs2026
- steps:
- - uses: vedantmgoyal9/winget-releaser@v2
- with:
- identifier: Syngnat.GoNavi
- installers-regex: 'GoNavi-windows-(amd64|arm64)\.exe$'
- release-tag: ${{ inputs.release_tag || github.ref_name }}
- token: ${{ secrets.WINGET_TOKEN }}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 198caf32..eb6377e6 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4114,7 +4114,7 @@ function App() {
) : null,
isLatestUpdateDownloaded ? (
} onClick={handleInstallFromProgress}>
- {isMacRuntime ? t('app.about.action.open_install_directory') : t('app.about.action.install_update')}
+ {t('app.about.action.install_update')}
) : null,
].filter(Boolean)}
@@ -5111,7 +5111,7 @@ function App() {
] : (updateDownloadProgress.status === 'done' ? [
,
] : (updateDownloadProgress.status === 'error' ? [
diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx
index a12ef1d3..e7fb12e4 100644
--- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx
+++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx
@@ -5,10 +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 { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
import { normalizeQueryResultMessages } from './queryEditor/QueryEditorHelpers';
import QueryEditor, {
@@ -360,10 +359,9 @@ vi.mock('./DataGrid', () => ({
}));
vi.mock('./LogPanel', () => ({
- default: ({ variant, executionError }: { variant?: string; executionError?: string }) => (
-
- SQL 执行日志
- {executionError ? ` ${executionError}` : ''}
+ default: ({ executionError }: any) => (
+
+ {executionError || 'log-panel'}
),
}));
@@ -474,28 +472,12 @@ vi.mock('antd', () => {
});
const textContent = (node: any): string => {
- if (node === null || node === undefined) return '';
- if (typeof node === 'string' || typeof node === 'number') return String(node);
+ if (node == null) return '';
+ if (typeof node === 'string') return node;
if (Array.isArray(node)) return node.map((item) => textContent(item)).join('');
- return textContent(node.children || []);
-};
-
-const queryResultMessageText = (renderer: ReactTestRenderer): string => {
- const values: string[] = [];
- const walk = (node: any) => {
- if (!node) return;
- if (Array.isArray(node)) {
- node.forEach(walk);
- return;
- }
- if (typeof node !== 'object') return;
- if (typeof node.props?.['data-query-result-message-textarea'] === 'string') {
- values.push(String(node.props.value || ''));
- }
- walk(node.children || []);
- };
- walk(renderer.toJSON());
- return values.join('\n');
+ return (node.children || [])
+ .map((item: any) => (typeof item === 'string' ? item : textContent(item)))
+ .join('');
};
const findButton = (renderer: ReactTestRenderer, text: string) =>
@@ -507,6 +489,11 @@ const findButtons = (renderer: ReactTestRenderer, text: string) =>
const findExactButton = (renderer: ReactTestRenderer, text: string) =>
renderer.root.findAll((node) => node.type === 'button' && textContent(node) === text)[0];
+const findResultMessageTextarea = (renderer: ReactTestRenderer, mode: 'compact' | 'full' = 'full') =>
+ renderer.root.find((node) =>
+ node.type === 'textarea' && node.props['data-query-result-message-textarea'] === mode,
+ );
+
const findEditorAction = (id: string) =>
editorState.editor.addAction.mock.calls
.map((call: any[]) => call[0])
@@ -808,21 +795,22 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
- const rendered = `${textContent(renderer!.toJSON())}\n${queryResultMessageText(renderer!)}`;
- expect(rendered).toContain('消息 1');
- expect(rendered).toContain("Table 'users'. Scan count 1, logical reads 3.");
- expect(dataGridState.latestProps?.columnNames).not.toEqual([]);
+ expect(textContent(renderer!.toJSON())).toContain('消息 1');
+ expect(findResultMessageTextarea(renderer!).props.value).toBe("Table 'users'. Scan count 1, logical reads 3.");
+ expect(dataGridState.latestProps).toBeNull();
});
- it('normalizes sqlserver mssql-prefixed message lines line-by-line', () => {
+ it('preserves sqlserver message indentation and blank lines after stripping mssql prefixes', () => {
expect(normalizeQueryResultMessages([
"mssql: select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
"mssql: 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
+ '',
" where funcno = @funcno and tabname = '$vendorclass'",
])).toEqual([
- "select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
- "'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
- "where funcno = @funcno and tabname = '$vendorclass'",
+ " select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
+ " 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
+ '',
+ " where funcno = @funcno and tabname = '$vendorclass'",
]);
});
@@ -965,14 +953,16 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
- const rendered = `${textContent(renderer!.toJSON())}\n${queryResultMessageText(renderer!)}`;
- expect(rendered).toContain('消息 2');
- expect(rendered).toContain("insert into c_dyscript(projectid,name) values (1,'demo')");
- expect(rendered).not.toContain('影响行数:0');
+ expect(textContent(renderer!.toJSON())).toContain('消息 2');
+ expect(findResultMessageTextarea(renderer!).props.value).toBe([
+ "insert into c_dyscript(projectid,name) values (1,'demo')",
+ "insert into c_dyscript(projectid,name) values (2,'next')",
+ ].join('\n'));
+ expect(textContent(renderer!.toJSON())).not.toContain('影响行数:0');
expect(dataGridState.latestProps).toBeNull();
});
- it('strips mssql prefixes before rendering sqlserver message-only results', async () => {
+ it('preserves sqlserver message indentation in the rendered result message textarea', async () => {
storeState.connections[0].config.type = 'sqlserver';
storeState.connections[0].config.database = 'hydee';
backendApp.DBQueryMulti.mockResolvedValueOnce({
@@ -985,6 +975,7 @@ describe('QueryEditor external SQL save', () => {
messages: [
"mssql: select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
"mssql: 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
+ '',
" where funcno = @funcno and tabname = '$vendorclass'",
],
},
@@ -1004,12 +995,16 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
- const rendered = `${textContent(renderer!.toJSON())}\n${queryResultMessageText(renderer!)}`;
+ const rendered = textContent(renderer!.toJSON());
+ const messageTextarea = findResultMessageTextarea(renderer!);
expect(rendered).toContain('消息 1');
- expect(rendered).toContain("select c.queryno,'' ,left(dbo.f_vendor_class");
- expect(rendered).toContain("'char','',''),'自动生成'");
- expect(rendered).toContain("where funcno = @funcno and tabname = '$vendorclass'");
- expect(rendered).not.toContain('mssql:');
+ expect(messageTextarea.props.value).toBe([
+ " select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
+ " 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
+ '',
+ " where funcno = @funcno and tabname = '$vendorclass'",
+ ].join('\n'));
+ expect(messageTextarea.props.value).not.toContain('mssql:');
});
it('renders top-level sqlserver print messages when result sets contain only status rows', async () => {
@@ -1038,10 +1033,9 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
- const rendered = `${textContent(renderer!.toJSON())}\n${queryResultMessageText(renderer!)}`;
- expect(rendered).toContain('消息 2');
- expect(rendered).toContain("insert into c_dyscript(projectid,name) values (1,'demo')");
- expect(rendered).not.toContain('影响行数:0');
+ expect(textContent(renderer!.toJSON())).toContain('消息 2');
+ expect(findResultMessageTextarea(renderer!).props.value).toBe("insert into c_dyscript(projectid,name) values (1,'demo')");
+ expect(textContent(renderer!.toJSON())).not.toContain('影响行数:0');
expect(dataGridState.latestProps).toBeNull();
});
@@ -1388,16 +1382,12 @@ describe('QueryEditor external SQL save', () => {
let renderer: ReactTestRenderer;
await act(async () => {
- renderer = create(
);
- });
-
- await act(async () => {
- findButton(renderer!, 'Show results panel').props.onClick();
+ renderer = create(
);
});
const rendered = textContent(renderer!.toJSON());
- expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.title']);
- expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.description']);
+ expect(rendered).toContain('Awaiting SQL execution');
+ expect(rendered).toContain('Run a query to display results below in the new data grid.');
expect(rendered).not.toContain('等待执行 SQL');
expect(rendered).not.toContain('运行查询后,结果会在下方以新版数据网格展示。');
});
@@ -1733,14 +1723,15 @@ describe('QueryEditor external SQL save', () => {
await findButton(renderer, 'Run').props.onClick();
});
await act(async () => {
- await Promise.resolve();
- await Promise.resolve();
- });
+ await Promise.resolve();
+ await Promise.resolve();
+ });
- const rendered = textContent(renderer.toJSON());
- expect(rendered).toContain('Statement 2 failed:');
- expect(rendered).toContain('Raw error: driver exploded');
- expect(rendered).not.toContain('第 2 条语句执行失败:driver exploded');
+ const rendered = textContent(renderer.toJSON());
+ expect(rendered).toContain(formatSqlExecutionError('driver exploded', {
+ prefix: 'Statement 2 failed:',
+ }));
+ expect(rendered).not.toContain('第 2 条语句执行失败:driver exploded');
});
it('shows the Mongo zero-result success toast in English', async () => {
@@ -1811,16 +1802,11 @@ describe('QueryEditor external SQL save', () => {
expect(messageApi.success).not.toHaveBeenCalledWith('已执行完成,生成 2 个结果集。');
});
- 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.DBQueryMultiTransactional.mockResolvedValueOnce({
- success: true,
- transactionId: 'tx-zero-rows',
- transactionPending: true,
- data: [{ columns: ['affectedRows'], rows: [{ affectedRows: 0 }], statementIndex: 1 }],
- });
+ it('shows the non-Mongo zero-result success toast in English', async () => {
+ storeState.languagePreference = 'en-US';
+ setCurrentLanguage('en-US');
+ const query = 'update users set active = 1 where 1 = 0;';
+ backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({ success: true, data: [] });
let renderer!: ReactTestRenderer;
await act(async () => {
@@ -1845,17 +1831,13 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
- const rendered = textContent(renderer.toJSON());
- expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled();
- 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.');
- });
+ expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled();
+ 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(messageApi.success).toHaveBeenCalledWith('Execution succeeded.');
+ expect(messageApi.success).not.toHaveBeenCalledWith('执行成功。');
+ });
it('shows the wrapped execution failure toast in English while preserving raw error detail', async () => {
storeState.languagePreference = 'en-US';
diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts
index cc8dbf94..f4c09132 100644
--- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts
+++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts
@@ -25,7 +25,19 @@ export type CompletionTriggerMeta = {dbName: string, triggerName: string, tableN
export type CompletionRoutineMeta = {dbName: string, routineName: string, routineType: string, schemaName?: string};
export const QUERY_LOCATOR_ALIAS_PREFIX = '__gonavi_locator_';
-const SQLSERVER_MESSAGE_PREFIX_RE = /^\s*mssql:\s*/i;
+const SQLSERVER_MESSAGE_PREFIX_RE = /^\s*mssql:/i;
+
+const trimBoundaryBlankEntries = (entries: string[]): string[] => {
+ let start = 0;
+ let end = entries.length;
+ while (start < end && !String(entries[start] || '').trim()) start++;
+ while (end > start && !String(entries[end - 1] || '').trim()) end--;
+ return entries.slice(start, end);
+};
+
+const stripSqlServerMessagePrefix = (line: string): string => (
+ line.replace(SQLSERVER_MESSAGE_PREFIX_RE, '').replace(/^[ \t]/, '')
+);
export const buildQueryReadOnlyLocator = (reason: string): EditRowLocator => ({
strategy: 'none',
@@ -121,33 +133,33 @@ export const stripQueryIdentifierQuotes = (part: string): string => {
return text;
};
-export const normalizeQueryResultMessageText = (message: unknown): string => {
+export const normalizeQueryResultMessageText = (
+ message: unknown,
+ options?: { preserveIndentation?: boolean },
+): string => {
const text = String(message ?? '').replace(/\r\n?/g, '\n');
- if (!text.trim()) return '';
+ if (!text) return '';
- let prefixRemoved = false;
- const normalizedLines = text
- .split('\n')
- .map((line) => {
- if (!line.trim()) return '';
+ const preserveIndentation = options?.preserveIndentation === true;
+ const normalizedLines = trimBoundaryBlankEntries(
+ text.split('\n').map((line) => {
if (SQLSERVER_MESSAGE_PREFIX_RE.test(line)) {
- prefixRemoved = true;
- return line.replace(SQLSERVER_MESSAGE_PREFIX_RE, '').trimStart();
+ return stripSqlServerMessagePrefix(line);
}
- return line;
- });
-
- const normalized = (prefixRemoved
- ? normalizedLines.map((line) => line.trim() ? line.trimStart() : '').join('\n')
- : normalizedLines.join('\n'))
- .trim();
-
- return prefixRemoved ? normalized : text.trim();
+ return preserveIndentation ? line : (line.trim() ? line : '');
+ }),
+ );
+ if (normalizedLines.length === 0) return '';
+ return preserveIndentation ? normalizedLines.join('\n') : normalizedLines.join('\n').trim();
};
export const normalizeQueryResultMessages = (messages: unknown): string[] => (
Array.isArray(messages)
- ? messages.map((item) => normalizeQueryResultMessageText(item)).filter(Boolean)
+ ? (() => {
+ const preserveIndentation = messages.some((item) => SQLSERVER_MESSAGE_PREFIX_RE.test(String(item ?? '')));
+ const normalized = messages.map((item) => normalizeQueryResultMessageText(item, { preserveIndentation }));
+ return preserveIndentation ? trimBoundaryBlankEntries(normalized) : normalized.filter(Boolean);
+ })()
: []
);
diff --git a/frontend/src/hooks/useAppUpdateManager.test.tsx b/frontend/src/hooks/useAppUpdateManager.test.tsx
new file mode 100644
index 00000000..6b1296f2
--- /dev/null
+++ b/frontend/src/hooks/useAppUpdateManager.test.tsx
@@ -0,0 +1,159 @@
+import React from 'react';
+import { act, create, type ReactTestRenderer } from 'react-test-renderer';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { useAppUpdateManager } from './useAppUpdateManager';
+
+const runtimeApi = vi.hoisted(() => ({
+ EventsOn: vi.fn(() => vi.fn()),
+}));
+
+const messageApi = vi.hoisted(() => ({
+ info: vi.fn(),
+ success: vi.fn(),
+ error: vi.fn(),
+}));
+
+vi.mock('../../wailsjs/runtime', () => runtimeApi);
+
+vi.mock('antd', () => ({
+ message: messageApi,
+}));
+
+type BackendAppMock = {
+ CheckForUpdates: ReturnType
;
+ CheckForUpdatesSilently: ReturnType;
+ DownloadUpdate: ReturnType;
+ InstallUpdateAndRestart: ReturnType;
+ OpenDownloadedUpdateDirectory: ReturnType;
+ GetAppInfo: ReturnType;
+};
+
+const createBackendAppMock = (): BackendAppMock => ({
+ CheckForUpdates: vi.fn(),
+ CheckForUpdatesSilently: vi.fn(),
+ DownloadUpdate: vi.fn(),
+ InstallUpdateAndRestart: vi.fn(),
+ OpenDownloadedUpdateDirectory: vi.fn(),
+ GetAppInfo: vi.fn(async () => ({ success: true, data: { version: '0.8.1', author: 'Syngnat' } })),
+});
+
+describe('useAppUpdateManager', () => {
+ let backendApp: BackendAppMock;
+ let hook: ReturnType | null = null;
+ let renderer: ReactTestRenderer | null = null;
+
+ const t = (key: string, params?: Record) => {
+ if (params?.version) return `${key}:${params.version}`;
+ if (params?.path) return `${key}:${params.path}`;
+ if (params?.error) return `${key}:${params.error}`;
+ return key;
+ };
+
+ const renderHook = (isMacRuntime: boolean) => {
+ const Harness = () => {
+ hook = useAppUpdateManager({
+ isMacRuntime,
+ runtimeBuildType: 'release',
+ t,
+ });
+ return null;
+ };
+
+ act(() => {
+ renderer = create();
+ });
+ };
+
+ beforeEach(() => {
+ backendApp = createBackendAppMock();
+ hook = null;
+ renderer = null;
+ runtimeApi.EventsOn.mockClear();
+ messageApi.info.mockReset();
+ messageApi.success.mockReset();
+ messageApi.error.mockReset();
+ vi.useFakeTimers();
+ vi.stubGlobal('window', {
+ setTimeout,
+ clearTimeout,
+ setInterval,
+ clearInterval,
+ go: {
+ app: {
+ App: backendApp,
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ act(() => {
+ renderer?.unmount();
+ });
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ });
+
+ it('uses InstallUpdateAndRestart for downloaded macOS updates', async () => {
+ backendApp.CheckForUpdates.mockResolvedValue({
+ success: true,
+ data: {
+ hasUpdate: true,
+ currentVersion: '0.8.1',
+ latestVersion: '0.8.2',
+ downloaded: true,
+ assetSize: 1024,
+ },
+ });
+ backendApp.InstallUpdateAndRestart.mockResolvedValue({ success: true });
+ backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true });
+
+ renderHook(true);
+
+ await act(async () => {
+ await hook?.checkForUpdates(false);
+ });
+
+ await act(async () => {
+ await hook?.handleInstallFromProgress();
+ });
+
+ expect(backendApp.InstallUpdateAndRestart).toHaveBeenCalledTimes(1);
+ expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
+ });
+
+ it('does not auto-open the downloaded macOS package directory after download succeeds', async () => {
+ backendApp.CheckForUpdates.mockResolvedValue({
+ success: true,
+ data: {
+ hasUpdate: true,
+ currentVersion: '0.8.1',
+ latestVersion: '0.8.2',
+ downloaded: false,
+ assetSize: 2048,
+ },
+ });
+ backendApp.DownloadUpdate.mockResolvedValue({
+ success: true,
+ data: {
+ downloadPath: '/Users/test/Desktop/GoNavi-0.8.2-MacOS-Arm64.dmg',
+ },
+ });
+ backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true });
+
+ renderHook(true);
+
+ await act(async () => {
+ await hook?.checkForUpdates(false);
+ });
+
+ await act(async () => {
+ await hook?.downloadUpdate(hook?.lastUpdateInfo!, false);
+ });
+
+ expect(backendApp.DownloadUpdate).toHaveBeenCalledTimes(1);
+ expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
+ expect(hook?.lastUpdateInfo?.downloaded).toBe(true);
+ });
+});
diff --git a/frontend/src/hooks/useAppUpdateManager.ts b/frontend/src/hooks/useAppUpdateManager.ts
index d83e2807..6f36d333 100644
--- a/frontend/src/hooks/useAppUpdateManager.ts
+++ b/frontend/src/hooks/useAppUpdateManager.ts
@@ -167,16 +167,6 @@ export const useAppUpdateManager = ({
void message.success({ content: t('app.about.message.download_completed'), duration: 2 });
}
setAboutUpdateStatus(formatAboutUpdateStatus({ ...info, downloaded: true }));
- if (isMacRuntime && !updateUserDismissedRef.current) {
- try {
- const openRes = await (window as any).go.app.App.OpenDownloadedUpdateDirectory();
- if (openRes?.success) {
- void message.success(openRes?.message || t('app.about.message.install_directory_opened_manual_replace'));
- }
- } catch (e) {
- console.warn('自动打开下载目录失败', e);
- }
- }
} else {
setUpdateDownloadProgress((prev) => ({
...prev,
@@ -218,28 +208,6 @@ export const useAppUpdateManager = ({
if (!canInstall) {
return;
}
- if (isMacRuntime) {
- const res = await (window as any).go.app.App.OpenDownloadedUpdateDirectory();
- if (!res?.success) {
- void message.error(t('app.about.message.open_install_directory_failed_with_error', { error: res?.message || t('common.unknown') }));
- updateDownloadedVersionRef.current = null;
- updateDownloadMetaRef.current = null;
- setUpdateDownloadProgress((prev) => ({
- ...prev,
- status: 'idle',
- percent: 0,
- downloaded: 0,
- open: false,
- }));
- setLastUpdateInfo((prev) => prev ? { ...prev, downloaded: false, downloadPath: undefined } : prev);
- setAboutUpdateStatus((prev) => lastUpdateInfo ? formatAboutUpdateStatus({ ...lastUpdateInfo, downloaded: false, downloadPath: undefined }) : prev);
- return;
- }
- updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null;
- hideUpdateDownloadProgress();
- void message.success(res?.message || t('app.about.message.install_directory_opened_manual_replace'));
- return;
- }
const res = await (window as any).go.app.App.InstallUpdateAndRestart();
if (!res?.success) {
void message.error(t('app.about.message.install_failed_with_error', { error: res?.message || t('common.unknown') }));
@@ -247,7 +215,7 @@ export const useAppUpdateManager = ({
}
updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null;
hideUpdateDownloadProgress();
- }, [formatAboutUpdateStatus, hideUpdateDownloadProgress, isMacRuntime, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]);
+ }, [hideUpdateDownloadProgress, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]);
const checkForUpdates = useCallback(async (silent: boolean) => {
if (updateCheckInFlightRef.current) return;
diff --git a/internal/app/methods_update.go b/internal/app/methods_update.go
index 7b6e909c..73eb22b1 100644
--- a/internal/app/methods_update.go
+++ b/internal/app/methods_update.go
@@ -1076,6 +1076,7 @@ if not exist "%SOURCE%" (
)
for %%I in ("%TARGET%") do set "TARGET_NAME=%%~nxI"
+for %%I in ("%TARGET%") do set "TARGET_DIR=%%~dpI"
for %%I in ("%SOURCE%") do set "SOURCE_EXT=%%~xI"
set "SOURCE_EXE="
@@ -1161,10 +1162,10 @@ exit /b 1
:move_done
del /F /Q "%TARGET_OLD%" >> "%LOG_FILE%" 2>&1
-start "" "%TARGET%" >> "%LOG_FILE%" 2>&1
+start "" /D "%TARGET_DIR%" "%TARGET%" >> "%LOG_FILE%" 2>&1
if %ERRORLEVEL% NEQ 0 (
call :log cmd start failed, trying powershell Start-Process
- powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%'" >> "%LOG_FILE%" 2>&1
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'" >> "%LOG_FILE%" 2>&1
if !ERRORLEVEL! NEQ 0 (
call :log relaunch failed
exit /b 1
diff --git a/internal/app/methods_update_windows_script_test.go b/internal/app/methods_update_windows_script_test.go
index a8926b0e..d3f58c55 100644
--- a/internal/app/methods_update_windows_script_test.go
+++ b/internal/app/methods_update_windows_script_test.go
@@ -102,7 +102,7 @@ func TestBuildWindowsScriptUsesDelayedErrorlevelInsideBlocks(t *testing.T) {
for _, token := range []string{
`if !ERRORLEVEL! NEQ 0 (`,
- `powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%'" >> "%LOG_FILE%" 2>&1`,
+ `powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'" >> "%LOG_FILE%" 2>&1`,
`set "TARGET_OLD=%TARGET%.old"`,
} {
if !strings.Contains(script, token) {
@@ -111,6 +111,26 @@ func TestBuildWindowsScriptUsesDelayedErrorlevelInsideBlocks(t *testing.T) {
}
}
+func TestBuildWindowsScriptRelaunchUsesTargetDirectory(t *testing.T) {
+ script := buildWindowsScript(
+ `C:\tmp\GoNavi-v0.5.0-windows-amd64.exe`,
+ `C:\Program Files\GoNavi\GoNavi.exe`,
+ `C:\Program Files\GoNavi\.gonavi-update-windows-v0.5.0`,
+ `C:\Program Files\GoNavi\logs\update-install.log`,
+ 99999,
+ )
+
+ for _, token := range []string{
+ `for %%I in ("%TARGET%") do set "TARGET_DIR=%%~dpI"`,
+ `start "" /D "%TARGET_DIR%" "%TARGET%" >> "%LOG_FILE%" 2>&1`,
+ `Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'`,
+ } {
+ if !strings.Contains(script, token) {
+ t.Fatalf("windows update relaunch missing token: %s\nscript:\n%s", token, script)
+ }
+ }
+}
+
func TestBuildWindowsLaunchCommandUsesDirectHiddenCall(t *testing.T) {
cmd := buildWindowsLaunchCommand(`C:\tmp\gonavi-update\update.cmd`)