Files
powershell-scripts/remote-execution/Update-ShopfloorPCs-Remote.ps1
cproudlock 7d3519f613 Add comprehensive documentation and update deployment paths
Documentation:
- Add ShopDB-API.md with full API reference (all GET/POST endpoints)
- Add detailed docs for Update-ShopfloorPCs-Remote, Invoke-RemoteMaintenance, Update-PC-CompleteAsset
- Add DATA_COLLECTION_PARITY.md comparing local vs remote data collection
- Add HTML versions of all documentation with styled code blocks
- Document software deployment mechanism and how to add new apps
- Document deprecated scripts (Invoke-RemoteAssetCollection, Install-KioskApp)

Script Updates:
- Update deployment source paths to network share (tsgwp00525.wjs.geaerospace.net)
  - InstallDashboard: \\...\scripts\Dashboard\GEAerospaceDashboardSetup.exe
  - InstallLobbyDisplay: \\...\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe
  - UpdateEMxAuthToken: \\...\scripts\eMx\eMxInfo.txt
  - DeployUDCWebServerConfig: \\...\scripts\UDC\udc_webserver_settings.json
- Update machine network detection to include 100.0.0.* for CMM cases
- Rename PC Type #9 from "Part Marker" to "Inspection"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 11:45:00 -05:00

1686 lines
68 KiB
PowerShell

