Vaporup.Wsl.psm1
|
function Get-WslDistroInstallState { <# .SYNOPSIS Lists all WSL distros available online (wsl -l -o) with their installed state. .EXAMPLE Get-WslDistroInstallState .EXAMPLE Get-WslDistroInstallState -Missing .EXAMPLE Get-WslDistroInstallState -Missing | ForEach-Object { wsl --install $_.Name } #> [CmdletBinding()] param( # Only return distros that are NOT installed [switch]$Missing ) #$env:WSL_UTF8=1 # Installed distro names (registered names) $installed = @(wsl --list --quiet | ForEach-Object { $_.Trim() } | Where-Object { $_ }) # Available distros: lines of "NAME FRIENDLY NAME" $online = wsl --list --online | ForEach-Object { if ($_ -match '^\s*(\S+)\s{2,}(.+?)\s*$' -and $Matches[1] -ne 'NAME') { [pscustomobject]@{ Name = $Matches[1] FriendlyName = $Matches[2] } } } $result = foreach ($d in $online) { [pscustomobject]@{ Name = $d.Name FriendlyName = $d.FriendlyName Installed = $installed -contains $d.Name # -contains is case-insensitive } } if ($Missing) { $result | Where-Object { -not $_.Installed } } else { $result } } function Update-WslDistroPackages { <# .SYNOPSIS Updates all packages in your WSL instances .DESCRIPTION The script loops over all your WSL instances and calls the distribution-specific package manager to upgrade it Environment Variables: HTTP_PROXY : http://[username:passwort@]host:port HTTPS_PROXY : https://[username:passwort@]host:port If not set, defaults are used .NOTES Author: Sven Wick <sven.wick@gmx.de> Using Out-Default in this script to also capture WSL output for Start-Transcript https://stackoverflow.com/questions/13160759/start-transcript-not-capturing-all-output-to-log-file https://github.com/PowerShell/PowerShell/issues/10994 #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [string[]]$Instance, [string]$Proxy, [string]$CheckUrl = "https://www.google.com", [string]$LogDirName = "Vaporup.Wsl", [string]$LogFileName = "Update-WslDistroPackages.log", [switch]$Force ) # # Defaults # # Make sure we use UTF-8 so we can properly parse the output from WSL # https://stackoverflow.com/questions/67746179/how-do-i-match-on-wsl-output-in-powershell # WSL produces UTF-16LE-encoded output by default # instead of respecting the console's active code page $SavedEncoding = [Console]::OutputEncoding #$env:WSL_UTF8=1 # # Logging # $log_dir = "$env:LOCALAPPDATA\$LogDirName" $log_name = "$LogFileName" $log_file = Join-Path $log_dir $log_name $log_max_size_mb = 5 # # System Proxy # $system_proxy = Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" if ($system_proxy.ProxyEnable -eq 1) { $proxy = $system_proxy.ProxyServer } # # Proxy via ENV vars # if ( $env:HTTP_PROXY ) { $proxy = $env:HTTP_PROXY } if ( $env:HTTPS_PROXY ) { $proxy = $env:HTTPS_PROXY } if ( $Proxy ) { $proxy = $Proxy } else { $proxy = "no proxy" } $proxy_uri = [System.Uri]$proxy #$proxy_protocol = $proxy_uri.Scheme $proxy_host = $proxy_uri.Host $proxy_port = $proxy_uri.Port $proxy_userInfo = $proxy_uri.UserInfo # Split UserInfo into username and password $proxy_username, $proxy_password = $proxy_userInfo -split ":", 2 # # Commands # $proxy_cmd = "export http_proxy=""$proxy"" ; export https_proxy=""$proxy""" $apk_cmd = "apk upgrade --no-interactive" $apk_cmd_proxy = "$proxy_cmd ; $apk_cmd" $zypper_cmd = "zypper update -y" $zypper_cmd_proxy = "$proxy_cmd ; $zypper_cmd" $pacman_cmd = "pacman -Syu --noconfirm" $pacman_cmd_proxy = "$proxy_cmd ; $pacman_cmd" # https://stackoverflow.com/questions/33370297/apt-get-update-non-interactive $apt_cmd = "export DEBIAN_FRONTEND=noninteractive ; apt-get update ; apt-get -y dist-upgrade; apt-get -y autoremove" $apt_cmd_proxy = "$proxy_cmd ; $apt_cmd" $dnf_cmd = "dnf upgrade -y" $dnf_cmd_proxy = "$proxy_cmd ; $dnf_cmd" $dnf_or_yum_cmd = "command -v dnf > /dev/null && dnf upgrade -y ; command -v yum > /dev/null && yum -y update" $dnf_or_yum_cmd_proxy = "$proxy_cmd ; $dnf_or_yum_cmd" # # Functions # function CheckInternetConnection { # # Try direct # Add-Type -AssemblyName "System.Net.Http" $handler = New-Object System.Net.Http.HttpClientHandler $handler.UseProxy = $false $httpClient = New-Object System.Net.Http.HttpClient($handler) $response = $httpClient.GetAsync($CheckUrl).Result if ($response.IsSuccessStatusCode) { return "direct" } # # Try Proxy # # but only when configured if ($proxy_host -and $proxy_port) { $connect = Test-NetConnection -ComputerName $proxy_host -Port $proxy_port -WarningAction:SilentlyContinue if ($connect.TcpTestSucceeded) { return "proxy" } } else { return "failed" } # # No internet connection possible # return "failed" } function Write-TerminalLine { param( [string]$Character = '-', [int]$FallbackWidth = 120 ) # Get terminal width, fallback if running headlessly $width = try { [Console]::WindowWidth } catch { $FallbackWidth } # Print the line $Character * $width } # # Locking # $mutexName = "Global\Update-WslDistroPackages" $mutex = New-Object System.Threading.Mutex($false, $mutexName) if (-not $mutex.WaitOne(0, $false)) { Write-Warning "Another update process is already running." Return } # # Main # try { Write-Output "" # Use a temporary transcript file $shortId = ([guid]::NewGuid().Guid -replace '-','').Substring(0, 10) $tempTranscript = Join-Path $env:TEMP ("Update-WslDistroPackages_" + $shortId + ".log") # Start the transcript Start-Transcript -Path $tempTranscript | Out-Null # Ensure log directory exists if (-not (Test-Path $log_dir)) { New-Item -ItemType Directory -Path $log_dir | Out-Null } # Start new logfile when it exceeds max size if (Test-Path $log_file) { $logSizeMB = (Get-Item $log_file).Length / 1MB if ($logSizeMB -ge $log_max_size_mb) { Remove-Item $log_file -Force } } # # Output configuration # Write-Output "Transcript : $tempTranscript" Write-Output "Logfile : $log_file" # # Return early if there is no WSL # $wslServiceFound = $false $wslServices = Get-Service "WSLService" -ErrorAction SilentlyContinue if ( $wslServices.Count -gt 0 ) { $wslServiceFound = $true } if (-not $wslServiceFound ) { Write-Warning "No WSLService found." Return } if (-not (Test-Path "C:\Windows\System32\wsl.exe")) { Write-Warning "WSL is not installed." Return } # # Get the list of WSL instances # $wslInstances = @() $allInstances = wsl --list --quiet | Sort-Object # # Return early if WSL throws an error # if ($LASTEXITCODE -ne 0) { Write-Warning "Finding WSL instances failed with exit code $LASTEXITCODE" Return } # # Return early if no instances found # if (-not $allInstances) { Write-Warning "Could not find any WSL instances" Return } # # Check if there was supplied a subset of instances # If not, update all that were found # if ($Instance) { foreach ($name in $Instance) { if ($allInstances -contains $name) { $wslInstances = $wslInstances + $name } else { Write-Warning "Instance '$name' not found." } } } else { $wslInstances = $allInstances } Write-Output "" Write-Output "The following instances will be updated:" Write-Output "" $wslInstances | ForEach-Object { " * $_" } if (-not ($Force -or $PSCmdlet.ShouldProcess("WSL Instance(s)", "Update"))) { Write-Warning "Update cancelled." return } Write-Output "" Write-Warning "Setting Console Output to UTF8" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # # Detect Internet Connection # Write-Output "" Write-Output "Using $CheckUrl for checking direct internet connection with $proxy as fallback" Write-Output "Checking Internet Connection..." $connection = CheckInternetConnection if ($connection -like "failed") { Write-Warning "No Internet Connection available" Return } Write-Output "" Write-Output "CONNECTION : $connection" # # Updating # $counter = 0 foreach ($wslInstance in $wslInstances) { if ($wslInstance -eq "") { continue } $counter = $counter + 1 $distroID = "unknown" $pkgManager = "unknown" try { $distroInfo = wsl -u root -d $wslInstance cat /etc/os-release $distroID = ($distroInfo | Select-String -Pattern "^ID=").Line.Split('=')[1].Trim().Trim('"') } catch { $distroID = "unknown" } # # Check special instances like "docker-desktop" (alpine) # if ($distroID -eq "unknown") { try { $distroID = wsl -u root -d $wslInstance sh -c "test -f /etc/alpine-release && echo alpine" if (-not $distroID) { $distroID = "unknown" } } catch { $distroID = "unknown" } } switch -Wildcard ($distroID) { "*unknown*" { $pkgManager = "unknown" } "*alpine*" { $pkgManager = "apk" } "*debian*" { $pkgManager = "apt" } "*ubuntu*" { $pkgManager = "apt" } "*kali*" { $pkgManager = "apt" } "*elxr*" { $pkgManager = "apt" } "*suse*" { $pkgManager = "zypper" } "*sles*" { $pkgManager = "zypper" } "*fedora*" { $pkgManager = "dnf" } "*alma*" { $pkgManager = "dnf" } "*rocky*" { $pkgManager = "dnf" } "*arch*" { $pkgManager = "pacman" } } # Oracle Linux (Old versions still use yum) if ($distroID -eq "ol") { $pkgManager = "dnf_or_yum" } # +----------+ # | UPDATING | # +----------+ Write-Output "" Write-TerminalLine Write-Output "INSTANCE_NUMBER : ${counter}/$($wslInstances.Count)" Write-Output "INSTANCE_NAME : $wslInstance" Write-Output "DISTRO_ID : $distroID" Write-Output "PACKAGE_MANAGER : $pkgManager" # # Unknown # if ($pkgManager -eq "unknown") { Write-TerminalLine Write-Output "" Write-Warning "Unknown how to update. Skipping $wslInstance" continue } switch ($pkgManager) { "dnf_or_yum" { $update_cmd = $dnf_or_yum_cmd if ($connection -like "proxy") { $update_cmd = $dnf_or_yum_cmd_proxy } } "dnf" { $update_cmd = $dnf_cmd if ($connection -like "proxy") { $update_cmd = $dnf_cmd_proxy } } "apk" { $update_cmd = $apk_cmd if ($connection -like "proxy") { $update_cmd = $apk_cmd_proxy } } "apt" { $update_cmd = $apt_cmd if ($connection -like "proxy") { $update_cmd = $apt_cmd_proxy } } "pacman" { $update_cmd = $pacman_cmd if ($connection -like "proxy") { $update_cmd = $pacman_cmd_proxy } } "zypper" { $update_cmd = $zypper_cmd if ($connection -like "proxy") { $update_cmd = $zypper_cmd_proxy } } } Write-Output "UPDATE_COMMAND : $update_cmd" Write-TerminalLine Write-Output "" wsl -u root -d $wslInstance sh -c $update_cmd | Out-Default continue } Write-Output "" Write-TerminalLine "=" Write-Output "FINISHED..." Write-Output "" Write-Output "Logfile: $log_file" Write-Output "" Stop-Transcript | Out-Null Write-Output "" # Append temp transcript to daily log and then remove it if (Test-Path $tempTranscript) { Get-Content $tempTranscript | Add-Content -Path $log_file -Encoding UTF8 Remove-Item $tempTranscript -Force } } finally { Write-Warning "Setting Console Output back to Default" [Console]::OutputEncoding = $SavedEncoding #[Console]::OutputEncoding = [System.Text.Encoding]::Default try { Stop-Transcript | Out-Null } catch { } # Append temp transcript to daily log and then remove it if (Test-Path $tempTranscript) { Get-Content $tempTranscript | Add-Content -Path $log_file -Encoding UTF8 Remove-Item $tempTranscript -Force } $mutex.ReleaseMutex() $mutex.Dispose() } } # # TAB Completion for $Instance # # Make sure we use UTF-8 so we can properly parse the output from WSL # https://stackoverflow.com/questions/67746179/how-do-i-match-on-wsl-output-in-powershell # WSL produces UTF-16LE-encoded output by default # instead of respecting the console's active code page $env:WSL_UTF8=1 Register-ArgumentCompleter -CommandName Update-WslDistroPackages -ParameterName Instance -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) wsl.exe --list --quiet | ForEach-Object { if ($_ -like "$wordToComplete*") { Write-Output "$_" } } } |