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:
273
remote-execution/Install-KioskApp.ps1
Normal file
273
remote-execution/Install-KioskApp.ps1
Normal 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
|
||||
3
remote-execution/Test-UserRegistryDetection.bat
Normal file
3
remote-execution/Test-UserRegistryDetection.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Test-UserRegistryDetection.ps1"
|
||||
pause
|
||||
50
remote-execution/Test-UserRegistryDetection.ps1
Normal file
50
remote-execution/Test-UserRegistryDetection.ps1
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user