From de6a6f14612f19dad3873d5a3a1aea0cbb77170b Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Sun, 2 Aug 2026 05:21:49 +0000 Subject: [PATCH 1/2] fix: ask Windows users to restart instead of failing WSL setup Start-Process -Verb RunAs -Wait can return while the elevated child is still enabling Windows optional features. The signed-in pass then read a non-terminal status, saw exit code 0, probed for a WSL runtime the child had not finished installing, and reported "Microsoft WSL is not ready in the signed-in user's session" as a failed installation - while the PowerShell window was visibly still enabling VirtualMachinePlatform. Seconds later the child wrote the correct restart_required status that nothing read. The signed-in pass now blocks on the real process handle and waits a bounded period for a terminal child status. A reboot Windows has not taken yet is recognised as restart_required rather than failure, via EnablePending, DISM's ambiguous "Possible" flag, an absent vmcompute service, or a pending Component Based Servicing restart. The restart is genuinely unavoidable: Windows cannot activate WSL 2 or VirtualMachinePlatform without one. So the UI now states that plainly instead of implying breakage - no danger colouring, no "setup failed" heading, and explicit numbered steps telling the Captain to restart, sign back in, and reopen 1Helm to resume. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 16 ++++++++++++ scripts/install-wsl-runtime.ps1 | 43 ++++++++++++++++++++++++++++++--- src/client/onboarding.ts | 22 ++++++++++++----- test/desktop.mjs | 14 +++++++++++ 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e15c780..b6389af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Windows first-run no longer reports "Shared runtime setup failed" when all + Windows actually needs is a restart. The signed-in pass now blocks on the + elevated process handle and waits for a terminal status: `Start-Process + -Verb RunAs -Wait` can return while the elevated child is still enabling + WSL features, and probing for a WSL runtime in that window reported a + failure the Captain could not act on. +- A reboot Windows has not taken yet is now recognised as `restart_required` + rather than a failure, including the `EnablePending` feature state, DISM's + ambiguous `Possible` restart flag, an absent `vmcompute` service, and a + pending Component Based Servicing restart. +- The setup card no longer paints a pending restart in the error colour, and + gives plain numbered steps: restart the PC, sign back in as the same user, + reopen 1Helm and setup resumes where it left off. + ## [0.0.37] - 2026-08-02 ### Fixed diff --git a/scripts/install-wsl-runtime.ps1 b/scripts/install-wsl-runtime.ps1 index 7bc5343..8688f4c 100644 --- a/scripts/install-wsl-runtime.ps1 +++ b/scripts/install-wsl-runtime.ps1 @@ -72,12 +72,27 @@ function Test-WslRestartFailure { } function Require-WindowsRestart { - $message = "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." - Write-SetupStatus -Status "restart_required" -Step $message -Progress 20 -ErrorMessage "Windows restart required to finish enabling WSL 2." + $message = "Restart this PC to finish enabling WSL 2, then open 1Helm again. Setup continues automatically." + Write-SetupStatus -Status "restart_required" -Step $message -Progress 20 -ErrorMessage "Windows must restart to finish enabling WSL 2. No other action is needed." Write-Host $message exit 10 } +# Windows cannot activate the WSL 2 features until it reboots: the features sit +# in EnablePending, DISM reports RestartRequired as the ambiguous "Possible", +# and the vmcompute service does not exist yet. Any of those is a reboot, not a +# failure, and must never be reported to the Captain as a broken installation. +function Test-PendingWslRestart { + if ($null -eq (Get-Service -Name vmcompute -ErrorAction SilentlyContinue)) { return $true } + foreach ($name in @("Microsoft-Windows-Subsystem-Linux", "VirtualMachinePlatform")) { + $feature = Get-WindowsOptionalFeature -Online -FeatureName $name -ErrorAction SilentlyContinue + if ($null -eq $feature) { continue } + if ([string]$feature.State -ne "Enabled") { return $true } + if (Test-RestartRequired $feature) { return $true } + } + return (Test-Path -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') +} + function Get-WslDistributionNames { $result = Get-WslText -ArgumentList @("--list", "--quiet") if ($result.ExitCode -ne 0) { return @() } @@ -261,15 +276,37 @@ try { $hostArguments += @("-StatusPath", ('"{0}"' -f $statusArg)) } $hostProcess = Start-Process -FilePath "powershell.exe" -ArgumentList ($hostArguments -join " ") -Verb RunAs -Wait -PassThru + # -Wait is not dependable for an elevated ShellExecute launch: it can return + # while the child is still enabling Windows features. Continuing here probes + # for a WSL runtime the child has not finished installing and reports a false + # failure, so block on the real process handle before reading any outcome. + if ($null -ne $hostProcess) { + try { $hostProcess.WaitForExit() } catch { } + } $hostExitCode = if ($null -eq $hostProcess) { $null } else { $hostProcess.ExitCode } + # The status file is written by the child immediately before it exits. Give a + # bounded grace period for a terminal record rather than racing its last write. + $settleDeadline = (Get-Date).AddSeconds(30) + while ((Get-Date) -lt $settleDeadline) { + $pending = Read-ReportedSetupStatus + if ($null -ne $pending -and @("restart_required", "failed", "complete") -contains [string]$pending.status) { break } + Start-Sleep -Milliseconds 500 + } $hostOutcome = Get-HostSetupOutcome -ExitCode $hostExitCode if ($hostOutcome.Status -eq "restart_required") { Write-SetupStatus -Status "restart_required" -Step $hostOutcome.Step -Progress 20 -ErrorMessage $hostOutcome.Detail Write-Host $hostOutcome.Step exit 10 } - if ($hostOutcome.Status -eq "failed") { Fail-Setup $hostOutcome.Detail } + if ($hostOutcome.Status -eq "failed") { + # A reboot that Windows has not taken yet is the single most common reason + # the elevated pass cannot finish. Tell the Captain to restart instead of + # presenting a failed installation they cannot act on. + if (Test-PendingWslRestart) { Require-WindowsRestart } + Fail-Setup $hostOutcome.Detail + } if (-not (Test-PinnedWslRuntime)) { + if (Test-PendingWslRestart) { Require-WindowsRestart } Fail-Setup "Microsoft WSL $wslVersion is not ready in the signed-in user's session." } diff --git a/src/client/onboarding.ts b/src/client/onboarding.ts index f8aafa2..18b86e4 100644 --- a/src/client/onboarding.ts +++ b/src/client/onboarding.ts @@ -156,14 +156,24 @@ export function openOnboarding(root: HTMLElement, opts: WizardOptions): void { h("div", { class: "font-semibold text-fg" }, restart ? "Windows restart required" : failed ? "Shared runtime setup failed" : "Setting up shared Windows runtime"), h("p", { class: "mt-2 text-sm leading-6 text-muted" }, restart - ? "Windows enabled WSL 2 components that need a reboot. Restart the PC, reopen 1Helm, then create the workspace again." + ? "Windows finished enabling WSL 2, which needs a restart before it can run. Nothing went wrong and nothing is lost." : "One-time administrator setup. Keep the PowerShell window open until it finishes; 1Helm tracks progress here."), h("div", { class: "wizard-progress mt-4" }, h("span", { style: `width:${width}%` })), h("p", { class: "mt-3 text-sm leading-6 text-fg" }, setup.step || "Working…"), - setup.error ? h("p", { class: "mt-2 text-sm text-danger" }, setup.error) : null, - h("p", { class: "mt-2 text-xs text-muted" }, failed || restart - ? "After a successful setup, 1Helm will prepare the sealed channel image automatically." - : "This downloads Microsoft's pinned WSL package and the shared Linux runtime, then installs Podman inside it."), + // A pending restart is an expected step, not a fault: never paint it in + // the danger colour that tells the Captain their installation broke. + setup.error ? h("p", { class: `mt-2 text-sm ${restart ? "text-muted" : "text-danger"}` }, setup.error) : null, + restart + ? h("ol", { class: "mt-3 list-decimal space-y-1 pl-5 text-sm leading-6 text-fg" }, + h("li", {}, "Restart this PC."), + h("li", {}, "Sign back in as the same Windows user."), + h("li", {}, "Open 1Helm and continue — setup picks up where it left off.")) + : null, + h("p", { class: "mt-2 text-xs text-muted" }, restart + ? "1Helm keeps this progress. After the restart it finishes the runtime and prepares the sealed channel image automatically." + : failed + ? "After a successful setup, 1Helm will prepare the sealed channel image automatically." + : "This downloads Microsoft's pinned WSL package and the shared Linux runtime, then installs Podman inside it."), ]; if ((failed || restart) && opts?.retry) { children.push(h("button", { @@ -171,7 +181,7 @@ export function openOnboarding(root: HTMLElement, opts: WizardOptions): void { onclick: () => { opts.retry?.(); }, }, restart ? "I restarted — retry setup" : "Retry shared runtime setup")); } - status.replaceChildren(h("div", { class: `card p-4 ${failed || restart ? "border-danger/40" : "border-accent/30"}` }, ...children)); + status.replaceChildren(h("div", { class: `card p-4 ${failed ? "border-danger/40" : "border-accent/30"}` }, ...children)); }; const runWindowsRuntimeSetup = async (button: HTMLButtonElement): Promise => { diff --git a/test/desktop.mjs b/test/desktop.mjs index 89144a1..67ead7a 100644 --- a/test/desktop.mjs +++ b/test/desktop.mjs @@ -181,6 +181,20 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn assert.match(windowsRemoval, /"--exec", "\/usr\/libexec\/1helm-oci-runtime"/, "Windows removal delegates ownership checks to the narrow installed OCI helper"); assert.match(windowsRuntime, /VirtualMachinePlatform/); assert.match(windowsRuntime, /2\.7\.10\.0/); + // A pending Windows reboot must never surface to the Captain as a broken + // installation: -Wait can return while the elevated child is still enabling + // features, and probing for WSL in that window reports a false failure. + assert.match(windowsRuntime, /\$hostProcess\.WaitForExit\(\)/, "the signed-in pass blocks on the real elevated process handle, not only Start-Process -Wait"); + assert.match(windowsRuntime, /function Test-PendingWslRestart/, "the installer can recognise a reboot Windows has not taken yet"); + assert.match(windowsRuntime, /vmcompute[\s\S]{0,400}EnablePending|EnablePending[\s\S]{0,400}vmcompute|-ne "Enabled"/, "pending feature activation counts as a restart, not a failure"); + assert.match(windowsRuntime, /if \(Test-PendingWslRestart\) \{ Require-WindowsRestart \}[\s\S]{0,200}Fail-Setup "Microsoft WSL \$wslVersion is not ready/, "the user-session WSL probe asks for a restart before declaring failure"); + assert.match(windowsRuntime, /Restart this PC to finish enabling WSL 2/, "the restart status carries plain-language instructions"); + assert.match(windowsRuntime, /-Status "restart_required"/, "the restart path reports the dedicated restart status"); + assert.match(windowsRuntime, /@\("restart_required", "failed", "complete"\) -contains/, "the parent waits for a terminal child status instead of racing its last write"); + const onboardingRuntimeUi = await readFile(join(root, "src", "client", "onboarding.ts"), "utf8"); + assert.match(onboardingRuntimeUi, /restart \? "text-muted" : "text-danger"/, "a pending restart is not painted as an error"); + assert.match(onboardingRuntimeUi, /\$\{failed \? "border-danger\/40" : "border-accent\/30"\}/, "only real failures get the danger card treatment"); + assert.match(onboardingRuntimeUi, /Restart this PC\./, "the restart state gives the Captain explicit numbered steps"); assert.match(windowsRuntime, /github\.com\/microsoft\/WSL\/releases\/download\/2\.7\.10\/wsl\.2\.7\.10\.0\.x64\.msi/); assert.match(windowsRuntime, /1a62f90a43c03cc5bda47dfd0b6faf496ac70fd4389190518120a4f84fc895cf/); assert.match(windowsRuntime, /Get-AuthenticodeSignature/); From 275959c06f14ac51f7fada3125f68d7e49438a33 Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Sun, 2 Aug 2026 05:22:28 +0000 Subject: [PATCH 2/2] chore: restamp index.html for the rebuilt onboarding bundle Co-Authored-By: Claude Fable 5 --- public/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index 5eba792..36115f8 100644 --- a/public/index.html +++ b/public/index.html @@ -30,12 +30,12 @@ document.querySelectorAll('meta[name="theme-color"]').forEach(function (m) { m.setAttribute("content", color); }); })(); - +
- +