From 86b32d859761dcf53f4c32eadde671ff67d50ad2 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 17 Apr 2026 12:04:40 -0400 Subject: [PATCH] Add fixnetworkshare, winrm-setup-package, udc remote-execution suites - NetworkDriveManager.ps1: S: drive repair utility - winrm-setup-package: Invoke-RemoteTask helper + Setup-WinRM.bat + HTML guide - remote-execution/udc: UDC_Update.ps1 and batch wrappers for updating DNC controllers on shop-floor PCs - Invoke-RemoteMaintenance.ps1: substantial rework (~1650 lines) - Schedule-Maintenance and complete-asset minor updates - Bump edncfix gitlink to v1.6.0 (2748bfa) - .gitignore: block inventory.csv/xlsx (CUI) and logs_*.txt (per-host logs) Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 7 + complete-asset/Update-PC-CompleteAsset.ps1 | 21 +- docs/Invoke-RemoteMaintenance.html | 2535 +++++++++++------ docs/Invoke-RemoteMaintenance.md | 301 +- docs/SCRIPTS_REFERENCE.md | 2 +- docs/convert_to_docx.py | 308 ++ edncfix | 2 +- fixnetworkshare/NetworkDriveManager.ps1 | 1149 ++++++++ minimal-asset/Update-PC-Minimal.ps1 | 6 +- .../DeployOpenTextProfiles-Examples.txt | 29 + remote-execution/INSTRUCTIONS.txt | 141 +- remote-execution/Invoke-RemoteMaintenance.ps1 | 1696 +++++++++-- remote-execution/Resume-Download.bat | 18 + remote-execution/Resume-Download.ps1 | 117 + remote-execution/Run-UpdateDNCMXHosts.bat | 32 + remote-execution/Schedule-Maintenance.ps1 | 95 +- remote-execution/udc/UDC_Update.ps1 | 279 ++ remote-execution/udc/udc_update.bat | 8 + remote-execution/udc/udc_update_override.bat | 11 + winrm-setup-package/Invoke-RemoteTask.ps1 | 535 ++++ winrm-setup-package/README.md | 296 ++ winrm-setup-package/Setup-WinRM.bat | 269 ++ winrm-setup-package/WinRM-Setup-Guide.html | 424 +++ winrm-setup-package/hosts.txt | 16 + 24 files changed, 6945 insertions(+), 1352 deletions(-) create mode 100644 docs/convert_to_docx.py create mode 100644 fixnetworkshare/NetworkDriveManager.ps1 create mode 100644 remote-execution/DeployOpenTextProfiles-Examples.txt create mode 100644 remote-execution/Resume-Download.bat create mode 100644 remote-execution/Resume-Download.ps1 create mode 100644 remote-execution/Run-UpdateDNCMXHosts.bat create mode 100755 remote-execution/udc/UDC_Update.ps1 create mode 100755 remote-execution/udc/udc_update.bat create mode 100644 remote-execution/udc/udc_update_override.bat create mode 100644 winrm-setup-package/Invoke-RemoteTask.ps1 create mode 100644 winrm-setup-package/README.md create mode 100644 winrm-setup-package/Setup-WinRM.bat create mode 100644 winrm-setup-package/WinRM-Setup-Guide.html create mode 100644 winrm-setup-package/hosts.txt diff --git a/.gitignore b/.gitignore index c0bddfc..1266a13 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,13 @@ logs/ # CSV data files (generated) applications.csv +# Inventory data (contains CUI / employee SSO / MAC addresses) +inventory.csv +inventory.xlsx + +# Per-host log files written by remote-execution scripts +logs_*.txt + # Text files with hostnames/IPs (sensitive) computers.txt shopfloor-pcs.txt diff --git a/complete-asset/Update-PC-CompleteAsset.ps1 b/complete-asset/Update-PC-CompleteAsset.ps1 index f0dec63..363536d 100644 --- a/complete-asset/Update-PC-CompleteAsset.ps1 +++ b/complete-asset/Update-PC-CompleteAsset.ps1 @@ -614,11 +614,19 @@ function Get-PCType { if ($domain -eq "logon.ds.ge.com") { Write-Host " [OK] Shopfloor domain detected" -ForegroundColor Green - # Check for specific machine type applications - $installedApps = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName } | - Select-Object -ExpandProperty DisplayName + # Check for specific machine type applications (include per-user installs) + $regPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + $installedApps = foreach ($regPath in $regPaths) { + if (Test-Path $regPath) { + Get-ItemProperty $regPath -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName } | + Select-Object -ExpandProperty DisplayName + } + } # ================================================================ # PC Type Detection based on installed software @@ -821,6 +829,9 @@ function Collect-SystemInfo { $installedApps = @() $installedApps += Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName} $installedApps += Get-ItemProperty HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName} + if (Test-Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*") { + $installedApps += Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName} + } $filteredApps = $installedApps | Select-Object DisplayName, DisplayVersion | Sort-Object DisplayName -Unique diff --git a/docs/Invoke-RemoteMaintenance.html b/docs/Invoke-RemoteMaintenance.html index 7182b19..7ae6c70 100644 --- a/docs/Invoke-RemoteMaintenance.html +++ b/docs/Invoke-RemoteMaintenance.html @@ -3,906 +3,1497 @@ - Invoke-RemoteMaintenance.ps1 + Invoke-RemoteMaintenance.ps1 - GE Aerospace Knowledge Base -

Invoke-RemoteMaintenance.ps1

-

Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via WinRM.

-

Table of Contents

- -
-

Overview

-

This script provides a comprehensive remote maintenance toolkit for managing shopfloor PCs. It executes maintenance tasks via WinRM (Windows Remote Management) and can target PCs individually, by type, by business unit, or all at once.

-

Location: S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1

-

Key Features:

- -
-

API Integration

-

When using -All, -PcType, or -BusinessUnit targeting, the script retrieves PC lists from the ShopDB API:

-
GET /api.asp?action=getShopfloorPCs
+    
+ +
+
+ Knowledge Base Article +

Invoke-RemoteMaintenance.ps1

+

Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via WinRM

+
+ +
+ + +
+ + + + + +
+

Overview

+

This script provides a comprehensive remote maintenance toolkit for managing shopfloor PCs. It executes maintenance tasks via WinRM (Windows Remote Management) and can target PCs individually, by type, by business unit, or all at once.

+

Location: S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1

+ +

Key Features:

+
    +
  • 22 maintenance tasks available
  • +
  • General-purpose file deployment (CopyFile) and registry import (ImportReg) tasks
  • +
  • Registry imports run as logged-in user via scheduled task (HKCU support)
  • +
  • Optional post-copy commands via scheduled task (service/app restarts)
  • +
  • Multiple targeting options (by name, type, business unit, or all)
  • +
  • Concurrent execution with configurable throttling
  • +
  • Integration with ShopDB for PC discovery
  • +
+
+ + +

First-Time Setup

+

Before running this script for the first time, you must allow PowerShell script execution and unblock the script file.

+ +
+ Step 1 +

Allow PowerShell Script Execution

+

Open PowerShell as Administrator and run:

+
+ +
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
+
+

When prompted, type Y and press Enter. This allows locally-created scripts to run and requires downloaded scripts to be signed or unblocked.

+
+ Note: + You only need to do this once per user account. RemoteSigned is the recommended policy — it allows local scripts while still protecting against untrusted downloads. +
+
+ +
+ Step 2 +

Unblock the Script File

+

Files downloaded from a network share or the internet are marked as "blocked" by Windows. You must unblock the script before it can run.

+

Option A — File Explorer (GUI):

+
    +
  1. Navigate to S:\dt\shopfloor\scripts\remote-execution\
  2. +
  3. Right-click Invoke-RemoteMaintenance.ps1 and select Properties
  4. +
  5. At the bottom of the General tab, check the Unblock checkbox
  6. +
  7. Click Apply, then OK
  8. +
+

Option B — PowerShell:

+
+ +
Unblock-File -Path "S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1"
+
+
+ Important: + If you skip this step, you will get a security error when trying to run the script: "cannot be loaded because running scripts is disabled on this system" or "cannot be loaded. The file is not digitally signed." +
+
+ +
+ + +

API Integration

+

When using -All, -PcType, or -BusinessUnit targeting, the script retrieves PC lists from the ShopDB API:

+ +
+ +
GET /api.asp?action=getShopfloorPCs
 GET /api.asp?action=getShopfloorPCs&pctypeid=2      # CMM PCs only
-GET /api.asp?action=getShopfloorPCs&businessunitid=1 # Specific business unit
-

PC Type IDs:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IDTypeIDType
1Shopfloor7Heat Treat
2CMM8Engineer
3Wax Trace9Standard
4Keyence10Inspection
5EAS100011Dashboard
6Genspect12Lobby Display
-

See: ShopDB API Reference for complete API documentation.

-
-

Prerequisites

-

On Your Workstation

-
    -
  1. PowerShell 5.1 or higher
  2. -
  3. Network access to target PCs (TCP port 5985)
  4. -
  5. Admin credentials for target PCs
  6. -
-

On Target PCs

-
    -
  1. WinRM enabled (Enable-PSRemoting -Force)
  2. -
  3. Firewall rules allowing WinRM traffic
  4. -
-

Verify Connectivity

-
# Test WinRM connectivity
-Test-WSMan -ComputerName "SHOPFLOOR-PC01"
+GET /api.asp?action=getShopfloorPCs&businessunitid=1 # Specific business unit
+
+ +

PC Type IDs

+ + + + + + + + + + + + +
IDTypeIDType
1Standard7Keyence
2Engineer8Genspect
3Shopfloor9Heat Treat
4Uncategorized10Inspection
5CMM11Dashboard
6Wax / Trace12Lobby Display
+ +
+ Reference: + See ShopDB API Reference for complete API documentation. +
+ +
+ + +

Prerequisites

+ +

On Your Workstation

+
    +
  1. PowerShell 5.1 or higher
  2. +
  3. Network access to target PCs (TCP port 5985)
  4. +
  5. Admin credentials for target PCs
  6. +
+ +

On Target PCs

+
    +
  1. WinRM enabled (Enable-PSRemoting -Force)
  2. +
  3. Firewall rules allowing WinRM traffic
  4. +
+ +

Verify Connectivity

+
+ +
# Test WinRM connectivity
+Test-WSMan -ComputerName "SHOPFLOOR-PC01"
 
 # Test with credentials
 $cred = Get-Credential
-Test-WSMan -ComputerName "SHOPFLOOR-PC01" -Credential $cred
-
-

Quick Start

-

Step 1: Get Credentials

-
$cred = Get-Credential -Message "Enter domain admin credentials"
-

Step 2: Run a Simple Task

-
# Flush DNS on a single PC
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "SHOPFLOOR-PC01" -Task FlushDNS -Credential $cred
-

Step 3: Check Results

-

The script outputs status for each PC:

-
[SHOPFLOOR-PC01] FlushDNS: SUCCESS
-  DNS Resolver Cache flushed successfully
-
-

Parameters Reference

-

Targeting Parameters (Mutually Exclusive)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterTypeDescription
-ComputerNamestring[]One or more computer names or IPs
-ComputerListFilestringPath to text file with hostnames
-AllswitchTarget all shopfloor PCs from ShopDB
-PcTypestringTarget by PC type (see PC Types)
-BusinessUnitstringTarget by business unit (see Business Units)
-

Task Parameter (Required)

- - - - - - - - - - - - -
ParameterTypeDescription
-TaskstringMaintenance task to execute
-

Optional Parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterTypeDefaultDescription
-CredentialPSCredentialPromptRemote authentication
-ApiUrlstringProductionShopDB API endpoint
-ThrottleLimitint5Max concurrent sessions
-DnsSuffixstringlogon.ds.ge.comDNS suffix for resolution
-

PC Types

-
Standard, Engineer, Shopfloor, CMM, Wax / Trace, Keyence,
-Genspect, Heat Treat, Inspection, Dashboard, Lobby Display, Uncategorized
-

Business Units

-
TBD, Blisk, HPT, Spools, Inspection, Venture, Turn/Burn, DT
-
-

Available Tasks

-

Repair Tasks

- - - - - - - - - - - - - - - - - - - - -
TaskDescriptionDurationImpact
DISMRepair Windows component store15-60 minLow
SFCSystem File Checker scan10-30 minLow
-

Optimization Tasks

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TaskDescriptionDurationImpact
OptimizeDiskTRIM (SSD) or Defrag (HDD)5-60 minMedium
DiskCleanupRemove temp files, updates5-15 minLow
ClearUpdateCacheClear Windows Update cache1-2 minLow
ClearBrowserCacheClear Chrome/Edge cache1-2 minLow
-

Service Tasks

- - - - - - - - - - - - - - - - - - - - - - - - - - -
TaskDescriptionDurationImpact
RestartSpoolerRestart Print Spooler<1 minLow
FlushDNSClear DNS cache<1 minNone
RestartWinRMRestart WinRM service<1 minTemp disconnect
-

Time/Date Tasks

- - - - - - - - - - - - - - - - - - - - -
TaskDescriptionDurationImpact
SetTimezoneSet to Eastern Time<1 minNone
SyncTimeForce time sync with DC<1 minNone
-

DNC Tasks

- - - - - - - - - - - - - - - - - - - - -
TaskDescriptionDurationImpact
UpdateEMxAuthTokenUpdate eMx auth from share1-2 minNone
DeployUDCWebServerConfigDeploy UDC config1-2 minNone
-

System Tasks

- - - - - - - - - - - - - - -
TaskDescriptionDurationImpact
RebootRestart PC (30s delay)2-5 minHigh
-

Software Deployment Tasks

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TaskDescriptionDurationImpact
InstallDashboardInstall GE Dashboard app2-5 minMedium
InstallLobbyDisplayInstall Lobby Display app2-5 minMedium
UninstallDashboardRemove GE Dashboard1-2 minLow
UninstallLobbyDisplayRemove Lobby Display1-2 minLow
-
-

Software Deployment Mechanism

-

Source File Locations

-

Deployment tasks require source files to be available before execution:

- - - - - - - - - - - - - - - - - - - - - - -
TaskSource File Path
InstallDashboard\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe
InstallLobbyDisplay\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe
UpdateEMxAuthToken\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt
DeployUDCWebServerConfig\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\UDC\udc_webserver_settings.json
-

How Deployment Works

-
    -
  1. Pre-flight Check: Script verifies source file exists
  2. -
  3. WinRM Session: Opens remote session to target PC
  4. -
  5. File Push: Copies source file to C:\Windows\Temp\ on remote PC
  6. -
  7. Execution: Runs install/copy task using pushed file
  8. -
  9. Cleanup: Removes temp file from remote PC
  10. -
-
+---------------------+     WinRM      +---------------------+
-|  Your Workstation   | ------------>  |   Target PC         |
+Test-WSMan -ComputerName "SHOPFLOOR-PC01" -Credential $cred
+
+ +
+ + +

Quick Start

+ +
+ Step 1 +

Get Credentials

+
+ +
$cred = Get-Credential -Message "Enter domain admin credentials"
+
+
+ +
+ Step 2 +

Run a Simple Task

+
+ +
# Flush DNS on a single PC
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "SHOPFLOOR-PC01" -Task FlushDNS -Credential $cred
+
+
+ +
+ Step 3 +

Check Results

+

The script outputs status for each PC:

+
+
[SHOPFLOOR-PC01] FlushDNS: SUCCESS
+  DNS Resolver Cache flushed successfully
+
+
+ +
+ + +

Parameters Reference

+ +

Targeting Parameters (Mutually Exclusive)

+ + + + + + + + + + + +
ParameterTypeDescription
-ComputerNamestring[]One or more computer names or IPs
-ComputerListFilestringPath to text file with hostnames
-AllswitchTarget all shopfloor PCs from ShopDB
-PcTypestringTarget by PC type (see PC Types)
-BusinessUnitstringTarget by business unit (see Business Units)
+ +

Task Parameter (Required)

+ + + + + + + +
ParameterTypeDescription
-TaskstringMaintenance task to execute
+ +

File Deployment Parameters (CopyFile / ImportReg)

+ + + + + + + + + + +
ParameterTypeDescription
-SourcePathstringSource file path (local or UNC). Required for CopyFile and ImportReg
-DestinationPathstringDestination file path on remote PCs. Required for CopyFile
-RunCommandstringCommand to run after CopyFile. Runs as logged-in user by default (via scheduled task)
-AsSystemswitchRun ImportReg and -RunCommand in the WinRM session (SYSTEM context) instead of as logged-in user
+ +

Optional Parameters

+ + + + + + + + + + + +
ParameterTypeDefaultDescription
-CredentialPSCredentialPromptRemote authentication
-ApiUrlstringProductionShopDB API endpoint
-ThrottleLimitint5Max concurrent sessions
-DnsSuffixstringlogon.ds.ge.comDNS suffix for FQDN resolution
-LogFileswitchOffEnable transcript logging to logs/ directory
+ +

PC Types

+
+
Standard, Engineer, Shopfloor, CMM, Wax / Trace, Keyence,
+Genspect, Heat Treat, Inspection, Dashboard, Lobby Display, Uncategorized
+
+ +

Business Units

+
+
TBD, Blisk, HPT, Spools, Inspection, Venture, Turn/Burn, DT
+
+ +
+ + +

Available Tasks

+ +

Repair Tasks

+ + + + + + +
TaskDescriptionDurationImpact
DISMRepair Windows component store15-60 minLow
SFCSystem File Checker scan10-30 minLow
+ +

Optimization Tasks

+ + + + + + + + +
TaskDescriptionDurationImpact
OptimizeDiskTRIM (SSD) or Defrag (HDD)5-60 minMedium
DiskCleanupRemove temp files, updates5-15 minLow
ClearUpdateCacheClear Windows Update cache1-2 minLow
ClearBrowserCacheClear Chrome/Edge cache1-2 minLow
+ +

Service Tasks

+ + + + + + + +
TaskDescriptionDurationImpact
RestartSpoolerRestart Print Spooler<1 minLow
FlushDNSClear DNS cache<1 minNone
RestartWinRMRestart WinRM service<1 minTemp disconnect
+ +

Time/Date Tasks

+ + + + + + +
TaskDescriptionDurationImpact
SetTimezoneSet to Eastern Time<1 minNone
SyncTimeForce time sync with DC<1 minNone
+ +

DNC Tasks

+ + + + + + + +
TaskDescriptionDurationImpact
UpdateDNCMXHostsUpdate FtpHostPrimary/Secondary in DNC\MX registry<1 minNone
AuditDNCConfigCompare DNC registry vs UDC backup JSON, export CSV1-5 minNone
CheckDefectTrackerCheck if Defect_Tracker.exe is running, export CSV1-5 minNone
+ +

File Deployment Tasks

+ + + + + + +
TaskDescriptionDurationImpact
CopyFileCopy file from -SourcePath to -DestinationPath on remote PCs1-2 minLow
ImportRegCopy .reg file and import via scheduled task as logged-in user1-2 minLow
+ +

System Tasks

+ + + + + + +
TaskDescriptionDurationImpact
GPUpdateForce Group Policy refresh (gpupdate /force)<1 minLow
RebootRestart PC (30s delay)2-5 minHigh
+ +

Software Deployment Tasks

+ + + + + + + + +
TaskDescriptionDurationImpact
InstallDashboardInstall GE Dashboard app2-5 minMedium
InstallLobbyDisplayInstall Lobby Display app2-5 minMedium
UninstallDashboardRemove GE Dashboard1-2 minLow
UninstallLobbyDisplayRemove Lobby Display1-2 minLow
+ +
+ + +

Software Deployment Mechanism

+ +

Source File Locations

+

Deployment tasks require source files to be available before execution:

+ + + + + + + + +
TaskSource File Path
InstallDashboard\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe
InstallLobbyDisplay\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe
CopyFileAny file — specified via -SourcePath parameter
ImportRegAny .reg file — specified via -SourcePath parameter
+ +

How Deployment Works

+
    +
  1. Pre-flight Check: Script verifies source file exists
  2. +
  3. WinRM Session: Opens remote session to target PC
  4. +
  5. File Push: Copies source file to C:\Windows\Temp\ on remote PC
  6. +
  7. Execution: Runs install/copy/import task using pushed file
  8. +
  9. Post-action: Optionally runs command as logged-in user via scheduled task
  10. +
  11. Cleanup: Removes temp file from remote PC
  12. +
+ +
+
+---------------------+     WinRM      +---------------------+
+|  Your Workstation   | ------------>  |   Target PC         |
 |                     |                |                     |
 |  Source Files:      |   Push File    |  Temp Location:     |
-|  - Setup.exe        | ------------>  |  C:\Windows\Temp    |
+|  - Setup.exe        | ------------>  |  C:\Windows\Temp    |
 |  - config.json      |                |                     |
-|  - eMxInfo.txt      |   Execute      |  Final Location:    |
-|    (network)        | ------------>  |  C:\Program Files   |
-+---------------------+                +---------------------+
-

Directory Structure

-

Ensure your script directory contains the required files:

-
S:\dt\shopfloor\scripts\remote-execution\
-├── Invoke-RemoteMaintenance.ps1
-├── GEAerospaceDashboardSetup.exe      # For InstallDashboard
-├── GEAerospaceLobbyDisplaySetup.exe   # For InstallLobbyDisplay
-└── udc_webserver_settings.json        # For DeployUDCWebServerConfig
-

eMx Auth Token Details

-

The UpdateEMxAuthToken task:

-
    -
  1. Source: \\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt
  2. -
  3. Destinations: (both paths if they exist)
  4. -
-
    -
  • C:\Program Files\GE Aircraft Engines\DNC\eMxInfo.txt
  • -
  • C:\Program Files (x86)\GE Aircraft Engines\DNC\eMxInfo.txt
  • -
-
    -
  1. Backup: Creates eMxInfo-old-YYYYMMDD-HHMMSS.txt before overwriting
  2. -
  3. Post-action: Restarts DNC service (LDnc.exe)
  4. -
-

UDC Web Server Config Details

-

The DeployUDCWebServerConfig task:

-
    -
  1. Pre-check: Verifies UDC is installed (C:\Program Files\UDC exists)
  2. -
  3. Skip: PCs without UDC are skipped (not counted as failures)
  4. -
  5. Destination: C:\ProgramData\UDC\udc_webserver_settings.json
  6. -
  7. Backup: Creates backup before overwriting
  8. -
-

Dashboard/Lobby Display Install Details

-

Both kiosk app installers:

-
    -
  1. Installer type: Inno Setup (supports /VERYSILENT)
  2. -
  3. Execution: Silent install with no user prompts
  4. -
  5. Cleanup: Installer removed from temp after execution
  6. -
-

Uninstall GUIDs:

-
    -
  • Dashboard: {9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}
  • -
  • Lobby Display: {42FFB952-0B72-493F-8869-D957344CA305}
  • -
-

Adding New Deployable Applications

-

To add a new application for deployment, edit the script in two places:

-

Step 1: Add to $KioskAppConfig hashtable (~line 1388)

