Add reboot mode, TLS fallback, kiosk app detection, and user registry scanning

- Add -Reboot/-MinUptimeDays parameter set to reboot high-uptime PCs via API
- Add WebClient fallback for TLS/SSL connection errors on API calls
- Add per-user registry scanning (HKU hives) for installed app detection
- Add Dashboard and Lobby Display kiosk app detection (app IDs 82, 83)
- Add SkipCertificateCheck support for PowerShell 7+
- Increase session timeouts (20s connect, 120s operation)
- Increase default ThrottleLimit from 10 to 25
- Add Install-KioskApp.ps1 and user registry detection test scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-03 10:44:42 -05:00
parent 8c9dac5e2e
commit f40b79c087
4 changed files with 723 additions and 20 deletions

View File

@@ -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

View File

@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Test-UserRegistryDetection.ps1"
pause

View File

@@ -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

View File

@@ -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