fix(tmux): tighten windows reboot flow
This commit is contained in:
parent
80221884ed
commit
898a795182
3 changed files with 140 additions and 11 deletions
|
|
@ -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<TmuxStatus> {
|
||||
async #installWindowsWslCore(currentStatus: TmuxStatus): Promise<TmuxStatus> {
|
||||
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<void> {
|
||||
this.#setSnapshot({
|
||||
phase: 'preparing',
|
||||
|
|
|
|||
|
|
@ -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('перезагрузить систему');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
className="mb-6 rounded-lg border-l-4 px-4 py-4"
|
||||
className={`mb-6 rounded-lg border-l-4 px-4 py-3 ${BANNER_MIN_H}`}
|
||||
style={{
|
||||
borderLeftColor: viewModel.error ? '#ef4444' : '#f59e0b',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.08)',
|
||||
|
|
@ -89,17 +90,22 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
type="button"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => 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"
|
||||
>
|
||||
<span className="flex min-w-0 items-start gap-2 text-base font-semibold leading-6">
|
||||
<span className="pt-0.5">
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<span className="shrink-0">
|
||||
{viewModel.error ? (
|
||||
<AlertTriangle className="size-4 text-red-300" />
|
||||
) : (
|
||||
<Wrench className="size-4 text-amber-300" />
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate">{SUMMARY_TITLE}</span>
|
||||
<span
|
||||
className="truncate text-sm leading-5"
|
||||
style={{ color: 'var(--color-text)' }}
|
||||
>
|
||||
{SUMMARY_TITLE}
|
||||
</span>
|
||||
</span>
|
||||
{expanded ? (
|
||||
<ChevronUp className="size-4 shrink-0" />
|
||||
|
|
@ -109,7 +115,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="mt-3 space-y-3">
|
||||
<div className="min-w-0 max-w-4xl">
|
||||
{viewModel.title !== SUMMARY_TITLE && (
|
||||
<div
|
||||
|
|
|
|||
Loading…
Reference in a new issue