From 6c9e4b35b2c2e5e5a1b58c57392cae9e99922889 Mon Sep 17 00:00:00 2001 From: Vasiliy Kovalchuk Date: Sun, 3 Feb 2019 17:59:16 +0300 Subject: [PATCH 1/2] Fix bug in `Reboot-AndResume`, add new params, etc. Fixes: - Fix bug in `Reboot-AndResume`: if spaces were contained in `$script_path`, then script wouldn't restart after rebooting. - Fix error throwing in `Reboot-Resume`: it's not an error that user may not want to reboot immediately, so we should exit normally without throwing an error. - Fix comparison against `$null` [1] Features: - Suppress all interactions with user by adding new parameter `-Force` and restarting computer automatically - User now could specify domain by using new `-Domain` parameter Changes: - Refactor tests for whether or not to install .NET. If `Get-ItemProperty` couldn't find specified item, then `$dotnet_version` will be `$null`, and it's property `Release` as well. `$null` casts to `0` in comparisons against integers, so expression `$dotnet_version.Release -lt 379893` always resulted in `$true` - Use `$PSHOME` instead of hardcoded path - Replace `| Out-Null` with `> $null` for consistency - Use `[uri]` type for download URLs for more elegant and robust way for filename extraction from them. - Switch to `Join-Path` for paths concatenation. - Generalize behavior of enabling and disabling AutoLogon feature with a new `Set-AutoLogon` function which expects one of the two parameters: - `-Enable` for enabling auto logon with specified `$sctipt:username`, `$sctipt:domain`, and `$sctipt:password` - `-Disable` for disabling auto logon `Set-AutoLogon` DOES NOT check if credentials ($Username, etc.) were provided to script at all, as it's a caller responsibility. - Refactor `Download-Wmf5Server2008` logic: `Download-Wmf5Server2008` is only called on systems with WFM3 and .NET 4.5.2 already installed, so it's safe to presume that we don't need a fallback to legacy `Shell.Application` unzipping functionality, especially as it is full of pitfalls like asynchronous nature [2]. [1] https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md#ensure-null-is-on-left-side-of-comparisons [2] https://social.technet.microsoft.com/Forums/en-US/6988d856-09ae-41c5-aa79-3d78a9e4d03a/powershell-use-shellapplication-to-zip-files?forum=ITCG --- scripts/Upgrade-PowerShell.ps1 | 156 ++++++++++++++++----------------- 1 file changed, 76 insertions(+), 80 deletions(-) diff --git a/scripts/Upgrade-PowerShell.ps1 b/scripts/Upgrade-PowerShell.ps1 index e217535..a9c0e4d 100644 --- a/scripts/Upgrade-PowerShell.ps1 +++ b/scripts/Upgrade-PowerShell.ps1 @@ -47,9 +47,8 @@ # require the user to log back in manually after the reboot before # continuing. # -# A log of this process is created in -# $env:SystemDrive\temp\upgrade_powershell.log which is usually C:\temp\. This -# log can used to see how the script faired after an automatic reboot. +# A log of this process is created in '$env:temp\upgrade_powershell.log'. +# This log can be used to see how did the script worked after an automatic reboot. # # See https://github.com/jborean93/ansible-windows/tree/master/scripts for more # details. @@ -64,11 +63,15 @@ # [string] - The username of a local admin user that will be automatically # logged in after a reboot to continue the script install. The 'password' # parameter is also required if this is set. +# .PARAMETER domain +# [string] - fully qualified domain name (FQDN) of the computer domain. # .PARAMETER password # [string] - The password for 'username', this is required if the 'username' # parameter is also set. # .PARAMETER Verbose # [switch] - Whether to display Verbose logs on the console +# .PARAMETER force +# [switch] - Forces script to reboot automatically without user confirmation # .EXAMPLE # # upgrade from powershell 1.0 to 3.0 with automatic login and reboots # Set-ExecutionPolicy Unrestricted -Force @@ -83,9 +86,12 @@ Param( [string]$version = "5.1", [string]$username, + [string]$domain, [string]$password, - [switch]$verbose = $false + [switch]$verbose = $false, + [switch]$force ) + $ErrorActionPreference = 'Stop' if ($verbose) { $VerbosePreference = "Continue" @@ -100,7 +106,7 @@ Function Write-Log($message, $level="INFO") { # Poor man's implementation of Log4Net $date_stamp = Get-Date -Format s $log_entry = "$date_stamp - $level - $message" - $log_file = "$tmp_dir\upgrade_powershell.log" + $log_file = Join-Path -Path "$tmp_dir" -ChildPath 'upgrade_powershell.log' Write-Verbose -Message $log_entry Add-Content -Path $log_file -Value $log_entry } @@ -108,7 +114,7 @@ Function Write-Log($message, $level="INFO") { Function Reboot-AndResume { Write-Log -message "adding script to run on next logon" $script_path = $script:MyInvocation.MyCommand.Path - $ps_path = "$env:SystemDrive\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + $ps_path = Join-Path -Path "$PSHOME" -ChildPath 'powershell.exe' $arguments = "-version $version" if ($username -and $password) { $arguments = "$arguments -username `"$username`" -password `"$password`"" @@ -117,27 +123,23 @@ Function Reboot-AndResume { $arguments = "$arguments -Verbose" } - $command = "$ps_path -ExecutionPolicy ByPass -File $script_path $arguments" + $command = "$ps_path -ExecutionPolicy ByPass -File `"$script_path`" $arguments" $reg_key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" $reg_property_name = "ps-upgrade" Set-ItemProperty -Path $reg_key -Name $reg_property_name -Value $command if ($username -and $password) { - $reg_winlogon_path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" - Set-ItemProperty -Path $reg_winlogon_path -Name AutoAdminLogon -Value 1 - Set-ItemProperty -Path $reg_winlogon_path -Name DefaultUserName -Value $username - Set-ItemProperty -Path $reg_winlogon_path -Name DefaultPassword -Value $password - Write-Log -message "rebooting server to continue powershell upgrade" - } else { - Write-Log -message "need to reboot server to continue powershell upgrade" + Set-AutoLogon -Enable + } elseif ($null -eq $Force) { $reboot_confirmation = Read-Host -Prompt "need to reboot server to continue powershell upgrade, do you wish to proceed (y/n)" if ($reboot_confirmation -ne "y") { - $error_msg = "please reboot server manually and login to continue upgrade process, the script will restart on the next login automatically" - Write-Log -message $error_msg -level "ERROR" - throw $error_msg + $msg = "please reboot server manually and login to continue upgrade process, the script will restart on the next login automatically" + Write-Log -Message $msg + exit 0 } } - + + Write-Log -Message 'rebooting server to continue powershell upgrade' if (Get-Command -Name Restart-Computer -ErrorAction SilentlyContinue) { Restart-Computer -Force } else { @@ -152,9 +154,9 @@ Function Run-Process($executable, $arguments) { $psi.FileName = $executable $psi.Arguments = $arguments Write-Log -message "starting new process '$executable $arguments'" - $process.Start() | Out-Null + $process.Start() > $null - $process.WaitForExit() | Out-Null + $process.WaitForExit() > $null $exit_code = $process.ExitCode Write-Log -message "process completed with exit code '$exit_code'" @@ -167,62 +169,68 @@ Function Download-File($url, $path) { $client.DownloadFile($url, $path) } -Function Clear-AutoLogon { - $reg_winlogon_path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" - Write-Log -message "clearing auto logon registry properties" - Set-ItemProperty -Path $reg_winlogon_path -Name AutoAdminLogon -Value 0 - Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultUserName -ErrorAction SilentlyContinue - Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultPassword -ErrorAction SilentlyContinue +Function Set-AutoLogon { + param ( + [switch] $Enable, + [switch] $Disable + ) + $reg_winlogon_path = 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon' + + if ($Enable) { + Write-Log -Message 'setting auto logon registry properties' + Set-ItemProperty -Path $reg_winlogon_path -Name AutoAdminLogon -Value 1 + Set-ItemProperty -Path $reg_winlogon_path -Name DefaultUserName -Value $username + Set-ItemProperty -Path $reg_winlogon_path -Name DefaultPassword -Value $password + if ($domain) { + Set-ItemProperty -Path $reg_winlogon_path -Name DefaultDomain -Value $domain + } + } elseif ($Disable) { + Write-Log -Message 'clearing auto logon registry properties' + Set-ItemProperty -Path $reg_winlogon_path -Name AutoAdminLogon -Value 0 + Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultUserName -ErrorAction SilentlyContinue + Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultPassword -ErrorAction SilentlyContinue + Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultDomain -ErrorAction SilentlyContinue + } else { + $error_msg = 'ambiguous calling of Set-Autolon: expecting a parameter -Enable or -Disable, none has been provided' + Write-Log -Message $error_msg -Level 'ERROR' + throw $error_msg + } } Function Download-Wmf5Server2008($architecture) { if ($architecture -eq "x64") { - $zip_url = "http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win7AndW2K8R2-KB3191566-x64.zip" - $file = "$tmp_dir\Win7AndW2K8R2-KB3191566-x64.msu" + $zip_url = [uri]"http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win7AndW2K8R2-KB3191566-x64.zip" } else { - $zip_url = "http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win7-KB3191566-x86.zip" - $file = "$tmp_dir\Win7-KB3191566-x86.msu" + $zip_url = [uri]"http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win7-KB3191566-x86.zip" } + $filename = $zip_url.Segments[-1] + $file = Join-Path -Path "$tmp_dir" -ChildPath $filename.Replace('.zip', '.msu') if (Test-Path -Path $file) { return $file } - $filename = $zip_url.Split("/")[-1] - $zip_file = "$tmp_dir\$filename" + $zip_file = Join-Path -Path "$tmp_dir" -ChildPath "$filename" Download-File -url $zip_url -path $zip_file Write-Log -message "extracting '$zip_file' to '$tmp_dir'" - try { - Add-Type -AssemblyName System.IO.Compression.FileSystem > $null - $legacy = $false - } catch { - $legacy = $true - } - - if ($legacy) { - $shell = New-Object -ComObject Shell.Application - $zip_src = $shell.NameSpace($zip_file) - $zip_dest = $shell.NameSpace($tmp_dir) - $zip_dest.CopyHere($zip_src.Items(), 1044) - } else { - [System.IO.Compression.ZipFile]::ExtractToDirectory($zip_file, $tmp_dir) - } + Add-Type -AssemblyName System.IO.Compression.FileSystem > $null + [System.IO.Compression.ZipFile]::ExtractToDirectory($zip_file, $tmp_dir) return $file } Write-Log -message "starting script" # on PS v1.0, upgrade to 2.0 and then run the script again -if ($PSVersionTable -eq $null) { +if ($null -eq $PSVersionTable) { Write-Log -message "upgrading powershell v1.0 to v2.0" $architecture = $env:PROCESSOR_ARCHITECTURE if ($architecture -eq "AMD64") { - $url = "https://download.microsoft.com/download/2/8/6/28686477-3242-4E96-9009-30B16BED89AF/Windows6.0-KB968930-x64.msu" + $url = [uri]"https://download.microsoft.com/download/2/8/6/28686477-3242-4E96-9009-30B16BED89AF/Windows6.0-KB968930-x64.msu" } else { - $url = "https://download.microsoft.com/download/F/9/E/F9EF6ACB-2BA8-4845-9C10-85FC4A69B207/Windows6.0-KB968930-x86.msu" + $url = [uri]"https://download.microsoft.com/download/F/9/E/F9EF6ACB-2BA8-4845-9C10-85FC4A69B207/Windows6.0-KB968930-x86.msu" } - $filename = $url.Split("/")[-1] - $file = "$tmp_dir\$filename" + $filename = $url.Segments[-1] + $file = Join-Path -Path "$tmp_dir" -ChildPath "$filename" Download-File -url $url -path $file $exit_code = Run-Process -executable $file -arguments "/quiet /norestart" if ($exit_code -ne 0 -and $exit_code -ne 3010) { @@ -237,7 +245,7 @@ if ($PSVersionTable -eq $null) { $current_ps_version = [version]"$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)" if ($current_ps_version -eq [version]$version) { Write-Log -message "current and target PS version are the same, no action is required" - Clear-AutoLogon + Set-AutoLogon -Disable exit 0 } @@ -289,20 +297,8 @@ switch ($version) { # detect if .NET 4.5.2 is not installed and add to the actions $dotnet_path = "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -if (-not (Test-Path -Path $dotnet_path)) { - $dotnet_upgrade_needed = $true -} else { - $dotnet_version = Get-ItemProperty -Path $dotnet_path -Name Release -ErrorAction SilentlyContinue - if ($dotnet_version) { - # 379893 == 4.5.2 - if ($dotnet_version.Release -lt 379893) { - $dotnet_upgrade_needed = $true - } - } else { - $dotnet_upgrade_needed = $true - } -} -if ($dotnet_upgrade_needed) { +$dotnet_version = Get-ItemProperty -Path $dotnet_path -Name Release -ErrorAction SilentlyContinue +if ($dotnet_version.Release -lt 379893) { $actions = @("dotnet") + $actions } @@ -315,7 +311,7 @@ foreach ($action in $actions) { switch ($action) { "dotnet" { Write-Log -message "running .NET update to 4.5.2" - $url = "https://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe" + $url = [uri]"https://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe" $error_msg = "failed to update .NET to 4.5.2" $arguments = "/q /norestart" break @@ -334,9 +330,9 @@ foreach ($action in $actions) { "3.0" { Write-Log -message "running powershell update to version 3" if ($os_version.Minor -eq 1) { - $url = "https://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.1-KB2506143-$($architecture).msu" + $url = [uri]"https://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.1-KB2506143-$($architecture).msu" } else { - $url = "https://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.0-KB2506146-$($architecture).msu" + $url = [uri]"https://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.0-KB2506146-$($architecture).msu" } $error_msg = "failed to update Powershell to version 3" break @@ -344,9 +340,9 @@ foreach ($action in $actions) { "4.0" { Write-Log -message "running powershell update to version 4" if ($os_version.Minor -eq 1) { - $url = "https://download.microsoft.com/download/3/D/6/3D61D262-8549-4769-A660-230B67E15B25/Windows6.1-KB2819745-$($architecture)-MultiPkg.msu" + $url = [uri]"https://download.microsoft.com/download/3/D/6/3D61D262-8549-4769-A660-230B67E15B25/Windows6.1-KB2819745-$($architecture)-MultiPkg.msu" } else { - $url = "https://download.microsoft.com/download/3/D/6/3D61D262-8549-4769-A660-230B67E15B25/Windows8-RT-KB2799888-x64.msu" + $url = [uri]"https://download.microsoft.com/download/3/D/6/3D61D262-8549-4769-A660-230B67E15B25/Windows8-RT-KB2799888-x64.msu" } $error_msg = "failed to update Powershell to version 4" break @@ -358,13 +354,13 @@ foreach ($action in $actions) { $file = Download-Wmf5Server2008 -architecture $architecture } elseif ($os_version.Minor -eq 2) { # Server 2012 - $url = "http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/W2K12-KB3191565-x64.msu" + $url = [uri]"http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/W2K12-KB3191565-x64.msu" } else { # Server 2012 R2 and Windows 8.1 if ($architecture -eq "x64") { - $url = "http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1AndW2K12R2-KB3191564-x64.msu" + $url = [uri]"http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1AndW2K12R2-KB3191564-x64.msu" } else { - $url = "http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1-KB3191564-x86.msu" + $url = [uri]"http://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1-KB3191564-x86.msu" } } break @@ -375,16 +371,16 @@ foreach ($action in $actions) { } } - if ($file -eq $null) { - $filename = $url.Split("/")[-1] - $file = "$tmp_dir\$filename" + if ($null -eq $file) { + $filename = $url.Segments[-1] + $file = Join-Path -Path "$tmp_dir" -ChildPath "$filename" } - if ($url -ne $null) { + if ($null -ne $url) { Download-File -url $url -path $file } $exit_code = Run-Process -executable $file -arguments $arguments - if ($exit_code -ne 0 -and $exit_code -ne 3010) { + if (@(0, 3010) -notcontains $exit_code) { $log_msg = "$($error_msg): exit code $exit_code" Write-Log -message $log_msg -level "ERROR" throw $log_msg From e036a4d69acfcbe3f3d5642bda698c3ea7f1f19f Mon Sep 17 00:00:00 2001 From: Vasiliy Kovalchuk Date: Mon, 18 Feb 2019 19:37:20 +0300 Subject: [PATCH 2/2] Fix bug in `-Force` param processing in `Reboot-AndResume` function `-Force` parameter was ignored if `-Username` and `-Password` params have been set. Also comparison to `$null` changed to comparison with `$false`, because `[switch]` param cannot be `$null`. --- scripts/Upgrade-PowerShell.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/Upgrade-PowerShell.ps1 b/scripts/Upgrade-PowerShell.ps1 index a9c0e4d..3078d3a 100644 --- a/scripts/Upgrade-PowerShell.ps1 +++ b/scripts/Upgrade-PowerShell.ps1 @@ -130,10 +130,11 @@ Function Reboot-AndResume { if ($username -and $password) { Set-AutoLogon -Enable - } elseif ($null -eq $Force) { + } + if ($Force -eq $false) { $reboot_confirmation = Read-Host -Prompt "need to reboot server to continue powershell upgrade, do you wish to proceed (y/n)" if ($reboot_confirmation -ne "y") { - $msg = "please reboot server manually and login to continue upgrade process, the script will restart on the next login automatically" + $msg = "please reboot server manually to continue upgrade process, the script will restart on the next login automatically" Write-Log -Message $msg exit 0 }