lib/Docker.ps1
|
function Get-DockerDesktopWslHomeConfig { <# .SYNOPSIS Reads DockerDesktop.WslHome from the script's config file. Returns $null when the key is absent. #> [CmdletBinding()] param( [Parameter()] [string] $ConfigFile = $script:ConfigFilePath ) if (-not $ConfigFile -or -not (Test-Path -Path $ConfigFile -PathType Leaf)) { return $null } try { $raw = Get-Content -Path $ConfigFile -Raw -Encoding UTF8 $parsed = $raw | ConvertFrom-Json -ErrorAction Stop } catch { throw "Failed to parse '$ConfigFile': $($_.Exception.Message)" } if ($null -eq $parsed) { return $null } if (-not $parsed.PSObject.Properties['DockerDesktop']) { return $null } if (-not $parsed.DockerDesktop.PSObject.Properties['WslHome']) { return $null } $value = [string]$parsed.DockerDesktop.WslHome if ([string]::IsNullOrWhiteSpace($value)) { return $null } return $value } function Get-DockerSettingsStorePath { <# .SYNOPSIS Resolves Docker Desktop's settings file. Modern versions (~4.30+) use settings-store.json; older ones used settings.json. Returns $null when neither exists - typically because Docker Desktop isn't installed yet. #> [CmdletBinding()] param() if (-not $env:APPDATA) { return $null } $base = Join-Path $env:APPDATA 'Docker' foreach ($name in 'settings-store.json', 'settings.json') { $p = Join-Path $base $name if (Test-Path -Path $p -PathType Leaf) { return $p } } return $null } function Get-DockerCustomWslDistroDir { <# .SYNOPSIS Returns the value of the CustomWslDistroDir property from Docker Desktop's settings file, or $null when the key is absent. #> [CmdletBinding()] param( [Parameter()] [string] $SettingsPath ) if (-not $SettingsPath) { $SettingsPath = Get-DockerSettingsStorePath } if (-not $SettingsPath -or -not (Test-Path -Path $SettingsPath -PathType Leaf)) { return $null } try { $raw = Get-Content -Path $SettingsPath -Raw -Encoding UTF8 $parsed = $raw | ConvertFrom-Json -ErrorAction Stop } catch { throw "Failed to parse '$SettingsPath': $($_.Exception.Message)" } if ($null -eq $parsed) { return $null } if (-not $parsed.PSObject.Properties['CustomWslDistroDir']) { return $null } return [string]$parsed.CustomWslDistroDir } function Set-DockerCustomWslDistroDir { <# .SYNOPSIS Surgically replaces the CustomWslDistroDir value in Docker's settings file via regex. Avoids parse+reserialize so Docker Desktop's own JSON formatting (key order, indentation, comments if any) is preserved untouched. Throws if the key isn't present in the file (we don't want to silently add unknown keys to a Docker-managed settings file). #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory)] [string] $NewValue, [Parameter()] [string] $SettingsPath ) if (-not $SettingsPath) { $SettingsPath = Get-DockerSettingsStorePath } if (-not $SettingsPath -or -not (Test-Path -Path $SettingsPath -PathType Leaf)) { throw 'Docker settings file not found. Is Docker Desktop installed?' } $content = Get-Content -Path $SettingsPath -Raw -Encoding UTF8 # JSON-escape backslashes for the replacement string. $jsonEscaped = $NewValue.Replace('\', '\\') $pattern = '("CustomWslDistroDir"\s*:\s*)"[^"]*"' $replacement = "`$1`"$jsonEscaped`"" $newContent = [System.Text.RegularExpressions.Regex]::Replace($content, $pattern, $replacement) if ($newContent -eq $content) { throw "Could not find CustomWslDistroDir key in '$SettingsPath'." } if ($PSCmdlet.ShouldProcess($SettingsPath, "Set CustomWslDistroDir to '$NewValue'")) { # UTF-8 without BOM; write atomically via WriteAllText. [System.IO.File]::WriteAllText( $SettingsPath, $newContent, [System.Text.UTF8Encoding]::new($false) ) } } function Get-DockerDesktopInstallPath { <# .SYNOPSIS Locates Docker Desktop.exe. Returns the absolute path if found, $null otherwise. Searches the standard install location and the App Paths registry as a fallback. #> [CmdletBinding()] param() $candidates = New-Object System.Collections.Generic.List[string] [void]$candidates.Add('C:\Program Files\Docker\Docker\Docker Desktop.exe') if ($env:ProgramFiles) { [void]$candidates.Add((Join-Path $env:ProgramFiles 'Docker\Docker\Docker Desktop.exe')) } foreach ($c in $candidates | Select-Object -Unique) { if (Test-Path -LiteralPath $c -PathType Leaf) { return $c } } $appPaths = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Docker Desktop.exe' if (Test-Path -Path $appPaths) { $val = (Get-ItemProperty -Path $appPaths -ErrorAction SilentlyContinue).'(default)' if ($val -and (Test-Path -LiteralPath $val -PathType Leaf)) { return [string]$val } } return $null } function Wait-DockerDesktopReady { <# .SYNOPSIS Polls until Docker Desktop's first-run state is in place: both %APPDATA%\Docker\settings-store.json exists AND the docker-desktop WSL distro is registered. Returns $true on success, $false if the deadline elapses without both artifacts. Emits a one-line progress message every -ProgressIntervalSeconds so callers (and CI logs) can see what's still missing. #> [CmdletBinding()] param( [int] $TimeoutSeconds = 180, [int] $PollIntervalSeconds = 5, [int] $ProgressIntervalSeconds = 30 ) $start = Get-Date $deadline = $start.AddSeconds($TimeoutSeconds) $nextProgress = $start.AddSeconds($ProgressIntervalSeconds) while ((Get-Date) -lt $deadline) { $settings = Get-DockerSettingsStorePath $base = Get-WslDistroBasePath -Name 'docker-desktop' if ($settings -and $base) { return $true } $now = Get-Date if ($now -ge $nextProgress) { $elapsed = [int]($now - $start).TotalSeconds $sFlag = if ($settings) { 'yes' } else { 'no' } $dFlag = if ($base) { 'yes' } else { 'no' } Write-Status -Level Debug -Message (" [DEBUG] t+{0,4}s settings={1} docker-desktop-distro={2}" -f $elapsed, $sFlag, $dFlag) $nextProgress = $now.AddSeconds($ProgressIntervalSeconds) } Start-Sleep -Seconds $PollIntervalSeconds } return $false } function Start-DockerDesktopAndWait { <# .SYNOPSIS Launches Docker Desktop and waits for it to write its settings file and register the docker-desktop WSL distro. Required before the WSL data move can proceed - those artifacts only appear after Docker Desktop's first run. Throws if Docker Desktop isn't installed, or if it fails to bootstrap within the timeout. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [int] $TimeoutSeconds = 180 ) $exe = Get-DockerDesktopInstallPath if (-not $exe) { throw 'Docker Desktop is not installed.' } if (-not $PSCmdlet.ShouldProcess('Docker Desktop', 'Start and wait for first-run bootstrap')) { return } # Docker Desktop needs WSL to be installed and the kernel current # before it can register its docker-desktop distro on first launch. # That setup is owned by the Wsl task (tasks/60-Wsl.ps1), which # always runs before DockerDesktop in -Task All. If a caller invokes # DockerDesktop on its own without WSL ready, the wait below will # time out with a useful diagnostic dump. Write-Status -Level Info -Message ' [INFO] Starting Docker Desktop (first-run bootstrap)...' # --accept-license avoids the interactive EULA dialog blocking on # headless / CI machines. Harmless on a developer machine that's # already accepted. Start-Process -FilePath $exe -ArgumentList '--accept-license' -ErrorAction Stop Write-Status -Level Info -Message " [INFO] Waiting for settings-store.json and docker-desktop distro (timeout ${TimeoutSeconds}s)..." if (-not (Wait-DockerDesktopReady -TimeoutSeconds $TimeoutSeconds)) { Write-DockerBootstrapDiagnostics throw ("Docker Desktop did not finish bootstrapping within {0} seconds. " + "Launch it manually once and re-run the script.") -f $TimeoutSeconds } Write-Status -Level Info -Message ' [INFO] Docker Desktop bootstrapped.' } function Write-DockerBootstrapDiagnostics { <# .SYNOPSIS Dumps everything the user / CI log reader needs to figure out why Docker Desktop's first-run bootstrap stalled. Called from Start-DockerDesktopAndWait when Wait-DockerDesktopReady times out. Never throws - diagnostics are best-effort. #> [CmdletBinding()] param() Write-Status -Level Warn -Message ' [WARN] Docker Desktop bootstrap timeout - dumping diagnostics:' Write-Status -Level Debug -Message ' [DEBUG] settings-store.json:' $settingsPath = Join-Path $env:APPDATA 'Docker\settings-store.json' if (Test-Path -LiteralPath $settingsPath) { Get-Content -LiteralPath $settingsPath -Raw -ErrorAction SilentlyContinue | ForEach-Object { ($_ -split "`n") } | ForEach-Object { Write-Information " $_" } } else { Write-Information " <not present at $settingsPath>" } Write-Status -Level Debug -Message ' [DEBUG] wsl --list --verbose:' & wsl.exe --list --verbose 2>&1 | ForEach-Object { Write-Information " $_" } Write-Status -Level Info -Message ' [INFO] Diagnostic - running Docker processes:' $procs = Get-Process -Name 'Docker*','com.docker.*' -ErrorAction SilentlyContinue if ($procs) { $procs | Select-Object Id, Name, StartTime | Format-Table -AutoSize | Out-String -Stream | ForEach-Object { Write-Information " $_" } } else { Write-Information ' <none>' } Write-Status -Level Info -Message ' [INFO] Diagnostic - Docker-related Windows services:' Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'docker|com\.docker' -or $_.DisplayName -match 'Docker' } | Select-Object Name, Status, StartType | Format-Table -AutoSize | Out-String -Stream | ForEach-Object { Write-Information " $_" } } function Stop-DockerDesktopProcesses { <# .SYNOPSIS Stops Docker Desktop and its background helpers, then runs `wsl --shutdown` so the docker-desktop distro is in a movable state. Idempotent - silently no-ops for processes that aren't running. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param() $names = @( 'Docker Desktop', 'com.docker.backend', 'com.docker.dev-envs', 'com.docker.build', 'com.docker.cli' ) foreach ($n in $names) { $procs = Get-Process -Name $n -ErrorAction SilentlyContinue if ($procs -and $PSCmdlet.ShouldProcess($n, 'Stop-Process')) { Write-Status -Level Info -Message " [INFO] Stopping $n" $procs | Stop-Process -Force -ErrorAction SilentlyContinue } } # Give Docker's watchdog a moment to settle before WSL shutdown. if (-not $WhatIfPreference) { Start-Sleep -Seconds 2 } if ($PSCmdlet.ShouldProcess('WSL', 'wsl --shutdown')) { Write-Status -Level Info -Message ' [INFO] Shutting down WSL...' wsl --shutdown 2>&1 | Format-ToolOutput } } |