From 898a795182cc71953b9442f466bafad4460c6aa0 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 14 Apr 2026 21:58:55 +0300 Subject: [PATCH] fix(tmux): tighten windows reboot flow --- .../runtime/TmuxInstallerRunnerAdapter.ts | 61 ++++++++++++++-- .../TmuxInstallerRunnerAdapter.test.ts | 72 +++++++++++++++++++ .../renderer/ui/TmuxInstallerBannerView.tsx | 18 +++-- 3 files changed, 140 insertions(+), 11 deletions(-) diff --git a/src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts b/src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts index f484ba55..6b272d78 100644 --- a/src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts +++ b/src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts @@ -190,10 +190,7 @@ export class TmuxInstallerRunnerAdapter let status = currentStatus; if (!status.wsl?.wslInstalled) { - status = await this.#installWindowsWslCore(); - if (!status.wsl?.wslInstalled) { - return; - } + status = await this.#installWindowsWslCore(status); } if (status.wsl?.rebootRequired) { @@ -213,6 +210,10 @@ export class TmuxInstallerRunnerAdapter return; } + if (!status.wsl?.wslInstalled) { + return; + } + if (!status.wsl?.distroName) { status = await this.#installWindowsDistro(); if (!status.wsl?.distroName) { @@ -277,7 +278,7 @@ export class TmuxInstallerRunnerAdapter } } - async #installWindowsWslCore(): Promise { + async #installWindowsWslCore(currentStatus: TmuxStatus): Promise { this.#appendLog('Starting the elevated WSL core install step...'); this.#setSnapshot({ phase: 'pending_external_elevation', @@ -297,6 +298,28 @@ export class TmuxInstallerRunnerAdapter this.#appendLog(elevationResult.detail); } + const immediateRebootRequired = + elevationResult.restartRequired || this.#looksLikeRestartRequired(elevationResult.detail); + if (immediateRebootRequired) { + const rebootStatus = this.#markWindowsStatusAsRebootRequired(currentStatus, elevationResult.detail); + this.#statusSource.invalidateStatus(); + this.#setSnapshot({ + phase: 'needs_restart', + strategy: 'wsl', + message: 'Restart Windows before continuing with tmux setup', + detail: + elevationResult.detail ?? + rebootStatus.wsl?.statusDetail ?? + 'WSL was installed, but Windows still needs a restart before tmux setup can continue.', + error: null, + canCancel: false, + acceptsInput: false, + inputPrompt: null, + inputSecret: false, + }); + return rebootStatus; + } + this.#setSnapshot({ phase: 'waiting_for_external_step', strategy: 'wsl', @@ -485,6 +508,34 @@ export class TmuxInstallerRunnerAdapter return RESTART_REQUIRED_PATTERNS.some((pattern) => lowered.includes(pattern)); } + #markWindowsStatusAsRebootRequired( + status: TmuxStatus, + detail: string | null | undefined + ): TmuxStatus { + return { + ...status, + autoInstall: { + ...status.autoInstall, + requiresRestart: true, + }, + wsl: { + wslInstalled: status.wsl?.wslInstalled ?? false, + rebootRequired: true, + distroName: status.wsl?.distroName ?? null, + distroVersion: status.wsl?.distroVersion ?? null, + distroBootstrapped: status.wsl?.distroBootstrapped ?? false, + innerPackageManager: status.wsl?.innerPackageManager ?? null, + tmuxAvailableInsideWsl: status.wsl?.tmuxAvailableInsideWsl ?? false, + tmuxVersion: status.wsl?.tmuxVersion ?? null, + tmuxBinaryPath: status.wsl?.tmuxBinaryPath ?? null, + statusDetail: + detail ?? + status.wsl?.statusDetail ?? + 'Windows still needs a restart before tmux setup can continue.', + }, + }; + } + async #runResolvedPlan(plan: TmuxInstallPlan, resetLogs = true): Promise { this.#setSnapshot({ phase: 'preparing', diff --git a/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts b/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts index 1a976f1a..3c1fc042 100644 --- a/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts +++ b/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts @@ -559,8 +559,80 @@ describe('TmuxInstallerRunnerAdapter', () => { await expect(runner.install()).resolves.toBeUndefined(); + expect(statusCallCount).toBe(1); expect(runner.getSnapshot().phase).toBe('needs_restart'); expect(runner.getSnapshot().message).toContain('Restart Windows'); expect(runner.getSnapshot().detail).toContain('WSL core installation command completed'); }); + + it('switches immediately into needs_restart when the elevated step only returns a localized restart message', async () => { + const presenter = createPresenter(); + const initialStatus = createBaseStatus({ + platform: 'win32', + autoInstall: { + supported: true, + strategy: 'wsl', + packageManagerLabel: 'WSL', + requiresTerminalInput: false, + requiresAdmin: true, + requiresRestart: true, + mayOpenExternalWindow: true, + reasonIfUnsupported: null, + manualHints: [], + }, + wsl: { + wslInstalled: false, + rebootRequired: false, + distroName: null, + distroVersion: null, + distroBootstrapped: false, + innerPackageManager: null, + tmuxAvailableInsideWsl: false, + tmuxVersion: null, + tmuxBinaryPath: null, + statusDetail: 'WSL is not installed yet.', + }, + }); + const statusSource = { + getStatus: vi.fn(async () => initialStatus), + invalidateStatus: vi.fn(), + }; + const runner = new TmuxInstallerRunnerAdapter( + statusSource as never, + presenter as never, + { + resolve: vi.fn(async () => { + throw new Error('resolve() should not run before restart'); + }), + } as never, + { + run: vi.fn(), + cancel: vi.fn(), + } as never, + { + run: vi.fn(), + writeLine: vi.fn(), + cancel: vi.fn(), + } as never, + { + persistPreferredDistro: vi.fn(async () => undefined), + } as never, + { + runWslCoreInstall: vi.fn(async () => ({ + outcome: 'elevated_unknown_outcome', + detail: + 'Требуемая операция выполнена успешно. Чтобы сделанные изменения вступили в силу, следует перезагрузить систему.', + restartRequired: false, + featureStates: [], + resultFilePath: null, + })), + } as never + ); + + await expect(runner.install()).resolves.toBeUndefined(); + + expect(statusSource.getStatus).toHaveBeenCalledTimes(1); + expect(runner.getSnapshot().phase).toBe('needs_restart'); + expect(runner.getSnapshot().detail).toContain('перезагрузить систему'); + }); }); diff --git a/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx b/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx index 8e8e63b4..81469306 100644 --- a/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx +++ b/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx @@ -13,6 +13,7 @@ import { import { useTmuxInstallerBanner } from '../hooks/useTmuxInstallerBanner'; const SUMMARY_TITLE = 'tmux is not installed'; +const BANNER_MIN_H = 'min-h-[4.25rem]'; const SourceLink = ({ label, @@ -78,7 +79,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { return (
setExpanded((current) => !current)} - className="flex w-full items-center justify-between gap-3 text-left" + className="flex min-h-[1.75rem] w-full items-center justify-between gap-3 text-left" > - - + + {viewModel.error ? ( ) : ( )} - {SUMMARY_TITLE} + + {SUMMARY_TITLE} + {expanded ? ( @@ -109,7 +115,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { {expanded && ( -
+
{viewModel.title !== SUMMARY_TITLE && (