diff --git a/remote-execution/Install-KioskApp.ps1 b/remote-execution/Install-KioskApp.ps1 new file mode 100644 index 0000000..4aaa5e5 --- /dev/null +++ b/remote-execution/Install-KioskApp.ps1 @@ -0,0 +1,273 @@ +<# +.SYNOPSIS + Remote installer for Dashboard and Lobby Display kiosk applications. + +.DESCRIPTION + Deploys GE Aerospace Dashboard or Lobby Display kiosk installers to remote PCs via WinRM. + Pushes the installer file and runs it silently. + +.PARAMETER ComputerName + Single computer name, IP address, or array of computers to target. + +.PARAMETER ComputerListFile + Path to text file containing computer names/IPs (one per line). + +.PARAMETER App + Which application to install: Dashboard or LobbyDisplay + +.PARAMETER InstallerPath + Path to the installer .exe file. If not specified, looks in the script directory. + +.PARAMETER Credential + PSCredential for remote authentication. Prompts if not provided. + +.PARAMETER Uninstall + Uninstall the application instead of installing. + +.EXAMPLE + # Install Dashboard on a single PC + .\Install-KioskApp.ps1 -ComputerName "PC001" -App Dashboard + +.EXAMPLE + # Install Lobby Display on multiple PCs from file + .\Install-KioskApp.ps1 -ComputerListFile ".\lobby-pcs.txt" -App LobbyDisplay + +.EXAMPLE + # Uninstall Dashboard from a PC + .\Install-KioskApp.ps1 -ComputerName "PC001" -App Dashboard -Uninstall + +.NOTES + Author: Shop Floor Tools + Requires: PowerShell 5.1+, WinRM enabled on targets, Admin credentials +#> + +[CmdletBinding()] +param( + [Parameter(Position=0)] + [string[]]$ComputerName, + + [Parameter()] + [string]$ComputerListFile, + + [Parameter(Mandatory=$true)] + [ValidateSet('Dashboard', 'LobbyDisplay')] + [string]$App, + + [Parameter()] + [string]$InstallerPath, + + [Parameter()] + [PSCredential]$Credential, + + [Parameter()] + [string]$DnsSuffix = "logon.ds.ge.com", + + [Parameter()] + [switch]$Uninstall +) + +# ============================================================================= +# Configuration +# ============================================================================= +$AppConfig = @{ + 'Dashboard' = @{ + InstallerName = 'GEAerospaceDashboardSetup.exe' + AppName = 'GE Aerospace Dashboard' + UninstallGuid = '{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}' + } + 'LobbyDisplay' = @{ + InstallerName = 'GEAerospaceLobbyDisplaySetup.exe' + AppName = 'GE Aerospace Lobby Display' + UninstallGuid = '{42FFB952-0B72-493F-8869-D957344CA305}' + } +} + +$config = $AppConfig[$App] + +# ============================================================================= +# Helper Functions +# ============================================================================= + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $color = switch ($Level) { + "ERROR" { "Red" } + "WARNING" { "Yellow" } + "SUCCESS" { "Green" } + "TASK" { "Cyan" } + default { "White" } + } + Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color +} + +# ============================================================================= +# Main Execution +# ============================================================================= + +Write-Host "" +Write-Host ("=" * 70) -ForegroundColor Cyan +Write-Host " Kiosk App Installer - $($config.AppName)" -ForegroundColor Cyan +Write-Host ("=" * 70) -ForegroundColor Cyan +Write-Host "" + +# Get credentials +if (-not $Credential) { + Write-Log "Enter credentials for remote PCs:" -Level "INFO" + $Credential = Get-Credential -Message "Enter admin credentials for remote PCs" + if (-not $Credential) { + Write-Log "Credentials required. Exiting." -Level "ERROR" + exit 1 + } +} + +# Build computer list +$computers = @() + +if ($ComputerListFile) { + if (Test-Path $ComputerListFile) { + $computers = Get-Content $ComputerListFile | Where-Object { $_.Trim() -and -not $_.StartsWith("#") } + } else { + Write-Log "Computer list file not found: $ComputerListFile" -Level "ERROR" + exit 1 + } +} elseif ($ComputerName) { + $computers = $ComputerName +} else { + Write-Log "No computers specified. Use -ComputerName or -ComputerListFile" -Level "ERROR" + exit 1 +} + +if ($computers.Count -eq 0) { + Write-Log "No computers to process." -Level "ERROR" + exit 1 +} + +Write-Log "Target computers: $($computers.Count)" -Level "INFO" +Write-Log "Action: $(if ($Uninstall) { 'Uninstall' } else { 'Install' })" -Level "TASK" +Write-Host "" + +# Find installer if not specified (for install only) +if (-not $Uninstall) { + if (-not $InstallerPath) { + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + $InstallerPath = Join-Path $scriptDir $config.InstallerName + } + + if (-not (Test-Path $InstallerPath)) { + Write-Log "Installer not found: $InstallerPath" -Level "ERROR" + Write-Log "Please specify -InstallerPath or place $($config.InstallerName) in the script directory" -Level "ERROR" + exit 1 + } + + Write-Log "Installer: $InstallerPath" -Level "INFO" +} + +# Build FQDNs +$targetFQDNs = $computers | ForEach-Object { + if ($_ -like "*.*") { $_ } else { "$_.$DnsSuffix" } +} + +# Create session options +$sessionOption = New-PSSessionOption -OpenTimeout 30000 -OperationTimeout 300000 -NoMachineProfile + +$successCount = 0 +$failCount = 0 + +foreach ($fqdn in $targetFQDNs) { + Write-Host "" + Write-Log "Processing: $fqdn" -Level "TASK" + + try { + # Create session + $session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop + + if ($Uninstall) { + # Uninstall + Write-Log " Uninstalling $($config.AppName)..." -Level "INFO" + + $result = Invoke-Command -Session $session -ScriptBlock { + param($guid, $appName) + $result = @{ Success = $false; Output = ""; Error = $null } + + # Find uninstaller + $uninstallPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$guid`_is1" + if (-not (Test-Path $uninstallPath)) { + $uninstallPath = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$guid`_is1" + } + + if (Test-Path $uninstallPath) { + $uninstallString = (Get-ItemProperty $uninstallPath).UninstallString + if ($uninstallString) { + # Run uninstaller silently + $uninstallExe = $uninstallString -replace '"', '' + $proc = Start-Process -FilePath $uninstallExe -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" -Wait -PassThru -WindowStyle Hidden + $result.Success = ($proc.ExitCode -eq 0) + $result.Output = "Exit code: $($proc.ExitCode)" + } else { + $result.Error = "No uninstall string found" + } + } else { + $result.Error = "$appName not found in registry" + } + + return $result + } -ArgumentList $config.UninstallGuid, $config.AppName + + } else { + # Install + $remoteTempPath = "C:\Windows\Temp\$($config.InstallerName)" + + Write-Log " Pushing installer to remote PC..." -Level "INFO" + Copy-Item -Path $InstallerPath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop + + Write-Log " Running installer silently..." -Level "INFO" + $result = Invoke-Command -Session $session -ScriptBlock { + param($installerPath) + $result = @{ Success = $false; Output = ""; Error = $null } + + try { + $proc = Start-Process -FilePath $installerPath -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" -Wait -PassThru -WindowStyle Hidden + $result.Success = ($proc.ExitCode -eq 0) + $result.Output = "Exit code: $($proc.ExitCode)" + + # Clean up installer + Remove-Item $installerPath -Force -ErrorAction SilentlyContinue + } catch { + $result.Error = $_.Exception.Message + } + + return $result + } -ArgumentList $remoteTempPath + } + + # Close session + Remove-PSSession $session -ErrorAction SilentlyContinue + + # Process result + if ($result.Success) { + Write-Log "[OK] $fqdn - $($result.Output)" -Level "SUCCESS" + $successCount++ + } else { + $errorMsg = if ($result.Error) { $result.Error } else { $result.Output } + Write-Log "[FAIL] $fqdn - $errorMsg" -Level "ERROR" + $failCount++ + } + + } catch { + Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR" + $failCount++ + if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue } + } +} + +# Summary +Write-Host "" +Write-Host ("=" * 70) -ForegroundColor Cyan +Write-Host " SUMMARY" -ForegroundColor Cyan +Write-Host ("=" * 70) -ForegroundColor Cyan +Write-Host " App: $($config.AppName)" -ForegroundColor White +Write-Host " Action: $(if ($Uninstall) { 'Uninstall' } else { 'Install' })" -ForegroundColor White +Write-Host " Successful: $successCount" -ForegroundColor Green +Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" }) +Write-Host ("=" * 70) -ForegroundColor Cyan diff --git a/remote-execution/Test-UserRegistryDetection.bat b/remote-execution/Test-UserRegistryDetection.bat new file mode 100644 index 0000000..7cdda42 --- /dev/null +++ b/remote-execution/Test-UserRegistryDetection.bat @@ -0,0 +1,3 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Test-UserRegistryDetection.ps1" +pause diff --git a/remote-execution/Test-UserRegistryDetection.ps1 b/remote-execution/Test-UserRegistryDetection.ps1 new file mode 100644 index 0000000..ea8bf52 --- /dev/null +++ b/remote-execution/Test-UserRegistryDetection.ps1 @@ -0,0 +1,50 @@ +# Test script to debug user registry detection +Write-Host "=== User Registry Detection Test ===" -ForegroundColor Cyan + +# 1. Check HKU (HKEY_USERS) for all loaded user hives +Write-Host "`n[1] Checking loaded user hives in HKU:" -ForegroundColor Yellow +$hkuKeys = Get-ChildItem "Registry::HKU" -ErrorAction SilentlyContinue +foreach ($key in $hkuKeys) { + $sid = $key.PSChildName + if ($sid -match "^S-1-5-21-" -and $sid -notmatch "_Classes$") { + Write-Host " SID: $sid" -ForegroundColor Gray + $uninstallPath = "Registry::HKU\$sid\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + if (Test-Path $uninstallPath) { + $apps = Get-ItemProperty $uninstallPath -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "*Dashboard*" -or $_.DisplayName -like "*GE Aerospace*" } + if ($apps) { + foreach ($app in $apps) { + Write-Host " FOUND: $($app.DisplayName)" -ForegroundColor Green + } + } else { + Write-Host " No Dashboard/GE Aerospace apps" -ForegroundColor Gray + } + } + } +} + +# 2. Check HKCU directly +Write-Host "`n[2] Checking HKCU (current user):" -ForegroundColor Yellow +$hkcuApps = Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "*Dashboard*" -or $_.DisplayName -like "*GE Aerospace*" } +if ($hkcuApps) { + foreach ($app in $hkcuApps) { + Write-Host " FOUND: $($app.DisplayName)" -ForegroundColor Green + } +} else { + Write-Host " No Dashboard/GE Aerospace apps in HKCU" -ForegroundColor Gray +} + +# 3. Check user profiles +Write-Host "`n[3] User profiles in C:\Users:" -ForegroundColor Yellow +$profiles = Get-ChildItem "C:\Users" -Directory | Where-Object { $_.Name -notin @('Public','Default','Default User','All Users') } +foreach ($p in $profiles) { + Write-Host " $($p.Name)" -ForegroundColor Gray +} + +# 4. Current user context +Write-Host "`n[4] Current execution context:" -ForegroundColor Yellow +Write-Host " Username: $env:USERNAME" -ForegroundColor Gray +Write-Host " UserDomain: $env:USERDOMAIN" -ForegroundColor Gray + +Write-Host "`n=== Done ===" -ForegroundColor Cyan diff --git a/remote-execution/Update-ShopfloorPCs-Remote.ps1 b/remote-execution/Update-ShopfloorPCs-Remote.ps1 index 35ffb9b..b475312 100644 --- a/remote-execution/Update-ShopfloorPCs-Remote.ps1 +++ b/remote-execution/Update-ShopfloorPCs-Remote.ps1 @@ -35,6 +35,18 @@ $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) @@ -53,6 +65,15 @@ param( [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, @@ -69,16 +90,36 @@ param( [switch]$UseSSL, [Parameter()] - [int]$ThrottleLimit = 10, + [int]$ThrottleLimit = 25, [Parameter()] [switch]$WhatIf ) -# SSL/TLS Certificate Bypass for HTTPS connections +# SSL/TLS Configuration - MUST be set before any web requests +# Set ALL modern TLS versions - fixes "underlying connection was closed" errors try { - if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) { - Add-Type @" + # 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 { @@ -89,10 +130,22 @@ public class TrustAllCertsPolicy : ICertificatePolicy { } } "@ + } + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + } catch { + Write-Warning "Could not set certificate policy: $_" } - [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy -} catch { } -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + # 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 @@ -115,21 +168,108 @@ function Get-ShopfloorPCsFromApi { #> param([string]$ApiUrl) + $fullUrl = "$ApiUrl`?action=getShopfloorPCs" + Write-Log "Querying API: $fullUrl" -Level "INFO" + + # Try PowerShell native method first try { - Write-Log "Querying API: $ApiUrl`?action=getShopfloorPCs" -Level "INFO" - $response = Invoke-RestMethod -Uri "$ApiUrl`?action=getShopfloorPCs" -Method Get -ErrorAction Stop + # 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 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" + $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 @() } } @@ -461,7 +601,8 @@ function Get-RemotePCInfo { $hasVnc = $false $regPaths = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\SOFTWARE\WOW6432Node\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) { @@ -486,6 +627,8 @@ function Get-RemotePCInfo { # 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 | @@ -500,6 +643,29 @@ function Get-RemotePCInfo { } } + # 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) # ================================================================ @@ -520,6 +686,8 @@ function Get-RemotePCInfo { @{ 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 = @() @@ -678,10 +846,36 @@ function Get-RemotePCInfo { } $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: CMM > Wax Trace > Keyence > EAS1000 > Genspect > Heat Treat > Generic hint > Shopfloor (default) + # Priority: Dashboard > Lobby Display > CMM > Wax Trace > Keyence > EAS1000 > Genspect > Heat Treat > Generic hint > Shopfloor (default) $detectedPcType = "Shopfloor" # Default for shopfloor PCs - if ($result.IsCMM) { + if ($hasDashboard) { + $detectedPcType = "Dashboard" + } elseif ($hasLobbyDisplay) { + $detectedPcType = "Lobby Display" + } elseif ($result.IsCMM) { $detectedPcType = "CMM" } elseif ($result.IsWaxTrace) { $detectedPcType = "Wax Trace" @@ -776,8 +970,23 @@ function Send-PCDataToApi { $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 -Uri $ApiUrl -Method Post -Body $postData -ErrorAction Stop + $response = Invoke-RestMethod @restParams return @{ Success = $response.success @@ -785,9 +994,37 @@ function Send-PCDataToApi { MachineId = $response.machineid } } 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 + } + } catch { + # WebClient also failed + } + } + return @{ Success = $false - Message = $_.Exception.Message + Message = if ($innerMsg) { "$errMsg ($innerMsg)" } else { $errMsg } MachineId = $null } } @@ -815,6 +1052,121 @@ if ($SetupTrustedHosts) { 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" @@ -919,7 +1271,8 @@ if ($WhatIf) { $fqdnList = $targetComputers | ForEach-Object { $_.FQDN } # Create session options with short timeout (15 seconds per PC) -$sessionOption = New-PSSessionOption -OpenTimeout 15000 -OperationTimeout 30000 -NoMachineProfile +# Timeouts: 20s to connect, 120s for operations (app detection takes time) +$sessionOption = New-PSSessionOption -OpenTimeout 20000 -OperationTimeout 120000 -NoMachineProfile $sessionParams = @{ ComputerName = $fqdnList @@ -973,6 +1326,8 @@ foreach ($result in $results) { # 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" } @@ -1011,7 +1366,18 @@ foreach ($result in $results) { hostname = $result.Hostname hasWinRM = "1" } - $winrmResponse = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $winrmBody -ErrorAction Stop + $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" } @@ -1071,7 +1437,18 @@ if ($connectionFailures.Count -gt 0) { action = "getRecordedIP" hostname = $hostname } - $ipResponse = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $ipLookupBody -ErrorAction Stop + $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