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;
|
let status = currentStatus;
|
||||||
|
|
||||||
if (!status.wsl?.wslInstalled) {
|
if (!status.wsl?.wslInstalled) {
|
||||||
status = await this.#installWindowsWslCore();
|
status = await this.#installWindowsWslCore(status);
|
||||||
if (!status.wsl?.wslInstalled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.wsl?.rebootRequired) {
|
if (status.wsl?.rebootRequired) {
|
||||||
|
|
@ -213,6 +210,10 @@ export class TmuxInstallerRunnerAdapter
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!status.wsl?.wslInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!status.wsl?.distroName) {
|
if (!status.wsl?.distroName) {
|
||||||
status = await this.#installWindowsDistro();
|
status = await this.#installWindowsDistro();
|
||||||
if (!status.wsl?.distroName) {
|
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.#appendLog('Starting the elevated WSL core install step...');
|
||||||
this.#setSnapshot({
|
this.#setSnapshot({
|
||||||
phase: 'pending_external_elevation',
|
phase: 'pending_external_elevation',
|
||||||
|
|
@ -297,6 +298,28 @@ export class TmuxInstallerRunnerAdapter
|
||||||
this.#appendLog(elevationResult.detail);
|
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({
|
this.#setSnapshot({
|
||||||
phase: 'waiting_for_external_step',
|
phase: 'waiting_for_external_step',
|
||||||
strategy: 'wsl',
|
strategy: 'wsl',
|
||||||
|
|
@ -485,6 +508,34 @@ export class TmuxInstallerRunnerAdapter
|
||||||
return RESTART_REQUIRED_PATTERNS.some((pattern) => lowered.includes(pattern));
|
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> {
|
async #runResolvedPlan(plan: TmuxInstallPlan, resetLogs = true): Promise<void> {
|
||||||
this.#setSnapshot({
|
this.#setSnapshot({
|
||||||
phase: 'preparing',
|
phase: 'preparing',
|
||||||
|
|
|
||||||
|
|
@ -559,8 +559,80 @@ describe('TmuxInstallerRunnerAdapter', () => {
|
||||||
|
|
||||||
await expect(runner.install()).resolves.toBeUndefined();
|
await expect(runner.install()).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(statusCallCount).toBe(1);
|
||||||
expect(runner.getSnapshot().phase).toBe('needs_restart');
|
expect(runner.getSnapshot().phase).toBe('needs_restart');
|
||||||
expect(runner.getSnapshot().message).toContain('Restart Windows');
|
expect(runner.getSnapshot().message).toContain('Restart Windows');
|
||||||
expect(runner.getSnapshot().detail).toContain('WSL core installation command completed');
|
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';
|
import { useTmuxInstallerBanner } from '../hooks/useTmuxInstallerBanner';
|
||||||
|
|
||||||
const SUMMARY_TITLE = 'tmux is not installed';
|
const SUMMARY_TITLE = 'tmux is not installed';
|
||||||
|
const BANNER_MIN_H = 'min-h-[4.25rem]';
|
||||||
|
|
||||||
const SourceLink = ({
|
const SourceLink = ({
|
||||||
label,
|
label,
|
||||||
|
|
@ -78,7 +79,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
borderLeftColor: viewModel.error ? '#ef4444' : '#f59e0b',
|
borderLeftColor: viewModel.error ? '#ef4444' : '#f59e0b',
|
||||||
backgroundColor: 'rgba(245, 158, 11, 0.08)',
|
backgroundColor: 'rgba(245, 158, 11, 0.08)',
|
||||||
|
|
@ -89,17 +90,22 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
type="button"
|
type="button"
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
onClick={() => setExpanded((current) => !current)}
|
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="flex min-w-0 items-center gap-3">
|
||||||
<span className="pt-0.5">
|
<span className="shrink-0">
|
||||||
{viewModel.error ? (
|
{viewModel.error ? (
|
||||||
<AlertTriangle className="size-4 text-red-300" />
|
<AlertTriangle className="size-4 text-red-300" />
|
||||||
) : (
|
) : (
|
||||||
<Wrench className="size-4 text-amber-300" />
|
<Wrench className="size-4 text-amber-300" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate">{SUMMARY_TITLE}</span>
|
<span
|
||||||
|
className="truncate text-sm leading-5"
|
||||||
|
style={{ color: 'var(--color-text)' }}
|
||||||
|
>
|
||||||
|
{SUMMARY_TITLE}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<ChevronUp className="size-4 shrink-0" />
|
<ChevronUp className="size-4 shrink-0" />
|
||||||
|
|
@ -109,7 +115,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
<div className="min-w-0 max-w-4xl">
|
<div className="min-w-0 max-w-4xl">
|
||||||
{viewModel.title !== SUMMARY_TITLE && (
|
{viewModel.title !== SUMMARY_TITLE && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue