Files
powershell-scripts/remote-execution/Invoke-RemoteMaintenance.ps1
cproudlock 7ed9f42f44 Add UpdateEMxInfo task for remote DNC eMxInfo.txt updates
- New task to update eMxInfo.txt on shopfloor PCs via WinRM
- Pushes file from local workstation to avoid double-hop auth issues
- Checks both Program Files (x86) and Program Files paths
- Backs up existing file with date stamp before replacing
- Kills DNCMain.exe before copy, restarts LDnc.exe after success
- Reports per-path success/failure with detailed error messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 16:59:01 -05:00

1097 lines
38 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 database updates.
.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 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:
- UpdateEMxInfo : Update eMxInfo.txt from network share (backs up old file first)
.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
# Update database with disk health info
.\Invoke-RemoteMaintenance.ps1 -All -Task DiskHealth
.EXAMPLE
# Run all database update tasks
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task AllDatabaseUpdates
.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(Mandatory=$true)]
[ValidateSet(
'DISM', 'SFC', 'OptimizeDisk', 'DiskCleanup', 'ClearUpdateCache',
'RestartSpooler', 'FlushDNS', 'ClearBrowserCache', 'RestartWinRM',
'SetTimezone', 'SyncTime', 'UpdateEMxInfo'
)]
[string]$Task,
[Parameter()]
[PSCredential]$Credential,
[Parameter()]
[string]$ApiUrl = "https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp",
[Parameter()]
[string]$DnsSuffix = "logon.ds.ge.com",
[Parameter()]
[int]$ThrottleLimit = 5
)
# =============================================================================
# SSL/TLS Configuration
# =============================================================================
try {
if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) {
Add-Type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"@
}
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
} catch { }
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# =============================================================================
# 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)
try {
$response = Invoke-RestMethod -Uri "$ApiUrl`?action=getShopfloorPCs" -Method Get -ErrorAction Stop
if ($response.success -and $response.data) {
return $response.data
}
return @()
} catch {
Write-Log "Failed to query API: $_" -Level "ERROR"
return @()
}
}
# =============================================================================
# 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
}
# -------------------------------------------------------------------------
# UpdateEMxInfo - Backup and prepare for file copy (runs on remote PC)
# The actual file is pushed via Copy-Item -ToSession from the caller
# -------------------------------------------------------------------------
'UpdateEMxInfo' = {
param($SourceFileContent)
$result = @{
Success = $false
Task = 'UpdateEMxInfo'
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
FailReason = ""
PathsUpdated = @()
PathsFailed = @()
BackupsCreated = @()
TempFile = ""
DNCKilled = $false
DNCRestarted = $false
}
$destFile = "eMxInfo.txt"
$tempPath = "C:\Windows\Temp\eMxInfo-2026.txt"
# Check both possible DNC installation paths
$destDirs = @(
"C:\Program Files (x86)\DNC\Server Files",
"C:\Program Files\DNC\Server Files"
)
try {
# Check if temp file was pushed
if (-not (Test-Path $tempPath)) {
$result.FailReason = "Source file not found at $tempPath - file push may have failed"
$result.Error = $result.FailReason
Write-Output $result.FailReason
return $result
}
# Track which paths exist
$validPaths = @()
foreach ($destDir in $destDirs) {
if (Test-Path $destDir) {
$validPaths += $destDir
}
}
if ($validPaths.Count -eq 0) {
$result.FailReason = "No DNC Server Files directory found in Program Files or Program Files (x86)"
$result.Error = $result.FailReason
Write-Output $result.FailReason
# Clean up temp file
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
return $result
}
Write-Output "Found $($validPaths.Count) DNC installation(s)"
# Kill DNCMain.exe before copying
Write-Output "Stopping DNCMain.exe..."
$dncProcess = Get-Process -Name "DNCMain" -ErrorAction SilentlyContinue
if ($dncProcess) {
try {
taskkill /IM DNCMain.exe /F 2>&1 | Out-Null
Start-Sleep -Seconds 2
$result.DNCKilled = $true
Write-Output " DNCMain.exe stopped"
} catch {
Write-Output " Warning: Could not stop DNCMain.exe - $($_.Exception.Message)"
}
} else {
Write-Output " DNCMain.exe not running"
}
# Process each valid path
foreach ($destDir in $validPaths) {
$destPath = Join-Path $destDir $destFile
$pathLabel = if ($destDir -like "*x86*") { "x86" } else { "x64" }
Write-Output "Processing $pathLabel path: $destDir"
# Check if destination file exists and back it up
if (Test-Path $destPath) {
$dateStamp = Get-Date -Format "yyyyMMdd"
$backupName = "eMxInfo-old-$dateStamp.txt"
$backupPath = Join-Path $destDir $backupName
Write-Output " File exists, renaming to $backupName..."
try {
# Remove existing backup if same date
if (Test-Path $backupPath) {
Remove-Item $backupPath -Force -ErrorAction Stop
}
Rename-Item -Path $destPath -NewName $backupName -Force -ErrorAction Stop
$result.BackupsCreated += "$pathLabel`:$backupName"
} catch {
$result.PathsFailed += "$pathLabel`: Failed to rename - $($_.Exception.Message)"
Write-Output " FAILED to rename: $($_.Exception.Message)"
continue
}
} else {
Write-Output " File does not exist, creating new..."
}
# Copy from temp location to destination
try {
Copy-Item -Path $tempPath -Destination $destPath -Force -ErrorAction Stop
# Verify the copy
if (Test-Path $destPath) {
$result.PathsUpdated += $pathLabel
Write-Output " SUCCESS"
} else {
$result.PathsFailed += "$pathLabel`: Copy succeeded but file not found"
Write-Output " FAILED: File not found after copy"
}
} catch {
$result.PathsFailed += "$pathLabel`: Failed to copy - $($_.Exception.Message)"
Write-Output " FAILED to copy: $($_.Exception.Message)"
}
}
# Clean up temp file
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
# Determine overall success
if ($result.PathsUpdated.Count -gt 0) {
$result.Success = $true
$result.Output = "Updated: $($result.PathsUpdated -join ', ')"
if ($result.BackupsCreated.Count -gt 0) {
$result.Output += " | Backups: $($result.BackupsCreated -join ', ')"
}
if ($result.PathsFailed.Count -gt 0) {
$result.Output += " | Failed: $($result.PathsFailed.Count)"
}
# Restart DNC - find LDnc.exe in one of the valid paths
Write-Output "Starting LDnc.exe..."
$ldncStarted = $false
foreach ($destDir in $validPaths) {
$ldncPath = Join-Path $destDir "LDnc.exe"
if (Test-Path $ldncPath) {
try {
Start-Process -FilePath $ldncPath -ErrorAction Stop
$result.DNCRestarted = $true
$ldncStarted = $true
Write-Output " LDnc.exe started from $destDir"
break
} catch {
Write-Output " Warning: Could not start LDnc.exe from $destDir - $($_.Exception.Message)"
}
}
}
if (-not $ldncStarted) {
Write-Output " Warning: LDnc.exe not found or could not be started"
}
$result.Output += " | DNC restarted: $($result.DNCRestarted)"
} else {
$result.FailReason = "All paths failed: $($result.PathsFailed -join '; ')"
$result.Error = $result.FailReason
}
} catch {
$result.FailReason = "Unexpected error: $($_.Exception.Message)"
$result.Error = $result.FailReason
Write-Output $result.FailReason
}
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 ""
# 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 ($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, or -All" -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 ""
# 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 UpdateEMxInfo - requires pushing file first
if ($Task -eq 'UpdateEMxInfo') {
$sourcePath = "\\tsgwp00525.rd.ds.ge.com\shared\cameron\eMxInfo-2026.txt"
$remoteTempPath = "C:\Windows\Temp\eMxInfo-2026.txt"
Write-Log "UpdateEMxInfo: Checking source file..." -Level "INFO"
if (-not (Test-Path $sourcePath)) {
Write-Log "Source file not found: $sourcePath" -Level "ERROR"
exit 1
}
Write-Log "Source file found. Will push to each PC before executing." -Level "INFO"
$successCount = 0
$failCount = 0
foreach ($fqdn in $targetFQDNs) {
Write-Host ""
Write-Log "Processing: $fqdn" -Level "TASK"
try {
# Create session
$session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop
# Push the file to remote temp location
Write-Log " Pushing file to remote PC..." -Level "INFO"
Copy-Item -Path $sourcePath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop
# Execute the scriptblock
Write-Log " Executing update task..." -Level "INFO"
$result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['UpdateEMxInfo'] -ErrorAction Stop
# Close session
Remove-PSSession $session -ErrorAction SilentlyContinue
# Process result
if ($result.Success) {
Write-Log "[OK] $($result.Hostname)" -Level "SUCCESS"
Write-Host " $($result.Output)" -ForegroundColor Gray
if ($result.PathsFailed.Count -gt 0) {
foreach ($fail in $result.PathsFailed) {
Write-Host " [!] $fail" -ForegroundColor Yellow
}
}
$successCount++
} else {
$errorMsg = if ($result.FailReason) { $result.FailReason } else { $result.Error }
Write-Log "[FAIL] $($result.Hostname): $errorMsg" -Level "ERROR"
$failCount++
}
} catch {
Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR"
$failCount++
# Clean up session if it exists
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 " Successful: $successCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
Write-Host ("=" * 70) -ForegroundColor Cyan
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
}
'UpdateEMxInfo' {
Write-Host " $($result.Output)" -ForegroundColor Gray
if ($result.PathsFailed.Count -gt 0) {
foreach ($fail in $result.PathsFailed) {
Write-Host " [!] $fail" -ForegroundColor Yellow
}
}
}
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