From 940bbef9dcd5f638bdec173fd250f563fd672b48 Mon Sep 17 00:00:00 2001 From: Qiuner Date: Mon, 17 Aug 2026 19:19:39 +0800 Subject: [PATCH] fix(desktop): make diagnostic export user-visible --- dsh-plugin-desktop/src/electron-runtime.ts | 23 ++++++++++- .../tests/electron-runtime.spec.ts | 38 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/dsh-plugin-desktop/src/electron-runtime.ts b/dsh-plugin-desktop/src/electron-runtime.ts index 2ef98222ef..7d41ca5912 100644 --- a/dsh-plugin-desktop/src/electron-runtime.ts +++ b/dsh-plugin-desktop/src/electron-runtime.ts @@ -88,6 +88,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime { private quitting = false private readonly trayItems = new Map() private terminalSpec: DesktopTerminalSpec | undefined + private diagnosticExport: Promise | undefined private rendererBootReported = false constructor( @@ -216,13 +217,22 @@ export class ElectronDesktopRuntime implements DesktopRuntime { /** @inheritdoc */ exportDiagnostics(): Promise { + if (this.diagnosticExport !== undefined) return this.diagnosticExport + const operation = this.performDiagnosticExport().finally(() => { + if (this.diagnosticExport === operation) this.diagnosticExport = undefined + }) + this.diagnosticExport = operation + return operation + } + + private performDiagnosticExport(): Promise { return exportDiagnosticsZip( join(app.getPath('userData'), 'logs'), app.getPath('userData'), ).then((path) => { shell.showItemInFolder(path) }).catch((cause: unknown) => { - this.logError(`dsh-plugin-desktop: failed to export diagnostics: ${cause instanceof Error ? cause.message : String(cause)}`) + this.reportDiagnosticExportError(cause) }) } @@ -467,6 +477,17 @@ export class ElectronDesktopRuntime implements DesktopRuntime { } } + /** Keep diagnostic export failures visible in a packaged GUI process. */ + private reportDiagnosticExportError(cause: unknown): void { + const error = cause instanceof Error ? cause : new Error(String(cause)) + this.logError(`dsh-plugin-desktop: failed to export diagnostics: ${error.message}`) + try { + dialog.showErrorBox('Unable to Export Diagnostics', error.message) + } catch (dialogCause) { + this.logError(`dsh-plugin-desktop: failed to show diagnostics error: ${dialogCause instanceof Error ? dialogCause.message : String(dialogCause)}`) + } + } + private rebuildTrayMenu(): void { const tray = this.tray const spec = this.scheduled diff --git a/dsh-plugin-desktop/tests/electron-runtime.spec.ts b/dsh-plugin-desktop/tests/electron-runtime.spec.ts index cc0cafff6c..4858e2bcb2 100644 --- a/dsh-plugin-desktop/tests/electron-runtime.spec.ts +++ b/dsh-plugin-desktop/tests/electron-runtime.spec.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { DesktopShellSpec } from '../src/runtime.ts' const terminal = vi.hoisted(() => ({ open: vi.fn() })) +const diagnostics = vi.hoisted(() => ({ export: vi.fn() })) const updater = vi.hoisted(() => ({ download: vi.fn() })) const childProcess = vi.hoisted(() => { type Listener = (...args: unknown[]) => void @@ -35,6 +36,10 @@ vi.mock('../src/desktop-terminal.ts', async (importOriginal) => ({ openDesktopTerminal: terminal.open, })) +vi.mock('../src/diagnostic-export.ts', () => ({ + exportDiagnosticsZip: diagnostics.export, +})) + vi.mock('../src/update-download.ts', () => ({ downloadDesktopUpdate: updater.download, })) @@ -162,6 +167,7 @@ const electron = vi.hoisted(() => { shell: { openExternal: vi.fn(async () => {}), openPath: vi.fn(async () => ''), + showItemInFolder: vi.fn(), }, templateIcon, Tray, @@ -213,6 +219,7 @@ describe('Electron compatibility runtime', () => { childProcess.reset() vi.clearAllMocks() updater.download.mockReset() + diagnostics.export.mockReset() electron.loadURL.mockReset() electron.loadURL.mockResolvedValue(undefined) electron.dialog.showMessageBox.mockResolvedValue({ response: 0, checkboxChecked: false }) @@ -495,6 +502,37 @@ describe('Electron compatibility runtime', () => { } }) + it('coalesces concurrent diagnostic exports and reveals the completed archive', async () => { + const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts') + const runtime = new ElectronDesktopRuntime(async () => {}) + let finishExport: ((path: string) => void) | undefined + diagnostics.export.mockReturnValue(new Promise(resolve => { finishExport = resolve })) + + const first = runtime.exportDiagnostics() + const second = runtime.exportDiagnostics() + finishExport?.('C:\\Users\\Example\\diagnostics.zip') + await Promise.all([first, second]) + + expect(diagnostics.export).toHaveBeenCalledOnce() + expect(electron.shell.showItemInFolder).toHaveBeenCalledOnce() + expect(electron.shell.showItemInFolder).toHaveBeenCalledWith('C:\\Users\\Example\\diagnostics.zip') + }) + + it('shows a native error when diagnostic export fails', async () => { + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts') + const runtime = new ElectronDesktopRuntime(async () => {}) + diagnostics.export.mockRejectedValueOnce(new Error('disk is full')) + + await expect(runtime.exportDiagnostics()).resolves.toBeUndefined() + + expect(electron.dialog.showErrorBox).toHaveBeenCalledWith( + 'Unable to Export Diagnostics', + 'disk is full', + ) + expect(stderr).toHaveBeenCalledWith(expect.stringContaining('failed to export diagnostics: disk is full')) + }) + it('shows native errors for synchronous and asynchronous terminal launch failures', async () => { vi.spyOn(process, 'platform', 'get').mockReturnValue('win32') const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)