-
$KioskAppConfig = @{
+|  - settings.reg     |   Execute      |  Final Location:    |
+|    (any path/UNC)   | ------------>  |  -DestinationPath   |
+|                     |                |                     |
+|                     |  Sched. Task   |  Logged-in User:    |
+|                     | ------------>  |  - regedit /s       |
+|                     |                |  - RunCommand        |
++---------------------+                +---------------------+
+
+ +

CopyFile Details

+

The CopyFile task:

+
    +
  1. Source: Any file specified via -SourcePath (local or UNC path)
  2. +
  3. Destination: Specified via -DestinationPath
  4. +
  5. Backup: Existing file is backed up as <name>-old-<timestamp>.<ext>
  6. +
  7. Post-action: If -RunCommand is specified, runs as the logged-in user via a one-shot scheduled task (same pattern as Dashboard/Lobby kiosk relaunch)
  8. +
+ +

ImportReg Details

+

The ImportReg task:

+
    +
  1. Source: .reg file specified via -SourcePath
  2. +
  3. Import method: regedit.exe /s via one-shot scheduled task as logged-in user
  4. +
  5. HKCU support: Runs as the logged-in user, so both HKLM and HKCU keys apply correctly
  6. +
  7. Fallback: If no user is logged in, runs regedit.exe /s directly (HKLM only)
  8. +
  9. Cleanup: Temp .reg file removed after import
  10. +
+ +
+ HKCU vs HKLM: + By default, ImportReg runs as the logged-in user via scheduled task so HKCU keys apply to the correct user profile. Use -AsSystem for HKLM-only .reg files to skip the scheduled task overhead. +
+ +

Dashboard/Lobby Display Install Details

+

Both kiosk app installers:

+
    +
  1. Installer type: Inno Setup (supports /VERYSILENT)
  2. +
  3. Pre-install: Kills running Edge kiosk via PrepareToInstall in Inno Setup
  4. +
  5. Execution: Silent install with no user prompts (120-second timeout)
  6. +
  7. Post-install: Creates a one-shot scheduled task to relaunch Edge in kiosk mode as the logged-in user, then auto-deletes the task
  8. +
  9. Cleanup: Installer removed from temp after execution
  10. +
  11. Connectivity: Offline PCs are skipped with a ping check before connecting
  12. +
+ +

Uninstall GUIDs:

+
    +
  • Dashboard: {9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}
  • +
  • Lobby Display: {42FFB952-0B72-493F-8869-D957344CA305}
  • +
+ +

Adding New Deployable Applications

+ +
+ Step 1 +

Add to $KioskAppConfig hashtable

+
+ +
$KioskAppConfig = @{
     # Existing entries...
 
     # Add new application
-    'InstallNewApp' = @{
-        Action = 'Install'
-        InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe'
-        InstallerName = 'NewAppSetup.exe'
-        AppName = 'New Application Name'
-        UninstallGuid = '{YOUR-GUID-HERE}'  # Find in registry after manual install
+    'InstallNewApp' = @{
+        Action = 'Install'
+        InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe'
+        InstallerName = 'NewAppSetup.exe'
+        AppName = 'New Application Name'
+        UninstallGuid = '{YOUR-GUID-HERE}'  # Find in registry after manual install
+        KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/your-page/'
     }
-    'UninstallNewApp' = @{
-        Action = 'Uninstall'
-        InstallerName = 'NewAppSetup.exe'
-        AppName = 'New Application Name'
-        UninstallGuid = '{YOUR-GUID-HERE}'
+    'UninstallNewApp' = @{
+        Action = 'Uninstall'
+        InstallerName = 'NewAppSetup.exe'
+        AppName = 'New Application Name'
+        UninstallGuid = '{YOUR-GUID-HERE}'
     }
-}
-

Step 2: Add task names to ValidateSet (~line 142)

-
[ValidateSet(
-    'DISM', 'SFC', 'OptimizeDisk', 'DiskCleanup', 'ClearUpdateCache',
-    'RestartSpooler', 'FlushDNS', 'RestartWinRM', 'ClearBrowserCache',
-    'SetTimezone', 'SyncTime', 'UpdateEMxAuthToken', 'DeployUDCWebServerConfig', 'Reboot',
-    'InstallDashboard', 'InstallLobbyDisplay', 'UninstallDashboard', 'UninstallLobbyDisplay',
-    'InstallNewApp', 'UninstallNewApp'  # Add new tasks here
+}
+
+
+ +
+ Step 2 +

Add task names to ValidateSet (~line 142)

+
+ +
[ValidateSet(
+    'DISM', 'SFC', 'OptimizeDisk', 'DiskCleanup', 'ClearUpdateCache',
+    'RestartSpooler', 'FlushDNS', 'RestartWinRM', 'ClearBrowserCache',
+    'SetTimezone', 'SyncTime', 'UpdateDNCMXHosts', 'AuditDNCConfig',
+    'CheckDefectTracker', 'GPUpdate', 'Reboot',
+    'CopyFile', 'ImportReg',
+    'InstallDashboard', 'InstallLobbyDisplay', 'UninstallDashboard', 'UninstallLobbyDisplay',
+    'InstallNewApp', 'UninstallNewApp'  # Add new tasks here
 )]
-[string]$Task
-

Step 3: Place installer on network share

-
\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe
-

Finding the Uninstall GUID:

-

After manually installing the application on a test PC, find the GUID in registry:

-
# Search for app in registry
-Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" |
-    Get-ItemProperty | Where-Object { $_.DisplayName -like "*AppName*" } |
-    Select-Object DisplayName, PSChildName, UninstallString
-

The PSChildName is typically the GUID (e.g., {9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}).

-

Installer Requirements:

-
    -
  • Must support silent installation flags
  • -
  • Inno Setup: /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
  • -
  • MSI: /qn /norestart
  • -
  • NSIS: /S
  • -
-

If your installer uses different flags, modify the InstallKioskApp scriptblock.

-
-

How-To Guides

-

How to Repair System Files

-

Scenario: A PC has corrupted system files causing crashes or errors.

-

Option 1: DISM (Component Store Repair)

-
# Run DISM repair on a single PC
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task DISM -Credential $cred
-

What it does:

-
    -
  • Downloads missing/corrupted files from Windows Update
  • -
  • Repairs the Windows component store
  • -
  • Required before SFC if component store is damaged
  • -
-

Expected output:

-
[PROBLEM-PC] DISM: SUCCESS
+[string]$Task
+
+
+ +
+ Step 3 +

Place installer on network share

+
+
\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe
+
+
+ +

Finding the Uninstall GUID

+

After manually installing the application on a test PC, find the GUID in registry:

+
+ +
# Search for app in registry
+Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" |
+    Get-ItemProperty | Where-Object { $_.DisplayName -like "*AppName*" } |
+    Select-Object DisplayName, PSChildName, UninstallString
+
+

The PSChildName is typically the GUID (e.g., {9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}).

+ +
+ Installer Requirements: +
    +
  • Must support silent installation flags
  • +
  • Inno Setup: /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
  • +
  • MSI: /qn /norestart
  • +
  • NSIS: /S
  • +
+ If your installer uses different flags, modify the InstallKioskApp scriptblock. +
+ +
+ + +

How-To Guides

+ + +

How to Repair System Files

+

Scenario: A PC has corrupted system files causing crashes or errors.

+ +

Option 1: DISM (Component Store Repair)

+
+ +
# Run DISM repair on a single PC
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task DISM -Credential $cred
+
+

What it does:

+
    +
  • Downloads missing/corrupted files from Windows Update
  • +
  • Repairs the Windows component store
  • +
  • Required before SFC if component store is damaged
  • +
+

Expected output:

+
+
[PROBLEM-PC] DISM: SUCCESS
   Deployment Image Servicing and Management tool
-  The restore operation completed successfully.
-

Option 2: SFC (System File Checker)

-
# Run SFC after DISM
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task SFC -Credential $cred
-

What it does:

-
    -
  • Scans all protected system files
  • -
  • Replaces corrupted files from component store
  • -
  • Creates log at C:\Windows\Logs\CBS\CBS.log
  • -
-

Best Practice - Full Repair Sequence:

-
# Step 1: Run DISM first
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task DISM -Credential $cred
+  The restore operation completed successfully.
+
+ +

Option 2: SFC (System File Checker)

+
+ +
# Run SFC after DISM
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task SFC -Credential $cred
+
+

What it does:

+
    +
  • Scans all protected system files
  • +
  • Replaces corrupted files from component store
  • +
  • Creates log at C:\Windows\Logs\CBS\CBS.log
  • +
+ +
+ Best Practice - Full Repair Sequence: +
+
+ +
# Step 1: Run DISM first
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task DISM -Credential $cred
 
 # Step 2: Run SFC after DISM completes
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task SFC -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task SFC -Credential $cred
 
 # Step 3: Reboot to apply changes
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task Reboot -Credential $cred
-
-

How to Optimize Disks

-

Scenario: PCs are running slow due to disk fragmentation or lack of TRIM.

-

Single PC Optimization

-
.\Invoke-RemoteMaintenance.ps1 -ComputerName "SLOW-PC" -Task OptimizeDisk -Credential $cred
-

What it does:

-
    -
  • Detects drive type (SSD vs HDD)
  • -
  • For SSDs: Runs TRIM to reclaim deleted blocks
  • -
  • For HDDs: Runs defragmentation
  • -
-

Optimize All CMM PCs (After Hours)

-
# CMM PCs often have large files - optimize overnight
-.\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task OptimizeDisk -Credential $cred -ThrottleLimit 3
-

Full Cleanup Sequence

-
# Step 1: Clear update cache
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task ClearUpdateCache -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task Reboot -Credential $cred
+
+ +
+ + +

How to Optimize Disks

+

Scenario: PCs are running slow due to disk fragmentation or lack of TRIM.

+ +

Single PC Optimization

+
+ +
.\Invoke-RemoteMaintenance.ps1 -ComputerName "SLOW-PC" -Task OptimizeDisk -Credential $cred
+
+

What it does:

+
    +
  • Detects drive type (SSD vs HDD)
  • +
  • For SSDs: Runs TRIM to reclaim deleted blocks
  • +
  • For HDDs: Runs defragmentation
  • +
+ +

Optimize All CMM PCs (After Hours)

+
+ +
# CMM PCs often have large files - optimize overnight
+.\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task OptimizeDisk -Credential $cred -ThrottleLimit 3
+
+ +

Full Cleanup Sequence

+
+ +
# Step 1: Clear update cache
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task ClearUpdateCache -Credential $cred
 
 # Step 2: Run disk cleanup
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task DiskCleanup -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task DiskCleanup -Credential $cred
 
 # Step 3: Optimize disk
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task OptimizeDisk -Credential $cred
-
-

How to Fix Stuck Windows Updates

-

Scenario: Windows Update is stuck or failing repeatedly.

-
# Clear the Windows Update cache
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "UPDATE-STUCK-PC" -Task ClearUpdateCache -Credential $cred
-

What it does:

-
    -
  1. Stops Windows Update service
  2. -
  3. Stops BITS service
  4. -
  5. Clears C:\Windows\SoftwareDistribution\Download
  6. -
  7. Restarts services
  8. -
-

After clearing, trigger new update check:

-
# On the target PC (optional follow-up)
-wuauclt /detectnow
-
-

How to Clear Browser Cache

-

Scenario: CMM or inspection PCs have slow browser performance.

-
# Single PC
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "CMM-PC01" -Task ClearBrowserCache -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task OptimizeDisk -Credential $cred
+
+ +
+ + +

How to Fix Stuck Windows Updates

+

Scenario: Windows Update is stuck or failing repeatedly.

+
+ +
# Clear the Windows Update cache
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "UPDATE-STUCK-PC" -Task ClearUpdateCache -Credential $cred
+
+

What it does:

+
    +
  1. Stops Windows Update service
  2. +
  3. Stops BITS service
  4. +
  5. Clears C:\Windows\SoftwareDistribution\Download
  6. +
  7. Restarts services
  8. +
+ +
+ + +

How to Clear Browser Cache

+

Scenario: CMM or inspection PCs have slow browser performance.

+
+ +
# Single PC
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "CMM-PC01" -Task ClearBrowserCache -Credential $cred
 
 # All CMM PCs
-.\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task ClearBrowserCache -Credential $cred
-

What it does:

-
    -
  • Clears Chrome cache directories
  • -
  • Clears Edge cache directories
  • -
  • Does NOT clear saved passwords or bookmarks
  • -
-
-

How to Manage Services

-

Fix Printing Issues

-
# Restart print spooler on a PC with stuck print jobs
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PRINT-PROBLEM-PC" -Task RestartSpooler -Credential $cred
-

What it does:

-
    -
  1. Stops Print Spooler service
  2. -
  3. Clears print queue
  4. -
  5. Restarts Print Spooler service
  6. -
-

Fix DNS Resolution Issues

-
# Flush DNS cache when a PC can't resolve hostnames
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNS-ISSUE-PC" -Task FlushDNS -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task ClearBrowserCache -Credential $cred
+
+

What it does:

+
    +
  • Clears Chrome cache directories
  • +
  • Clears Edge cache directories
  • +
  • Does NOT clear saved passwords or bookmarks
  • +
+ +
+ + +

How to Manage Services

+ +

Fix Printing Issues

+
+ +
# Restart print spooler on a PC with stuck print jobs
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PRINT-PROBLEM-PC" -Task RestartSpooler -Credential $cred
+
+

What it does:

+
    +
  1. Stops Print Spooler service
  2. +
  3. Clears print queue
  4. +
  5. Restarts Print Spooler service
  6. +
+ +

Fix DNS Resolution Issues

+
+ +
# Flush DNS cache when a PC can't resolve hostnames
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNS-ISSUE-PC" -Task FlushDNS -Credential $cred
 
 # Flush DNS on all PCs in a business unit
-.\Invoke-RemoteMaintenance.ps1 -BusinessUnit Blisk -Task FlushDNS -Credential $cred
-

Fix Remote Management Issues

-
# Restart WinRM if subsequent remote commands fail
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "WINRM-ISSUE-PC" -Task RestartWinRM -Credential $cred
-

Note: Connection will briefly drop during restart.

-
-

How to Fix Time Sync Issues

-

Scenario: PC clock is wrong, causing certificate errors or login issues.

-

Set Correct Timezone

-
# Single PC
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "WRONG-TIME-PC" -Task SetTimezone -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -BusinessUnit Blisk -Task FlushDNS -Credential $cred
+
+ +

Fix Remote Management Issues

+
+ +
# Restart WinRM if subsequent remote commands fail
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "WINRM-ISSUE-PC" -Task RestartWinRM -Credential $cred
+
+
+ Note: + Connection will briefly drop during WinRM restart. +
+ +
+ + +

How to Fix Time Sync Issues

+

Scenario: PC clock is wrong, causing certificate errors or login issues.

+ +

Set Correct Timezone

+
+ +
# Single PC
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "WRONG-TIME-PC" -Task SetTimezone -Credential $cred
 
 # All shopfloor PCs
-.\Invoke-RemoteMaintenance.ps1 -All -Task SetTimezone -Credential $cred
-

Sets timezone to: Eastern Standard Time

-

Force Time Synchronization

-
# Sync time with domain controller
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "WRONG-TIME-PC" -Task SyncTime -Credential $cred
-

Full time fix sequence:

-
# Step 1: Set correct timezone
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task SetTimezone -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -All -Task SetTimezone -Credential $cred
+
+

Sets timezone to: Eastern Standard Time

+ +

Force Time Synchronization

+
+ +
# Sync time with domain controller
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "WRONG-TIME-PC" -Task SyncTime -Credential $cred
+
+ +

Full Time Fix Sequence

+
+ +
# Step 1: Set correct timezone
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task SetTimezone -Credential $cred
 
 # Step 2: Sync time
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task SyncTime -Credential $cred
-
-

How to Update DNC Configurations

-

Update eMx Authentication Token

-

Scenario: eMx authentication is failing on DNC PCs.

-
# Single PC
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateEMxAuthToken -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task SyncTime -Credential $cred
+
+ +
+ + +

How to Update DNC Configurations

+ +

Update DNC MX Hosts

+

Scenario: FtpHostPrimary/FtpHostSecondary in DNC\MX registry needs updating (hostname migration).

+
+ +
# Single PC
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateDNCMXHosts -Credential $cred
 
 # All shopfloor PCs
-.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateEMxAuthToken -Credential $cred
-

What it does:

-
    -
  1. Backs up existing eMxInfo.txt with timestamp
  2. -
  3. Copies new token file from network share
  4. -
  5. Verifies file was updated
  6. -
-

Deploy UDC Web Server Configuration

-

Scenario: UDC web server settings need to be updated.

-
# Deploy to PCs with UDC installed
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "UDC-PC01","UDC-PC02" -Task DeployUDCWebServerConfig -Credential $cred
-

What it does:

-
    -
  1. Checks if UDC is installed
  2. -
  3. Backs up existing configuration
  4. -
  5. Deploys new web server settings
  6. -
  7. Does NOT restart UDC (requires manual restart)
  8. -
-
-

How to Deploy Software

-

Install GE Aerospace Dashboard

-

Scenario: Convert a PC to a Dashboard kiosk.

-
# Single PC installation
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "NEWKIOSK-01" -Task InstallDashboard -Credential $cred
+.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateDNCMXHosts -Credential $cred
+
+

What it does:

+
    +
  1. Checks both 32-bit (WOW6432Node) and 64-bit registry paths
  2. +
  3. Only updates values matching the old hostname — skips unexpected values
  4. +
  5. Safe to run on all PCs: no-ops if DNC\MX key doesn't exist
  6. +
+ +

Audit DNC Config vs UDC Backup

+

Scenario: Verify DNC registry settings match UDC backup JSON files.

+
+ +
.\Invoke-RemoteMaintenance.ps1 -All -Task AuditDNCConfig -Credential $cred -LogFile
+
+

What it does:

+
    +
  1. Reads DNC registry values (General, eFocas, Hssb, PPDCS keys)
  2. +
  3. Compares against UDC backup JSON files on the network share
  4. +
  5. Reports MATCH/MISMATCH/MISSING for each field
  6. +
  7. Exports CSV report to logs/
  8. +
+ +

Check Defect Tracker Status

+
+ +
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CheckDefectTracker -Credential $cred
+
+

What it does:

+
    +
  1. Checks if Defect_Tracker.exe is running on each PC
  2. +
  3. Reports machine number + running status
  4. +
  5. Exports CSV report to logs/
  6. +
+ +
+ + +

How to Deploy Files

+

Scenario: Push any file to remote PCs with automatic backup of existing files.

+ +

Basic File Copy

+
+ +
# Copy a config file to a specific destination
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01","PC02" -Task CopyFile `
+    -SourcePath "\\server\share\config.json" `
+    -DestinationPath "C:\ProgramData\App\config.json" `
+    -Credential $cred
+
+

What it does:

+
    +
  1. Copies source file to C:\Windows\Temp\ on remote PC via WinRM
  2. +
  3. Creates backup of existing file (if any) with timestamp
  4. +
  5. Moves temp file to final destination
  6. +
  7. Verifies deployment
  8. +
+ +

File Copy with Post-Copy Command

+

Scenario: Deploy a file and restart a service/app in the logged-in user's session.

+
+ +
# Deploy eMxInfo.txt and kill DNC so it picks up the new file
+.\Invoke-RemoteMaintenance.ps1 -All -Task CopyFile `
+    -SourcePath "\\server\share\eMxInfo.txt" `
+    -DestinationPath "C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt" `
+    -RunCommand "taskkill /IM DNCMain.exe /F" `
+    -Credential $cred
+
+# Deploy UDC config (no restart needed)
+.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CopyFile `
+    -SourcePath "\\server\share\udc_webserver_settings.json" `
+    -DestinationPath "C:\ProgramData\UDC\udc_webserver_settings.json" `
+    -Credential $cred
+
+ +
+ About -RunCommand: + The -RunCommand runs via a one-shot scheduled task as the logged-in user, so it works for user-session processes (same pattern as Dashboard/Lobby Display kiosk relaunch). Use -AsSystem if the command should run as SYSTEM instead. +
+ +
+ + +

How to Import Registry Files

+

Scenario: Apply registry settings from a .reg file to remote PCs.

+ +
+ +
# Import a .reg file on all shopfloor PCs
+.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task ImportReg `
+    -SourcePath "\\server\share\intranet-zone.reg" `
+    -Credential $cred
+
+# Import on specific PCs
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task ImportReg `
+    -SourcePath "C:\Scripts\my-settings.reg" `
+    -Credential $cred
+
+ +

What it does (default — as logged-in user):

+
    +
  1. Copies .reg file to C:\Windows\Temp\ on remote PC via WinRM
  2. +
  3. Creates a one-shot scheduled task as the logged-in user
  4. +
  5. Runs regedit.exe /s to silently import the registry file
  6. +
  7. Cleans up temp file and scheduled task
  8. +
+ +
+ HKCU Support: + Because the import runs as the logged-in user (via scheduled task), both HKLM and HKCU keys in the .reg file are applied correctly. If no user is logged in, it falls back to direct import (HKLM only). +
+ +

HKLM-Only / System Context

+

Use -AsSystem when the .reg file only contains HKLM keys and you want to skip the scheduled task overhead:

+
+ +
# Import HKLM-only registry settings directly as SYSTEM
+.\Invoke-RemoteMaintenance.ps1 -All -Task ImportReg `
+    -SourcePath "\\server\share\machine-policy.reg" `
+    -AsSystem -Credential $cred
+
+ +
+ Warning: + Running with -AsSystem means HKCU keys in the .reg file will NOT apply to any user profile. Only use this for HKLM-only registry files. +
+ +
+ + +

How to Deploy Software

+ +

Install GE Aerospace Dashboard

+

Scenario: Convert a PC to a Dashboard kiosk.

+
+ +
# Single PC installation
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "NEWKIOSK-01" -Task InstallDashboard -Credential $cred
 
 # Multiple PCs from a list
-$kiosks = @("KIOSK-01", "KIOSK-02", "KIOSK-03")
-.\Invoke-RemoteMaintenance.ps1 -ComputerName $kiosks -Task InstallDashboard -Credential $cred
-

What it does:

-
    -
  1. Copies installer from network share
  2. -
  3. Runs silent installation
  4. -
  5. Configures auto-start
  6. -
  7. Cleans up installer
  8. -
-

After installation:

-
    -
  • Run data collection to update PC type
  • -
  • Reboot PC to complete setup
  • -
-
# Complete Dashboard deployment sequence
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task InstallDashboard -Credential $cred
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task Reboot -Credential $cred
+$kiosks = @("KIOSK-01", "KIOSK-02", "KIOSK-03")
+.\Invoke-RemoteMaintenance.ps1 -ComputerName $kiosks -Task InstallDashboard -Credential $cred
+
+

What it does:

+
    +
  1. Pings target PC (skips if offline)
  2. +
  3. Copies installer from network share to C:\Windows\Temp\
  4. +
  5. Kills running Edge kiosk
  6. +
  7. Runs silent installation (120-second timeout)
  8. +
  9. Relaunches Edge kiosk via scheduled task as the logged-in user
  10. +
  11. Cleans up installer and scheduled task
  12. +
-# After reboot, update ShopDB -.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "KIOSK-01" -Credential $cred
-

Install Lobby Display

-
.\Invoke-RemoteMaintenance.ps1 -ComputerName "LOBBY-01" -Task InstallLobbyDisplay -Credential $cred
-

Uninstall Dashboard or Lobby Display

-
# Remove Dashboard
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "OLD-KIOSK" -Task UninstallDashboard -Credential $cred
+            
+ No reboot required + Edge relaunches automatically in the logged-in user's session. +
+ +
+ +
# Deploy to all Dashboard kiosks
+.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task InstallDashboard -Credential $cred
+
+# Deploy to all Lobby Display kiosks
+.\Invoke-RemoteMaintenance.ps1 -PcType "Lobby Display" -Task InstallLobbyDisplay -Credential $cred
+
+ +

Uninstall Dashboard or Lobby Display

+
+ +
# Remove Dashboard
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "OLD-KIOSK" -Task UninstallDashboard -Credential $cred
 
 # Remove Lobby Display
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "OLD-LOBBY" -Task UninstallLobbyDisplay -Credential $cred
-
-

How to Reboot PCs

-

Single PC Reboot

-
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC-TO-REBOOT" -Task Reboot -Credential $cred
-

Note: Reboot has a 30-second delay to allow graceful shutdown.

-

Reboot All Dashboard PCs

-
# Reboot all Dashboard PCs (e.g., for software update)
-.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task Reboot -Credential $cred
-

Reboot All Lobby Display PCs

-
.\Invoke-RemoteMaintenance.ps1 -PcType "Lobby Display" -Task Reboot -Credential $cred
-

Reboot PCs by Business Unit

-
# Reboot all HPT PCs during maintenance window
-.\Invoke-RemoteMaintenance.ps1 -BusinessUnit HPT -Task Reboot -Credential $cred
-
-

How to Run Batch Operations

-

Using a Computer List File

-

Create a text file with one hostname per line:

-
# shopfloor-pcs.txt
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "OLD-LOBBY" -Task UninstallLobbyDisplay -Credential $cred
+
+ +
+ + +

How to Reboot PCs

+ +

Single PC Reboot

+
+ +
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC-TO-REBOOT" -Task Reboot -Credential $cred
+
+
+ Note: + Reboot has a 30-second delay to allow graceful shutdown. +
+ +

Reboot by PC Type

+
+ +
# Reboot all Dashboard PCs
+.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task Reboot -Credential $cred
+
+# Reboot all Lobby Display PCs
+.\Invoke-RemoteMaintenance.ps1 -PcType "Lobby Display" -Task Reboot -Credential $cred
+
+ +

Reboot by Business Unit

+
+ +
# Reboot all HPT PCs during maintenance window
+.\Invoke-RemoteMaintenance.ps1 -BusinessUnit HPT -Task Reboot -Credential $cred
+
+ +
+ + +

How to Run Batch Operations

+ +

Using a Computer List File

+

Create a text file with one hostname per line:

+
+
# shopfloor-pcs.txt
 PC001
 PC002
 PC003
 PC004
-PC005
-

Run tasks against the list:

-
.\Invoke-RemoteMaintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task FlushDNS -Credential $cred
-

Running Multiple Tasks in Sequence

-
# Maintenance routine for a PC
-$pc = "SHOPFLOOR-PC01"
+PC005
+
+ +

Run tasks against the list:

+
+ +
.\Invoke-RemoteMaintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task FlushDNS -Credential $cred
+
+ +

Running Multiple Tasks in Sequence

+
+ +
# Maintenance routine for a PC
+$pc = "SHOPFLOOR-PC01"
 
 # Step 1: Clear caches
 .\Invoke-RemoteMaintenance.ps1 -ComputerName $pc -Task ClearUpdateCache -Credential $cred
@@ -920,105 +1511,231 @@ $pc = "SHOPFLOOR-PC01"
 .\Invoke-RemoteMaintenance.ps1 -ComputerName $pc -Task SyncTime -Credential $cred
 
 # Step 5: Reboot
-.\Invoke-RemoteMaintenance.ps1 -ComputerName $pc -Task Reboot -Credential $cred
-
-

Targeting Strategies

-

By Individual PCs

-

Best for: Specific troubleshooting, targeted fixes

-
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task DISM -Credential $cred
-

By PC Type

-

Best for: Type-specific maintenance, software updates

-
# All CMM PCs
+.\Invoke-RemoteMaintenance.ps1 -ComputerName $pc -Task Reboot -Credential $cred
+
+ +
+ + +

Targeting Strategies

+ +

By Individual PCs

+

Best for: Specific troubleshooting, targeted fixes

+
+ +
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PROBLEM-PC" -Task DISM -Credential $cred
+
+ +

By PC Type

+

Best for: Type-specific maintenance, software updates

+
+ +
# All CMM PCs
 .\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task DiskCleanup -Credential $cred
 
 # All Dashboard kiosks
-.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task Reboot -Credential $cred
-

By Business Unit

-

Best for: Department-specific maintenance windows

-
# All Blisk area PCs
-.\Invoke-RemoteMaintenance.ps1 -BusinessUnit Blisk -Task SyncTime -Credential $cred
-

All Shopfloor PCs

-

Best for: Global maintenance, security updates

-
# Flush DNS everywhere
-.\Invoke-RemoteMaintenance.ps1 -All -Task FlushDNS -Credential $cred -ThrottleLimit 10
-

Using a List File

-

Best for: Custom groups, staged rollouts

-
.\Invoke-RemoteMaintenance.ps1 -ComputerListFile ".\phase1-pcs.txt" -Task DISM -Credential $cred
-
-

Troubleshooting

-

Task Times Out

-

Cause: Task takes longer than session timeout.

-

Solution: DISM and SFC can take a long time. Check if task completed on target:

-
# Check DISM log
-Invoke-Command -ComputerName "PC01" -Credential $cred -ScriptBlock {
-    Get-Content "C:\Windows\Logs\DISM\dism.log" -Tail 50
-}
-

"Access Denied" on Some PCs

-

Cause: Credentials don't have admin rights on that PC.

-

Solutions:

-
    -
  1. Use different credentials
  2. -
  3. Add account to local Administrators group on target
  4. -
  5. Check if UAC is blocking remote admin
  6. -
-

Software Installation Fails

-

Cause: Network share not accessible or installer missing.

-

Solutions:

-
    -
  1. Verify network share path is accessible
  2. -
  3. Check installer exists at expected location
  4. -
  5. Verify credentials can access the share
  6. -
-

Reboot Doesn't Happen

-

Cause: User cancelled shutdown or application blocked it.

-

Solutions:

-
# Force immediate reboot (no 30-second delay)
-Invoke-Command -ComputerName "PC01" -Credential $cred -ScriptBlock {
+.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task Reboot -Credential $cred
+
+ +

By Business Unit

+

Best for: Department-specific maintenance windows

+
+ +
# All Blisk area PCs
+.\Invoke-RemoteMaintenance.ps1 -BusinessUnit Blisk -Task SyncTime -Credential $cred
+
+ +

All Shopfloor PCs

+

Best for: Global maintenance, security updates

+
+ +
# Flush DNS everywhere
+.\Invoke-RemoteMaintenance.ps1 -All -Task FlushDNS -Credential $cred -ThrottleLimit 10
+
+ +

Using a List File

+

Best for: Custom groups, staged rollouts

+
+ +
.\Invoke-RemoteMaintenance.ps1 -ComputerListFile ".\phase1-pcs.txt" -Task DISM -Credential $cred
+
+ +
+ + +

Troubleshooting

+ +
+

Issue: Task Times Out

+

Cause: Task takes longer than session timeout.

+

Resolution: DISM and SFC can take a long time. Check if task completed on target:

+
+ +
# Check DISM log
+Invoke-Command -ComputerName "PC01" -Credential $cred -ScriptBlock {
+    Get-Content "C:\Windows\Logs\DISM\dism.log" -Tail 50
+}
+
+
+ +
+

Issue: "Access Denied" on Some PCs

+

Cause: Credentials don't have admin rights on that PC.

+

Resolution:

+
    +
  • Use different credentials
  • +
  • Add account to local Administrators group on target
  • +
  • Check if UAC is blocking remote admin
  • +
+
+ +
+

Issue: Software Installation Fails

+

Cause: Network share not accessible or installer missing.

+

Resolution:

+
    +
  • Verify network share path is accessible
  • +
  • Check installer exists at expected location
  • +
  • Verify credentials can access the share
  • +
+
+ +
+

Issue: Reboot Doesn't Happen

+

Cause: User cancelled shutdown or application blocked it.

+

Resolution:

+
+ +
# Force immediate reboot (no 30-second delay)
+Invoke-Command -ComputerName "PC01" -Credential $cred -ScriptBlock {
     Restart-Computer -Force
-}
-
-

Best Practices

-

1. Start Small

-

Test on one PC before running against groups:

-
# Test on single PC first
-.\Invoke-RemoteMaintenance.ps1 -ComputerName "TEST-PC" -Task DISM -Credential $cred
-

2. Use Appropriate Throttle Limits

- - - - - - - - - - - - - - - - - - - - - - -
ScenarioRecommended ThrottleLimit
Fast network, light tasks10-25
Normal operations5 (default)
Heavy tasks (DISM, Defrag)2-3
Slow network2-3
-

3. Schedule Disruptive Tasks

-

Run reboots and heavy tasks during maintenance windows:

-
    -
  • DISM/SFC: After hours
  • -
  • Disk optimization: After hours
  • -
  • Reboots: During shift changes or maintenance windows
  • -
-

4. Verify Before Rebooting

-

Always confirm which PCs will be affected:

-
# Check PC type before reboot
-.\Update-ShopfloorPCs-Remote.ps1 -PcType Dashboard -WhatIf
-

5. Keep Logs

-

Redirect output for audit trail:

-
.\Invoke-RemoteMaintenance.ps1 -All -Task SyncTime -Credential $cred | Tee-Object -FilePath "maintenance-log-$(Get-Date -Format 'yyyyMMdd').txt"
+}
+ + + +
+

Issue: ImportReg HKCU Keys Not Applied

+

Cause: No user is logged in on the target PC, or -AsSystem was used.

+

Resolution:

+
    +
  • Ensure a user is logged in on the target PC
  • +
  • Remove -AsSystem flag if the .reg file contains HKCU keys
  • +
  • Check the script output for "No logged-in user found" messages
  • +
+
+ +
+ + +

Best Practices

+ +

1. Start Small

+

Test on one PC before running against groups:

+
+ +
# Test on single PC first
+.\Invoke-RemoteMaintenance.ps1 -ComputerName "TEST-PC" -Task DISM -Credential $cred
+
+ +

2. Use Appropriate Throttle Limits

+ + + + + + + + +
ScenarioRecommended ThrottleLimit
Fast network, light tasks10-25
Normal operations5 (default)
Heavy tasks (DISM, Defrag)2-3
Slow network2-3
+ +

3. Schedule Disruptive Tasks

+

Run reboots and heavy tasks during maintenance windows:

+ + +

4. Verify Before Rebooting

+

Always confirm which PCs will be affected:

+
+ +
# Check PC type before reboot
+.\Update-ShopfloorPCs-Remote.ps1 -PcType Dashboard -WhatIf
+
+ +

5. Keep Logs

+

Use the -LogFile flag or redirect output for audit trail:

+
+ +
# Built-in logging
+.\Invoke-RemoteMaintenance.ps1 -All -Task SyncTime -Credential $cred -LogFile
+
+# Or redirect output manually
+.\Invoke-RemoteMaintenance.ps1 -All -Task SyncTime -Credential $cred | Tee-Object -FilePath "maintenance-log-$(Get-Date -Format 'yyyyMMdd').txt"
+
+ + +
+ Reference Complete + This document covers all 22 maintenance tasks available in Invoke-RemoteMaintenance.ps1. For questions or updates, contact the GE Aerospace DT Team. +
+ + + + + + + + + - \ No newline at end of file + diff --git a/docs/Invoke-RemoteMaintenance.md b/docs/Invoke-RemoteMaintenance.md index 8c7bd00..32184bb 100644 --- a/docs/Invoke-RemoteMaintenance.md +++ b/docs/Invoke-RemoteMaintenance.md @@ -5,6 +5,7 @@ Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via ## Table of Contents - [Overview](#overview) +- [First-Time Setup](#first-time-setup) - [API Integration](#api-integration) - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) @@ -17,6 +18,8 @@ Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via - [Service Management](#how-to-manage-services) - [Time Synchronization](#how-to-fix-time-sync-issues) - [DNC Configuration](#how-to-update-dnc-configurations) + - [File Deployment](#how-to-deploy-files) + - [Registry Import](#how-to-import-registry-files) - [Software Deployment](#how-to-deploy-software) - [Batch Operations](#how-to-run-batch-operations) - [Targeting Strategies](#targeting-strategies) @@ -32,13 +35,53 @@ This script provides a comprehensive remote maintenance toolkit for managing sho **Location:** `S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1` **Key Features:** -- 19 maintenance tasks available +- 22 maintenance tasks available +- General-purpose file deployment (`CopyFile`) and registry import (`ImportReg`) tasks +- Registry imports run as logged-in user via scheduled task (HKCU support) +- Optional post-copy commands via scheduled task (service/app restarts) - Multiple targeting options (by name, type, business unit, or all) - Concurrent execution with configurable throttling - Integration with ShopDB for PC discovery --- +## First-Time Setup + +Before running this script for the first time, you must allow PowerShell script execution and unblock the script file. + +### Step 1: Allow PowerShell Script Execution + +Open PowerShell **as Administrator** and run: + +```powershell +Set-ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +When prompted, type `Y` and press Enter. This allows locally-created scripts to run and requires downloaded scripts to be signed or unblocked. + +> **Note:** You only need to do this once per user account. `RemoteSigned` is the recommended policy — it allows local scripts while still protecting against untrusted downloads. + +### Step 2: Unblock the Script File + +Files downloaded from a network share or the internet are marked as "blocked" by Windows. You must unblock the script before it can run. + +**Option A — File Explorer (GUI):** + +1. Navigate to `S:\dt\shopfloor\scripts\remote-execution\` +2. Right-click `Invoke-RemoteMaintenance.ps1` and select **Properties** +3. At the bottom of the General tab, check the **Unblock** checkbox +4. Click **Apply**, then **OK** + +**Option B — PowerShell:** + +```powershell +Unblock-File -Path "S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1" +``` + +> **Important:** If you skip this step, you will get a security error when trying to run the script: *"cannot be loaded because running scripts is disabled on this system"* or *"cannot be loaded. The file is not digitally signed."* + +--- + ## API Integration When using `-All`, `-PcType`, or `-BusinessUnit` targeting, the script retrieves PC lists from the ShopDB API: @@ -53,12 +96,12 @@ GET /api.asp?action=getShopfloorPCs&businessunitid=1 # Specific business unit | ID | Type | ID | Type | |----|------|----|------| -| 1 | Shopfloor | 7 | Heat Treat | -| 2 | CMM | 8 | Engineer | -| 3 | Wax Trace | 9 | Standard | -| 4 | Keyence | 10 | Inspection | -| 5 | EAS1000 | 11 | Dashboard | -| 6 | Genspect | 12 | Lobby Display | +| 1 | Standard | 7 | Keyence | +| 2 | Engineer | 8 | Genspect | +| 3 | Shopfloor | 9 | Heat Treat | +| 4 | Uncategorized | 10 | Inspection | +| 5 | CMM | 11 | Dashboard | +| 6 | Wax / Trace | 12 | Lobby Display | **See:** [ShopDB API Reference](ShopDB-API.html) for complete API documentation. @@ -133,6 +176,15 @@ The script outputs status for each PC: |-----------|------|-------------| | `-Task` | string | Maintenance task to execute | +### File Deployment Parameters (CopyFile / ImportReg) + +| Parameter | Type | Description | +|-----------|------|-------------| +| `-SourcePath` | string | Source file path (local or UNC). Required for CopyFile and ImportReg | +| `-DestinationPath` | string | Destination file path on remote PCs. Required for CopyFile | +| `-RunCommand` | string | Command to run after CopyFile. Runs as logged-in user by default (via scheduled task) | +| `-AsSystem` | switch | Run ImportReg and -RunCommand in the WinRM session (SYSTEM context) instead of as logged-in user | + ### Optional Parameters | Parameter | Type | Default | Description | @@ -140,7 +192,8 @@ The script outputs status for each PC: | `-Credential` | PSCredential | Prompt | Remote authentication | | `-ApiUrl` | string | Production | ShopDB API endpoint | | `-ThrottleLimit` | int | 5 | Max concurrent sessions | -| `-DnsSuffix` | string | logon.ds.ge.com | DNS suffix for resolution | +| `-DnsSuffix` | string | logon.ds.ge.com | DNS suffix for FQDN resolution | +| `-LogFile` | switch | Off | Enable transcript logging to `logs/` directory | ### PC Types @@ -194,13 +247,22 @@ TBD, Blisk, HPT, Spools, Inspection, Venture, Turn/Burn, DT | Task | Description | Duration | Impact | |------|-------------|----------|--------| -| `UpdateEMxAuthToken` | Update eMx auth from share | 1-2 min | None | -| `DeployUDCWebServerConfig` | Deploy UDC config | 1-2 min | None | +| `UpdateDNCMXHosts` | Update FtpHostPrimary/Secondary in DNC\MX registry | <1 min | None | +| `AuditDNCConfig` | Compare DNC registry vs UDC backup JSON, export CSV | 1-5 min | None | +| `CheckDefectTracker` | Check if Defect_Tracker.exe is running, export CSV | 1-5 min | None | + +### File Deployment Tasks + +| Task | Description | Duration | Impact | +|------|-------------|----------|--------| +| `CopyFile` | Copy file from `-SourcePath` to `-DestinationPath` on remote PCs | 1-2 min | Low | +| `ImportReg` | Copy `.reg` file and import via scheduled task as logged-in user | 1-2 min | Low | ### System Tasks | Task | Description | Duration | Impact | |------|-------------|----------|--------| +| `GPUpdate` | Force Group Policy refresh (`gpupdate /force`) | <1 min | Low | | `Reboot` | Restart PC (30s delay) | 2-5 min | High | ### Software Deployment Tasks @@ -222,18 +284,19 @@ Deployment tasks require source files to be available before execution: | Task | Source File Path | |------|------------------| -| `InstallDashboard` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe` | -| `InstallLobbyDisplay` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe` | -| `UpdateEMxAuthToken` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt` | -| `DeployUDCWebServerConfig` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\UDC\udc_webserver_settings.json` | +| `InstallDashboard` | `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe` | +| `InstallLobbyDisplay` | `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe` | +| `CopyFile` | Any file - specified via `-SourcePath` parameter | +| `ImportReg` | Any `.reg` file - specified via `-SourcePath` parameter | ### How Deployment Works 1. **Pre-flight Check:** Script verifies source file exists 2. **WinRM Session:** Opens remote session to target PC 3. **File Push:** Copies source file to `C:\Windows\Temp\` on remote PC -4. **Execution:** Runs install/copy task using pushed file -5. **Cleanup:** Removes temp file from remote PC +4. **Execution:** Runs install/copy/import task using pushed file +5. **Post-action:** Optionally runs command as logged-in user via scheduled task +6. **Cleanup:** Removes temp file from remote PC ``` +---------------------+ WinRM +---------------------+ @@ -242,50 +305,44 @@ Deployment tasks require source files to be available before execution: | Source Files: | Push File | Temp Location: | | - Setup.exe | ------------> | C:\Windows\Temp | | - config.json | | | -| - eMxInfo.txt | Execute | Final Location: | -| (network) | ------------> | C:\Program Files | +| - settings.reg | Execute | Final Location: | +| (any path/UNC) | ------------> | -DestinationPath | +| | | | +| | Sched. Task | Logged-in User: | +| | ------------> | - regedit /s | +| | | - RunCommand | +---------------------+ +---------------------+ ``` -### Directory Structure +### CopyFile Details -Ensure your script directory contains the required files: +The `CopyFile` task: -``` -S:\dt\shopfloor\scripts\remote-execution\ -├── Invoke-RemoteMaintenance.ps1 -├── GEAerospaceDashboardSetup.exe # For InstallDashboard -├── GEAerospaceLobbyDisplaySetup.exe # For InstallLobbyDisplay -└── udc_webserver_settings.json # For DeployUDCWebServerConfig -``` +1. **Source:** Any file specified via `-SourcePath` (local or UNC path) +2. **Destination:** Specified via `-DestinationPath` +3. **Backup:** Existing file is backed up as `-old-.` +4. **Post-action:** If `-RunCommand` is specified, runs as the logged-in user via a one-shot scheduled task (same pattern as Dashboard/Lobby kiosk relaunch) -### eMx Auth Token Details +### ImportReg Details -The `UpdateEMxAuthToken` task: +The `ImportReg` task: -1. **Source:** `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt` -2. **Destinations:** (both paths if they exist) - - `C:\Program Files\GE Aircraft Engines\DNC\eMxInfo.txt` - - `C:\Program Files (x86)\GE Aircraft Engines\DNC\eMxInfo.txt` -3. **Backup:** Creates `eMxInfo-old-YYYYMMDD-HHMMSS.txt` before overwriting -4. **Post-action:** Restarts DNC service (`LDnc.exe`) - -### UDC Web Server Config Details - -The `DeployUDCWebServerConfig` task: - -1. **Pre-check:** Verifies UDC is installed (`C:\Program Files\UDC` exists) -2. **Skip:** PCs without UDC are skipped (not counted as failures) -3. **Destination:** `C:\ProgramData\UDC\udc_webserver_settings.json` -4. **Backup:** Creates backup before overwriting +1. **Source:** `.reg` file specified via `-SourcePath` +2. **Import method:** `regedit.exe /s` via one-shot scheduled task as logged-in user +3. **HKCU support:** Runs as the logged-in user, so both HKLM and HKCU keys apply correctly +4. **Fallback:** If no user is logged in, runs `regedit.exe /s` directly (HKLM only) +5. **Cleanup:** Temp `.reg` file removed after import ### Dashboard/Lobby Display Install Details Both kiosk app installers: 1. **Installer type:** Inno Setup (supports `/VERYSILENT`) -2. **Execution:** Silent install with no user prompts -3. **Cleanup:** Installer removed from temp after execution +2. **Pre-install:** Kills running Edge kiosk via `PrepareToInstall` in Inno Setup +3. **Execution:** Silent install with no user prompts (120-second timeout) +4. **Post-install:** Creates a one-shot scheduled task to relaunch Edge in kiosk mode as the logged-in user (e.g. `lg044513sd`), then auto-deletes the task +5. **Cleanup:** Installer removed from temp after execution +6. **Connectivity:** Offline PCs are skipped with a ping check before connecting **Uninstall GUIDs:** - Dashboard: `{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}` @@ -295,7 +352,7 @@ Both kiosk app installers: To add a new application for deployment, edit the script in two places: -**Step 1: Add to `$KioskAppConfig` hashtable (~line 1388)** +**Step 1: Add to `$KioskAppConfig` hashtable** ```powershell $KioskAppConfig = @{ @@ -304,10 +361,11 @@ $KioskAppConfig = @{ # Add new application 'InstallNewApp' = @{ Action = 'Install' - InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe' + InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe' InstallerName = 'NewAppSetup.exe' AppName = 'New Application Name' UninstallGuid = '{YOUR-GUID-HERE}' # Find in registry after manual install + KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/your-page/' # Optional: relaunch Edge kiosk after install } 'UninstallNewApp' = @{ Action = 'Uninstall' @@ -334,7 +392,7 @@ $KioskAppConfig = @{ **Step 3: Place installer on network share** ``` -\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe +\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe ``` **Finding the Uninstall GUID:** @@ -562,37 +620,129 @@ wuauclt /detectnow ### How to Update DNC Configurations -#### Update eMx Authentication Token +#### Update DNC MX Hosts -**Scenario:** eMx authentication is failing on DNC PCs. +**Scenario:** FtpHostPrimary/FtpHostSecondary in DNC\MX registry needs updating (hostname migration). ```powershell # Single PC -.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateEMxAuthToken -Credential $cred +.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateDNCMXHosts -Credential $cred # All shopfloor PCs -.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateEMxAuthToken -Credential $cred +.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateDNCMXHosts -Credential $cred ``` **What it does:** -1. Backs up existing `eMxInfo.txt` with timestamp -2. Copies new token file from network share -3. Verifies file was updated +1. Checks both 32-bit (WOW6432Node) and 64-bit registry paths +2. Only updates values matching the old hostname - skips unexpected values +3. Safe to run on all PCs: no-ops if DNC\MX key doesn't exist -#### Deploy UDC Web Server Configuration +#### Audit DNC Config vs UDC Backup -**Scenario:** UDC web server settings need to be updated. +**Scenario:** Verify DNC registry settings match UDC backup JSON files. ```powershell -# Deploy to PCs with UDC installed -.\Invoke-RemoteMaintenance.ps1 -ComputerName "UDC-PC01","UDC-PC02" -Task DeployUDCWebServerConfig -Credential $cred +.\Invoke-RemoteMaintenance.ps1 -All -Task AuditDNCConfig -Credential $cred -LogFile ``` **What it does:** -1. Checks if UDC is installed -2. Backs up existing configuration -3. Deploys new web server settings -4. Does NOT restart UDC (requires manual restart) +1. Reads DNC registry values (General, eFocas, Hssb, PPDCS keys) +2. Compares against UDC backup JSON files on the network share +3. Reports MATCH/MISMATCH/MISSING for each field +4. Exports CSV report to `logs/` + +#### Check Defect Tracker Status + +```powershell +.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CheckDefectTracker -Credential $cred +``` + +**What it does:** +1. Checks if `Defect_Tracker.exe` is running on each PC +2. Reports machine number + running status +3. Exports CSV report to `logs/` + +--- + +### How to Deploy Files + +**Scenario:** Push any file to remote PCs with automatic backup of existing files. + +#### Basic File Copy + +```powershell +# Copy a config file to a specific destination +.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01","PC02" -Task CopyFile ` + -SourcePath "\\server\share\config.json" ` + -DestinationPath "C:\ProgramData\App\config.json" ` + -Credential $cred +``` + +**What it does:** +1. Copies source file to `C:\Windows\Temp\` on remote PC via WinRM +2. Creates backup of existing file (if any) with timestamp +3. Moves temp file to final destination +4. Verifies deployment + +#### File Copy with Post-Copy Command + +**Scenario:** Deploy a file and restart a service/app in the logged-in user's session. + +```powershell +# Deploy eMxInfo.txt and kill DNC so it picks up the new file +.\Invoke-RemoteMaintenance.ps1 -All -Task CopyFile ` + -SourcePath "\\server\share\eMxInfo.txt" ` + -DestinationPath "C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt" ` + -RunCommand "taskkill /IM DNCMain.exe /F" ` + -Credential $cred + +# Deploy UDC config (no restart needed) +.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CopyFile ` + -SourcePath "\\server\share\udc_webserver_settings.json" ` + -DestinationPath "C:\ProgramData\UDC\udc_webserver_settings.json" ` + -Credential $cred +``` + +The `-RunCommand` runs via a one-shot scheduled task as the logged-in user, so it works for user-session processes (same pattern as Dashboard/Lobby Display kiosk relaunch). + +--- + +### How to Import Registry Files + +**Scenario:** Apply registry settings from a `.reg` file to remote PCs. + +```powershell +# Import a .reg file on all shopfloor PCs +.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task ImportReg ` + -SourcePath "\\server\share\intranet-zone.reg" ` + -Credential $cred + +# Import on specific PCs +.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task ImportReg ` + -SourcePath "C:\Scripts\my-settings.reg" ` + -Credential $cred +``` + +**What it does (default — as logged-in user):** +1. Copies `.reg` file to `C:\Windows\Temp\` on remote PC via WinRM +2. Creates a one-shot scheduled task as the logged-in user +3. Runs `regedit.exe /s` to silently import the registry file +4. Cleans up temp file and scheduled task + +**HKCU support:** Because the import runs as the logged-in user (via scheduled task), both `HKLM` and `HKCU` keys in the `.reg` file are applied correctly. If no user is logged in, it falls back to direct import (HKLM only). + +#### HKLM-Only / System Context + +Use `-AsSystem` when the `.reg` file only contains `HKLM` keys and you want to skip the scheduled task overhead: + +```powershell +# Import HKLM-only registry settings directly as SYSTEM +.\Invoke-RemoteMaintenance.ps1 -All -Task ImportReg ` + -SourcePath "\\server\share\machine-policy.reg" ` + -AsSystem -Credential $cred +``` + +This runs `regedit.exe /s` directly in the WinRM session (SYSTEM context). Faster, but HKCU keys will not apply to any user. --- @@ -612,22 +762,21 @@ $kiosks = @("KIOSK-01", "KIOSK-02", "KIOSK-03") ``` **What it does:** -1. Copies installer from network share -2. Runs silent installation -3. Configures auto-start -4. Cleans up installer +1. Pings target PC (skips if offline) +2. Copies installer from network share to `C:\Windows\Temp\` +3. Kills running Edge kiosk +4. Runs silent installation (120-second timeout) +5. Relaunches Edge kiosk via scheduled task as the logged-in user +6. Cleans up installer and scheduled task -**After installation:** -- Run data collection to update PC type -- Reboot PC to complete setup +**No reboot required** — Edge relaunches automatically in the logged-in user's session. ```powershell -# Complete Dashboard deployment sequence -.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task InstallDashboard -Credential $cred -.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task Reboot -Credential $cred +# Deploy to all Dashboard kiosks +.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task InstallDashboard -Credential $cred -# After reboot, update ShopDB -.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "KIOSK-01" -Credential $cred +# Deploy to all Lobby Display kiosks +.\Invoke-RemoteMaintenance.ps1 -PcType "Lobby Display" -Task InstallLobbyDisplay -Credential $cred ``` #### Install Lobby Display diff --git a/docs/SCRIPTS_REFERENCE.md b/docs/SCRIPTS_REFERENCE.md index 3bfa10f..ded7977 100644 --- a/docs/SCRIPTS_REFERENCE.md +++ b/docs/SCRIPTS_REFERENCE.md @@ -23,7 +23,7 @@ powershell-scripts/ ## Table of Contents 1. [Asset Collection Scripts](#asset-collection-scripts) (`asset-collection/`) -2. [Remote Execution Scripts](#remote-execution-scripts) (`remote-execution/`) +2. [Remote Execution Scripts](#remote-execution-scripts) (`S:\dt\shopfloor\scripts\remote-execution\`) 3. [Setup & Utility Scripts](#setup--utility-scripts) (`setup-utilities/`) 4. [Registry Backup Scripts](#registry-backup-scripts) (`registry-backup/`) 5. [WinRM HTTPS Scripts](#winrm-https-scripts) (`winrm-https/`) diff --git a/docs/convert_to_docx.py b/docs/convert_to_docx.py new file mode 100644 index 0000000..8c9f88f --- /dev/null +++ b/docs/convert_to_docx.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +""" +Convert Markdown documentation to Word documents (.docx) +With proper code block formatting (shaded boxes) +""" + +import re +import os +from docx import Document +from docx.shared import Inches, Pt, RGBColor, Twips +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.style import WD_STYLE_TYPE +from docx.enum.table import WD_TABLE_ALIGNMENT +from docx.oxml.ns import qn, nsmap +from docx.oxml import OxmlElement + +def set_cell_shading(cell, color="E8E8E8"): + """Set cell background shading color.""" + tc = cell._tc + tcPr = tc.get_or_add_tcPr() + shd = OxmlElement('w:shd') + shd.set(qn('w:fill'), color) + shd.set(qn('w:val'), 'clear') + tcPr.append(shd) + +def set_cell_borders(cell, color="CCCCCC"): + """Set cell border color.""" + tc = cell._tc + tcPr = tc.get_or_add_tcPr() + tcBorders = OxmlElement('w:tcBorders') + for border_name in ['top', 'left', 'bottom', 'right']: + border = OxmlElement(f'w:{border_name}') + border.set(qn('w:val'), 'single') + border.set(qn('w:sz'), '4') + border.set(qn('w:color'), color) + tcBorders.append(border) + tcPr.append(tcBorders) + +def add_code_block(doc, code_text, language=""): + """Add a formatted code block with shading.""" + # Create a single-cell table for the code block + table = doc.add_table(rows=1, cols=1) + table.autofit = True + + cell = table.rows[0].cells[0] + + # Set cell shading (light gray background) + set_cell_shading(cell, "F5F5F5") + set_cell_borders(cell, "DDDDDD") + + # Clear default paragraph and add code + cell.paragraphs[0].clear() + + # Add each line of code + lines = code_text.split('\n') + for i, line in enumerate(lines): + if i == 0: + para = cell.paragraphs[0] + else: + para = cell.add_paragraph() + + para.paragraph_format.space_before = Pt(0) + para.paragraph_format.space_after = Pt(0) + para.paragraph_format.line_spacing = 1.0 + + run = para.add_run(line if line else ' ') # Use space for empty lines + run.font.name = 'Consolas' + run.font.size = Pt(9) + run.font.color.rgb = RGBColor(0, 0, 0) + + # Add spacing after the code block + doc.add_paragraph() + +def parse_markdown(md_content): + """Parse markdown content into structured elements.""" + lines = md_content.split('\n') + elements = [] + i = 0 + + while i < len(lines): + line = lines[i] + + # Skip empty lines + if not line.strip(): + i += 1 + continue + + # Headers + if line.startswith('# '): + elements.append(('h1', line[2:].strip())) + i += 1 + elif line.startswith('## '): + elements.append(('h2', line[3:].strip())) + i += 1 + elif line.startswith('### '): + elements.append(('h3', line[4:].strip())) + i += 1 + elif line.startswith('#### '): + elements.append(('h4', line[5:].strip())) + i += 1 + + # Horizontal rule + elif line.strip() == '---': + elements.append(('hr', '')) + i += 1 + + # Code blocks + elif line.strip().startswith('```'): + code_lang = line.strip()[3:] + code_lines = [] + i += 1 + while i < len(lines) and not lines[i].strip().startswith('```'): + code_lines.append(lines[i]) + i += 1 + # Store language info with code + elements.append(('code', (code_lang, '\n'.join(code_lines)))) + i += 1 # Skip closing ``` + + # Tables + elif '|' in line and i + 1 < len(lines) and '---' in lines[i + 1]: + table_lines = [line] + i += 1 + while i < len(lines) and '|' in lines[i]: + table_lines.append(lines[i]) + i += 1 + elements.append(('table', table_lines)) + + # Bullet lists + elif line.strip().startswith('- ') or line.strip().startswith('* '): + list_items = [] + while i < len(lines) and (lines[i].strip().startswith('- ') or lines[i].strip().startswith('* ') or (lines[i].startswith(' ') and lines[i].strip())): + if lines[i].strip().startswith('- ') or lines[i].strip().startswith('* '): + list_items.append(lines[i].strip()[2:]) + elif lines[i].startswith(' ') and list_items: + list_items[-1] += ' ' + lines[i].strip() + i += 1 + elements.append(('bullet', list_items)) + + # Numbered lists + elif re.match(r'^\d+\.\s', line.strip()): + list_items = [] + while i < len(lines) and (re.match(r'^\d+\.\s', lines[i].strip()) or lines[i].startswith(' ')): + if re.match(r'^\d+\.\s', lines[i].strip()): + list_items.append(re.sub(r'^\d+\.\s', '', lines[i].strip())) + elif lines[i].startswith(' ') and list_items: + list_items[-1] += ' ' + lines[i].strip() + i += 1 + elements.append(('numbered', list_items)) + + # Regular paragraph + else: + para_lines = [line] + i += 1 + while i < len(lines) and lines[i].strip() and not lines[i].startswith('#') and not lines[i].startswith('```') and not lines[i].startswith('- ') and not lines[i].startswith('* ') and '|' not in lines[i] and not re.match(r'^\d+\.\s', lines[i].strip()): + para_lines.append(lines[i]) + i += 1 + elements.append(('para', ' '.join(para_lines))) + + return elements + +def clean_text(text): + """Remove markdown formatting from text.""" + # Bold + text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) + # Italic + text = re.sub(r'\*([^*]+)\*', r'\1', text) + # Code + text = re.sub(r'`([^`]+)`', r'\1', text) + # Links + text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text) + return text + +def add_formatted_text(paragraph, text): + """Add text with basic formatting to a paragraph.""" + # Split by formatting markers and add runs + parts = re.split(r'(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\))', text) + + for part in parts: + if not part: + continue + if part.startswith('**') and part.endswith('**'): + run = paragraph.add_run(part[2:-2]) + run.bold = True + elif part.startswith('`') and part.endswith('`'): + run = paragraph.add_run(part[1:-1]) + run.font.name = 'Consolas' + run.font.size = Pt(9) + # Add light background for inline code + run.font.highlight_color = 15 # Light gray (WD_COLOR_INDEX.GRAY_25) + elif part.startswith('[') and '](' in part: + match = re.match(r'\[([^\]]+)\]\(([^)]+)\)', part) + if match: + run = paragraph.add_run(match.group(1)) + run.font.color.rgb = RGBColor(0, 0, 255) + run.underline = True + else: + paragraph.add_run(part) + +def convert_md_to_docx(md_file, docx_file): + """Convert a markdown file to a Word document.""" + print(f"Converting {md_file} to {docx_file}...") + + with open(md_file, 'r', encoding='utf-8') as f: + content = f.read() + + elements = parse_markdown(content) + + doc = Document() + + # Set default font + style = doc.styles['Normal'] + style.font.name = 'Calibri' + style.font.size = Pt(11) + + for elem_type, elem_content in elements: + if elem_type == 'h1': + p = doc.add_heading(clean_text(elem_content), level=0) + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + + elif elem_type == 'h2': + doc.add_heading(clean_text(elem_content), level=1) + + elif elem_type == 'h3': + doc.add_heading(clean_text(elem_content), level=2) + + elif elem_type == 'h4': + doc.add_heading(clean_text(elem_content), level=3) + + elif elem_type == 'hr': + p = doc.add_paragraph() + p.add_run('─' * 70) + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + + elif elem_type == 'para': + p = doc.add_paragraph() + add_formatted_text(p, elem_content) + + elif elem_type == 'code': + code_lang, code_text = elem_content + add_code_block(doc, code_text, code_lang) + + elif elem_type == 'bullet': + for item in elem_content: + p = doc.add_paragraph(style='List Bullet') + add_formatted_text(p, item) + + elif elem_type == 'numbered': + for item in elem_content: + p = doc.add_paragraph(style='List Number') + add_formatted_text(p, item) + + elif elem_type == 'table': + # Parse table + rows = [] + for line in elem_content: + if '---' in line: + continue + cells = [c.strip() for c in line.split('|')[1:-1]] + if cells: + rows.append(cells) + + if rows: + num_cols = len(rows[0]) + table = doc.add_table(rows=len(rows), cols=num_cols) + table.style = 'Table Grid' + table.alignment = WD_TABLE_ALIGNMENT.CENTER + + for i, row in enumerate(rows): + for j, cell in enumerate(row): + if j < num_cols: + table.rows[i].cells[j].text = clean_text(cell) + # Bold and shade header row + if i == 0: + set_cell_shading(table.rows[i].cells[j], "E0E0E0") + for para in table.rows[i].cells[j].paragraphs: + for run in para.runs: + run.bold = True + + # Add spacing after table + doc.add_paragraph() + + doc.save(docx_file) + print(f" Created: {docx_file}") + +def main(): + docs_dir = '/home/camp/projects/powershell/docs' + + md_files = [ + 'Update-ShopfloorPCs-Remote.md', + 'Invoke-RemoteMaintenance.md', + 'Update-PC-CompleteAsset.md', + 'DATA_COLLECTION_PARITY.md' + ] + + for md_file in md_files: + md_path = os.path.join(docs_dir, md_file) + docx_path = os.path.join(docs_dir, md_file.replace('.md', '.docx')) + + if os.path.exists(md_path): + convert_md_to_docx(md_path, docx_path) + else: + print(f"Warning: {md_path} not found") + + print("\nConversion complete!") + print(f"Word documents saved to: {docs_dir}") + +if __name__ == '__main__': + main() diff --git a/edncfix b/edncfix index 28641c4..2748bfa 160000 --- a/edncfix +++ b/edncfix @@ -1 +1 @@ -Subproject commit 28641c47c5af83021bd6b5fefe7b6dcc22a4251b +Subproject commit 2748bfa0373e143841deb70c4d4b5051f3cb95b6 diff --git a/fixnetworkshare/NetworkDriveManager.ps1 b/fixnetworkshare/NetworkDriveManager.ps1 new file mode 100644 index 0000000..15590f5 --- /dev/null +++ b/fixnetworkshare/NetworkDriveManager.ps1 @@ -0,0 +1,1149 @@ +<# +.SYNOPSIS + Network Drive Manager - Backup, validate credentials, and reconnect SMB shares +.DESCRIPTION + This script helps users manage network drives connected to legacy domain servers. + It backs up current mappings, validates credentials, and reconnects drives after + password changes. +.NOTES + Author: Cameron Proudlock / GE Aerospace + Version: 1.0 + Requires: PowerShell 5.1+ +#> + +#Requires -Version 5.1 + +# ============================================================================ +# CONFIGURATION - Modify these variables as needed +# ============================================================================ + +# Legacy domain servers to manage (add/remove as needed) +$LegacyServers = @( + "tsgwp00525.rd.ds.ge.com" + # Add additional servers here: + # "server2.rd.ds.ge.com" + # "server3.rd.ds.ge.com" +) + +# Legacy domain suffix +$LegacyDomain = "logon.ds.ge.com" + +# Password reset URL +$PasswordResetURL = "https://mypassword.ge.com" + +# Backup file location (OneDrive Documents) +$OneDrivePath = [Environment]::GetFolderPath('MyDocuments') # Falls back if OneDrive not configured +$BackupFileName = "NetworkDriveMappings.json" +$BackupFilePath = Join-Path -Path $OneDrivePath -ChildPath $BackupFileName + +# ============================================================================ +# FUNCTIONS +# ============================================================================ + +function Get-LegacyUsername { + <# + .SYNOPSIS + Derives the legacy username from the current Windows profile + #> + $currentUser = $env:USERNAME + return "$currentUser@$LegacyDomain" +} + +function Invoke-PasswordResetFlow { + <# + .SYNOPSIS + Guides user through password reset and re-prompts for new credentials + .OUTPUTS + Returns new SecureString password if successful, $null if cancelled + #> + + Write-Host "" + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host " Password Reset Required" -ForegroundColor Yellow + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host "" + Write-Host " Your legacy domain password needs to be reset." -ForegroundColor White + Write-Host "" + Write-Host " Steps to complete:" -ForegroundColor Cyan + Write-Host " 1. Click 'Y' below to open the password reset portal" + Write-Host " 2. Reset your password at $PasswordResetURL" + Write-Host " 3. Return here and type 'DONE'" + Write-Host " 4. Wait for the 10-minute sync timer" + Write-Host " 5. Enter your NEW password to continue" + Write-Host "" + Write-Host " NOTE: Password changes can take up to 10 minutes to sync" -ForegroundColor Gray + Write-Host " from Azure AD to the legacy domain. The script will wait" -ForegroundColor Gray + Write-Host " automatically after you confirm the reset." -ForegroundColor Gray + Write-Host "" + + $openPortal = Read-Host "Open password reset portal now? (Y/n)" + if ($openPortal.ToLower() -ne 'n') { + Start-Process $PasswordResetURL + Write-Host "" + Write-Host " Browser opened to password reset portal..." -ForegroundColor Gray + } + + Write-Host "" + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host "" + Write-Host " [ ] I have reset my password at $PasswordResetURL" -ForegroundColor White + Write-Host "" + + $confirmed = $false + while (-not $confirmed) { + $confirmation = Read-Host "Type 'DONE' when you have reset your password (or 'CANCEL' to abort)" + + switch ($confirmation.ToUpper()) { + "DONE" { + $confirmed = $true + } + "CANCEL" { + Write-Host "" + Write-Host " Password reset cancelled." -ForegroundColor Yellow + return $null + } + default { + Write-Host " Please type 'DONE' or 'CANCEL'" -ForegroundColor Yellow + } + } + } + + Write-Host "" + Write-Host " [X] Password reset confirmed!" -ForegroundColor Green + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host " Waiting for AD Sync (~10 minutes)" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host " Your new password needs time to sync to the legacy domain." -ForegroundColor White + Write-Host " Please wait for the timer to complete, or press 'S' to skip" -ForegroundColor White + Write-Host " if you've already waited." -ForegroundColor White + Write-Host "" + + # 10 minute countdown timer + $totalSeconds = 600 # 10 minutes + $startTime = Get-Date + $endTime = $startTime.AddSeconds($totalSeconds) + $skipped = $false + + # Check if a key is available to read (non-blocking) + $host.UI.RawUI.FlushInputBuffer() + + while ((Get-Date) -lt $endTime -and -not $skipped) { + $remaining = $endTime - (Get-Date) + $minutes = [math]::Floor($remaining.TotalMinutes) + $seconds = $remaining.Seconds + + # Create progress bar + $elapsed = ((Get-Date) - $startTime).TotalSeconds + $percentComplete = [math]::Min(100, [math]::Floor(($elapsed / $totalSeconds) * 100)) + $barLength = 40 + $filledLength = [math]::Floor($barLength * $percentComplete / 100) + $bar = ("█" * $filledLength) + ("░" * ($barLength - $filledLength)) + + # Write progress on same line + Write-Host "`r [$bar] $($minutes.ToString('00')):$($seconds.ToString('00')) remaining (Press 'S' to skip) " -NoNewline -ForegroundColor Cyan + + # Check for keypress (non-blocking) + if ([Console]::KeyAvailable) { + $key = [Console]::ReadKey($true) + if ($key.Key -eq 'S') { + $skipped = $true + Write-Host "" + Write-Host "" + Write-Host " Timer skipped by user." -ForegroundColor Yellow + } + } + + Start-Sleep -Milliseconds 500 + } + + if (-not $skipped) { + Write-Host "" + Write-Host "" + Write-Host " [OK] Sync wait complete!" -ForegroundColor Green + } + + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host " Please enter your NEW password below." -ForegroundColor White + Write-Host "" + + $newPassword = Read-Host " Enter your NEW legacy password" -AsSecureString + + if ($newPassword.Length -eq 0) { + Write-Host "" + Write-Host " [ERROR] Password cannot be empty." -ForegroundColor Red + return $null + } + + # Confirm password + Write-Host "" + $confirmPassword = Read-Host " Confirm your NEW legacy password" -AsSecureString + + # Compare the two passwords + $BSTR1 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newPassword) + $BSTR2 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($confirmPassword) + $plain1 = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR1) + $plain2 = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR2) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR1) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR2) + + if ($plain1 -ne $plain2) { + Write-Host "" + Write-Host " [ERROR] Passwords do not match. Please try again." -ForegroundColor Red + $plain1 = $null + $plain2 = $null + return $null + } + + $plain1 = $null + $plain2 = $null + + Write-Host "" + Write-Host " [OK] Password confirmed!" -ForegroundColor Green + + return $newPassword +} + +function Invoke-ZscalerReauthPrompt { + <# + .SYNOPSIS + Prompts user to re-authenticate to Zscaler and retry + .OUTPUTS + Returns $true if user wants to retry, $false to cancel + #> + + Write-Host "" + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host " Zscaler Re-Authentication Required" -ForegroundColor Yellow + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host "" + Write-Host " The network path could not be found. This typically means" -ForegroundColor White + Write-Host " your Zscaler connection needs to be re-authenticated." -ForegroundColor White + Write-Host "" + Write-Host " Steps to fix:" -ForegroundColor Cyan + Write-Host " 1. Look for the Zscaler icon in your system tray" + Write-Host " 2. Right-click and select 'Sign In' or 'Authenticate'" + Write-Host " 3. Complete the authentication process" + Write-Host " 4. Return here and select 'Retry'" + Write-Host "" + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host "" + + $retry = Read-Host "Type 'RETRY' after re-authenticating to Zscaler (or 'CANCEL' to abort)" + + return ($retry.ToUpper() -eq "RETRY") +} + +function Invoke-ErrorActionHandler { + <# + .SYNOPSIS + Handles specific error actions and returns whether to retry + .OUTPUTS + Hashtable with: + - Retry: $true if operation should be retried + - NewPassword: SecureString if password was reset, $null otherwise + - Cancel: $true if user wants to cancel entirely + #> + param( + [Parameter(Mandatory)] + [string]$Action, + + [Parameter(Mandatory)] + [int]$ErrorCode, + + [string]$AdditionalInfo = "" + ) + + $result = @{ + Retry = $false + NewPassword = $null + Cancel = $false + } + + switch ($Action) { + "contact_manager" { + Write-Host "" + Write-Host "============================================================" -ForegroundColor Red + Write-Host " Access Denied (Error 5)" -ForegroundColor Red + Write-Host "============================================================" -ForegroundColor Red + Write-Host "" + Write-Host " You do not have permission to access this share." -ForegroundColor White + Write-Host "" + Write-Host " ACTION REQUIRED:" -ForegroundColor Yellow + Write-Host " Contact your manager to request access to this resource." + Write-Host "" + Write-Host "============================================================" -ForegroundColor Red + Write-Host "" + Read-Host "Press Enter to continue..." + $result.Cancel = $true + } + + "zscaler_reauth" { + $retry = Invoke-ZscalerReauthPrompt + $result.Retry = $retry + $result.Cancel = (-not $retry) + } + + "retry_or_zscaler" { + Write-Host "" + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host " Connection Dropped (Error 64)" -ForegroundColor Yellow + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host "" + Write-Host " The network connection was unexpectedly terminated." -ForegroundColor White + Write-Host "" + Write-Host " This could be caused by:" -ForegroundColor Cyan + Write-Host " - Zscaler connection timeout (most common)" + Write-Host " - Network instability" + Write-Host " - Server-side connection reset" + Write-Host "" + Write-Host " Try re-authenticating to Zscaler first." -ForegroundColor Yellow + Write-Host "" + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host "" + + $retry = Invoke-ZscalerReauthPrompt + $result.Retry = $retry + $result.Cancel = (-not $retry) + } + + "verify_path" { + Write-Host "" + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host " Invalid Share Path (Error 67)" -ForegroundColor Yellow + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host "" + Write-Host " The share path appears to be invalid or malformed." -ForegroundColor White + if ($AdditionalInfo) { + Write-Host " Path: $AdditionalInfo" -ForegroundColor Gray + } + Write-Host "" + Write-Host " Possible causes:" -ForegroundColor Cyan + Write-Host " - The share may have been renamed or removed" + Write-Host " - The path may be incorrectly formatted" + Write-Host " - Typo in the server or share name" + Write-Host "" + Write-Host " ACTION REQUIRED:" -ForegroundColor Yellow + Write-Host " Verify the share path is correct, or contact IT support" + Write-Host " if you believe this share should exist." + Write-Host "" + Write-Host "============================================================" -ForegroundColor Yellow + Write-Host "" + Read-Host "Press Enter to continue..." + # Don't retry automatically - path needs manual verification + $result.Cancel = $true + } + + "reassign_drive_letter" { + # This is handled automatically - we'll disconnect and reconnect + Write-Host "" + Write-Host " [INFO] Drive letter conflict detected - will reassign automatically" -ForegroundColor Cyan + $result.Retry = $true + } + + "reset_password_flow" { + $newPassword = Invoke-PasswordResetFlow + if ($null -ne $newPassword) { + $result.Retry = $true + $result.NewPassword = $newPassword + } + else { + $result.Cancel = $true + } + } + + "clear_connections" { + Write-Host "" + Write-Host " [INFO] Clearing existing connections to resolve conflict..." -ForegroundColor Cyan + # This is handled in the main flow + $result.Retry = $true + } + + "contact_helpdesk" { + Write-Host "" + Write-Host "============================================================" -ForegroundColor Red + Write-Host " Account Issue Detected" -ForegroundColor Red + Write-Host "============================================================" -ForegroundColor Red + Write-Host "" + Write-Host " Your account has a restriction that prevents connection." -ForegroundColor White + Write-Host " Error Code: $ErrorCode" -ForegroundColor Gray + Write-Host "" + Write-Host " ACTION REQUIRED:" -ForegroundColor Yellow + Write-Host " Please contact the IT Helpdesk for assistance." + Write-Host "" + Write-Host "============================================================" -ForegroundColor Red + Write-Host "" + Read-Host "Press Enter to continue..." + $result.Cancel = $true + } + + "unknown_error" { + Write-Host "" + Write-Host "============================================================" -ForegroundColor Red + Write-Host " Unknown Error (Code: $ErrorCode)" -ForegroundColor Red + Write-Host "============================================================" -ForegroundColor Red + Write-Host "" + Write-Host " An unexpected error occurred." -ForegroundColor White + if ($AdditionalInfo) { + Write-Host " Details: $AdditionalInfo" -ForegroundColor Gray + } + Write-Host "" + Write-Host " Options:" -ForegroundColor Cyan + Write-Host " - Try re-authenticating to Zscaler" + Write-Host " - Check your network connection" + Write-Host " - Contact IT Helpdesk if the issue persists" + Write-Host "" + Write-Host "============================================================" -ForegroundColor Red + Write-Host "" + + $choice = Read-Host "Type 'RETRY' to try again, or press Enter to cancel" + $result.Retry = ($choice.ToUpper() -eq "RETRY") + $result.Cancel = (-not $result.Retry) + } + + default { + $result.Cancel = $true + } + } + + return $result +} + +function Get-CurrentDriveMappings { + <# + .SYNOPSIS + Gets all current network drive mappings and filters for legacy servers + #> + param( + [switch]$AllDrives, + [string[]]$ServerFilter = $LegacyServers + ) + + $mappings = @() + + # Get all network drives + $netDrives = Get-WmiObject -Class Win32_MappedLogicalDisk -ErrorAction SilentlyContinue + + foreach ($drive in $netDrives) { + $mapping = [PSCustomObject]@{ + DriveLetter = $drive.DeviceID + RemotePath = $drive.ProviderName + ServerName = if ($drive.ProviderName -match '\\\\([^\\]+)\\') { $Matches[1] } else { $null } + ShareName = if ($drive.ProviderName -match '\\\\[^\\]+\\(.+)$') { $Matches[1] } else { $null } + IsLegacy = $false + } + + # Check if this is a legacy server drive + foreach ($server in $ServerFilter) { + if ($mapping.ServerName -like "*$server*" -or $mapping.RemotePath -like "*$server*") { + $mapping.IsLegacy = $true + break + } + } + + if ($AllDrives -or $mapping.IsLegacy) { + $mappings += $mapping + } + } + + return $mappings +} + +function Backup-DriveMappings { + <# + .SYNOPSIS + Saves current drive mappings to a JSON file in OneDrive + #> + param( + [string]$FilePath = $BackupFilePath + ) + + $mappings = Get-CurrentDriveMappings -AllDrives + + $backupData = [PSCustomObject]@{ + BackupDate = (Get-Date -Format "yyyy-MM-dd HH:mm:ss") + ComputerName = $env:COMPUTERNAME + Username = $env:USERNAME + Mappings = $mappings + } + + try { + $backupData | ConvertTo-Json -Depth 3 | Out-File -FilePath $FilePath -Encoding UTF8 -Force + Write-Host "[OK] " -ForegroundColor Green -NoNewline + Write-Host "Backup saved to: $FilePath" + return $true + } + catch { + Write-Host "[ERROR] " -ForegroundColor Red -NoNewline + Write-Host "Failed to save backup: $_" + return $false + } +} + +function Get-SavedMappings { + <# + .SYNOPSIS + Loads previously saved drive mappings from backup file + #> + param( + [string]$FilePath = $BackupFilePath + ) + + if (-not (Test-Path $FilePath)) { + Write-Host "[WARN] " -ForegroundColor Yellow -NoNewline + Write-Host "No backup file found at: $FilePath" + return $null + } + + try { + $backupData = Get-Content -Path $FilePath -Raw | ConvertFrom-Json + Write-Host "[OK] " -ForegroundColor Green -NoNewline + Write-Host "Loaded backup from: $($backupData.BackupDate)" + return $backupData + } + catch { + Write-Host "[ERROR] " -ForegroundColor Red -NoNewline + Write-Host "Failed to read backup: $_" + return $null + } +} + +function Test-SMBCredentials { + <# + .SYNOPSIS + Tests credentials against a legacy SMB server + .OUTPUTS + Returns a hashtable with Success, ErrorCode, and Message + #> + param( + [Parameter(Mandatory)] + [string]$Server, + + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [SecureString]$Password + ) + + $result = @{ + Success = $false + ErrorCode = 0 + Message = "" + Action = "" + } + + # Convert SecureString to plain text for net use (unfortunately required) + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password) + $PlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) + + # Try to connect to the server's IPC$ share (doesn't require a specific share to exist) + $testPath = "\\$Server\IPC$" + + # First, remove any existing connection to this server + $null = net use $testPath /delete /y 2>&1 + + # Attempt connection + $netUseOutput = net use $testPath /user:$Username $PlainPassword 2>&1 + $exitCode = $LASTEXITCODE + + # Clean up the test connection + $null = net use $testPath /delete /y 2>&1 + + # Clear password from memory + $PlainPassword = $null + + # Interpret the result + switch ($exitCode) { + 0 { + $result.Success = $true + $result.Message = "Credentials validated successfully" + $result.Action = "none" + } + 5 { + $result.ErrorCode = 5 + $result.Message = "Access denied - you may not have permission to this share" + $result.Action = "contact_manager" + } + 53 { + $result.ErrorCode = 53 + $result.Message = "Network path not found - Zscaler may need re-authentication" + $result.Action = "zscaler_reauth" + } + 64 { + $result.ErrorCode = 64 + $result.Message = "Network connection was dropped unexpectedly" + $result.Action = "retry_or_zscaler" + } + 67 { + $result.ErrorCode = 67 + $result.Message = "Network name cannot be found - invalid share path" + $result.Action = "verify_path" + } + 85 { + $result.ErrorCode = 85 + $result.Message = "Drive letter is already in use" + $result.Action = "reassign_drive_letter" + } + 86 { + $result.ErrorCode = 86 + $result.Message = "Invalid password" + $result.Action = "reset_password_flow" + } + 1219 { + $result.ErrorCode = 1219 + $result.Message = "Multiple connections to server exist with different credentials" + $result.Action = "clear_connections" + } + 1326 { + $result.ErrorCode = 1326 + $result.Message = "Logon failure - unknown username or bad password" + $result.Action = "reset_password_flow" + } + 1327 { + $result.ErrorCode = 1327 + $result.Message = "Account restriction - policy is preventing logon" + $result.Action = "contact_helpdesk" + } + 1330 { + $result.ErrorCode = 1330 + $result.Message = "Password has expired" + $result.Action = "reset_password_flow" + } + 1331 { + $result.ErrorCode = 1331 + $result.Message = "Account is disabled" + $result.Action = "contact_helpdesk" + } + 1909 { + $result.ErrorCode = 1909 + $result.Message = "Account is locked out" + $result.Action = "reset_password_flow" + } + default { + $result.ErrorCode = $exitCode + $result.Message = "Unknown error (code: $exitCode) - $netUseOutput" + $result.Action = "unknown_error" + } + } + + return $result +} + +function Remove-LegacyDriveMappings { + <# + .SYNOPSIS + Removes all network drive mappings for legacy servers + #> + param( + [string[]]$Servers = $LegacyServers + ) + + $currentMappings = Get-CurrentDriveMappings + $removed = @() + + foreach ($mapping in $currentMappings) { + if ($mapping.IsLegacy) { + Write-Host " Removing $($mapping.DriveLetter) ($($mapping.RemotePath))... " -NoNewline + + $result = net use $mapping.DriveLetter /delete /y 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Host "OK" -ForegroundColor Green + $removed += $mapping + } + else { + Write-Host "Failed" -ForegroundColor Red + } + } + } + + # Also clear any cached connections to these servers + foreach ($server in $Servers) { + $null = net use "\\$server\IPC$" /delete /y 2>&1 + } + + return $removed +} + +function Remove-WindowsCredentials { + <# + .SYNOPSIS + Removes stored Windows credentials for legacy servers + #> + param( + [string[]]$Servers = $LegacyServers + ) + + foreach ($server in $Servers) { + Write-Host " Clearing credentials for $server... " -NoNewline + + # Try different credential target formats + $targets = @( + $server, + "Domain:target=$server", + "LegacyGeneric:target=$server", + "*$server*" + ) + + $cleared = $false + foreach ($target in $targets) { + $result = cmdkey /delete:$target 2>&1 + if ($LASTEXITCODE -eq 0) { + $cleared = $true + } + } + + if ($cleared) { + Write-Host "OK" -ForegroundColor Green + } + else { + Write-Host "Not found/Already cleared" -ForegroundColor Yellow + } + } +} + +function Add-DriveMappings { + <# + .SYNOPSIS + Reconnects drive mappings with new credentials + Handles error 85 (drive letter in use) automatically + #> + param( + [Parameter(Mandatory)] + [PSCustomObject[]]$Mappings, + + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [SecureString]$Password + ) + + # Convert SecureString to plain text + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password) + $PlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) + + $results = @() + + foreach ($mapping in $Mappings) { + if (-not $mapping.IsLegacy) { continue } + + Write-Host " Mapping $($mapping.DriveLetter) to $($mapping.RemotePath)... " -NoNewline + + # First attempt + $netUseOutput = net use $mapping.DriveLetter $mapping.RemotePath /user:$Username $PlainPassword /persistent:yes 2>&1 + $exitCode = $LASTEXITCODE + + # Handle error 85 - drive letter already in use + if ($exitCode -eq 85) { + Write-Host "in use, reassigning... " -NoNewline -ForegroundColor Yellow + + # Try to disconnect the existing mapping + $null = net use $mapping.DriveLetter /delete /y 2>&1 + Start-Sleep -Milliseconds 500 + + # Retry the mapping + $netUseOutput = net use $mapping.DriveLetter $mapping.RemotePath /user:$Username $PlainPassword /persistent:yes 2>&1 + $exitCode = $LASTEXITCODE + } + + $mapResult = [PSCustomObject]@{ + DriveLetter = $mapping.DriveLetter + RemotePath = $mapping.RemotePath + Success = ($exitCode -eq 0) + ErrorCode = $exitCode + Message = $netUseOutput + } + + if ($exitCode -eq 0) { + Write-Host "OK" -ForegroundColor Green + } + else { + Write-Host "Failed (Error: $exitCode)" -ForegroundColor Red + } + + $results += $mapResult + } + + # Clear password from memory + $PlainPassword = $null + + return $results +} + +function Show-Menu { + <# + .SYNOPSIS + Displays the main menu + #> + Clear-Host + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host " GE Aerospace - Network Drive Manager" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host " Current User: $env:USERNAME" -ForegroundColor Gray + Write-Host " Legacy User: $(Get-LegacyUsername)" -ForegroundColor Gray + Write-Host " Backup File: $BackupFilePath" -ForegroundColor Gray + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host " 1. " -NoNewline -ForegroundColor Yellow + Write-Host "View current network drive mappings" + Write-Host " 2. " -NoNewline -ForegroundColor Yellow + Write-Host "Backup current mappings to OneDrive" + Write-Host " 3. " -NoNewline -ForegroundColor Yellow + Write-Host "Test legacy credentials" + Write-Host " 4. " -NoNewline -ForegroundColor Yellow + Write-Host "Full reset - Disconnect, clear creds, and reconnect drives" + Write-Host " 5. " -NoNewline -ForegroundColor Yellow + Write-Host "Restore drives from backup" + Write-Host " 6. " -NoNewline -ForegroundColor Yellow + Write-Host "Open password reset portal" + Write-Host "" + Write-Host " Q. " -NoNewline -ForegroundColor Red + Write-Host "Quit" + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan +} + +function Invoke-FullReset { + <# + .SYNOPSIS + Performs full credential reset and drive reconnection + With intelligent error handling and retry logic + #> + + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host " Full Credential Reset & Drive Reconnection" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + # Step 1: Backup current mappings + Write-Host "[Step 1/5] Backing up current drive mappings..." -ForegroundColor Yellow + $backupSuccess = Backup-DriveMappings + if (-not $backupSuccess) { + Write-Host "" + $continue = Read-Host "Backup failed. Continue anyway? (y/N)" + if ($continue -ne 'y') { return } + } + + # Load the backup we just made (or existing one) + $savedMappings = Get-SavedMappings + $legacyMappings = $savedMappings.Mappings | Where-Object { $_.IsLegacy -eq $true } + + if (-not $legacyMappings -or $legacyMappings.Count -eq 0) { + Write-Host "" + Write-Host "[WARN] " -ForegroundColor Yellow -NoNewline + Write-Host "No legacy drive mappings found to restore." + Write-Host " Press Enter to return to menu..." + Read-Host + return + } + + Write-Host "" + Write-Host "Found $($legacyMappings.Count) legacy drive(s) to manage:" -ForegroundColor Cyan + foreach ($map in $legacyMappings) { + Write-Host " $($map.DriveLetter) -> $($map.RemotePath)" -ForegroundColor Gray + } + Write-Host "" + + # Step 2: Get credentials from user + Write-Host "[Step 2/5] Enter your legacy domain credentials" -ForegroundColor Yellow + $legacyUser = Get-LegacyUsername + Write-Host " Username: $legacyUser" -ForegroundColor Gray + Write-Host "" + $password = Read-Host " Enter your legacy password" -AsSecureString + + if ($password.Length -eq 0) { + Write-Host "[ERROR] " -ForegroundColor Red -NoNewline + Write-Host "Password cannot be empty." + Read-Host "Press Enter to return to menu..." + return + } + + # Step 3: Test credentials first (with retry loop for errors) + $credentialsValid = $false + $maxRetries = 3 + $retryCount = 0 + + while (-not $credentialsValid -and $retryCount -lt $maxRetries) { + Write-Host "" + Write-Host "[Step 3/5] Testing credentials against $($LegacyServers[0])..." -ForegroundColor Yellow + $testResult = Test-SMBCredentials -Server $LegacyServers[0] -Username $legacyUser -Password $password + + if ($testResult.Success) { + $credentialsValid = $true + Write-Host "[OK] " -ForegroundColor Green -NoNewline + Write-Host "Credentials validated successfully!" + } + else { + Write-Host "" + Write-Host "[FAILED] " -ForegroundColor Red -NoNewline + Write-Host $testResult.Message + + # Handle the specific error + $errorHandler = Invoke-ErrorActionHandler -Action $testResult.Action -ErrorCode $testResult.ErrorCode + + if ($errorHandler.Cancel) { + Write-Host "" + Write-Host "Operation cancelled." -ForegroundColor Yellow + Read-Host "Press Enter to return to menu..." + return + } + + if ($errorHandler.NewPassword) { + # User reset their password - use the new one + $password = $errorHandler.NewPassword + Write-Host "" + Write-Host " Retrying with new password..." -ForegroundColor Cyan + } + elseif ($errorHandler.Retry) { + # Retry with same credentials (e.g., after Zscaler reauth) + Write-Host "" + Write-Host " Retrying..." -ForegroundColor Cyan + } + + $retryCount++ + } + } + + if (-not $credentialsValid) { + Write-Host "" + Write-Host "[ERROR] " -ForegroundColor Red -NoNewline + Write-Host "Maximum retry attempts reached. Please try again later." + Read-Host "Press Enter to return to menu..." + return + } + + Write-Host "" + + # Step 4: Remove existing mappings and credentials + Write-Host "[Step 4/5] Removing existing mappings and credentials..." -ForegroundColor Yellow + Remove-LegacyDriveMappings + Remove-WindowsCredentials + + Write-Host "" + + # Step 5: Reconnect drives (with individual error handling) + Write-Host "[Step 5/5] Reconnecting drives with new credentials..." -ForegroundColor Yellow + $mapResults = Add-DriveMappings -Mappings $legacyMappings -Username $legacyUser -Password $password + + # Check for failures that need individual handling + $failedMappings = $mapResults | Where-Object { -not $_.Success } + + foreach ($failed in $failedMappings) { + Write-Host "" + Write-Host " Handling failed mapping: $($failed.DriveLetter)" -ForegroundColor Yellow + + # Determine the action based on error code + $action = switch ($failed.ErrorCode) { + 5 { "contact_manager" } + 53 { "zscaler_reauth" } + 64 { "retry_or_zscaler" } + 67 { "verify_path" } + 86 { "reset_password_flow" } + 1326 { "reset_password_flow" } + 1330 { "reset_password_flow" } + 1909 { "reset_password_flow" } + default { "unknown_error" } + } + + $errorHandler = Invoke-ErrorActionHandler -Action $action -ErrorCode $failed.ErrorCode -AdditionalInfo $failed.RemotePath + + if ($errorHandler.Retry -and -not $errorHandler.Cancel) { + if ($errorHandler.NewPassword) { + $password = $errorHandler.NewPassword + } + + # Retry just this one mapping + Write-Host " Retrying $($failed.DriveLetter)... " -NoNewline + + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) + $PlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) + + $retryOutput = net use $failed.DriveLetter $failed.RemotePath /user:$legacyUser $PlainPassword /persistent:yes 2>&1 + $PlainPassword = $null + + if ($LASTEXITCODE -eq 0) { + Write-Host "OK" -ForegroundColor Green + # Update the result + $failed.Success = $true + $failed.ErrorCode = 0 + } + else { + Write-Host "Still failed (Error: $LASTEXITCODE)" -ForegroundColor Red + } + } + } + + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host " Summary" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + + $successCount = ($mapResults | Where-Object { $_.Success }).Count + $failCount = ($mapResults | Where-Object { -not $_.Success }).Count + + Write-Host "" + Write-Host " Successful: " -NoNewline + Write-Host $successCount -ForegroundColor Green + Write-Host " Failed: " -NoNewline + Write-Host $failCount -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "Green" }) + Write-Host "" + + if ($failCount -gt 0) { + Write-Host "Failed mappings:" -ForegroundColor Red + foreach ($result in ($mapResults | Where-Object { -not $_.Success })) { + Write-Host " $($result.DriveLetter) - Error $($result.ErrorCode)" -ForegroundColor Red + } + Write-Host "" + Write-Host " You may need to map these drives manually or contact IT support." -ForegroundColor Yellow + } + + Write-Host "" + Read-Host "Press Enter to return to menu..." +} + +# ============================================================================ +# MAIN SCRIPT +# ============================================================================ + +# Check if running interactively or with parameters +param( + [switch]$Silent, + [switch]$BackupOnly, + [switch]$Reset +) + +if ($BackupOnly) { + Backup-DriveMappings + exit 0 +} + +if ($Reset) { + # Non-interactive reset - would need credentials passed in or prompted + Write-Host "Non-interactive reset not yet implemented. Please run without -Reset for interactive mode." + exit 1 +} + +# Interactive menu loop +do { + Show-Menu + $choice = Read-Host "Select an option" + + switch ($choice.ToLower()) { + "1" { + # View current mappings + Write-Host "" + Write-Host "Current Network Drive Mappings:" -ForegroundColor Cyan + Write-Host "================================" -ForegroundColor Cyan + $mappings = Get-CurrentDriveMappings -AllDrives + + if ($mappings.Count -eq 0) { + Write-Host " No network drives mapped." -ForegroundColor Yellow + } + else { + foreach ($map in $mappings) { + $legacyTag = if ($map.IsLegacy) { " [LEGACY]" } else { "" } + $color = if ($map.IsLegacy) { "Yellow" } else { "Gray" } + Write-Host " $($map.DriveLetter) -> $($map.RemotePath)$legacyTag" -ForegroundColor $color + } + } + Write-Host "" + Read-Host "Press Enter to continue..." + } + "2" { + # Backup mappings + Write-Host "" + Backup-DriveMappings + Write-Host "" + Read-Host "Press Enter to continue..." + } + "3" { + # Test credentials + Write-Host "" + Write-Host "Testing Legacy Credentials" -ForegroundColor Cyan + Write-Host "==========================" -ForegroundColor Cyan + $legacyUser = Get-LegacyUsername + Write-Host "Username: $legacyUser" -ForegroundColor Gray + Write-Host "" + $password = Read-Host "Enter your legacy password" -AsSecureString + + Write-Host "" + Write-Host "Testing against servers..." -ForegroundColor Yellow + + foreach ($server in $LegacyServers) { + Write-Host " $server... " -NoNewline + $result = Test-SMBCredentials -Server $server -Username $legacyUser -Password $password + + if ($result.Success) { + Write-Host "OK" -ForegroundColor Green + } + else { + Write-Host "FAILED - $($result.Message)" -ForegroundColor Red + } + } + + Write-Host "" + Read-Host "Press Enter to continue..." + } + "4" { + # Full reset + Invoke-FullReset + } + "5" { + # Restore from backup + Write-Host "" + $savedMappings = Get-SavedMappings + + if ($savedMappings) { + Write-Host "" + Write-Host "Saved mappings from $($savedMappings.BackupDate):" -ForegroundColor Cyan + foreach ($map in $savedMappings.Mappings) { + $legacyTag = if ($map.IsLegacy) { " [LEGACY]" } else { "" } + Write-Host " $($map.DriveLetter) -> $($map.RemotePath)$legacyTag" + } + + Write-Host "" + $restore = Read-Host "Restore legacy drives from this backup? (y/N)" + + if ($restore -eq 'y') { + $legacyUser = Get-LegacyUsername + Write-Host "" + Write-Host "Username: $legacyUser" -ForegroundColor Gray + $password = Read-Host "Enter your legacy password" -AsSecureString + + $legacyMappings = $savedMappings.Mappings | Where-Object { $_.IsLegacy } + Add-DriveMappings -Mappings $legacyMappings -Username $legacyUser -Password $password + } + } + + Write-Host "" + Read-Host "Press Enter to continue..." + } + "6" { + # Open password portal + Write-Host "" + Write-Host "Opening $PasswordResetURL ..." -ForegroundColor Cyan + Start-Process $PasswordResetURL + Start-Sleep -Seconds 2 + } + "q" { + Write-Host "" + Write-Host "Goodbye!" -ForegroundColor Cyan + } + default { + Write-Host "" + Write-Host "Invalid option. Please try again." -ForegroundColor Red + Start-Sleep -Seconds 1 + } + } +} while ($choice.ToLower() -ne 'q') diff --git a/minimal-asset/Update-PC-Minimal.ps1 b/minimal-asset/Update-PC-Minimal.ps1 index 7f42ce3..5ffc607 100644 --- a/minimal-asset/Update-PC-Minimal.ps1 +++ b/minimal-asset/Update-PC-Minimal.ps1 @@ -160,9 +160,9 @@ try { # Machine numbers that indicate specific PC types (these ARE valid machine numbers) $machineTypeIndicators = @{ "^0?600$" = "Wax Trace" # Wax trace machines - "^0?(612|613|615)$" = "Part Marker" # Part marker machines - "^M?(612|613|615)$" = "Part Marker" # Part marker machines (M prefix) - "^8003$" = "Part Marker" # Part marker machines + "^0?(612|613|615)$" = "Inspection" # Part marker machines + "^M?(612|613|615)$" = "Inspection" # Part marker machines (M prefix) + "^8003$" = "Inspection" # Part marker machines } # Check if machine number indicates a specific PC type diff --git a/remote-execution/DeployOpenTextProfiles-Examples.txt b/remote-execution/DeployOpenTextProfiles-Examples.txt new file mode 100644 index 0000000..73a1d0a --- /dev/null +++ b/remote-execution/DeployOpenTextProfiles-Examples.txt @@ -0,0 +1,29 @@ +# DeployOpenTextProfiles - Example Usage +# Source: \\tsgwp00525.wjs.geaerospace.net\shared\dt\csf\ + +# Single PC +.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF" -Task DeployOpenTextProfiles + +# Multiple PCs +.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF","G1ZTNCX4ESF","G1ZTNCX5ESF" -Task DeployOpenTextProfiles + +# All shopfloor PCs +.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task DeployOpenTextProfiles + +# Wax / Trace PCs +.\Invoke-RemoteMaintenance.ps1 -PcType "Wax / Trace" -Task DeployOpenTextProfiles + +# Keyence PCs +.\Invoke-RemoteMaintenance.ps1 -PcType Keyence -Task DeployOpenTextProfiles + +# Genspect PCs +.\Invoke-RemoteMaintenance.ps1 -PcType Genspect -Task DeployOpenTextProfiles + +# Heat Treat PCs +.\Invoke-RemoteMaintenance.ps1 -PcType "Heat Treat" -Task DeployOpenTextProfiles + +# CMM PCs +.\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task DeployOpenTextProfiles + +# Inspection PCs +.\Invoke-RemoteMaintenance.ps1 -PcType Inspection -Task DeployOpenTextProfiles diff --git a/remote-execution/INSTRUCTIONS.txt b/remote-execution/INSTRUCTIONS.txt index 618151d..a7e3643 100644 --- a/remote-execution/INSTRUCTIONS.txt +++ b/remote-execution/INSTRUCTIONS.txt @@ -13,8 +13,11 @@ REQUIREMENTS - PowerShell 5.1+ - Run as Administrator (required for scheduling only) - - Invoke-RemoteMaintenance.ps1 in the same folder - - A PC list text file (one hostname per line) + - ALL scripts must be in the SAME folder: + Invoke-RemoteMaintenance.ps1 + Schedule-Maintenance.ps1 + Export-PCList.ps1 + shopfloor-pcs.txt (or your PC list file) ============================================================ @@ -34,53 +37,92 @@ PARAMETERS ============================================================ -USAGE +STEP 1: Save Credentials (one time) ============================================================ -1. SAVE CREDENTIALS (one time, does not require admin) + .\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\570005354" -Password "MyP@ssw0rd" - .\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\570005354" -Password "MyP@ssw0rd" - - - Encrypted with Windows DPAPI - - Only your user account on this machine can decrypt - - Re-run if your password changes - - -2. RUN IMMEDIATELY (does not require admin) - - .\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot - - -3. SCHEDULE A ONE-TIME TASK (requires admin) - - # Reboot one PC today at 3:00 PM - .\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\test-reboot.txt" -Task Reboot -TaskFrequency Once -TaskTime "15:00" -TaskDate "2026-02-19" - - # Reboot all PCs Sunday Feb 22 at 12:01 AM - .\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Once -TaskTime "00:01" -TaskDate "2026-02-22" - - -4. SCHEDULE A RECURRING TASK (requires admin) - - # Every Sunday at 12:01 AM - .\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "00:01" - - # Every day at 2:00 AM - .\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task DiskCleanup -TaskFrequency Daily -TaskTime "02:00" - - -5. MANAGE SCHEDULED TASKS - - Get-ScheduledTask | Where-Object { $_.TaskName -like "ShopfloorMaintenance*" } - Start-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" - Unregister-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" + - Encrypted with AES-256 key + - Works from normal or admin PowerShell + - Stored in .creds\ folder (not plaintext) + - Re-run if your password changes ============================================================ -LOGS +STEP 2: Generate PC List ============================================================ - .\logs\maintenance-YYYY-MM-DD_HHMMSS-TaskName.log + # All shopfloor PCs from API + .\Export-PCList.ps1 + + # Filter by type + .\Export-PCList.ps1 -PcType Shopfloor + + # Single PC for testing + "G63TVG04ESF" | Out-File -FilePath ".\test-reboot.txt" -Encoding UTF8 + + +============================================================ +STEP 3: Run or Schedule +============================================================ + + RUN IMMEDIATELY (no admin needed): + .\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot + + SCHEDULE ONE-TIME (admin required): + .\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Once -TaskTime "00:01" -TaskDate "2026-02-22" + + SCHEDULE RECURRING (admin required): + .\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "00:01" + + +============================================================ +CHECKING RESULTS +============================================================ + + AFTER A SCHEDULED RUN: + Get-Content ".\logs\LAST-RUN-SUMMARY.txt" + + FULL LOG (most recent): + Get-ChildItem ".\logs\" -Filter "maintenance-*-Reboot.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content + + CHECK IF TASK RAN: + Get-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" | Get-ScheduledTaskInfo + + LastTaskResult = 0 means success + Anything else means it errored before writing logs + + LOGS LOCATION: + .\logs\ (inside your scripts folder) + + +============================================================ +MANAGING SCHEDULED TASKS +============================================================ + + # List maintenance tasks + Get-ScheduledTask | Where-Object { $_.TaskName -like "ShopfloorMaintenance*" } + + # Run now (don't wait for schedule) + Start-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" + + # Delete a task + Unregister-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" + + # Or use: Task Scheduler GUI (taskschd.msc) + + +============================================================ +AVAILABLE TASKS +============================================================ + + Reboot, DISM, SFC, OptimizeDisk, DiskCleanup, + ClearUpdateCache, ClearBrowserCache, RestartSpooler, + FlushDNS, RestartWinRM, SetTimezone, SyncTime, + UpdateEMxAuthToken, DeployUDCWebServerConfig, + UpdateDNCMXHosts, + InstallDashboard, InstallLobbyDisplay, + UninstallDashboard, UninstallLobbyDisplay ============================================================ @@ -88,13 +130,24 @@ TROUBLESHOOTING ============================================================ "No saved credentials found" - -> Run -SaveCredential with -Username and -Password + -> .\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\user" -Password "pass" "Access is denied" when scheduling -> Right-click PowerShell -> Run as Administrator "No credentials provided. Exiting." - -> GUI prompt failed. Use -Username and -Password flags + -> Use -Username and -Password flags instead of GUI prompt + + No logs folder / empty logs + -> Task may not have run yet. Check: + Get-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" | Get-ScheduledTaskInfo Password changed - -> Re-run -SaveCredential with new password + -> .\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\user" -Password "newpass" + + NOTE ABOUT "Running as AEROAD\SSO": + This is normal. The scheduled task runs as your Windows login. + It still uses your SAVED credentials for WinRM connections + to the remote shopfloor PCs. Two separate accounts: + 1. Your Windows login = runs the script + 2. Saved credentials = connects to remote PCs diff --git a/remote-execution/Invoke-RemoteMaintenance.ps1 b/remote-execution/Invoke-RemoteMaintenance.ps1 index 13cd28e..00413e1 100644 --- a/remote-execution/Invoke-RemoteMaintenance.ps1 +++ b/remote-execution/Invoke-RemoteMaintenance.ps1 @@ -48,10 +48,22 @@ - SyncTime : Force time sync with domain controller DNC: - - UpdateEMxAuthToken : Update eMx auth token (eMxInfo.txt) from network share (backs up old file first) - - DeployUDCWebServerConfig : Deploy UDC web server settings to PCs with UDC installed + - UpdateDNCMXHosts : Update FtpHostPrimary/Secondary in DNC\MX registry key (both 32-bit and 64-bit paths) + - AuditDNCConfig : Compare DNC registry settings against UDC backup JSON files, report mismatches + - CheckDefectTracker : Check if Defect_Tracker.exe is running on target PCs (reports machine number + status) + - BackupNTLARS : Export DNC registry (NTLARS settings + subkeys) to .reg file, save to \\tsgwp00525 backup share + Named by machine number (from DNC\General\MachineNo), falls back to serial number + - VerifyNTLARS : Compare collected .reg backups against ShopDB - reports which machines are missing backups + + FILE DEPLOYMENT: + - CopyFile : Copy file from -SourcePath to -DestinationPath on remote PCs (backs up existing files) + - ImportReg : Copy .reg file from -SourcePath to remote PCs and execute reg import + - DeployOpenTextProfiles : Deploy OpenText HostExplorer profiles, accessories, keymaps, and menus to ProgramData + and all user AppData\Roaming profiles. Kills hostex32.exe, deploys from csf.zip, + then relaunches HostExplorer with WJ Shopfloor profile as the logged-in user. SYSTEM: + - GPUpdate : Force Group Policy refresh (gpupdate /force) - Reboot : Restart the computer (30 second delay) SOFTWARE DEPLOYMENT: @@ -60,6 +72,23 @@ - UninstallDashboard : Uninstall GE Aerospace Dashboard - UninstallLobbyDisplay : Uninstall GE Aerospace Lobby Display +.PARAMETER SourcePath + Source file path (local or UNC) for CopyFile and ImportReg tasks. + +.PARAMETER DestinationPath + Destination file path on remote PCs for CopyFile task. + +.PARAMETER RunCommand + Optional command to execute after CopyFile completes. + By default runs as the logged-in user via a scheduled task (for user-session + processes like Edge kiosk). Use -AsSystem to run in the WinRM session instead. + Example: "taskkill /IM MyApp.exe /F" + +.PARAMETER AsSystem + Run ImportReg and -RunCommand directly in the WinRM session (SYSTEM context) + instead of as the logged-in user via scheduled task. + Use this for HKLM-only registry imports or system-level commands. + .PARAMETER Credential PSCredential for remote authentication. Prompts if not provided. @@ -109,6 +138,30 @@ # Uninstall Dashboard from a PC .\Invoke-RemoteMaintenance.ps1 -ComputerName "PC001" -Task UninstallDashboard +.EXAMPLE + # Copy a config file to remote PCs + .\Invoke-RemoteMaintenance.ps1 -ComputerName "PC001" -Task CopyFile -SourcePath "\\server\share\config.json" -DestinationPath "C:\ProgramData\App\config.json" + +.EXAMPLE + # Copy file and restart an app as the logged-in user + .\Invoke-RemoteMaintenance.ps1 -All -Task CopyFile -SourcePath "\\server\share\eMxInfo.txt" -DestinationPath "C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt" -RunCommand "taskkill /IM DNCMain.exe /F" + +.EXAMPLE + # Import a .reg file on remote PCs (runs as logged-in user for HKCU support) + .\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task ImportReg -SourcePath "\\server\share\intranet-zone.reg" + +.EXAMPLE + # Deploy OpenText HostExplorer profiles to a single PC + .\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF" -Task DeployOpenTextProfiles + +.EXAMPLE + # Deploy OpenText HostExplorer profiles to all shopfloor PCs + .\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task DeployOpenTextProfiles + +.EXAMPLE + # Deploy OpenText HostExplorer profiles to all Keyence PCs + .\Invoke-RemoteMaintenance.ps1 -PcType Keyence -Task DeployOpenTextProfiles + .NOTES Author: Shop Floor Tools Date: 2025-12-26 @@ -139,7 +192,8 @@ param( [ValidateSet( 'DISM', 'SFC', 'OptimizeDisk', 'DiskCleanup', 'ClearUpdateCache', 'RestartSpooler', 'FlushDNS', 'ClearBrowserCache', 'RestartWinRM', - 'SetTimezone', 'SyncTime', 'UpdateEMxAuthToken', 'DeployUDCWebServerConfig', 'Reboot', + 'SetTimezone', 'SyncTime', 'UpdateDNCMXHosts', 'AuditDNCConfig', 'CheckDefectTracker', 'BackupNTLARS', 'VerifyNTLARS', 'GPUpdate', 'Reboot', + 'CopyFile', 'ImportReg', 'DeployOpenTextProfiles', 'InstallDashboard', 'InstallLobbyDisplay', 'UninstallDashboard', 'UninstallLobbyDisplay' )] [string]$Task, @@ -148,13 +202,31 @@ param( [PSCredential]$Credential, [Parameter()] - [string]$ApiUrl = "https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp", + [string]$ApiUrl = "https://tsgwp00525.wjs.geaerospace.net/shopdb/api.asp", [Parameter()] [string]$DnsSuffix = "logon.ds.ge.com", [Parameter()] - [int]$ThrottleLimit = 5 + [int]$ThrottleLimit = 5, + + [Parameter()] + [string]$SourcePath, + + [Parameter()] + [string]$DestinationPath, + + [Parameter()] + [string]$RunCommand, + + [Parameter()] + [switch]$AsSystem, + + [Parameter()] + [PSCredential]$ShareCredential, + + [Parameter()] + [switch]$LogFile ) # ============================================================================= @@ -756,271 +828,633 @@ $TaskScripts = @{ } # ------------------------------------------------------------------------- - # UpdateEMxAuthToken - Backup and prepare for file copy (runs on remote PC) - # The actual file is pushed via Copy-Item -ToSession from the caller + # CopyFile - General-purpose file deployment to remote PCs + # File is pushed to C:\Windows\Temp\ via Copy-Item -ToSession, then + # moved to the destination. Optionally runs a post-copy command as the + # logged-in user via a one-shot scheduled task. # ------------------------------------------------------------------------- - 'UpdateEMxAuthToken' = { - param($SourceFileContent) + 'CopyFile' = { + param($DestinationPath, $RunCommand, [bool]$AsSystem = $false) $result = @{ Success = $false - Task = 'UpdateEMxAuthToken' + Task = 'CopyFile' Hostname = $env:COMPUTERNAME Output = "" Error = $null - FailReason = "" - PathsUpdated = @() - PathsFailed = @() - BackupsCreated = @() - TempFile = "" - DNCKilled = $false - DNCRestarted = $false + BackupCreated = $false + CommandRan = $false } - $destFile = "eMxInfo.txt" - $tempPath = "C:\Windows\Temp\eMxInfo.txt" - - # Check both possible DNC installation paths - $destDirs = @( - "C:\Program Files (x86)\DNC\Server Files", - "C:\Program Files\DNC\Server Files" - ) + $fileName = Split-Path $DestinationPath -Leaf + $tempPath = "C:\Windows\Temp\$fileName" try { - # Check if temp file was pushed + # Verify temp file was pushed if (-not (Test-Path $tempPath)) { - $result.FailReason = "Source file not found at $tempPath - file push may have failed" - $result.Error = $result.FailReason - Write-Output $result.FailReason + $result.Error = "Source file not found at $tempPath - file push may have failed" + Write-Output $result.Error return $result } - # Track which paths exist - $validPaths = @() - foreach ($destDir in $destDirs) { - if (Test-Path $destDir) { - $validPaths += $destDir - } + # Create destination directory if needed + $destDir = Split-Path $DestinationPath -Parent + if (-not (Test-Path $destDir)) { + New-Item -Path $destDir -ItemType Directory -Force | Out-Null + Write-Output "Created directory: $destDir" } - if ($validPaths.Count -eq 0) { - $result.FailReason = "No DNC Server Files directory found in Program Files or Program Files (x86)" - $result.Error = $result.FailReason - Write-Output $result.FailReason - # Clean up temp file - Remove-Item $tempPath -Force -ErrorAction SilentlyContinue + # Backup existing file + if (Test-Path $DestinationPath) { + $dateStamp = Get-Date -Format "yyyyMMdd-HHmmss" + $ext = [System.IO.Path]::GetExtension($fileName) + $base = [System.IO.Path]::GetFileNameWithoutExtension($fileName) + $backupName = "${base}-old-${dateStamp}${ext}" + $backupPath = Join-Path $destDir $backupName + + Copy-Item -Path $DestinationPath -Destination $backupPath -Force -ErrorAction Stop + $result.BackupCreated = $true + Write-Output "Backed up existing file to: $backupName" + } + + # Move temp file to destination + Move-Item -Path $tempPath -Destination $DestinationPath -Force -ErrorAction Stop + + # Verify + if (-not (Test-Path $DestinationPath)) { + $result.Error = "File not found at destination after move" + Write-Output $result.Error return $result } - Write-Output "Found $($validPaths.Count) DNC installation(s)" + $result.Success = $true + $result.Output = "Deployed to $DestinationPath" + if ($result.BackupCreated) { $result.Output += " (backup created)" } + Write-Output $result.Output - # Kill DNCMain.exe before copying - Write-Output "Stopping DNCMain.exe..." - $dncProcess = Get-Process -Name "DNCMain" -ErrorAction SilentlyContinue - if ($dncProcess) { - try { - taskkill /IM DNCMain.exe /F 2>&1 | Out-Null - Start-Sleep -Seconds 2 - $result.DNCKilled = $true - Write-Output " DNCMain.exe stopped" - } catch { - Write-Output " Warning: Could not stop DNCMain.exe - $($_.Exception.Message)" - } - } else { - Write-Output " DNCMain.exe not running" - } - - # Process each valid path - foreach ($destDir in $validPaths) { - $destPath = Join-Path $destDir $destFile - $pathLabel = if ($destDir -like "*x86*") { "x86" } else { "x64" } - - Write-Output "Processing $pathLabel path: $destDir" - - # Check if destination file exists and back it up - if (Test-Path $destPath) { - $dateStamp = Get-Date -Format "yyyyMMdd" - $backupName = "eMxInfo-old-$dateStamp.txt" - $backupPath = Join-Path $destDir $backupName - - Write-Output " File exists, renaming to $backupName..." - - try { - # Remove existing backup if same date - if (Test-Path $backupPath) { - Remove-Item $backupPath -Force -ErrorAction Stop - } - - Rename-Item -Path $destPath -NewName $backupName -Force -ErrorAction Stop - $result.BackupsCreated += "$pathLabel`:$backupName" - } catch { - $result.PathsFailed += "$pathLabel`: Failed to rename - $($_.Exception.Message)" - Write-Output " FAILED to rename: $($_.Exception.Message)" - continue - } + # Run optional post-copy command + if ($RunCommand) { + if ($AsSystem) { + # Run directly in WinRM session (SYSTEM context) + Write-Output "Running post-copy command as SYSTEM..." + $cmdOutput = & cmd.exe /c $RunCommand 2>&1 + $result.CommandRan = $true + $result.Output += " | Post-copy command executed as SYSTEM" + Write-Output " Post-copy command completed" } else { - Write-Output " File does not exist, creating new..." - } + # Run as logged-in user via scheduled task + $loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem).UserName - # Copy from temp location to destination - try { - Copy-Item -Path $tempPath -Destination $destPath -Force -ErrorAction Stop + if ($loggedInUser) { + $taskName = "CopyFilePostCmd_$(Get-Random)" + Write-Output "Running post-copy command as $loggedInUser..." - # Verify the copy - if (Test-Path $destPath) { - $result.PathsUpdated += $pathLabel - Write-Output " SUCCESS" + $action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c $RunCommand" + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3) + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $loggedInUser -Force | Out-Null + + Start-Sleep -Seconds 8 + + $taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + + $result.CommandRan = $true + $result.Output += " | Post-copy command executed as $loggedInUser" + Write-Output " Post-copy command completed" } else { - $result.PathsFailed += "$pathLabel`: Copy succeeded but file not found" - Write-Output " FAILED: File not found after copy" - } - } catch { - $result.PathsFailed += "$pathLabel`: Failed to copy - $($_.Exception.Message)" - Write-Output " FAILED to copy: $($_.Exception.Message)" - } - } - - # Clean up temp file - Remove-Item $tempPath -Force -ErrorAction SilentlyContinue - - # Determine overall success - if ($result.PathsUpdated.Count -gt 0) { - $result.Success = $true - $result.Output = "Updated: $($result.PathsUpdated -join ', ')" - if ($result.BackupsCreated.Count -gt 0) { - $result.Output += " | Backups: $($result.BackupsCreated -join ', ')" - } - if ($result.PathsFailed.Count -gt 0) { - $result.Output += " | Failed: $($result.PathsFailed.Count)" - } - - # Restart DNC - find LDnc.exe in one of the valid paths - Write-Output "Starting LDnc.exe..." - $ldncStarted = $false - foreach ($destDir in $validPaths) { - $ldncPath = Join-Path $destDir "LDnc.exe" - if (Test-Path $ldncPath) { - try { - Start-Process -FilePath $ldncPath -ErrorAction Stop - $result.DNCRestarted = $true - $ldncStarted = $true - Write-Output " LDnc.exe started from $destDir" - break - } catch { - Write-Output " Warning: Could not start LDnc.exe from $destDir - $($_.Exception.Message)" - } + $result.Output += " | No logged-in user found, post-copy command skipped" + Write-Output " Warning: No logged-in user - post-copy command skipped" } } - if (-not $ldncStarted) { - Write-Output " Warning: LDnc.exe not found or could not be started" - } - - $result.Output += " | DNC restarted: $($result.DNCRestarted)" - } else { - $result.FailReason = "All paths failed: $($result.PathsFailed -join '; ')" - $result.Error = $result.FailReason } } catch { - $result.FailReason = "Unexpected error: $($_.Exception.Message)" - $result.Error = $result.FailReason - Write-Output $result.FailReason + $result.Error = $_.Exception.Message + Write-Output "Error: $($result.Error)" + Remove-Item $tempPath -Force -ErrorAction SilentlyContinue } return $result } # ------------------------------------------------------------------------- - # DeployUDCWebServerConfig - Deploy udc_webserver_settings.json to UDC PCs - # The actual file is pushed via Copy-Item -ToSession from the caller + # DeployOpenTextProfiles - Deploy HostExplorer config files to ProgramData + # A single zip is pushed to C:\Windows\Temp\, extracted, then files are + # copied to ProgramData destinations, overwriting existing. # ------------------------------------------------------------------------- - 'DeployUDCWebServerConfig' = { + 'DeployOpenTextProfiles' = { $result = @{ Success = $false - Task = 'DeployUDCWebServerConfig' + Task = 'DeployOpenTextProfiles' Hostname = $env:COMPUTERNAME Output = "" Error = $null - FailReason = "" - UDCInstalled = $false - BackupCreated = $false + FilesDeployed = 0 + UserProfile = "" } - $udcInstallDir = "C:\Program Files\UDC" - $destDir = "C:\ProgramData\UDC" - $destFile = "udc_webserver_settings.json" - $destPath = Join-Path $destDir $destFile - $tempPath = "C:\Windows\Temp\udc_webserver_settings.json" + $tempZip = "C:\Windows\Temp\csf.zip" + $tempExtract = "C:\Windows\Temp\csf" + $pdRoot = "C:\ProgramData\Hummingbird\Connectivity\15.00\Shared" + + # Build deploy map: ProgramData destinations + $deployMap = @( + @{ Source = "$tempExtract\Profile"; Dest = "$pdRoot\Profile" } + @{ Source = "$tempExtract\Accessories\EB"; Dest = "$pdRoot\Accessories\EB" } + @{ Source = "$tempExtract\HostExplorer\Keymap"; Dest = "$pdRoot\HostExplorer\Keymap" } + @{ Source = "$tempExtract\HostExplorer\Menu"; Dest = "$pdRoot\HostExplorer\Menu" } + ) + + # Add AppData\Roaming destinations for all user profiles + $skipUsers = @('Public', 'Default', 'Default User', 'All Users') + $userProfiles = Get-ChildItem "C:\Users" -Directory | Where-Object { + ($skipUsers -notcontains $_.Name) -and (Test-Path (Join-Path $_.FullName "AppData\Roaming")) + } + + $userCount = 0 + foreach ($profile in $userProfiles) { + $adRoot = "$($profile.FullName)\AppData\Roaming\Hummingbird\Connectivity\15.00" + $deployMap += @( + @{ Source = "$tempExtract\Profile"; Dest = "$adRoot\Profile" } + @{ Source = "$tempExtract\Accessories\EB"; Dest = "$adRoot\Accessories\EB" } + @{ Source = "$tempExtract\HostExplorer\Keymap"; Dest = "$adRoot\HostExplorer\Keymap" } + @{ Source = "$tempExtract\HostExplorer\Menu"; Dest = "$adRoot\HostExplorer\Menu" } + ) + $userCount++ + Write-Output "Found user profile: $($profile.Name)" + } + $result.UserProfile = "$userCount user(s)" + if ($userCount -eq 0) { Write-Output "WARNING: No user profiles found under C:\Users" } try { - # Check if UDC is installed - if (-not (Test-Path $udcInstallDir)) { - $result.FailReason = "UDC not installed ($udcInstallDir not found) - skipping" - $result.Output = $result.FailReason - Write-Output $result.FailReason + if (-not (Test-Path $tempZip)) { + $result.Error = "Zip file not found at $tempZip - file push may have failed" + Write-Output $result.Error return $result } - $result.UDCInstalled = $true - Write-Output "UDC installation found at $udcInstallDir" - - # Check if temp file was pushed - if (-not (Test-Path $tempPath)) { - $result.FailReason = "Source file not found at $tempPath - file push may have failed" - $result.Error = $result.FailReason - Write-Output $result.FailReason - return $result - } - - # Create destination directory if it doesn't exist - if (-not (Test-Path $destDir)) { - New-Item -Path $destDir -ItemType Directory -Force | Out-Null - Write-Output "Created directory: $destDir" - } - - # Backup existing config if present - if (Test-Path $destPath) { - $dateStamp = Get-Date -Format "yyyyMMdd" - $backupName = "udc_webserver_settings-old-$dateStamp.json" - $backupPath = Join-Path $destDir $backupName - - Write-Output "Existing config found, backing up to $backupName..." - - try { - if (Test-Path $backupPath) { - Remove-Item $backupPath -Force -ErrorAction Stop - } - Rename-Item -Path $destPath -NewName $backupName -Force -ErrorAction Stop - $result.BackupCreated = $true - } catch { - Write-Output "Warning: Could not backup existing config - $($_.Exception.Message)" - } - } - - # Copy from temp location to destination - Copy-Item -Path $tempPath -Destination $destPath -Force -ErrorAction Stop - - # Verify the copy - if (Test-Path $destPath) { - $result.Success = $true - $result.Output = "Config deployed to $destPath" - if ($result.BackupCreated) { - $result.Output += " (backup created)" - } - Write-Output "SUCCESS: $($result.Output)" + # Kill HostExplorer if running + $hostex = Get-Process -Name "hostex32" -ErrorAction SilentlyContinue + if ($hostex) { + Stop-Process -Name "hostex32" -Force -ErrorAction SilentlyContinue + Write-Output "Killed hostex32.exe" } else { - $result.FailReason = "Copy succeeded but file not found at destination" - $result.Error = $result.FailReason - Write-Output "FAILED: $($result.FailReason)" + Write-Output "hostex32.exe not running" } - # Clean up temp file - Remove-Item $tempPath -Force -ErrorAction SilentlyContinue + # Extract zip + if (Test-Path $tempExtract) { Remove-Item -Path $tempExtract -Recurse -Force } + Expand-Archive -Path $tempZip -DestinationPath $tempExtract -Force + Write-Output "Extracted csf.zip to $tempExtract" + + foreach ($mapping in $deployMap) { + if (-not (Test-Path $mapping.Source)) { continue } + + # Create destination directory if needed + if (-not (Test-Path $mapping.Dest)) { + New-Item -Path $mapping.Dest -ItemType Directory -Force | Out-Null + Write-Output "Created directory: $($mapping.Dest)" + } + + $files = Get-ChildItem -Path $mapping.Source -File + foreach ($file in $files) { + $destFile = Join-Path $mapping.Dest $file.Name + Copy-Item -Path $file.FullName -Destination $destFile -Force -ErrorAction Stop + $result.FilesDeployed++ + Write-Output " Deployed: $($file.Name) -> $($mapping.Dest)" + } + } + + # Cleanup temp files + Remove-Item -Path $tempZip -Force -ErrorAction SilentlyContinue + Remove-Item -Path $tempExtract -Recurse -Force -ErrorAction SilentlyContinue + + # Relaunch HostExplorer with WJ Shopfloor profile as logged-in user + # Must run from the user's Profile directory: hostex32.exe -P "WJ Shopfloor.hep" + $loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem).UserName + if ($loggedInUser) { + $hostexPath = "C:\Program Files\Hummingbird\Connectivity\15.00\HostExplorer\hostex32.exe" + $username = ($loggedInUser -split '\\')[-1] + $profileDir = "C:\Users\$username\AppData\Roaming\Hummingbird\Connectivity\15.00\Profile" + + if ((Test-Path $hostexPath) -and (Test-Path $profileDir)) { + $taskName = "OpenTextRelaunch_$(Get-Random)" + $action = New-ScheduledTaskAction -Execute $hostexPath -Argument "-P `"WJ Shopfloor.hep`"" -WorkingDirectory $profileDir + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3) + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $loggedInUser -Force | Out-Null + + Start-Sleep -Seconds 5 + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + + Write-Output "Relaunched hostex32.exe -P 'WJ Shopfloor.hep' as $loggedInUser from $profileDir" + } else { + Write-Output "WARNING: hostex32.exe or profile directory not found - skipping relaunch" + } + } else { + Write-Output "WARNING: No logged-in user found - skipping relaunch" + } + + $result.Success = $true + $result.Output = "Deployed $($result.FilesDeployed) files to ProgramData + $($result.UserProfile)" + Write-Output $result.Output } catch { - $result.FailReason = "Unexpected error: $($_.Exception.Message)" - $result.Error = $result.FailReason - Write-Output $result.FailReason + $result.Error = $_.Exception.Message + Write-Output "Error: $($result.Error)" + Remove-Item -Path $tempZip -Force -ErrorAction SilentlyContinue + Remove-Item -Path $tempExtract -Recurse -Force -ErrorAction SilentlyContinue + } + + return $result + } + + # ------------------------------------------------------------------------- + # ImportReg - Import a .reg file on remote PCs + # File is pushed to C:\Windows\Temp\ via Copy-Item -ToSession, then + # imported via a one-shot scheduled task running as the logged-in user + # so that both HKLM and HKCU keys are applied correctly. + # ------------------------------------------------------------------------- + 'ImportReg' = { + param($RegFileName, [bool]$AsSystem = $false) + + $result = @{ + Success = $false + Task = 'ImportReg' + Hostname = $env:COMPUTERNAME + Output = "" + Error = $null + } + + $tempRegPath = "C:\Windows\Temp\$RegFileName" + + try { + if (-not (Test-Path $tempRegPath)) { + $result.Error = "Registry file not found at $tempRegPath - file push may have failed" + Write-Output $result.Error + return $result + } + + if ($AsSystem) { + # Run directly in WinRM session (SYSTEM context) - HKLM only + Write-Output "Importing $RegFileName directly as SYSTEM..." + $regOutput = & regedit.exe /s "$tempRegPath" 2>&1 + $result.Success = $true + $result.Output = "Registry imported as SYSTEM (HKCU keys will not apply to logged-in user)" + } else { + $loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem).UserName + + if ($loggedInUser) { + # Use scheduled task as logged-in user (handles HKCU keys) + $taskName = "ImportReg_$(Get-Random)" + Write-Output "Importing $RegFileName as $loggedInUser via scheduled task..." + + $action = New-ScheduledTaskAction -Execute "regedit.exe" -Argument "/s `"$tempRegPath`"" + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3) + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $loggedInUser -Force | Out-Null + + Start-Sleep -Seconds 8 + + $taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue + $lastResult = if ($taskInfo) { $taskInfo.LastTaskResult } else { -1 } + + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + + if ($lastResult -eq 0) { + $result.Success = $true + $result.Output = "Registry imported successfully as $loggedInUser" + } else { + $result.Success = $true # Task ran, regedit /s doesn't reliably set exit codes + $result.Output = "Registry import task completed as $loggedInUser (task result: $lastResult)" + } + } else { + # No user logged in - direct import (HKLM only) + Write-Output "No logged-in user, importing directly (HKLM only)..." + $regOutput = & regedit.exe /s "$tempRegPath" 2>&1 + $result.Success = $true + $result.Output = "Registry imported directly (no logged-in user, HKCU keys will not apply)" + } + } + + Write-Output $result.Output + + } catch { + $result.Error = $_.Exception.Message + Write-Output "Error: $($result.Error)" + } finally { + Remove-Item $tempRegPath -Force -ErrorAction SilentlyContinue + } + + return $result + } + + # ------------------------------------------------------------------------- + # UpdateDNCMXHosts - Update FtpHostPrimary/Secondary in DNC\MX registry + # Checks both WOW6432Node (32-bit) and native 64-bit registry paths. + # Only updates values that match the old hostname - skips anything else. + # Safe to run on all shopfloor PCs: no-ops if DNC\MX key doesn't exist. + # ------------------------------------------------------------------------- + 'UpdateDNCMXHosts' = { + $result = @{ + Success = $false + Task = 'UpdateDNCMXHosts' + Hostname = $env:COMPUTERNAME + Output = "" + Error = $null + FailReason = "" + Changed = @() + Skipped = @() + Warnings = @() + } + + $OldHost = "tsgwp00525.us.ae.ge.com" + $NewHost = "tsgwp00525.wjs.geaerospace.net" + $ValueNames = @("FtpHostPrimary", "FtpHostSecondary") + $RegPaths = @( + "HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\MX", + "HKLM:\SOFTWARE\GE Aircraft Engines\DNC\MX" + ) + + try { + $anyKeyFound = $false + + foreach ($regPath in $RegPaths) { + if (-not (Test-Path $regPath)) { + continue + } + + $anyKeyFound = $true + + foreach ($valueName in $ValueNames) { + try { + $current = (Get-ItemProperty -Path $regPath -Name $valueName -ErrorAction Stop).$valueName + + if ($current -eq $OldHost) { + Set-ItemProperty -Path $regPath -Name $valueName -Value $NewHost -ErrorAction Stop + $result.Changed += "$valueName @ $(Split-Path $regPath -Leaf): $OldHost -> $NewHost" + } elseif ($current -eq $NewHost) { + $result.Skipped += "$valueName @ $(Split-Path $regPath -Leaf): already correct" + } else { + $result.Warnings += "$valueName @ $(Split-Path $regPath -Leaf): unexpected value '$current' - NOT modified" + } + } catch { + $result.Skipped += "$valueName @ $(Split-Path $regPath -Leaf): value not found" + } + } + } + + if (-not $anyKeyFound) { + # PC doesn't have DNC\MX at all - not an error, just not applicable + $result.Success = $true + $result.Output = "DNC\MX key not present - PC does not use MX DNC (skipped)" + return $result + } + + $result.Success = $true + $result.Output = if ($result.Changed.Count -gt 0 -and $result.Warnings.Count -gt 0) { + "Updated $($result.Changed.Count) value(s), $($result.Warnings.Count) unexpected value(s) - review output" + } elseif ($result.Changed.Count -gt 0) { + "Updated $($result.Changed.Count) value(s)" + } elseif ($result.Warnings.Count -gt 0) { + "$($result.Warnings.Count) unexpected value(s) - review output" + } else { + "No changes needed" + } + + } catch { + $result.FailReason = $_.Exception.Message + $result.Error = $result.FailReason + } + + return $result + } + + # ------------------------------------------------------------------------- + # AuditDNCConfig - Read DNC registry values for comparison with UDC backup + # Reads General, eFocas, and Hssb keys from both 32-bit and 64-bit paths. + # ------------------------------------------------------------------------- + 'AuditDNCConfig' = { + $result = @{ + Success = $false + Task = 'AuditDNCConfig' + Hostname = $env:COMPUTERNAME + Error = $null + Values = @{} + FoundDNC = $false + } + + $regRoots = @( + "HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC", + "HKLM:\SOFTWARE\GE Aircraft Engines\DNC" + ) + + try { + foreach ($root in $regRoots) { + if (Test-Path $root) { + $result.FoundDNC = $true + + # General subkey + $generalPath = Join-Path $root "General" + if (Test-Path $generalPath) { + $props = Get-ItemProperty -Path $generalPath -ErrorAction SilentlyContinue + if ($props.MachineNo) { $result.Values['General_MachineNo'] = [string]$props.MachineNo } + if ($props.NcIF) { $result.Values['General_NcIF'] = [string]$props.NcIF } + if ($null -ne $props.DualPath) { $result.Values['General_DualPath'] = [string]$props.DualPath } + } + + # eFocas subkey + $efocasPath = Join-Path $root "eFocas" + if (Test-Path $efocasPath) { + $props = Get-ItemProperty -Path $efocasPath -ErrorAction SilentlyContinue + if ($props.IpAddr) { $result.Values['eFocas_IpAddr'] = [string]$props.IpAddr } + if ($null -ne $props.SocketNo) { $result.Values['eFocas_SocketNo'] = [string]$props.SocketNo } + if ($null -ne $props.DualPath) { $result.Values['eFocas_DualPath'] = [string]$props.DualPath } + } + + # Hssb subkey + $hssbPath = Join-Path $root "Hssb" + if (Test-Path $hssbPath) { + $props = Get-ItemProperty -Path $hssbPath -ErrorAction SilentlyContinue + if ($null -ne $props.KRelay1) { $result.Values['Hssb_KRelay1'] = [string][int]$props.KRelay1 } + } + + # PPDCS subkey + $ppdcsPath = Join-Path $root "PPDCS" + if (Test-Path $ppdcsPath) { + $props = Get-ItemProperty -Path $ppdcsPath -ErrorAction SilentlyContinue + if ($null -ne $props.'CycleStart Inhibits') { $result.Values['PPDCS_CycleStartInhibits'] = [string]$props.'CycleStart Inhibits' } + } + + # Found values in this root, no need to check the other + if ($result.Values.Count -gt 0) { break } + } + } + + $result.Success = $true + } catch { + $result.Error = $_.Exception.Message + } + + return $result + } + + # ------------------------------------------------------------------------- + # CheckDefectTracker - Check if Defect_Tracker.exe is running + # ------------------------------------------------------------------------- + 'CheckDefectTracker' = { + $result = @{ + Success = $false + Task = 'CheckDefectTracker' + Hostname = $env:COMPUTERNAME + Running = $false + Output = '' + Error = $null + } + + try { + $output = & tasklist /FI "IMAGENAME eq Defect_Tracker.exe" /NH 2>&1 + $result.Output = ($output | Out-String).Trim() + $result.Running = $result.Output -match 'Defect_Tracker\.exe' + $result.Success = $true + } catch { + $result.Error = $_.Exception.Message + } + + return $result + } + + # ------------------------------------------------------------------------- + # BackupNTLARS - Export DNC registry tree to .reg content + # Returns the .reg file content + machine number + serial number. + # Uses PowerShell registry provider (Get-ItemProperty) instead of reg.exe + # because GE Group Policy blocks reg.exe on managed workstations. + # The caller writes the file to the backup share (avoids WinRM double-hop). + # ------------------------------------------------------------------------- + 'BackupNTLARS' = { + $result = @{ + Success = $false + Task = 'BackupNTLARS' + Hostname = $env:COMPUTERNAME + Error = $null + MachineNo = $null + Serial = $null + RegContent = $null + FoundDNC = $false + } + + $regRoots = @( + "HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC", + "HKLM:\SOFTWARE\GE Aircraft Engines\DNC" + ) + + try { + # Get serial number for fallback naming + $bios = Get-WmiObject Win32_BIOS -ErrorAction SilentlyContinue + if ($bios.SerialNumber -and $bios.SerialNumber -ne "To be filled by O.E.M.") { + $result.Serial = $bios.SerialNumber.Trim() + } + + # Find DNC registry root + $dncRoot = $null + foreach ($root in $regRoots) { + if (Test-Path $root) { + $dncRoot = $root + $result.FoundDNC = $true + break + } + } + + if (-not $dncRoot) { + $result.Success = $true + return $result + } + + # Read machine number from DNC\General + $generalPath = Join-Path $dncRoot "General" + if (Test-Path $generalPath) { + $props = Get-ItemProperty -Path $generalPath -ErrorAction SilentlyContinue + if ($props.MachineNo) { + $result.MachineNo = [string]$props.MachineNo + } + } + + # Build .reg file content using PowerShell registry provider + # (reg.exe is blocked by GE Group Policy) + $regLines = @() + $regLines += "Windows Registry Editor Version 5.00" + $regLines += "" + $regLines += "; NTLARS DNC Registry Backup" + $regLines += "; Computer: $env:COMPUTERNAME" + $regLines += "; Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + $regLines += "" + + # Recursive function to export a key and all subkeys + function Export-RegKey { + param([string]$Path) + + # Handle both "HKLM:\..." and "Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\..." + $regPath = $Path -replace '^Microsoft\.PowerShell\.Core\\Registry::', '' -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' + $lines = @() + $lines += "[$regPath]" + + # Export values in this key + $item = Get-Item -Path $Path -ErrorAction SilentlyContinue + if ($item) { + foreach ($valueName in $item.GetValueNames()) { + $valueData = $item.GetValue($valueName, $null, 'DoNotExpandEnvironmentNames') + $valueKind = $item.GetValueKind($valueName) + + # Format the value name + $nameStr = if ($valueName -eq '') { '@' } else { "`"$valueName`"" } + + switch ($valueKind) { + 'String' { + $escaped = [string]$valueData -replace '\\', '\\' -replace '"', '\"' + $lines += "$nameStr=`"$escaped`"" + } + 'DWord' { + $lines += "$nameStr=dword:$($valueData.ToString('x8'))" + } + 'QWord' { + $lines += "$nameStr=hex(b):$(([BitConverter]::GetBytes([long]$valueData) | ForEach-Object { $_.ToString('x2') }) -join ',')" + } + 'Binary' { + $hexStr = ($valueData | ForEach-Object { $_.ToString('x2') }) -join ',' + $lines += "$nameStr=hex:$hexStr" + } + 'MultiString' { + # Multi-string: UTF-16LE encoded, double-null terminated + $raw = [System.Text.Encoding]::Unicode.GetBytes(($valueData -join "`0") + "`0`0") + $hexStr = ($raw | ForEach-Object { $_.ToString('x2') }) -join ',' + $lines += "$nameStr=hex(7):$hexStr" + } + 'ExpandString' { + $raw = [System.Text.Encoding]::Unicode.GetBytes($valueData + "`0") + $hexStr = ($raw | ForEach-Object { $_.ToString('x2') }) -join ',' + $lines += "$nameStr=hex(2):$hexStr" + } + default { + $lines += "; Unsupported type $valueKind for $valueName" + } + } + } + } + + $lines += "" + + # Recurse into subkeys + $subKeys = Get-ChildItem -Path $Path -ErrorAction SilentlyContinue + foreach ($sub in $subKeys) { + $lines += Export-RegKey -Path $sub.PSPath + } + + return $lines + } + + $regLines += Export-RegKey -Path $dncRoot + $result.RegContent = $regLines -join "`r`n" + $result.Success = $true + } catch { + $result.Error = $_.Exception.Message } return $result @@ -1030,7 +1464,7 @@ $TaskScripts = @{ # InstallDashboard / InstallLobbyDisplay - Install kiosk app # ------------------------------------------------------------------------- 'InstallKioskApp' = { - param($InstallerPath, $AppName) + param($InstallerPath, $AppName, $KioskUrl) $result = @{ Success = $false @@ -1049,14 +1483,41 @@ $TaskScripts = @{ } Write-Output "Installing $AppName..." - $proc = Start-Process -FilePath $InstallerPath -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" -Wait -PassThru -WindowStyle Hidden + $proc = Start-Process -FilePath $InstallerPath -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" -PassThru -WindowStyle Hidden + + # Wait up to 120 seconds for installer to finish + $completed = $proc.WaitForExit(120000) # Clean up installer Remove-Item $InstallerPath -Force -ErrorAction SilentlyContinue - if ($proc.ExitCode -eq 0) { + if (-not $completed) { + $proc | Stop-Process -Force -ErrorAction SilentlyContinue + $result.Error = "Installer timed out after 120 seconds" + } elseif ($proc.ExitCode -eq 0) { $result.Success = $true $result.Output = "$AppName installed successfully" + + # Relaunch Edge kiosk via scheduled task running as the logged-in user + if ($KioskUrl) { + $edgePath = "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe" + $taskName = "RelaunchKiosk_$($AppName -replace '\s','')" + $loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem).UserName + + if ($loggedInUser) { + $action = New-ScheduledTaskAction -Execute $edgePath -Argument "-kiosk $KioskUrl --edge-kiosk-type=fullscreen" + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(5) + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $loggedInUser -Force | Out-Null + + # Wait for it to fire, then clean up + Start-Sleep -Seconds 10 + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + + $result.Output += " | Edge relaunched via scheduled task as $loggedInUser" + } else { + $result.Output += " | No logged-in user found, reboot required to relaunch Edge" + } + } } else { $result.Error = "Installer exited with code $($proc.ExitCode)" } @@ -1122,6 +1583,32 @@ $TaskScripts = @{ return $result } + # ------------------------------------------------------------------------- + # GPUpdate - Force Group Policy refresh + # ------------------------------------------------------------------------- + 'GPUpdate' = { + $result = @{ + Success = $false + Task = 'GPUpdate' + Hostname = $env:COMPUTERNAME + Output = "" + Error = $null + } + + try { + $gpOutput = & gpupdate /target:computer /force 2>&1 + $result.Output = ($gpOutput | Out-String).Trim() + $result.Success = $result.Output -match 'completed successfully' + if (-not $result.Success) { + $result.Error = $result.Output + } + } catch { + $result.Error = $_.Exception.Message + } + + return $result + } + # ------------------------------------------------------------------------- # Reboot - Restart the computer # ------------------------------------------------------------------------- @@ -1168,6 +1655,15 @@ Write-Host " Remote Maintenance Tool - Task: $Task" -ForegroundColor Cyan Write-Host "=" * 70 -ForegroundColor Cyan Write-Host "" +if ($LogFile) { + $logDir = Join-Path $PSScriptRoot "logs" + if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } + $logPath = Join-Path $logDir "maintenance-$(Get-Date -Format 'yyyy-MM-dd_HHmmss')-$Task.log" + Start-Transcript -Path $logPath -Force | Out-Null + Write-Host "Logging to: $logPath" -ForegroundColor DarkGray + Write-Host "" +} + # Get credentials if (-not $Credential) { Write-Log "Enter credentials for remote PCs:" -Level "INFO" @@ -1221,6 +1717,115 @@ Write-Log "Target computers: $($computers.Count)" -Level "INFO" Write-Log "Task: $Task" -Level "TASK" Write-Host "" +# Special handling for VerifyNTLARS - no WinRM needed, just API + file comparison +if ($Task -eq 'VerifyNTLARS') { + $backupShare = "\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\ntlars" + + # Need ShopDB data - fetch if not already loaded via -All / -PcType / -BusinessUnit + if (-not $shopfloorPCs) { + Write-Log "Fetching all shopfloor PCs from ShopDB for verification..." -Level "INFO" + $shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl + } + + # Build list of PCs that have machine numbers (these are the ones that should have backups) + $expectedMachines = @{} + foreach ($pc in $shopfloorPCs) { + if ($pc.hostname -and $pc.machinenumber) { + # Machine numbers can be comma-separated + $numbers = $pc.machinenumber -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + foreach ($num in $numbers) { + if (-not $expectedMachines.ContainsKey($num)) { + $expectedMachines[$num] = @() + } + $expectedMachines[$num] += $pc.hostname.ToUpper() + } + } + } + + Write-Log "ShopDB: $($expectedMachines.Count) unique machine numbers across $($shopfloorPCs.Count) PCs" -Level "INFO" + + # Verify share is accessible + if (-not (Test-Path $backupShare)) { + Write-Log "Cannot access backup share: $backupShare" -Level "ERROR" + exit 1 + } + + # List .reg files and extract machine numbers from filenames (prefix before first "-") + $regFiles = Get-ChildItem -Path $backupShare -Filter "*.reg" -ErrorAction SilentlyContinue + $backedUpNumbers = @{} + foreach ($file in $regFiles) { + $baseName = $file.BaseName + if ($baseName -match '^(\d+)-') { + $num = $matches[1] + $backedUpNumbers[$num] = $file.Name + } elseif ($baseName -match '^\d+$') { + # Plain number without hostname suffix (old format) + $backedUpNumbers[$baseName] = $file.Name + } + } + + Write-Log "Backup share: $($regFiles.Count) .reg files, $($backedUpNumbers.Count) unique machine numbers" -Level "INFO" + Write-Host "" + + # Compare: find machines in ShopDB without backups + $missing = @() + $found = 0 + + foreach ($num in ($expectedMachines.Keys | Sort-Object)) { + $hosts = $expectedMachines[$num] -join ', ' + if ($backedUpNumbers.ContainsKey($num)) { + Write-Log "[OK] Machine $num ($hosts) -> $($backedUpNumbers[$num])" -Level "SUCCESS" + $found++ + } else { + Write-Log "[MISSING] Machine $num ($hosts) - no backup found" -Level "WARNING" + $missing += [PSCustomObject]@{ + MachineNumber = $num + Hostnames = $hosts + } + } + } + + # Check for backups with no ShopDB match (orphans) + $orphans = @() + foreach ($num in ($backedUpNumbers.Keys | Sort-Object)) { + if (-not $expectedMachines.ContainsKey($num)) { + Write-Log "[ORPHAN] $($backedUpNumbers[$num]) - machine $num not in ShopDB" -Level "INFO" + $orphans += $backedUpNumbers[$num] + } + } + + # Summary + Write-Host "" + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " NTLARS BACKUP VERIFICATION" -ForegroundColor Cyan + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " ShopDB machines: $($expectedMachines.Count)" -ForegroundColor White + Write-Host " Backups found: $found" -ForegroundColor Green + Write-Host " Missing backups: $($missing.Count)" -ForegroundColor $(if ($missing.Count -gt 0) { "Red" } else { "Green" }) + Write-Host " Orphan backups: $($orphans.Count)" -ForegroundColor $(if ($orphans.Count -gt 0) { "Yellow" } else { "White" }) + Write-Host " Backup dir: $backupShare" -ForegroundColor White + Write-Host ("=" * 70) -ForegroundColor Cyan + + # Export missing list to CSV + if ($missing.Count -gt 0) { + $logDir = Join-Path $PSScriptRoot "logs" + if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } + $csvPath = Join-Path $logDir "VerifyNTLARS-Missing-$(Get-Date -Format 'yyyy-MM-dd_HHmmss').csv" + $missing | Export-Csv -Path $csvPath -NoTypeInformation -Force + Write-Host "" + Write-Log "Missing machines CSV: $csvPath" -Level "INFO" + Write-Log "Re-run BackupNTLARS targeting these PCs to fill gaps." -Level "INFO" + } + + if ($LogFile) { + Stop-Transcript | Out-Null + Write-Host "" + Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray + } + + exit 0 +} + # Build FQDNs $targetFQDNs = $computers | ForEach-Object { if ($_ -like "*.*") { $_ } else { "$_.$DnsSuffix" } @@ -1232,19 +1837,26 @@ $tasksToRun = @($Task) # Create session options $sessionOption = New-PSSessionOption -OpenTimeout 30000 -OperationTimeout 600000 -NoMachineProfile -# Special handling for UpdateEMxAuthToken - requires pushing file first -if ($Task -eq 'UpdateEMxAuthToken') { - $sourcePath = "\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt" - $remoteTempPath = "C:\Windows\Temp\eMxInfo.txt" - - Write-Log "UpdateEMxAuthToken: Checking source file..." -Level "INFO" - - if (-not (Test-Path $sourcePath)) { - Write-Log "Source file not found: $sourcePath" -Level "ERROR" +# Special handling for CopyFile - push file from source to destination on remote PCs +if ($Task -eq 'CopyFile') { + if (-not $SourcePath) { + Write-Log "-SourcePath is required for CopyFile task" -Level "ERROR" + exit 1 + } + if (-not $DestinationPath) { + Write-Log "-DestinationPath is required for CopyFile task" -Level "ERROR" + exit 1 + } + if (-not (Test-Path $SourcePath)) { + Write-Log "Source file not found: $SourcePath" -Level "ERROR" exit 1 } - Write-Log "Source file found. Will push to each PC before executing." -Level "INFO" + $fileName = Split-Path $SourcePath -Leaf + $remoteTempPath = "C:\Windows\Temp\$fileName" + + Write-Log "CopyFile: $fileName -> $DestinationPath" -Level "INFO" + if ($RunCommand) { Write-Log "Post-copy command: $RunCommand $(if ($AsSystem) { '(as SYSTEM)' } else { '(as logged-in user)' })" -Level "INFO" } $successCount = 0 $failCount = 0 @@ -1253,51 +1865,43 @@ if ($Task -eq 'UpdateEMxAuthToken') { Write-Host "" Write-Log "Processing: $fqdn" -Level "TASK" + if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) { + Write-Log "[FAIL] ${fqdn}: Offline or unreachable (ping timeout)" -Level "ERROR" + $failCount++ + continue + } + + $session = $null try { - # Create session $session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop - # Push the file to remote temp location Write-Log " Pushing file to remote PC..." -Level "INFO" - Copy-Item -Path $sourcePath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop + Copy-Item -Path $SourcePath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop - # Execute the scriptblock - Write-Log " Executing update task..." -Level "INFO" - $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['UpdateEMxAuthToken'] -ErrorAction Stop + Write-Log " Deploying to destination..." -Level "INFO" + $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['CopyFile'] -ArgumentList $DestinationPath, $RunCommand, [bool]$AsSystem -ErrorAction Stop - # Close session Remove-PSSession $session -ErrorAction SilentlyContinue - # Process result if ($result.Success) { - Write-Log "[OK] $($result.Hostname)" -Level "SUCCESS" - Write-Host " $($result.Output)" -ForegroundColor Gray - if ($result.PathsFailed.Count -gt 0) { - foreach ($fail in $result.PathsFailed) { - Write-Host " [!] $fail" -ForegroundColor Yellow - } - } + Write-Log "[OK] $($result.Hostname): $($result.Output)" -Level "SUCCESS" $successCount++ } else { - $errorMsg = if ($result.FailReason) { $result.FailReason } else { $result.Error } - Write-Log "[FAIL] $($result.Hostname): $errorMsg" -Level "ERROR" + Write-Log "[FAIL] $($result.Hostname): $($result.Error)" -Level "ERROR" $failCount++ } - } catch { Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR" $failCount++ - # Clean up session if it exists 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 " Task: $Task" -ForegroundColor White + Write-Host " Task: CopyFile ($fileName -> $DestinationPath)" -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 @@ -1305,64 +1909,58 @@ if ($Task -eq 'UpdateEMxAuthToken') { exit 0 } -# Special handling for DeployUDCWebServerConfig - check for UDC installation, then push config file -if ($Task -eq 'DeployUDCWebServerConfig') { - $sourcePath = "\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\UDC\udc_webserver_settings.json" - $remoteTempPath = "C:\Windows\Temp\udc_webserver_settings.json" - - Write-Log "DeployUDCWebServerConfig: Checking source file..." -Level "INFO" - - if (-not (Test-Path $sourcePath)) { - Write-Log "Source file not found: $sourcePath" -Level "ERROR" +# Special handling for ImportReg - push .reg file and import via scheduled task as logged-in user +if ($Task -eq 'ImportReg') { + if (-not $SourcePath) { + Write-Log "-SourcePath is required for ImportReg task" -Level "ERROR" exit 1 } + if (-not (Test-Path $SourcePath)) { + Write-Log "Source file not found: $SourcePath" -Level "ERROR" + exit 1 + } + if ($SourcePath -notlike "*.reg") { + Write-Log "Warning: SourcePath does not end in .reg - are you sure this is a registry file?" -Level "WARNING" + } - Write-Log "Source file found: $sourcePath" -Level "INFO" - Write-Log "Will check each PC for UDC installation before deploying." -Level "INFO" + $regFileName = Split-Path $SourcePath -Leaf + $remoteTempPath = "C:\Windows\Temp\$regFileName" + + $modeLabel = if ($AsSystem) { "as SYSTEM (HKLM only)" } else { "as logged-in user (HKCU support)" } + Write-Log "ImportReg: $regFileName ($modeLabel)" -Level "INFO" $successCount = 0 $failCount = 0 - $skippedCount = 0 foreach ($fqdn in $targetFQDNs) { Write-Host "" Write-Log "Processing: $fqdn" -Level "TASK" + if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) { + Write-Log "[FAIL] ${fqdn}: Offline or unreachable (ping timeout)" -Level "ERROR" + $failCount++ + continue + } + + $session = $null try { - # Create session $session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop - # Check if UDC is installed before pushing the file - $udcInstalled = Invoke-Command -Session $session -ScriptBlock { Test-Path "C:\Program Files\UDC" } -ErrorAction Stop + Write-Log " Pushing .reg file to remote PC..." -Level "INFO" + Copy-Item -Path $SourcePath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop - if (-not $udcInstalled) { - Write-Log "[SKIP] $fqdn - UDC not installed" -Level "INFO" - Remove-PSSession $session -ErrorAction SilentlyContinue - $skippedCount++ - continue - } + Write-Log " Importing registry file..." -Level "INFO" + $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['ImportReg'] -ArgumentList $regFileName, [bool]$AsSystem -ErrorAction Stop - # Push the config file to remote temp location - Write-Log " UDC installed - pushing config file..." -Level "INFO" - Copy-Item -Path $sourcePath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop - - # Execute the scriptblock - Write-Log " Executing deploy task..." -Level "INFO" - $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['DeployUDCWebServerConfig'] -ErrorAction Stop - - # Close session Remove-PSSession $session -ErrorAction SilentlyContinue - # Process result if ($result.Success) { Write-Log "[OK] $($result.Hostname): $($result.Output)" -Level "SUCCESS" $successCount++ } else { - $errorMsg = if ($result.FailReason) { $result.FailReason } else { $result.Error } - Write-Log "[FAIL] $($result.Hostname): $errorMsg" -Level "ERROR" + Write-Log "[FAIL] $($result.Hostname): $($result.Error)" -Level "ERROR" $failCount++ } - } catch { Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR" $failCount++ @@ -1370,14 +1968,78 @@ if ($Task -eq 'DeployUDCWebServerConfig') { } } - # Summary Write-Host "" Write-Host ("=" * 70) -ForegroundColor Cyan Write-Host " SUMMARY" -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan - Write-Host " Task: $Task" -ForegroundColor White + Write-Host " Task: ImportReg ($regFileName)" -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 + + exit 0 +} + +# Special handling for DeployOpenTextProfiles - zip, push single file, extract and deploy +if ($Task -eq 'DeployOpenTextProfiles') { + $localZipPath = "\\tsgwp00525.wjs.geaerospace.net\shared\dt\csf.zip" + $remoteZipPath = "C:\Windows\Temp\csf.zip" + + # Verify zip exists on network share + if (-not (Test-Path $localZipPath)) { + Write-Log "OpenText csf.zip not found: $localZipPath" -Level "ERROR" + exit 1 + } + + $zipSize = [math]::Round((Get-Item $localZipPath).Length / 1KB, 1) + Write-Log "DeployOpenTextProfiles: csf.zip (${zipSize} KB) from $localZipPath" -Level "INFO" + + $successCount = 0 + $failCount = 0 + + foreach ($fqdn in $targetFQDNs) { + Write-Host "" + Write-Log "Processing: $fqdn" -Level "TASK" + + if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) { + Write-Log "[FAIL] ${fqdn}: Offline or unreachable (ping timeout)" -Level "ERROR" + $failCount++ + continue + } + + $session = $null + try { + $session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop + + Write-Log " Pushing csf.zip to remote temp..." -Level "INFO" + Copy-Item -Path $localZipPath -Destination $remoteZipPath -ToSession $session -Force -ErrorAction Stop + + Write-Log " Extracting and deploying to ProgramData..." -Level "INFO" + $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['DeployOpenTextProfiles'] -ErrorAction Stop + + Remove-PSSession $session -ErrorAction SilentlyContinue + + if ($result.Success) { + Write-Log "[OK] $($result.Hostname): $($result.Output)" -Level "SUCCESS" + $successCount++ + } else { + Write-Log "[FAIL] $($result.Hostname): $($result.Error)" -Level "ERROR" + $failCount++ + } + } catch { + Write-Log "[FAIL] ${fqdn}: $($_.Exception.Message)" -Level "ERROR" + $failCount++ + if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue } + } + } + + Write-Host "" + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " SUMMARY" -ForegroundColor Cyan + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " Task: DeployOpenTextProfiles" -ForegroundColor White + Write-Host " Source: $localZipPath (${zipSize} KB)" -ForegroundColor White Write-Host " Successful: $successCount" -ForegroundColor Green - Write-Host " Skipped: $skippedCount (UDC not installed)" -ForegroundColor Yellow Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" }) Write-Host ("=" * 70) -ForegroundColor Cyan @@ -1388,17 +2050,19 @@ if ($Task -eq 'DeployUDCWebServerConfig') { $KioskAppConfig = @{ 'InstallDashboard' = @{ Action = 'Install' - InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe' + InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe' InstallerName = 'GEAerospaceDashboardSetup.exe' AppName = 'GE Aerospace Dashboard' UninstallGuid = '{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}' + KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/shopfloor-dashboard/' } 'InstallLobbyDisplay' = @{ Action = 'Install' - InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe' + InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe' InstallerName = 'GEAerospaceLobbyDisplaySetup.exe' AppName = 'GE Aerospace Lobby Display' UninstallGuid = '{42FFB952-0B72-493F-8869-D957344CA305}' + KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/tv-dashboard/' } 'UninstallDashboard' = @{ Action = 'Uninstall' @@ -1438,6 +2102,13 @@ if ($KioskAppConfig.ContainsKey($Task)) { Write-Host "" Write-Log "Processing: $fqdn" -Level "TASK" + # Quick connectivity check before attempting session + if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) { + Write-Log "[FAIL] ${fqdn}: Offline or unreachable (ping timeout)" -Level "ERROR" + $failCount++ + continue + } + try { $session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop @@ -1448,7 +2119,7 @@ if ($KioskAppConfig.ContainsKey($Task)) { Copy-Item -Path $installerPath -Destination $remoteTempPath -ToSession $session -Force -ErrorAction Stop Write-Log " Running installer silently..." -Level "INFO" - $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['InstallKioskApp'] -ArgumentList $remoteTempPath, $appConfig.AppName -ErrorAction Stop + $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['InstallKioskApp'] -ArgumentList $remoteTempPath, $appConfig.AppName, $appConfig.KioskUrl -ErrorAction Stop } else { Write-Log " Running uninstaller..." -Level "INFO" $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['UninstallKioskApp'] -ArgumentList $appConfig.UninstallGuid, $appConfig.AppName -ErrorAction Stop @@ -1486,6 +2157,431 @@ if ($KioskAppConfig.ContainsKey($Task)) { exit 0 } +# Special handling for CheckDefectTracker - report Defect_Tracker.exe process status +if ($Task -eq 'CheckDefectTracker') { + # Fetch shopfloor PCs for machine number lookup + if (-not $shopfloorPCs) { + Write-Log "Fetching shopfloor PC list from API for machine number lookup..." -Level "INFO" + $shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl + } + + # Build hostname -> machine number lookup + $machineLookup = @{} + foreach ($pc in $shopfloorPCs) { + if ($pc.hostname -and $pc.machinenumber) { + $machineLookup[$pc.hostname.ToUpper()] = $pc.machinenumber + } + } + + $checkResults = @() + $totalChecked = 0 + $runningCount = 0 + $notRunningCount = 0 + $offlineCount = 0 + + foreach ($fqdn in $targetFQDNs) { + Write-Host "" + Write-Log "Processing: $fqdn" -Level "TASK" + + $hostname = ($fqdn -split '\.')[0].ToUpper() + $machineNumber = $machineLookup[$hostname] + + # Connectivity check + if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) { + Write-Log "[OFFLINE] ${hostname}: Unreachable" -Level "WARNING" + $offlineCount++ + $checkResults += [PSCustomObject]@{ + Hostname = $hostname + MachineNumber = $machineNumber + Running = 'N/A' + Status = 'Offline' + } + continue + } + + # Open WinRM session and invoke scriptblock + $session = $null + try { + $session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop + $result = Invoke-Command -Session $session -ScriptBlock $TaskScripts['CheckDefectTracker'] -ErrorAction Stop + Remove-PSSession $session -ErrorAction SilentlyContinue + + $totalChecked++ + $runningStr = if ($result.Running) { 'Yes' } else { 'No' } + + if ($result.Running) { + Write-Log "[RUNNING] ${hostname} (Machine: $machineNumber): Defect_Tracker.exe is running" -Level "SUCCESS" + $runningCount++ + } else { + Write-Log "[NOT RUNNING] ${hostname} (Machine: $machineNumber): Defect_Tracker.exe is NOT running" -Level "ERROR" + $notRunningCount++ + } + + $checkResults += [PSCustomObject]@{ + Hostname = $hostname + MachineNumber = $machineNumber + Running = $runningStr + Status = 'OK' + } + } catch { + Write-Log "[FAIL] ${hostname}: $($_.Exception.Message)" -Level "ERROR" + if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue } + $offlineCount++ + $checkResults += [PSCustomObject]@{ + Hostname = $hostname + MachineNumber = $machineNumber + Running = 'N/A' + Status = 'Offline' + } + } + } + + # Save CSV report + $logDir = Join-Path $PSScriptRoot "logs" + if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } + $csvPath = Join-Path $logDir "CheckDefectTracker-$(Get-Date -Format 'yyyy-MM-dd_HHmmss').csv" + if ($checkResults.Count -gt 0) { + $checkResults | Export-Csv -Path $csvPath -NoTypeInformation -Force + Write-Host "" + Write-Log "CSV report saved: $csvPath" -Level "INFO" + } + + # Summary + Write-Host "" + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " DEFECT TRACKER CHECK SUMMARY" -ForegroundColor Cyan + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " PCs checked: $totalChecked" -ForegroundColor White + Write-Host " Running: $runningCount" -ForegroundColor $(if ($runningCount -gt 0) { "Green" } else { "White" }) + Write-Host " Not running: $notRunningCount" -ForegroundColor $(if ($notRunningCount -gt 0) { "Red" } else { "White" }) + Write-Host " Offline: $offlineCount" -ForegroundColor $(if ($offlineCount -gt 0) { "Yellow" } else { "White" }) + Write-Host ("=" * 70) -ForegroundColor Cyan + + if ($LogFile) { + Stop-Transcript | Out-Null + Write-Host "" + Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray + } + + exit 0 +} + +# Special handling for AuditDNCConfig - compare DNC registry vs UDC backup JSON +if ($Task -eq 'AuditDNCConfig') { + $udcBackupRoot = "\\tsgwp00525.wjs.geaerospace.net\shared\spc\udc\settings_backups" + + # We need the shopfloorPCs data for equipment numbers. + # If we already fetched it via -All, -PcType, or -BusinessUnit, reuse it. + # If targeting by -ComputerName or -ComputerListFile, fetch all shopfloor PCs to build the lookup. + if (-not $shopfloorPCs) { + Write-Log "Fetching shopfloor PC list from API for equipment number lookup..." -Level "INFO" + $shopfloorPCs = Get-ShopfloorPCsFromApi -ApiUrl $ApiUrl + } + + # Build hostname -> equipment_numbers lookup + $equipmentLookup = @{} + foreach ($pc in $shopfloorPCs) { + if ($pc.hostname -and $pc.machinenumber) { + $equipmentLookup[$pc.hostname.ToUpper()] = $pc.machinenumber + } + } + + # Field mapping: Registry key -> UDC JSON path + # Format: RegistryKey = @{ UDCPath; DisplayName } + $fieldMappings = @( + @{ RegKey = 'General_MachineNo'; UDCPath = 'GeneralSettings.MachineNumber'; Display = 'MachineNo/MachineNumber' } + @{ RegKey = 'General_NcIF'; UDCPath = 'GeneralSettings.ControlType'; Display = 'NcIF/ControlType' } + @{ RegKey = 'eFocas_IpAddr'; UDCPath = 'FocasSettings.IPAddress'; Display = 'eFocas IpAddr/FocasSettings.IPAddress' } + @{ RegKey = 'eFocas_SocketNo'; UDCPath = 'FocasSettings.Port'; Display = 'eFocas SocketNo/FocasSettings.Port' } + @{ RegKey = 'Hssb_KRelay1'; UDCPath = 'FocasSettings.KeepRelay'; Display = 'Hssb KRelay1/FocasSettings.KeepRelay' } + @{ RegKey = 'PPDCS_CycleStartInhibits'; UDCPath = 'FocasSettings.InhibitOnOff'; Display = 'PPDCS CycleStart Inhibits/FocasSettings.InhibitOnOff' } + @{ RegKey = 'eFocas_DualPath'; UDCPath = 'GeneralSettings.DualPath'; Display = 'eFocas DualPath/GeneralSettings.DualPath' } + ) + + $auditResults = @() + $pcChecked = 0 + $pcMismatch = 0 + $pcNoUDC = 0 + $pcOffline = 0 + $pcNoEquipment = 0 + + foreach ($fqdn in $targetFQDNs) { + Write-Host "" + Write-Log "Processing: $fqdn" -Level "TASK" + + # Extract hostname from FQDN + $hostname = ($fqdn -split '\.')[0].ToUpper() + + # Look up equipment numbers for this PC + $equipmentStr = $equipmentLookup[$hostname] + if (-not $equipmentStr) { + Write-Log "[SKIP] ${hostname}: No equipment number in ShopDB" -Level "WARNING" + $pcNoEquipment++ + continue + } + + # Split comma-separated equipment numbers + $machineNumbers = $equipmentStr -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + + # Connectivity check + if (-not (Test-Connection -ComputerName $fqdn -Count 1 -Quiet -ErrorAction SilentlyContinue)) { + Write-Log "[FAIL] ${hostname}: Offline or unreachable" -Level "ERROR" + $pcOffline++ + continue + } + + # Get registry values from remote PC + $regResult = $null + $session = $null + try { + $session = New-PSSession -ComputerName $fqdn -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate -ErrorAction Stop + $regResult = Invoke-Command -Session $session -ScriptBlock $TaskScripts['AuditDNCConfig'] -ErrorAction Stop + Remove-PSSession $session -ErrorAction SilentlyContinue + } catch { + Write-Log "[FAIL] ${hostname}: $($_.Exception.Message)" -Level "ERROR" + if ($session) { Remove-PSSession $session -ErrorAction SilentlyContinue } + $pcOffline++ + continue + } + + if (-not $regResult.Success) { + Write-Log "[FAIL] ${hostname}: Registry read error - $($regResult.Error)" -Level "ERROR" + $pcOffline++ + continue + } + + if (-not $regResult.FoundDNC) { + Write-Log "[SKIP] ${hostname}: No DNC registry keys found" -Level "INFO" + $pcNoEquipment++ + continue + } + + $pcChecked++ + $pcHasMismatch = $false + + foreach ($machineNum in $machineNumbers) { + # Read UDC backup JSON + $udcJsonPath = Join-Path $udcBackupRoot "udc_settings_${machineNum}.json" + $udcData = $null + + if (Test-Path $udcJsonPath) { + try { + $udcData = Get-Content $udcJsonPath -Raw | ConvertFrom-Json + } catch { + Write-Log " [WARN] Could not parse UDC JSON for machine $machineNum" -Level "WARNING" + } + } else { + Write-Log " [WARN] No UDC backup found for machine ${machineNum}: $udcJsonPath" -Level "WARNING" + $pcNoUDC++ + } + + # Compare each field + foreach ($mapping in $fieldMappings) { + $regValue = $regResult.Values[$mapping.RegKey] + + # Get UDC value by navigating the JSON path + $udcValue = $null + if ($udcData) { + $parts = $mapping.UDCPath -split '\.' + $obj = $udcData + foreach ($part in $parts) { + if ($obj) { $obj = $obj.$part } + } + if ($null -ne $obj) { $udcValue = [string]$obj } + } + + # Normalize values for comparison + if ($mapping.RegKey -like '*DualPath*' -and $regValue) { + # DualPath: registry YES/NO -> True/False + $regValueNormalized = if ($regValue -eq 'YES') { 'True' } elseif ($regValue -eq 'NO') { 'False' } else { $regValue } + } elseif ($mapping.RegKey -eq 'PPDCS_CycleStartInhibits' -and $regValue) { + # CycleStart Inhibits: registry YES/NO -> True/False + $regValueNormalized = if ($regValue -eq 'YES') { 'True' } elseif ($regValue -eq 'NO') { 'False' } else { $regValue } + } else { + $regValueNormalized = $regValue + } + + # Determine status + $status = if ($null -eq $regValue -and $null -eq $udcValue) { + continue # Both absent, skip + } elseif ($null -eq $regValue) { + 'REGISTRY_MISSING' + } elseif ($null -eq $udcValue -and -not $udcData) { + 'UDC_FILE_MISSING' + } elseif ($null -eq $udcValue) { + 'UDC_MISSING' + } elseif ($regValueNormalized -eq $udcValue) { + 'MATCH' + } else { + 'MISMATCH' + } + + $row = [PSCustomObject]@{ + Hostname = $hostname + MachineNumber = $machineNum + Field = $mapping.Display + RegistryValue = if ($regValue) { $regValue } else { '' } + UDCValue = if ($udcValue) { $udcValue } else { '' } + Status = $status + } + $auditResults += $row + + # Console output + $color = switch ($status) { + 'MATCH' { 'Green' } + 'MISMATCH' { 'Red' } + 'REGISTRY_MISSING' { 'Yellow' } + 'UDC_MISSING' { 'Yellow' } + 'UDC_FILE_MISSING' { 'DarkYellow' } + default { 'Gray' } + } + if ($status -ne 'MATCH') { + Write-Host " [$status] $($mapping.Display): Registry='$regValue' UDC='$udcValue'" -ForegroundColor $color + $pcHasMismatch = $true + } + } + } + + if ($pcHasMismatch) { + Write-Log "[MISMATCH] ${hostname}: Differences found (see above)" -Level "WARNING" + $pcMismatch++ + } else { + Write-Log "[OK] ${hostname}: All fields match" -Level "SUCCESS" + } + } + + # Save CSV report + $logDir = Join-Path $PSScriptRoot "logs" + if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } + $csvPath = Join-Path $logDir "AuditDNCConfig-$(Get-Date -Format 'yyyy-MM-dd_HHmmss').csv" + if ($auditResults.Count -gt 0) { + $auditResults | Export-Csv -Path $csvPath -NoTypeInformation -Force + Write-Host "" + Write-Log "CSV report saved: $csvPath" -Level "INFO" + } + + # Summary + Write-Host "" + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " AUDIT SUMMARY" -ForegroundColor Cyan + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " PCs checked: $pcChecked" -ForegroundColor White + Write-Host " PCs with mismatches: $pcMismatch" -ForegroundColor $(if ($pcMismatch -gt 0) { "Red" } else { "Green" }) + Write-Host " PCs with no UDC backup: $pcNoUDC" -ForegroundColor $(if ($pcNoUDC -gt 0) { "Yellow" } else { "White" }) + Write-Host " PCs offline: $pcOffline" -ForegroundColor $(if ($pcOffline -gt 0) { "Red" } else { "White" }) + Write-Host " PCs no equipment #: $pcNoEquipment" -ForegroundColor $(if ($pcNoEquipment -gt 0) { "Yellow" } else { "White" }) + Write-Host ("=" * 70) -ForegroundColor Cyan + + if ($LogFile) { + Stop-Transcript | Out-Null + Write-Host "" + Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray + } + + exit 0 +} + +# Special handling for BackupNTLARS - export DNC registry from each PC, save to share +if ($Task -eq 'BackupNTLARS') { + $backupShare = "\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\ntlars" + + # Verify share is accessible + if (-not (Test-Path $backupShare)) { + Write-Log "Cannot access backup share: $backupShare" -Level "ERROR" + Write-Log "Ensure you have access to \\tsgwp00525 and the ntlars folder exists." -Level "ERROR" + exit 1 + } + + # Run scriptblock on all PCs in parallel + Write-Log "Collecting DNC registry from $($targetFQDNs.Count) PC(s) in parallel..." -Level "INFO" + + $invokeParams = @{ + ComputerName = $targetFQDNs + ScriptBlock = $TaskScripts['BackupNTLARS'] + Credential = $Credential + SessionOption = $sessionOption + ErrorAction = 'SilentlyContinue' + ErrorVariable = 'remoteErrors' + } + + if ($ThrottleLimit -and $PSVersionTable.PSVersion.Major -ge 7) { + $invokeParams.ThrottleLimit = $ThrottleLimit + } + + $results = Invoke-Command @invokeParams + + # Write results to share + $backedUp = 0 + $skipped = 0 + $failed = 0 + + foreach ($regResult in $results) { + $hostname = $regResult.Hostname.ToUpper() + + if (-not $regResult.Success) { + Write-Log "[FAIL] ${hostname}: $($regResult.Error)" -Level "ERROR" + $failed++ + continue + } + + if (-not $regResult.FoundDNC) { + Write-Log "[SKIP] ${hostname}: No DNC registry keys found" -Level "INFO" + $skipped++ + continue + } + + # Determine filename: machine number or serial, always include hostname to avoid collisions + if ($regResult.MachineNo) { + $fileName = "$($regResult.MachineNo)-$hostname.reg" + $label = "Machine $($regResult.MachineNo)" + } elseif ($regResult.Serial) { + $fileName = "$($regResult.Serial)-$hostname.reg" + $label = "Serial $($regResult.Serial) (no machine number)" + } else { + $fileName = "$hostname.reg" + $label = "Hostname $hostname (no machine number or serial)" + } + + $destPath = Join-Path $backupShare $fileName + + try { + $regResult.RegContent | Out-File -FilePath $destPath -Encoding Unicode -Force + $fileSize = [math]::Round((Get-Item $destPath).Length / 1KB, 1) + Write-Log "[OK] ${hostname}: $label -> $fileName (${fileSize} KB)" -Level "SUCCESS" + $backedUp++ + } catch { + Write-Log "[FAIL] ${hostname}: Failed to write $destPath - $($_.Exception.Message)" -Level "ERROR" + $failed++ + } + } + + # Handle connection errors + foreach ($err in $remoteErrors) { + $target = if ($err.TargetObject) { $err.TargetObject } else { "Unknown" } + Write-Log "[FAIL] ${target}: $($err.Exception.Message)" -Level "ERROR" + $failed++ + } + + # Summary + Write-Host "" + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " NTLARS BACKUP SUMMARY" -ForegroundColor Cyan + Write-Host ("=" * 70) -ForegroundColor Cyan + Write-Host " Backed up: $backedUp" -ForegroundColor Green + Write-Host " Skipped: $skipped (no DNC keys)" -ForegroundColor $(if ($skipped -gt 0) { "Yellow" } else { "White" }) + Write-Host " Failed: $failed" -ForegroundColor $(if ($failed -gt 0) { "Red" } else { "White" }) + Write-Host " Backup dir: $backupShare" -ForegroundColor White + Write-Host ("=" * 70) -ForegroundColor Cyan + + if ($LogFile) { + Stop-Transcript | Out-Null + Write-Host "" + Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray + } + + exit 0 +} + # Process each task foreach ($currentTask in $tasksToRun) { @@ -1544,12 +2640,16 @@ foreach ($currentTask in $tasksToRun) { 'DiskCleanup' { Write-Host " Space freed: $($result.SpaceFreed) GB" -ForegroundColor Gray } - 'UpdateEMxAuthToken' { + 'UpdateDNCMXHosts' { Write-Host " $($result.Output)" -ForegroundColor Gray - if ($result.PathsFailed.Count -gt 0) { - foreach ($fail in $result.PathsFailed) { - Write-Host " [!] $fail" -ForegroundColor Yellow - } + foreach ($change in $result.Changed) { + Write-Host " [changed] $change" -ForegroundColor Green + } + foreach ($warn in $result.Warnings) { + Write-Host " [warn] $warn" -ForegroundColor Yellow + } + foreach ($skip in $result.Skipped) { + Write-Host " [skip] $skip" -ForegroundColor DarkGray } } default { @@ -1585,3 +2685,9 @@ Write-Host " Task: $Task" -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 + +if ($LogFile) { + Stop-Transcript | Out-Null + Write-Host "" + Write-Host "Log saved to: $logPath" -ForegroundColor DarkGray +} diff --git a/remote-execution/Resume-Download.bat b/remote-execution/Resume-Download.bat new file mode 100644 index 0000000..18f871e --- /dev/null +++ b/remote-execution/Resume-Download.bat @@ -0,0 +1,18 @@ +@echo off +:: Resume-Download — Resumable download for SharePoint and general URLs +:: Uses domain credentials for SharePoint auth + +if "%~1"=="" ( + set /p "URL=Enter download URL: " +) else ( + set "URL=%~1" +) + +if "%~2"=="" ( + set /p "DEST=Save as (full path, e.g. C:\Temp\file.iso): " +) else ( + set "DEST=%~2" +) + +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Resume-Download.ps1" -Url "%URL%" -Destination "%DEST%" +pause diff --git a/remote-execution/Resume-Download.ps1 b/remote-execution/Resume-Download.ps1 new file mode 100644 index 0000000..d920c1d --- /dev/null +++ b/remote-execution/Resume-Download.ps1 @@ -0,0 +1,117 @@ +param( + [Parameter(Mandatory=$true)] + [string]$Url, + + [Parameter(Mandatory=$true)] + [string]$Destination +) + +$ErrorActionPreference = 'Stop' + +Write-Host "============================================" +Write-Host " Resumable File Download" +Write-Host "============================================" +Write-Host "" +Write-Host "Destination: $Destination" +Write-Host "" + +try { + # Check for partial file to resume + $startBytes = 0 + if (Test-Path $Destination) { + $startBytes = (Get-Item $Destination).Length + if ($startBytes -gt 0) { + Write-Host "Partial file found: $([math]::Round($startBytes / 1MB, 1)) MB already downloaded" + Write-Host "Resuming..." + Write-Host "" + } + } + + # Ensure destination directory exists + $destDir = Split-Path $Destination -Parent + if ($destDir -and !(Test-Path $destDir)) { + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + } + + # Build headers + $headers = @{} + if ($startBytes -gt 0) { + $headers["Range"] = "bytes=$startBytes-" + } + + # Use Invoke-WebRequest with domain credentials for SharePoint auth + $params = @{ + Uri = $Url + OutFile = $Destination + UseDefaultCredentials = $true + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + Headers = $headers + UseBasicParsing = $true + } + + # If resuming, we need to handle appending manually + if ($startBytes -gt 0) { + # Download to a temp file, then append + $tempFile = "$Destination.partial" + $params.OutFile = $tempFile + + Write-Host "Downloading..." + Invoke-WebRequest @params + + # Check if we got actual content + $tempSize = (Get-Item $tempFile).Length + if ($tempSize -eq 0) { + Remove-Item $tempFile -Force + Write-Host "" + Write-Host "WARNING: Server returned 0 bytes. The URL may have expired." + Write-Host "Get a fresh SharePoint link and try again." + exit 1 + } + + # Append to existing file + $existingBytes = [System.IO.File]::ReadAllBytes($Destination) + $newBytes = [System.IO.File]::ReadAllBytes($tempFile) + $combined = New-Object byte[] ($existingBytes.Length + $newBytes.Length) + [System.Buffer]::BlockCopy($existingBytes, 0, $combined, 0, $existingBytes.Length) + [System.Buffer]::BlockCopy($newBytes, 0, $combined, $existingBytes.Length, $newBytes.Length) + [System.IO.File]::WriteAllBytes($Destination, $combined) + Remove-Item $tempFile -Force + + $totalMB = [math]::Round(($existingBytes.Length + $newBytes.Length) / 1MB, 1) + Write-Host "" + Write-Host "Download complete: $totalMB MB saved to $Destination" + } + else { + Write-Host "Downloading..." + Invoke-WebRequest @params + + # Verify we got actual content + $fileSize = (Get-Item $Destination).Length + if ($fileSize -eq 0) { + Remove-Item $Destination -Force + Write-Host "" + Write-Host "WARNING: Downloaded 0 bytes. Possible causes:" + Write-Host " - SharePoint URL expired or requires browser login" + Write-Host " - URL is a redirect/login page, not the actual file" + Write-Host "" + Write-Host "Try this: In Edge, start the download, then go to" + Write-Host "edge://downloads and copy the source URL from there." + exit 1 + } + + $totalMB = [math]::Round($fileSize / 1MB, 1) + Write-Host "" + Write-Host "Download complete: $totalMB MB saved to $Destination" + } + +} catch { + Write-Host "" + Write-Host "Error: $_" + Write-Host "" + if (Test-Path $Destination) { + $partialMB = [math]::Round((Get-Item $Destination).Length / 1MB, 1) + Write-Host "Partial file kept: $partialMB MB" + } + Write-Host "Re-run with the same arguments to resume." + exit 1 +} diff --git a/remote-execution/Run-UpdateDNCMXHosts.bat b/remote-execution/Run-UpdateDNCMXHosts.bat new file mode 100644 index 0000000..79895c7 --- /dev/null +++ b/remote-execution/Run-UpdateDNCMXHosts.bat @@ -0,0 +1,32 @@ +@echo off +REM Run-UpdateDNCMXHosts.bat +REM Updates FtpHostPrimary/Secondary in DNC\MX registry on remote shopfloor PCs +REM Usage: +REM Run-UpdateDNCMXHosts.bat -> runs on all PCs in shopfloor-pcs.txt +REM Run-UpdateDNCMXHosts.bat G5N9PWM3ESF -> runs on a single PC (for testing) + +echo ============================================================ +echo UpdateDNCMXHosts - FTP Host Migration +echo tsgwp00525.us.ae.ge.com -^> tsgwp00525.wjs.geaerospace.net +echo ============================================================ +echo. + +if not "%~1"=="" ( + echo Target: %~1 + echo. + powershell.exe -ExecutionPolicy Bypass -File "%~dp0Invoke-RemoteMaintenance.ps1" -ComputerName "%~1" -Task UpdateDNCMXHosts -LogFile +) else ( + if not exist "%~dp0shopfloor-pcs.txt" ( + echo ERROR: shopfloor-pcs.txt not found in %~dp0 + echo Either place shopfloor-pcs.txt in the same folder or pass a PC name: + echo Run-UpdateDNCMXHosts.bat G5N9PWM3ESF + pause + exit /b 1 + ) + echo Target: shopfloor-pcs.txt + echo. + powershell.exe -ExecutionPolicy Bypass -File "%~dp0Invoke-RemoteMaintenance.ps1" -ComputerListFile "%~dp0shopfloor-pcs.txt" -Task UpdateDNCMXHosts -LogFile +) + +echo. +pause diff --git a/remote-execution/Schedule-Maintenance.ps1 b/remote-execution/Schedule-Maintenance.ps1 index 76c9854..4f02b3b 100644 --- a/remote-execution/Schedule-Maintenance.ps1 +++ b/remote-execution/Schedule-Maintenance.ps1 @@ -68,11 +68,41 @@ param( [string]$Password ) -$credFile = Join-Path $PSScriptRoot ".maintenance-cred.xml" +$credDir = Join-Path $PSScriptRoot ".creds" +$keyFile = Join-Path $credDir "aes.key" +$userFile = Join-Path $credDir "username.txt" +$passFile = Join-Path $credDir "password.txt" $scriptDir = $PSScriptRoot # --------------------------------------------------------------------------- -# Save credentials (DPAPI - only decryptable by this user on this machine) +# Helper: load or create AES key (works across all user contexts on this PC) +# --------------------------------------------------------------------------- +function Get-AESKey { + if (-not (Test-Path $credDir)) { + New-Item -Path $credDir -ItemType Directory -Force | Out-Null + } + if (Test-Path $keyFile) { + return [byte[]](Get-Content $keyFile) + } + $key = New-Object byte[] 32 + [System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($key) + $key | Set-Content $keyFile + return $key +} + +function Load-SavedCredential { + if (-not (Test-Path $userFile) -or -not (Test-Path $passFile)) { + return $null + } + $key = Get-AESKey + $user = Get-Content $userFile + $encPass = Get-Content $passFile + $secPass = $encPass | ConvertTo-SecureString -Key $key + return New-Object System.Management.Automation.PSCredential($user, $secPass) +} + +# --------------------------------------------------------------------------- +# Save credentials (AES key file - works from any user context on this PC) # --------------------------------------------------------------------------- if ($SaveCredential) { if ($Username -and $Password) { @@ -87,9 +117,11 @@ if ($SaveCredential) { Write-Host "No credentials provided. Exiting." -ForegroundColor Red exit 1 } - $cred | Export-Clixml -Path $credFile - Write-Host "Credentials saved to: $credFile" -ForegroundColor Green - Write-Host "Encrypted with DPAPI - only YOUR user account on THIS machine can decrypt them." -ForegroundColor Yellow + $key = Get-AESKey + $cred.UserName | Set-Content $userFile + $cred.Password | ConvertFrom-SecureString -Key $key | Set-Content $passFile + Write-Host "Credentials saved to: $credDir" -ForegroundColor Green + Write-Host "Encrypted with AES-256 key - works from any user context (normal or admin)." -ForegroundColor Yellow Write-Host "" Write-Host "You can now run tasks unattended:" -ForegroundColor Cyan Write-Host " .\Schedule-Maintenance.ps1 -ComputerListFile '.\shopfloor-pcs.txt' -Task Reboot" @@ -113,7 +145,7 @@ if ($CreateScheduledTask) { $absListFile = (Resolve-Path $ComputerListFile -ErrorAction Stop).Path $absScript = Join-Path $scriptDir "Schedule-Maintenance.ps1" - if (-not (Test-Path $credFile)) { + if (-not (Test-Path $passFile)) { Write-Host "ERROR: No saved credentials found. Run with -SaveCredential first." -ForegroundColor Red exit 1 } @@ -177,20 +209,13 @@ if (-not $ComputerListFile) { } # Load saved credentials -if (-not (Test-Path $credFile)) { - Write-Host "ERROR: No saved credentials found at $credFile" -ForegroundColor Red +$cred = Load-SavedCredential +if (-not $cred) { + Write-Host "ERROR: No saved credentials found in $credDir" -ForegroundColor Red Write-Host "Run with -SaveCredential first to store credentials." -ForegroundColor Yellow exit 1 } - -try { - $cred = Import-Clixml -Path $credFile - Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green -} catch { - Write-Host "ERROR: Failed to load credentials: $_" -ForegroundColor Red - Write-Host "Re-run with -SaveCredential to save new credentials." -ForegroundColor Yellow - exit 1 -} +Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green # Log output $logDir = Join-Path $scriptDir "logs" @@ -202,7 +227,41 @@ Write-Host "Log: $logFile" -ForegroundColor Gray $mainScript = Join-Path $scriptDir "Invoke-RemoteMaintenance.ps1" -& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred 2>&1 | Tee-Object -FilePath $logFile +Start-Transcript -Path $logFile -Force | Out-Null +& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred +Stop-Transcript | Out-Null + +# Parse log for results summary +$logContent = Get-Content $logFile -ErrorAction SilentlyContinue +$okPCs = @($logContent | Select-String '\[OK\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value }) +$failPCs = @($logContent | Select-String '\[FAIL\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value } | Where-Object { $_ -ne ':' } | Sort-Object -Unique) + +$summaryFile = Join-Path $logDir "LAST-RUN-SUMMARY.txt" +$summary = @() +$summary += "============================================" +$summary += " MAINTENANCE RESULTS - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +$summary += " Task: $Task" +$summary += "============================================" +$summary += "" +$summary += "SUCCEEDED: $($okPCs.Count)" +foreach ($pc in $okPCs) { $summary += " [OK] $pc" } +$summary += "" +$summary += "FAILED: $($failPCs.Count)" +foreach ($pc in $failPCs) { $summary += " [FAIL] $pc" } +$summary += "" +$summary += "Full log: $logFile" + +$summary | Out-File -FilePath $summaryFile -Encoding UTF8 Write-Host "" Write-Host "Complete. Log saved to: $logFile" -ForegroundColor Green +Write-Host "" +Write-Host "=== RESULTS ===" -ForegroundColor White +Write-Host " Succeeded: $($okPCs.Count)" -ForegroundColor Green +foreach ($pc in $okPCs) { Write-Host " $pc" -ForegroundColor Green } +if ($failPCs.Count -gt 0) { + Write-Host " Failed: $($failPCs.Count)" -ForegroundColor Red + foreach ($pc in $failPCs) { Write-Host " $pc" -ForegroundColor Red } +} +Write-Host "" +Write-Host "Summary also saved to: $summaryFile" -ForegroundColor Yellow diff --git a/remote-execution/udc/UDC_Update.ps1 b/remote-execution/udc/UDC_Update.ps1 new file mode 100755 index 0000000..762087a --- /dev/null +++ b/remote-execution/udc/UDC_Update.ps1 @@ -0,0 +1,279 @@ +# ============================================ +# UDC Application Update Script (PowerShell) +# ============================================ + +param( + [string]$Version +) + +# Set error action preference +$ErrorActionPreference = "Stop" + +# Set variables +$UDC_PATH = "C:\Program Files\UDC" +$VERSION_FILE = "S:\SPC\UDC\UDC_Update.txt" +$LOG_DIR = "S:\DT\Cameron\UDC\logs" + +# Get hostname and timestamp +$HOSTNAME = $env:COMPUTERNAME +$TIMESTAMP = Get-Date -Format "yyyyMMdd_HHmmss" +$LOG_FILE = "$LOG_DIR\logs_$($HOSTNAME)_$TIMESTAMP.txt" + +# Flag to track if we should skip the update +$SKIP_UPDATE = $false + +# Function to write logs +function Write-Log { + param([string]$Message) + $LogMessage = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message" + Write-Host $Message + Add-Content -Path $LOG_FILE -Value $LogMessage -ErrorAction SilentlyContinue +} + +# Create log directory if needed +try { + if (-not (Test-Path $LOG_DIR)) { + New-Item -Path $LOG_DIR -ItemType Directory -Force | Out-Null + } +} catch { + Write-Host "WARNING: Cannot access log directory $LOG_DIR" + Write-Host "Update will continue but logging is disabled." +} + +Write-Log "============================================" +Write-Log "UDC Update Script Started on $HOSTNAME" +Write-Log "============================================" + +# ============================================ +# Step 1: Check if PPMon.exe or ppdcs.exe are running +# ============================================ +Write-Host "Checking for conflicting processes..." +$conflictingProcesses = Get-Process -Name "PPMon", "ppdcs" -ErrorAction SilentlyContinue + +if ($conflictingProcesses) { + foreach ($proc in $conflictingProcesses) { + Write-Log "$($proc.Name).exe is running. Skipping update process." + Write-Host "$($proc.Name).exe is running. Update will be skipped." + } + $SKIP_UPDATE = $true +} + +# ============================================ +# Step 2: Check if UDC directory exists +# ============================================ +Write-Host "Checking if UDC is installed..." +if (-not (Test-Path $UDC_PATH)) { + Write-Log "UDC directory not found. Exiting." + Write-Host "UDC is not installed on this machine." + exit 0 +} + +# ============================================ +# Step 3: Check if UDC.exe exists +# ============================================ +$UDC_EXE = "$UDC_PATH\UDC.exe" +if (-not (Test-Path $UDC_EXE)) { + Write-Log "UDC.exe not found. Exiting." + Write-Host "UDC.exe not found in installation directory." + exit 0 +} + +# ============================================ +# ONLY PROCEED WITH UPDATE IF NOT SKIPPED +# ============================================ +if (-not $SKIP_UPDATE) { + + # ============================================ + # Step 4: Determine target version + # ============================================ + if ($Version) { + $NETWORK_VERSION = $Version + Write-Host "Using override version: $NETWORK_VERSION" + Write-Log "Override version specified: $NETWORK_VERSION" + } else { + if (-not (Test-Path $VERSION_FILE)) { + Write-Log "Version file not found: $VERSION_FILE" + Write-Host "ERROR: Version file not found." + exit 1 + } + + Write-Host "Reading version information..." + try { + $versionContent = Get-Content $VERSION_FILE | Where-Object { $_ -match "Version:" } + $NETWORK_VERSION = ($versionContent -split ":")[1].Trim() + + if ([string]::IsNullOrEmpty($NETWORK_VERSION)) { + throw "Could not parse network version" + } + + Write-Host "Network version: $NETWORK_VERSION" + Write-Log "Network version: $NETWORK_VERSION" + } catch { + Write-Log "Could not read network version. Error: $_" + Write-Host "ERROR: Could not determine network version." + exit 1 + } + } + + # Build source path from version + $SOURCE_PATH = "S:\SPC\UDC\UDC_$NETWORK_VERSION" + + # ============================================ + # Step 5: Check if source files exist + # ============================================ + if (-not (Test-Path $SOURCE_PATH)) { + Write-Log "Source path not found: $SOURCE_PATH" + Write-Host "ERROR: Update source files not found at $SOURCE_PATH" + exit 1 + } + + # ============================================ + # Step 6: Get local UDC.exe version + # ============================================ + try { + $fileVersion = (Get-Item $UDC_EXE).VersionInfo.FileVersion + # Trim to 3 parts if it has 4 (e.g., 1.0.30.0 -> 1.0.30) + $versionParts = $fileVersion.Split('.') + if ($versionParts.Count -eq 4) { + $LOCAL_VERSION = "$($versionParts[0]).$($versionParts[1]).$($versionParts[2])" + } else { + $LOCAL_VERSION = $fileVersion + } + + Write-Host "Local version: $LOCAL_VERSION" + Write-Log "Local version: $LOCAL_VERSION" + } catch { + Write-Log "Could not read local version. Error: $_" + Write-Host "ERROR: Could not determine local version." + exit 1 + } + + # ============================================ + # Step 7: Compare versions + # ============================================ + try { + $netVer = [version]$NETWORK_VERSION + $localVer = [version]$LOCAL_VERSION + + if ($netVer -eq $localVer) { + Write-Log "Versions match ($LOCAL_VERSION). No update needed." + Write-Host "Version is current. No update required." + $SKIP_UPDATE = $true + } + elseif ($netVer -le $localVer) { + Write-Log "Network version ($NETWORK_VERSION) not newer than local ($LOCAL_VERSION)." + Write-Host "Local version is current or newer. No update required." + $SKIP_UPDATE = $true + } + else { + Write-Host "Update required: $LOCAL_VERSION -> $NETWORK_VERSION" + Write-Log "Update required: $LOCAL_VERSION -> $NETWORK_VERSION" + } + } catch { + Write-Log "Error comparing versions. Error: $_" + Write-Host "ERROR: Could not compare versions." + exit 1 + } + + # ============================================ + # Step 8: Perform update if needed + # ============================================ + if (-not $SKIP_UPDATE) { + + # Kill UDC.exe if running + Write-Host "Checking if UDC.exe is running..." + $udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue + + if ($udcProcess) { + Write-Host "UDC.exe is running. Stopping process..." + Write-Log "Killing UDC.exe" + try { + Stop-Process -Name "UDC" -Force + Start-Sleep -Seconds 2 + Write-Log "UDC.exe stopped successfully" + } catch { + Write-Log "Warning: Could not stop UDC.exe. Error: $_" + Write-Host "WARNING: Could not stop UDC.exe" + } + } + + # Copy update files + Write-Host "Copying update files..." + Write-Log "Copying files from $SOURCE_PATH to $UDC_PATH" + + try { + # Use robocopy for better performance and logging + $robocopyArgs = @( + $SOURCE_PATH, + $UDC_PATH, + "/E", # Copy subdirectories including empty ones + "/NFL", # No file list + "/NDL", # No directory list + "/NJH", # No job header + "/NJS", # No job summary + "/NC", # No class + "/NS", # No size + "/NP" # No progress + ) + + $result = robocopy @robocopyArgs + + # Robocopy exit codes: 0-7 are success, 8+ are errors + if ($LASTEXITCODE -ge 8) { + throw "Robocopy failed with exit code $LASTEXITCODE" + } + + Write-Host "Files copied successfully." + Write-Log "Files copied successfully." + } catch { + Write-Log "ERROR: File copy failed. Error: $_" + Write-Host "ERROR: Failed to copy update files." + exit 1 + } + } +} + +# ============================================ +# Step 9: Ensure UDC.exe is running (ONLY if no conflicting processes) +# ============================================ +# Re-check for conflicting processes before starting UDC +$conflictingProcesses = Get-Process -Name "PPMon", "ppdcs" -ErrorAction SilentlyContinue + +if ($conflictingProcesses) { + Write-Log "Conflicting processes still running. Will not start UDC.exe" + Write-Host "Conflicting processes detected. UDC.exe will not be started." +} else { + Write-Host "Verifying UDC.exe is running..." + $udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue + + if (-not $udcProcess) { + Write-Host "UDC.exe is not running. Starting it now..." + Write-Log "UDC.exe not running. Starting UDC.exe" + + try { + Start-Process -FilePath $UDC_EXE + Start-Sleep -Seconds 2 + + # Verify it started + $udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue + + if ($udcProcess) { + Write-Log "UDC.exe started successfully." + Write-Host "UDC.exe started successfully!" + } else { + Write-Log "WARNING: UDC.exe may not have started." + Write-Host "WARNING: UDC.exe may not have started properly." + } + } catch { + Write-Log "WARNING: Could not start UDC.exe. Error: $_" + Write-Host "WARNING: Could not start UDC.exe" + } + } else { + Write-Log "UDC.exe is already running." + Write-Host "UDC.exe is already running." + } +} + +Write-Log "UDC Update Script Completed" +Write-Log "============================================" +exit 0 \ No newline at end of file diff --git a/remote-execution/udc/udc_update.bat b/remote-execution/udc/udc_update.bat new file mode 100755 index 0000000..48f32ee --- /dev/null +++ b/remote-execution/udc/udc_update.bat @@ -0,0 +1,8 @@ +@echo off +:: ============================================ +:: UDC Update PowerShell Launcher +:: ============================================ + +powershell -NoProfile -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File ""U:/Scripts/UDC/UDC_Update.ps1""' -Verb RunAs}" + +exit /b %ERRORLEVEL% diff --git a/remote-execution/udc/udc_update_override.bat b/remote-execution/udc/udc_update_override.bat new file mode 100644 index 0000000..e7568e8 --- /dev/null +++ b/remote-execution/udc/udc_update_override.bat @@ -0,0 +1,11 @@ +@echo off +:: ============================================ +:: UDC Update Override Launcher +:: Edit the version below to force a specific update +:: ============================================ + +set "UDC_VERSION=1.0.32" + +powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& 'U:/Scripts/UDC/UDC_Update.ps1' -Version '%UDC_VERSION%'" + +exit /b %ERRORLEVEL% diff --git a/winrm-setup-package/Invoke-RemoteTask.ps1 b/winrm-setup-package/Invoke-RemoteTask.ps1 new file mode 100644 index 0000000..0106cc1 --- /dev/null +++ b/winrm-setup-package/Invoke-RemoteTask.ps1 @@ -0,0 +1,535 @@ +<# +.SYNOPSIS + Simple remote maintenance toolkit for Windows PCs via WinRM. + +.DESCRIPTION + Executes maintenance tasks on remote Windows PCs using WinRM. + Reads target computers from a text file (one hostname/IP per line). + +.PARAMETER HostsFile + Path to text file containing computer names/IPs (one per line). + Lines starting with # are treated as comments. + Default: .\hosts.txt + +.PARAMETER ComputerName + Single computer name or IP address (alternative to HostsFile). + +.PARAMETER Task + Maintenance task to execute. Available tasks: + - RestartSpooler : Restart Print Spooler service + - FlushDNS : Clear DNS resolver cache + - ClearTempFiles : Clear Windows temp files + - DiskCleanup : Run Windows Disk Cleanup + - OptimizeDisk : TRIM (SSD) or Defrag (HDD) + - SyncTime : Force time sync with domain controller + - RestartService : Restart a specific Windows service + - RunCommand : Run a custom command + - RestartComputer : Restart the remote PC (requires confirmation) + +.PARAMETER ServiceName + Service name for RestartService task. + +.PARAMETER Command + Custom command for RunCommand task. + +.PARAMETER Credential + PSCredential for remote authentication. Prompts if not provided. + +.PARAMETER DnsSuffix + DNS suffix to append to hostnames (if not already FQDN). + Default: logon.ds.ge.com + +.PARAMETER ThrottleLimit + Maximum number of concurrent remote connections. + Default: 10 + +.PARAMETER LogResults + Save results to a timestamped log file in the script directory. + Log files are saved as: RemoteTask_YYYYMMDD_HHMMSS.log + +.EXAMPLE + # Restart print spooler on all hosts in hosts.txt + .\Invoke-RemoteTask.ps1 -Task RestartSpooler + +.EXAMPLE + # Flush DNS on a single computer + .\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task FlushDNS + +.EXAMPLE + # Run disk cleanup on hosts from custom file + .\Invoke-RemoteTask.ps1 -HostsFile ".\shopfloor-pcs.txt" -Task DiskCleanup + +.EXAMPLE + # Restart a specific service + .\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName "Spooler" + +.EXAMPLE + # Run custom command + .\Invoke-RemoteTask.ps1 -Task RunCommand -Command "Get-Process | Select -First 5" + +.NOTES + Author: Shop Floor Tools + Requirements: PowerShell 5.1+, WinRM enabled on targets +#> + +[CmdletBinding(DefaultParameterSetName='ByFile')] +param( + [Parameter(ParameterSetName='ByFile')] + [string]$HostsFile = ".\hosts.txt", + + [Parameter(ParameterSetName='ByName')] + [string[]]$ComputerName, + + [Parameter(Mandatory=$true)] + [ValidateSet( + 'RestartSpooler', 'FlushDNS', 'ClearTempFiles', 'DiskCleanup', + 'OptimizeDisk', 'SyncTime', 'RestartService', 'RunCommand', + 'GetDiskSpace', 'GetUptime', 'TestConnection', 'RestartComputer' + )] + [string]$Task, + + [Parameter()] + [string]$ServiceName, + + [Parameter()] + [string]$Command, + + [Parameter()] + [PSCredential]$Credential, + + [Parameter()] + [string]$DnsSuffix = "logon.ds.ge.com", + + [Parameter()] + [int]$ThrottleLimit = 10, + + [Parameter()] + [switch]$LogResults +) + +# ============================================================================= +# 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 +} + +# ============================================================================= +# Task Scriptblocks +# ============================================================================= + +$TaskScripts = @{ + + 'RestartSpooler' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + try { + Stop-Service -Name Spooler -Force -ErrorAction Stop + $queuePath = "$env:SystemRoot\System32\spool\PRINTERS" + if (Test-Path $queuePath) { Remove-Item "$queuePath\*" -Force -ErrorAction SilentlyContinue } + Start-Service -Name Spooler -ErrorAction Stop + $status = (Get-Service -Name Spooler).Status + $result.Success = ($status -eq 'Running') + $result.Output = "Print Spooler restarted. Status: $status" + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'FlushDNS' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + try { + $flushResult = & ipconfig /flushdns 2>&1 + $result.Output = ($flushResult -join " ").Trim() + $result.Success = $true + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'ClearTempFiles' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null; FilesDeleted = 0 } + try { + $tempPaths = @("$env:TEMP", "$env:SystemRoot\Temp") + foreach ($path in $tempPaths) { + if (Test-Path $path) { + $files = Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue + foreach ($file in $files) { + try { Remove-Item $file.FullName -Force -Recurse -ErrorAction SilentlyContinue; $result.FilesDeleted++ } catch { } + } + } + } + $result.Success = $true + $result.Output = "Deleted $($result.FilesDeleted) temp files" + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'DiskCleanup' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null; SpaceFreedMB = 0 } + try { + $initialFree = (Get-PSDrive C).Free + $cleanupPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" + $categories = @("Temporary Files", "Temporary Setup Files", "Old ChkDsk Files", "Windows Update Cleanup", "Recycle Bin") + foreach ($cat in $categories) { + $catPath = Join-Path $cleanupPath $cat + if (Test-Path $catPath) { Set-ItemProperty -Path $catPath -Name "StateFlags0100" -Value 2 -ErrorAction SilentlyContinue } + } + Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:100" -Wait -WindowStyle Hidden + Start-Sleep -Seconds 2 + $finalFree = (Get-PSDrive C).Free + $result.SpaceFreedMB = [math]::Round(($finalFree - $initialFree) / 1MB, 0) + $result.Success = $true + $result.Output = "Disk cleanup completed. Space freed: $($result.SpaceFreedMB) MB" + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'OptimizeDisk' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + try { + $volumes = Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.DriveLetter } + $optimized = @() + foreach ($vol in $volumes) { + $driveLetter = $vol.DriveLetter + $physicalDisk = Get-PhysicalDisk | Where-Object { $_.DeviceId -eq (Get-Partition -DriveLetter $driveLetter -ErrorAction SilentlyContinue).DiskNumber } + $mediaType = if ($physicalDisk) { $physicalDisk.MediaType } else { "Unknown" } + try { + if ($mediaType -eq 'SSD') { Optimize-Volume -DriveLetter $driveLetter -ReTrim -ErrorAction Stop; $action = "TRIM" } + else { Optimize-Volume -DriveLetter $driveLetter -Defrag -ErrorAction Stop; $action = "Defrag" } + $optimized += "${driveLetter}:($action)" + } catch { $optimized += "${driveLetter}:(Failed)" } + } + $result.Success = $true + $result.Output = "Optimized: $($optimized -join ', ')" + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'SyncTime' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + try { + $source = (& w32tm /query /source 2>&1) -join " " + $syncResult = & w32tm /resync /force 2>&1 + $result.Success = ($syncResult -match "success" -or $LASTEXITCODE -eq 0) + $result.Output = "Time synced with $source. Current: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'RestartService' = { + param($ServiceName) + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + if (-not $ServiceName) { $result.Error = "ServiceName parameter required"; return $result } + try { + Restart-Service -Name $ServiceName -Force -ErrorAction Stop + $status = (Get-Service -Name $ServiceName).Status + $result.Success = ($status -eq 'Running') + $result.Output = "Service '$ServiceName' restarted. Status: $status" + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'RunCommand' = { + param($Command) + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + if (-not $Command) { $result.Error = "Command parameter required"; return $result } + try { + $output = Invoke-Expression $Command 2>&1 + $result.Output = ($output | Out-String).Trim() + $result.Success = $true + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'GetDiskSpace' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + try { + $drives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -ne $null } + $info = foreach ($drive in $drives) { + $freeGB = [math]::Round($drive.Free / 1GB, 1) + $usedGB = [math]::Round($drive.Used / 1GB, 1) + $totalGB = $freeGB + $usedGB + $pctFree = if ($totalGB -gt 0) { [math]::Round(($freeGB / $totalGB) * 100, 0) } else { 0 } + "$($drive.Name): $freeGB GB free ($pctFree%)" + } + $result.Output = $info -join ", " + $result.Success = $true + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'GetUptime' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + try { + $os = Get-CimInstance -ClassName Win32_OperatingSystem + $uptime = (Get-Date) - $os.LastBootUpTime + $result.Output = "Up $([math]::Floor($uptime.TotalDays))d $($uptime.Hours)h $($uptime.Minutes)m (Last boot: $($os.LastBootUpTime.ToString('yyyy-MM-dd HH:mm')))" + $result.Success = $true + } catch { $result.Error = $_.Exception.Message } + return $result + } + + 'TestConnection' = { + $result = @{ Success = $true; Hostname = $env:COMPUTERNAME; Output = "Connection successful"; Error = $null } + return $result + } + + 'RestartComputer' = { + $result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null } + try { + $result.Output = "Restart initiated" + $result.Success = $true + # Schedule restart in 5 seconds to allow response to return + Start-Process -FilePath "shutdown.exe" -ArgumentList "/r /t 5 /c `"Remote restart initiated via WinRM`"" -NoNewWindow + } catch { $result.Error = $_.Exception.Message } + return $result + } +} + +# ============================================================================= +# Main Execution +# ============================================================================= + +Write-Host "" +Write-Host ("=" * 60) -ForegroundColor Cyan +Write-Host " Remote Task Executor - Task: $Task" -ForegroundColor Cyan +Write-Host ("=" * 60) -ForegroundColor Cyan +Write-Host "" + +# Validate task-specific parameters +if ($Task -eq 'RestartService' -and -not $ServiceName) { + Write-Log "ServiceName parameter is required for RestartService task" -Level "ERROR" + exit 1 +} +if ($Task -eq 'RunCommand' -and -not $Command) { + Write-Log "Command parameter is required for RunCommand task" -Level "ERROR" + exit 1 +} +if ($Task -eq 'RestartComputer') { + Write-Host "" + Write-Host "WARNING: This will restart the target computer(s)!" -ForegroundColor Yellow + $confirm = Read-Host "Type 'YES' to confirm" + if ($confirm -ne 'YES') { + Write-Log "Restart cancelled by user" -Level "WARNING" + exit 0 + } +} + +# 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 ($ComputerName) { + $computers = $ComputerName +} else { + if (-not (Test-Path $HostsFile)) { + Write-Log "Hosts file not found: $HostsFile" -Level "ERROR" + Write-Log "Create a text file with one hostname or IP per line." -Level "INFO" + exit 1 + } + $computers = Get-Content $HostsFile | Where-Object { $_.Trim() -and -not $_.StartsWith("#") } +} + +if ($computers.Count -eq 0) { + Write-Log "No computers specified." -Level "ERROR" + exit 1 +} + +Write-Log "Target computers: $($computers.Count)" -Level "INFO" +Write-Host "" + +# Build FQDNs if DNS suffix provided +$targets = $computers | ForEach-Object { + $name = $_.Trim() + if ($DnsSuffix -and $name -notlike "*.*") { + "$name.$DnsSuffix" + } else { + $name + } +} + +# Get the scriptblock +$scriptBlock = $TaskScripts[$Task] + +# Create session options +$sessionOption = New-PSSessionOption -OpenTimeout 30000 -OperationTimeout 300000 -NoMachineProfile + +Write-Log "Executing on $($targets.Count) computer(s) in parallel (ThrottleLimit: $ThrottleLimit)..." -Level "INFO" +Write-Host "" + +# Build arguments for tasks that need them +$taskArgs = @() +if ($Task -eq 'RestartService') { $taskArgs = @($ServiceName) } +if ($Task -eq 'RunCommand') { $taskArgs = @($Command) } + +# Show progress indicator +$stopwatch = [System.Diagnostics.Stopwatch]::StartNew() +Write-Host " [" -NoNewline +Write-Host "Running..." -ForegroundColor Yellow -NoNewline +Write-Host "] Please wait..." -NoNewline + +# Execute on all remote computers in parallel using Invoke-Command +$results = @() +$connectionErrors = @() + +try { + if ($taskArgs.Count -gt 0) { + $results = Invoke-Command -ComputerName $targets -ScriptBlock $scriptBlock -ArgumentList $taskArgs ` + -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate ` + -ThrottleLimit $ThrottleLimit -ErrorAction SilentlyContinue -ErrorVariable connectionErrors + } else { + $results = Invoke-Command -ComputerName $targets -ScriptBlock $scriptBlock ` + -Credential $Credential -SessionOption $sessionOption -Authentication Negotiate ` + -ThrottleLimit $ThrottleLimit -ErrorAction SilentlyContinue -ErrorVariable connectionErrors + } +} catch { + Write-Host "" + Write-Log "Execution error: $($_.Exception.Message)" -Level "ERROR" +} + +$stopwatch.Stop() +$elapsed = $stopwatch.Elapsed.TotalSeconds + +# Clear the progress line +Write-Host "`r" -NoNewline +Write-Host (" " * 60) -NoNewline +Write-Host "`r" -NoNewline +Write-Log "Completed in $([math]::Round($elapsed, 1)) seconds" -Level "INFO" +Write-Host "" + +# Collect all results (successes, failures, connection errors) +$allResults = @() +$successCount = 0 +$failCount = 0 + +foreach ($result in $results) { + $status = if ($result.Success) { "OK"; $successCount++ } else { "FAIL"; $failCount++ } + $message = if ($result.Success) { $result.Output } else { $result.Error } + $allResults += [PSCustomObject]@{ + Status = $status + Computer = $result.Hostname + Message = $message + } +} + +# Process connection errors +foreach ($err in $connectionErrors) { + $targetName = if ($err.TargetObject) { $err.TargetObject } else { "Unknown" } + # Extract just the computer name from FQDN for display + $shortName = ($targetName -split '\.')[0] + $errorMsg = $err.Exception.Message -replace '\r?\n', ' ' + # Truncate long error messages + if ($errorMsg.Length -gt 60) { $errorMsg = $errorMsg.Substring(0, 57) + "..." } + $allResults += [PSCustomObject]@{ + Status = "FAIL" + Computer = $shortName + Message = $errorMsg + } + $failCount++ +} + +# Sort results: failures first, then successes +$allResults = $allResults | Sort-Object @{Expression={$_.Status}; Descending=$true}, Computer + +# Display results in a formatted table +Write-Host " STATUS COMPUTER RESULT" -ForegroundColor Cyan +Write-Host " ------ -------- ------" -ForegroundColor Cyan + +foreach ($r in $allResults) { + $statusColor = if ($r.Status -eq "OK") { "Green" } else { "Red" } + $statusIcon = if ($r.Status -eq "OK") { "[OK] " } else { "[FAIL]" } + + # Pad/truncate computer name to 20 chars + $compName = $r.Computer + if ($compName.Length -gt 18) { $compName = $compName.Substring(0, 15) + "..." } + $compName = $compName.PadRight(20) + + # Truncate message if too long + $msg = $r.Message + if ($msg.Length -gt 50) { $msg = $msg.Substring(0, 47) + "..." } + + Write-Host " " -NoNewline + Write-Host $statusIcon -ForegroundColor $statusColor -NoNewline + Write-Host " $compName " -NoNewline + Write-Host $msg -ForegroundColor $(if ($r.Status -eq "OK") { "White" } else { "Yellow" }) +} + +Write-Host "" + +# Summary +Write-Host "" +Write-Host ("=" * 60) -ForegroundColor Cyan +Write-Host " SUMMARY" -ForegroundColor Cyan +Write-Host ("=" * 60) -ForegroundColor Cyan +Write-Host " Task: $Task" -ForegroundColor White +Write-Host " Total: $($computers.Count)" -ForegroundColor White +Write-Host " Successful: $successCount" -ForegroundColor Green +Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" }) +Write-Host ("=" * 60) -ForegroundColor Cyan +Write-Host "" + +# Save to log file if requested +if ($LogResults) { + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + if (-not $scriptDir) { $scriptDir = Get-Location } + $logTimestamp = Get-Date -Format "yyyyMMdd_HHmmss" + $logFile = Join-Path $scriptDir "RemoteTask_$logTimestamp.log" + + $logContent = @() + $logContent += "=" * 60 + $logContent += "Remote Task Execution Log" + $logContent += "=" * 60 + $logContent += "Date/Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + $logContent += "Task: $Task" + $logContent += "Targets: $($computers.Count)" + $logContent += "ThrottleLimit: $ThrottleLimit" + $logContent += "Elapsed: $([math]::Round($elapsed, 1)) seconds" + if ($ServiceName) { $logContent += "ServiceName: $ServiceName" } + if ($Command) { $logContent += "Command: $Command" } + $logContent += "" + $logContent += "=" * 60 + $logContent += "RESULTS" + $logContent += "=" * 60 + $logContent += "" + $logContent += "STATUS COMPUTER RESULT" + $logContent += "------ -------- ------" + + foreach ($r in $allResults) { + $statusText = if ($r.Status -eq "OK") { "[OK] " } else { "[FAIL] " } + $compText = $r.Computer.PadRight(28) + $logContent += "$statusText $compText $($r.Message)" + } + + $logContent += "" + $logContent += "=" * 60 + $logContent += "SUMMARY" + $logContent += "=" * 60 + $logContent += "Total: $($computers.Count)" + $logContent += "Successful: $successCount" + $logContent += "Failed: $failCount" + $logContent += "=" * 60 + + $logContent | Out-File -FilePath $logFile -Encoding UTF8 + Write-Log "Results saved to: $logFile" -Level "SUCCESS" + Write-Host "" +} + +# Results are displayed above and optionally saved to log file +# To capture results programmatically, use: $results = Invoke-Command ... directly diff --git a/winrm-setup-package/README.md b/winrm-setup-package/README.md new file mode 100644 index 0000000..ad83cf5 --- /dev/null +++ b/winrm-setup-package/README.md @@ -0,0 +1,296 @@ +# WinRM Setup Package for Shopfloor PCs + +This package provides scripts to configure WinRM (Windows Remote Management) on shopfloor PCs and execute remote maintenance tasks. + +## Contents + +| File | Description | +|------|-------------| +| `Setup-WinRM.bat` | Run on each PC to enable and configure WinRM | +| `Invoke-RemoteTask.ps1` | PowerShell script to execute tasks on remote PCs | +| `hosts.txt` | List of target computers (edit before use) | +| `README.md` | This documentation | + +--- + +## Quick Start + +### Step 1: Configure Your Admin Workstation + +Before connecting to remote PCs, run this once on your admin workstation (as Administrator): + +```powershell +Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com" -Force +``` + +This allows your workstation to connect to any PC in the domain. + +### Step 2: Configure the Setup Script + +Edit `Setup-WinRM.bat` and update these values at the top: + +```batch +REM Default security group - who can use WinRM to connect +set "DEFAULT_SECURITY_GROUP=logon\groupid" + +REM Where to log the inventory CSV (network share recommended) +set "DEFAULT_LOG_PATH=\\server\share\winrm-inventory" + +REM Domain suffix for TrustedHosts +set "TRUSTED_DOMAIN=*.logon.ds.ge.com" + +REM Optional: Trust a specific subnet (uncomment and set) +REM set "TRUSTED_SUBNET=10.48.130.*" +``` + +### Step 3: Create Security Group in Active Directory + +1. Open **Active Directory Users and Computers** +2. Create a new Security Group (or use existing group matching `groupid`) +3. Add users who should have remote management access + +### Step 4: Run Setup on Each Shopfloor PC + +Run as Administrator on each PC: + +```cmd +Setup-WinRM.bat +``` + +Or with parameters: +```cmd +Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory" +``` + +The script will: +- Enable WinRM service +- Configure authentication (Negotiate/Kerberos) +- Set firewall rules (domain profile only) +- Restrict access to the security group +- Log hostname/IP to CSV inventory + +### Step 5: Run Remote Tasks + +From your admin workstation, edit `hosts.txt` with target PCs, then: + +```powershell +# Test connectivity +.\Invoke-RemoteTask.ps1 -Task TestConnection + +# Restart print spooler on all hosts +.\Invoke-RemoteTask.ps1 -Task RestartSpooler + +# Check disk space +.\Invoke-RemoteTask.ps1 -Task GetDiskSpace + +# Run on a single PC +.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task FlushDNS +``` + +--- + +## Setup Script Details + +### What Setup-WinRM.bat Configures + +| Setting | Value | Purpose | +|---------|-------|---------| +| WinRM Service | Auto-start | Ensures WinRM starts on boot | +| AllowUnencrypted | false | Security: require encrypted connections | +| Negotiate Auth | true | Enables Kerberos/NTLM authentication | +| CredSSP Auth | true | Enables credential delegation (double-hop) | +| Firewall | Domain profile | Opens port 5985 for domain connections only | +| TrustedHosts | *.logon.ds.ge.com | Trusts domain-joined PCs | +| RootSDDL | Security group | Restricts who can connect | + +### CSV Inventory + +The setup script logs each PC to a CSV file: + +```csv +Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup +PC001,10.48.130.101,2026-01-08 09:30:00,10.0,logon\groupid +PC002,10.48.130.102,2026-01-08 09:35:00,10.0,logon\groupid +``` + +You can use this CSV as your hosts file: +```powershell +# Extract hostnames from CSV +Import-Csv "\\server\share\winrm-inventory\winrm-inventory.csv" | + Select-Object -ExpandProperty Hostname | + Set-Content .\hosts.txt +``` + +--- + +## Available Remote Tasks + +| Task | Description | +|------|-------------| +| `TestConnection` | Verify WinRM connectivity | +| `GetUptime` | Show system uptime and last boot time | +| `GetDiskSpace` | Show free space on all drives | +| `RestartSpooler` | Restart Print Spooler service | +| `FlushDNS` | Clear DNS resolver cache | +| `ClearTempFiles` | Delete Windows temp files | +| `DiskCleanup` | Run Windows Disk Cleanup | +| `OptimizeDisk` | TRIM (SSD) or Defrag (HDD) | +| `SyncTime` | Force time sync with domain controller | +| `RestartService` | Restart any Windows service (requires `-ServiceName`) | +| `RunCommand` | Run custom PowerShell command (requires `-Command`) | +| `RestartComputer` | Restart the remote PC (requires YES confirmation) | + +### Examples + +```powershell +# Check uptime on all hosts +.\Invoke-RemoteTask.ps1 -Task GetUptime + +# Restart a specific service +.\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName "Spooler" + +# Run custom command +.\Invoke-RemoteTask.ps1 -Task RunCommand -Command "Get-Process | Sort CPU -Desc | Select -First 5" + +# Use custom hosts file +.\Invoke-RemoteTask.ps1 -HostsFile ".\cnc-machines.txt" -Task FlushDNS + +# Specify DNS suffix for short hostnames +.\Invoke-RemoteTask.ps1 -DnsSuffix "logon.ds.ge.com" -Task TestConnection + +# Restart a remote PC (will prompt for confirmation) +.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task RestartComputer + +# Increase parallelism for faster execution on many PCs +.\Invoke-RemoteTask.ps1 -Task FlushDNS -ThrottleLimit 20 + +# Save results to a log file +.\Invoke-RemoteTask.ps1 -Task GetDiskSpace -LogResults +``` + +### Logging Results + +Use `-LogResults` to save task output to a timestamped log file in the script directory: + +```powershell +.\Invoke-RemoteTask.ps1 -Task RestartSpooler -LogResults +# Creates: RemoteTask_20260108_143022.log +``` + +Log files contain: +- Task name and parameters +- Execution time +- Status of each computer (OK/FAIL) +- Result messages +- Summary totals + +### Targeting Multiple PCs + +```powershell +# Comma-separated list +.\Invoke-RemoteTask.ps1 -ComputerName "PC001","PC002","PC003" -Task GetUptime + +# Array variable +$pcs = @("PC001", "PC002", "PC003") +.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task FlushDNS + +# From hosts.txt file (default) +.\Invoke-RemoteTask.ps1 -Task RestartSpooler + +# From CSV inventory +$pcs = (Import-Csv "\\server\share\winrm-inventory.csv").Hostname +.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task GetDiskSpace + +# From Active Directory query +$pcs = (Get-ADComputer -Filter "Name -like 'SHOPFLOOR-*'").Name +.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task SyncTime +``` + +All commands run in parallel (default: 10 concurrent connections, adjust with `-ThrottleLimit`). + +--- + +## Troubleshooting + +### "Access Denied" when connecting + +1. Verify you're a member of the WinRM security group +2. Check that your credentials are correct +3. Verify the target PC ran Setup-WinRM.bat successfully + +### "WinRM cannot complete the operation" + +1. Verify the target PC is reachable: `ping PC001` +2. Check WinRM is running on target: `sc query winrm` (on target PC) +3. Verify firewall allows port 5985 + +### "The WinRM client cannot process the request" + +1. Add target to TrustedHosts on your admin workstation: + ```powershell + Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com" -Force + ``` + +### Test WinRM Configuration + +On your admin workstation: +```powershell +# Test basic connectivity +Test-WSMan -ComputerName PC001 + +# Test with credentials +$cred = Get-Credential +Test-WSMan -ComputerName PC001 -Credential $cred -Authentication Negotiate + +# Enter interactive session +Enter-PSSession -ComputerName PC001 -Credential $cred -Authentication Negotiate +``` + +On the target PC: +```cmd +winrm enumerate winrm/config/listener +winrm get winrm/config/service +``` + +--- + +## Security Considerations + +1. **Use Security Groups**: Always restrict WinRM access to a specific AD group +2. **Domain Profile Only**: Firewall rules only allow connections on domain networks +3. **No Unencrypted Traffic**: AllowUnencrypted is set to false +4. **Audit Access**: Enable Windows Security auditing for logon events +5. **Credential Protection**: Use dedicated admin accounts, not personal accounts + +--- + +## Adding Custom Tasks + +Edit `Invoke-RemoteTask.ps1` and add to the `$TaskScripts` hashtable: + +```powershell +'MyCustomTask' = { + $result = @{ + Success = $false + Hostname = $env:COMPUTERNAME + Output = "" + Error = $null + } + try { + # Your code here + $result.Output = "Task completed" + $result.Success = $true + } catch { + $result.Error = $_.Exception.Message + } + return $result +} +``` + +Then add the task name to the `ValidateSet` in the param block. + +--- + +## Support + +For issues or questions, contact your IT support team. diff --git a/winrm-setup-package/Setup-WinRM.bat b/winrm-setup-package/Setup-WinRM.bat new file mode 100644 index 0000000..df92c65 --- /dev/null +++ b/winrm-setup-package/Setup-WinRM.bat @@ -0,0 +1,269 @@ +@echo off +REM ============================================================================ +REM WinRM Setup Script for Shopfloor PCs +REM ============================================================================ +REM +REM PURPOSE: Configures WinRM on a Windows PC and restricts access to members +REM of a specific Active Directory security group. Logs setup to CSV. +REM +REM USAGE: Run as Administrator on each shopfloor PC +REM Setup-WinRM.bat [SecurityGroupName] [LogPath] +REM +REM EXAMPLE: Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory" +REM +REM REQUIREMENTS: +REM - Must be run as Administrator +REM - PC must be domain-joined +REM - Security group must exist in Active Directory +REM +REM ============================================================================ + +setlocal EnableDelayedExpansion + +REM Check for admin privileges +net session >nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo. + echo ERROR: This script must be run as Administrator. + echo Right-click and select "Run as administrator" + echo. + pause + exit /b 1 +) + +REM ============================================================================ +REM Configuration - EDIT THESE VALUES FOR YOUR ENVIRONMENT +REM ============================================================================ + +REM Default security group (can be overridden by parameter) +set "DEFAULT_SECURITY_GROUP=logon\groupid" + +REM Default log path for CSV inventory (can be overridden by parameter) +REM Use a network share that all PCs can write to +set "DEFAULT_LOG_PATH=\\server\share\winrm-inventory" + +REM Domain suffix for TrustedHosts (e.g., *.logon.ds.ge.com) +set "TRUSTED_DOMAIN=*.logon.ds.ge.com" + +REM Optional: Trusted subnets - comma-separated (leave empty to skip) +REM For /24 subnet: "10.48.130.*" +REM For /23 subnet: "10.48.130.*,10.48.131.*" +REM For /22 subnet: "10.48.128.*,10.48.129.*,10.48.130.*,10.48.131.*" +REM set "TRUSTED_SUBNET=10.48.130.*,10.48.131.*" +set "TRUSTED_SUBNET=" + +REM ============================================================================ + +REM Get parameters or use defaults +set "SECURITY_GROUP=%~1" +set "LOG_PATH=%~2" + +if "%SECURITY_GROUP%"=="" set "SECURITY_GROUP=%DEFAULT_SECURITY_GROUP%" +if "%LOG_PATH%"=="" set "LOG_PATH=%DEFAULT_LOG_PATH%" + +echo. +echo ============================================================================ +echo WinRM Setup Script +echo ============================================================================ +echo. +echo Computer: %COMPUTERNAME% +echo Security Group: %SECURITY_GROUP% +echo Log Path: %LOG_PATH% +echo Trusted Domain: %TRUSTED_DOMAIN% +if not "%TRUSTED_SUBNET%"=="" echo Trusted Subnet: %TRUSTED_SUBNET% +echo. +echo ============================================================================ +echo. + +REM Step 1: Enable WinRM service +echo [1/7] Enabling WinRM service... +sc config WinRM start= auto >nul 2>&1 +net start WinRM >nul 2>&1 +if %ERRORLEVEL% equ 0 ( + echo WinRM service started +) else ( + echo WinRM service already running +) + +REM Step 2: Run quick config (creates listener, firewall rules) +echo [2/7] Running WinRM quick configuration... +winrm quickconfig -quiet >nul 2>&1 +echo Quick config completed + +REM Step 3: Configure WinRM settings +echo [3/7] Configuring WinRM settings... + +REM Disable unencrypted traffic (security best practice) +winrm set winrm/config/service @{AllowUnencrypted="false"} >nul 2>&1 + +REM Enable Negotiate authentication (Kerberos/NTLM) +winrm set winrm/config/service/auth @{Negotiate="true"} >nul 2>&1 + +REM Enable CredSSP for double-hop scenarios (optional) +winrm set winrm/config/service/auth @{CredSSP="true"} >nul 2>&1 + +REM Set max concurrent operations +winrm set winrm/config/service @{MaxConcurrentOperationsPerUser="50"} >nul 2>&1 + +REM Set max memory per shell (512MB) +winrm set winrm/config/winrs @{MaxMemoryPerShellMB="512"} >nul 2>&1 + +echo WinRM settings configured + +REM Step 4: Configure TrustedHosts on CLIENT side (for the admin workstation) +REM This step configures this PC to trust connections TO other PCs +echo [4/7] Configuring TrustedHosts... + +REM Build TrustedHosts value +set "TRUSTED_HOSTS=%TRUSTED_DOMAIN%" +if not "%TRUSTED_SUBNET%"=="" ( + set "TRUSTED_HOSTS=%TRUSTED_HOSTS%,%TRUSTED_SUBNET%" +) + +REM Get current TrustedHosts and append if needed +powershell -ExecutionPolicy Bypass -Command ^ + "$currentTrusted = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction SilentlyContinue).Value; " ^ + "$newHosts = '%TRUSTED_HOSTS%'; " ^ + "if ([string]::IsNullOrEmpty($currentTrusted)) { " ^ + " Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newHosts -Force; " ^ + " Write-Host ' Set TrustedHosts: ' $newHosts; " ^ + "} elseif ($currentTrusted -notlike '*%TRUSTED_DOMAIN%*') { " ^ + " $combined = $currentTrusted + ',' + $newHosts; " ^ + " Set-Item WSMan:\localhost\Client\TrustedHosts -Value $combined -Force; " ^ + " Write-Host ' Added to TrustedHosts: ' $newHosts; " ^ + "} else { " ^ + " Write-Host ' TrustedHosts already configured'; " ^ + "}" + +REM Step 5: Configure firewall rules +echo [5/7] Configuring firewall rules... + +REM Enable WinRM firewall rule for domain profile +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes profile=domain >nul 2>&1 +if %ERRORLEVEL% neq 0 ( + netsh advfirewall firewall add rule name="Windows Remote Management (HTTP-In)" dir=in action=allow protocol=tcp localport=5985 profile=domain >nul 2>&1 +) +echo Firewall rule enabled for domain profile + +REM Step 6: Set WinRM permissions for security group +echo [6/7] Configuring WinRM permissions for security group... + +powershell -ExecutionPolicy Bypass -Command ^ + "$group = '%SECURITY_GROUP%'; " ^ + "try { " ^ + " $ntAccount = New-Object System.Security.Principal.NTAccount($group); " ^ + " $sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]); " ^ + " $sidString = $sid.Value; " ^ + " Write-Host ' Group SID: ' $sidString; " ^ + " $currentSDDL = (Get-Item WSMan:\localhost\Service\RootSDDL).Value; " ^ + " $newACE = '(A;;GXGR;;;' + $sidString + ')'; " ^ + " if ($currentSDDL -notmatch [regex]::Escape($sidString)) { " ^ + " $newSDDL = $currentSDDL -replace 'D:', ('D:' + $newACE); " ^ + " Set-Item WSMan:\localhost\Service\RootSDDL -Value $newSDDL -Force; " ^ + " Write-Host ' Added security group to WinRM permissions'; " ^ + " } else { " ^ + " Write-Host ' Security group already has WinRM permissions'; " ^ + " } " ^ + "} catch { " ^ + " Write-Host ' ERROR: Could not resolve security group - ' $_.Exception.Message; " ^ + " exit 1; " ^ + "}" + +if %ERRORLEVEL% neq 0 ( + echo. + echo ERROR: Failed to configure security group permissions. + echo Verify the security group exists in Active Directory. + echo. + pause + exit /b 1 +) + +REM Step 7: Log to CSV inventory file +echo [7/7] Logging to inventory CSV... + +REM Get IP address +for /f "tokens=2 delims=:" %%a in ('ipconfig ^| findstr /i "IPv4"') do ( + set "IP_ADDRESS=%%a" + goto :gotip +) +:gotip +set "IP_ADDRESS=%IP_ADDRESS: =%" + +REM Get current date/time +for /f "tokens=2 delims==" %%a in ('wmic os get localdatetime /value') do set "DT=%%a" +set "SETUP_DATE=%DT:~0,4%-%DT:~4,2%-%DT:~6,2% %DT:~8,2%:%DT:~10,2%:%DT:~12,2%" + +REM Get OS version +for /f "tokens=4-5 delims=. " %%a in ('ver') do set "OS_VERSION=%%a.%%b" + +REM Create CSV directory if it doesn't exist +if not exist "%LOG_PATH%" ( + mkdir "%LOG_PATH%" 2>nul + if %ERRORLEVEL% neq 0 ( + echo WARNING: Could not create log directory. Logging skipped. + goto :skiplog + ) +) + +REM Define CSV file +set "CSV_FILE=%LOG_PATH%\winrm-inventory.csv" + +REM Create CSV header if file doesn't exist +if not exist "%CSV_FILE%" ( + echo Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup > "%CSV_FILE%" + if %ERRORLEVEL% neq 0 ( + echo WARNING: Could not create CSV file. Logging skipped. + goto :skiplog + ) +) + +REM Check if this hostname already exists in CSV and update or append +powershell -ExecutionPolicy Bypass -Command ^ + "$csvFile = '%CSV_FILE%'; " ^ + "$hostname = '%COMPUTERNAME%'; " ^ + "$newLine = '%COMPUTERNAME%,%IP_ADDRESS%,%SETUP_DATE%,%OS_VERSION%,%SECURITY_GROUP%'; " ^ + "try { " ^ + " $content = Get-Content $csvFile -ErrorAction SilentlyContinue; " ^ + " $found = $false; " ^ + " $newContent = @(); " ^ + " foreach ($line in $content) { " ^ + " if ($line -like \"$hostname,*\") { " ^ + " $newContent += $newLine; " ^ + " $found = $true; " ^ + " } else { " ^ + " $newContent += $line; " ^ + " } " ^ + " } " ^ + " if (-not $found) { $newContent += $newLine; } " ^ + " $newContent | Set-Content $csvFile -Force; " ^ + " Write-Host ' Logged: %COMPUTERNAME% (%IP_ADDRESS%)'; " ^ + "} catch { " ^ + " Write-Host ' WARNING: Could not write to CSV - ' $_.Exception.Message; " ^ + "}" + +:skiplog + +REM Verify configuration +echo. +echo ============================================================================ +echo WinRM Setup Complete! +echo ============================================================================ +echo. +echo Computer: %COMPUTERNAME% +echo IP Address: %IP_ADDRESS% +echo Security Group: %SECURITY_GROUP% +echo WinRM Port: 5985 (HTTP) +echo Trusted Hosts: %TRUSTED_HOSTS% +echo. +echo Inventory logged to: %CSV_FILE% +echo. +echo Members of '%SECURITY_GROUP%' can now connect using: +echo Enter-PSSession -ComputerName %COMPUTERNAME% -Credential (Get-Credential) +echo. +echo To test from a remote PC (as a member of the security group): +echo Test-WSMan -ComputerName %COMPUTERNAME% +echo. +echo ============================================================================ + +pause +exit /b 0 diff --git a/winrm-setup-package/WinRM-Setup-Guide.html b/winrm-setup-package/WinRM-Setup-Guide.html new file mode 100644 index 0000000..f108f0a --- /dev/null +++ b/winrm-setup-package/WinRM-Setup-Guide.html @@ -0,0 +1,424 @@ + + + + + + WinRM Setup Package for Shopfloor PCs + + + + +

WinRM Setup Package for Shopfloor PCs

+ +

This package provides scripts to configure WinRM (Windows Remote Management) on shopfloor PCs and execute remote maintenance tasks.

+ + + +

Package Contents

+ + + + + + + +
FileDescription
Setup-WinRM.batRun on each PC to enable and configure WinRM
Invoke-RemoteTask.ps1PowerShell script to execute tasks on remote PCs
hosts.txtList of target computers (edit before use)
WinRM-Setup-Guide.htmlThis documentation
+ +
+ +

Quick Start

+ +

Step 1: Configure Your Admin Workstation

+ +

Before connecting to remote PCs, run this once on your admin workstation (as Administrator):

+ +
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com" -Force
+ +

This allows your workstation to connect to any PC in the domain.

+ +

Step 2: Configure the Setup Script

+ +

Edit Setup-WinRM.bat and update these values at the top:

+ +
REM Default security group - who can use WinRM to connect
+set "DEFAULT_SECURITY_GROUP=logon\groupid"
+
+REM Where to log the inventory CSV (network share recommended)
+set "DEFAULT_LOG_PATH=\\server\share\winrm-inventory"
+
+REM Domain suffix for TrustedHosts
+set "TRUSTED_DOMAIN=*.logon.ds.ge.com"
+
+REM Optional: Trust a specific subnet (uncomment and set)
+REM set "TRUSTED_SUBNET=10.48.130.*"
+ +

Step 3: Create Security Group in Active Directory

+ +
    +
  1. Open Active Directory Users and Computers
  2. +
  3. Create a new Security Group (or use existing group matching groupid)
  4. +
  5. Add users who should have remote management access
  6. +
+ +

Step 4: Run Setup on Each Shopfloor PC

+ +

Run as Administrator on each PC:

+ +
Setup-WinRM.bat
+ +

Or with parameters:

+ +
Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory"
+ +

The script will:

+
    +
  • Enable WinRM service
  • +
  • Configure authentication (Negotiate/Kerberos)
  • +
  • Set firewall rules (domain profile only)
  • +
  • Restrict access to the security group
  • +
  • Log hostname/IP to CSV inventory
  • +
+ +

Step 5: Run Remote Tasks

+ +

From your admin workstation, edit hosts.txt with target PCs, then:

+ +
# Test connectivity
+.\Invoke-RemoteTask.ps1 -Task TestConnection
+
+# Restart print spooler on all hosts
+.\Invoke-RemoteTask.ps1 -Task RestartSpooler
+
+# Check disk space
+.\Invoke-RemoteTask.ps1 -Task GetDiskSpace
+
+# Run on a single PC
+.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task FlushDNS
+ +
+ +

Setup Script Details

+ +

What Setup-WinRM.bat Configures

+ + + + + + + + + + +
SettingValuePurpose
WinRM ServiceAuto-startEnsures WinRM starts on boot
AllowUnencryptedfalseSecurity: require encrypted connections
Negotiate AuthtrueEnables Kerberos/NTLM authentication
CredSSP AuthtrueEnables credential delegation (double-hop)
FirewallDomain profileOpens port 5985 for domain connections only
TrustedHosts*.logon.ds.ge.comTrusts domain-joined PCs
RootSDDLSecurity groupRestricts who can connect
+ +

CSV Inventory

+ +

The setup script logs each PC to a CSV file:

+ +
Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup
+PC001,10.48.130.101,2026-01-08 09:30:00,10.0,logon\groupid
+PC002,10.48.130.102,2026-01-08 09:35:00,10.0,logon\groupid
+ +

You can use this CSV as your hosts file:

+ +
# Extract hostnames from CSV
+Import-Csv "\\server\share\winrm-inventory\winrm-inventory.csv" |
+    Select-Object -ExpandProperty Hostname |
+    Set-Content .\hosts.txt
+ +
+ +

Available Remote Tasks

+ + + + + + + + + + + + + + + +
TaskDescription
TestConnectionVerify WinRM connectivity
GetUptimeShow system uptime and last boot time
GetDiskSpaceShow free space on all drives
RestartSpoolerRestart Print Spooler service
FlushDNSClear DNS resolver cache
ClearTempFilesDelete Windows temp files
DiskCleanupRun Windows Disk Cleanup
OptimizeDiskTRIM (SSD) or Defrag (HDD)
SyncTimeForce time sync with domain controller
RestartServiceRestart any Windows service (requires -ServiceName)
RunCommandRun custom PowerShell command (requires -Command)
RestartComputerRestart the remote PC (requires YES confirmation)
+ +

Examples

+ +
# Check uptime on all hosts
+.\Invoke-RemoteTask.ps1 -Task GetUptime
+
+# Restart a specific service
+.\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName "Spooler"
+
+# Run custom command
+.\Invoke-RemoteTask.ps1 -Task RunCommand -Command "Get-Process | Sort CPU -Desc | Select -First 5"
+
+# Use custom hosts file
+.\Invoke-RemoteTask.ps1 -HostsFile ".\cnc-machines.txt" -Task FlushDNS
+
+# Specify DNS suffix for short hostnames
+.\Invoke-RemoteTask.ps1 -DnsSuffix "logon.ds.ge.com" -Task TestConnection
+
+# Restart a remote PC (will prompt for confirmation)
+.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task RestartComputer
+
+# Increase parallelism for faster execution on many PCs
+.\Invoke-RemoteTask.ps1 -Task FlushDNS -ThrottleLimit 20
+
+# Save results to a log file
+.\Invoke-RemoteTask.ps1 -Task GetDiskSpace -LogResults
+ +

Targeting Multiple PCs

+ +
# Comma-separated list
+.\Invoke-RemoteTask.ps1 -ComputerName "PC001","PC002","PC003" -Task GetUptime
+
+# Array variable
+$pcs = @("PC001", "PC002", "PC003")
+.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task FlushDNS
+
+# From hosts.txt file (default)
+.\Invoke-RemoteTask.ps1 -Task RestartSpooler
+
+# From CSV inventory
+$pcs = (Import-Csv "\\server\share\winrm-inventory.csv").Hostname
+.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task GetDiskSpace
+
+# From Active Directory query
+$pcs = (Get-ADComputer -Filter "Name -like 'SHOPFLOOR-*'").Name
+.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task SyncTime
+ +
+ Parallel Execution: All commands run in parallel (default: 10 concurrent connections). Adjust with -ThrottleLimit parameter. +
+ +

Logging Results

+ +

Use -LogResults to save task output to a timestamped log file in the script directory:

+ +
.\Invoke-RemoteTask.ps1 -Task RestartSpooler -LogResults
+# Creates: RemoteTask_20260108_143022.log
+ +

Log files contain:

+
    +
  • Task name and parameters
  • +
  • Execution time
  • +
  • Status of each computer (OK/FAIL)
  • +
  • Result messages
  • +
  • Summary totals
  • +
+ +
+ +

Troubleshooting

+ +

"Access Denied" when connecting

+ +
    +
  1. Verify you're a member of the WinRM security group
  2. +
  3. Check that your credentials are correct
  4. +
  5. Verify the target PC ran Setup-WinRM.bat successfully
  6. +
+ +

"WinRM cannot complete the operation"

+ +
    +
  1. Verify the target PC is reachable: ping PC001
  2. +
  3. Check WinRM is running on target: sc query winrm (on target PC)
  4. +
  5. Verify firewall allows port 5985
  6. +
+ +

"The WinRM client cannot process the request"

+ +

Add target to TrustedHosts on your admin workstation:

+ +
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com" -Force
+ +

Test WinRM Configuration

+ +

On your admin workstation:

+ +
# Test basic connectivity
+Test-WSMan -ComputerName PC001
+
+# Test with credentials
+$cred = Get-Credential
+Test-WSMan -ComputerName PC001 -Credential $cred -Authentication Negotiate
+
+# Enter interactive session
+Enter-PSSession -ComputerName PC001 -Credential $cred -Authentication Negotiate
+ +

On the target PC:

+ +
winrm enumerate winrm/config/listener
+winrm get winrm/config/service
+ +
+ +

Security Considerations

+ +
+ Important Security Notes: +
+ +
    +
  1. Use Security Groups: Always restrict WinRM access to a specific AD group
  2. +
  3. Domain Profile Only: Firewall rules only allow connections on domain networks
  4. +
  5. No Unencrypted Traffic: AllowUnencrypted is set to false
  6. +
  7. Audit Access: Enable Windows Security auditing for logon events
  8. +
  9. Credential Protection: Use dedicated admin accounts, not personal accounts
  10. +
+ +
+ +

Adding Custom Tasks

+ +

Edit Invoke-RemoteTask.ps1 and add to the $TaskScripts hashtable:

+ +
'MyCustomTask' = {
+    $result = @{
+        Success = $false
+        Hostname = $env:COMPUTERNAME
+        Output = ""
+        Error = $null
+    }
+    try {
+        # Your code here
+        $result.Output = "Task completed"
+        $result.Success = $true
+    } catch {
+        $result.Error = $_.Exception.Message
+    }
+    return $result
+}
+ +

Then add the task name to the ValidateSet in the param block.

+ +
+ +

Support

+ +

For issues or questions, contact your IT support team.

+ +

+ WinRM Setup Package © 2026 | Generated from README.md +

+ + + diff --git a/winrm-setup-package/hosts.txt b/winrm-setup-package/hosts.txt new file mode 100644 index 0000000..799e242 --- /dev/null +++ b/winrm-setup-package/hosts.txt @@ -0,0 +1,16 @@ +# WinRM Target Hosts +# ------------------- +# Add one hostname or IP address per line +# Lines starting with # are comments +# +# You can use: +# - Hostnames: PC001, SHOPFLOOR-PC-01 +# - FQDNs: PC001.logon.ds.ge.com +# - IP addresses: 10.48.130.100 +# +# Example: +# PC001 +# PC002 +# 10.48.130.100 +# shopfloor-cnc-01.logon.ds.ge.com +