Enhance WinRM remote script with app tracking and IP fallback

- Fix FormTracePak app_id from 68 to 76 (68 is Media Creator Full Edition)
- Add installed app tracking to remote WinRM script with embedded patterns
- Add IP fallback for failed hostname connections (uses recorded 10.134.* IPs)
- Add getRecordedIP API endpoint to lookup primary IP by hostname
- Mark 10.134.*.* as primary IPs, other ranges as secondary/equipment IPs
- Fix WinRM serialization issue by converting matched apps to JSON before return

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
cproudlock
2025-12-05 16:36:44 -05:00
parent b0c60ebbd5
commit c7834d4b99
4 changed files with 1657 additions and 158 deletions

View File

@@ -339,19 +339,30 @@ function Get-RemotePCInfo {
$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
IsMachineNetwork = ($ip -like "192.168.*" -or $ip -like "10.0.*")
IsPrimary = $isPrimary
IsMachineNetwork = $isMachineNetwork
}
}
}
}
}
$result.NetworkInterfaces = $networkInterfaces
# Sort so 10.134.*.* (primary) comes first
$result.NetworkInterfaces = $networkInterfaces | Sort-Object -Property @{Expression={$_.IsPrimary}; Descending=$true}
# Get DNC configuration if it exists
$dncConfig = @{}
@@ -360,23 +371,57 @@ function Get-RemotePCInfo {
$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 'MachineNumber\s*=\s*(.+)') { $dncConfig.MachineNumber = $matches[1].Trim() }
if ($iniContent -match 'FTPHost\s*=\s*(.+)') { $dncConfig.FTPHostPrimary = $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
$regPath = "HKLM:\SOFTWARE\GE\ShopFloor"
if (Test-Path $regPath) {
$machineNo = (Get-ItemProperty -Path $regPath -Name "MachineNumber" -ErrorAction SilentlyContinue).MachineNumber
# 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.MachineNumber) {
$machineNo = $dncConfig.MachineNumber
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$" = "Part Marker" # Part markers
"^0613$" = "Part Marker" # Part markers
"^0615" = "Part Marker" # 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
@@ -413,25 +458,100 @@ function Get-RemotePCInfo {
}
$result.HasVnc = $hasVnc
# Check for PC-DMIS installation (CMM software)
$hasPcDmis = $false
# ================================================================
# Detect installed applications for PC type classification
# ================================================================
# Get all installed apps once for efficiency (with version info)
$installedApps = @()
$installedAppsWithVersion = @()
foreach ($path in $regPaths) {
if (Test-Path $path) {
$pcDmisApps = Get-ItemProperty $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -like "*PC-DMIS*" -or $_.DisplayName -like "*PCDMIS*" }
if ($pcDmisApps) {
$hasPcDmis = $true
$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 { "" }
}
}
}
}
# ================================================================
# 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") }
)
$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
}
}
}
# 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:\Program Files (x86)\WAI\PC-DMIS*",
"C:\ProgramData\Hexagon\PC-DMIS*"
)
foreach ($dmisPath in $pcDmisPaths) {
if (Test-Path $dmisPath) {
@@ -441,6 +561,76 @@ function Get-RemotePCInfo {
}
}
$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 (could be Keyence or EAS1000)
$hasGenspect = $false
foreach ($app in $installedApps) {
if ($app -match "^Genspect") {
$hasGenspect = $true
break
}
}
$result.HasGenspect = $hasGenspect
# Determine PC Type based on detected software
# Priority: CMM > Wax Trace > Keyence > EAS1000 > Generic hint > Measuring (default)
$detectedPcType = "Measuring" # Default for shopfloor PCs
if ($result.IsCMM) {
$detectedPcType = "CMM"
} elseif ($result.IsWaxTrace) {
$detectedPcType = "Wax Trace"
} elseif ($result.IsKeyence) {
$detectedPcType = "Keyence"
} elseif ($result.IsEAS1000) {
$detectedPcType = "EAS1000"
} elseif ($result.GenericTypeHint) {
# Use generic machine number hint when no software detected
$detectedPcType = $result.GenericTypeHint
}
$result.DetectedPcType = $detectedPcType
$result.Success = $true
} catch {
@@ -467,8 +657,13 @@ function Send-PCDataToApi {
)
try {
# Determine PC type based on machine number
$pcType = if ($PCData.MachineNo) { "Shopfloor" } else { "Standard" }
# 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 = @{
@@ -504,6 +699,11 @@ function Send-PCDataToApi {
$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
}
# Send to API
$response = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $postData -ErrorAction Stop
@@ -694,6 +894,30 @@ foreach ($result in $results) {
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.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" }
$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
@@ -706,6 +930,22 @@ foreach ($result in $results) {
if ($apiResult.Success) {
Write-Log " -> Updated in ShopDB (MachineID: $($apiResult.MachineId))" -Level "SUCCESS"
# Update WinRM status - since we successfully connected via WinRM, mark it as enabled
try {
$winrmBody = @{
action = "updateWinRMStatus"
hostname = $result.Hostname
hasWinRM = "1"
}
$winrmResponse = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $winrmBody -ErrorAction Stop
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
@@ -731,17 +971,114 @@ foreach ($result in $results) {
Write-Host ""
}
# Report any connection errors
# 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"
$failCount++
$failedPCs += @{
$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
}
$ipResponse = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $ipLookupBody -ErrorAction Stop
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"
$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