Skip to content
Open
Changes from all 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
157 changes: 77 additions & 80 deletions scripts/Upgrade-PowerShell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -83,9 +86,12 @@
Param(
[string]$version = "5.1",
[string]$username,
[string]$domain,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not specify the domain as part of the $username, like DOMAIN\user or user@DOMAIN.COM?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I don't know and haven't got domain controller at my disposal to check it. Nevertheless in my opinion separate parameter for domain name is more clear and less obscure way for users to login using their domain accounts.

[string]$password,
[switch]$verbose = $false
[switch]$verbose = $false,
[switch]$force
)

$ErrorActionPreference = 'Stop'
if ($verbose) {
$VerbosePreference = "Continue"
Expand All @@ -100,15 +106,15 @@ 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
}

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`""
Expand All @@ -117,27 +123,24 @@ 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
}
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") {
$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 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 {
Expand All @@ -152,9 +155,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'"

Expand All @@ -167,62 +170,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,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably just have the 1 switch, if set then enable AutoLogon, if not then remove AutoLogon. That way you don't need the ambiguous if case and Enable/Disable are opposites of each other.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, maybe a better way would be to make two separate functions - Enable-AutoLogon and Disable-AutoLogon?

[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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this in here, Server 2008/R2 probably won't include .NET 4.5 which is when System.IO.Compression.ZipFile was added.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct that .NET 4.5 isn't available in clean Server 2008 R2, and you definitely found a bug in my code, but my intentions was to get rid of the CopyHere() method from Shell.Application object as it is unreliable and unpredictable.
We could achieve that by loading assembly in a slightly different way and also telling PowerShell to use latest installed NET Framework - it will work because first step in upgrading PS to version 3.0 and highter is to install .NET 4.5.2. You can find more information in that SO answer: https://stackoverflow.com/a/37815418/10504393

So, after .NET installation we could add DWORD that forces usage of the latest CRL, respawn script and load assembly like this:

[Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.Filesystem")

Also, this could allow us to get rid of Download-Wmf5Server2008 and rewrite script in more consistent way, that all steps logic will be determined in foreach ($action in $actions) loop.

What do you think?

$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) {
Expand All @@ -237,7 +246,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
}

Expand Down Expand Up @@ -289,20 +298,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
}

Expand All @@ -315,7 +312,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
Expand All @@ -334,19 +331,19 @@ 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
}
"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
Expand All @@ -358,13 +355,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
Expand All @@ -375,16 +372,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
Expand Down