- 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>
2694 lines
106 KiB
PowerShell
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
|
|
}
|