From 7c3093923448d7fc76812f2e48c80061ac1a4523 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 30 Dec 2025 13:08:08 -0500 Subject: [PATCH] Add maintenance toolkit, DNC/OnGuard utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Invoke-RemoteMaintenance.ps1: Remote maintenance tasks (DISM, SFC, disk cleanup, etc.) - Add DNC/, dncfix/, edncfix/: DNC configuration utilities - Add onguard/: OnGuard integration scripts - Add tools/: Additional utility scripts - Update remote-execution/README.md with maintenance toolkit docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- DNC/update_dnc.bat | 12 + dncfix/fixvtm.bat | 3 + dncfix/vtmfix.ps1 | 64 ++ edncfix | 1 + onguard/silentInstall.ps1 | 93 ++ remote-execution/Invoke-RemoteMaintenance.ps1 | 836 ++++++++++++++++++ remote-execution/README.md | 100 ++- tools/Enable-WinRM.bat | 40 + 8 files changed, 1127 insertions(+), 22 deletions(-) create mode 100644 DNC/update_dnc.bat create mode 100644 dncfix/fixvtm.bat create mode 100644 dncfix/vtmfix.ps1 create mode 160000 edncfix create mode 100644 onguard/silentInstall.ps1 create mode 100644 remote-execution/Invoke-RemoteMaintenance.ps1 create mode 100644 tools/Enable-WinRM.bat diff --git a/DNC/update_dnc.bat b/DNC/update_dnc.bat new file mode 100644 index 0000000..d21474e --- /dev/null +++ b/DNC/update_dnc.bat @@ -0,0 +1,12 @@ +@echo off +echo Updating DNC files... + +echo Stopping DncMain.exe if running... +taskkill /F /IM DncMain.exe 2>nul + +copy /Y "S:\DT\DNC\DNCdll.dll" "C:\Program Files (x86)\dnc\bin\" +copy /Y "S:\DT\DNC\DncMain.exe" "C:\Program Files (x86)\dnc\bin\" +copy /Y "S:\DT\DNC\mxTransactionDll.dll" "C:\Program Files (x86)\dnc\bin\" + +echo Done. +pause diff --git a/dncfix/fixvtm.bat b/dncfix/fixvtm.bat new file mode 100644 index 0000000..fb6386e --- /dev/null +++ b/dncfix/fixvtm.bat @@ -0,0 +1,3 @@ +@echo off +powershell.exe -ExecutionPolicy Bypass -File ".\vtmfix.ps1" +pause diff --git a/dncfix/vtmfix.ps1 b/dncfix/vtmfix.ps1 new file mode 100644 index 0000000..755ce1f --- /dev/null +++ b/dncfix/vtmfix.ps1 @@ -0,0 +1,64 @@ +# Real-time file watcher to strip 0xFF from .pun files +# Watches folder and cleans files as soon as they're created/modified + +$watchFolder = "C:\Dnc_Files\Q" # Change to your DNC program folder +$fileFilter = "*.pun" # Watch .pun files + +Write-Host "Watching $watchFolder for new/modified $fileFilter files..." +Write-Host "Press Ctrl+C to stop" + +# Create file system watcher +$watcher = New-Object System.IO.FileSystemWatcher +$watcher.Path = $watchFolder +$watcher.Filter = $fileFilter +$watcher.IncludeSubdirectories = $true +$watcher.EnableRaisingEvents = $true + +# Define what to do when file is created or changed +$action = { + $path = $Event.SourceEventArgs.FullPath + $changeType = $Event.SourceEventArgs.ChangeType + + Write-Host "$(Get-Date -Format 'HH:mm:ss') - $changeType detected: $path" + + # Wait a moment for file to finish writing + Start-Sleep -Milliseconds 500 + + try { + # Read file as bytes + $bytes = [System.IO.File]::ReadAllBytes($path) + $originalCount = $bytes.Count + + # Remove all 0xFF bytes + $cleaned = $bytes | Where-Object { $_ -ne 255 } + $newCount = $cleaned.Count + + # Only rewrite if we found 0xFF + if ($originalCount -ne $newCount) { + [System.IO.File]::WriteAllBytes($path, $cleaned) + $removed = $originalCount - $newCount + Write-Host " [OK] Cleaned! Removed $removed byte(s) [0xFF]" -ForegroundColor Green + } else { + Write-Host " [SKIP] No 0xFF found, file OK" -ForegroundColor Gray + } + } + catch { + Write-Host " [ERROR] $_" -ForegroundColor Red + } +} + +# Register event handlers +Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action +Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action + +# Keep script running +try { + while ($true) { + Start-Sleep -Seconds 1 + } +} +finally { + # Cleanup on exit + $watcher.Dispose() + Write-Host "`nStopped watching folder" +} diff --git a/edncfix b/edncfix new file mode 160000 index 0000000..28641c4 --- /dev/null +++ b/edncfix @@ -0,0 +1 @@ +Subproject commit 28641c47c5af83021bd6b5fefe7b6dcc22a4251b diff --git a/onguard/silentInstall.ps1 b/onguard/silentInstall.ps1 new file mode 100644 index 0000000..181966c --- /dev/null +++ b/onguard/silentInstall.ps1 @@ -0,0 +1,93 @@ +#Requires -RunAsAdministrator + +param( + + [Parameter(Mandatory=$True)] + [string] + $LicenseServer, + + [Parameter(Mandatory=$True)] + [string] + $DatabaseServer + +) + +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass + +# Only Client installations are supported at this time +# $Features can optionally be assigned to an array of specific Client Features from within available features +# The available client features are: AlarmMonitoring AreaAccessManager BadgeDesigner DeviceDicoveryService DeviceDiscovery FormsDesigner IDCredentialCenter MapDesigner SystemAdministration VideoViewer VisitorManagement +# example: $Features = @("AlarmMonitoring","IDCredentialCenter","SystemAdministration") + +$Features = "Client" + +$availableFeatures = @("AlarmMonitoring","AreaAccessManager","BadgeDesigner","DeviceDicoveryService","DeviceDiscovery","FormsDesigner","IDCredentialCenter","MapDesigner","SystemAdministration","VideoViewer","VisitorManagement","ApplicationServer","ClientUpdateServer","CommunicationServer","DataConduITService","DataExchangeServer","EnterpriseAdministration","GlobalOutputServer","IDAllocationService","Import","LicenseSystemServer","LoginDriver","OpenAccess","Replicator","ReportsDashboard","SetupDB","UniversalTimeConversionUtility","VideoArchiveServer") + +# Formats the feature list parameter based on the installation type or features passed in +if ( "Client" -in $Features ) { + Write-Host "Installing client features silently." + $featureParam = "ADDLOCAL=""AlarmMonitoring,AreaAccessManager,BadgeDesigner,DeviceDicoveryService,DeviceDiscovery,FormsDesigner,IDCredentialCenter,MapDesigner,SystemAdministration,VideoViewer,VisitorManagement"" REMOVE=""ApplicationServer,ClientUpdateServer,CommunicationServer,DataConduITService,DataExchangeServer,EnterpriseAdministration,GlobalOutputServer,IDAllocationService,Import,LicenseSystemServer,LoginDriver,OpenAccess,Replicator,ReportsDashboard,SetupDB,UniversalTimeConversionUtility,VideoArchiveServer""" + Write-Host $featureParam +} +elseif ( -not @($Features | where {$availableFeatures -notcontains $_}).Count ) { + # Ensures any arguments are in the available features + # Features can optionabe an array of available Client Features Only + Write-Host "Installing a custom set of features." + $removeFeatures = @() + foreach ($feature in $availableFeatures) { + if ($args -notcontains $feature) { + $removeFeatures += $feature + } + } + $featureParam = "ADDLOCAL=""$($args -join ",")"" REMOVE=""$($removeFeatures -join ",")""" +} +else { + Throw "An error was encountered with the arguments" +} + +if ( test-path 'HKLM:\SOFTWARE\WOW6432Node\Lenel\OnGuard') { + $productCode = Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Lenel\OnGuard -Name "ProductCode" + $systemtypedetect="PREV101102=""102""" +} +else { + $systemtypedetect="SYSTEMTYPE=""C""" +} + +# When an existing instance is installed: backup the registry, remove the product, restore the registry values for the reinstall to pickup +If ( $productCode ) { + Md Registry::HKLM\SOFTWARE\WOW6432Node\LenelBackup -Force + Copy-Item -Path Registry::HKLM\SOFTWARE\WOW6432Node\Lenel\OnGuard -Destination Registry::HKLM\SOFTWARE\WOW6432Node\LenelBackup -Recurse + $Og_productCode=$productCode.ProductCode.ToString() + cmd /c msiexec.exe /x $Og_productCode /qn + Write-Host $productCode.ProductCode + Copy-Item -Path Registry::HKLM\SOFTWARE\WOW6432Node\LenelBackup\OnGuard -Destination Registry::HKLM\SOFTWARE\WOW6432Node\Lenel -Recurse +} + +$currentpath = (Get-Item -Path ".\").FullName + +# Install Microsoft Direct X for Managed Code +Write-Host "Installing Direct X for Managed Code..." +cmd /c msiexec.exe /i "$($currentpath)\Windows\Temp\DXManaged\mdxredist.msi" /qn + +# Install Acuant Capture/Scanning SDK +Write-Host "Install Acuant Capture/Scanning SDK..." +cmd /c "$($currentpath)\Windows\Temp\CSSN_SDK\sdk_setup_is.exe" /s /a /s /f1 "$($currentpath)\Windows\Temp\CSSN_SDK\setup.iss" + +# Install Crystal Reports Runtime +Write-Host "Installing Crystal Reports Runtime" +cmd /c msiexec.exe /i "$($currentpath)\Windows\Temp\Crystal\CRRuntime_32bit_13_0_32.msi" /qn UPGRADE=1 + +# Install Microsoft Windows Media Encoder +Write-Host "Installing Microsoft Windows Media Encoder" +cmd /c msiexec.exe /i "$($currentpath)\Windows\Temp\WMEncoder\WMEncoder.msi" /qn + +# Install UltrView SDK +Write-Host "Installing UltrView SDK" +cmd /c "$($currentpath)\Windows\Temp\UltraView\UltraViewSoftwareDevelopmentKit.exe" -silent + +# Setting environmental variables +cmd /c setx /M PATH "%PATH%;C:\Program Files (x86)\Common Files\Lenel;C:\Program Files (x86)\Acuant\SDK" | Out-Null + +# Install OnGuard +Write-Host "Installing OnGuard" +cmd /c "$($currentpath)\setup.exe" /s /v"/qn /L*V "$($env:LOCALAPPDATA)\OnGuardSetup.log" $systemtypedetect LICENSESERVER=$LicenseServer DSN=$DatabaseServer DATABASETYPE="SQL" REBOOT=Suppress $featureParam" diff --git a/remote-execution/Invoke-RemoteMaintenance.ps1 b/remote-execution/Invoke-RemoteMaintenance.ps1 new file mode 100644 index 0000000..e36f867 --- /dev/null +++ b/remote-execution/Invoke-RemoteMaintenance.ps1 @@ -0,0 +1,836 @@ +<# +.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 + +.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' + )] + [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 + } +} + +# ============================================================================= +# 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 + +# 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 + } + default { + if ($result.Output) { + Write-Host " $($result.Output)" -ForegroundColor Gray + } + } + } + + $successCount++ + + } else { + Write-Log "[FAIL] $($result.Hostname): $($result.Error)" -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 diff --git a/remote-execution/README.md b/remote-execution/README.md index edce6f2..8aa03b8 100644 --- a/remote-execution/README.md +++ b/remote-execution/README.md @@ -25,6 +25,57 @@ Or run PowerShell directly: ## PowerShell Scripts +### Invoke-RemoteMaintenance.ps1 +**Remote maintenance toolkit** - Execute maintenance tasks on shopfloor PCs via WinRM. + +**Available Tasks:** + +| Category | Task | Description | +|----------|------|-------------| +| **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 | + +**Usage:** +```powershell +# Run DISM on a single PC +.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF" -Task DISM + +# Optimize disks on multiple PCs +.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01","PC02" -Task OptimizeDisk + +# Run disk cleanup on all shopfloor PCs +.\Invoke-RemoteMaintenance.ps1 -All -Task DiskCleanup + +# Clear Windows Update cache (fixes stuck updates) +.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task ClearUpdateCache +``` + +**Parameters:** +| Parameter | Default | Description | +|-----------|---------|-------------| +| `-ComputerName` | - | Single or multiple computer names/IPs | +| `-ComputerListFile` | - | Path to text file with computer list | +| `-All` | - | Target all shopfloor PCs from ShopDB | +| `-Task` | (required) | Maintenance task to execute | +| `-Credential` | (prompts) | PSCredential for authentication | +| `-ThrottleLimit` | `5` | Maximum concurrent sessions | + +**Notes:** +- DISM and SFC tasks can take 10-30 minutes per PC +- OptimizeDisk automatically detects SSD vs HDD +- ClearUpdateCache stops Windows Update service, clears cache, restarts service + +--- + ### Invoke-RemoteAssetCollection.ps1 **Remote collection via WinRM HTTP** - Execute asset collection on multiple PCs using WinRM over HTTP (port 5985). @@ -151,28 +202,33 @@ $cred = Get-Credential ## Architecture ``` -┌─────────────────────────────────────┐ -│ Management Server │ -│ ┌───────────────────────────────┐ │ -│ │ Invoke-RemoteAssetCollection │ │ -│ │ Update-ShopfloorPCs-Remote │ │ -│ └──────────────┬────────────────┘ │ -└─────────────────┼───────────────────┘ - │ WinRM (5985/5986) - ▼ -┌─────────────────────────────────────┐ -│ Shopfloor PC 1 │ -│ ┌───────────────────────────────┐ │ -│ │ Update-PC-CompleteAsset.ps1 │ │ -│ └───────────────────────────────┘ │ -└─────────────────────────────────────┘ -┌─────────────────────────────────────┐ -│ Shopfloor PC 2 │ -│ ┌───────────────────────────────┐ │ -│ │ Update-PC-CompleteAsset.ps1 │ │ -│ └───────────────────────────────┘ │ -└─────────────────────────────────────┘ - ... (parallel execution) +┌──────────────────────────────────────────────────────────────┐ +│ Management Server │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Update-ShopfloorPCs-Remote.ps1 - Data collection │ │ +│ │ Invoke-RemoteMaintenance.ps1 - Maintenance tasks │ │ +│ │ Invoke-RemoteAssetCollection.ps1 - General execution │ │ +│ └────────────────────────┬───────────────────────────────┘ │ +└───────────────────────────┼──────────────────────────────────┘ + │ WinRM (5985/5986) + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ Shopfloor PCs │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Data Collection: │ │ +│ │ - System info, network, DNC config, installed apps │ │ +│ │ │ │ +│ │ Maintenance Tasks: │ │ +│ │ - DISM, SFC, Disk Cleanup, Optimize Disk │ │ +│ │ - Restart Spooler, Flush DNS, Clear Caches │ │ +│ └────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ HTTPS POST +┌──────────────────────────────────────────────────────────────┐ +│ ShopDB API │ +│ api.asp -> MySQL (machines, communications, dncconfig) │ +└──────────────────────────────────────────────────────────────┘ ``` ## WinRM Setup diff --git a/tools/Enable-WinRM.bat b/tools/Enable-WinRM.bat new file mode 100644 index 0000000..1535393 --- /dev/null +++ b/tools/Enable-WinRM.bat @@ -0,0 +1,40 @@ +@echo off +:: Enable WinRM for Remote Management +:: Run as Administrator + +echo ============================================ +echo Enable WinRM for ShopDB Remote Management +echo ============================================ +echo. + +:: Check for admin rights +net session >nul 2>&1 +if %errorLevel% neq 0 ( + echo ERROR: Please run as Administrator! + echo Right-click and select "Run as administrator" + pause + exit /b 1 +) + +echo Enabling WinRM... +powershell -Command "Enable-PSRemoting -Force -SkipNetworkProfileCheck" 2>nul + +echo. +echo Setting firewall rules... +powershell -Command "Set-NetFirewallRule -Name 'WINRM-HTTP-In-TCP' -Enabled True -Profile Any" 2>nul +netsh advfirewall firewall add rule name="WinRM-HTTP" dir=in localport=5985 protocol=TCP action=allow >nul 2>&1 + +echo. +echo Starting WinRM service... +sc config winrm start= auto >nul +net start winrm >nul 2>&1 + +echo. +echo ============================================ +echo WinRM Enabled Successfully! +echo This PC can now be remotely managed. +echo ============================================ +echo. +echo Hostname: %COMPUTERNAME% +echo. +pause