Files
powershell-scripts/remote-execution/Invoke-RemoteMaintenance.ps1
cproudlock 86b32d8597 Add fixnetworkshare, winrm-setup-package, udc remote-execution suites
- NetworkDriveManager.ps1: S: drive repair utility
- winrm-setup-package: Invoke-RemoteTask helper + Setup-WinRM.bat + HTML guide
- remote-execution/udc: UDC_Update.ps1 and batch wrappers for updating
  DNC controllers on shop-floor PCs
- Invoke-RemoteMaintenance.ps1: substantial rework (~1650 lines)
- Schedule-Maintenance and complete-asset minor updates
- Bump edncfix gitlink to v1.6.0 (2748bfa)
- .gitignore: block inventory.csv/xlsx (CUI) and logs_*.txt (per-host logs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:04:40 -04:00

2694 lines
106 KiB
PowerShell

<#
.SYNOPSIS
Remote maintenance toolkit for shopfloor PCs via WinRM.
.DESCRIPTION
Executes maintenance tasks on remote shopfloor PCs using WinRM.
Supports system repair, disk optimization, cleanup, and reboots.
Can target PCs by name, file list, PC type, business unit, or all.
.PARAMETER ComputerName
Single computer name, IP address, or array of computers to target.
.PARAMETER ComputerListFile
Path to text file containing computer names/IPs (one per line).
.PARAMETER All
Target all shopfloor PCs from ShopDB database.
.PARAMETER PcType
Target PCs by type (e.g., Dashboard, Lobby Display, CMM, Shopfloor).
Valid values: Standard, Engineer, Shopfloor, CMM, Wax / Trace, Keyence,
Genspect, Heat Treat, Inspection, Dashboard, Lobby Display, Uncategorized
.PARAMETER BusinessUnit
Target PCs by business unit (e.g., Blisk, HPT, Spools).
Valid values: TBD, Blisk, HPT, Spools, Inspection, Venture, Turn/Burn, DT
.PARAMETER Task
Maintenance task to execute. Available tasks:
REPAIR:
- DISM : Run DISM /Online /Cleanup-Image /RestoreHealth
- SFC : Run SFC /scannow (System File Checker)
OPTIMIZATION:
- OptimizeDisk : TRIM for SSD, Defrag for HDD
- DiskCleanup : Windows Disk Cleanup (temp files, updates)
- ClearUpdateCache : Clear Windows Update cache (fixes stuck updates)
- ClearBrowserCache: Clear Chrome/Edge cache files
SERVICES:
- RestartSpooler : Restart Print Spooler service
- FlushDNS : Clear DNS resolver cache
- RestartWinRM : Restart WinRM service
TIME/DATE:
- SetTimezone : Set timezone to Eastern Standard Time
- SyncTime : Force time sync with domain controller
DNC:
- UpdateDNCMXHosts : Update FtpHostPrimary/Secondary in DNC\MX registry key (both 32-bit and 64-bit paths)
- AuditDNCConfig : Compare DNC registry settings against UDC backup JSON files, report mismatches
- CheckDefectTracker : Check if Defect_Tracker.exe is running on target PCs (reports machine number + status)
- BackupNTLARS : Export DNC registry (NTLARS settings + subkeys) to .reg file, save to \\tsgwp00525 backup share
Named by machine number (from DNC\General\MachineNo), falls back to serial number
- VerifyNTLARS : Compare collected .reg backups against ShopDB - reports which machines are missing backups
FILE DEPLOYMENT:
- CopyFile : Copy file from -SourcePath to -DestinationPath on remote PCs (backs up existing files)
- ImportReg : Copy .reg file from -SourcePath to remote PCs and execute reg import
- DeployOpenTextProfiles : Deploy OpenText HostExplorer profiles, accessories, keymaps, and menus to ProgramData
and all user AppData\Roaming profiles. Kills hostex32.exe, deploys from csf.zip,
then relaunches HostExplorer with WJ Shopfloor profile as the logged-in user.
SYSTEM:
- GPUpdate : Force Group Policy refresh (gpupdate /force)
- Reboot : Restart the computer (30 second delay)
SOFTWARE DEPLOYMENT:
- InstallDashboard : Install GE Aerospace Dashboard kiosk app
- InstallLobbyDisplay : Install GE Aerospace Lobby Display kiosk app
- UninstallDashboard : Uninstall GE Aerospace Dashboard
- UninstallLobbyDisplay : Uninstall GE Aerospace Lobby Display
.PARAMETER SourcePath
Source file path (local or UNC) for CopyFile and ImportReg tasks.
.PARAMETER DestinationPath
Destination file path on remote PCs for CopyFile task.
.PARAMETER RunCommand
Optional command to execute after CopyFile completes.
By default runs as the logged-in user via a scheduled task (for user-session
processes like Edge kiosk). Use -AsSystem to run in the WinRM session instead.
Example: "taskkill /IM MyApp.exe /F"
.PARAMETER AsSystem
Run ImportReg and -RunCommand directly in the WinRM session (SYSTEM context)
instead of as the logged-in user via scheduled task.
Use this for HKLM-only registry imports or system-level commands.
.PARAMETER Credential
PSCredential for remote authentication. Prompts if not provided.
.PARAMETER ApiUrl
ShopDB API URL for database updates.
.PARAMETER ThrottleLimit
Maximum concurrent remote sessions (default: 5).
.EXAMPLE
# Run DISM on a single PC
.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF" -Task DISM
.EXAMPLE
# Optimize disks on multiple PCs
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01","PC02" -Task OptimizeDisk
.EXAMPLE
# Run disk cleanup on all shopfloor PCs
.\Invoke-RemoteMaintenance.ps1 -All -Task DiskCleanup
.EXAMPLE
# Reboot all Dashboard PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task Reboot
.EXAMPLE
# Reboot all Lobby Display PCs
.\Invoke-RemoteMaintenance.ps1 -PcType "Lobby Display" -Task Reboot
.EXAMPLE
# Flush DNS on all PCs in Blisk business unit
.\Invoke-RemoteMaintenance.ps1 -BusinessUnit Blisk -Task FlushDNS
.EXAMPLE
# Clear browser cache on all CMM PCs
.\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task ClearBrowserCache
.EXAMPLE
# Install Dashboard on specific PCs
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC001","PC002" -Task InstallDashboard
.EXAMPLE
# Install Lobby Display from a list file
.\Invoke-RemoteMaintenance.ps1 -ComputerListFile ".\lobby-pcs.txt" -Task InstallLobbyDisplay
.EXAMPLE
# Uninstall Dashboard from a PC
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC001" -Task UninstallDashboard
.EXAMPLE
# Copy a config file to remote PCs
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC001" -Task CopyFile -SourcePath "\\server\share\config.json" -DestinationPath "C:\ProgramData\App\config.json"
.EXAMPLE
# Copy file and restart an app as the logged-in user
.\Invoke-RemoteMaintenance.ps1 -All -Task CopyFile -SourcePath "\\server\share\eMxInfo.txt" -DestinationPath "C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt" -RunCommand "taskkill /IM DNCMain.exe /F"
.EXAMPLE
# Import a .reg file on remote PCs (runs as logged-in user for HKCU support)
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task ImportReg -SourcePath "\\server\share\intranet-zone.reg"
.EXAMPLE
# Deploy OpenText HostExplorer profiles to a single PC
.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF" -Task DeployOpenTextProfiles
.EXAMPLE
# Deploy OpenText HostExplorer profiles to all shopfloor PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task DeployOpenTextProfiles
.EXAMPLE
# Deploy OpenText HostExplorer profiles to all Keyence PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Keyence -Task DeployOpenTextProfiles
.NOTES
Author: Shop Floor Tools
Date: 2025-12-26
Requires: PowerShell 5.1+, WinRM enabled on targets, Admin credentials
#>
[CmdletBinding(DefaultParameterSetName='ByName')]
param(
[Parameter(ParameterSetName='ByName', Position=0)]
[string[]]$ComputerName,
[Parameter(ParameterSetName='ByFile')]
[string]$ComputerListFile,
[Parameter(ParameterSetName='All')]
[switch]$All,
[Parameter(ParameterSetName='ByPcType')]
[ValidateSet('Standard', 'Engineer', 'Shopfloor', 'CMM', 'Wax / Trace', 'Keyence',
'Genspect', 'Heat Treat', 'Inspection', 'Dashboard', 'Lobby Display', 'Uncategorized')]
[string]$PcType,
[Parameter(ParameterSetName='ByBusinessUnit')]
[ValidateSet('TBD', 'Blisk', 'HPT', 'Spools', 'Inspection', 'Venture', 'Turn/Burn', 'DT')]
[string]$BusinessUnit,
[Parameter(Mandatory=$true)]
[ValidateSet(
'DISM', 'SFC', 'OptimizeDisk', 'DiskCleanup', 'ClearUpdateCache',
'RestartSpooler', 'FlushDNS', 'ClearBrowserCache', 'RestartWinRM',
'SetTimezone', 'SyncTime', 'UpdateDNCMXHosts', 'AuditDNCConfig', 'CheckDefectTracker', 'BackupNTLARS', 'VerifyNTLARS', 'GPUpdate', 'Reboot',
'CopyFile', 'ImportReg', 'DeployOpenTextProfiles',
'InstallDashboard', 'InstallLobbyDisplay', 'UninstallDashboard', 'UninstallLobbyDisplay'
)]
[string]$Task,
[Parameter()]
[PSCredential]$Credential,
[Parameter()]
[string]$ApiUrl = "https://tsgwp00525.wjs.geaerospace.net/shopdb/api.asp",
[Parameter()]
[string]$DnsSuffix = "logon.ds.ge.com",
[Parameter()]
[int]$ThrottleLimit = 5,
[Parameter()]
[string]$SourcePath,
[Parameter()]
[string]$DestinationPath,
[Parameter()]
[string]$RunCommand,
[Parameter()]
[switch]$AsSystem,
[Parameter()]
[PSCredential]$ShareCredential,
[Parameter()]
[switch]$LogFile
)
# =============================================================================
# SSL/TLS Configuration
# =============================================================================
# Enable all modern TLS versions
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls
# Bypass SSL certificate validation (for self-signed certs)
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { param($sender, $cert, $chain, $errors) return $true }
# =============================================================================
# Helper Functions
# =============================================================================
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$color = switch ($Level) {
"ERROR" { "Red" }
"WARNING" { "Yellow" }
"SUCCESS" { "Green" }
"TASK" { "Cyan" }
default { "White" }
}
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
}
function Get-ShopfloorPCsFromApi {
param(
[string]$ApiUrl,
[int]$PcTypeId = 0,
[int]$BusinessUnitId = 0
)
try {
# Force TLS 1.2 immediately before request
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$queryParams = "action=getShopfloorPCs"
if ($PcTypeId -gt 0) {
$queryParams += "&pctypeid=$PcTypeId"
}
if ($BusinessUnitId -gt 0) {
$queryParams += "&businessunitid=$BusinessUnitId"
}
$fullUrl = "$ApiUrl`?$queryParams"
# Use WebClient - more reliable with TLS in script context
$webClient = New-Object System.Net.WebClient
$json = $webClient.DownloadString($fullUrl)
$response = $json | ConvertFrom-Json
if ($response.success -and $response.data) {
return $response.data
}
return @()
} catch {
Write-Log "Failed to query API: $_" -Level "ERROR"
return @()
}
}
# Lookup tables for filtering
$PcTypeLookup = @{
'Standard' = 1; 'Engineer' = 2; 'Shopfloor' = 3; 'Uncategorized' = 4;
'CMM' = 5; 'Wax / Trace' = 6; 'Keyence' = 7; 'Genspect' = 8;
'Heat Treat' = 9; 'Inspection' = 10; 'Dashboard' = 11; 'Lobby Display' = 12
}
$BusinessUnitLookup = @{
'TBD' = 1; 'Blisk' = 2; 'HPT' = 3; 'Spools' = 4;
'Inspection' = 5; 'Venture' = 6; 'Turn/Burn' = 7; 'DT' = 8
}
# =============================================================================
# Maintenance Task Scriptblocks
# =============================================================================
$TaskScripts = @{
# -------------------------------------------------------------------------
# DISM - Deployment Image Servicing and Management
# -------------------------------------------------------------------------
'DISM' = {
$result = @{
Success = $false
Task = 'DISM'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
StartTime = Get-Date
Duration = 0
}
try {
Write-Output "Starting DISM /Online /Cleanup-Image /RestoreHealth..."
Write-Output "This may take 10-30 minutes..."
$dismResult = & dism.exe /Online /Cleanup-Image /RestoreHealth 2>&1
$result.Output = $dismResult -join "`n"
$result.ExitCode = $LASTEXITCODE
$result.Success = ($LASTEXITCODE -eq 0)
if ($result.Success) {
Write-Output "DISM completed successfully."
} else {
Write-Output "DISM completed with exit code: $LASTEXITCODE"
}
} catch {
$result.Error = $_.Exception.Message
}
$result.EndTime = Get-Date
$result.Duration = [math]::Round(((Get-Date) - $result.StartTime).TotalMinutes, 2)
return $result
}
# -------------------------------------------------------------------------
# SFC - System File Checker
# -------------------------------------------------------------------------
'SFC' = {
$result = @{
Success = $false
Task = 'SFC'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
StartTime = Get-Date
Duration = 0
}
try {
Write-Output "Starting SFC /scannow..."
Write-Output "This may take 10-20 minutes..."
$sfcResult = & sfc.exe /scannow 2>&1
$result.Output = $sfcResult -join "`n"
$result.ExitCode = $LASTEXITCODE
# SFC exit codes: 0 = no issues, 1 = issues found and fixed
$result.Success = ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 1)
# Parse output for summary
if ($result.Output -match "found corrupt files and successfully repaired") {
$result.Summary = "Corrupt files found and repaired"
} elseif ($result.Output -match "did not find any integrity violations") {
$result.Summary = "No integrity violations found"
} elseif ($result.Output -match "found corrupt files but was unable to fix") {
$result.Summary = "Corrupt files found but could not be repaired"
$result.Success = $false
} else {
$result.Summary = "Scan completed"
}
} catch {
$result.Error = $_.Exception.Message
}
$result.EndTime = Get-Date
$result.Duration = [math]::Round(((Get-Date) - $result.StartTime).TotalMinutes, 2)
return $result
}
# -------------------------------------------------------------------------
# OptimizeDisk - TRIM for SSD, Defrag for HDD
# -------------------------------------------------------------------------
'OptimizeDisk' = {
$result = @{
Success = $false
Task = 'OptimizeDisk'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
Drives = @()
}
try {
# Get all fixed drives
$volumes = Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.DriveLetter }
foreach ($vol in $volumes) {
$driveLetter = $vol.DriveLetter
$driveResult = @{
DriveLetter = $driveLetter
Success = $false
MediaType = "Unknown"
Action = ""
}
# Detect if SSD or HDD
$physicalDisk = Get-PhysicalDisk | Where-Object {
$diskNum = (Get-Partition -DriveLetter $driveLetter -ErrorAction SilentlyContinue).DiskNumber
$_.DeviceId -eq $diskNum
}
if ($physicalDisk) {
$driveResult.MediaType = $physicalDisk.MediaType
}
Write-Output "Optimizing drive ${driveLetter}: ($($driveResult.MediaType))..."
try {
if ($driveResult.MediaType -eq 'SSD') {
# TRIM for SSD
Optimize-Volume -DriveLetter $driveLetter -ReTrim -ErrorAction Stop
$driveResult.Action = "TRIM"
} else {
# Defrag for HDD
Optimize-Volume -DriveLetter $driveLetter -Defrag -ErrorAction Stop
$driveResult.Action = "Defrag"
}
$driveResult.Success = $true
Write-Output " ${driveLetter}: $($driveResult.Action) completed"
} catch {
$driveResult.Error = $_.Exception.Message
Write-Output " ${driveLetter}: Failed - $($_.Exception.Message)"
}
$result.Drives += $driveResult
}
$result.Success = ($result.Drives | Where-Object { $_.Success }).Count -gt 0
$result.Output = "Optimized $($result.Drives.Count) drive(s)"
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# DiskCleanup - Windows Disk Cleanup
# -------------------------------------------------------------------------
'DiskCleanup' = {
$result = @{
Success = $false
Task = 'DiskCleanup'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
SpaceFreed = 0
}
try {
# Get initial free space
$initialFree = (Get-PSDrive C).Free
Write-Output "Running Disk Cleanup..."
# Set cleanup flags in registry for automated cleanup
$cleanupPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches"
$categories = @(
"Temporary Files",
"Temporary Setup Files",
"Old ChkDsk Files",
"Setup Log Files",
"Windows Update Cleanup",
"Windows Defender",
"Thumbnail Cache",
"Recycle Bin"
)
foreach ($cat in $categories) {
$catPath = Join-Path $cleanupPath $cat
if (Test-Path $catPath) {
Set-ItemProperty -Path $catPath -Name "StateFlags0100" -Value 2 -ErrorAction SilentlyContinue
}
}
# Run cleanmgr with sagerun
$cleanupProcess = Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:100" -Wait -PassThru -WindowStyle Hidden
# Also clear temp folders directly
$tempPaths = @(
"$env:TEMP",
"$env:SystemRoot\Temp",
"$env:SystemRoot\Prefetch"
)
$filesDeleted = 0
foreach ($path in $tempPaths) {
if (Test-Path $path) {
$files = Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue
foreach ($file in $files) {
try {
Remove-Item $file.FullName -Force -Recurse -ErrorAction SilentlyContinue
$filesDeleted++
} catch { }
}
}
}
# Calculate space freed
Start-Sleep -Seconds 2
$finalFree = (Get-PSDrive C).Free
$result.SpaceFreed = [math]::Round(($finalFree - $initialFree) / 1GB, 2)
$result.Success = $true
$result.Output = "Cleanup completed. Space freed: $($result.SpaceFreed) GB. Temp files deleted: $filesDeleted"
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# ClearUpdateCache - Clear Windows Update cache
# -------------------------------------------------------------------------
'ClearUpdateCache' = {
$result = @{
Success = $false
Task = 'ClearUpdateCache'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
}
try {
Write-Output "Stopping Windows Update service..."
Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
Stop-Service -Name bits -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Write-Output "Clearing SoftwareDistribution folder..."
$swDistPath = "$env:SystemRoot\SoftwareDistribution"
if (Test-Path $swDistPath) {
Remove-Item "$swDistPath\*" -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Output "Clearing catroot2 folder..."
$catroot2Path = "$env:SystemRoot\System32\catroot2"
if (Test-Path $catroot2Path) {
Remove-Item "$catroot2Path\*" -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Output "Starting Windows Update service..."
Start-Service -Name wuauserv -ErrorAction SilentlyContinue
Start-Service -Name bits -ErrorAction SilentlyContinue
$result.Success = $true
$result.Output = "Windows Update cache cleared successfully"
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# RestartSpooler - Restart Print Spooler service
# -------------------------------------------------------------------------
'RestartSpooler' = {
$result = @{
Success = $false
Task = 'RestartSpooler'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
}
try {
Write-Output "Stopping Print Spooler..."
Stop-Service -Name Spooler -Force -ErrorAction Stop
# Clear print queue
$printQueuePath = "$env:SystemRoot\System32\spool\PRINTERS"
if (Test-Path $printQueuePath) {
Remove-Item "$printQueuePath\*" -Force -ErrorAction SilentlyContinue
}
Write-Output "Starting Print Spooler..."
Start-Service -Name Spooler -ErrorAction Stop
$spoolerStatus = (Get-Service -Name Spooler).Status
$result.Success = ($spoolerStatus -eq 'Running')
$result.Output = "Print Spooler restarted. Status: $spoolerStatus"
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# FlushDNS - Clear DNS resolver cache
# -------------------------------------------------------------------------
'FlushDNS' = {
$result = @{
Success = $false
Task = 'FlushDNS'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
}
try {
Write-Output "Flushing DNS cache..."
$flushResult = & ipconfig /flushdns 2>&1
$result.Output = $flushResult -join "`n"
$result.Success = ($LASTEXITCODE -eq 0)
Write-Output "DNS cache flushed successfully"
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# ClearBrowserCache - Clear Chrome/Edge cache
# -------------------------------------------------------------------------
'ClearBrowserCache' = {
$result = @{
Success = $false
Task = 'ClearBrowserCache'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
BrowsersCleared = @()
}
try {
# Get all user profiles
$userProfiles = Get-ChildItem "C:\Users" -Directory | Where-Object { $_.Name -notin @('Public', 'Default', 'Default User') }
foreach ($profile in $userProfiles) {
$userName = $profile.Name
# Chrome cache paths
$chromeCachePaths = @(
"$($profile.FullName)\AppData\Local\Google\Chrome\User Data\Default\Cache",
"$($profile.FullName)\AppData\Local\Google\Chrome\User Data\Default\Code Cache"
)
# Edge cache paths
$edgeCachePaths = @(
"$($profile.FullName)\AppData\Local\Microsoft\Edge\User Data\Default\Cache",
"$($profile.FullName)\AppData\Local\Microsoft\Edge\User Data\Default\Code Cache"
)
$allPaths = $chromeCachePaths + $edgeCachePaths
foreach ($cachePath in $allPaths) {
if (Test-Path $cachePath) {
try {
Remove-Item "$cachePath\*" -Recurse -Force -ErrorAction SilentlyContinue
$browserType = if ($cachePath -like "*Chrome*") { "Chrome" } else { "Edge" }
$result.BrowsersCleared += "$userName-$browserType"
} catch { }
}
}
}
$result.Success = $true
$result.Output = "Cleared cache for: $($result.BrowsersCleared -join ', ')"
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# RestartWinRM - Restart WinRM service
# -------------------------------------------------------------------------
'RestartWinRM' = {
$result = @{
Success = $false
Task = 'RestartWinRM'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
}
try {
Write-Output "Restarting WinRM service..."
Restart-Service -Name WinRM -Force -ErrorAction Stop
Start-Sleep -Seconds 2
$winrmStatus = (Get-Service -Name WinRM).Status
$result.Success = ($winrmStatus -eq 'Running')
$result.Output = "WinRM service restarted. Status: $winrmStatus"
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# SetTimezone - Set timezone to Eastern Standard Time
# -------------------------------------------------------------------------
'SetTimezone' = {
$result = @{
Success = $false
Task = 'SetTimezone'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
PreviousTimezone = ""
NewTimezone = ""
}
try {
# Get current timezone
$currentTz = Get-TimeZone
$result.PreviousTimezone = $currentTz.Id
$targetTz = "Eastern Standard Time"
if ($currentTz.Id -eq $targetTz) {
$result.Success = $true
$result.NewTimezone = $currentTz.Id
$result.Output = "Timezone already set to $targetTz"
Write-Output $result.Output
} else {
Write-Output "Changing timezone from $($currentTz.Id) to $targetTz..."
Set-TimeZone -Id $targetTz -ErrorAction Stop
# Verify change
$newTz = Get-TimeZone
$result.NewTimezone = $newTz.Id
$result.Success = ($newTz.Id -eq $targetTz)
$result.Output = "Timezone changed: $($currentTz.Id) -> $($newTz.Id)"
Write-Output $result.Output
}
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# SyncTime - Sync time with domain controller
# -------------------------------------------------------------------------
'SyncTime' = {
$result = @{
Success = $false
Task = 'SyncTime'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
TimeBefore = ""
TimeAfter = ""
TimeSource = ""
}
try {
$result.TimeBefore = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Write-Output "Syncing time with domain controller..."
# Get current time source
$w32tmSource = & w32tm /query /source 2>&1
$result.TimeSource = ($w32tmSource -join " ").Trim()
# Force time resync
$resyncResult = & w32tm /resync /force 2>&1
$resyncOutput = $resyncResult -join "`n"
# Check if successful
if ($resyncOutput -match "The command completed successfully" -or $LASTEXITCODE -eq 0) {
Start-Sleep -Seconds 1
$result.TimeAfter = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$result.Success = $true
$result.Output = "Time synced with $($result.TimeSource). Time: $($result.TimeAfter)"
} else {
$result.Output = "Sync attempted. Result: $resyncOutput"
$result.Success = $false
}
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# CopyFile - General-purpose file deployment to remote PCs
# File is pushed to C:\Windows\Temp\ via Copy-Item -ToSession, then
# moved to the destination. Optionally runs a post-copy command as the
# logged-in user via a one-shot scheduled task.
# -------------------------------------------------------------------------
'CopyFile' = {
param($DestinationPath, $RunCommand, [bool]$AsSystem = $false)
$result = @{
Success = $false
Task = 'CopyFile'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
BackupCreated = $false
CommandRan = $false
}
$fileName = Split-Path $DestinationPath -Leaf
$tempPath = "C:\Windows\Temp\$fileName"
try {
# Verify temp file was pushed
if (-not (Test-Path $tempPath)) {
$result.Error = "Source file not found at $tempPath - file push may have failed"
Write-Output $result.Error
return $result
}
# Create destination directory if needed
$destDir = Split-Path $DestinationPath -Parent
if (-not (Test-Path $destDir)) {
New-Item -Path $destDir -ItemType Directory -Force | Out-Null
Write-Output "Created directory: $destDir"
}
# Backup existing file
if (Test-Path $DestinationPath) {
$dateStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$ext = [System.IO.Path]::GetExtension($fileName)
$base = [System.IO.Path]::GetFileNameWithoutExtension($fileName)
$backupName = "${base}-old-${dateStamp}${ext}"
$backupPath = Join-Path $destDir $backupName
Copy-Item -Path $DestinationPath -Destination $backupPath -Force -ErrorAction Stop
$result.BackupCreated = $true
Write-Output "Backed up existing file to: $backupName"
}
# Move temp file to destination
Move-Item -Path $tempPath -Destination $DestinationPath -Force -ErrorAction Stop
# Verify
if (-not (Test-Path $DestinationPath)) {
$result.Error = "File not found at destination after move"
Write-Output $result.Error
return $result
}
$result.Success = $true
$result.Output = "Deployed to $DestinationPath"
if ($result.BackupCreated) { $result.Output += " (backup created)" }
Write-Output $result.Output
# Run optional post-copy command
if ($RunCommand) {
if ($AsSystem) {
# Run directly in WinRM session (SYSTEM context)
Write-Output "Running post-copy command as SYSTEM..."
$cmdOutput = & cmd.exe /c $RunCommand 2>&1
$result.CommandRan = $true
$result.Output += " | Post-copy command executed as SYSTEM"
Write-Output " Post-copy command completed"
} else {
# Run as logged-in user via scheduled task
$loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem).UserName
if ($loggedInUser) {
$taskName = "CopyFilePostCmd_$(Get-Random)"
Write-Output "Running post-copy command as $loggedInUser..."
$action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c $RunCommand"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $loggedInUser -Force | Out-Null
Start-Sleep -Seconds 8
$taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
$result.CommandRan = $true
$result.Output += " | Post-copy command executed as $loggedInUser"
Write-Output " Post-copy command completed"
} else {
$result.Output += " | No logged-in user found, post-copy command skipped"
Write-Output " Warning: No logged-in user - post-copy command skipped"
}
}
}
} catch {
$result.Error = $_.Exception.Message
Write-Output "Error: $($result.Error)"
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
}
return $result
}
# -------------------------------------------------------------------------
# DeployOpenTextProfiles - Deploy HostExplorer config files to ProgramData
# A single zip is pushed to C:\Windows\Temp\, extracted, then files are
# copied to ProgramData destinations, overwriting existing.
# -------------------------------------------------------------------------
'DeployOpenTextProfiles' = {
$result = @{
Success = $false
Task = 'DeployOpenTextProfiles'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
FilesDeployed = 0
UserProfile = ""
}
$tempZip = "C:\Windows\Temp\csf.zip"
$tempExtract = "C:\Windows\Temp\csf"
$pdRoot = "C:\ProgramData\Hummingbird\Connectivity\15.00\Shared"
# Build deploy map: ProgramData destinations
$deployMap = @(
@{ Source = "$tempExtract\Profile"; Dest = "$pdRoot\Profile" }
@{ Source = "$tempExtract\Accessories\EB"; Dest = "$pdRoot\Accessories\EB" }
@{ Source = "$tempExtract\HostExplorer\Keymap"; Dest = "$pdRoot\HostExplorer\Keymap" }
@{ Source = "$tempExtract\HostExplorer\Menu"; Dest = "$pdRoot\HostExplorer\Menu" }
)
# Add AppData\Roaming destinations for all user profiles
$skipUsers = @('Public', 'Default', 'Default User', 'All Users')
$userProfiles = Get-ChildItem "C:\Users" -Directory | Where-Object {
($skipUsers -notcontains $_.Name) -and (Test-Path (Join-Path $_.FullName "AppData\Roaming"))
}
$userCount = 0
foreach ($profile in $userProfiles) {
$adRoot = "$($profile.FullName)\AppData\Roaming\Hummingbird\Connectivity\15.00"
$deployMap += @(
@{ Source = "$tempExtract\Profile"; Dest = "$adRoot\Profile" }
@{ Source = "$tempExtract\Accessories\EB"; Dest = "$adRoot\Accessories\EB" }
@{ Source = "$tempExtract\HostExplorer\Keymap"; Dest = "$adRoot\HostExplorer\Keymap" }
@{ Source = "$tempExtract\HostExplorer\Menu"; Dest = "$adRoot\HostExplorer\Menu" }
)
$userCount++
Write-Output "Found user profile: $($profile.Name)"
}
$result.UserProfile = "$userCount user(s)"
if ($userCount -eq 0) { Write-Output "WARNING: No user profiles found under C:\Users" }
try {
if (-not (Test-Path $tempZip)) {
$result.Error = "Zip file not found at $tempZip - file push may have failed"
Write-Output $result.Error
return $result
}
# Kill HostExplorer if running
$hostex = Get-Process -Name "hostex32" -ErrorAction SilentlyContinue
if ($hostex) {
Stop-Process -Name "hostex32" -Force -ErrorAction SilentlyContinue
Write-Output "Killed hostex32.exe"
} else {
Write-Output "hostex32.exe not running"
}
# Extract zip
if (Test-Path $tempExtract) { Remove-Item -Path $tempExtract -Recurse -Force }
Expand-Archive -Path $tempZip -DestinationPath $tempExtract -Force
Write-Output "Extracted csf.zip to $tempExtract"
foreach ($mapping in $deployMap) {
if (-not (Test-Path $mapping.Source)) { continue }
# Create destination directory if needed
if (-not (Test-Path $mapping.Dest)) {
New-Item -Path $mapping.Dest -ItemType Directory -Force | Out-Null
Write-Output "Created directory: $($mapping.Dest)"
}
$files = Get-ChildItem -Path $mapping.Source -File
foreach ($file in $files) {
$destFile = Join-Path $mapping.Dest $file.Name
Copy-Item -Path $file.FullName -Destination $destFile -Force -ErrorAction Stop
$result.FilesDeployed++
Write-Output " Deployed: $($file.Name) -> $($mapping.Dest)"
}
}
# Cleanup temp files
Remove-Item -Path $tempZip -Force -ErrorAction SilentlyContinue
Remove-Item -Path $tempExtract -Recurse -Force -ErrorAction SilentlyContinue
# Relaunch HostExplorer with WJ Shopfloor profile as logged-in user
# Must run from the user's Profile directory: hostex32.exe -P "WJ Shopfloor.hep"
$loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem).UserName
if ($loggedInUser) {
$hostexPath = "C:\Program Files\Hummingbird\Connectivity\15.00\HostExplorer\hostex32.exe"
$username = ($loggedInUser -split '\\')[-1]
$profileDir = "C:\Users\$username\AppData\Roaming\Hummingbird\Connectivity\15.00\Profile"
if ((Test-Path $hostexPath) -and (Test-Path $profileDir)) {
$taskName = "OpenTextRelaunch_$(Get-Random)"
$action = New-ScheduledTaskAction -Execute $hostexPath -Argument "-P `"WJ Shopfloor.hep`"" -WorkingDirectory $profileDir
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $loggedInUser -Force | Out-Null
Start-Sleep -Seconds 5
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Write-Output "Relaunched hostex32.exe -P 'WJ Shopfloor.hep' as $loggedInUser from $profileDir"
} else {
Write-Output "WARNING: hostex32.exe or profile directory not found - skipping relaunch"
}
} else {
Write-Output "WARNING: No logged-in user found - skipping relaunch"
}
$result.Success = $true
$result.Output = "Deployed $($result.FilesDeployed) files to ProgramData + $($result.UserProfile)"
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
Write-Output "Error: $($result.Error)"
Remove-Item -Path $tempZip -Force -ErrorAction SilentlyContinue
Remove-Item -Path $tempExtract -Recurse -Force -ErrorAction SilentlyContinue
}
return $result
}
# -------------------------------------------------------------------------
# ImportReg - Import a .reg file on remote PCs
# File is pushed to C:\Windows\Temp\ via Copy-Item -ToSession, then
# imported via a one-shot scheduled task running as the logged-in user
# so that both HKLM and HKCU keys are applied correctly.
# -------------------------------------------------------------------------
'ImportReg' = {
param($RegFileName, [bool]$AsSystem = $false)
$result = @{
Success = $false
Task = 'ImportReg'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
}
$tempRegPath = "C:\Windows\Temp\$RegFileName"
try {
if (-not (Test-Path $tempRegPath)) {
$result.Error = "Registry file not found at $tempRegPath - file push may have failed"
Write-Output $result.Error
return $result
}
if ($AsSystem) {
# Run directly in WinRM session (SYSTEM context) - HKLM only
Write-Output "Importing $RegFileName directly as SYSTEM..."
$regOutput = & regedit.exe /s "$tempRegPath" 2>&1
$result.Success = $true
$result.Output = "Registry imported as SYSTEM (HKCU keys will not apply to logged-in user)"
} else {
$loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem).UserName
if ($loggedInUser) {
# Use scheduled task as logged-in user (handles HKCU keys)
$taskName = "ImportReg_$(Get-Random)"
Write-Output "Importing $RegFileName as $loggedInUser via scheduled task..."
$action = New-ScheduledTaskAction -Execute "regedit.exe" -Argument "/s `"$tempRegPath`""
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $loggedInUser -Force | Out-Null
Start-Sleep -Seconds 8
$taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
$lastResult = if ($taskInfo) { $taskInfo.LastTaskResult } else { -1 }
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
if ($lastResult -eq 0) {
$result.Success = $true
$result.Output = "Registry imported successfully as $loggedInUser"
} else {
$result.Success = $true # Task ran, regedit /s doesn't reliably set exit codes
$result.Output = "Registry import task completed as $loggedInUser (task result: $lastResult)"
}
} else {
# No user logged in - direct import (HKLM only)
Write-Output "No logged-in user, importing directly (HKLM only)..."
$regOutput = & regedit.exe /s "$tempRegPath" 2>&1
$result.Success = $true
$result.Output = "Registry imported directly (no logged-in user, HKCU keys will not apply)"
}
}
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
Write-Output "Error: $($result.Error)"
} finally {
Remove-Item $tempRegPath -Force -ErrorAction SilentlyContinue
}
return $result
}
# -------------------------------------------------------------------------
# UpdateDNCMXHosts - Update FtpHostPrimary/Secondary in DNC\MX registry
# Checks both WOW6432Node (32-bit) and native 64-bit registry paths.
# Only updates values that match the old hostname - skips anything else.
# Safe to run on all shopfloor PCs: no-ops if DNC\MX key doesn't exist.
# -------------------------------------------------------------------------
'UpdateDNCMXHosts' = {
$result = @{
Success = $false
Task = 'UpdateDNCMXHosts'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
FailReason = ""
Changed = @()
Skipped = @()
Warnings = @()
}
$OldHost = "tsgwp00525.us.ae.ge.com"
$NewHost = "tsgwp00525.wjs.geaerospace.net"
$ValueNames = @("FtpHostPrimary", "FtpHostSecondary")
$RegPaths = @(
"HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\MX",
"HKLM:\SOFTWARE\GE Aircraft Engines\DNC\MX"
)
try {
$anyKeyFound = $false
foreach ($regPath in $RegPaths) {
if (-not (Test-Path $regPath)) {
continue
}
$anyKeyFound = $true
foreach ($valueName in $ValueNames) {
try {
$current = (Get-ItemProperty -Path $regPath -Name $valueName -ErrorAction Stop).$valueName
if ($current -eq $OldHost) {
Set-ItemProperty -Path $regPath -Name $valueName -Value $NewHost -ErrorAction Stop
$result.Changed += "$valueName @ $(Split-Path $regPath -Leaf): $OldHost -> $NewHost"
} elseif ($current -eq $NewHost) {
$result.Skipped += "$valueName @ $(Split-Path $regPath -Leaf): already correct"
} else {
$result.Warnings += "$valueName @ $(Split-Path $regPath -Leaf): unexpected value '$current' - NOT modified"
}
} catch {
$result.Skipped += "$valueName @ $(Split-Path $regPath -Leaf): value not found"
}
}
}
if (-not $anyKeyFound) {
# PC doesn't have DNC\MX at all - not an error, just not applicable
$result.Success = $true
$result.Output = "DNC\MX key not present - PC does not use MX DNC (skipped)"
return $result
}
$result.Success = $true
$result.Output = if ($result.Changed.Count -gt 0 -and $result.Warnings.Count -gt 0) {
"Updated $($result.Changed.Count) value(s), $($result.Warnings.Count) unexpected value(s) - review output"
} elseif ($result.Changed.Count -gt 0) {
"Updated $($result.Changed.Count) value(s)"
} elseif ($result.Warnings.Count -gt 0) {
"$($result.Warnings.Count) unexpected value(s) - review output"
} else {
"No changes needed"
}
} catch {
$result.FailReason = $_.Exception.Message
$result.Error = $result.FailReason
}
return $result
}
# -------------------------------------------------------------------------
# AuditDNCConfig - Read DNC registry values for comparison with UDC backup
# Reads General, eFocas, and Hssb keys from both 32-bit and 64-bit paths.
# -------------------------------------------------------------------------
'AuditDNCConfig' = {
$result = @{
Success = $false
Task = 'AuditDNCConfig'
Hostname = $env:COMPUTERNAME
Error = $null
Values = @{}
FoundDNC = $false
}
$regRoots = @(
"HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC",
"HKLM:\SOFTWARE\GE Aircraft Engines\DNC"
)
try {
foreach ($root in $regRoots) {
if (Test-Path $root) {
$result.FoundDNC = $true
# General subkey
$generalPath = Join-Path $root "General"
if (Test-Path $generalPath) {
$props = Get-ItemProperty -Path $generalPath -ErrorAction SilentlyContinue
if ($props.MachineNo) { $result.Values['General_MachineNo'] = [string]$props.MachineNo }
if ($props.NcIF) { $result.Values['General_NcIF'] = [string]$props.NcIF }
if ($null -ne $props.DualPath) { $result.Values['General_DualPath'] = [string]$props.DualPath }
}
# eFocas subkey
$efocasPath = Join-Path $root "eFocas"
if (Test-Path $efocasPath) {
$props = Get-ItemProperty -Path $efocasPath -ErrorAction SilentlyContinue
if ($props.IpAddr) { $result.Values['eFocas_IpAddr'] = [string]$props.IpAddr }
if ($null -ne $props.SocketNo) { $result.Values['eFocas_SocketNo'] = [string]$props.SocketNo }
if ($null -ne $props.DualPath) { $result.Values['eFocas_DualPath'] = [string]$props.DualPath }
}
# Hssb subkey
$hssbPath = Join-Path $root "Hssb"
if (Test-Path $hssbPath) {
$props = Get-ItemProperty -Path $hssbPath -ErrorAction SilentlyContinue
if ($null -ne $props.KRelay1) { $result.Values['Hssb_KRelay1'] = [string][int]$props.KRelay1 }
}
# PPDCS subkey
$ppdcsPath = Join-Path $root "PPDCS"
if (Test-Path $ppdcsPath) {
$props = Get-ItemProperty -Path $ppdcsPath -ErrorAction SilentlyContinue
if ($null -ne $props.'CycleStart Inhibits') { $result.Values['PPDCS_CycleStartInhibits'] = [string]$props.'CycleStart Inhibits' }
}
# Found values in this root, no need to check the other
if ($result.Values.Count -gt 0) { break }
}
}
$result.Success = $true
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# CheckDefectTracker - Check if Defect_Tracker.exe is running
# -------------------------------------------------------------------------
'CheckDefectTracker' = {
$result = @{
Success = $false
Task = 'CheckDefectTracker'
Hostname = $env:COMPUTERNAME
Running = $false
Output = ''
Error = $null
}
try {
$output = & tasklist /FI "IMAGENAME eq Defect_Tracker.exe" /NH 2>&1
$result.Output = ($output | Out-String).Trim()
$result.Running = $result.Output -match 'Defect_Tracker\.exe'
$result.Success = $true
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# BackupNTLARS - Export DNC registry tree to .reg content
# Returns the .reg file content + machine number + serial number.
# Uses PowerShell registry provider (Get-ItemProperty) instead of reg.exe
# because GE Group Policy blocks reg.exe on managed workstations.
# The caller writes the file to the backup share (avoids WinRM double-hop).
# -------------------------------------------------------------------------
'BackupNTLARS' = {
$result = @{
Success = $false
Task = 'BackupNTLARS'
Hostname = $env:COMPUTERNAME
Error = $null
MachineNo = $null
Serial = $null
RegContent = $null
FoundDNC = $false
}
$regRoots = @(
"HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC",
"HKLM:\SOFTWARE\GE Aircraft Engines\DNC"
)
try {
# Get serial number for fallback naming
$bios = Get-WmiObject Win32_BIOS -ErrorAction SilentlyContinue
if ($bios.SerialNumber -and $bios.SerialNumber -ne "To be filled by O.E.M.") {
$result.Serial = $bios.SerialNumber.Trim()
}
# Find DNC registry root
$dncRoot = $null
foreach ($root in $regRoots) {
if (Test-Path $root) {
$dncRoot = $root
$result.FoundDNC = $true
break
}
}
if (-not $dncRoot) {
$result.Success = $true
return $result
}
# Read machine number from DNC\General
$generalPath = Join-Path $dncRoot "General"
if (Test-Path $generalPath) {
$props = Get-ItemProperty -Path $generalPath -ErrorAction SilentlyContinue
if ($props.MachineNo) {
$result.MachineNo = [string]$props.MachineNo
}
}
# Build .reg file content using PowerShell registry provider
# (reg.exe is blocked by GE Group Policy)
$regLines = @()
$regLines += "Windows Registry Editor Version 5.00"
$regLines += ""
$regLines += "; NTLARS DNC Registry Backup"
$regLines += "; Computer: $env:COMPUTERNAME"
$regLines += "; Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
$regLines += ""
# Recursive function to export a key and all subkeys
function Export-RegKey {
param([string]$Path)
# Handle both "HKLM:\..." and "Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\..."
$regPath = $Path -replace '^Microsoft\.PowerShell\.Core\\Registry::', '' -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\'
$lines = @()
$lines += "[$regPath]"
# Export values in this key
$item = Get-Item -Path $Path -ErrorAction SilentlyContinue
if ($item) {
foreach ($valueName in $item.GetValueNames()) {
$valueData = $item.GetValue($valueName, $null, 'DoNotExpandEnvironmentNames')
$valueKind = $item.GetValueKind($valueName)
# Format the value name
$nameStr = if ($valueName -eq '') { '@' } else { "`"$valueName`"" }
switch ($valueKind) {
'String' {
$escaped = [string]$valueData -replace '\\', '\\' -replace '"', '\"'
$lines += "$nameStr=`"$escaped`""
}
'DWord' {
$lines += "$nameStr=dword:$($valueData.ToString('x8'))"
}
'QWord' {
$lines += "$nameStr=hex(b):$(([BitConverter]::GetBytes([long]$valueData) | ForEach-Object { $_.ToString('x2') }) -join ',')"
}
'Binary' {
$hexStr = ($valueData | ForEach-Object { $_.ToString('x2') }) -join ','
$lines += "$nameStr=hex:$hexStr"
}
'MultiString' {
# Multi-string: UTF-16LE encoded, double-null terminated
$raw = [System.Text.Encoding]::Unicode.GetBytes(($valueData -join "`0") + "`0`0")
$hexStr = ($raw | ForEach-Object { $_.ToString('x2') }) -join ','
$lines += "$nameStr=hex(7):$hexStr"
}
'ExpandString' {
$raw = [System.Text.Encoding]::Unicode.GetBytes($valueData + "`0")
$hexStr = ($raw | ForEach-Object { $_.ToString('x2') }) -join ','
$lines += "$nameStr=hex(2):$hexStr"
}
default {
$lines += "; Unsupported type $valueKind for $valueName"
}
}
}
}
$lines += ""
# Recurse into subkeys
$subKeys = Get-ChildItem -Path $Path -ErrorAction SilentlyContinue
foreach ($sub in $subKeys) {
$lines += Export-RegKey -Path $sub.PSPath
}
return $lines
}
$regLines += Export-RegKey -Path $dncRoot
$result.RegContent = $regLines -join "`r`n"
$result.Success = $true
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# InstallDashboard / InstallLobbyDisplay - Install kiosk app
# -------------------------------------------------------------------------
'InstallKioskApp' = {
param($InstallerPath, $AppName, $KioskUrl)
$result = @{
Success = $false
Task = 'InstallKioskApp'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
AppName = $AppName
}
try {
if (-not (Test-Path $InstallerPath)) {
$result.Error = "Installer not found at $InstallerPath"
Write-Output $result.Error
return $result
}
Write-Output "Installing $AppName..."
$proc = Start-Process -FilePath $InstallerPath -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" -PassThru -WindowStyle Hidden
# Wait up to 120 seconds for installer to finish
$completed = $proc.WaitForExit(120000)
# Clean up installer
Remove-Item $InstallerPath -Force -ErrorAction SilentlyContinue
if (-not $completed) {
$proc | Stop-Process -Force -ErrorAction SilentlyContinue
$result.Error = "Installer timed out after 120 seconds"
} elseif ($proc.ExitCode -eq 0) {
$result.Success = $true
$result.Output = "$AppName installed successfully"
# Relaunch Edge kiosk via scheduled task running as the logged-in user
if ($KioskUrl) {
$edgePath = "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe"
$taskName = "RelaunchKiosk_$($AppName -replace '\s','')"
$loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem).UserName
if ($loggedInUser) {
$action = New-ScheduledTaskAction -Execute $edgePath -Argument "-kiosk $KioskUrl --edge-kiosk-type=fullscreen"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(5)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $loggedInUser -Force | Out-Null
# Wait for it to fire, then clean up
Start-Sleep -Seconds 10
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
$result.Output += " | Edge relaunched via scheduled task as $loggedInUser"
} else {
$result.Output += " | No logged-in user found, reboot required to relaunch Edge"
}
}
} else {
$result.Error = "Installer exited with code $($proc.ExitCode)"
}
Write-Output $result.Output
} catch {
$result.Error = $_.Exception.Message
Write-Output "Error: $($result.Error)"
}
return $result
}
# -------------------------------------------------------------------------
# UninstallDashboard / UninstallLobbyDisplay - Uninstall kiosk app
# -------------------------------------------------------------------------
'UninstallKioskApp' = {
param($UninstallGuid, $AppName)
$result = @{
Success = $false
Task = 'UninstallKioskApp'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
AppName = $AppName
}
try {
Write-Output "Uninstalling $AppName..."
# Find uninstaller in registry
$uninstallPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$UninstallGuid`_is1"
if (-not (Test-Path $uninstallPath)) {
$uninstallPath = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$UninstallGuid`_is1"
}
if (Test-Path $uninstallPath) {
$uninstallString = (Get-ItemProperty $uninstallPath).UninstallString
if ($uninstallString) {
$uninstallExe = $uninstallString -replace '"', ''
$proc = Start-Process -FilePath $uninstallExe -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" -Wait -PassThru -WindowStyle Hidden
if ($proc.ExitCode -eq 0) {
$result.Success = $true
$result.Output = "$AppName uninstalled successfully"
} else {
$result.Error = "Uninstaller exited with code $($proc.ExitCode)"
}
} else {
$result.Error = "No uninstall string found in registry"
}
} else {
$result.Error = "$AppName not found in registry (may not be installed)"
}
Write-Output $(if ($result.Success) { $result.Output } else { $result.Error })
} catch {
$result.Error = $_.Exception.Message
Write-Output "Error: $($result.Error)"
}
return $result
}
# -------------------------------------------------------------------------
# GPUpdate - Force Group Policy refresh
# -------------------------------------------------------------------------
'GPUpdate' = {
$result = @{
Success = $false
Task = 'GPUpdate'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
}
try {
$gpOutput = & gpupdate /target:computer /force 2>&1
$result.Output = ($gpOutput | Out-String).Trim()
$result.Success = $result.Output -match 'completed successfully'
if (-not $result.Success) {
$result.Error = $result.Output
}
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
# -------------------------------------------------------------------------
# Reboot - Restart the computer
# -------------------------------------------------------------------------
'Reboot' = {
$result = @{
Success = $false
Task = 'Reboot'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
}
try {
Write-Output "Initiating system restart..."
# Use shutdown command with 30 second delay to allow WinRM to return
$shutdownResult = & shutdown.exe /r /t 30 /c "Remote maintenance reboot initiated" 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
$result.Success = $true
$result.Output = "Reboot scheduled in 30 seconds"
Write-Output $result.Output
} else {
$result.Error = "Shutdown command failed with exit code $exitCode : $shutdownResult"
Write-Output $result.Error
}
} catch {
$result.Error = $_.Exception.Message
Write-Output "Error: $($result.Error)"
}
return $result
}
}
# =============================================================================
# Main Execution
# =============================================================================
Write-Host ""
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Host " Remote Maintenance Tool - Task: $Task" -ForegroundColor Cyan
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Host ""
if ($LogFile) {
$logDir = Join-Path $PSScriptRoot "logs"
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
$logPath = Join-Path $logDir "maintenance-$(Get-Date -Format 'yyyy-MM-dd_HHmmss')-$Task.log"
Start-Transcript -Path $logPath -Force | Out-Null
Write-Host "Logging to: $logPath" -ForegroundColor DarkGray
Write-Host ""
}
# Get credentials
if (-not $Credential) {
Write-Log "Enter credentials for remote PCs:" -Level "INFO"
$Credential = Get-Credential -Message "Enter admin credentials for remote PCs"
if (-not $Credential) {
Write-Log "Credentials required. Exiting." -Level "ERROR"
exit 1
}
}
# Build computer list
$computers = @()
if ($All) {
Write-Log "Querying ShopDB for all shopfloor PCs..." -Level "INFO"
$shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl
$computers = $shopfloorPCs | ForEach-Object { $_.hostname } | Where-Object { $_ }
Write-Log "Found $($computers.Count) shopfloor PCs" -Level "INFO"
} elseif ($PcType) {
$pcTypeId = $PcTypeLookup[$PcType]
Write-Log "Querying ShopDB for PCs of type '$PcType' (ID: $pcTypeId)..." -Level "INFO"
$shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl -PcTypeId $pcTypeId
$computers = $shopfloorPCs | ForEach-Object { $_.hostname } | Where-Object { $_ }
Write-Log "Found $($computers.Count) PCs of type '$PcType'" -Level "INFO"
} elseif ($BusinessUnit) {
$businessUnitId = $BusinessUnitLookup[$BusinessUnit]
Write-Log "Querying ShopDB for PCs in business unit '$BusinessUnit' (ID: $businessUnitId)..." -Level "INFO"
$shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl -BusinessUnitId $businessUnitId
$computers = $shopfloorPCs | ForEach-Object { $_.hostname } | Where-Object { $_ }
Write-Log "Found $($computers.Count) PCs in business unit '$BusinessUnit'" -Level "INFO"
} elseif ($ComputerListFile) {
if (Test-Path $ComputerListFile) {
$computers = Get-Content $ComputerListFile | Where-Object { $_.Trim() -and -not $_.StartsWith("#") }
} else {
Write-Log "Computer list file not found: $ComputerListFile" -Level "ERROR"
exit 1
}
} elseif ($ComputerName) {
$computers = $ComputerName
} else {
Write-Log "No computers specified. Use -ComputerName, -ComputerListFile, -All, -PcType, or -BusinessUnit" -Level "ERROR"
exit 1
}
if ($computers.Count -eq 0) {
Write-Log "No computers to process." -Level "ERROR"
exit 1
}
Write-Log "Target computers: $($computers.Count)" -Level "INFO"
Write-Log "Task: $Task" -Level "TASK"
Write-Host ""
# Special handling for VerifyNTLARS - no WinRM needed, just API + file comparison
if ($Task -eq 'VerifyNTLARS') {
$backupShare = "\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\ntlars"
# Need ShopDB data - fetch if not already loaded via -All / -PcType / -BusinessUnit
if (-not $shopfloorPCs) {
Write-Log "Fetching all shopfloor PCs from ShopDB for verification..." -Level "INFO"
$shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl
}
# Build list of PCs that have machine numbers (these are the ones that should have backups)
$expectedMachines = @{}
foreach ($pc in $shopfloorPCs) {
if ($pc.hostname -and $pc.machinenumber) {
# Machine numbers can be comma-separated
$numbers = $pc.machinenumber -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
foreach ($num in $numbers) {
if (-not $expectedMachines.ContainsKey($num)) {
$expectedMachines[$num] = @()
}
$expectedMachines[$num] += $pc.hostname.ToUpper()
}
}
}
Write-Log "ShopDB: $($expectedMachines.Count) unique machine numbers across $($shopfloorPCs.Count) PCs" -Level "INFO"
# Verify share is accessible
if (-not (Test-Path $backupShare)) {
Write-Log "Cannot access backup share: $backupShare" -Level "ERROR"
exit 1
}
# List .reg files and extract machine numbers from filenames (prefix before first "-")
$regFiles = Get-ChildItem -Path $backupShare -Filter "*.reg" -ErrorAction SilentlyContinue
$backedUpNumbers = @{}
foreach ($file in $regFiles) {
$baseName = $file.BaseName
if ($baseName -match '^(\d+)-') {
$num = $matches[1]
$backedUpNumbers[$num] = $file.Name
} elseif ($baseName -match '^\d+$') {
# Plain number without hostname suffix (old format)
$backedUpNumbers[$baseName] = $file.Name
}
}
Write-Log "Backup share: $($regFiles.Count) .reg files, $($backedUpNumbers.Count) unique machine numbers" -Level "INFO"
Write-Host ""
# Compare: find machines in ShopDB without backups
$missing = @()
$found = 0
foreach ($num in ($expectedMachines.Keys | Sort-Object)) {
$hosts = $expectedMachines[$num] -join ', '
if ($backedUpNumbers.ContainsKey($num)) {
Write-Log "[OK] Machine $num ($hosts) -> $($backedUpNumbers[$num])" -Level "SUCCESS"
$found++
} else {
Write-Log "[MISSING] Machine $num ($hosts) - no backup found" -Level "WARNING"
$missing += [PSCustomObject]@{
MachineNumber = $num
Hostnames = $hosts
}
}
}
# Check for backups with no ShopDB match (orphans)
$orphans = @()
foreach ($num in ($backedUpNumbers.Keys | Sort-Object)) {
if (-not $expectedMachines.ContainsKey($num)) {
Write-Log "[ORPHAN] $($backedUpNumbers[$num]) - machine $num not in ShopDB" -Level "INFO"
$orphans += $backedUpNumbers[$num]
}
}
# Summary
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " NTLARS BACKUP VERIFICATION" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " ShopDB machines: $($expectedMachines.Count)" -ForegroundColor White
Write-Host " Backups found: $found" -ForegroundColor Green
Write-Host " Missing backups: $($missing.Count)" -ForegroundColor $(if ($missing.Count -gt 0) { "Red" } else { "Green" })
Write-Host " Orphan backups: $($orphans.Count)" -ForegroundColor $(if ($orphans.Count -gt 0) { "Yellow" } else { "White" })
Write-Host " Backup dir: $backupShare" -ForegroundColor White
Write-Host ("=" * 70) -ForegroundColor Cyan
# Export missing list to CSV
if ($missing.Count -gt 0) {
$logDir = Join-Path $PSScriptRoot "logs"
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
$csvPath = Join-Path $logDir "VerifyNTLARS-Missing-$(Get-Date -Format 'yyyy-MM-dd_HHmmss').csv"
$missing | Export-Csv -Path $csvPath -NoTypeInformation -Force
Write-Host ""
Write-Log "Missing machines CSV: $csvPath" -Level "INFO"
Write-Log "Re-run BackupNTLARS targeting these PCs to fill gaps." -Level "INFO"
}
if ($LogFile) {
Stop-Transcript | Out-Null
Write-Host ""
Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray
}
exit 0
}
# Build FQDNs
$targetFQDNs = $computers | ForEach-Object {
if ($_ -like "*.*") { $_ } else { "$_.$DnsSuffix" }
}
# Determine which tasks to run
$tasksToRun = @($Task)
# Create session options
$sessionOption = New-PSSessionOption -OpenTimeout 30000 -OperationTimeout 600000 -NoMachineProfile
# Special handling for CopyFile - push file from source to destination on remote PCs
if ($Task -eq 'CopyFile') {
if (-not $SourcePath) {
Write-Log "-SourcePath is required for CopyFile task" -Level "ERROR"
exit 1
}
if (-not $DestinationPath) {
Write-Log "-DestinationPath is required for CopyFile task" -Level "ERROR"
exit 1
}
if (-not (Test-Path $SourcePath)) {
Write-Log "Source file not found: $SourcePath" -Level "ERROR"
exit 1
}
$fileName = Split-Path $SourcePath -Leaf
$remoteTempPath = "C:\Windows\Temp\$fileName"
Write-Log "CopyFile: $fileName -> $DestinationPath" -Level "INFO"
if ($RunCommand) { Write-Log "Post-copy command: $RunCommand $(if ($AsSystem) { '(as SYSTEM)' } else { '(as logged-in user)' })" -Level "INFO" }
$successCount = 0
$failCount = 0
foreach ($fqdn in $targetFQDNs) {
Write-Host ""
Write-Log "Processing: $fqdn" -Level "TASK"
if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) {
Write-Log "[FAIL] ${fqdn}: Offline or unreachable (ping timeout)" -Level "ERROR"
$failCount++
continue
}
$session = $null
try {
$session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop
Write-Log " Pushing file to remote PC..." -Level "INFO"
Copy-Item -Path $SourcePath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop
Write-Log " Deploying to destination..." -Level "INFO"
$result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['CopyFile'] -ArgumentList $DestinationPath, $RunCommand, [bool]$AsSystem -ErrorAction Stop
Remove-PSSession $session -ErrorAction SilentlyContinue
if ($result.Success) {
Write-Log "[OK] $($result.Hostname): $($result.Output)" -Level "SUCCESS"
$successCount++
} else {
Write-Log "[FAIL] $($result.Hostname): $($result.Error)" -Level "ERROR"
$failCount++
}
} catch {
Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR"
$failCount++
if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue }
}
}
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " Task: CopyFile ($fileName -> $DestinationPath)" -ForegroundColor White
Write-Host " Successful: $successCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
Write-Host ("=" * 70) -ForegroundColor Cyan
exit 0
}
# Special handling for ImportReg - push .reg file and import via scheduled task as logged-in user
if ($Task -eq 'ImportReg') {
if (-not $SourcePath) {
Write-Log "-SourcePath is required for ImportReg task" -Level "ERROR"
exit 1
}
if (-not (Test-Path $SourcePath)) {
Write-Log "Source file not found: $SourcePath" -Level "ERROR"
exit 1
}
if ($SourcePath -notlike "*.reg") {
Write-Log "Warning: SourcePath does not end in .reg - are you sure this is a registry file?" -Level "WARNING"
}
$regFileName = Split-Path $SourcePath -Leaf
$remoteTempPath = "C:\Windows\Temp\$regFileName"
$modeLabel = if ($AsSystem) { "as SYSTEM (HKLM only)" } else { "as logged-in user (HKCU support)" }
Write-Log "ImportReg: $regFileName ($modeLabel)" -Level "INFO"
$successCount = 0
$failCount = 0
foreach ($fqdn in $targetFQDNs) {
Write-Host ""
Write-Log "Processing: $fqdn" -Level "TASK"
if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) {
Write-Log "[FAIL] ${fqdn}: Offline or unreachable (ping timeout)" -Level "ERROR"
$failCount++
continue
}
$session = $null
try {
$session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop
Write-Log " Pushing .reg file to remote PC..." -Level "INFO"
Copy-Item -Path $SourcePath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop
Write-Log " Importing registry file..." -Level "INFO"
$result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['ImportReg'] -ArgumentList $regFileName, [bool]$AsSystem -ErrorAction Stop
Remove-PSSession $session -ErrorAction SilentlyContinue
if ($result.Success) {
Write-Log "[OK] $($result.Hostname): $($result.Output)" -Level "SUCCESS"
$successCount++
} else {
Write-Log "[FAIL] $($result.Hostname): $($result.Error)" -Level "ERROR"
$failCount++
}
} catch {
Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR"
$failCount++
if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue }
}
}
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " Task: ImportReg ($regFileName)" -ForegroundColor White
Write-Host " Successful: $successCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
Write-Host ("=" * 70) -ForegroundColor Cyan
exit 0
}
# Special handling for DeployOpenTextProfiles - zip, push single file, extract and deploy
if ($Task -eq 'DeployOpenTextProfiles') {
$localZipPath = "\\tsgwp00525.wjs.geaerospace.net\shared\dt\csf.zip"
$remoteZipPath = "C:\Windows\Temp\csf.zip"
# Verify zip exists on network share
if (-not (Test-Path $localZipPath)) {
Write-Log "OpenText csf.zip not found: $localZipPath" -Level "ERROR"
exit 1
}
$zipSize = [math]::Round((Get-Item $localZipPath).Length / 1KB, 1)
Write-Log "DeployOpenTextProfiles: csf.zip (${zipSize} KB) from $localZipPath" -Level "INFO"
$successCount = 0
$failCount = 0
foreach ($fqdn in $targetFQDNs) {
Write-Host ""
Write-Log "Processing: $fqdn" -Level "TASK"
if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) {
Write-Log "[FAIL] ${fqdn}: Offline or unreachable (ping timeout)" -Level "ERROR"
$failCount++
continue
}
$session = $null
try {
$session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop
Write-Log " Pushing csf.zip to remote temp..." -Level "INFO"
Copy-Item -Path $localZipPath -Destination $remoteZipPath -ToSession $session -Force -ErrorAction Stop
Write-Log " Extracting and deploying to ProgramData..." -Level "INFO"
$result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['DeployOpenTextProfiles'] -ErrorAction Stop
Remove-PSSession $session -ErrorAction SilentlyContinue
if ($result.Success) {
Write-Log "[OK] $($result.Hostname): $($result.Output)" -Level "SUCCESS"
$successCount++
} else {
Write-Log "[FAIL] $($result.Hostname): $($result.Error)" -Level "ERROR"
$failCount++
}
} catch {
Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR"
$failCount++
if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue }
}
}
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " Task: DeployOpenTextProfiles" -ForegroundColor White
Write-Host " Source: $localZipPath (${zipSize} KB)" -ForegroundColor White
Write-Host " Successful: $successCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
Write-Host ("=" * 70) -ForegroundColor Cyan
exit 0
}
# Special handling for Install/Uninstall kiosk apps
$KioskAppConfig = @{
'InstallDashboard' = @{
Action = 'Install'
InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe'
InstallerName = 'GEAerospaceDashboardSetup.exe'
AppName = 'GE Aerospace Dashboard'
UninstallGuid = '{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}'
KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/shopfloor-dashboard/'
}
'InstallLobbyDisplay' = @{
Action = 'Install'
InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe'
InstallerName = 'GEAerospaceLobbyDisplaySetup.exe'
AppName = 'GE Aerospace Lobby Display'
UninstallGuid = '{42FFB952-0B72-493F-8869-D957344CA305}'
KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/tv-dashboard/'
}
'UninstallDashboard' = @{
Action = 'Uninstall'
InstallerName = 'GEAerospaceDashboardSetup.exe'
AppName = 'GE Aerospace Dashboard'
UninstallGuid = '{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}'
}
'UninstallLobbyDisplay' = @{
Action = 'Uninstall'
InstallerName = 'GEAerospaceLobbyDisplaySetup.exe'
AppName = 'GE Aerospace Lobby Display'
UninstallGuid = '{42FFB952-0B72-493F-8869-D957344CA305}'
}
}
if ($KioskAppConfig.ContainsKey($Task)) {
$appConfig = $KioskAppConfig[$Task]
if ($appConfig.Action -eq 'Install') {
# Find installer from network share
$installerPath = $appConfig.InstallerPath
if (-not (Test-Path $installerPath)) {
Write-Log "Installer not found: $installerPath" -Level "ERROR"
Write-Log "Ensure the installer exists on the network share" -Level "ERROR"
exit 1
}
Write-Log "$($appConfig.Action): $($appConfig.AppName)" -Level "INFO"
Write-Log "Installer: $installerPath" -Level "INFO"
} else {
Write-Log "$($appConfig.Action): $($appConfig.AppName)" -Level "INFO"
}
$successCount = 0
$failCount = 0
foreach ($fqdn in $targetFQDNs) {
Write-Host ""
Write-Log "Processing: $fqdn" -Level "TASK"
# Quick connectivity check before attempting session
if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) {
Write-Log "[FAIL] ${fqdn}: Offline or unreachable (ping timeout)" -Level "ERROR"
$failCount++
continue
}
try {
$session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop
if ($appConfig.Action -eq 'Install') {
$remoteTempPath = "C:\Windows\Temp\$($appConfig.InstallerName)"
Write-Log " Pushing installer to remote PC..." -Level "INFO"
Copy-Item -Path $installerPath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop
Write-Log " Running installer silently..." -Level "INFO"
$result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['InstallKioskApp'] -ArgumentList $remoteTempPath, $appConfig.AppName, $appConfig.KioskUrl -ErrorAction Stop
} else {
Write-Log " Running uninstaller..." -Level "INFO"
$result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['UninstallKioskApp'] -ArgumentList $appConfig.UninstallGuid, $appConfig.AppName -ErrorAction Stop
}
Remove-PSSession $session -ErrorAction SilentlyContinue
if ($result.Success) {
Write-Log "[OK] $($result.Hostname) - $($result.Output)" -Level "SUCCESS"
$successCount++
} else {
$errorMsg = if ($result.Error) { $result.Error } else { "Unknown error" }
Write-Log "[FAIL] $($result.Hostname): $errorMsg" -Level "ERROR"
$failCount++
}
} catch {
Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR"
$failCount++
if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue }
}
}
# Summary
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " Task: $Task" -ForegroundColor White
Write-Host " App: $($appConfig.AppName)" -ForegroundColor White
Write-Host " Successful: $successCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
Write-Host ("=" * 70) -ForegroundColor Cyan
exit 0
}
# Special handling for CheckDefectTracker - report Defect_Tracker.exe process status
if ($Task -eq 'CheckDefectTracker') {
# Fetch shopfloor PCs for machine number lookup
if (-not $shopfloorPCs) {
Write-Log "Fetching shopfloor PC list from API for machine number lookup..." -Level "INFO"
$shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl
}
# Build hostname -> machine number lookup
$machineLookup = @{}
foreach ($pc in $shopfloorPCs) {
if ($pc.hostname -and $pc.machinenumber) {
$machineLookup[$pc.hostname.ToUpper()] = $pc.machinenumber
}
}
$checkResults = @()
$totalChecked = 0
$runningCount = 0
$notRunningCount = 0
$offlineCount = 0
foreach ($fqdn in $targetFQDNs) {
Write-Host ""
Write-Log "Processing: $fqdn" -Level "TASK"
$hostname = ($fqdn -split '\.')[0].ToUpper()
$machineNumber = $machineLookup[$hostname]
# Connectivity check
if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) {
Write-Log "[OFFLINE] ${hostname}: Unreachable" -Level "WARNING"
$offlineCount++
$checkResults += [PSCustomObject]@{
Hostname = $hostname
MachineNumber = $machineNumber
Running = 'N/A'
Status = 'Offline'
}
continue
}
# Open WinRM session and invoke scriptblock
$session = $null
try {
$session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop
$result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['CheckDefectTracker'] -ErrorAction Stop
Remove-PSSession $session -ErrorAction SilentlyContinue
$totalChecked++
$runningStr = if ($result.Running) { 'Yes' } else { 'No' }
if ($result.Running) {
Write-Log "[RUNNING] ${hostname} (Machine: $machineNumber): Defect_Tracker.exe is running" -Level "SUCCESS"
$runningCount++
} else {
Write-Log "[NOT RUNNING] ${hostname} (Machine: $machineNumber): Defect_Tracker.exe is NOT running" -Level "ERROR"
$notRunningCount++
}
$checkResults += [PSCustomObject]@{
Hostname = $hostname
MachineNumber = $machineNumber
Running = $runningStr
Status = 'OK'
}
} catch {
Write-Log "[FAIL] ${hostname}: $($_.Exception.Message)" -Level "ERROR"
if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue }
$offlineCount++
$checkResults += [PSCustomObject]@{
Hostname = $hostname
MachineNumber = $machineNumber
Running = 'N/A'
Status = 'Offline'
}
}
}
# Save CSV report
$logDir = Join-Path $PSScriptRoot "logs"
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
$csvPath = Join-Path $logDir "CheckDefectTracker-$(Get-Date -Format 'yyyy-MM-dd_HHmmss').csv"
if ($checkResults.Count -gt 0) {
$checkResults | Export-Csv -Path $csvPath -NoTypeInformation -Force
Write-Host ""
Write-Log "CSV report saved: $csvPath" -Level "INFO"
}
# Summary
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " DEFECT TRACKER CHECK SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " PCs checked: $totalChecked" -ForegroundColor White
Write-Host " Running: $runningCount" -ForegroundColor $(if ($runningCount -gt 0) { "Green" } else { "White" })
Write-Host " Not running: $notRunningCount" -ForegroundColor $(if ($notRunningCount -gt 0) { "Red" } else { "White" })
Write-Host " Offline: $offlineCount" -ForegroundColor $(if ($offlineCount -gt 0) { "Yellow" } else { "White" })
Write-Host ("=" * 70) -ForegroundColor Cyan
if ($LogFile) {
Stop-Transcript | Out-Null
Write-Host ""
Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray
}
exit 0
}
# Special handling for AuditDNCConfig - compare DNC registry vs UDC backup JSON
if ($Task -eq 'AuditDNCConfig') {
$udcBackupRoot = "\\tsgwp00525.wjs.geaerospace.net\shared\spc\udc\settings_backups"
# We need the shopfloorPCs data for equipment numbers.
# If we already fetched it via -All, -PcType, or -BusinessUnit, reuse it.
# If targeting by -ComputerName or -ComputerListFile, fetch all shopfloor PCs to build the lookup.
if (-not $shopfloorPCs) {
Write-Log "Fetching shopfloor PC list from API for equipment number lookup..." -Level "INFO"
$shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl
}
# Build hostname -> equipment_numbers lookup
$equipmentLookup = @{}
foreach ($pc in $shopfloorPCs) {
if ($pc.hostname -and $pc.machinenumber) {
$equipmentLookup[$pc.hostname.ToUpper()] = $pc.machinenumber
}
}
# Field mapping: Registry key -> UDC JSON path
# Format: RegistryKey = @{ UDCPath; DisplayName }
$fieldMappings = @(
@{ RegKey = 'General_MachineNo'; UDCPath = 'GeneralSettings.MachineNumber'; Display = 'MachineNo/MachineNumber' }
@{ RegKey = 'General_NcIF'; UDCPath = 'GeneralSettings.ControlType'; Display = 'NcIF/ControlType' }
@{ RegKey = 'eFocas_IpAddr'; UDCPath = 'FocasSettings.IPAddress'; Display = 'eFocas IpAddr/FocasSettings.IPAddress' }
@{ RegKey = 'eFocas_SocketNo'; UDCPath = 'FocasSettings.Port'; Display = 'eFocas SocketNo/FocasSettings.Port' }
@{ RegKey = 'Hssb_KRelay1'; UDCPath = 'FocasSettings.KeepRelay'; Display = 'Hssb KRelay1/FocasSettings.KeepRelay' }
@{ RegKey = 'PPDCS_CycleStartInhibits'; UDCPath = 'FocasSettings.InhibitOnOff'; Display = 'PPDCS CycleStart Inhibits/FocasSettings.InhibitOnOff' }
@{ RegKey = 'eFocas_DualPath'; UDCPath = 'GeneralSettings.DualPath'; Display = 'eFocas DualPath/GeneralSettings.DualPath' }
)
$auditResults = @()
$pcChecked = 0
$pcMismatch = 0
$pcNoUDC = 0
$pcOffline = 0
$pcNoEquipment = 0
foreach ($fqdn in $targetFQDNs) {
Write-Host ""
Write-Log "Processing: $fqdn" -Level "TASK"
# Extract hostname from FQDN
$hostname = ($fqdn -split '\.')[0].ToUpper()
# Look up equipment numbers for this PC
$equipmentStr = $equipmentLookup[$hostname]
if (-not $equipmentStr) {
Write-Log "[SKIP] ${hostname}: No equipment number in ShopDB" -Level "WARNING"
$pcNoEquipment++
continue
}
# Split comma-separated equipment numbers
$machineNumbers = $equipmentStr -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
# Connectivity check
if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) {
Write-Log "[FAIL] ${hostname}: Offline or unreachable" -Level "ERROR"
$pcOffline++
continue
}
# Get registry values from remote PC
$regResult = $null
$session = $null
try {
$session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop
$regResult = Invoke-Command -Session $session -ScriptBlock $TaskScripts['AuditDNCConfig'] -ErrorAction Stop
Remove-PSSession $session -ErrorAction SilentlyContinue
} catch {
Write-Log "[FAIL] ${hostname}: $($_.Exception.Message)" -Level "ERROR"
if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue }
$pcOffline++
continue
}
if (-not $regResult.Success) {
Write-Log "[FAIL] ${hostname}: Registry read error - $($regResult.Error)" -Level "ERROR"
$pcOffline++
continue
}
if (-not $regResult.FoundDNC) {
Write-Log "[SKIP] ${hostname}: No DNC registry keys found" -Level "INFO"
$pcNoEquipment++
continue
}
$pcChecked++
$pcHasMismatch = $false
foreach ($machineNum in $machineNumbers) {
# Read UDC backup JSON
$udcJsonPath = Join-Path $udcBackupRoot "udc_settings_${machineNum}.json"
$udcData = $null
if (Test-Path $udcJsonPath) {
try {
$udcData = Get-Content $udcJsonPath -Raw | ConvertFrom-Json
} catch {
Write-Log " [WARN] Could not parse UDC JSON for machine $machineNum" -Level "WARNING"
}
} else {
Write-Log " [WARN] No UDC backup found for machine ${machineNum}: $udcJsonPath" -Level "WARNING"
$pcNoUDC++
}
# Compare each field
foreach ($mapping in $fieldMappings) {
$regValue = $regResult.Values[$mapping.RegKey]
# Get UDC value by navigating the JSON path
$udcValue = $null
if ($udcData) {
$parts = $mapping.UDCPath -split '\.'
$obj = $udcData
foreach ($part in $parts) {
if ($obj) { $obj = $obj.$part }
}
if ($null -ne $obj) { $udcValue = [string]$obj }
}
# Normalize values for comparison
if ($mapping.RegKey -like '*DualPath*' -and $regValue) {
# DualPath: registry YES/NO -> True/False
$regValueNormalized = if ($regValue -eq 'YES') { 'True' } elseif ($regValue -eq 'NO') { 'False' } else { $regValue }
} elseif ($mapping.RegKey -eq 'PPDCS_CycleStartInhibits' -and $regValue) {
# CycleStart Inhibits: registry YES/NO -> True/False
$regValueNormalized = if ($regValue -eq 'YES') { 'True' } elseif ($regValue -eq 'NO') { 'False' } else { $regValue }
} else {
$regValueNormalized = $regValue
}
# Determine status
$status = if ($null -eq $regValue -and $null -eq $udcValue) {
continue # Both absent, skip
} elseif ($null -eq $regValue) {
'REGISTRY_MISSING'
} elseif ($null -eq $udcValue -and -not $udcData) {
'UDC_FILE_MISSING'
} elseif ($null -eq $udcValue) {
'UDC_MISSING'
} elseif ($regValueNormalized -eq $udcValue) {
'MATCH'
} else {
'MISMATCH'
}
$row = [PSCustomObject]@{
Hostname = $hostname
MachineNumber = $machineNum
Field = $mapping.Display
RegistryValue = if ($regValue) { $regValue } else { '' }
UDCValue = if ($udcValue) { $udcValue } else { '' }
Status = $status
}
$auditResults += $row
# Console output
$color = switch ($status) {
'MATCH' { 'Green' }
'MISMATCH' { 'Red' }
'REGISTRY_MISSING' { 'Yellow' }
'UDC_MISSING' { 'Yellow' }
'UDC_FILE_MISSING' { 'DarkYellow' }
default { 'Gray' }
}
if ($status -ne 'MATCH') {
Write-Host " [$status] $($mapping.Display): Registry='$regValue' UDC='$udcValue'" -ForegroundColor $color
$pcHasMismatch = $true
}
}
}
if ($pcHasMismatch) {
Write-Log "[MISMATCH] ${hostname}: Differences found (see above)" -Level "WARNING"
$pcMismatch++
} else {
Write-Log "[OK] ${hostname}: All fields match" -Level "SUCCESS"
}
}
# Save CSV report
$logDir = Join-Path $PSScriptRoot "logs"
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
$csvPath = Join-Path $logDir "AuditDNCConfig-$(Get-Date -Format 'yyyy-MM-dd_HHmmss').csv"
if ($auditResults.Count -gt 0) {
$auditResults | Export-Csv -Path $csvPath -NoTypeInformation -Force
Write-Host ""
Write-Log "CSV report saved: $csvPath" -Level "INFO"
}
# Summary
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " AUDIT SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " PCs checked: $pcChecked" -ForegroundColor White
Write-Host " PCs with mismatches: $pcMismatch" -ForegroundColor $(if ($pcMismatch -gt 0) { "Red" } else { "Green" })
Write-Host " PCs with no UDC backup: $pcNoUDC" -ForegroundColor $(if ($pcNoUDC -gt 0) { "Yellow" } else { "White" })
Write-Host " PCs offline: $pcOffline" -ForegroundColor $(if ($pcOffline -gt 0) { "Red" } else { "White" })
Write-Host " PCs no equipment #: $pcNoEquipment" -ForegroundColor $(if ($pcNoEquipment -gt 0) { "Yellow" } else { "White" })
Write-Host ("=" * 70) -ForegroundColor Cyan
if ($LogFile) {
Stop-Transcript | Out-Null
Write-Host ""
Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray
}
exit 0
}
# Special handling for BackupNTLARS - export DNC registry from each PC, save to share
if ($Task -eq 'BackupNTLARS') {
$backupShare = "\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\ntlars"
# Verify share is accessible
if (-not (Test-Path $backupShare)) {
Write-Log "Cannot access backup share: $backupShare" -Level "ERROR"
Write-Log "Ensure you have access to \\tsgwp00525 and the ntlars folder exists." -Level "ERROR"
exit 1
}
# Run scriptblock on all PCs in parallel
Write-Log "Collecting DNC registry from $($targetFQDNs.Count) PC(s) in parallel..." -Level "INFO"
$invokeParams = @{
ComputerName = $targetFQDNs
ScriptBlock = $TaskScripts['BackupNTLARS']
Credential = $Credential
SessionOption = $sessionOption
ErrorAction = 'SilentlyContinue'
ErrorVariable = 'remoteErrors'
}
if ($ThrottleLimit -and $PSVersionTable.PSVersion.Major -ge 7) {
$invokeParams.ThrottleLimit = $ThrottleLimit
}
$results = Invoke-Command @invokeParams
# Write results to share
$backedUp = 0
$skipped = 0
$failed = 0
foreach ($regResult in $results) {
$hostname = $regResult.Hostname.ToUpper()
if (-not $regResult.Success) {
Write-Log "[FAIL] ${hostname}: $($regResult.Error)" -Level "ERROR"
$failed++
continue
}
if (-not $regResult.FoundDNC) {
Write-Log "[SKIP] ${hostname}: No DNC registry keys found" -Level "INFO"
$skipped++
continue
}
# Determine filename: machine number or serial, always include hostname to avoid collisions
if ($regResult.MachineNo) {
$fileName = "$($regResult.MachineNo)-$hostname.reg"
$label = "Machine $($regResult.MachineNo)"
} elseif ($regResult.Serial) {
$fileName = "$($regResult.Serial)-$hostname.reg"
$label = "Serial $($regResult.Serial) (no machine number)"
} else {
$fileName = "$hostname.reg"
$label = "Hostname $hostname (no machine number or serial)"
}
$destPath = Join-Path $backupShare $fileName
try {
$regResult.RegContent | Out-File -FilePath $destPath -Encoding Unicode -Force
$fileSize = [math]::Round((Get-Item $destPath).Length / 1KB, 1)
Write-Log "[OK] ${hostname}: $label -> $fileName (${fileSize} KB)" -Level "SUCCESS"
$backedUp++
} catch {
Write-Log "[FAIL] ${hostname}: Failed to write $destPath - $($_.Exception.Message)" -Level "ERROR"
$failed++
}
}
# Handle connection errors
foreach ($err in $remoteErrors) {
$target = if ($err.TargetObject) { $err.TargetObject } else { "Unknown" }
Write-Log "[FAIL] ${target}: $($err.Exception.Message)" -Level "ERROR"
$failed++
}
# Summary
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " NTLARS BACKUP SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host " Backed up: $backedUp" -ForegroundColor Green
Write-Host " Skipped: $skipped (no DNC keys)" -ForegroundColor $(if ($skipped -gt 0) { "Yellow" } else { "White" })
Write-Host " Failed: $failed" -ForegroundColor $(if ($failed -gt 0) { "Red" } else { "White" })
Write-Host " Backup dir: $backupShare" -ForegroundColor White
Write-Host ("=" * 70) -ForegroundColor Cyan
if ($LogFile) {
Stop-Transcript | Out-Null
Write-Host ""
Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray
}
exit 0
}
# Process each task
foreach ($currentTask in $tasksToRun) {
if ($tasksToRun.Count -gt 1) {
Write-Host ""
Write-Log "Running task: $currentTask" -Level "TASK"
}
$scriptBlock = $TaskScripts[$currentTask]
if (-not $scriptBlock) {
Write-Log "Unknown task: $currentTask" -Level "ERROR"
continue
}
# Execute on remote computers
$sessionParams = @{
ComputerName = $targetFQDNs
ScriptBlock = $scriptBlock
Credential = $Credential
SessionOption = $sessionOption
ErrorAction = 'SilentlyContinue'
ErrorVariable = 'remoteErrors'
}
if ($ThrottleLimit -and $PSVersionTable.PSVersion.Major -ge 7) {
$sessionParams.ThrottleLimit = $ThrottleLimit
}
Write-Log "Executing on $($targetFQDNs.Count) computer(s)..." -Level "INFO"
$results = Invoke-Command @sessionParams
# Process results
$successCount = 0
$failCount = 0
foreach ($result in $results) {
if ($result.Success) {
Write-Log "[OK] $($result.Hostname)" -Level "SUCCESS"
# Display task-specific output
switch ($currentTask) {
'OptimizeDisk' {
foreach ($drive in $result.Drives) {
$status = if ($drive.Success) { "OK" } else { "FAIL" }
Write-Host " Drive $($drive.DriveLetter): $($drive.MediaType) - $($drive.Action) [$status]" -ForegroundColor Gray
}
}
'DISM' {
Write-Host " Duration: $($result.Duration) minutes, Exit code: $($result.ExitCode)" -ForegroundColor Gray
}
'SFC' {
Write-Host " $($result.Summary), Duration: $($result.Duration) minutes" -ForegroundColor Gray
}
'DiskCleanup' {
Write-Host " Space freed: $($result.SpaceFreed) GB" -ForegroundColor Gray
}
'UpdateDNCMXHosts' {
Write-Host " $($result.Output)" -ForegroundColor Gray
foreach ($change in $result.Changed) {
Write-Host " [changed] $change" -ForegroundColor Green
}
foreach ($warn in $result.Warnings) {
Write-Host " [warn] $warn" -ForegroundColor Yellow
}
foreach ($skip in $result.Skipped) {
Write-Host " [skip] $skip" -ForegroundColor DarkGray
}
}
default {
if ($result.Output) {
Write-Host " $($result.Output)" -ForegroundColor Gray
}
}
}
$successCount++
} else {
$errorMsg = if ($result.FailReason) { $result.FailReason } else { $result.Error }
Write-Log "[FAIL] $($result.Hostname): $errorMsg" -Level "ERROR"
$failCount++
}
}
# Handle connection errors
foreach ($err in $remoteErrors) {
$target = if ($err.TargetObject) { $err.TargetObject } else { "Unknown" }
Write-Log "[FAIL] ${target}: $($err.Exception.Message)" -Level "ERROR"
$failCount++
}
}
# Summary
Write-Host ""
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Host " SUMMARY" -ForegroundColor Cyan
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Host " Task: $Task" -ForegroundColor White
Write-Host " Successful: $successCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
Write-Host "=" * 70 -ForegroundColor Cyan
if ($LogFile) {
Stop-Transcript | Out-Null
Write-Host ""
Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray
}