<# .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 .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()] [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 = 10, [Parameter()] [switch]$WhatIf ) # SSL/TLS Certificate Bypass for HTTPS connections 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 #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) try { Write-Log "Querying API: $ApiUrl`?action=getShopfloorPCs" -Level "INFO" $response = Invoke-RestMethod -Uri "$ApiUrl`?action=getShopfloorPCs" -Method Get -ErrorAction Stop if ($response.success -and $response.data) { Write-Log "API returned $($response.count) shopfloor PCs" -Level "SUCCESS" # Return full PC objects, not just hostnames return $response.data } else { Write-Log "No shopfloor PCs returned from API" -Level "WARNING" return @() } } catch { Write-Log "Failed to query API for shopfloor PCs: $_" -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.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 } # 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$" = "Part Marker" # Part markers "^0613$" = "Part Marker" # Part markers "^0615" = "Part Marker" # Part markers "^8003$" = "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 # 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\*" ) 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 # ================================================================ # 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) { $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") } @{ app_id = 77; app_name = "HeatTreat"; patterns = @("HeatTreat") } ) $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 # Determine PC Type based on detected software # Priority: CMM > Wax Trace > Keyence > EAS1000 > Genspect > Heat Treat > Generic hint > Shopfloor (default) $detectedPcType = "Shopfloor" # 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 ($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 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 } # Send to API $response = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $postData -ErrorAction Stop return @{ Success = $response.success Message = $response.message MachineId = $response.machineid } } catch { return @{ Success = $false Message = $_.Exception.Message 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 } 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) $sessionOption = New-PSSessionOption -OpenTimeout 15000 -OperationTimeout 30000 -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.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" } 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" # 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 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 } $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 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