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 ? ( ) : 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`)