Phase 1.
This commit is contained in:
@@ -80,7 +80,9 @@ param(
|
||||
[switch]$RestoreConfiguration,
|
||||
[ValidateRange(1, 10)][int]$DownloadRetryCount = 3,
|
||||
[ValidateRange(30, 3600)][int]$DownloadTimeoutSeconds = 900,
|
||||
[switch]$EnableTranscript
|
||||
[switch]$EnableTranscript,
|
||||
[string]$ExecutionId,
|
||||
[switch]$ResumeFromCheckpoint
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -117,7 +119,7 @@ if ($Profile.Count -ne 1) {
|
||||
$Profile = $Profile[0]
|
||||
$ProfileDataRoot = "{0}\Workstations\{1}" -f $SharedDataRoot.TrimEnd('\'), $Profile.Folder
|
||||
$ProfileConfigRoot = "{0}\Config\{1}" -f $SharedPortableRoot.TrimEnd('\'), $Profile.Folder
|
||||
$ProfileInstallerRoot = "P:\Install\{0}" -f $Profile.Folder
|
||||
$ProfileInstallerRoot = Join-Path $SharedPortableRoot (Join-Path 'Install' $Profile.Folder)
|
||||
|
||||
$Paths = [ordered]@{
|
||||
Root = $LocalRoot
|
||||
@@ -141,13 +143,29 @@ $Paths = [ordered]@{
|
||||
Quarantine = Join-Path $LocalRoot 'Quarantine'
|
||||
}
|
||||
|
||||
$null = New-Item -ItemType Directory -Path $Paths.Logs -Force
|
||||
$null = New-Item -ItemType Directory -Path $Paths.Logs, $Paths.Config -Force
|
||||
$TimeStamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||
$ScriptStartTime = Get-Date
|
||||
$LogFile = Join-Path $Paths.Logs ("{0}_Setup_{1}.log" -f $Profile.Folder, $TimeStamp)
|
||||
$SummaryFile = Join-Path $Paths.Logs ("{0}_Setup_{1}.csv" -f $Profile.Folder, $TimeStamp)
|
||||
$CheckpointFile = Join-Path $Paths.Config '.workstation-setup-state.json'
|
||||
$ExecutionIdWasProvided = -not [string]::IsNullOrWhiteSpace($ExecutionId)
|
||||
$ExecutionId = if ([string]::IsNullOrWhiteSpace($ExecutionId)) {
|
||||
'{0}-{1}' -f $TimeStamp, ([guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
} else {
|
||||
$ExecutionId
|
||||
}
|
||||
$Results = [System.Collections.Generic.List[object]]::new()
|
||||
$Events = [System.Collections.Generic.List[object]]::new()
|
||||
$ExecutionState = [ordered]@{
|
||||
ExecutionId = $ExecutionId
|
||||
Profile = $WorkstationProfile
|
||||
Mode = 'Apply'
|
||||
StartTime = $ScriptStartTime.ToString('o')
|
||||
LastCheckpoint = $null
|
||||
Status = 'InProgress'
|
||||
CompletedPhases = @()
|
||||
}
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [ValidateSet('INFO','WARN','ERROR','OK')][string]$Level = 'INFO')
|
||||
@@ -166,6 +184,65 @@ function Add-Result {
|
||||
})
|
||||
}
|
||||
|
||||
function Save-ExecutionCheckpoint {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Phase,
|
||||
[ValidateSet('Completed', 'InProgress', 'Failed', 'AwaitingRestart')]
|
||||
[string]$Status = 'Completed',
|
||||
[string]$Details = ''
|
||||
)
|
||||
|
||||
$ExecutionState.LastCheckpoint = (Get-Date).ToString('o')
|
||||
$ExecutionState.Status = $Status
|
||||
$ExecutionState.CompletedPhases = @(
|
||||
@($ExecutionState.CompletedPhases | Where-Object { $_.Phase -ne $Phase })
|
||||
[pscustomobject]@{
|
||||
Phase = $Phase
|
||||
Status = $Status
|
||||
Timestamp = $ExecutionState.LastCheckpoint
|
||||
Details = $Details
|
||||
}
|
||||
)
|
||||
$ExecutionState | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $CheckpointFile -Encoding UTF8
|
||||
Write-Log "Checkpoint saved: $Phase ($Status)" 'INFO'
|
||||
}
|
||||
|
||||
function Load-ExecutionCheckpoint {
|
||||
if (-not (Test-Path -LiteralPath $CheckpointFile -PathType Leaf)) {
|
||||
throw "Checkpoint file was not found: $CheckpointFile"
|
||||
}
|
||||
|
||||
$checkpoint = Get-Content -LiteralPath $CheckpointFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
|
||||
if ($checkpoint.Profile -ne $WorkstationProfile) {
|
||||
throw "Checkpoint profile '$($checkpoint.Profile)' does not match '$WorkstationProfile'."
|
||||
}
|
||||
if ($ExecutionIdWasProvided -and $checkpoint.ExecutionId -ne $ExecutionId) {
|
||||
throw "Checkpoint execution ID '$($checkpoint.ExecutionId)' does not match '$ExecutionId'."
|
||||
}
|
||||
return $checkpoint
|
||||
}
|
||||
|
||||
function Test-SetupPhaseCompleted {
|
||||
param([Parameter(Mandatory)][string]$Phase)
|
||||
if (-not $ResumeFromCheckpoint -or -not (Test-Path -LiteralPath $CheckpointFile -PathType Leaf)) {
|
||||
return $false
|
||||
}
|
||||
$checkpoint = Load-ExecutionCheckpoint
|
||||
return @($checkpoint.CompletedPhases | Where-Object { $_.Phase -eq $Phase -and $_.Status -eq 'Completed' }).Count -gt 0
|
||||
}
|
||||
|
||||
function Detect-PendingRestart {
|
||||
$cbs = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
|
||||
$windowsUpdate = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
|
||||
$sessionManager = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name PendingFileRenameOperations -ErrorAction SilentlyContinue
|
||||
return $cbs -or $windowsUpdate -or ($sessionManager -and $null -ne $sessionManager.PendingFileRenameOperations)
|
||||
}
|
||||
|
||||
function Complete-SetupPhase {
|
||||
param([Parameter(Mandatory)][string]$Phase, [string]$Details = '')
|
||||
Save-ExecutionCheckpoint -Phase $Phase -Status 'Completed' -Details $Details
|
||||
}
|
||||
|
||||
function Invoke-Safe {
|
||||
param([string]$Category, [string]$Name, [scriptblock]$Action)
|
||||
try {
|
||||
@@ -691,6 +768,15 @@ Write-Log "Administrator: $(Test-Administrator)"
|
||||
Write-Log "Local root: $LocalRoot"
|
||||
Write-Log "Shared data root: $SharedDataRoot"
|
||||
Write-Log "Shared portable root: $SharedPortableRoot"
|
||||
Write-Log "Execution ID: $ExecutionId"
|
||||
|
||||
if ($ResumeFromCheckpoint) {
|
||||
$checkpoint = Load-ExecutionCheckpoint
|
||||
$ExecutionId = $checkpoint.ExecutionId
|
||||
$ExecutionState.StartTime = $checkpoint.StartTime
|
||||
$ExecutionState.CompletedPhases = @($checkpoint.CompletedPhases)
|
||||
Write-Log "Loaded checkpoint from $($checkpoint.LastCheckpoint). Resume is idempotent; completed work will be revalidated." 'INFO'
|
||||
}
|
||||
|
||||
if ($EnableTranscript) {
|
||||
$transcriptPath = Join-Path $Paths.Logs ("{0}_Transcript_{1}.log" -f $Profile.Folder, $TimeStamp)
|
||||
@@ -708,6 +794,8 @@ if (-not (Test-Administrator)) {
|
||||
throw 'Run this script from Windows PowerShell or PowerShell as Administrator.'
|
||||
}
|
||||
|
||||
Save-ExecutionCheckpoint -Phase 'Initialize' -Status 'InProgress' -Details 'Prerequisites and administrator check passed.'
|
||||
|
||||
Invoke-Safe 'Configuration' 'Disable hibernation' {
|
||||
$powerProcess = Start-Process -FilePath 'powercfg.exe' -ArgumentList '/hibernate', 'off' -Wait -PassThru -NoNewWindow
|
||||
if ($powerProcess.ExitCode -ne 0) { throw "powercfg.exe /hibernate off failed with exit code $($powerProcess.ExitCode)." }
|
||||
@@ -742,6 +830,7 @@ foreach ($relative in $Subfolders) {
|
||||
$null = New-Item -ItemType Directory -Path $path -Force
|
||||
}
|
||||
Add-Result 'Folders' 'Local automotive structure' 'Success' $LocalRoot
|
||||
Complete-SetupPhase -Phase 'LocalStructure' -Details $LocalRoot
|
||||
|
||||
Invoke-Safe 'Configuration' 'Workstation profile metadata' {
|
||||
@"
|
||||
@@ -798,6 +887,8 @@ if ((Test-Path -LiteralPath $SharedPortableRoot) -and (Test-Path -LiteralPath $S
|
||||
} | Out-Null
|
||||
}
|
||||
|
||||
Complete-SetupPhase -Phase 'SharedPartitions' -Details "DataRoot=$SharedDataRoot; PortableRoot=$SharedPortableRoot"
|
||||
|
||||
# 3. Portable applications are installed after WinGet so archive prerequisites are available.
|
||||
|
||||
# 4. Core applications. WinGet uses package manifests and each vendor's installer.
|
||||
@@ -818,6 +909,8 @@ if (-not $SkipWinget) {
|
||||
}
|
||||
}
|
||||
|
||||
Complete-SetupPhase -Phase 'WinGetPackages' -Details "Skipped=$SkipWinget; Optional=$InstallOptionalApps"
|
||||
|
||||
# 5. Download, install/update, validate, and configure portable applications.
|
||||
if (-not $SkipDownloads) {
|
||||
$PortableRoot = Get-PortableInstallRoot
|
||||
@@ -1469,6 +1562,8 @@ according to the vendor instructions. Do not use a third-party mirror.
|
||||
Export-Csv -LiteralPath (Join-Path $manifestRoot "PortableApps_$TimeStamp.csv") -NoTypeInformation -Encoding UTF8
|
||||
}
|
||||
|
||||
Complete-SetupPhase -Phase 'PortableApplications' -Details "Skipped=$SkipDownloads"
|
||||
|
||||
# 6. Convenience shortcuts.
|
||||
|
||||
if (-not $SkipShortcuts) {
|
||||
@@ -1523,6 +1618,8 @@ if (-not $SkipShortcuts) {
|
||||
}
|
||||
}
|
||||
|
||||
Complete-SetupPhase -Phase 'Shortcuts' -Details "Skipped=$SkipShortcuts"
|
||||
|
||||
# 7. Environment and safety optimizations.
|
||||
Invoke-Safe 'Configuration' 'Local TEMP workspace' {
|
||||
$machineTemp = Join-Path $Paths.Temp 'System'
|
||||
@@ -1542,6 +1639,8 @@ Recommended manual checks after setup:
|
||||
"@ | Set-Content -LiteralPath (Join-Path $Paths.Config 'Post-Setup-Recommendations.txt') -Encoding UTF8
|
||||
} | Out-Null
|
||||
|
||||
Complete-SetupPhase -Phase 'EnvironmentAndSafety'
|
||||
|
||||
# Record the final shared-partition state for troubleshooting and future Windows installs.
|
||||
Invoke-Safe 'Report' 'Export shared partition inventory' {
|
||||
Get-Volume -DriveLetter S,P -ErrorAction SilentlyContinue |
|
||||
@@ -1556,6 +1655,11 @@ Invoke-Safe 'Report' 'Export workstation inventory' { Export-WorkstationInventor
|
||||
# Export machine-readable results even when some packages failed.
|
||||
$Results | Export-Csv -LiteralPath $SummaryFile -NoTypeInformation -Encoding UTF8
|
||||
Invoke-Safe 'Report' 'Export setup report' { Export-SetupReport } | Out-Null
|
||||
if (Detect-PendingRestart) {
|
||||
Save-ExecutionCheckpoint -Phase 'Complete' -Status 'AwaitingRestart' -Details 'Windows reports a pending restart.'
|
||||
} else {
|
||||
Save-ExecutionCheckpoint -Phase 'Complete' -Status 'Completed' -Details 'Setup flow completed.'
|
||||
}
|
||||
Write-Log "Completed. Detailed log: $LogFile" 'OK'
|
||||
Write-Log "Result summary: $SummaryFile" 'OK'
|
||||
Write-Log 'Restart Windows before installing automotive interface drivers or OEM suites.' 'WARN'
|
||||
|
||||
Reference in New Issue
Block a user