<#
.SYNOPSIS
Remotely collects PC information from shopfloor PCs via WinRM and updates ShopDB.
.DESCRIPTION
This script uses WinRM (Invoke-Command) to remotely execute commands on shopfloor PCs,
collect system information, and POST it to the ShopDB API.
.PARAMETER ComputerName
Single computer name or array of computer names to update.
.PARAMETER All
Query ShopDB for all shopfloor PCs and update them.
.PARAMETER Credential
PSCredential object for authentication. If not provided, will prompt or use current user.
.PARAMETER ApiUrl
URL to the ShopDB API. Defaults to http://192.168.122.151:8080/api.asp
.EXAMPLE
# Update a single PC
.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "SHOPFLOOR-PC01"
.EXAMPLE
# Update multiple PCs
.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "PC01","PC02","PC03"
.EXAMPLE
# Update all shopfloor PCs from ShopDB
.\Update-ShopfloorPCs-Remote.ps1 -All
.EXAMPLE
# With specific credentials
$cred = Get-Credential
.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "SHOPFLOOR-PC01" -Credential $cred
.EXAMPLE
# Reboot all PCs with uptime >= 30 days (will prompt for confirmation)
.\Update-ShopfloorPCs-Remote.ps1 -Reboot -MinUptimeDays 30
.EXAMPLE
# Preview which PCs would be rebooted (no actual reboot)
.\Update-ShopfloorPCs-Remote.ps1 -Reboot -MinUptimeDays 30 -WhatIf
.EXAMPLE
# Reboot PCs with uptime >= 60 days without confirmation prompt
.\Update-ShopfloorPCs-Remote.ps1 -Reboot -MinUptimeDays 60 -Force -Credential $cred
.NOTES
Requires:
- WinRM enabled on target PCs (Enable-PSRemoting)
- Admin credentials on target PCs
- Network access to target PCs on port 5985 (HTTP) or 5986 (HTTPS)
#>
[CmdletBinding(DefaultParameterSetName='ByName')]
param(
[Parameter(ParameterSetName='ByName', Position=0)]
[string[]]$ComputerName,
[Parameter(ParameterSetName='All')]
[switch]$All,
[Parameter(ParameterSetName='SetupTrust')]
[switch]$SetupTrustedHosts,
[Parameter(ParameterSetName='Reboot', Mandatory)]
[switch]$Reboot,
[Parameter(ParameterSetName='Reboot', Mandatory)]
[int]$MinUptimeDays,
[Parameter(ParameterSetName='Reboot')]
[switch]$Force,
[Parameter()]
[PSCredential]$Credential,
[Parameter()]
[string]$ApiUrl = "https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp",
[Parameter()]
[string]$DnsSuffix = "logon.ds.ge.com",
[Parameter()]
[switch]$SkipDnsLookup,
[Parameter()]
[switch]$UseSSL,
[Parameter()]
[int]$ThrottleLimit = 25,
[Parameter()]
[switch]$WhatIf
)
# SSL/TLS Configuration - MUST be set before any web requests
# Set ALL modern TLS versions - fixes "underlying connection was closed" errors
try {
# Enable all available TLS protocols (1.0, 1.1, 1.2, 1.3 if available)
$protocols = [Net.SecurityProtocolType]::Tls12
try { $protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13 } catch {}
try { $protocols = $protocols -bor [Net.SecurityProtocolType]::Tls11 } catch {}
try { $protocols = $protocols -bor [Net.SecurityProtocolType]::Tls } catch {}
[Net.ServicePointManager]::SecurityProtocol = $protocols
} catch {
# Absolute fallback
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
# Increase connection limits and timeouts
[Net.ServicePointManager]::DefaultConnectionLimit = 100
[Net.ServicePointManager]::Expect100Continue = $false
[Net.ServicePointManager]::MaxServicePointIdleTime = 10000
# Certificate Bypass for HTTPS connections (self-signed certs) - PowerShell 5.x
if ($PSVersionTable.PSVersion.Major -lt 7) {
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 {
Write-Warning "Could not set certificate policy: $_"
}
# Also set callback for newer .NET versions (backup method)
try {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { param($sender, $cert, $chain, $errors) return $true }
} catch {}
}
# Load System.Web for URL encoding (used in WebClient fallback)
try {
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
} catch {}
#region 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" }
default { "White" }
}
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
}
function Get-ShopfloorPCsFromApi {
<#
.SYNOPSIS
Queries ShopDB API to get list of shopfloor PCs with details.
#>
param([string]$ApiUrl)
$fullUrl = "$ApiUrl`?action=getShopfloorPCs"
Write-Log "Querying API: $fullUrl" -Level "INFO"
# Try PowerShell native method first
try {
# Build params - use SkipCertificateCheck for PS7+, rely on ServicePointManager for PS5
$restParams = @{
Uri = $fullUrl
Method = 'Get'
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 30
}
# Add SkipCertificateCheck for PowerShell 7+
if ($PSVersionTable.PSVersion.Major -ge 7) {
$restParams.SkipCertificateCheck = $true
}
$response = Invoke-RestMethod @restParams
if ($response.success -and $response.data) {
Write-Log "API returned $($response.count) shopfloor PCs" -Level "SUCCESS"
return $response.data
} else {
Write-Log "No shopfloor PCs returned from API" -Level "WARNING"
return @()
}
} catch {
$errMsg = $_.Exception.Message
$innerMsg = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { "" }
# Check if it's a TLS/connection error - try WebClient as fallback
if ($errMsg -match "underlying connection|closed|SSL|TLS|secure channel" -or $innerMsg -match "underlying connection|closed|SSL|TLS|secure channel") {
Write-Log "TLS error detected, trying WebClient fallback..." -Level "WARNING"
try {
$webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("User-Agent", "PowerShell/ShopDB-Updater")
$jsonResponse = $webClient.DownloadString($fullUrl)
$response = $jsonResponse | ConvertFrom-Json
if ($response.success -and $response.data) {
Write-Log "API returned $($response.count) shopfloor PCs (via WebClient)" -Level "SUCCESS"
return $response.data
} else {
Write-Log "No shopfloor PCs returned from API" -Level "WARNING"
return @()
}
} catch {
Write-Log "WebClient fallback also failed: $($_.Exception.Message)" -Level "ERROR"
}
}
Write-Log "Failed to query API for shopfloor PCs: $errMsg" -Level "ERROR"
Write-Log " Exception Type: $($_.Exception.GetType().FullName)" -Level "ERROR"
if ($innerMsg) {
Write-Log " Inner Exception: $innerMsg" -Level "ERROR"
}
Write-Log " Tip: Try using HTTP instead of HTTPS, or check if the server certificate is valid" -Level "INFO"
return @()
}
}
function Get-HighUptimePCsFromApi {
<#
.SYNOPSIS
Queries ShopDB API to get PCs with uptime >= specified days.
#>
param(
[string]$ApiUrl,
[int]$MinUptimeDays
)
$fullUrl = "$ApiUrl`?action=getHighUptimePCs&minUptime=$MinUptimeDays"
Write-Log "Querying API for PCs with uptime >= $MinUptimeDays days: $fullUrl" -Level "INFO"
try {
$restParams = @{
Uri = $fullUrl
Method = 'Get'
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 30
}
if ($PSVersionTable.PSVersion.Major -ge 7) {
$restParams.SkipCertificateCheck = $true
}
$response = Invoke-RestMethod @restParams
if ($response.success -and $response.data) {
Write-Log "API returned $($response.count) PCs with uptime >= $MinUptimeDays days" -Level "SUCCESS"
return $response.data
} else {
Write-Log "No high-uptime PCs returned from API" -Level "WARNING"
return @()
}
} catch {
$errMsg = $_.Exception.Message
Write-Log "Failed to query API for high-uptime PCs: $errMsg" -Level "ERROR"
return @()
}
}
function Get-PCConnectionInfo {
<#
.SYNOPSIS
Builds FQDN and resolves IP for a PC hostname.
#>
param(
[Parameter(Mandatory)]
[string]$Hostname,
[Parameter()]
[string]$DnsSuffix = "logon.ds.ge.com",
[Parameter()]
[switch]$SkipDnsLookup
)
$result = @{
Hostname = $Hostname
FQDN = $null
IPAddress = $null
Reachable = $false
Error = $null
}
# Build FQDN - if hostname already contains dots, assume it's already an FQDN
if ($Hostname -like "*.*") {
$result.FQDN = $Hostname
} else {
$result.FQDN = "$Hostname.$DnsSuffix"
}
if (-not $SkipDnsLookup) {
try {
# Resolve DNS to get current IP
$dnsResult = Resolve-DnsName -Name $result.FQDN -Type A -ErrorAction Stop
$result.IPAddress = ($dnsResult | Where-Object { $_.Type -eq 'A' } | Select-Object -First 1).IPAddress
if ($result.IPAddress) {
# Quick connectivity test (WinRM port 5985)
$tcpTest = Test-NetConnection -ComputerName $result.FQDN -Port 5985 -WarningAction SilentlyContinue
$result.Reachable = $tcpTest.TcpTestSucceeded
}
} catch {
$result.Error = "DNS lookup failed: $($_.Exception.Message)"
}
} else {
# Skip DNS lookup, just use FQDN directly
$result.Reachable = $true # Assume reachable, let WinRM fail if not
}
return $result
}
function Test-WinRMConnectivity {
<#
.SYNOPSIS
Tests WinRM connectivity to a remote PC.
#>
param(
[Parameter(Mandatory)]
[string]$ComputerName,
[Parameter()]
[PSCredential]$Credential
)
$testParams = @{
ComputerName = $ComputerName
ErrorAction = 'Stop'
}
if ($Credential) {
$testParams.Credential = $Credential
}
try {
$result = Test-WSMan @testParams
return @{ Success = $true; Error = $null }
} catch {
return @{ Success = $false; Error = $_.Exception.Message }
}
}
function Add-TrustedHosts {
<#
.SYNOPSIS
Adds computers to WinRM TrustedHosts list.
#>
param(
[Parameter()]
[string[]]$ComputerNames,
[Parameter()]
[string]$DnsSuffix = "logon.ds.ge.com",
[Parameter()]
[switch]$TrustAllInDomain
)
# Check if running as admin
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Log "ERROR: Must run as Administrator to modify TrustedHosts" -Level "ERROR"
return $false
}
try {
# Get current trusted hosts
$currentHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction SilentlyContinue).Value
Write-Log "Current TrustedHosts: $(if ($currentHosts) { $currentHosts } else { '(empty)' })" -Level "INFO"
if ($TrustAllInDomain) {
# Trust all hosts in the domain (wildcard) AND the shopfloor IP subnet for IP fallback
$newValue = "*.$DnsSuffix,10.134.*"
Write-Log "Adding wildcard trust for: *.$DnsSuffix and 10.134.* (IP fallback)" -Level "INFO"
} else {
# Build list of FQDNs to add
$fqdnsToAdd = @()
foreach ($computer in $ComputerNames) {
if ($computer -like "*.*") {
$fqdnsToAdd += $computer
} else {
$fqdnsToAdd += "$computer.$DnsSuffix"
}
}
# Merge with existing
$existingList = if ($currentHosts) { $currentHosts -split ',' } else { @() }
$mergedList = ($existingList + $fqdnsToAdd) | Select-Object -Unique
$newValue = $mergedList -join ','
Write-Log "Adding to TrustedHosts: $($fqdnsToAdd -join ', ')" -Level "INFO"
}
# Set the new value
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newValue -Force
Write-Log "TrustedHosts updated successfully" -Level "SUCCESS"
# Show new value
$updatedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
Write-Log "New TrustedHosts: $updatedHosts" -Level "INFO"
return $true
} catch {
Write-Log "Failed to update TrustedHosts: $_" -Level "ERROR"
return $false
}
}
function Show-TrustedHostsHelp {
<#
.SYNOPSIS
Shows help for setting up TrustedHosts.
#>
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " WinRM TrustedHosts Setup Guide" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Since you're not on the same domain as the shopfloor PCs," -ForegroundColor White
Write-Host "you need to add them to your TrustedHosts list." -ForegroundColor White
Write-Host ""
Write-Host "OPTION 1: Trust all PCs in the domain + IP fallback (Recommended)" -ForegroundColor Yellow
Write-Host " Run as Administrator:" -ForegroundColor Gray
Write-Host ' Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com,10.134.*" -Force' -ForegroundColor Green
Write-Host ""
Write-Host "OPTION 2: Trust specific PCs" -ForegroundColor Yellow
Write-Host " Run as Administrator:" -ForegroundColor Gray
Write-Host ' Set-Item WSMan:\localhost\Client\TrustedHosts -Value "PC01.logon.ds.ge.com,PC02.logon.ds.ge.com" -Force' -ForegroundColor Green
Write-Host ""
Write-Host "OPTION 3: Use this script to set it up" -ForegroundColor Yellow
Write-Host " # Trust all in domain:" -ForegroundColor Gray
Write-Host " .\Update-ShopfloorPCs-Remote.ps1 -SetupTrustedHosts" -ForegroundColor Green
Write-Host ""
Write-Host " # Trust specific PCs:" -ForegroundColor Gray
Write-Host ' .\Update-ShopfloorPCs-Remote.ps1 -SetupTrustedHosts -ComputerName "PC01","PC02"' -ForegroundColor Green
Write-Host ""
Write-Host "VIEW CURRENT SETTINGS:" -ForegroundColor Yellow
Write-Host ' Get-Item WSMan:\localhost\Client\TrustedHosts' -ForegroundColor Green
Write-Host ""
Write-Host "CLEAR TRUSTEDHOSTS:" -ForegroundColor Yellow
Write-Host ' Set-Item WSMan:\localhost\Client\TrustedHosts -Value "" -Force' -ForegroundColor Green
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
}
function Get-RemotePCInfo {
<#
.SYNOPSIS
Script block to execute on remote PC to collect system information.
#>
# This scriptblock runs on the REMOTE computer
$scriptBlock = {
$result = @{
Success = $false
Hostname = $env:COMPUTERNAME
Error = $null
}
try {
# Get basic system info
$computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem
$bios = Get-CimInstance -ClassName Win32_BIOS
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$result.SerialNumber = $bios.SerialNumber
$result.ServiceTag = $bios.SerialNumber # Same as serial for Dell
$result.Manufacturer = $computerSystem.Manufacturer
$result.Model = $computerSystem.Model
$result.LoggedInUser = $computerSystem.UserName
$result.OSVersion = $os.Caption
$result.LastBootUpTime = if ($os.LastBootUpTime) { $os.LastBootUpTime.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
$result.TotalPhysicalMemory = [Math]::Round($computerSystem.TotalPhysicalMemory / 1GB, 2)
$result.DomainRole = $computerSystem.DomainRole
$result.CurrentTimeZone = (Get-TimeZone).Id
# Get network interfaces
$networkAdapters = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration |
Where-Object { $_.IPEnabled -eq $true }
$networkInterfaces = @()
foreach ($adapter in $networkAdapters) {
if ($adapter.IPAddress) {
foreach ($i in 0..($adapter.IPAddress.Count - 1)) {
$ip = $adapter.IPAddress[$i]
# Only include IPv4 addresses
if ($ip -match '^\d+\.\d+\.\d+\.\d+$') {
# 10.134.*.* is always primary for shopfloor PCs
$isPrimary = ($ip -like "10.134.*")
# Secondary/equipment IPs: 192.168.*, 10.0.*, 100.* or any non-10.134 private IP
$isMachineNetwork = (
($ip -like "192.168.*") -or
($ip -like "10.0.*") -or
($ip -like "100.*") -or
(($ip -like "10.*") -and ($ip -notlike "10.134.*"))
)
$networkInterfaces += @{
IPAddress = $ip
MACAddress = $adapter.MACAddress
SubnetMask = if ($adapter.IPSubnet) { $adapter.IPSubnet[$i] } else { "" }
DefaultGateway = if ($adapter.DefaultIPGateway) { $adapter.DefaultIPGateway[0] } else { "" }
InterfaceName = $adapter.Description
IsPrimary = $isPrimary
IsMachineNetwork = $isMachineNetwork
}
}
}
}
}
# Sort so 10.134.*.* (primary) comes first
$result.NetworkInterfaces = $networkInterfaces | Sort-Object -Property @{Expression={$_.IsPrimary}; Descending=$true}
# Get DNC configuration if it exists
$dncConfig = @{}
$dncIniPath = "C:\DNC\DNC.ini"
if (Test-Path $dncIniPath) {
$iniContent = Get-Content $dncIniPath -Raw
# Parse INI file for common DNC settings
if ($iniContent -match 'Site\s*=\s*(.+)') { $dncConfig.Site = $matches[1].Trim() }
if ($iniContent -match 'Cnc\s*=\s*(.+)') { $dncConfig.CNC = $matches[1].Trim() }
if ($iniContent -match 'NcIF\s*=\s*(.+)') { $dncConfig.NCIF = $matches[1].Trim() }
if ($iniContent -match 'MachineNo\s*=\s*(.+)') { $dncConfig.MachineNo = $matches[1].Trim() }
if ($iniContent -match 'FtpHostPrimary\s*=\s*(.+)') { $dncConfig.FTPHostPrimary = $matches[1].Trim() }
}
$result.DNCConfig = $dncConfig
# Get machine number from registry or DNC config
$machineNo = $null
# Try registry first - GE Aircraft Engines DNC location
$regPaths = @(
"HKLM:\SOFTWARE\GE Aircraft Engines\DNC\General",
"HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General"
)
foreach ($regPath in $regPaths) {
if (Test-Path $regPath) {
$machineNo = (Get-ItemProperty -Path $regPath -Name "MachineNo" -ErrorAction SilentlyContinue).MachineNo
if ($machineNo) { break }
}
}
# Fall back to DNC config
if (-not $machineNo -and $dncConfig.MachineNo) {
$machineNo = $dncConfig.MachineNo
}
# Check if machine number is generic (don't send to API)
$genericTypeHint = $null
if ($machineNo) {
$genericMachineTypes = @{
"^WJPRT" = "Measuring" # Generic printer/measuring tool
"^WJCMM" = "CMM" # Generic CMM
"^WJMEAS" = "Measuring" # Generic measuring
"^0600$" = "Wax Trace" # Wax trace machines
"^0612$" = "Inspection" # Part markers
"^0613$" = "Inspection" # Part markers
"^0615" = "Inspection" # Part markers
"^8003$" = "Inspection" # Part markers
"^TEST" = $null # Test machines
"^TEMP" = $null # Temporary
"^DEFAULT"= $null # Default value
"^0+$" = $null # All zeros
}
foreach ($pattern in $genericMachineTypes.Keys) {
if ($machineNo -match $pattern) {
$genericTypeHint = $genericMachineTypes[$pattern]
$result.IsGenericMachineNo = $true
$result.GenericTypeHint = $genericTypeHint
$machineNo = $null # Don't send generic machine numbers
break
}
}
}
$result.MachineNo = $machineNo
# Get GE Aircraft Engines registry info (DualPath configuration)
$geInfo = @{
Registry32Bit = $false
Registry64Bit = $false
DualPathEnabled = $null
Path1Name = $null
Path2Name = $null
}
$gePaths = @{
'32bit' = 'HKLM:\SOFTWARE\GE Aircraft Engines'
'64bit' = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines'
}
foreach ($pathType in $gePaths.Keys) {
$basePath = $gePaths[$pathType]
if (Test-Path $basePath) {
if ($pathType -eq '32bit') { $geInfo.Registry32Bit = $true }
else { $geInfo.Registry64Bit = $true }
$efocasPath = "$basePath\DNC\eFocas"
if (Test-Path $efocasPath) {
$efocasValues = Get-ItemProperty -Path $efocasPath -ErrorAction SilentlyContinue
if ($efocasValues.DualPath -and $geInfo.DualPathEnabled -eq $null) {
$geInfo.DualPathEnabled = ($efocasValues.DualPath -eq 'YES')
}
if (-not $geInfo.Path1Name -and $efocasValues.Path1Name) {
$geInfo.Path1Name = $efocasValues.Path1Name
}
if (-not $geInfo.Path2Name -and $efocasValues.Path2Name) {
$geInfo.Path2Name = $efocasValues.Path2Name
}
}
}
}
$result.GERegistryInfo = $geInfo
# Get serial port configuration
$comPorts = @()
$serialPorts = Get-CimInstance -ClassName Win32_SerialPort -ErrorAction SilentlyContinue
foreach ($port in $serialPorts) {
$comPorts += @{
PortName = $port.DeviceID
Description = $port.Description
}
}
$result.SerialPorts = $comPorts
# Check for VNC installation
$hasVnc = $false
$regPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
foreach ($path in $regPaths) {
if (Test-Path $path) {
$vncApps = Get-ItemProperty $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -like "*VNC Server*" -or $_.DisplayName -like "*VNC Connect*" -or $_.DisplayName -like "*RealVNC*" }
if ($vncApps) {
$hasVnc = $true
break
}
}
}
if (-not $hasVnc) {
$vncService = Get-Service -Name "vncserver*" -ErrorAction SilentlyContinue
if ($vncService) { $hasVnc = $true }
}
$result.HasVnc = $hasVnc
# Get default printer FQDN (network printers only)
$defaultPrinterFQDN = $null
try {
$defaultPrinter = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Default=$true" -ErrorAction SilentlyContinue
if ($defaultPrinter -and $defaultPrinter.PortName) {
$portName = $defaultPrinter.PortName
# Skip local/virtual printers
$localPorts = @('USB', 'LPT', 'COM', 'PORTPROMPT:', 'FILE:', 'NUL:', 'XPS', 'PDF', 'FOXIT', 'Microsoft')
$isLocalPrinter = $false
foreach ($localPort in $localPorts) {
if ($portName -like "$localPort*") { $isLocalPrinter = $true; break }
}
if (-not $isLocalPrinter) {
# Strip anything after underscore (e.g., 10.80.92.53_2 -> 10.80.92.53)
$defaultPrinterFQDN = $portName -replace '_.*$', ''
}
}
} catch { }
$result.DefaultPrinterFQDN = $defaultPrinterFQDN
# ================================================================
# Detect installed applications for PC type classification
# ================================================================
# Get all installed apps once for efficiency (with version info)
$installedApps = @()
$installedAppsWithVersion = @()
# Check machine-wide registry paths
foreach ($path in $regPaths) {
if (Test-Path $path) {
$apps = Get-ItemProperty $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and $_.DisplayName.Trim() -ne "" }
foreach ($app in $apps) {
$installedApps += $app.DisplayName
$installedAppsWithVersion += @{
DisplayName = $app.DisplayName.Trim()
Version = if ($app.DisplayVersion) { $app.DisplayVersion.Trim() } else { "" }
}
}
}
}
# Check per-user installed apps by enumerating loaded hives in HKU
$hkuKeys = Get-ChildItem "Registry::HKU" -ErrorAction SilentlyContinue
foreach ($key in $hkuKeys) {
$sid = $key.PSChildName
# Only check real user SIDs (S-1-5-21-*), skip _Classes entries
if ($sid -match "^S-1-5-21-" -and $sid -notmatch "_Classes$") {
$userUninstallPath = "Registry::HKU\$sid\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
if (Test-Path $userUninstallPath -ErrorAction SilentlyContinue) {
$apps = Get-ItemProperty $userUninstallPath -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and $_.DisplayName.Trim() -ne "" }
foreach ($app in $apps) {
if ($app.DisplayName -notin $installedApps) {
$installedApps += $app.DisplayName
$installedAppsWithVersion += @{
DisplayName = $app.DisplayName.Trim()
Version = if ($app.DisplayVersion) { $app.DisplayVersion.Trim() } else { "" }
}
}
}
}
}
}
# ================================================================
# Match against tracked applications (embedded from applications.csv)
# ================================================================
$trackedAppPatterns = @(
@{ app_id = 2; app_name = "UDC"; patterns = @("Universal Data Collection") }
@{ app_id = 4; app_name = "CLM"; patterns = @("PPDCS", "CLM") }
@{ app_id = 6; app_name = "PC-DMIS"; patterns = @("PC-DMIS", "PCDMIS") }
@{ app_id = 7; app_name = "Oracle"; patterns = @("OracleDatabase", "Oracle Database", "Oracle.*Database") }
@{ app_id = 8; app_name = "eDNC"; patterns = @("eDNC") }
@{ app_id = 22; app_name = "OpenText"; patterns = @("OpenText", "CSF") }
@{ app_id = 30; app_name = "Tanium"; patterns = @("^Tanium Client") }
@{ app_id = 76; app_name = "FormTracePak"; patterns = @("FormTracePak", "Formtracepak", "Form Trace", "FormTrace") }
@{ app_id = 69; app_name = "Keyence VR Series"; patterns = @("VR-3000", "VR-5000", "VR-6000", "KEYENCE VR") }
@{ app_id = 70; app_name = "Genspect"; patterns = @("Genspect") }
@{ app_id = 71; app_name = "GageCal"; patterns = @("GageCal") }
@{ app_id = 72; app_name = "NI Software"; patterns = @("^NI-", "National Instruments", "NI System", "NI Measurement", "NI LabVIEW") }
@{ app_id = 73; app_name = "goCMM"; patterns = @("goCMM") }
@{ app_id = 74; app_name = "DODA"; patterns = @("Dovetail Digital Analysis", "DODA") }
@{ app_id = 75; app_name = "FormStatusMonitor"; patterns = @("FormStatusMonitor") }
@{ app_id = 77; app_name = "HeatTreat"; patterns = @("HeatTreat") }
@{ app_id = 82; app_name = "GE Aerospace Dashboard"; patterns = @("^GE Aerospace Dashboard") }
@{ app_id = 83; app_name = "GE Aerospace Lobby Display"; patterns = @("^GE Aerospace Lobby Display") }
)
$matchedApps = @()
foreach ($tracked in $trackedAppPatterns) {
foreach ($installedApp in $installedAppsWithVersion) {
$matched = $false
foreach ($pattern in $tracked.patterns) {
if ($installedApp.DisplayName -match $pattern) {
$matched = $true
break
}
}
if ($matched) {
# Avoid duplicates
if (-not ($matchedApps | Where-Object { $_.appid -eq $tracked.app_id })) {
$matchedApps += @{
appid = $tracked.app_id
appname = $tracked.app_name
version = $installedApp.Version
displayname = $installedApp.DisplayName
}
}
break
}
}
}
# ================================================================
# Detect running processes for UDC and CLM to set isactive
# ================================================================
$udcRunning = $false
$clmRunning = $false
# Check for UDC process
$udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue
if ($udcProcess) { $udcRunning = $true }
# Check for CLM process (PPMon.exe)
$clmProcess = Get-Process -Name "PPMon" -ErrorAction SilentlyContinue
if ($clmProcess) { $clmRunning = $true }
# Update matched apps with isactive status
foreach ($app in $matchedApps) {
if ($app.appid -eq 2) {
# UDC
$app.isactive = if ($udcRunning) { 1 } else { 0 }
} elseif ($app.appid -eq 4) {
# CLM
$app.isactive = if ($clmRunning) { 1 } else { 0 }
} else {
# Other apps - default to active if installed
$app.isactive = 1
}
}
$result.UDCRunning = $udcRunning
$result.CLMRunning = $clmRunning
# Store matched apps as JSON string to survive WinRM serialization
$result.MatchedAppsCount = $matchedApps.Count
$result.MatchedAppNames = ($matchedApps | ForEach-Object { $_.appname }) -join ", "
$result.AllInstalledApps = ($installedApps | Sort-Object) -join "|"
$result.AllInstalledAppsCount = $installedApps.Count
if ($matchedApps.Count -gt 0) {
$result.MatchedAppsJson = ($matchedApps | ConvertTo-Json -Compress)
} else {
$result.MatchedAppsJson = ""
}
# CMM Detection: PC-DMIS, goCMM, DODA
$hasPcDmis = $false
$hasGoCMM = $false
$hasDODA = $false
foreach ($app in $installedApps) {
if ($app -match "PC-DMIS|PCDMIS") { $hasPcDmis = $true }
if ($app -match "^goCMM") { $hasGoCMM = $true }
if ($app -match "Dovetail Digital Analysis|DODA") { $hasDODA = $true }
}
# Also check common PC-DMIS installation paths
if (-not $hasPcDmis) {
$pcDmisPaths = @(
"C:\Program Files\Hexagon\PC-DMIS*",
"C:\Program Files (x86)\Hexagon\PC-DMIS*",
"C:\Program Files\WAI\PC-DMIS*",
"C:\Program Files (x86)\WAI\PC-DMIS*",
"C:\ProgramData\Hexagon\PC-DMIS*"
)
foreach ($dmisPath in $pcDmisPaths) {
if (Test-Path $dmisPath) {
$hasPcDmis = $true
break
}
}
}
$result.HasPcDmis = $hasPcDmis
$result.HasGoCMM = $hasGoCMM
$result.HasDODA = $hasDODA
$result.IsCMM = ($hasPcDmis -or $hasGoCMM -or $hasDODA)
# Wax Trace Detection: FormTracePak, FormStatusMonitor
$hasFormTracePak = $false
$hasFormStatusMonitor = $false
foreach ($app in $installedApps) {
if ($app -match "FormTracePak|Formtracepak|Form Trace|FormTrace") { $hasFormTracePak = $true }
if ($app -match "FormStatusMonitor") { $hasFormStatusMonitor = $true }
}
# Check file path fallback
if (-not $hasFormTracePak) {
$ftPaths = @("C:\Program Files\MitutoyoApp*", "C:\Program Files (x86)\MitutoyoApp*")
foreach ($ftPath in $ftPaths) {
if (Test-Path $ftPath) { $hasFormTracePak = $true; break }
}
}
$result.HasFormTracePak = $hasFormTracePak
$result.HasFormStatusMonitor = $hasFormStatusMonitor
$result.IsWaxTrace = ($hasFormTracePak -or $hasFormStatusMonitor)
# Keyence Detection: VR Series, Keyence VR USB Driver
$hasKeyence = $false
foreach ($app in $installedApps) {
if ($app -match "VR-3000|VR-5000|VR-6000|KEYENCE VR") {
$hasKeyence = $true
break
}
}
$result.HasKeyence = $hasKeyence
$result.IsKeyence = $hasKeyence
# EAS1000 Detection: GageCal, NI Software (National Instruments)
$hasGageCal = $false
$hasNISoftware = $false
foreach ($app in $installedApps) {
if ($app -match "^GageCal") { $hasGageCal = $true }
if ($app -match "^NI-|National Instruments|NI System|NI Measurement|NI LabVIEW") { $hasNISoftware = $true }
}
$result.HasGageCal = $hasGageCal
$result.HasNISoftware = $hasNISoftware
$result.IsEAS1000 = ($hasGageCal -or $hasNISoftware)
# Genspect Detection
$hasGenspect = $false
foreach ($app in $installedApps) {
if ($app -match "^Genspect") {
$hasGenspect = $true
break
}
}
$result.HasGenspect = $hasGenspect
# Heat Treat Detection
$hasHeatTreat = $false
foreach ($app in $installedApps) {
if ($app -match "^HeatTreat") {
$hasHeatTreat = $true
break
}
}
$result.HasHeatTreat = $hasHeatTreat
# Dashboard Detection (GE Aerospace Dashboard kiosk installer)
$hasDashboard = $false
foreach ($app in $installedApps) {
if ($app -match "^GE Aerospace Dashboard") {
$hasDashboard = $true
break
}
}
$result.HasDashboard = $hasDashboard
$result.IsDashboard = $hasDashboard
# Lobby Display Detection (GE Aerospace Lobby Display kiosk installer)
$hasLobbyDisplay = $false
foreach ($app in $installedApps) {
if ($app -match "^GE Aerospace Lobby Display") {
$hasLobbyDisplay = $true
break
}
}
$result.HasLobbyDisplay = $hasLobbyDisplay
$result.IsLobbyDisplay = $hasLobbyDisplay
# Determine PC Type based on detected software
# Priority: Dashboard > Lobby Display > CMM > Wax Trace > Keyence > EAS1000 > Genspect > Heat Treat > Generic hint > Shopfloor (default)
$detectedPcType = "Shopfloor" # Default for shopfloor PCs
if ($hasDashboard) {
$detectedPcType = "Dashboard"
} elseif ($hasLobbyDisplay) {
$detectedPcType = "Lobby Display"
} elseif ($result.IsCMM) {
$detectedPcType = "CMM"
} elseif ($result.IsWaxTrace) {
$detectedPcType = "Wax Trace"
} elseif ($result.IsKeyence) {
$detectedPcType = "Keyence"
} elseif ($result.IsEAS1000) {
$detectedPcType = "EAS1000"
} elseif ($hasGenspect) {
$detectedPcType = "Genspect"
} elseif ($hasHeatTreat) {
$detectedPcType = "Heat Treat"
} elseif ($result.GenericTypeHint) {
# Use generic machine number hint when no software detected
$detectedPcType = $result.GenericTypeHint
}
$result.DetectedPcType = $detectedPcType
$result.Success = $true
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
return $scriptBlock
}
function Send-PCDataToApi {
<#
.SYNOPSIS
Sends collected PC data to the ShopDB API.
#>
param(
[Parameter(Mandatory)]
[hashtable]$PCData,
[Parameter(Mandatory)]
[string]$ApiUrl
)
try {
# Determine PC type - use detected type from software analysis, fallback to machine number
$pcType = "Measuring" # Default
if ($PCData.DetectedPcType) {
$pcType = $PCData.DetectedPcType
} elseif ($PCData.MachineNo) {
$pcType = "Shopfloor"
}
# Build the POST body
$postData = @{
action = 'updateCompleteAsset'
hostname = $PCData.Hostname
serialNumber = $PCData.SerialNumber
manufacturer = $PCData.Manufacturer
model = $PCData.Model
pcType = $pcType
loggedInUser = $PCData.LoggedInUser
osVersion = $PCData.OSVersion
}
# Add last boot time if available
if ($PCData.LastBootUpTime) {
$postData.lastBootUpTime = $PCData.LastBootUpTime
}
# Add machine number if available
if ($PCData.MachineNo) {
$postData.machineNo = $PCData.MachineNo
}
# Add VNC status
if ($PCData.HasVnc) {
$postData.hasVnc = "1"
} else {
$postData.hasVnc = "0"
}
# Add new parity fields
if ($PCData.ServiceTag) {
$postData.serviceTag = $PCData.ServiceTag
}
if ($PCData.TotalPhysicalMemory) {
$postData.totalPhysicalMemory = $PCData.TotalPhysicalMemory
}
if ($PCData.DomainRole -ne $null) {
$postData.domainRole = $PCData.DomainRole
}
if ($PCData.CurrentTimeZone) {
$postData.currentTimeZone = $PCData.CurrentTimeZone
}
# Add GE Registry info (DualPath configuration)
if ($PCData.GERegistryInfo) {
$postData.dncGeRegistry32Bit = if ($PCData.GERegistryInfo.Registry32Bit) { "1" } else { "0" }
$postData.dncGeRegistry64Bit = if ($PCData.GERegistryInfo.Registry64Bit) { "1" } else { "0" }
if ($PCData.GERegistryInfo.DualPathEnabled -ne $null) {
$postData.dncDualPathEnabled = if ($PCData.GERegistryInfo.DualPathEnabled) { "1" } else { "0" }
}
if ($PCData.GERegistryInfo.Path1Name) {
$postData.dncPath1Name = $PCData.GERegistryInfo.Path1Name
}
if ($PCData.GERegistryInfo.Path2Name) {
$postData.dncPath2Name = $PCData.GERegistryInfo.Path2Name
}
}
# Add default printer FQDN
if ($PCData.DefaultPrinterFQDN) {
$postData.defaultPrinterFQDN = $PCData.DefaultPrinterFQDN
}
# Add network interfaces as JSON
if ($PCData.NetworkInterfaces -and $PCData.NetworkInterfaces.Count -gt 0) {
$postData.networkInterfaces = ($PCData.NetworkInterfaces | ConvertTo-Json -Compress)
}
# Add DNC config if available
if ($PCData.DNCConfig -and $PCData.DNCConfig.Keys.Count -gt 0) {
$postData.dncConfig = ($PCData.DNCConfig | ConvertTo-Json -Compress)
}
# Add matched/tracked applications (already JSON from remote scriptblock)
if ($PCData.MatchedAppsJson -and $PCData.MatchedAppsJson -ne "") {
$postData.installedApps = $PCData.MatchedAppsJson
}
# Build params for API call
$restParams = @{
Uri = $ApiUrl
Method = 'Post'
Body = $postData
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 30
}
# Add SkipCertificateCheck for PowerShell 7+
if ($PSVersionTable.PSVersion.Major -ge 7) {
$restParams.SkipCertificateCheck = $true
}
# Send to API
$response = Invoke-RestMethod @restParams
# Debug: log full API response for relationship troubleshooting
Write-Log " API Response: $($response | ConvertTo-Json -Compress)" -Level "INFO"
return @{
Success = $response.success
Message = $response.message
MachineId = $response.machineid
RelationshipCreated = $response.relationshipCreated
}
} catch {
$errMsg = $_.Exception.Message
$innerMsg = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { "" }
# Try WebClient fallback for TLS errors
if ($errMsg -match "underlying connection|closed|SSL|TLS|secure channel" -or $innerMsg -match "underlying connection|closed|SSL|TLS|secure channel") {
try {
$webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("Content-Type", "application/x-www-form-urlencoded")
$webClient.Headers.Add("User-Agent", "PowerShell/ShopDB-Updater")
# Convert postData hashtable to form-urlencoded string
$formData = ($postData.GetEnumerator() | ForEach-Object {
"$([System.Web.HttpUtility]::UrlEncode($_.Key))=$([System.Web.HttpUtility]::UrlEncode($_.Value))"
}) -join "&"
$jsonResponse = $webClient.UploadString($ApiUrl, $formData)
$response = $jsonResponse | ConvertFrom-Json
return @{
Success = $response.success
Message = $response.message
MachineId = $response.machineid
RelationshipCreated = $response.relationshipCreated
}
} catch {
# WebClient also failed
}
}
return @{
Success = $false
Message = if ($innerMsg) { "$errMsg ($innerMsg)" } else { $errMsg }
MachineId = $null
}
}
}
#endregion Functions
#region Main
# Handle TrustedHosts setup mode
if ($SetupTrustedHosts) {
Write-Log "=== TrustedHosts Setup Mode ===" -Level "INFO"
if ($ComputerName -and $ComputerName.Count -gt 0) {
# Trust specific computers
$result = Add-TrustedHosts -ComputerNames $ComputerName -DnsSuffix $DnsSuffix
} else {
# Trust all in domain (wildcard)
$result = Add-TrustedHosts -DnsSuffix $DnsSuffix -TrustAllInDomain
}
if (-not $result) {
Show-TrustedHostsHelp
}
exit 0
}
# Handle Reboot mode
if ($Reboot) {
Write-Log "=== Reboot Mode ===" -Level "INFO"
Write-Log "Minimum Uptime: $MinUptimeDays days" -Level "INFO"
Write-Log "API URL: $ApiUrl" -Level "INFO"
# Query API for high-uptime PCs
$highUptimePCs = Get-HighUptimePCsFromApi -ApiUrl $ApiUrl -MinUptimeDays $MinUptimeDays
if ($highUptimePCs.Count -eq 0) {
Write-Log "No PCs found with uptime >= $MinUptimeDays days" -Level "WARNING"
exit 0
}
# Display PCs to be rebooted
Write-Host ""
Write-Host "=" * 80 -ForegroundColor Red
Write-Host " WARNING: The following $($highUptimePCs.Count) PCs will be REBOOTED" -ForegroundColor Red
Write-Host "=" * 80 -ForegroundColor Red
Write-Host ""
Write-Host (" {0,-25} {1,-15} {2,-12} {3,-20}" -f "Hostname", "Machine #", "Uptime Days", "Last Boot") -ForegroundColor Yellow
Write-Host " $("-" * 75)" -ForegroundColor Gray
foreach ($pc in $highUptimePCs) {
$machineNo = if ($pc.machinenumber) { $pc.machinenumber } else { "-" }
$uptimeDays = if ($pc.uptime_days) { $pc.uptime_days } else { "?" }
$lastBoot = if ($pc.lastboottime) { $pc.lastboottime } else { "Unknown" }
Write-Host (" {0,-25} {1,-15} {2,-12} {3,-20}" -f $pc.hostname, $machineNo, $uptimeDays, $lastBoot)
}
Write-Host " $("-" * 75)" -ForegroundColor Gray
Write-Host ""
if ($WhatIf) {
Write-Log "WhatIf mode - no reboots will be performed" -Level "WARNING"
Write-Host " Would reboot $($highUptimePCs.Count) PCs" -ForegroundColor Yellow
exit 0
}
# Confirmation
if (-not $Force) {
Write-Host " Are you sure you want to reboot these $($highUptimePCs.Count) PCs?" -ForegroundColor Red
Write-Host ""
$confirm = Read-Host " Type 'YES' to confirm"
if ($confirm -ne "YES") {
Write-Log "Reboot cancelled by user" -Level "WARNING"
exit 0
}
Write-Host ""
}
# Prompt for credentials if not provided
if (-not $Credential) {
Write-Log "No credentials provided. Prompting for credentials..." -Level "INFO"
$Credential = Get-Credential -Message "Enter admin credentials for remote PCs"
if (-not $Credential) {
Write-Log "Credentials required. Exiting." -Level "ERROR"
exit 1
}
}
Write-Log "Starting reboot process..." -Level "INFO"
# Build FQDN list
$targetPCs = @()
foreach ($pc in $highUptimePCs) {
$fqdn = if ($pc.hostname -like "*.*") { $pc.hostname } else { "$($pc.hostname).$DnsSuffix" }
$targetPCs += @{
Hostname = $pc.hostname
FQDN = $fqdn
UptimeDays = $pc.uptime_days
}
}
# Reboot each PC
$rebootSuccess = 0
$rebootFailed = 0
foreach ($target in $targetPCs) {
Write-Log "Rebooting $($target.Hostname) (uptime: $($target.UptimeDays) days)..." -Level "INFO"
try {
$rebootParams = @{
ComputerName = $target.FQDN
Force = $true
ErrorAction = 'Stop'
}
if ($Credential) {
$rebootParams.Credential = $Credential
}
Restart-Computer @rebootParams
Write-Log " -> Reboot command sent successfully" -Level "SUCCESS"
$rebootSuccess++
} catch {
Write-Log " -> FAILED: $($_.Exception.Message)" -Level "ERROR"
$rebootFailed++
}
}
# Summary
Write-Host ""
Write-Host "=" * 60 -ForegroundColor Cyan
Write-Host " REBOOT SUMMARY" -ForegroundColor Cyan
Write-Host "=" * 60 -ForegroundColor Cyan
Write-Host ""
Write-Host " Total PCs: $($targetPCs.Count)" -ForegroundColor White
Write-Host " Successful: $rebootSuccess" -ForegroundColor Green
Write-Host " Failed: $rebootFailed" -ForegroundColor $(if ($rebootFailed -gt 0) { "Red" } else { "White" })
Write-Host ""
Write-Host "=" * 60 -ForegroundColor Cyan
exit 0
}
Write-Log "=== ShopDB Remote PC Update Script ===" -Level "INFO"
Write-Log "API URL: $ApiUrl" -Level "INFO"
Write-Log "DNS Suffix: $DnsSuffix" -Level "INFO"
# Prompt for credentials if not provided
if (-not $Credential) {
Write-Log "No credentials provided. Prompting for credentials..." -Level "INFO"
$Credential = Get-Credential -Message "Enter credentials for remote PCs"
if (-not $Credential) {
Write-Log "Credentials required. Exiting." -Level "ERROR"
exit 1
}
}
# Show credential info (username only, not password)
Write-Log "Using credentials: $($Credential.UserName)" -Level "INFO"
# Determine list of computers to process
$computers = @()
if ($All) {
Write-Log "Querying ShopDB for all shopfloor PCs..." -Level "INFO"
$shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl
if ($shopfloorPCs.Count -eq 0) {
Write-Log "No shopfloor PCs found in ShopDB. Exiting." -Level "WARNING"
exit 0
}
# Display summary table
Write-Host ""
Write-Host " Shopfloor PCs from ShopDB:" -ForegroundColor Cyan
Write-Host " $("-" * 90)" -ForegroundColor Gray
Write-Host (" {0,-20} {1,-15} {2,-15} {3,-20} {4,-15}" -f "Hostname", "Machine #", "IP Address", "Last Updated", "Logged In") -ForegroundColor Cyan
Write-Host " $("-" * 90)" -ForegroundColor Gray
foreach ($pc in $shopfloorPCs) {
$lastUpdated = if ($pc.lastupdated) { $pc.lastupdated } else { "Never" }
$loggedIn = if ($pc.loggedinuser) { $pc.loggedinuser } else { "-" }
$machineNo = if ($pc.machinenumber) { $pc.machinenumber } else { "-" }
$ipAddr = if ($pc.ipaddress) { $pc.ipaddress } else { "-" }
Write-Host (" {0,-20} {1,-15} {2,-15} {3,-20} {4,-15}" -f $pc.hostname, $machineNo, $ipAddr, $lastUpdated, $loggedIn)
}
Write-Host " $("-" * 90)" -ForegroundColor Gray
Write-Host ""
# Extract hostnames for processing
$computers = $shopfloorPCs | ForEach-Object { $_.hostname } | Where-Object { $_ -ne "" -and $_ -ne $null }
Write-Log "Found $($computers.Count) shopfloor PCs with hostnames" -Level "INFO"
} elseif ($ComputerName) {
$computers = $ComputerName
} else {
Write-Log "No computers specified. Use -ComputerName or -All parameter." -Level "ERROR"
Write-Host ""
Write-Host "Usage Examples:" -ForegroundColor Yellow
Write-Host " # Update all shopfloor PCs from database:" -ForegroundColor Gray
Write-Host " .\Update-ShopfloorPCs-Remote.ps1 -All -Credential `$cred" -ForegroundColor Green
Write-Host ""
Write-Host " # Update specific PC:" -ForegroundColor Gray
Write-Host " .\Update-ShopfloorPCs-Remote.ps1 -ComputerName 'PC01' -Credential `$cred" -ForegroundColor Green
Write-Host ""
Write-Host " # Setup TrustedHosts first (run as admin):" -ForegroundColor Gray
Write-Host " .\Update-ShopfloorPCs-Remote.ps1 -SetupTrustedHosts" -ForegroundColor Green
Write-Host ""
exit 1
}
Write-Log "Processing $($computers.Count) computer(s)..." -Level "INFO"
# Build FQDNs directly - skip slow DNS/connectivity checks
# Let WinRM handle connection failures (much faster)
Write-Log "Building FQDN list..." -Level "INFO"
$targetComputers = @()
foreach ($computer in $computers) {
# Build FQDN - if hostname already contains dots, assume it's already an FQDN
$fqdn = if ($computer -like "*.*") { $computer } else { "$computer.$DnsSuffix" }
$targetComputers += @{
Hostname = $computer
FQDN = $fqdn
IPAddress = $null
}
}
if ($targetComputers.Count -eq 0) {
Write-Log "No computers to process. Exiting." -Level "ERROR"
exit 1
}
Write-Log "Will attempt connection to $($targetComputers.Count) PC(s)" -Level "INFO"
$skippedComputers = @()
if ($WhatIf) {
Write-Log "WhatIf mode - no changes will be made" -Level "WARNING"
Write-Log "Would process: $($targetComputers.FQDN -join ', ')" -Level "INFO"
exit 0
}
# Build session options - use FQDNs for WinRM connection
$fqdnList = $targetComputers | ForEach-Object { $_.FQDN }
# Create session options with short timeout (15 seconds per PC)
# Timeouts: 20s to connect, 120s for operations (app detection takes time)
$sessionOption = New-PSSessionOption -OpenTimeout 20000 -OperationTimeout 120000 -NoMachineProfile
$sessionParams = @{
ComputerName = $fqdnList
ScriptBlock = (Get-RemotePCInfo)
SessionOption = $sessionOption
Authentication = 'Negotiate'
ErrorAction = 'SilentlyContinue'
ErrorVariable = 'remoteErrors'
}
if ($Credential) {
$sessionParams.Credential = $Credential
Write-Log "Credential added to session params: $($Credential.UserName)" -Level "INFO"
} else {
Write-Log "WARNING: No credential in session params!" -Level "WARNING"
}
if ($ThrottleLimit -and $PSVersionTable.PSVersion.Major -ge 7) {
$sessionParams.ThrottleLimit = $ThrottleLimit
}
# Execute remote commands (runs in parallel by default)
Write-Log "Connecting to remote PCs via WinRM (timeout: 15s per PC)..." -Level "INFO"
$results = Invoke-Command @sessionParams
# Process results
$successCount = 0
$failCount = 0
$successPCs = @()
$failedPCs = @()
Write-Host ""
Write-Log "Processing WinRM results..." -Level "INFO"
foreach ($result in $results) {
if ($result.Success) {
Write-Log "[OK] $($result.Hostname)" -Level "SUCCESS"
Write-Log " Serial: $($result.SerialNumber) | Model: $($result.Model) | OS: $($result.OSVersion)" -Level "INFO"
if ($result.NetworkInterfaces -and $result.NetworkInterfaces.Count -gt 0) {
$ips = ($result.NetworkInterfaces | ForEach-Object { $_.IPAddress }) -join ", "
Write-Log " IPs: $ips" -Level "INFO"
}
if ($result.LoggedInUser) {
Write-Log " Logged In: $($result.LoggedInUser)" -Level "INFO"
} else {
Write-Log " Logged In: (none)" -Level "INFO"
}
if ($result.MachineNo) {
Write-Log " Machine #: $($result.MachineNo)" -Level "INFO"
} elseif ($result.IsGenericMachineNo) {
Write-Log " Machine #: (generic - requires manual assignment)" -Level "WARNING"
}
# Show detected PC type and software
if ($result.DetectedPcType) {
$detectedSoftware = @()
if ($result.HasDashboard) { $detectedSoftware += "GE Aerospace Dashboard" }
if ($result.HasLobbyDisplay) { $detectedSoftware += "GE Aerospace Lobby Display" }
if ($result.HasPcDmis) { $detectedSoftware += "PC-DMIS" }
if ($result.HasGoCMM) { $detectedSoftware += "goCMM" }
if ($result.HasDODA) { $detectedSoftware += "DODA" }
if ($result.HasFormTracePak) { $detectedSoftware += "FormTracePak" }
if ($result.HasFormStatusMonitor) { $detectedSoftware += "FormStatusMonitor" }
if ($result.HasKeyence) { $detectedSoftware += "Keyence VR" }
if ($result.HasGageCal) { $detectedSoftware += "GageCal" }
if ($result.HasNISoftware) { $detectedSoftware += "NI Software" }
if ($result.HasGenspect) { $detectedSoftware += "Genspect" }
if ($result.HasHeatTreat) { $detectedSoftware += "HeatTreat" }
$softwareStr = if ($detectedSoftware.Count -gt 0) { " (" + ($detectedSoftware -join ", ") + ")" } else { "" }
Write-Log " PC Type: $($result.DetectedPcType)$softwareStr" -Level "INFO"
}
# Log tracked apps count
if ($result.MatchedAppsCount -and $result.MatchedAppsCount -gt 0) {
Write-Log " Tracked Apps: $($result.MatchedAppsCount) matched ($($result.MatchedAppNames))" -Level "INFO"
}
# Pass the result hashtable directly to the API function
# Note: $result from Invoke-Command is already a hashtable,
# PSObject.Properties gives metadata (Keys, Values, Count) not the actual data
# Send to API
Write-Log " Sending to API..." -Level "INFO"
$apiResult = Send-PCDataToApi -PCData $result -ApiUrl $ApiUrl
if ($apiResult.Success) {
Write-Log " -> Updated in ShopDB (MachineID: $($apiResult.MachineId))" -Level "SUCCESS"
if ($apiResult.RelationshipCreated) {
Write-Log " -> Machine Relationship: Created" -Level "SUCCESS"
} elseif ($result.MachineNo) {
Write-Log " -> Machine Relationship: FAILED (machineNo=$($result.MachineNo))" -Level "ERROR"
}
# Update WinRM status - since we successfully connected via WinRM, mark it as enabled
try {
$winrmBody = @{
action = "updateWinRMStatus"
hostname = $result.Hostname
hasWinRM = "1"
}
$winrmParams = @{
Uri = $ApiUrl
Method = 'Post'
Body = $winrmBody
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 15
}
if ($PSVersionTable.PSVersion.Major -ge 7) {
$winrmParams.SkipCertificateCheck = $true
}
$winrmResponse = Invoke-RestMethod @winrmParams
if ($winrmResponse.success) {
Write-Log " -> WinRM status updated" -Level "SUCCESS"
}
} catch {
Write-Log " -> WinRM status update failed (non-critical): $_" -Level "WARNING"
}
$successCount++
$successPCs += @{
Hostname = $result.Hostname
MachineId = $apiResult.MachineId
Serial = $result.SerialNumber
}
} else {
Write-Log " -> API Error: $($apiResult.Message)" -Level "ERROR"
$failCount++
$failedPCs += @{
Hostname = $result.Hostname
Error = "API: $($apiResult.Message)"
}
}
} else {
Write-Log "[FAIL] $($result.Hostname): $($result.Error)" -Level "ERROR"
$failCount++
$failedPCs += @{
Hostname = $result.Hostname
Error = $result.Error
}
}
Write-Host ""
}
# Collect connection errors for IP fallback retry
$connectionFailures = @()
foreach ($err in $remoteErrors) {
$targetPC = if ($err.TargetObject) { $err.TargetObject } else { "Unknown" }
Write-Log "[FAIL] ${targetPC}: $($err.Exception.Message)" -Level "ERROR"
$connectionFailures += @{
Hostname = $targetPC
FQDN = $targetPC
Error = $err.Exception.Message
}
}
# IP Fallback: Retry failed connections using recorded IP addresses (10.134.*.*)
if ($connectionFailures.Count -gt 0) {
Write-Host ""
Write-Log "Attempting IP fallback for $($connectionFailures.Count) failed connection(s)..." -Level "INFO"
foreach ($failure in $connectionFailures) {
# Extract hostname from FQDN
$hostname = $failure.FQDN -replace "\..*$", ""
# Query API for recorded IP address
try {
$ipLookupBody = @{
action = "getRecordedIP"
hostname = $hostname
}
$ipLookupParams = @{
Uri = $ApiUrl
Method = 'Post'
Body = $ipLookupBody
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 15
}
if ($PSVersionTable.PSVersion.Major -ge 7) {
$ipLookupParams.SkipCertificateCheck = $true
}
$ipResponse = Invoke-RestMethod @ipLookupParams
if ($ipResponse.success -and $ipResponse.ipaddress -and $ipResponse.ipaddress -match "^10\.134\.") {
$fallbackIP = $ipResponse.ipaddress
Write-Log " Found recorded IP for ${hostname}: $fallbackIP - retrying..." -Level "INFO"
# Retry connection using IP address
$ipSessionParams = @{
ComputerName = $fallbackIP
ScriptBlock = (Get-RemotePCInfo)
SessionOption = $sessionOption
Authentication = 'Negotiate'
ErrorAction = 'Stop'
}
if ($Credential) {
$ipSessionParams.Credential = $Credential
}
try {
$ipResult = Invoke-Command @ipSessionParams
if ($ipResult.Success) {
Write-Log "[OK] $($ipResult.Hostname) (via IP: $fallbackIP)" -Level "SUCCESS"
Write-Log " Serial: $($ipResult.SerialNumber) | Model: $($ipResult.Model) | OS: $($ipResult.OSVersion)" -Level "INFO"
if ($ipResult.NetworkInterfaces -and $ipResult.NetworkInterfaces.Count -gt 0) {
$ips = ($ipResult.NetworkInterfaces | ForEach-Object { $_.IPAddress }) -join ", "
Write-Log " IPs: $ips" -Level "INFO"
}
if ($ipResult.DetectedPcType) {
Write-Log " PC Type: $($ipResult.DetectedPcType)" -Level "INFO"
}
if ($ipResult.MatchedAppsCount -and $ipResult.MatchedAppsCount -gt 0) {
Write-Log " Tracked Apps: $($ipResult.MatchedAppsCount) matched ($($ipResult.MatchedAppNames))" -Level "INFO"
}
# Send to API
Write-Log " Sending to API..." -Level "INFO"
$apiResult = Send-PCDataToApi -PCData $ipResult -ApiUrl $ApiUrl
if ($apiResult.Success) {
Write-Log " -> Updated in ShopDB (MachineID: $($apiResult.MachineId))" -Level "SUCCESS"
if ($apiResult.RelationshipCreated) {
Write-Log " -> Machine Relationship: Created" -Level "SUCCESS"
} elseif ($ipResult.MachineNo) {
Write-Log " -> Machine Relationship: FAILED (machineNo=$($ipResult.MachineNo))" -Level "ERROR"
}
$successCount++
$successPCs += @{
Hostname = $ipResult.Hostname
MachineId = $apiResult.MachineId
Serial = $ipResult.SerialNumber
ViaIP = $fallbackIP
}
} else {
Write-Log " -> API Error: $($apiResult.Message)" -Level "ERROR"
$failCount++
$failedPCs += @{ Hostname = $hostname; Error = "API: $($apiResult.Message)" }
}
} else {
Write-Log "[FAIL] $hostname (via IP: $fallbackIP): $($ipResult.Error)" -Level "ERROR"
$failCount++
$failedPCs += @{ Hostname = $hostname; Error = $ipResult.Error }
}
} catch {
Write-Log "[FAIL] $hostname (via IP: $fallbackIP): $($_.Exception.Message)" -Level "ERROR"
$failCount++
$failedPCs += @{ Hostname = $hostname; Error = $_.Exception.Message }
}
} else {
# No valid IP found, add to failed list
Write-Log " No 10.134.*.* IP recorded for $hostname - skipping fallback" -Level "WARNING"
$failCount++
$failedPCs += @{ Hostname = $hostname; Error = $failure.Error }
}
} catch {
Write-Log " Failed to lookup IP for ${hostname}: $($_.Exception.Message)" -Level "WARNING"
$failCount++
$failedPCs += @{ Hostname = $hostname; Error = $failure.Error }
}
Write-Host ""
}
}
# Final Summary
Write-Host ""
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Host " SUMMARY" -ForegroundColor Cyan
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Host ""
Write-Host " Total Processed: $($successCount + $failCount)" -ForegroundColor White
Write-Host " Successful: $successCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
Write-Host " Skipped (DNS): $($skippedComputers.Count)" -ForegroundColor $(if ($skippedComputers.Count -gt 0) { "Yellow" } else { "White" })
Write-Host ""
if ($successPCs.Count -gt 0) {
Write-Host " Successfully Updated:" -ForegroundColor Green
foreach ($pc in $successPCs) {
Write-Host " - $($pc.Hostname) (ID: $($pc.MachineId))" -ForegroundColor Gray
}
Write-Host ""
}
if ($failedPCs.Count -gt 0) {
Write-Host " Failed:" -ForegroundColor Red
foreach ($pc in $failedPCs) {
Write-Host " - $($pc.Hostname): $($pc.Error)" -ForegroundColor Gray
}
Write-Host ""
}
if ($skippedComputers.Count -gt 0) {
Write-Host " Skipped (DNS/Connectivity):" -ForegroundColor Yellow
foreach ($pc in $skippedComputers) {
Write-Host " - $pc" -ForegroundColor Gray
}
Write-Host ""
}
Write-Host "=" * 70 -ForegroundColor Cyan
#endregion Main