Make postinstall scripts more robust

(Still having problems, but I'm knocking them out one by one)

- Enable UAC in win81x86
- Throw if my scripts don't pass syntax check BEFORE running Packer
- Make the postinstall scripts more robust in general
- Add more robust Invoke-ExpressionEx, remove Invoke-ExpressionAndLog
- Simplify win-updates.ps1. Don't duplicate code.
- Use scheduled tasks instead of registry keys for running postinstall
  commands after reboots
jowjDev
Micah R Ledbetter 9 years ago
parent 72059d3d80
commit 202851cb2d

@ -8,9 +8,7 @@ Which build actions do you want to perform?
.parameter tag
A tag for the temporary directory, the output directory, and the resulting Vagrant box
#>
#function buildlab {
[cmdletbinding()]
param(
[cmdletbinding()] param(
[parameter(mandatory=$true,ParameterSetName="BuildPacker")]
[parameter(mandatory=$true,ParameterSetName="AddToVagrant")]
[parameter(mandatory=$true,ParameterSetName="VagrantUp")]
@ -31,7 +29,8 @@ param(
)
$errorActionPreference = "Stop"
Set-StrictMode -Version 2.0
Get-Module |? -Property Name -match "wintriallab-postinstall" | Remove-Module
import-module $PSScriptRoot\scripts\wintriallab-postinstall.psm1
$dateStamp = get-date -UFormat "%Y-%m-%d-%H-%M-%S"
$packerOutDir = "$baseOutDir\PackerOut"
@ -59,27 +58,6 @@ $packerConfigRoot = "${PSScriptRoot}\packer\${baseConfigName}"
$packerFile = "${packerConfigRoot}\${baseConfigName}.packerfile.json"
$packedBoxPath = "${outDir}\${baseConfigName}_virtualbox.box"
$vagrantTemplate = "${packerConfigRoot}\vagrantfile-${baseConfigName}.template"
function Test-PowershellSyntax {
[cmdletbinding()]
param(
[parameter(mandatory=$true)] [string] $fileName,
[switch] $ThrowOnFailure
)
$tokens = @()
$parseErrors = @()
$parser = [System.Management.Automation.Language.Parser]
$fileName = resolve-path $fileName
$parsed = $parser::ParseFile($fileName, [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.count -gt 0) {
$message = "$($parseErrors.count) parse errors found in file '$fileName':`r`n"
$parseErrors |% { $message += "`r`n $_" }
if ($ThrowOnFailure) { throw $message } else { write-verbose $message }
return $false
}
return $true
}
function Build-PackerFile {
[cmdletbinding()]
@ -185,7 +163,7 @@ function Show-LabVariable {
if (-not $SkipSyntaxcheck) {
foreach ($script in (gci $PSScriptRoot\scripts\* -include *.ps1,*.psm1)) {
$valid = Test-PowershellSyntax -fileName $script.fullname
$valid = Test-PowershellSyntax -fileName $script.fullname -ThrowOnFailure
New-Object PSObject -Property @{
ScriptName = $script.name
ValidSyntax = $valid
@ -228,6 +206,3 @@ if ($AddToVagrant) {
if ($VagrantUp) {
Run-VagrantBox -vagrantBoxName $fullConfigName -workingDirectory $outDir -whatif:$whatif
}
#}

@ -76,11 +76,6 @@
<UserLocale>en-US</UserLocale>
</component>
</settings>
<settings pass="offlineServicing">
<component name="Microsoft-Windows-LUA-Settings" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<EnableLUA>false</EnableLUA>
</component>
</settings>
<settings pass="oobeSystem">
<component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<UserAccounts>

@ -74,3 +74,8 @@ upstream improvements
- The shell, windows-shell, and powershell provisioners are VERY finicky. I canNOT make them work reliably. The easiest thing I can figure out how to do is to use a Powershell provisioner to call a file with no arguments over WinRM. lmfao
- However the situation was much improved when I switched to WinRM with the powershell provisioner. That seems to work OK
- I think the problem was that using the shell provisioner with OpenSSH, which provides an emulated POSIX environment of some kind
- There's lots of information on the Internet claiming you can use `HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunServices` (or `RunServicesOnce`) to run something at boot, before logon - analogous to the `Run`/`RunOnce` keys. This is apparently false for any NT operating system. Properties on these keys DO NOT RUN AT BOOT - they are completely ignored by the operating system.
- The original packer-windows crew got aroudn this by using the `Run` key and disabling UAC in `Autounattend.xml`
- I'm planning to get around this by creating a scheduled task that starts at boot and runs with highest privileges. This won't work pre-Vista/2008, but that's OK with me.
- This means I need to write an executor that can start at boot, and then check for things to execute located elsewhere. Bleh.

@ -1,4 +1,7 @@
[cmdletbinding()] param()
[cmdletbinding(DefaultParameterSetName="RunWindowsUpdates")] param(
[Parameter(ParameterSetName="RunWindowsUpdates")] [switch] $RunWindowsUpdates,
[Parameter(Mandatory=$true,ParameterSetName="SkipWindowsUpdates")] [switch] $SkipWindowsUpdates
)
import-module $PSScriptRoot\wintriallab-postinstall.psm1
$errorActionPreference = "Stop"
@ -9,25 +12,22 @@ Invoke-ScriptblockAndCatch -scriptBlock {
Set-PasswordExpiry -accountName "vagrant" -expirePassword $false
Disable-HibernationFile
Enable-MicrosoftUpdate
Set-AllNetworksToPrivate # Required for Windows 10, not required for 81, not sure about other OSes
Install-VBoxAdditions -fromDisc # Need to reboot for some of these drivers to take
Set-PinnedApplication -Action PinToTaskbar -Filepath "$PSHOME\Powershell.exe"
Set-PinnedApplication -Action PinToTaskbar -Filepath "${env:SystemRoot}\system32\eventvwr.msc"
# Need to reboot for some of these drivers to take
# Requires that the packer file attach the Guest VM driver disc, rather than upload it
# (Uploading it also gives problems when using WinRM - too big? - so this is a better solution anyway)
Install-VBoxAdditions -fromDisc
# To reboot, then run Windows updates, then enable WinRM:
$winRmCommand = "$PSHome\powershell.exe -File A:\enable-winrm.ps1"
$winUpdateCommand = "$PSHOME\powershell.exe -File A:\win-updates.ps1 -RestartAction RunAtLogon -PostUpdateExpression `"$winRmCommand`""
# Required for Windows 10, not required for 81, not sure about other OSes
# Should probably happen after installing Guest VM drivers, in case installing the drivers would cause Windows to see the network as a new connection
Set-AllNetworksToPrivate
# To install Windows Updates then enable WinRM after reboot:
Set-RestartRegistryEntry -restartAction RunAtLogon -restartCommand $winUpdateCommand
# To just enable WinRM without installing updates after reboot:
#Set-RestartRegistryEntry -restartAction RunAtLogon -restartCommand $winRmCommand
$message = "Checking restart registry key: `r`n"
Get-RestartRegistryEntry | select -expand StringRepr |% { $message += "`r`n$_`r`n"}
Write-EventLogWrapper $message
switch ($PsCmdlet.ParameterSetName) {
"RunWindowsUpdates" { $restartCommand = [ScriptBlock]::Create("A:\win-updates.ps1 -PostUpdateExpression A:\enable-winrm.ps1") }
"SkipWindowsUpdates" { $restartCommand = [ScriptBlock]::Create("A:\enable-winrm.ps1") }
default { throw "Not configured for this parameter set..." }
}
Set-RestartScheduledTask -RestartCommand $restartCommand | out-null
Restart-Computer -force
}

@ -6,13 +6,19 @@ Don't require parameters - it won't run with parameters during post install. Thi
$packerBuildName = ${env:PACKER_BUILD_NAME},
$packerBuilderType = ${env:PACKER_BUILDER_TYPE}
)
$errorActionPreference = "stop"
$errorActionPreference = "Continue"
import-module $PSScriptRoot\wintriallab-postinstall.psm1
# Gotta do this outside Invoke-ScriptblockAndCatch because I need to use try/catch here:
# These commands are fragile and shouldn't fail the build if they fail, so I put them in a try/catch outside of Invoke-ScriptblockAndCatch
try {
Set-PinnedApplication -Action UnpinFromTaskbar -Filepath "C:\Program Files\WindowsApps\Microsoft.WindowsStore_2015.10.5.0_x86__8wekyb3d8bbwe\WinStore.Mobile.exe" -ErrorAction Continue
Set-PinnedApplication -Action PinToTaskbar -Filepath "$PSHOME\Powershell.exe"
Set-PinnedApplication -Action PinToTaskbar -Filepath "${env:SystemRoot}\system32\eventvwr.msc"
$UserPinnedTaskBar = "${env:AppData}\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar"
if (test-path "$UserPinnedTaskBar\Server Manager.lnk") { rm "$UserPinnedTaskBar\Server Manager.lnk" }
}
catch {}
Invoke-ScriptblockAndCatch -scriptBlock {
Write-EventLogWrapper "PostInstall for packer build '$packerBuildName' of type '$packerBuilderType'"
Install-SevenZip
@ -33,14 +39,6 @@ Invoke-ScriptblockAndCatch -scriptBlock {
}
Set-UserOptions @suoParams
# TODO: This would be better done as links because they're easier to deal with later
# TODO: document the difference between using Set-PinnedApplication vs the links in AppData
Set-PinnedApplication -Action PinToTaskbar -Filepath "$PSHOME\Powershell.exe"
Set-PinnedApplication -Action PinToTaskbar -Filepath "${env:SystemRoot}\system32\eventvwr.msc"
$UserPinnedTaskBar = "${env:AppData}\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar"
if (test-path "$UserPinnedTaskBar\Server Manager.lnk") { rm "$UserPinnedTaskBar\Server Manager.lnk" }
Install-CompiledDotNetAssemblies # Takes about 15 minutes for me
Compress-WindowsInstall # Takes maybe another 15 minutes
}

@ -4,23 +4,18 @@ Run Windows Update, installing all available updates and rebooting as necessary
.parameter MaxCycles
The number of times to check for updates before forcing a reboot, even if Windows Update has not indicated that one is required
TODO: this doesn't appear to be working reliably
.parameter ScriptProductName
The name for this script that you want to make visible to the sysadmin in logs.
.parameter PostUpdateExpression
A string representing a PowerShell expression that is run one time after all updates have been applied (or MaxCycles has been reached without rebooting)
TODO: the parenthetical is nonobvious behavior and should be eliminated. In fact the whole of MaxCycles should be rethought.
.parameter RestartAction
When reboots are required, you can choose to run this script again before logon (using the RunServicesOnce key) or after logon (using the RunOnce key), or to skip reboots.
.parameter NoRestart
Don't reboot (useful for debugging)
.notes
This script can be run directly, but it can also be dot-sourced to get access to internal functions without automatically checking for updates, applying them, or rebooting
This script is intended to be 100% standalone because it needs to be able to tell Windows to call it again upon reboot. There are intentionally no dependencies, and features related to Windows Update that don't fit here (such as enable Microsoft Update) should live elsewhere
#>
param(
[int] $MaxCycles = 5,
[string] $ScriptProductName = "WinUp-Marionettist",
[string] $PostUpdateExpression,
[string] [ValidateSet('RunBeforeLogon','RunAtLogon','NoRestart')] $RestartAction = "NoRestart",
[switch] $CalledFromRegistry
[switch] $NoRestart
)
<# Notes on the code:
@ -28,123 +23,39 @@ param(
I'm trying to use StrictMode. That ends up making some code more complex than it would have to be otherwise. For instance, sometimes I have to check that a property exists (like $SomeVar.PSObject.Properties['PropertyName']) before I can use it.
TODO: Make sure every path of my program is writing unique log messages. Make sure all those paths are working.
TODO: Pass unique event IDs for every event to Write-WinUpEventLog. ??
TODO: Pass unique event IDs for every event to Write-EventLogWrapper. ??
#>
$ErrorActionPreference = "Stop"
Set-StrictMode -version 2.0
import-module $PSScriptRoot\wintriallab-postinstall.psm1
# I need to get the path to this script from inside functions, which mess with the $MyInvocation variable
$script:ScriptPath = $MyInvocation.MyCommand.Path
$script:RestartRegistryKeys = @{
RunBeforeLogon = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunServicesOnce"
RunAtLogon = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
}
$script:RestartRegistryProperty = "$ScriptProductName"
<#
.synopsis
Write to a special event log, named after the $ScriptProductName.
If that event log doesn't already exist, create it first.
#>
function Write-WinUpEventLog {
param(
[parameter(mandatory=$true)] [String] $message,
[int] $eventId = 0,
[ValidateSet("Error",'Warning','Information','SuccessAudit','FailureAudit')] $entryType = "Information"
)
$eventLogName = $ScriptProductName
if (-not (get-eventlog -logname * |? { $_.Log -eq $eventLogName })) {
New-EventLog -Source $ScriptProductName -LogName $eventLogName
}
$messagePlus = "$message`r`n`r`nScript: $($script:ScriptPath)`r`nUser: ${env:USERDOMAIN}\${env:USERNAME}"
Write-Host -foreground magenta "====Writing to $ScriptProductName event log===="
write-host -foreground darkgray "$messagePlus`r`n"
Write-EventLog -LogName $eventLogName -Source $ScriptProductName -EventID $eventId -EntryType $entryType -Message $MessagePlus
}
<#
.synopsis
Create and set the registry property which will run this script on reboot
#>
function Set-RestartRegistryEntry {
function Restart-ComputerAndUpdater {
param(
[parameter(mandatory=$true)] [int] $CyclesRemaining,
[parameter(mandatory=$true)] [ValidateSet('RunBeforeLogon','RunAtLogon','NoRestart')] [string] $RestartAction,
[string] $scriptPath = $script:ScriptPath
[parameter(mandatory=$true)] [switch] $NoRestart
)
if ($RestartAction -match "NoRestart") {
Write-WinUpEventLog "Called Set-RestartRegistryEntry with -RestartAction NoRestart, will not write registry key"
return
}
$psCallComponents = @(
"$PSHOME\powershell.exe"
('-File "{0}"' -f $scriptPath)
$restartCommandComponents = @(
('& "{0}"' -f $scriptPath)
"-MaxCycles $CyclesRemaining"
"-ScriptProductName $ScriptProductName"
"-CalledFromRegistry"
"-RestartAction ${script:RestartAction}"
('-PostUpdateExpression "{0}"' -f "$PostUpdateExpression")
)
$psCall = $psCallComponents -join " "
$message = "Setting the Restart Registry Key at: {0}\{1}`r`n{2}" -f $script:RestartRegistryKeys.$RestartAction, $script:RestartRegistryProperty, $psCall
Write-WinUpEventLog -message $message
New-Item $script:RestartRegistryKeys.$RestartAction -force | out-null
Set-ItemProperty -Path $script:RestartRegistryKeys.$RestartAction -Name $script:RestartRegistryProperty -Value $psCall
}
function Remove-RestartRegistryEntries {
[cmdletbinding()] param()
foreach ($key in $script:RestartRegistryKeys.Keys) {
try { Remove-ItemProperty -Path $script:RestartRegistryKeys[$key] -name $script:RestartRegistryProperty} catch {}
}
}
function Get-RestartRegistryEntry {
[cmdletbinding()] param()
$entries = @()
foreach ($key in $script:RestartRegistryKeys.Keys) {
$regKey = $script:RestartRegistryKeys[$key]
$entry = New-Object PSObject -Property @{
RegistryKey = $regKey
RegistryProperty = $script:RestartRegistryProperty
PropertyValue = $null
}
if (test-path $regKey) {
$regProps = get-item $regKey | select -expand Property
if ($regProps -contains $script:RestartRegistryProperty) {
$entry.PropertyValue = Get-ItemProperty -path $regKey | select -expand $script:RestartRegistryProperty
}
}
Add-Member -inputObject $entry -memberType ScriptProperty -Name StringRepr -Value {
"Key: $($this.RegistryKey), Property: $($this.RegistryProperty), Value: $($this.PropertyValue)"
}
$entries += @($entry)
}
return $entries
}
function Restart-ComputerAndUpdater {
param(
[parameter(mandatory=$true)] [int] $CyclesRemaining,
[parameter(mandatory=$true)] [ValidateSet('RunBeforeLogon','RunAtLogon','NoRestart')] [string] $RestartAction
)
Remove-RestartRegistryEntries
if ($RestartAction -match "NoRestart") {
Write-WinUpEventLog "Restart-ComputerAndUpdater was called, but '-RestartAction NoRestart' was passed; exiting instead..."
if ($PostUpdateExpression) { $restartCommandComponents += '-PostUpdateExpression "{0}"' -f "$PostUpdateExpression" }
if ($NoRestart) { $restartCommandComponents += "-NoRestart" }
$restartCommand = [ScriptBlock]::Create(($restartCommandComponents -join " "))
Set-RestartScheduledTask -RestartCommand $restartCommand | out-null
if ($NoRestart) {
Write-EventLogWrapper "Restart-ComputerAndUpdater was called, but '-NoRestart' was passed; exiting instead."
exit 1
}
else {
Set-RestartRegistryEntry -CyclesRemaining $CyclesRemaining -RestartAction $RestartAction
$message = "Rebooting...`r`n`r`nChecking restart registry key:"
Get-RestartRegistryEntry | select -expand StringRepr |% { $message += "`r`n`r`n$_"}
Write-WinUpEventLog $message
Write-EventLogWrapper "Restarting..."
Restart-Computer -Force
exit 0 # Restarting returns immediately and script execution continues until the reboot is processed. Lol.
}
exit 0 # Restarting returns immediately and script execution continues until the reboot is processed. Lol.
}
}
<#
@ -154,7 +65,7 @@ Return a new Microsoft Update Session for use with Check-WindowsUpdates and Inst
function New-UpdateSession {
[cmdletbinding()] param()
$UpdateSession = New-Object -ComObject 'Microsoft.Update.Session'
$UpdateSession.ClientApplicationID = "$ScriptProductName"
$UpdateSession.ClientApplicationID = "win-updates.ps1"
return $UpdateSession
}
@ -164,11 +75,11 @@ Run the expression passed to this script with -PostUpdateExpression, if any
#>
function Run-PostUpdate {
if ($PostUpdateExpression) {
Write-WinUpEventLog -message "Running PostUpdate expression:`r`n`r`n${PostUpdateExpression}"
Write-EventLogWrapper -message "Running PostUpdate expression:`r`n`r`n${PostUpdateExpression}"
Invoke-Expression $PostUpdateExpression
}
else {
Write-WinUpEventLog -message "No PostUpdate to run"
Write-EventLogWrapper -message "No PostUpdate to run"
}
}
@ -185,17 +96,17 @@ function Get-RestartPendingStatus {
# Component Based Servicing (aka Windows components) (Vista+ only)
$CsbProp = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing"
if ($CsbProp.PSObject.Properties['RebootPending']) {
Write-WinUpEventLog "(Un)installation of a Windows component requires a restart"
Write-EventLogWrapper "(Un)installation of a Windows component requires a restart"
return $true
}
$WuauProp = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update"
if ($WuauProp.PSObject.Properties['RebootRequired']) {
Write-WinUpEventLog "Updates have been installed recently, but the machine must be restarted before installation is complete"
Write-EventLogWrapper "Updates have been installed recently, but the machine must be restarted before installation is complete"
return $true
}
Write-WinUpEventLog "There are no Windows Update -related reboots pending"
Write-EventLogWrapper "There are no Windows Update -related reboots pending"
return $false
}
@ -219,7 +130,7 @@ function Install-WindowsUpdates {
)
if (-not $UpdateList) {
Write-WinUpEventLog -message "No Updates To Download..."
Write-EventLogWrapper -message "No Updates To Download..."
return $false
}
@ -239,9 +150,9 @@ function Install-WindowsUpdates {
}
$message = "There were $($AcceptedEulas.count) updates with a EULA which was automatically accepted`r`n"
$AcceptedEulas |% { $message += "`r`n - $($_.Title)"}
Write-WinUpEventLog $message
Write-EventLogWrapper $message
Write-WinUpEventLog -message 'Downloading Updates...'
Write-EventLogWrapper -message 'Downloading Updates...'
$ok = $false
$attempts = 0
while ((! $ok) -and ($attempts -lt $MaxDownloadAttempts)) {
@ -253,7 +164,7 @@ function Install-WindowsUpdates {
}
catch {
$message = "Error downloading updates. Retrying in 30s.`r`n`r`n$($_.Exception)"
Write-WinUpEventLog -message $message
Write-EventLogWrapper -message $message
$attempts += 1
Start-Sleep -s 30
}
@ -267,7 +178,7 @@ function Install-WindowsUpdates {
$UpdatesToInstall.Add($Update) |Out-Null
}
}
Write-WinUpEventLog -message $message
Write-EventLogWrapper -message $message
$RebootRequired = $false
if ($UpdatesToInstall.Count -gt 0) {
@ -275,7 +186,7 @@ function Install-WindowsUpdates {
$Installer.Updates = $UpdatesToInstall
if ($Installer.PSObject.Properties['RebootRequiredBeforeInstallation'] -and $Installer.RebootRequiredBeforeInstallation) {
Write-WinUpEventLog "Reboot required before installation. (This can happen when updates are installed but the machine is not rebooted before trying to install again.)"
Write-EventLogWrapper "Reboot required before installation. (This can happen when updates are installed but the machine is not rebooted before trying to install again.)"
return $true
}
@ -288,10 +199,10 @@ function Install-WindowsUpdates {
$message += "`r`n`r`nItem: $($update.Title)"
$message += "`r`nResult: $ResultCode"
}
Write-WinUpEventLog -message $message
Write-EventLogWrapper -message $message
}
else {
Write-WinUpEventLog -message 'No updates available to install...'
Write-EventLogWrapper -message 'No updates available to install...'
}
return $RebootRequired
}
@ -314,7 +225,7 @@ function Check-WindowsUpdates {
[switch] $FilterInteractiveUpdates,
[int] $MaxSearchAttempts = 12
)
Write-WinUpEventLog -message "Checking for Windows Updates at $(Get-Date)" -eventId 104
Write-EventLogWrapper -message "Checking for Windows Updates at $(Get-Date)" -eventId 104
$SearchResult = $null
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
@ -327,7 +238,7 @@ function Check-WindowsUpdates {
}
catch {
$message = "Search call to UpdateSearcher was unsuccessful. Retrying in 10s.`r`n`r`n$($_.Exception | Format-List -force)"
Write-WinUpEventLog -message $message
Write-EventLogWrapper -message $message
$attempts += 1
Start-Sleep -s 10
}
@ -358,7 +269,7 @@ function Check-WindowsUpdates {
if ($SkippedUpdates.Count -gt 0) {
$SkippedUpdates |% { $message += "`r`n - $($_.Title)" }
}
Write-WinUpEventLog -message $message
Write-EventLogWrapper -message $message
return $ApplicableUpdates
}
@ -366,32 +277,34 @@ function Check-WindowsUpdates {
<#
.synopsis
Run Windows Update, installing all available updates and rebooting as necessary
.notes
- Skips any update that requires user interaction
- Automatically accepts the EULA of all the updates it installs
#>
function Run-WindowsUpdate {
[cmdletbinding()] param()
$message = "Running Windows Update "
$message += if ($CalledFromRegistry) { "from the Restart Registry Entry" } else { "after being called directly" }
Write-WinUpEventLog $message
$message = "Running Windows Update..."
Write-EventLogWrapper $message
if (Get-RestartPendingStatus) {
Restart-ComputerAndUpdater -CyclesRemaining $maxCycles -RestartAction $script:RestartAction
Restart-ComputerAndUpdater -CyclesRemaining $maxCycles -NoRestart:$NoRestart
}
$UpdateSession = New-UpdateSession
for (; $maxCycles -gt 0; $maxCycles -= 1) {
Write-WinUpEventLog -message "Starting to check for updates. $maxCycles cycles remain."
Write-EventLogWrapper -message "Starting to check for updates. $maxCycles cycles remain."
$CheckedUpdates = Check-WindowsUpdates -FilterInteractiveUpdates -UpdateSession $UpdateSession
if (-not $CheckedUpdates) {
Write-WinUpEventLog "No applicable updates were detected. Done!"
Write-EventLogWrapper "No applicable updates were detected. Done!"
break
}
$RebootRequired = Install-WindowsUpdates -UpdateSession $UpdateSession -UpdateList $CheckedUpdates
if ($RebootRequired) {
Write-WinUpEventLog -message "Restart Required - Restarting..."
Restart-ComputerAndUpdater -CalledFromRegistry -CyclesRemaining $maxCycles -RestartAction $script:RestartAction
Write-EventLogWrapper -message "Restart Required - Restarting..."
Restart-ComputerAndUpdater -CyclesRemaining $maxCycles -NoRestart:$NoRestart
}
}
Run-PostUpdate
@ -406,7 +319,7 @@ if ($MyInvocation.InvocationName -ne '.') {
$message += "======== ERROR STACK ========`r`n"
$error |% { $message += "$_`r`n----`r`n" }
$message += "======== ========"
Write-WinUpEventLog $message
Write-EventLogWrapper $message
exit 666
}
}

@ -29,11 +29,6 @@ $URLs = @{
SdeleteDownload = "http://download.sysinternals.com/files/SDelete.zip"
}
$script:ScriptPath = $MyInvocation.MyCommand.Path
$script:RestartRegistryKeys = @{
RunBeforeLogon = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunServicesOnce"
RunAtLogon = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
}
$script:RestartRegistryProperty = "$ScriptProductName"
### Private support functions I use behind the scenes
@ -54,25 +49,38 @@ function Get-WebUrl {
<#
.synopsis
Invoke an expression; log the expression, any output, and the last exit code
Invoke an expression; log the expression, optionally with any output, and the last exit code if appropriate
#>
function Invoke-ExpressionAndLog {
function Invoke-ExpressionEx {
[cmdletbinding()] param(
[parameter(mandatory=$true)] [string] $command,
[switch] $invokeWithCmdExe,
[switch] $checkExitCode,
[switch] $logToStdout,
[int] $sleepSeconds
)
$global:LASTEXITCODE = 0
if ($invokeWithCmdExe) {
Write-EventLogWrapper "Invoking CMD: '$command'"
$output = cmd /c "$command"
$commandSb = if ($invokeWithCmdExe) {{cmd /c "$command"}} else {{invoke-expression -command $command}}
Write-EventLogWrapper "Invoke-ExpressionEx called to run command '$command'`r`n`r`nUsing scriptblock: $($commandSb.ToString())"
$output = $null
try {
if ($logToStdout) {
$commandSb.invoke()
$message = "Expression '$command' exited with code '$LASTEXITCODE'"
}
else {
$output = $commandSb.invoke()
$message = "Expression '$command' exited with code '$LASTEXITCODE' and output the following to the console:`r`n`r`n$output"
}
Write-EventLogWrapper -message $message
}
else {
Write-EventLogWrapper "Invoking Powershell expression: '$command'"
$output = invoke-expression -command $command
catch {
Write-EventLogWrapper -message "Invoke-ExpressionEx faile3d to run command '$command'"
Write-ErrorStackToEventLog -errorStack $_
throw $_
}
Write-EventLogWrapper "Expression '$command' had a last exit code of '$LastExitCode' and output the following to the console:`r`n`r`n$output"
if ($checkExitCode -and $global:LASTEXITCODE -ne 0) {
throw "LASTEXITCODE: ${global:LASTEXITCODE} for command: '${command}'"
}
@ -97,6 +105,7 @@ function Write-EventLogWrapper {
New-EventLog -Source $EventLogSource -LogName $eventLogName
}
$messagePlus = "$message`r`n`r`nScript: $($script:ScriptPath)`r`nUser: ${env:USERDOMAIN}\${env:USERNAME}"
if ($messagePlus.length -gt 32766) { $messagePlus = $messagePlus.SubString(0,32766) } # Because Write-EventLog will die otherwise
Write-Host -foreground magenta "====Writing to $EvengLogName event log===="
write-host -foreground darkgray "$messagePlus`r`n"
Write-EventLog -LogName $eventLogName -Source $EventLogSource -EventID $eventId -EntryType $entryType -Message $MessagePlus
@ -134,55 +143,89 @@ function Write-ErrorStackToEventLog {
Write-EventLogWrapper $message
}
<#
.synopsis
Create and set the registry property which will run this script on reboot
#>
function Set-RestartRegistryEntry {
function Test-PowershellSyntax {
[cmdletbinding(DefaultParameterSetName='FromText')]
param(
[parameter(mandatory=$true)] [ValidateSet('RunBeforeLogon','RunAtLogon','NoRestart')] [string] $RestartAction,
[string] $restartCommand
[parameter(mandatory=$true,ParameterSetName='FromText')] [string] $text,
[parameter(mandatory=$true,ParameterSetName='FromFile')] [string] $fileName,
[switch] $ThrowOnFailure
)
$tokens = @()
$parseErrors = @()
$parser = [System.Management.Automation.Language.Parser]
if ($pscmdlet.ParameterSetName -eq 'FromText') {
$parsed = $parser::ParseInput($text, [ref]$tokens, [ref]$parseErrors)
}
elseif ($pscmdlet.ParameterSetName -eq 'FromFile') {
$fileName = resolve-path $fileName
$parsed = $parser::ParseFile($fileName, [ref]$tokens, [ref]$parseErrors)
}
write-verbose "$($tokens.count) tokens found."
if ($RestartAction -match "NoRestart") {
Write-EventLogWrapper "Called Set-RestartRegistryEntry with -RestartAction NoRestart, will not write registry key"
return
if ($parseErrors.count -gt 0) {
$message = "$($parseErrors.count) parse errors found in file '$fileName':`r`n"
$parseErrors |% { $message += "`r`n $_" }
if ($ThrowOnFailure) { throw $message } else { write-verbose $message }
return $false
}
return $true
}
<#
.description
Set a scheduled task to run on next logon of the calling user. Intended for tasks that need to reboot and then be restarted such as applying Windows Updates
.notes
The Powershell New-ScheduledTask cmdlet is broken for me on Win81, but SchTasks.exe doesn't support actions with long arguments (requires a command line of < 200something characters). lmfao.
My workaround is to take a scriptblock, and then just save it to a file and call the file from Powershell.
I create the scheduled task with SchTasks.exe, then modify it with Powershell cmdlets that can handle long arguments just fine
#>
function Set-RestartScheduledTask {
[cmdletbinding()] param(
[Parameter(Mandatory=$true)] [Scriptblock] $restartCommand,
[string] $tempRestartScriptPath = "${env:temp}\$ScriptProductName-TempRestartScript.ps1",
[string] $taskName = "$ScriptProductName-RestartTask"
)
Remove-RestartScheduledTask -taskName $taskName
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name
$restartCommand.ToString() | Out-File -FilePath $tempRestartScriptPath
"Unregister-ScheduledTask -taskName '$taskName' -Confirm:`$false" | Out-File -Append -FilePath $tempRestartScriptPath
Test-PowershellSyntax -ThrowOnFailure -FileName $tempRestartScriptPath
$schTasksCmd = 'SchTasks.exe /create /sc ONLOGON /tn "{0}" /tr "cmd.exe /c echo TemporparyPlaceholderCommand" /ru "{1}" /it /rl HIGHEST /f' -f $taskName,$currentUser
Invoke-ExpressionEx -command $schTasksCmd -invokeWithCmdExe -checkExitCode
# SchTasks.exe cannot specify a user for the LOGON schedule - it applies to all users. Modify it here:
$trigger = New-ScheduledTaskTrigger -AtLogon -User $currentUser
# SchTasks.exe cannot specify an action with long arguments (maxes out at like 200something chars). Modify it here:
$action = New-ScheduledTaskAction -Execute "$PSHome\Powershell.exe" -Argument "-File `"$tempRestartScriptPath`""
Set-ScheduledTask -taskname $taskName -action $action -trigger $trigger
$message = "Setting the Restart Registry Key at: {0}\{1}`r`n{2}" -f $script:RestartRegistryKeys.$RestartAction, $script:RestartRegistryProperty, $restartCommand
Write-EventLogWrapper -message $message
New-Item $script:RestartRegistryKeys.$RestartAction -force | out-null
Set-ItemProperty -Path $script:RestartRegistryKeys.$RestartAction -Name $script:RestartRegistryProperty -Value $restartCommand
$message = "Created scheduled task called '$taskName', which will run a temp file at '$tempRestartScriptPath', containing:`r`n`r`n"
$message += (Get-Content $tempRestartScriptPath) -join "`r`n"
Write-EventLogWrapper -message $message
}
function Get-RestartRegistryEntry {
[cmdletbinding()] param()
$entries = @()
foreach ($key in $script:RestartRegistryKeys.Keys) {
$regKey = $script:RestartRegistryKeys[$key]
$entry = New-Object PSObject -Property @{
RegistryKey = $regKey
RegistryProperty = $script:RestartRegistryProperty
PropertyValue = $null
}
if (test-path $regKey) {
$regProps = get-item $regKey | select -expand Property
if ($regProps -contains $script:RestartRegistryProperty) {
$entry.PropertyValue = Get-ItemProperty -path $regKey | select -expand $script:RestartRegistryProperty
}
}
Add-Member -inputObject $entry -memberType ScriptProperty -Name StringRepr -Value {
"Key: $($this.RegistryKey), Property: $($this.RegistryProperty), Value: $($this.PropertyValue)"
}
$entries += @($entry)
}
return $entries
function Get-RestartScheduledTask {
[cmdletbinding()] param(
[string] $taskName = $ScriptProductName
)
Get-ScheduledTask |? -Property TaskName -match $taskName
}
function Remove-RestartRegistryEntries {
[cmdletbinding()] param()
foreach ($key in $script:RestartRegistryKeys.Keys) {
try { Remove-ItemProperty -Path $script:RestartRegistryKeys[$key] -name $script:RestartRegistryProperty} catch {}
function Remove-RestartScheduledTask {
[cmdletbinding()] param(
[string] $taskName = $ScriptProductName
)
$existingTask = Get-RestartScheduledTask -taskName $taskName
if ($existingTask) {
Write-EventLogWrapper -message "Found existing task named '$taskName'; deleting..."
Unregister-ScheduledTask -InputObject $existingTask -Confirm:$false | out-null
}
else {
Write-EventLogWrapper -message "Did not find any existing task named '$taskName'"
}
}
@ -221,7 +264,7 @@ function Install-SevenZip {
Write-EventLogWrapper "Downloaded '$($URLs.SevenZipDownload.$OSArch)' to '$szDlPath', now running msiexec..."
$msiCall = '& msiexec /qn /i "{0}"' -f $szDlPath
# Windows suxxx so msiexec sometimes returns right away? or something idk. fuck
Invoke-ExpressionAndLog -checkExitCode -command $msiCall -sleepSeconds 30
Invoke-ExpressionEx -checkExitCode -command $msiCall -sleepSeconds 30
}
finally {
rm -force $szDlPath
@ -242,9 +285,9 @@ function Install-VBoxAdditions {
Write-EventLogWrapper "Installing the Oracle certificate..."
$oracleCert = resolve-path "$baseDir\cert\oracle-vbox.cer" | select -expand path
# NOTE: Checking for exit code, but this command will fail with an error if the cert is already installed
Invoke-ExpressionAndLog -checkExitCode -command ('& "{0}" add-trusted-publisher "{1}" --root "{1}"' -f "$baseDir\cert\VBoxCertUtil.exe",$oracleCert)
Invoke-ExpressionEx -checkExitCode -command ('& "{0}" add-trusted-publisher "{1}" --root "{1}"' -f "$baseDir\cert\VBoxCertUtil.exe",$oracleCert)
Write-EventLogWrapper "Installing the virtualbox additions"
Invoke-ExpressionAndLog -checkExitCode -command ('& "{0}" /with_wddm /S' -f "$baseDir\VBoxWindowsAdditions.exe") # returns IMMEDIATELY, goddamn fuckers
Invoke-ExpressionEx -checkExitCode -command ('& "{0}" /with_wddm /S' -f "$baseDir\VBoxWindowsAdditions.exe") # returns IMMEDIATELY, goddamn fuckers
while (get-process -Name VBoxWindowsAdditions*) { write-host 'Waiting for VBox install to finish...'; sleep 1; }
Write-EventLogWrapper "virtualbox additions have now been installed"
}
@ -255,7 +298,7 @@ function Install-VBoxAdditions {
$vbgaPath = mkdir -force "${env:Temp}\InstallVbox" | select -expand fullname
try {
Write-EventLogWrapper "Extracting iso at '$isoPath' to directory at '$vbgaPath'..."
Invoke-ExpressionAndLog -checkExitCode -command ('sevenzip x "{0}" -o"{1}"' -f $isoPath, $vbgaPath)
Invoke-ExpressionEx -checkExitCode -command ('sevenzip x "{0}" -o"{1}"' -f $isoPath, $vbgaPath)
InstallVBoxAdditionsFromDir $vbgaPath
}
finally {
@ -292,15 +335,21 @@ function Install-CompiledDotNetAssemblies {
# http://support.microsoft.com/kb/2570538
# http://robrelyea.wordpress.com/2007/07/13/may-be-helpful-ngen-exe-executequeueditems/
# Don't check the return value - sometimes it fails and that's fine
set-alias ngen32 "${env:WinDir}\microsoft.net\framework\v4.0.30319\ngen.exe"
ngen32 update /force /queue
ngen32 executequeueditems
$ngen32 = "${env:WinDir}\microsoft.net\framework\v4.0.30319\ngen.exe"
Invoke-ExpressionEx "$ngen32 update /force /queue"
Invoke-ExpressionEx "$ngen32 executequeueditems"
# set-alias ngen32 "${env:WinDir}\microsoft.net\framework\v4.0.30319\ngen.exe"
# ngen32 update /force /queue
# ngen32 executequeueditems
if ((Get-OSArchitecture) -match $ArchitectureId.amd64) {
set-alias ngen64 "${env:WinDir}\microsoft.net\framework64\v4.0.30319\ngen.exe"
ngen64 update /force /queue
ngen64 executequeueditems
# set-alias ngen64 "${env:WinDir}\microsoft.net\framework64\v4.0.30319\ngen.exe"
# ngen64 update /force /queue
# ngen64 executequeueditems
$ngen64 = "${env:WinDir}\microsoft.net\framework64\v4.0.30319\ngen.exe"
Invoke-ExpressionEx "$ngen64 update /force /queue"
Invoke-ExpressionEx "$ngen64 executequeueditems"
}
}
@ -310,19 +359,19 @@ function Compress-WindowsInstall {
$udfZipPath = Get-WebUrl -url $URLs.UltraDefragDownload.$OSArch -outDir $env:temp
$udfExPath = "${env:temp}\ultradefrag-portable-6.1.0.$OSArch"
# This archive contains a folder - extract it directly to the temp dir
Invoke-ExpressionAndLog -checkExitCode -command ('sevenzip x "{0}" "-o{1}"' -f $udfZipPath,$env:temp)
Invoke-ExpressionEx -checkExitCode -command ('sevenzip x "{0}" "-o{1}"' -f $udfZipPath,$env:temp)
$sdZipPath = Get-WebUrl -url $URLs.SdeleteDownload -outDir $env:temp
$sdExPath = "${env:temp}\SDelete"
# This archive does NOT contain a folder - extract it to a subfolder (will create if necessary)
Invoke-ExpressionAndLog -checkExitCode -command ('sevenzip x "{0}" "-o{1}"' -f $sdZipPath,$sdExPath)
Invoke-ExpressionEx -checkExitCode -command ('sevenzip x "{0}" "-o{1}"' -f $sdZipPath,$sdExPath)
stop-service wuauserv
rm -recurse -force ${env:WinDir}\SoftwareDistribution\Download
start-service wuauserv
Invoke-ExpressionAndLog -checkExitCode -command ('& {0} --optimize --repeat "{1}"' -f "$udfExPath\udefrag.exe","$env:SystemDrive")
Invoke-ExpressionAndLog -checkExitCode -command ('& {0} /accepteula -q -z "{1}"' -f "$sdExPath\SDelete.exe",$env:SystemDrive)
Invoke-ExpressionEx -checkExitCode -logToStdout -command ('& {0} --optimize --repeat "{1}"' -f "$udfExPath\udefrag.exe","$env:SystemDrive")
Invoke-ExpressionEx -checkExitCode -command ('& {0} /accepteula -q -z "{1}"' -f "$sdExPath\SDelete.exe",$env:SystemDrive)
}
finally {
rm -recurse -force $udfZipPath,$udfExPath,$sdZipPath,$sdExPath -ErrorAction Continue
@ -494,21 +543,21 @@ function Enable-WinRM {
# call cmd.exe over and over like this, that problem goes away.
# Note: order is important. This order makes sure that any time packer can successfully
# connect to WinRm, it won't later turn winrm back off or make it unavailable.
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'net stop winrm'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'sc.exe config winrm start= auto'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm quickconfig -q'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm quickconfig -transport:http'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config @{MaxTimeoutms="1800000"}'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/winrs @{MaxMemoryPerShellMB="2048"}'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/service @{AllowUnencrypted="true"}'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/client @{AllowUnencrypted="true"}'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/service/auth @{Basic="true"}'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/client/auth @{Basic="true"}'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/service/auth @{CredSSP="true"}'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"}'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'netsh advfirewall firewall set rule group="remote administration" new enable=yes'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'netsh firewall add portopening TCP 5985 "Port 5985"'
Invoke-ExpressionAndLog -invokeWithCmdExe -command 'net start winrm'
Invoke-ExpressionEx -invokeWithCmdExe -command 'net stop winrm'
Invoke-ExpressionEx -invokeWithCmdExe -command 'sc.exe config winrm start= auto'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm quickconfig -q'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm quickconfig -transport:http'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm set winrm/config @{MaxTimeoutms="1800000"}'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm set winrm/config/winrs @{MaxMemoryPerShellMB="2048"}'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm set winrm/config/service @{AllowUnencrypted="true"}'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm set winrm/config/client @{AllowUnencrypted="true"}'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm set winrm/config/service/auth @{Basic="true"}'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm set winrm/config/client/auth @{Basic="true"}'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm set winrm/config/service/auth @{CredSSP="true"}'
Invoke-ExpressionEx -invokeWithCmdExe -command 'winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"}'
Invoke-ExpressionEx -invokeWithCmdExe -command 'netsh advfirewall firewall set rule group="remote administration" new enable=yes'
Invoke-ExpressionEx -invokeWithCmdExe -command 'netsh firewall add portopening TCP 5985 "Port 5985"'
Invoke-ExpressionEx -invokeWithCmdExe -command 'net start winrm'
}
function Set-PasswordExpiry { # TODO fixme use pure Powershell
@ -521,7 +570,7 @@ function Set-PasswordExpiry { # TODO fixme use pure Powershell
wmic useraccount where "name='{0}'" set "PasswordExpires={1}"
"@
$command = $command -f $accountName, $passwordExpiress
Invoke-ExpressionAndLog -invokeWithCmdExe -command $command
Invoke-ExpressionEx -invokeWithCmdExe -command $command
}
<#

Loading…
Cancel
Save