Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions host-setup/windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The tooling that makes a native Windows host satisfy the contract in [`docs/host

## What Is Here

- [`install-tools.ps1`][install-tools] installs and upgrades the host tools, and reports what each one is installed at, where it came from, and which scope it sits in.
- [`install-tools.ps1`][install-tools] installs and upgrades the host tools, and reports what each one is installed at, where it came from, and which scope it sits in. Installing or upgrading `docker` also brings the WSL platform up to Docker Desktop's own floor where it is behind.
- [`upgrade-host.ps1`][upgrade-host] upgrades the packages `winget` manages and updates the WSL platform.
- [`setup-github.ps1`][setup-github] configures the SSH key, git, and commit signing.
- [`setup-wsl.ps1`][setup-wsl] installs a WSL distribution and reports how Docker Desktop is integrated with the ones this host runs.
Expand Down Expand Up @@ -80,7 +80,13 @@ pwsh -NoProfile -File ..\bootstrap.ps1 -Help

Docker's own `docker-desktop` distribution is excluded from every distribution listing, since it is Docker's rather than one an operator installed.

`install-tools.ps1` checks, before installing or upgrading `docker`, that `wsl.exe` is present and reports a WSL version at or above `2.1.5`, Docker Desktop's own documented floor for the platform it depends on. Where either is not the case it skips `docker` and names the exact remedy (`wsl --install --no-distribution`, or `upgrade-host.ps1 -Wsl`) rather than installing against a platform Docker Desktop cannot use, or Windows-feature-installing on the caller's behalf. It never runs `wsl --install` or `wsl --update` itself: those stay `upgrade-host.ps1 -Wsl` and a person's own choice, since an update restarts every distribution and neither belongs as a side effect of installing a different tool. The same check surfaces as a note under `-Report`, read-only, before a caller ever runs `-Install`.
`install-tools.ps1` checks, before installing or upgrading `docker`, that `wsl.exe` is present and reports a WSL version at or above `2.1.5`, Docker Desktop's own documented floor for the platform it depends on. Where `wsl.exe` is missing entirely it skips `docker` and names the remedy (`wsl --install --no-distribution`), the one case this still leaves to a person: standing up WSL from nothing is a different action from bringing an existing platform current, the same reasoning that keeps a distribution *install* in `setup-wsl.ps1` rather than folded in here. The same check surfaces as a note under `-Report`, read-only, before a caller ever runs `-Install`.

Where WSL is present but behind the floor, or `docker` itself is about to change version, `install-tools.ps1` drives the fix itself rather than only naming it, asking first unless `-Yes` was given: it stops Docker Desktop through its own CLI (`docker desktop stop`, not the tray icon), runs `wsl --update` where the floor is not met, shuts every WSL distribution down with `wsl --shutdown`, and starts Docker Desktop again (`docker desktop start`) once the `docker` package itself is also settled.

This moved a boundary the tool used to hold: WSL used to be read-only here, and `upgrade-host.ps1 -Wsl` was the only thing that ever ran `wsl --update`, on the reasoning that an update restarts every distribution and that does not belong as a side effect of installing a different tool. Two things moved it. Docker Desktop's own per-distro WSL integration goes stale across an engine bump often enough to have a name on Docker's own tracker (`WSL integration with distro '<name>' unexpectedly stopped`), and the dialog's own "Restart the WSL integration" button does not clear it, since it retries the proxy inside the distro that is already running against the same stale state. A full stop, `wsl --shutdown`, start cycle does clear it. And once a `docker` version bump already needed that exact stop-then-restart window for its own integration to recover, updating WSL inside that same window costs nothing extra, and skips the `upgrade-host.ps1 -Wsl` refusal a Docker Desktop already running would otherwise walk straight into.

`wsl --update` installs through its own MSI and raises a UAC prompt when this `pwsh` is not itself elevated, the state the rest of this tooling deliberately stays in. Nothing can answer that prompt unattended, so where the process is neither elevated nor at an interactive console, `install-tools.ps1` refuses to start the update rather than hang, and restores Docker Desktop to how it found it first.

## Differences From the Linux Tooling

Expand Down
154 changes: 139 additions & 15 deletions host-setup/windows/install-tools.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ it finds and an installer that needs administrator asks for it itself. Naming a
disagrees with the installed copy would add a second copy beside the first rather than replacing
it, so an upgrade refuses that case and names -Reinstall, which removes the old copy first.

An install or upgrade of docker also brings the WSL platform itself up to Docker Desktop's own
floor where it is behind, and stops Docker Desktop first either way, since Docker Desktop holds
the WSL service open and both a platform update and its own WSL integration otherwise need a
restart to recover from an engine bump. wsl --update raises its own administrator prompt, which
this refuses to start unattended where nothing could answer it, rather than hang.

Examples:
install-tools.ps1 Report on every tool
install-tools.ps1 -Install Install what is missing
Expand Down Expand Up @@ -484,7 +490,8 @@ function Add-ToolNote {
# Docker Desktop's own documented floor for the WSL platform, per docs.docker.com/desktop/features/wsl.
$DOCKER_WSL_FLOOR = '2.1.5'

# Every wsl.exe call goes through here, because wsl.exe emits UTF-16 by default and its output then reads as NUL separated characters.
# Every wsl.exe read goes through here, because wsl.exe emits UTF-16 by default and its output then reads as NUL separated characters.
# A wsl.exe call that changes the host instead of reading it goes through Invoke-WslRun below, which applies the same guard to a live streamed run rather than a captured one.
# Mirrored from upgrade-host.ps1 rather than shared with it, on the same rule as the rest of this directory: a script here has to stay independently fetchable.
function Invoke-Wsl {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
Expand All @@ -500,6 +507,19 @@ function Invoke-Wsl {
}
}

# The mutation-side counterpart to Invoke-Wsl: runs a wsl.exe command that changes the host through the same run wrapper every other mutating command in this script uses, with the same WSL_UTF8 guard applied around it, so its live streamed output does not read to a person as NUL separated characters either.
function Invoke-WslRun {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$previous = $env:WSL_UTF8
try {
$env:WSL_UTF8 = '1'
return (run 'wsl.exe' @Arguments)
} finally {
if ($null -eq $previous) { Remove-Item Env:\WSL_UTF8 -ErrorAction SilentlyContinue }
else { $env:WSL_UTF8 = $previous }
}
}

# Just the "WSL version" line, as a value Compare-HostVersion can read.
# Get-WslVersion in upgrade-host.ps1 concatenates WSL, kernel and WSLg into one display string instead, which serves a report rather than a comparison.
function Get-WslPlatformVersion {
Expand All @@ -511,9 +531,8 @@ function Get-WslPlatformVersion {
}

# What stands between this host and installing or upgrading docker, or $null where nothing does.
# Read-only: this never runs wsl --install or wsl --update itself.
# Those stay a person's own action through upgrade-host.ps1 -Wsl and setup-wsl.ps1, confirmed with the maintainer as the boundary.
# A WSL platform update restarts every distribution, and neither action belongs as a silent side effect of installing a different tool.
# A pure report either way, used as-is for -Report's note and as Enter-DockerMaintenance's own pre-check below, which is what actually drives the WSL platform update this used to only name.
# Standing up WSL from nothing with wsl --install stays a person's own step regardless, since that is a different action on a different subject from bringing an existing platform current, the same reasoning that keeps distribution installs in setup-wsl.ps1 rather than folded in here.
function Test-WslReadyForDocker {
if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
return "wsl.exe was not found, and Docker Desktop needs WSL2. Install it with: wsl --install --no-distribution"
Expand All @@ -528,6 +547,100 @@ function Test-WslReadyForDocker {
return $null
}

# --- Docker ---

# Whether Docker Desktop answers as running right now, through its own CLI rather than a process probe.
# A vmmem process surviving a crash is not the same as the backend actually being up, and the CLI is what agrees with what the tray icon shows.
function Test-DockerDesktopRunning {
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { return $false }
$text = (& docker desktop status --format json 2>&1 | Out-String)
if ($LASTEXITCODE -ne 0) { return $false }
return ($text -match '"Status"\s*:\s*"running"')
}
Comment thread
ptr727 marked this conversation as resolved.

function Stop-DockerDesktop {
info 'Stopping Docker Desktop'
$code = run 'docker' @('desktop', 'stop', '--timeout', '90')
if ($code -ne 0) { warn "docker desktop stop exited $code" }
return $code
}

function Start-DockerDesktop {
info 'Starting Docker Desktop'
$code = run 'docker' @('desktop', 'start', '--timeout', '180')
if ($code -ne 0) { warn 'docker desktop start did not exit cleanly, start Docker Desktop by hand and check WSL integration per distro' }
return $code
}

# Everything a docker install, upgrade or reinstall needs from Docker Desktop and WSL before winget touches the package is folded into one window rather than run twice: Docker Desktop holds the WSL service open, so a platform update fails part way while it is running, the same reason upgrade-host.ps1 -Wsl refuses outright.
# Docker Desktop's own per distro WSL integration also goes stale across an engine bump often enough that it has a name on Docker's own tracker, surfacing as "WSL integration with distro '<name>' unexpectedly stopped" the next time anything in that distro touches docker, and the in app "Restart the WSL integration" button on that dialog does not clear it, since it retries the proxy inside the distro that is already running against the same stale state.
# Both are fixed the same way: Docker Desktop stopped, WSL brought current, every distro shut down with it so each one's integration remounts fresh rather than being patched in place, and Docker Desktop started again once the docker package itself is also settled, by Exit-DockerMaintenance below.
# Boundary note: this script used to be read-only on WSL by design, naming upgrade-host.ps1 -Wsl as a person's own step rather than running it.
# The maintainer moved that boundary once a docker version bump on its own already needed this same stop, then restart, window for its own integration to recover, since a platform update asks for nothing more than that same window with Docker Desktop already stopped inside it.
# Returns whether docker is clear to proceed, and whether this stopped Docker Desktop, which is what Exit-DockerMaintenance needs to know whether it owes a restart.
function Enter-DockerMaintenance {
param([Parameter(Mandatory)][bool]$WasRunning, [string]$WslProblem)

$result = @{ Proceed = $true; Stopped = $false }
if (-not $WasRunning -and -not $WslProblem) { return $result }

$question = if ($WslProblem -and $WasRunning) {
"docker needs WSL updated first ($WslProblem). Stop Docker Desktop, update WSL, and restart Docker Desktop to continue?"
} elseif ($WslProblem) {
"docker needs WSL updated first ($WslProblem). Update WSL to continue?"
} else {
"docker is about to change version, and Docker Desktop's own WSL integration commonly goes stale across an engine bump. Stop Docker Desktop first, and restart it after, to avoid that?"
}
if (-not (confirm $question)) {
if ($WslProblem) { $result.Proceed = $false; return $result }
note 'docker' 'WSL integration may report a stale per-distro error until Docker Desktop and WSL are both restarted (docker desktop stop; wsl --shutdown; docker desktop start); declined the automatic cycle'
return $result
}

# A platform update fails part way while Docker Desktop holds the WSL service open, so an unsuccessful stop is treated the same as a still-running Docker Desktop: both refuse the run rather than risk wsl --update against a host that never actually let go of the WSL service.
if ($WasRunning) {
if ((Stop-DockerDesktop) -ne 0 -or (Test-DockerDesktopRunning)) {
warn 'docker skipped, Docker Desktop did not stop, and a WSL platform update fails part way while it is running'
$result.Proceed = $false
return $result
}
$result.Stopped = $true
}
Comment thread
ptr727 marked this conversation as resolved.
if ($WslProblem) {
# Installing through wsl --update's own MSI raises a UAC prompt when this pwsh is not itself elevated, the state the rest of this script deliberately stays in (running elevated trades this one prompt for winget installers elsewhere that fail pre-elevated instead).
# Nothing can answer that prompt without a person at the console, so a run with neither would hang rather than fail, which -Yes is supposed to rule out, not walk into unattended.
if (-not $script:ELEVATED -and (-not [Environment]::UserInteractive -or [Console]::IsInputRedirected)) {
warn "wsl --update needs administrator and raises its own prompt, which nothing can answer unattended; it would hang rather than fail, so this refuses to start it. Run interactively once so the prompt has someone to answer, or update WSL first with: host-setup\windows\upgrade-host.ps1 -Wsl"
if ($result.Stopped) { Start-DockerDesktop | Out-Null; $result.Stopped = $false }
$result.Proceed = $false
return $result
}
info 'Updating the WSL platform'
info 'This restarts every distribution, so anything running inside one is stopped'
$updateCode = Invoke-WslRun '--update'
if ($updateCode -ne 0) { warn "wsl --update exited $updateCode" }
$stillBroken = Test-WslReadyForDocker
if ($stillBroken) {
warn "docker skipped, still $stillBroken after the update"
if ($result.Stopped) { Start-DockerDesktop | Out-Null; $result.Stopped = $false }
$result.Proceed = $false
return $result
}
}
return $result
}

# The other half of Enter-DockerMaintenance, run once the docker package itself is settled: shuts every WSL distro down so each one's integration remounts fresh, then starts Docker Desktop again.
# A no-op where Enter-DockerMaintenance never stopped anything, so a run that changed nothing about Docker Desktop does not restart it for no reason, and a run that was declined does not either.
function Exit-DockerMaintenance {
param([Parameter(Mandatory)][bool]$Stopped)
if (-not $Stopped) { return }
info 'Shutting down WSL'
$shutdownCode = Invoke-WslRun '--shutdown'
if ($shutdownCode -ne 0) { warn "wsl --shutdown exited $shutdownCode" }
Start-DockerDesktop | Out-Null
}

# --- Actions ---

function Show-List {
Expand Down Expand Up @@ -588,17 +701,6 @@ function Invoke-ToolApply {
return
}

# Docker Desktop needs WSL2 already present and current, and does not install or update it itself.
# A WSL gap has nothing to do with any other tool in this run, so it is collected here on the same rule as a failed install rather than ending the whole run.
if ($ToolName -eq 'docker') {
$wslProblem = Test-WslReadyForDocker
if ($wslProblem) {
warn "docker skipped, $wslProblem"
$script:FAILED += $ToolName
return
}
}

# Naming a scope the installed copy does not sit in would add a second copy beside it, so the removal is asked for rather than done on the way past.
if ($script:WANT_SCOPE -and $state.Rows.Count -gt 0 -and $state.Scope.Count -gt 0 -and
$state.Scope -notcontains $script:WANT_SCOPE -and $script:MODE -ne 'reinstall') {
Expand Down Expand Up @@ -643,6 +745,25 @@ function Invoke-ToolApply {
return
}

# Reached only once real work is about to happen: every "leave it alone" status above already returned.
# That is deliberate here, not incidental: an already current docker with a stale WSL platform has nothing this run needs to fix, and gating on WSL readiness any earlier would skip a tool that was never going to change anyway.
$dockerMaintenance = $null
if ($ToolName -eq 'docker') {
$wasRunning = Test-DockerDesktopRunning
$wslProblem = Test-WslReadyForDocker
if ($wslProblem -and -not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
warn "docker skipped, $wslProblem"
$script:FAILED += $ToolName
return
}
$dockerMaintenance = Enter-DockerMaintenance -WasRunning $wasRunning -WslProblem $wslProblem
if (-not $dockerMaintenance.Proceed) {
warn "docker skipped, $wslProblem"
$script:FAILED += $ToolName
return
}
}

if ($script:MODE -ne 'reinstall') {
log "${ToolName}: $($state.Status)$(if ($state.Available) { ", the source carries $($state.Available)" })"
}
Expand All @@ -666,6 +787,8 @@ function Invoke-ToolApply {
info 'Close whatever is running it, then run this again'
}
$script:FAILED += $ToolName
# Owed regardless of how this returns: Enter-DockerMaintenance stopped Docker Desktop for this attempt, and a failed install is not a reason to leave it down.
if ($dockerMaintenance) { Exit-DockerMaintenance -Stopped $dockerMaintenance.Stopped }
return
}
}
Expand All @@ -676,6 +799,7 @@ function Invoke-ToolApply {
$after = if ($now) { $now } else { '-' }
$script:CHANGED += "$ToolName $before -> $after"
}
if ($dockerMaintenance) { Exit-DockerMaintenance -Stopped $dockerMaintenance.Stopped }
}

function Invoke-Apply {
Expand Down