Generic build for other GE sites: no WJ data, one shopfloor type, Windows USB builder
Everything West Jefferson specific moved out to a separate repo (pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers, CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the WJ preinstall list, and the WJ shopfloor PC types (Collections, NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace, Display, PartMarker) with GE-Enforce, machine numbers and the desktop layout. The part-marker bench tools moved to their own repo. Dead scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models, mirror-from-gold, pull-bios, startnet-template.cmd. Shopfloor is one generic type, gea-shopfloor-standard, with today's timing: - PPKG at first logon (unattend Order 10, unchanged). - Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup. - A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once after the post-PPKG reboot. It also clears AutoLogonCount after the ShopFloor handover (the Users-level monitor can be denied that HKLM write) and removes the Intune Sync task when done. - Both lists ship empty. A site adds entries and installers. - WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal carry only the generic payload. The webapp's shopfloor sub-menu editor is gone, and the imaging tiles name the new stages. - The playbook deploys the empty lists only if missing. It now deploys the PowerShell 7 MSI the shopfloor unattend installs, which it never did before. Blancco site settings move into the webapp (Updates > Blancco releases): business location, licensing Wi-Fi SSID and password. They are rendered into the served preferences.xml and applied to every staged release, and are seeded from site-vars (site_name, blancco_wifi_ssid, blancco_wifi_password) only while none exist. The repo XML carries no site values; the plaintext Wi-Fi password is gone from it. New site_name and site_timezone settings: report and evidence-pack site name, server clock and webapp time zone. No more West Jefferson defaults in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the server address filled in at download instead of WJ's GE address. USB on Windows: - scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus payload) with diskpart and reads every file back by SHA-256. - Tested with Windows PowerShell 5.1 in the win11 VM, and the layout installs and first-boots in the pxetest VM. - scripts/stage-usb-share.sh stages the build folder on a share. - site-content/ is optional for both builders. KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment, Windows only, with a site-vars.yml guide. Checks: webapp tests pass (5 suites); the unattend lint and playbook syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names or PC types in playbook/, webapp/, scripts/ or autoinstall/.
This commit is contained in:
@@ -1,598 +0,0 @@
|
||||
# 00-PreInstall-MachineApps.ps1 - Install pre-staged apps from C:\PreInstall (Shopfloor baseline)
|
||||
#
|
||||
# Reads C:\PreInstall\preinstall.json (staged by startnet.cmd during WinPE phase) and
|
||||
# installs each app whose PCTypes filter matches the current PC type. Detection runs first
|
||||
# so already-installed apps are skipped. Numbered 00- so it runs before all other Shopfloor
|
||||
# baseline scripts - apps need to exist before later scripts (e.g. 01-eDNC.ps1) reference
|
||||
# them or rely on their state.
|
||||
#
|
||||
# Failure mode: log + continue. Intune DSC (Simple-Install.ps1) is the safety net for any
|
||||
# install that fails here.
|
||||
#
|
||||
# Logs to C:\Logs\PreInstall\install.log (wiped each run).
|
||||
|
||||
$preInstallDir = "C:\PreInstall"
|
||||
$jsonPath = Join-Path $preInstallDir "preinstall.json"
|
||||
$installerDir = Join-Path $preInstallDir "installers"
|
||||
$logDir = "C:\Logs\PreInstall"
|
||||
$logFile = Join-Path $logDir "install.log"
|
||||
|
||||
# --- Inline site-config reader (same pattern as Setup-OpenText.ps1; this script
|
||||
# runs from C:\Enrollment\shopfloor-setup\Shopfloor\ but Get-PCProfile.ps1 may
|
||||
# not be available yet, so keep the lookup self-contained) ---
|
||||
function Get-SiteConfig {
|
||||
$configPath = 'C:\Enrollment\site-config.json'
|
||||
if (-not (Test-Path $configPath)) { return $null }
|
||||
try {
|
||||
return Get-Content $configPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# --- Setup logging ---
|
||||
# IMPORTANT: do NOT wipe the log on each run. The runner can get killed by an
|
||||
# installer-triggered reboot mid-execution. On the next autologon, the dispatcher
|
||||
# re-runs us - if we wipe the log here, we destroy the forensic record from the
|
||||
# previous (interrupted) run. Instead, append a session header so runs are visible
|
||||
# but history is preserved.
|
||||
if (-not (Test-Path $logDir)) {
|
||||
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
function Write-PreInstallLog {
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)]
|
||||
[string]$Message,
|
||||
[Parameter(Position=1)]
|
||||
[ValidateSet('INFO','WARN','ERROR')]
|
||||
[string]$Level = 'INFO'
|
||||
)
|
||||
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$line = "[$stamp] [$Level] $Message"
|
||||
Write-Host $line
|
||||
|
||||
# Synchronous write-through so each line hits disk immediately, even if a system
|
||||
# reboot kills the process right after this call returns. Bypasses the OS write
|
||||
# cache via FileOptions.WriteThrough.
|
||||
try {
|
||||
$fs = New-Object System.IO.FileStream(
|
||||
$logFile,
|
||||
[System.IO.FileMode]::Append,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::Read,
|
||||
4096,
|
||||
[System.IO.FileOptions]::WriteThrough
|
||||
)
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n")
|
||||
$fs.Write($bytes, 0, $bytes.Length)
|
||||
$fs.Flush()
|
||||
$fs.Dispose()
|
||||
}
|
||||
catch {
|
||||
# Last-resort fallback if the FileStream open fails for any reason
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Session header so multiple runs are visually distinguishable in the log
|
||||
Write-PreInstallLog "================================================================"
|
||||
Write-PreInstallLog "=== Runner session start (PID $PID) ==="
|
||||
Write-PreInstallLog "================================================================"
|
||||
|
||||
# --- Bail early if no preinstall bundle staged ---
|
||||
if (-not (Test-Path $jsonPath)) {
|
||||
Write-PreInstallLog "No preinstall.json at $jsonPath - skipping (no bundle staged for this PC)."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Read PCTYPE and optional PCSUBTYPE from C:\Enrollment\ ---
|
||||
$pcTypeFile = "C:\Enrollment\pc-type.txt"
|
||||
if (-not (Test-Path $pcTypeFile)) {
|
||||
Write-PreInstallLog "No pc-type.txt at $pcTypeFile - skipping" "WARN"
|
||||
exit 0
|
||||
}
|
||||
$pcType = (Get-Content $pcTypeFile -First 1).Trim()
|
||||
if (-not $pcType) {
|
||||
Write-PreInstallLog "pc-type.txt is empty - skipping" "WARN"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$pcSubtype = ''
|
||||
$subtypeFile = "C:\Enrollment\pc-subtype.txt"
|
||||
if (Test-Path $subtypeFile) {
|
||||
$pcSubtype = (Get-Content $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
$pcProfileKey = if ($pcSubtype) { "$pcType-$pcSubtype" } else { $pcType }
|
||||
Write-PreInstallLog "PC type: $pcType$(if ($pcSubtype) { " (subtype: $pcSubtype, profile: $pcProfileKey)" })"
|
||||
|
||||
# --- Parse JSON ---
|
||||
try {
|
||||
$config = Get-Content $jsonPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-PreInstallLog "Failed to parse $jsonPath : $_" "ERROR"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $config.Applications) {
|
||||
Write-PreInstallLog "No Applications in preinstall.json"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-PreInstallLog "Staged installer dir: $installerDir"
|
||||
Write-PreInstallLog "Found $($config.Applications.Count) app entries in preinstall.json"
|
||||
|
||||
# --- Site-name override: replace 'West Jefferson' in InstallArgs if
|
||||
# site-config says otherwise. UDC_Setup.exe expects the spaced site
|
||||
# name in quotes as arg 1, with a dash-prefixed machine number as arg 2.
|
||||
$siteConfig = Get-SiteConfig
|
||||
if ($siteConfig -and $siteConfig.siteName -and $siteConfig.siteName -ne 'West Jefferson') {
|
||||
Write-PreInstallLog "Site config loaded - siteName: $($siteConfig.siteName)"
|
||||
foreach ($app in $config.Applications) {
|
||||
if ($app.InstallArgs -and $app.InstallArgs -match 'West Jefferson') {
|
||||
$app.InstallArgs = $app.InstallArgs -replace 'West Jefferson', $siteConfig.siteName
|
||||
Write-PreInstallLog " Overrode site name in $($app.Name) args: $($app.InstallArgs)"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-PreInstallLog "No site-config override for siteName (using defaults in preinstall.json)"
|
||||
}
|
||||
|
||||
# --- Machine-number override: replace "9999" in UDC InstallArgs if tech entered a number ---
|
||||
$machineNumFile = 'C:\Enrollment\machine-number.txt'
|
||||
if (Test-Path -LiteralPath $machineNumFile) {
|
||||
$machineNum = (Get-Content -LiteralPath $machineNumFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
if ($machineNum -and $machineNum -ne '9999' -and $machineNum -match '^\d+$') {
|
||||
Write-PreInstallLog "Machine number from PXE menu: $machineNum"
|
||||
foreach ($app in $config.Applications) {
|
||||
if ($app.InstallArgs -and $app.InstallArgs -match '9999') {
|
||||
$app.InstallArgs = $app.InstallArgs -replace '9999', $machineNum
|
||||
Write-PreInstallLog " Overrode machine number in $($app.Name) args: $($app.InstallArgs)"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-PreInstallLog "Machine number: $machineNum (default placeholder)"
|
||||
}
|
||||
} else {
|
||||
Write-PreInstallLog "No machine-number.txt found (using 9999 default)"
|
||||
}
|
||||
|
||||
# --- Pre-stage UDC settings backup if available. UDC_Setup.exe tries to
|
||||
# pull udc_settings_<num>.json from \\tsgwp00525\shared\SPC\UDC\Settings_Backups\,
|
||||
# but at imaging time that share is unreachable (no SFLD creds yet).
|
||||
# We carry a local mirror of Settings_Backups on the PXE server at
|
||||
# Y:\pre-install\udc-backups\. Pre-creating C:\ProgramData\UDC\udc_settings.json
|
||||
# before UDC_Setup.exe runs means the installer's File.Copy (overwrite:true)
|
||||
# would overwrite it IF the share were reachable, but since it isn't, our
|
||||
# pre-staged file survives and UDC launches with correct settings.
|
||||
# UDC payload (settings backups + webserver settings) lives only in the
|
||||
# collections per-pc-type dir - UDC is the "C" of "collections". On nocoll
|
||||
# bays the dir doesn't exist; Test-Path skips silently.
|
||||
$udcCollDir = Join-Path (Split-Path $PSScriptRoot -Parent) 'gea-shopfloor-collections'
|
||||
if ($machineNum -and $machineNum -ne '9999') {
|
||||
$udcBackupDir = Join-Path $udcCollDir 'udc-backups'
|
||||
$udcBackup = Join-Path $udcBackupDir "udc_settings_$machineNum.json"
|
||||
$udcTarget = 'C:\ProgramData\UDC\udc_settings.json'
|
||||
if (Test-Path -LiteralPath $udcBackup) {
|
||||
if (-not (Test-Path 'C:\ProgramData\UDC')) {
|
||||
New-Item -Path 'C:\ProgramData\UDC' -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $udcBackup -Destination $udcTarget -Force
|
||||
Write-PreInstallLog "Pre-staged UDC settings from $udcBackup -> $udcTarget"
|
||||
} else {
|
||||
Write-PreInstallLog "No UDC settings backup for machine $machineNum at $udcBackup (skipping - normal for nocoll bays)"
|
||||
}
|
||||
}
|
||||
|
||||
$udcWebSrc = Join-Path $udcCollDir 'udc_webserver_settings.json'
|
||||
$udcWebDst = 'C:\ProgramData\UDC\udc_webserver_settings.json'
|
||||
if (Test-Path -LiteralPath $udcWebSrc) {
|
||||
if (-not (Test-Path 'C:\ProgramData\UDC')) {
|
||||
New-Item -Path 'C:\ProgramData\UDC' -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $udcWebSrc -Destination $udcWebDst -Force
|
||||
Write-PreInstallLog "Pre-staged UDC webserver settings from $udcWebSrc -> $udcWebDst"
|
||||
} else {
|
||||
Write-PreInstallLog "No UDC webserver settings file at $udcWebSrc (skipping - normal for nocoll bays)"
|
||||
}
|
||||
|
||||
# --- Suppress Windows Defender Firewall "Allow access" prompts globally for
|
||||
# this preinstall pass. Oracle's OUI extracts a JRE to a per-run
|
||||
# %TEMP%\OraInstall<timestamp>\jdk\jre\bin\java.exe and binds a localhost
|
||||
# port, which trips the unsigned-app firewall dialog. The temp path is
|
||||
# timestamp-randomized so a path-based -Program rule can't match it.
|
||||
# Turning off NotifyOnListen stops the prompt without weakening the
|
||||
# firewall's actual block decisions.
|
||||
try {
|
||||
Set-NetFirewallProfile -Profile Domain,Public,Private -NotifyOnListen False -ErrorAction Stop
|
||||
Write-PreInstallLog "Firewall NotifyOnListen disabled (Domain/Public/Private)"
|
||||
} catch {
|
||||
Write-PreInstallLog "Failed to disable firewall NotifyOnListen: $_" "WARN"
|
||||
}
|
||||
|
||||
# --- Pre-enable .NET Framework 3.5. Oracle 11.2 OUI uses a .NET 2.0/3.5
|
||||
# component on its welcome path; on Win10/11 NetFx3 is OFF by default
|
||||
# and the first 3.5 API call pops the "Download and install this feature"
|
||||
# dialog. Enable-WindowsOptionalFeature pulls payload from Windows Update
|
||||
# when available; on hosts without internet at preinstall time this will
|
||||
# fail and Oracle will still prompt (TODO: stage NetFx3 sxs cab on image
|
||||
# and pass -Source for true air-gap support). Idempotent no-op when
|
||||
# already enabled. Mirrors the per-PC enable in CMM/09-Setup-CMM.ps1.
|
||||
try {
|
||||
$netfx = Get-WindowsOptionalFeature -Online -FeatureName 'NetFx3' -ErrorAction Stop
|
||||
if ($netfx.State -eq 'Enabled') {
|
||||
Write-PreInstallLog ".NET Framework 3.5 already enabled"
|
||||
} else {
|
||||
Write-PreInstallLog ".NET Framework 3.5 state is $($netfx.State) - enabling now (may take a minute)..."
|
||||
$result = Enable-WindowsOptionalFeature -Online -FeatureName 'NetFx3' -All -NoRestart -ErrorAction Stop
|
||||
Write-PreInstallLog " Enable-WindowsOptionalFeature RestartNeeded=$($result.RestartNeeded)"
|
||||
}
|
||||
} catch {
|
||||
Write-PreInstallLog "Failed to enable .NET 3.5: $_" "WARN"
|
||||
Write-PreInstallLog " Continuing - Oracle and other 3.5-dependent installers may surface a Windows feature-install prompt"
|
||||
}
|
||||
|
||||
# --- Pre-create Windows Firewall rules for UDC + MTConnect Agent so the
|
||||
# installer doesn't pop firewall-allow dialogs during silent install.
|
||||
# Rules are idempotent (New-NetFirewallRule -ErrorAction SilentlyContinue
|
||||
# on existing rule name is a no-op in practice; we remove-then-add).
|
||||
$fwRules = @(
|
||||
@{ Name = 'UDC'; Program = 'C:\Program Files\UDC\UDC.exe' },
|
||||
@{ Name = 'UDC MTConnect Agent'; Program = 'C:\ProgramData\UDC\MTConnect_UDC\Agent\agent.exe' }
|
||||
)
|
||||
foreach ($r in $fwRules) {
|
||||
try {
|
||||
Remove-NetFirewallRule -DisplayName $r.Name -ErrorAction SilentlyContinue
|
||||
New-NetFirewallRule -DisplayName $r.Name -Direction Inbound -Program $r.Program `
|
||||
-Action Allow -Profile Any -ErrorAction Stop | Out-Null
|
||||
Write-PreInstallLog "Firewall rule created: $($r.Name) -> $($r.Program)"
|
||||
} catch {
|
||||
Write-PreInstallLog "Firewall rule '$($r.Name)' failed: $_" "WARN"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Detection helper (mirrors Simple-Install.ps1's Test-ApplicationInstalled) ---
|
||||
function Test-AppInstalled {
|
||||
param($App)
|
||||
|
||||
if (-not $App.DetectionMethod) {
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($App.DetectionMethod) {
|
||||
"Registry" {
|
||||
if (-not (Test-Path $App.DetectionPath)) {
|
||||
return $false
|
||||
}
|
||||
if ($App.DetectionName) {
|
||||
$value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue
|
||||
if (-not $value) {
|
||||
return $false
|
||||
}
|
||||
if ($App.DetectionValue) {
|
||||
return ($value.$($App.DetectionName) -eq $App.DetectionValue)
|
||||
}
|
||||
return $true
|
||||
}
|
||||
return $true
|
||||
}
|
||||
"File" {
|
||||
return Test-Path $App.DetectionPath
|
||||
}
|
||||
default {
|
||||
Write-PreInstallLog " Unknown detection method: $($App.DetectionMethod)" "WARN"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-PreInstallLog " Detection check threw: $_" "WARN"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- Iterate apps ---
|
||||
$installed = 0
|
||||
$skipped = 0
|
||||
$failed = 0
|
||||
|
||||
foreach ($app in $config.Applications) {
|
||||
# Cancel any reboot a previous installer scheduled (some respect /norestart, some
|
||||
# don't - VC++ 2008's bootstrapper sometimes triggers an immediate Windows reboot
|
||||
# despite the flag). Doing this BEFORE each install protects the rest of the loop.
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
Write-PreInstallLog "==> $($app.Name)"
|
||||
|
||||
# Filter by PCTypes - matches on wildcard, base type, composite type-subtype
|
||||
# key, OR any alias-equivalent name. The 2026-05-04 rename reorg gave us
|
||||
# gea-shopfloor-* PC type strings that need to match legacy ["Standard"],
|
||||
# ["CMM"], etc filters in preinstall.json (and vice versa: a future
|
||||
# preinstall.json that uses gea-shopfloor-* should still match a fleet PC
|
||||
# whose pc-type.txt is still "Standard").
|
||||
$allowedTypes = @($app.PCTypes)
|
||||
$aliasGroups = @(
|
||||
@('Standard', 'Standard-Machine','Standard-Timeclock','gea-shopfloor-collections','gea-shopfloor-nocollections','gea-shopfloor-common'),
|
||||
@('Standard-Machine', 'gea-shopfloor-collections','gea-shopfloor-nocollections'),
|
||||
@('Standard-Timeclock', 'gea-shopfloor-common'),
|
||||
@('CMM', 'gea-shopfloor-cmm'),
|
||||
@('Keyence', 'gea-shopfloor-keyence'),
|
||||
@('Lab', 'gea-shopfloor-common'),
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat'),
|
||||
@('PartMarker', 'gea-shopfloor-partmarker')
|
||||
)
|
||||
$myNames = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($n in @($pcType, $pcProfileKey) | Where-Object { $_ }) {
|
||||
[void]$myNames.Add($n)
|
||||
foreach ($g in $aliasGroups) {
|
||||
if ($g -icontains $n) { foreach ($x in $g) { [void]$myNames.Add($x) } }
|
||||
}
|
||||
}
|
||||
# PCTypesStrict=true bypasses the alias-expansion matcher and requires
|
||||
# the actual pcType (or composite pcProfileKey) to literally equal one
|
||||
# of the allowedTypes entries. Used by UDC because the alias graph
|
||||
# transitively connects gea-shopfloor-collections <-> nocollections via
|
||||
# the legacy 'Standard' group, which would otherwise cause UDC to install
|
||||
# on nocoll bays even with PCTypes=['gea-shopfloor-collections'].
|
||||
$matchesType = ($allowedTypes -contains '*')
|
||||
if (-not $matchesType) {
|
||||
if ($app.PCTypesStrict) {
|
||||
foreach ($t in $allowedTypes) {
|
||||
if (($pcType -ieq $t) -or ($pcProfileKey -ieq $t)) { $matchesType = $true; break }
|
||||
}
|
||||
} else {
|
||||
foreach ($t in $allowedTypes) {
|
||||
if ($myNames.Contains($t)) { $matchesType = $true; break }
|
||||
foreach ($g in $aliasGroups) {
|
||||
if ($g -icontains $t) {
|
||||
foreach ($x in $g) { if ($myNames.Contains($x)) { $matchesType = $true; break } }
|
||||
}
|
||||
if ($matchesType) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $matchesType) {
|
||||
Write-PreInstallLog " PCTypes filter excludes '$pcProfileKey' (allowed: $($allowedTypes -join ', ')) - skipping"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
# Detection check
|
||||
if (Test-AppInstalled -App $app) {
|
||||
Write-PreInstallLog " Already installed - skipping"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
# Locate installer file
|
||||
$installerPath = Join-Path $installerDir $app.Installer
|
||||
if (-not (Test-Path $installerPath)) {
|
||||
Write-PreInstallLog " Installer file not found: $installerPath" "ERROR"
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
Write-PreInstallLog " Installing from $installerPath"
|
||||
if ($app.InstallArgs) {
|
||||
Write-PreInstallLog " InstallArgs: $($app.InstallArgs)"
|
||||
}
|
||||
|
||||
# Per-app verbose msiexec log path - written by /L*v on MSI installs and tailed
|
||||
# into the runner log on failure so we don't need to dig through C:\Logs manually.
|
||||
$safeName = $app.Name -replace '[^a-zA-Z0-9]','_'
|
||||
$msiLog = Join-Path $logDir "msi-$safeName.log"
|
||||
if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
try {
|
||||
# We use [System.Diagnostics.Process]::Start() directly instead of Start-Process
|
||||
# because PowerShell 5.1's Start-Process -PassThru has a bug where the returned
|
||||
# Process object's OS handle is disposed when control returns to the script,
|
||||
# causing ExitCode to always read as $null even after WaitForExit() succeeds.
|
||||
# Calling Process.Start() directly returns a Process object with a live handle
|
||||
# that exposes ExitCode correctly. We poll HasExited so UDC_Setup.exe (which
|
||||
# hangs forever after spawning a hidden WPF window) can be killed via the
|
||||
# detection-during-install path.
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
||||
|
||||
if ($app.Type -eq "MSI") {
|
||||
$psi.FileName = "msiexec.exe"
|
||||
$psi.Arguments = "/i `"$installerPath`""
|
||||
if ($app.InstallArgs) {
|
||||
$psi.Arguments += " " + $app.InstallArgs
|
||||
}
|
||||
$psi.Arguments += " /L*v `"$msiLog`""
|
||||
Write-PreInstallLog " msiexec verbose log: $msiLog"
|
||||
}
|
||||
elseif ($app.Type -eq "EXE") {
|
||||
$psi.FileName = $installerPath
|
||||
if ($app.InstallArgs) {
|
||||
$psi.Arguments = $app.InstallArgs
|
||||
}
|
||||
# If the JSON entry specifies a LogFile (per-installer log written by
|
||||
# the EXE itself - e.g. Setup-OpenText.ps1 -> C:\Logs\PreInstall\Setup-
|
||||
# OpenText.log), surface it on failure the same way we surface the
|
||||
# msiexec /L*v verbose log for MSI installs. Lets EXE wrappers actually
|
||||
# report what went wrong inside.
|
||||
if ($app.LogFile) {
|
||||
Write-PreInstallLog " Installer log: $($app.LogFile)"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-PreInstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR"
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
# Poll for completion: process exit OR detection success (whichever happens first)
|
||||
$timeoutSec = 600 # 10 min hard cap per app
|
||||
$pollInterval = 5
|
||||
$elapsed = 0
|
||||
$killedAfterDetect = $false
|
||||
|
||||
while ($elapsed -lt $timeoutSec) {
|
||||
if ($proc.HasExited) { break }
|
||||
|
||||
# Detection-during-install kill is OPT-IN via "KillAfterDetection: true"
|
||||
# in the JSON entry. It's only safe for installers that hang forever after
|
||||
# creating their detection markers (UDC_Setup.exe spawns a hidden WPF window
|
||||
# and never exits). For normal installers (Oracle, msiexec, etc.) the
|
||||
# detection registry key often gets written midway through the install AND
|
||||
# msiexec is still doing post-install cleanup AND msiserver is still holding
|
||||
# the install mutex - killing msiexec at that point leaves the system in a
|
||||
# bad state and the NEXT msiexec call returns 1618 ERROR_INSTALL_ALREADY_
|
||||
# RUNNING. So opt-out by default; only kill apps that explicitly need it.
|
||||
if ($app.KillAfterDetection -and (Test-AppInstalled -App $app)) {
|
||||
Write-PreInstallLog " Detection passed at $elapsed s - killing installer to advance (KillAfterDetection)"
|
||||
try { $proc.Kill(); $proc.WaitForExit(5000) | Out-Null } catch { }
|
||||
|
||||
# UDC's installer auto-launches UDC.exe silently; it's UDC.exe
|
||||
# (not UDC_Setup) that actually writes udc_settings.json from
|
||||
# the CLI args. Killing it immediately after the detection
|
||||
# marker appears loses the MachineNumber write. Wait (up to
|
||||
# 15s) for settings.json to reflect the target number, then
|
||||
# kill. Works uniformly for tech-typed numbers and the 9999
|
||||
# placeholder - whichever was passed as arg 2 is what we
|
||||
# expect to see persist.
|
||||
if ($app.Name -eq "UDC") {
|
||||
$udcJson = 'C:\ProgramData\UDC\udc_settings.json'
|
||||
$targetNum = $null
|
||||
if ($app.InstallArgs -match '-(\d+)\s*$') { $targetNum = $matches[1] }
|
||||
$waitSec = 0
|
||||
while ($waitSec -lt 15) {
|
||||
if (Test-Path $udcJson) {
|
||||
try {
|
||||
$mn = (Get-Content $udcJson -Raw -ErrorAction Stop |
|
||||
ConvertFrom-Json).GeneralSettings.MachineNumber
|
||||
if ($mn -and $mn -eq $targetNum) {
|
||||
Write-PreInstallLog " UDC persisted MachineNumber=$mn after $waitSec s"
|
||||
break
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
$waitSec++
|
||||
}
|
||||
if ($waitSec -ge 15) {
|
||||
Write-PreInstallLog " UDC did not persist MachineNumber within 15 s - killing anyway" "WARN"
|
||||
}
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill(); $_.WaitForExit(2000) | Out-Null } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
$killedAfterDetect = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $pollInterval
|
||||
$elapsed += $pollInterval
|
||||
}
|
||||
|
||||
if (-not $proc.HasExited -and -not $killedAfterDetect) {
|
||||
Write-PreInstallLog " TIMEOUT after $timeoutSec seconds - killing installer" "ERROR"
|
||||
try { $proc.Kill() } catch { }
|
||||
if ($app.Name -eq "UDC") {
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill() } catch { }
|
||||
}
|
||||
}
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
if ($killedAfterDetect) {
|
||||
Write-PreInstallLog " SUCCESS (verified via detection during install)"
|
||||
$installed++
|
||||
}
|
||||
else {
|
||||
# Start-Process -PassThru returns a Process object whose ExitCode is null
|
||||
# until WaitForExit() is called - even if HasExited is already true. Without
|
||||
# this, every install that goes through the exit-code branch gets reported
|
||||
# as "Exit code - FAILED" (empty) regardless of the real result.
|
||||
try { $proc.WaitForExit() } catch { }
|
||||
$exitCode = $proc.ExitCode
|
||||
|
||||
if ($exitCode -eq 0 -or $exitCode -eq 3010) {
|
||||
Write-PreInstallLog " Exit code $exitCode after $elapsed s - SUCCESS"
|
||||
if ($exitCode -eq 3010) {
|
||||
Write-PreInstallLog " (Reboot required for $($app.Name))"
|
||||
}
|
||||
$installed++
|
||||
}
|
||||
else {
|
||||
Write-PreInstallLog " Exit code $exitCode - FAILED" "ERROR"
|
||||
|
||||
# If the JSON entry specifies a LogFile for an EXE install (e.g.
|
||||
# Setup-OpenText.ps1 writes its own log at C:\Logs\PreInstall\Setup-
|
||||
# OpenText.log), tail it here so the runner log surfaces the actual
|
||||
# cause without us having to dig through C:\Logs manually.
|
||||
if ($app.Type -eq "EXE" -and $app.LogFile -and (Test-Path $app.LogFile)) {
|
||||
Write-PreInstallLog " --- last 30 lines of $($app.LogFile) ---"
|
||||
Get-Content $app.LogFile -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-PreInstallLog " $_"
|
||||
}
|
||||
Write-PreInstallLog " --- end installer log tail ---"
|
||||
}
|
||||
|
||||
# Surface the *meaningful* lines from the verbose msiexec log (not just
|
||||
# the tail, which is rollback/cleanup noise). The actual root-cause lines
|
||||
# are: MSI error notes (Note: 1: <code>), failed action returns (return
|
||||
# value 3), errors logged by custom actions, and any line containing the
|
||||
# word "Error". Grep these out so the runner log shows the smoking gun.
|
||||
if ($app.Type -eq "MSI" -and (Test-Path $msiLog)) {
|
||||
Write-PreInstallLog " --- meaningful lines from $msiLog ---"
|
||||
$patterns = @(
|
||||
'Note: 1: ',
|
||||
'return value 3',
|
||||
'Error \d+\.',
|
||||
'CustomAction .* returned actual error',
|
||||
'Failed to ',
|
||||
'Installation failed',
|
||||
'Calling custom action',
|
||||
'1: 2262', # Database not found
|
||||
'1: 2203', # Cannot open database
|
||||
'1: 2330' # Insufficient permissions
|
||||
)
|
||||
$regex = ($patterns -join '|')
|
||||
$matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 30
|
||||
if ($matches) {
|
||||
foreach ($m in $matches) {
|
||||
Write-PreInstallLog " $($m.Line.Trim())"
|
||||
}
|
||||
} else {
|
||||
Write-PreInstallLog " (no error patterns matched - falling back to last 25 lines)"
|
||||
Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-PreInstallLog " $_"
|
||||
}
|
||||
}
|
||||
Write-PreInstallLog " --- end MSI log scan ---"
|
||||
}
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-PreInstallLog " Install threw: $_" "ERROR"
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
|
||||
Write-PreInstallLog "============================================"
|
||||
Write-PreInstallLog "PreInstall complete: $installed installed, $skipped skipped, $failed failed"
|
||||
Write-PreInstallLog "============================================"
|
||||
|
||||
# Final reboot cancel - if the last installer in the loop scheduled one, the
|
||||
# dispatcher's later `shutdown /a` won't fire until the next baseline script starts.
|
||||
# Cancel here so control returns cleanly to Run-ShopfloorSetup.ps1.
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
exit 0
|
||||
@@ -1,127 +0,0 @@
|
||||
# 03-ShellDefaults.ps1 - Shell appearance defaults for all new user profiles.
|
||||
#
|
||||
# Applies the following tweaks so shopfloor PCs look/behave closer to the
|
||||
# Win10 baseline and don't reach out to Bing from the Start menu:
|
||||
# 1. Taskbar aligned to the left (TaskbarAl=0). Per-user setting, written
|
||||
# into the Default User hive so every NEW profile inherits it.
|
||||
# 2. Start menu "Recommended" section hidden (HideRecommendedSection=1).
|
||||
# 3. Web results + Bing search-box suggestions killed in Start/Search.
|
||||
# 4. Cortana disabled via policy (no-op on LTSC 2024 since Cortana isn't
|
||||
# shipped, but set anyway for belt-and-suspenders).
|
||||
#
|
||||
# 2-4 are HKLM policy values - W11 LTSC / Enterprise honour them; Home/Pro
|
||||
# do not. Fine here since the fleet is LTSC 2024.
|
||||
#
|
||||
# Existing profiles (SupportUser, pre-existing end-user accounts) are NOT
|
||||
# modified for TaskbarAl - the Default User hive is a template copied on
|
||||
# profile creation, not a live cascade. The HKLM policies affect everyone.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host "ERROR: 03-ShellDefaults.ps1 must run as Administrator." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---- 1. HKLM policy DWORDs (all LTSC-honoured) -------------------------------
|
||||
# Each entry: KeyPath, ValueName, ValueData. Key is created if missing.
|
||||
$hklmPolicies = @(
|
||||
# Hide Start menu "Recommended" section.
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer'
|
||||
Name = 'HideRecommendedSection'; Value = 1 }
|
||||
|
||||
# Kill web results in Start/taskbar search.
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'
|
||||
Name = 'DisableWebSearch'; Value = 1 }
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'
|
||||
Name = 'ConnectedSearchUseWeb'; Value = 0 }
|
||||
|
||||
# Disable Cortana (policy; LTSC doesn't ship Cortana but this is harmless).
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'
|
||||
Name = 'AllowCortana'; Value = 0 }
|
||||
|
||||
# Kill Bing-powered search-box suggestions (per-keystroke telemetry).
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer'
|
||||
Name = 'DisableSearchBoxSuggestions'; Value = 1 }
|
||||
)
|
||||
|
||||
foreach ($p in $hklmPolicies) {
|
||||
try {
|
||||
if (-not (Test-Path $p.Key)) {
|
||||
New-Item -Path $p.Key -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $p.Key -Name $p.Name -Value $p.Value -Type DWord -Force
|
||||
Write-Host ("Set {0}\{1} = {2}" -f $p.Key, $p.Name, $p.Value)
|
||||
} catch {
|
||||
Write-Warning ("Failed to set {0}\{1}: {2}" -f $p.Key, $p.Name, $_)
|
||||
}
|
||||
}
|
||||
|
||||
# ---- 2. Set Adobe Acrobat Reader as default PDF viewer ----
|
||||
# OEMDefaultAssociations.xml is read when a new user profile is created.
|
||||
# Deploying it before ShopFloor's first logon means the profile lands with
|
||||
# Acrobat as the PDF handler instead of Edge. Existing profiles (SupportUser)
|
||||
# are not affected. DISM /import-defaultappassociations writes the XML to
|
||||
# C:\Windows\System32\OEMDefaultAssociations.xml and sets the policy key.
|
||||
$assocXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<DefaultAssociations>
|
||||
<Association Identifier=".pdf" ProgId="Acrobat.Document.DC" ApplicationName="Adobe Acrobat" />
|
||||
</DefaultAssociations>
|
||||
"@
|
||||
$assocPath = Join-Path $env:TEMP 'DefaultAssociations.xml'
|
||||
try {
|
||||
Set-Content -Path $assocPath -Value $assocXml -Encoding UTF8 -Force
|
||||
$out = & dism.exe /online /import-defaultappassociations:"$assocPath" 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Set .pdf default to Acrobat.Document.DC via DISM"
|
||||
} else {
|
||||
Write-Warning "DISM import-defaultappassociations returned $LASTEXITCODE"
|
||||
}
|
||||
Remove-Item $assocPath -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Warning "Failed to set default PDF viewer: $_"
|
||||
}
|
||||
|
||||
# ---- 3. Taskbar left-align via Default User hive ----
|
||||
$defaultHive = 'C:\Users\Default\NTUSER.DAT'
|
||||
$mountPoint = 'HKU\SFLDDefault'
|
||||
$regPath = 'Registry::HKEY_USERS\SFLDDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $defaultHive)) {
|
||||
Write-Warning "Default User hive not found at $defaultHive - skipping taskbar tweak."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Unload a stale mount from a previous run, if any. reg unload returns
|
||||
# non-zero when nothing is mounted; ignore that.
|
||||
& reg.exe unload $mountPoint *> $null
|
||||
|
||||
$loadOutput = & reg.exe load $mountPoint $defaultHive 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "reg load failed ($LASTEXITCODE): $loadOutput"
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not (Test-Path $regPath)) {
|
||||
New-Item -Path $regPath -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $regPath -Name 'TaskbarAl' -Value 0 -Type DWord -Force
|
||||
Write-Host "Set Default User TaskbarAl=0 (left-aligned)"
|
||||
} catch {
|
||||
Write-Warning "Failed to set TaskbarAl: $_"
|
||||
} finally {
|
||||
# Force GC so PowerShell drops its handles on the hive before unload,
|
||||
# otherwise reg unload fails with "The process cannot access the file".
|
||||
[gc]::Collect()
|
||||
[gc]::WaitForPendingFinalizers()
|
||||
[gc]::Collect()
|
||||
& reg.exe unload $mountPoint *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "reg unload returned $LASTEXITCODE - Default User hive may still be mounted"
|
||||
}
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,15 +0,0 @@
|
||||
# 04-NetworkAndWinRM.ps1 -- Set network profiles to Private and enable WinRM (baseline)
|
||||
|
||||
# --- Transcript ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} }
|
||||
try { Start-Transcript -Path (Join-Path $logDir '04-NetworkAndWinRM.log') -Append -Force | Out-Null } catch {}
|
||||
|
||||
# --- Set all network profiles to Private ---
|
||||
Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private
|
||||
Write-Host "All network profiles set to Private."
|
||||
|
||||
# --- Enable and configure WinRM ---
|
||||
Enable-PSRemoting -Force -SkipNetworkProfileCheck
|
||||
Write-Host "WinRM enabled."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
@@ -1,101 +0,0 @@
|
||||
# 05-OfficeShortcuts.ps1 - Public desktop + Default User Start Menu shortcuts for
|
||||
# the core Office apps (Excel, Word, PowerPoint).
|
||||
#
|
||||
# Only runs if Office is actually installed - self-detects via EXE existence so it
|
||||
# silently no-ops on PC types whose ppkg didn't include Office (Display kiosks,
|
||||
# Wax/Trace, etc.). Office Click-to-Run installs the binaries but doesn't create
|
||||
# desktop shortcuts; this script fills that gap.
|
||||
#
|
||||
# Shortcuts land in two places:
|
||||
# - C:\Users\Public\Desktop\ - visible to all users immediately
|
||||
# - C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\
|
||||
# - inherited by every NEW user profile
|
||||
# (Azure AD logons after the device is
|
||||
# enrolled)
|
||||
#
|
||||
# Idempotent: overwrites existing .lnk files each run.
|
||||
|
||||
# --- Transcript ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} }
|
||||
try { Start-Transcript -Path (Join-Path $logDir '05-OfficeShortcuts.log') -Append -Force | Out-Null } catch {}
|
||||
|
||||
$officeApps = @(
|
||||
@{ Exe = 'EXCEL.EXE'; Name = 'Excel' }
|
||||
@{ Exe = 'WINWORD.EXE'; Name = 'Word' }
|
||||
@{ Exe = 'POWERPNT.EXE'; Name = 'PowerPoint' }
|
||||
)
|
||||
|
||||
# Office binary location varies by install type and version:
|
||||
# - Office 2019+ / M365 / LTSC Click-to-Run: <PF>\Microsoft Office\root\Office16\
|
||||
# - Office 2016 MSI (legacy): <PF>\Microsoft Office\Office16\ (no root\)
|
||||
# - Office 2013 MSI: <PF>\Microsoft Office\Office15\
|
||||
# - x86 Office on 64-bit Windows: Program Files (x86)\...
|
||||
# The standard ShopFloor Office ppkg is x86 Click-to-Run so normal case
|
||||
# is "Program Files (x86)\Microsoft Office\root\Office16\EXCEL.EXE", but
|
||||
# we check all variants so the script works across SKUs.
|
||||
$officeSearchRoots = @(
|
||||
'C:\Program Files\Microsoft Office\root\Office16',
|
||||
'C:\Program Files (x86)\Microsoft Office\root\Office16',
|
||||
'C:\Program Files\Microsoft Office\Office16',
|
||||
'C:\Program Files (x86)\Microsoft Office\Office16',
|
||||
'C:\Program Files\Microsoft Office\Office15',
|
||||
'C:\Program Files (x86)\Microsoft Office\Office15'
|
||||
)
|
||||
|
||||
$officeRoot = $null
|
||||
foreach ($base in $officeSearchRoots) {
|
||||
if (Test-Path (Join-Path $base 'EXCEL.EXE')) {
|
||||
$officeRoot = $base
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $officeRoot) {
|
||||
Write-Host "No Office install detected - skipping shortcut creation."
|
||||
Write-Host "Searched:"
|
||||
foreach ($p in $officeSearchRoots) {
|
||||
$hit = if (Test-Path $p) { "(dir exists)" } else { "(missing)" }
|
||||
Write-Host " $p $hit"
|
||||
}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Office detected at: $officeRoot"
|
||||
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$defaultStart = 'C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs'
|
||||
|
||||
if (-not (Test-Path $defaultStart)) {
|
||||
New-Item -Path $defaultStart -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
|
||||
foreach ($app in $officeApps) {
|
||||
$exePath = Join-Path $officeRoot $app.Exe
|
||||
if (-not (Test-Path $exePath)) {
|
||||
Write-Warning "$($app.Name) EXE not found at $exePath - skipping."
|
||||
continue
|
||||
}
|
||||
foreach ($dest in @($publicDesktop, $defaultStart)) {
|
||||
$lnkPath = Join-Path $dest "$($app.Name).lnk"
|
||||
try {
|
||||
$sc = $shell.CreateShortcut($lnkPath)
|
||||
$sc.TargetPath = $exePath
|
||||
$sc.WorkingDirectory = $officeRoot
|
||||
$sc.IconLocation = "$exePath,0"
|
||||
$sc.Save()
|
||||
Write-Host " $($app.Name) -> $lnkPath"
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to create $lnkPath : $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null
|
||||
Write-Host "Office shortcuts created."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,646 +0,0 @@
|
||||
# 06-OrganizeDesktop.ps1 - Single source of truth for the Public Desktop
|
||||
# layout. Builds three category folders and fills them, so end users see
|
||||
# a clean desktop instead of 20+ loose icons.
|
||||
#
|
||||
# Creates three subfolders under C:\Users\Public\Desktop:
|
||||
# Office\ - Excel, Word, PowerPoint, Outlook, OneNote, etc.
|
||||
# Shopfloor Tools\ - UDC, eDNC, HostExplorer, MarkZebra, PC-DMIS, Hexagon,
|
||||
# WJ Shopfloor, Defect_Tracker, NTLARS, etc.
|
||||
# Web Links\ - .url shortcuts (internal portals)
|
||||
#
|
||||
# Two phases:
|
||||
# PHASE 1 (Invoke-DesktopSweep): walks everything at the desktop ROOT,
|
||||
# classifies each .lnk / .url by filename / extension / .lnk target, and
|
||||
# moves it into the right category folder. Unknown items are LEFT AT
|
||||
# THE ROOT on purpose. eDNC.lnk and NTLARS.lnk are allowlisted to stay
|
||||
# at the root - end users click them too often to bury in a folder.
|
||||
#
|
||||
# PHASE 2 (Add-ShopfloorToolsApps): materializes specific app shortcuts
|
||||
# into Shopfloor Tools\ from known .exe paths (UDC, eDNC, NTLARS). For
|
||||
# MSI-advertised shortcuts that don't have a resolvable target (WJ
|
||||
# Shopfloor, Defect_Tracker), finds the existing .lnk wherever it lives
|
||||
# and copies it in. This runs AFTER the sweep so it overwrites any
|
||||
# stale copies the sweeper may have moved in.
|
||||
#
|
||||
# Also registers an "Organize Public Desktop" scheduled task that re-runs
|
||||
# the sweep (phase 1) at every logon. Phase 2 doesn't re-run - the
|
||||
# shortcuts it creates are stable and 06 on first run populates them.
|
||||
#
|
||||
# 07-TaskbarLayout.ps1 reads the result of this script and writes
|
||||
# LayoutModification.xml to pin specific shortcuts to the taskbar.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# ============================================================================
|
||||
# Admin check - writing to C:\Users\Public\Desktop\* and registering
|
||||
# system-wide scheduled tasks both require elevation. Fail fast if not
|
||||
# admin so we don't spam half-successful garbage across the output.
|
||||
# ============================================================================
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: 06-OrganizeDesktop.ps1 must run as Administrator." -ForegroundColor Red
|
||||
Write-Host " Re-run from an elevated PowerShell:" -ForegroundColor Red
|
||||
Write-Host " Start -> type 'powershell' -> right-click -> Run as administrator" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Transcript. 06 runs as a SYSTEM scheduled task at every logon via the
|
||||
# 'Organize Public Desktop' task, so without a transcript its decisions
|
||||
# (what got added, what got suppressed) are invisible. Append mode keeps
|
||||
# a history across logons; session header + PID makes individual runs
|
||||
# easy to find.
|
||||
$transcriptDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $transcriptDir)) {
|
||||
try { New-Item -ItemType Directory -Path $transcriptDir -Force | Out-Null } catch {}
|
||||
}
|
||||
$transcriptPath = Join-Path $transcriptDir '06-OrganizeDesktop.log'
|
||||
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
||||
Write-Host ""
|
||||
Write-Host "================================================================"
|
||||
Write-Host "=== 06-OrganizeDesktop session start (PID $PID, $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
Write-Host "================================================================"
|
||||
|
||||
# ============================================================================
|
||||
# Cleanup: remove 'Shopfloor Intune Sync' task after sync completes
|
||||
#
|
||||
# Monitor-IntuneProgress.ps1 registers that task as BUILTIN\Users + RunLevel
|
||||
# Limited (needs a user context to show the QR code / Configure-PC prompt),
|
||||
# and a Limited task cannot Unregister-ScheduledTask itself. So when sync
|
||||
# completes it just writes C:\Enrollment\sync-complete.txt as a marker and
|
||||
# the task stays around firing on every logon as a no-op.
|
||||
#
|
||||
# 06 runs as SYSTEM via its own sweep task, which DOES have permission to
|
||||
# delete any task. When we see the marker, drop the stale task so Task
|
||||
# Scheduler doesn't keep the dead entry forever.
|
||||
# ============================================================================
|
||||
$syncCompleteMarker = 'C:\Enrollment\sync-complete.txt'
|
||||
$syncTaskName = 'Shopfloor Intune Sync'
|
||||
if (Test-Path -LiteralPath $syncCompleteMarker) {
|
||||
$syncTask = Get-ScheduledTask -TaskName $syncTaskName -ErrorAction SilentlyContinue
|
||||
if ($syncTask) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $syncTaskName -Confirm:$false -ErrorAction Stop
|
||||
Write-Host "Removed stale '$syncTaskName' task (sync complete, marker present)."
|
||||
} catch {
|
||||
Write-Warning "Failed to remove '$syncTaskName' task: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Load site config + PC profile
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools'
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
$scriptDir = Split-Path -Parent $scriptPath
|
||||
|
||||
# Filenames that always stay at the desktop root regardless of classification.
|
||||
# Empty right now - every shortcut lives inside a category folder for a
|
||||
# clean end-user desktop. Add entries here if a future shortcut needs to
|
||||
# stay pinned at root.
|
||||
$keepAtRoot = @()
|
||||
|
||||
# ============================================================================
|
||||
# Phase 1: Sweep loose shortcuts at desktop root into category folders
|
||||
# ============================================================================
|
||||
function Invoke-DesktopSweep {
|
||||
param([string]$DesktopPath)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $DesktopPath)) {
|
||||
Write-Host "Public desktop not found at $DesktopPath - skipping sweep."
|
||||
return
|
||||
}
|
||||
|
||||
# Category definitions. Order matters: first match wins. Each category
|
||||
# can classify by file Extension, file Name (regex), or .lnk Target
|
||||
# (regex, resolved via WScript.Shell).
|
||||
$categories = [ordered]@{
|
||||
'Office' = @{
|
||||
Name = @(
|
||||
'^(Excel|Word|PowerPoint|Outlook|OneNote|Access|Publisher|OneDrive)(\s|$)'
|
||||
)
|
||||
Target = @(
|
||||
'\\(EXCEL|WINWORD|POWERPNT|OUTLOOK|ONENOTE|MSACCESS|MSPUB)\.EXE$',
|
||||
'\\OneDrive\.exe$'
|
||||
)
|
||||
}
|
||||
'Shopfloor Tools' = @{
|
||||
Name = @(
|
||||
'^UDC',
|
||||
'eDNC', '\bDNC\b', 'DncMain', 'GE DNC', 'NTLARS',
|
||||
'Host\s*Explorer', 'TN3270', 'TN5250', 'HE\s*3270', 'HE\s*5250',
|
||||
# OpenText / 'WJ Shopfloor' / 'ShopFloor' shortcuts left on
|
||||
# the desktop intentionally. The actual filename varies by
|
||||
# OpenText profile (e.g. 'WJ Shopfloor OpenText.lnk') so the
|
||||
# taskbar pin path mismatch silently skipped these. Leaving
|
||||
# them at the public desktop top level instead.
|
||||
'Defect[_\s-]?Tracker',
|
||||
'MarkZebra', 'Zebra',
|
||||
'PC-?DMIS',
|
||||
'Hexagon', 'CLM\s*Tools',
|
||||
'Blancco',
|
||||
'Keyence'
|
||||
)
|
||||
Target = @(
|
||||
'\\UDC\.exe$',
|
||||
'DncMain\.exe$', 'NTLARS\.exe$', 'DNCRemote',
|
||||
'HostExplorer\.exe$', 'hostex32\.exe$', 'humicon15\.exe$',
|
||||
'PC-?DMIS',
|
||||
'Hexagon',
|
||||
'Defect_Tracker\.application$'
|
||||
)
|
||||
}
|
||||
'Web Links' = @{
|
||||
Extension = @('.url')
|
||||
}
|
||||
}
|
||||
|
||||
# Category folders are created LAZILY - only when we're about to move
|
||||
# something into them - so a Display / Wax-Trace PC without Office
|
||||
# doesn't get an empty Office\ folder littering the desktop. See
|
||||
# Remove-EmptyCategoryFolders at the end of the script for the
|
||||
# post-sweep cleanup pass that finishes the job.
|
||||
|
||||
# WScript.Shell for resolving .lnk targets
|
||||
$shell = $null
|
||||
try { $shell = New-Object -ComObject WScript.Shell } catch {}
|
||||
|
||||
# Only sweep files at the desktop root, never recurse into the category
|
||||
# folders themselves (that would loop things back).
|
||||
$items = @(Get-ChildItem -LiteralPath $DesktopPath -File -ErrorAction SilentlyContinue)
|
||||
|
||||
$moved = 0
|
||||
$skipped = 0
|
||||
|
||||
foreach ($item in $items) {
|
||||
# Allowlist check: never sweep items the user wants to keep visible
|
||||
if ($keepAtRoot -contains $item.Name) {
|
||||
Write-Host " keep: $($item.Name) (allowlisted)"
|
||||
continue
|
||||
}
|
||||
|
||||
$category = $null
|
||||
|
||||
# 1. Extension match (cheapest)
|
||||
foreach ($cat in $categories.Keys) {
|
||||
$exts = $categories[$cat].Extension
|
||||
if ($exts -and ($exts -contains $item.Extension)) {
|
||||
$category = $cat
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# 2. File name regex match
|
||||
if (-not $category) {
|
||||
foreach ($cat in $categories.Keys) {
|
||||
$pats = $categories[$cat].Name
|
||||
if (-not $pats) { continue }
|
||||
foreach ($p in $pats) {
|
||||
if ($item.BaseName -match $p) {
|
||||
$category = $cat
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($category) { break }
|
||||
}
|
||||
}
|
||||
|
||||
# 3. .lnk target resolution
|
||||
if (-not $category -and $item.Extension -ieq '.lnk' -and $shell) {
|
||||
try {
|
||||
$sc = $shell.CreateShortcut($item.FullName)
|
||||
$target = $sc.TargetPath
|
||||
if ($target) {
|
||||
foreach ($cat in $categories.Keys) {
|
||||
$pats = $categories[$cat].Target
|
||||
if (-not $pats) { continue }
|
||||
foreach ($p in $pats) {
|
||||
if ($target -match $p) {
|
||||
$category = $cat
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($category) { break }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if ($category) {
|
||||
$destDir = Join-Path $DesktopPath $category
|
||||
if (-not (Test-Path -LiteralPath $destDir)) {
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $destDir -Force -ErrorAction Stop | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Failed to create category folder '$category' : $_"
|
||||
continue
|
||||
}
|
||||
}
|
||||
$destPath = Join-Path $destDir $item.Name
|
||||
try {
|
||||
Move-Item -LiteralPath $item.FullName -Destination $destPath -Force -ErrorAction Stop
|
||||
Write-Host " moved: $($item.Name) -> $category\"
|
||||
$moved++
|
||||
} catch {
|
||||
Write-Warning "Failed to move $($item.Name) : $_"
|
||||
}
|
||||
} else {
|
||||
$skipped++
|
||||
}
|
||||
}
|
||||
|
||||
if ($shell) {
|
||||
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null } catch {}
|
||||
}
|
||||
|
||||
Write-Host "Sweep complete: $moved moved, $skipped unclassified (left at root)."
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 2: Materialize specific app shortcuts into Shopfloor Tools\
|
||||
#
|
||||
# Runs AFTER the sweep. Creates fresh .lnk files for apps we can target by
|
||||
# .exe path (UDC, eDNC, NTLARS) and copies existing .lnk files for apps
|
||||
# that are MSI-advertised and have no resolvable target (WJ Shopfloor,
|
||||
# Defect_Tracker). Overwrites whatever the sweeper may have moved in -
|
||||
# this is intentional because fresh shortcuts are more reliable than
|
||||
# whatever install crud might be floating around.
|
||||
#
|
||||
# Each app is CONDITIONAL - if its source isn't present on this PC, the
|
||||
# entry is silently skipped. Safe to run on all shopfloor PC types.
|
||||
# ============================================================================
|
||||
|
||||
function New-ShopfloorLnk {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string]$Target,
|
||||
[string]$Arguments = '',
|
||||
[string]$WorkingDirectory = ''
|
||||
)
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
try {
|
||||
$sc = $wsh.CreateShortcut($Path)
|
||||
$sc.TargetPath = $Target
|
||||
if ($Arguments) { $sc.Arguments = $Arguments }
|
||||
if ($WorkingDirectory) { $sc.WorkingDirectory = $WorkingDirectory }
|
||||
elseif ($Target) {
|
||||
$parent = Split-Path -Parent $Target
|
||||
if ($parent) { $sc.WorkingDirectory = $parent }
|
||||
}
|
||||
$sc.IconLocation = "$Target,0"
|
||||
$sc.Save()
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to create $Path : $_"
|
||||
return $false
|
||||
} finally {
|
||||
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wsh) | Out-Null } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
# Look up an existing .lnk file across the paths where installers / ppkgs
|
||||
# typically drop them. Used for apps where we don't build the .lnk from
|
||||
# an .exe (because the target is an MSI-advertised Darwin descriptor).
|
||||
function Find-ExistingLnk {
|
||||
param([string]$Filename)
|
||||
|
||||
$candidates = @(
|
||||
(Join-Path $publicDesktop $Filename),
|
||||
(Join-Path $shopfloorToolsDir $Filename),
|
||||
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\$Filename",
|
||||
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Shopfloor Tools\$Filename"
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path -LiteralPath $c) { return $c }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Add-ShopfloorToolsApps {
|
||||
# App registry - each entry describes how to materialize one shortcut
|
||||
# into Shopfloor Tools\. Folder creation is deferred until we know we
|
||||
# have at least one app to put in it (lazy creation, so PC types with
|
||||
# nothing installed don't get an empty Shopfloor Tools folder).
|
||||
#
|
||||
# Kind = 'exe' -> build a fresh .lnk from ExePath
|
||||
# Kind = 'existing' -> copy an existing .lnk via Find-ExistingLnk
|
||||
$cfgApps = Get-ProfileValue 'desktopApps'
|
||||
|
||||
# An EMPTY configured list means "no desktop apps" and must be honoured.
|
||||
# Get-ProfileValue returns $null only when the key is absent from both the
|
||||
# profile and site-config, so $null is the real "not configured" signal.
|
||||
#
|
||||
# The old test also required .Count -gt 0, which meant "desktopApps": []
|
||||
# fell through to the hardcoded shopfloor list below - so a Display kiosk,
|
||||
# which explicitly configures an empty list, was given UDC, eDNC, NTLARS,
|
||||
# WJ Shopfloor and Defect_Tracker shortcuts. Configuring "none" produced
|
||||
# "everything". Observed on 579C144, 2026-08-06.
|
||||
if ($null -ne $cfgApps) {
|
||||
$apps = @($cfgApps | ForEach-Object {
|
||||
$entry = @{ Name = $_.name; Kind = $_.kind }
|
||||
if ($_.kind -eq 'exe') { $entry.ExePath = $_.exePath }
|
||||
if ($_.kind -eq 'existing') { $entry.SourceName = $_.sourceName }
|
||||
$entry
|
||||
})
|
||||
} else {
|
||||
$apps = @(
|
||||
@{ Name = 'UDC'; Kind = 'exe'; ExePath = 'C:\Program Files\UDC\UDC.exe' }
|
||||
@{ Name = 'eDNC'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\Dnc\bin\DncMain.exe' }
|
||||
@{ Name = 'NTLARS'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\Dnc\Common\NTLARS.exe' }
|
||||
@{ Name = 'WJ Shopfloor'; Kind = 'existing'; SourceName = 'WJ Shopfloor.lnk' }
|
||||
@{ Name = 'Defect_Tracker'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\WJF_Defect_Tracker\Defect_Tracker.exe' }
|
||||
)
|
||||
}
|
||||
|
||||
# Lazy folder creation - only create Shopfloor Tools\ the first time
|
||||
# we have an app that's actually going to land in it. PC types with
|
||||
# nothing installed get no empty folder.
|
||||
$ensureToolsDir = {
|
||||
if (-not (Test-Path -LiteralPath $shopfloorToolsDir)) {
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $shopfloorToolsDir -Force -ErrorAction Stop | Out-Null
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to create $shopfloorToolsDir : $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
foreach ($app in $apps) {
|
||||
$dest = Join-Path $shopfloorToolsDir "$($app.Name).lnk"
|
||||
|
||||
switch ($app.Kind) {
|
||||
'exe' {
|
||||
if (Test-Path -LiteralPath $app.ExePath) {
|
||||
if (-not (& $ensureToolsDir)) { break }
|
||||
if (New-ShopfloorLnk -Path $dest -Target $app.ExePath) {
|
||||
Write-Host " created: $($app.Name) -> $dest"
|
||||
}
|
||||
} else {
|
||||
Write-Host " skip: $($app.Name) - target not installed ($($app.ExePath))" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
'existing' {
|
||||
$src = Find-ExistingLnk $app.SourceName
|
||||
if ($src) {
|
||||
# Skip if the sweep already moved the source into the
|
||||
# destination folder (src == dest -> "cannot overwrite
|
||||
# the item with itself").
|
||||
if ((Resolve-Path -LiteralPath $src -ErrorAction SilentlyContinue).Path -eq
|
||||
(Join-Path $shopfloorToolsDir $app.SourceName)) {
|
||||
Write-Host " exists: $($app.Name) (already in Shopfloor Tools)"
|
||||
} else {
|
||||
if (-not (& $ensureToolsDir)) { break }
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $dest -Force -ErrorAction Stop
|
||||
Write-Host " copied: $($app.Name) from $src"
|
||||
} catch {
|
||||
Write-Warning "Failed to copy $src -> $dest : $_"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host " skip: $($app.Name) - no source .lnk found" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 3: Remove empty category folders
|
||||
#
|
||||
# If the sweep classified nothing into a given category (e.g. no Office
|
||||
# on a Display PC, no Shopfloor Tools apps on a kiosk), we don't want an
|
||||
# empty folder cluttering the desktop. The scheduled-task sweep also runs
|
||||
# this so categories that go empty between logons self-heal.
|
||||
# ============================================================================
|
||||
function Remove-EmptyCategoryFolders {
|
||||
foreach ($cat in @('Office', 'Shopfloor Tools', 'Web Links')) {
|
||||
$dir = Join-Path $publicDesktop $cat
|
||||
if (-not (Test-Path -LiteralPath $dir)) { continue }
|
||||
$contents = @(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue)
|
||||
if ($contents.Count -eq 0) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $dir -Force -ErrorAction Stop
|
||||
Write-Host " removed empty: $cat\"
|
||||
} catch {
|
||||
Write-Warning "Failed to remove empty $dir : $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 4: Edge default browser + startup tabs (delegated to 08)
|
||||
#
|
||||
# 08-EdgeDefaultBrowser.ps1 resolves startup-tab URLs from .url files on
|
||||
# the Public Desktop (or in Web Links\ after the sweep). The .url files
|
||||
# are delivered by DSC AFTER this initial shopfloor setup, so the first
|
||||
# run at imaging time won't find them (falls back to hardcoded URLs,
|
||||
# Plant Apps gets skipped). By calling 08 from inside 06, every SYSTEM
|
||||
# scheduled-task logon re-run of 06 also re-runs 08 -- so after DSC drops
|
||||
# the .url files and the next sweep files them into Web Links\, 08
|
||||
# picks them up and updates the Edge policy. Self-healing.
|
||||
# ============================================================================
|
||||
function Invoke-EdgeDefaultBrowser {
|
||||
$edgeScript = Join-Path $scriptDir '08-EdgeDefaultBrowser.ps1'
|
||||
if (-not (Test-Path -LiteralPath $edgeScript)) {
|
||||
Write-Host " 08-EdgeDefaultBrowser.ps1 not found - skipping"
|
||||
return
|
||||
}
|
||||
try {
|
||||
& $edgeScript
|
||||
} catch {
|
||||
Write-Warning "08-EdgeDefaultBrowser.ps1 failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Scheduled task registration (re-runs 06 at every logon as SYSTEM)
|
||||
# ============================================================================
|
||||
function Register-SweepScheduledTask {
|
||||
param([string]$ScriptPath)
|
||||
|
||||
$taskName = 'Organize Public Desktop'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ScriptPath)) {
|
||||
Write-Warning "Cannot register scheduled task - script not found at $ScriptPath"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""
|
||||
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
# Run as SYSTEM so the script can (a) pass its admin check at the
|
||||
# top, (b) write to Public Desktop / ProgramData Start Menu / the
|
||||
# Default User profile without Access Denied, and (c) register the
|
||||
# next iteration of itself idempotently. Earlier versions used
|
||||
# -GroupId 'BUILTIN\Users' -RunLevel Limited which fired at logon
|
||||
# but the script exited immediately on the non-admin check.
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-UserId 'NT AUTHORITY\SYSTEM' `
|
||||
-LogonType ServiceAccount `
|
||||
-RunLevel Highest
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
||||
|
||||
# -ErrorAction Stop is required because Register-ScheduledTask emits
|
||||
# non-terminating errors on failure (Access Denied etc), which try/catch
|
||||
# would otherwise miss and we'd falsely claim success.
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Principal $principal `
|
||||
-Settings $settings `
|
||||
-Force `
|
||||
-ErrorAction Stop | Out-Null
|
||||
|
||||
Write-Host "Scheduled task '$taskName' registered (runs at every logon)."
|
||||
} catch {
|
||||
Write-Warning "Failed to register scheduled task : $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 0: create Office shortcuts (if Office is installed) ==="
|
||||
# Delegated to 05-OfficeShortcuts.ps1. Office is installed via ppkg but
|
||||
# doesn't finish streaming until AFTER the first reboot, so the first
|
||||
# imaging-time run of 06 finds no Office and this no-ops. On the second
|
||||
# logon (after reboot), Office is installed and 05 creates the shortcuts
|
||||
# so Phase 1 below can sweep them into the Office\ folder.
|
||||
$officeScript = Join-Path $scriptDir '05-OfficeShortcuts.ps1'
|
||||
if (Test-Path -LiteralPath $officeScript) {
|
||||
try { & $officeScript } catch { Write-Warning "05-OfficeShortcuts.ps1 failed: $_" }
|
||||
} else {
|
||||
Write-Host " 05-OfficeShortcuts.ps1 not found - skipping"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 1: sweep loose shortcuts into category folders ==="
|
||||
Invoke-DesktopSweep -DesktopPath $publicDesktop
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 2: populate Shopfloor Tools with app shortcuts ==="
|
||||
Add-ShopfloorToolsApps
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 3: auto-apply startup items from PC profile ==="
|
||||
# Creates .lnk files in AllUsers Startup folder based on the profile's
|
||||
# startupItems. This runs as SYSTEM at every logon via the sweep scheduled
|
||||
# task, so any item the tech removes via Configure-PC would bounce right
|
||||
# back unless we remember the removal. startup-overrides.json tracks labels
|
||||
# the tech has explicitly suppressed; items in that list are skipped here
|
||||
# and re-enabling them via Configure-PC removes them from the list.
|
||||
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup'
|
||||
$overridesPath = 'C:\ProgramData\GE\Shopfloor\startup-overrides.json'
|
||||
|
||||
$suppressed = @()
|
||||
if (Test-Path -LiteralPath $overridesPath) {
|
||||
try {
|
||||
$overrides = Get-Content -LiteralPath $overridesPath -Raw | ConvertFrom-Json
|
||||
if ($overrides.suppressed) { $suppressed = @($overrides.suppressed) }
|
||||
} catch {
|
||||
Write-Warning " Failed to parse $overridesPath : $_"
|
||||
}
|
||||
}
|
||||
|
||||
$cfgStartup = Get-ProfileValue 'startupItems'
|
||||
|
||||
# Same rule as desktopApps: $null = not configured, empty array = configured as
|
||||
# none. Harmless here today because the else branch has no hardcoded fallback,
|
||||
# but kept consistent so the two keys cannot drift apart again.
|
||||
if ($null -ne $cfgStartup -and $cfgStartup.Count -gt 0) {
|
||||
if (-not (Test-Path $startupDir)) {
|
||||
New-Item -ItemType Directory -Path $startupDir -Force | Out-Null
|
||||
}
|
||||
$edgePath = @(
|
||||
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
||||
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
foreach ($si in $cfgStartup) {
|
||||
if ($suppressed -contains $si.label) {
|
||||
Write-Host " suppressed: $($si.label) (tech disabled via Configure-PC)"
|
||||
continue
|
||||
}
|
||||
$lnkPath = Join-Path $startupDir "$($si.label).lnk"
|
||||
# Skip if already exists (idempotent - don't overwrite user changes)
|
||||
if (Test-Path -LiteralPath $lnkPath) {
|
||||
Write-Host " exists: $($si.label)"
|
||||
continue
|
||||
}
|
||||
switch ($si.type) {
|
||||
'exe' {
|
||||
if (Test-Path -LiteralPath $si.target) {
|
||||
if (New-ShopfloorLnk -Path $lnkPath -Target $si.target) {
|
||||
Write-Host " added: $($si.label)"
|
||||
}
|
||||
} else {
|
||||
Write-Host " skip: $($si.label) - target not installed" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
'existing' {
|
||||
$src = Find-ExistingLnk $si.sourceLnk
|
||||
if ($src) {
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $lnkPath -Force
|
||||
Write-Host " added: $($si.label)"
|
||||
} catch { Write-Warning " failed: $($si.label) - $_" }
|
||||
} else {
|
||||
Write-Host " skip: $($si.label) - source not found" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
'url' {
|
||||
$url = $null
|
||||
if ($si.urlKey -and $siteConfig -and $siteConfig.urls) {
|
||||
$url = $siteConfig.urls.$($si.urlKey)
|
||||
}
|
||||
if ($url -and $edgePath) {
|
||||
if (New-ShopfloorLnk -Path $lnkPath -Target $edgePath -Arguments "--new-window `"$url`"") {
|
||||
Write-Host " added: $($si.label)"
|
||||
}
|
||||
} else {
|
||||
Write-Host " skip: $($si.label) - URL or Edge not available" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host " (no startup items in profile)"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 4: remove empty category folders ==="
|
||||
Remove-EmptyCategoryFolders
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 5: Edge default browser + startup tabs ==="
|
||||
Invoke-EdgeDefaultBrowser
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 6: register logon sweeper scheduled task ==="
|
||||
Register-SweepScheduledTask -ScriptPath $scriptPath
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== 06-OrganizeDesktop session end ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,164 +0,0 @@
|
||||
# 07-TaskbarLayout.ps1 - Minimal taskbar pinner.
|
||||
#
|
||||
# Reads the shortcuts that 06-OrganizeDesktop.ps1 created in
|
||||
# C:\Users\Public\Desktop\Shopfloor Tools\ and writes a LayoutModification.xml
|
||||
# that pins them to the taskbar along with Microsoft Edge.
|
||||
#
|
||||
# This script does NOT create, move, or copy any shortcuts - all shortcut
|
||||
# management lives in 06. 07 is the last-mile taskbar config only.
|
||||
#
|
||||
# Pin order (left to right): Edge, WJ Shopfloor, UDC, eDNC, NTLARS, Defect_Tracker.
|
||||
#
|
||||
# LayoutModification.xml is written to the Default User profile shell
|
||||
# directory, which means the pins apply on FIRST LOGON of any new user
|
||||
# profile. Existing profiles don't re-read the Default User template, so
|
||||
# they won't pick up the pins without a manual re-pin or profile delete.
|
||||
# This is a Windows design limitation - syspin.exe style hacks are the
|
||||
# only workaround for existing profiles, and they're unsupported.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# ============================================================================
|
||||
# Admin check - writing to the Default User profile requires elevation.
|
||||
# ============================================================================
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: 07-TaskbarLayout.ps1 must run as Administrator." -ForegroundColor Red
|
||||
Write-Host " Re-run from an elevated PowerShell." -ForegroundColor Red
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Load site config + PC profile
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools'
|
||||
$defaultUserShell = 'C:\Users\Default\AppData\Local\Microsoft\Windows\Shell'
|
||||
$layoutXmlPath = Join-Path $defaultUserShell 'LayoutModification.xml'
|
||||
|
||||
# ============================================================================
|
||||
# Pin list: exact ordered list of shortcut names to pin. Each entry points
|
||||
# at a .lnk file via a Windows environment-variable-expanded path. Edge is
|
||||
# special-cased to its default Start Menu location since Windows maintains
|
||||
# it; all other entries reference C:\Users\Public\Desktop\Shopfloor Tools\
|
||||
# which 06-OrganizeDesktop.ps1 populates.
|
||||
# ============================================================================
|
||||
$cfgPins = Get-ProfileValue 'taskbarPins'
|
||||
|
||||
# An EMPTY configured list means "no pins" and must be honoured. Get-ProfileValue
|
||||
# returns $null only when the key is absent from BOTH the profile and
|
||||
# site-config, so $null is the real "not configured" signal. Requiring
|
||||
# .Count -gt 0 as well makes "taskbarPins": [] fall through to the hardcoded
|
||||
# shopfloor list - configuring "none" producing "everything", the same bug that
|
||||
# put Plant Apps and Defect Tracker in a Display kiosk's Startup folder
|
||||
# (Configure-PC.ps1) and machine-tool shortcuts on its desktop
|
||||
# (06-OrganizeDesktop.ps1).
|
||||
if ($null -ne $cfgPins) {
|
||||
$pinSpec = @($cfgPins | ForEach-Object {
|
||||
@{
|
||||
Name = $_.name
|
||||
Path = $_.lnkPath
|
||||
Literal = [Environment]::ExpandEnvironmentVariables($_.lnkPath)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
$pinSpec = @(
|
||||
@{
|
||||
Name = 'Microsoft Edge'
|
||||
Path = '%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk'
|
||||
Literal = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk'
|
||||
}
|
||||
@{
|
||||
Name = 'WJ Shopfloor'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\WJ Shopfloor.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'WJ Shopfloor.lnk')
|
||||
}
|
||||
@{
|
||||
Name = 'UDC'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\UDC.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'UDC.lnk')
|
||||
}
|
||||
@{
|
||||
Name = 'eDNC'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\eDNC.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'eDNC.lnk')
|
||||
}
|
||||
@{
|
||||
Name = 'NTLARS'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\NTLARS.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'NTLARS.lnk')
|
||||
}
|
||||
@{
|
||||
Name = 'Defect_Tracker'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\Defect_Tracker.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'Defect_Tracker.lnk')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Build the pin list - skip any whose .lnk is missing
|
||||
# ============================================================================
|
||||
Write-Host ""
|
||||
Write-Host "Checking which Shopfloor Tools shortcuts exist..."
|
||||
|
||||
$pinPaths = @()
|
||||
foreach ($pin in $pinSpec) {
|
||||
if (Test-Path -LiteralPath $pin.Literal) {
|
||||
Write-Host " pin: $($pin.Name) -> $($pin.Literal)"
|
||||
$pinPaths += $pin.Path
|
||||
} else {
|
||||
Write-Host " skip: $($pin.Name) - not found at $($pin.Literal)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
if ($pinPaths.Count -eq 0) {
|
||||
Write-Warning "No pins to apply (nothing found in Shopfloor Tools\). Did 06-OrganizeDesktop.ps1 run first?"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Write LayoutModification.xml
|
||||
# ============================================================================
|
||||
$pinXml = ($pinPaths | ForEach-Object {
|
||||
" <taskbar:DesktopApp DesktopApplicationLinkPath=`"$_`"/>"
|
||||
}) -join "`r`n"
|
||||
|
||||
$layoutXml = @"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LayoutModificationTemplate
|
||||
xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
|
||||
xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout"
|
||||
xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout"
|
||||
xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout"
|
||||
Version="1">
|
||||
<CustomTaskbarLayoutCollection PinListPlacement="Replace">
|
||||
<defaultlayout:TaskbarLayout>
|
||||
<taskbar:TaskbarPinList>
|
||||
$pinXml
|
||||
</taskbar:TaskbarPinList>
|
||||
</defaultlayout:TaskbarLayout>
|
||||
</CustomTaskbarLayoutCollection>
|
||||
</LayoutModificationTemplate>
|
||||
"@
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $defaultUserShell)) {
|
||||
New-Item -ItemType Directory -Path $defaultUserShell -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
# -ErrorAction Stop so Access Denied becomes a catchable terminating error
|
||||
# instead of silently falling through to the success message.
|
||||
Set-Content -LiteralPath $layoutXmlPath -Value $layoutXml -Encoding UTF8 -Force -ErrorAction Stop
|
||||
Write-Host ""
|
||||
Write-Host "Wrote taskbar layout with $($pinPaths.Count) pin(s) to:"
|
||||
Write-Host " $layoutXmlPath"
|
||||
Write-Host ""
|
||||
Write-Host "Pins will apply on first logon of any NEW user profile."
|
||||
} catch {
|
||||
Write-Warning "Failed to write $layoutXmlPath : $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,325 +0,0 @@
|
||||
# 08-EdgeDefaultBrowser.ps1 - Set Microsoft Edge as the default browser
|
||||
# and PDF handler for all new user profiles, and configure startup tabs
|
||||
# so Edge opens with Plant Apps / Shop Floor Homepage / Shopfloor Dashboard
|
||||
# when the end user first launches it.
|
||||
#
|
||||
# WHY THIS EXISTS:
|
||||
# The West Jefferson shopfloor ppkg installs Google Chrome alongside Edge.
|
||||
# On first URL click in a new Azure AD user profile, Windows pops a "Choose
|
||||
# your default app" picker because multiple browsers are installed and
|
||||
# nothing has been marked as default. End users hit this on every fresh PC
|
||||
# and it derails the shopfloor workflow.
|
||||
#
|
||||
# HOW IT WORKS:
|
||||
# Two layers, belt-and-suspenders:
|
||||
#
|
||||
# 1. DISM default-app-associations XML
|
||||
# Writes an XML file mapping http/https/.htm/.html/.pdf/.svg/etc to
|
||||
# Edge's ProgIds, then runs:
|
||||
# dism /Online /Import-DefaultAppAssociations:<xml>
|
||||
# This imports the XML into the Default User profile template. Any
|
||||
# NEW user profile created after this point inherits the associations
|
||||
# on first logon. Microsoft-supported, works across Win10/Win11.
|
||||
#
|
||||
# 2. Group Policy registry key
|
||||
# HKLM:\SOFTWARE\Policies\Microsoft\Windows\System\DefaultAssociationsConfiguration
|
||||
# points at the same XML file. This is the "Set a default associations
|
||||
# configuration file" GPO. With this set, Windows re-applies the XML
|
||||
# on every logon, not just once at profile creation - so Windows
|
||||
# update defaults-reset and similar edge cases don't un-do us.
|
||||
#
|
||||
# CAVEATS:
|
||||
# - Applies to NEW profiles on first logon. An existing profile that's
|
||||
# already been logged in (e.g. SupportUser) won't pick up the change
|
||||
# without a manual Settings > Default apps visit. Acceptable because
|
||||
# SupportUser isn't the end-user account.
|
||||
# - Does not UNinstall Chrome. Chrome remains available, it's just not
|
||||
# the default handler for URLs.
|
||||
# - Microsoft blocks programmatic UserChoice hash tampering since Win10
|
||||
# 1703. The DISM + policy route is the only supported path, and it
|
||||
# only fires for new profiles - that's by design.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Admin check
|
||||
# ----------------------------------------------------------------------------
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: 08-EdgeDefaultBrowser.ps1 must run as Administrator." -ForegroundColor Red
|
||||
Write-Host " Re-run from an elevated PowerShell." -ForegroundColor Red
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Load site config + PC profile
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Sanity: Edge must be installed for this to mean anything
|
||||
# ----------------------------------------------------------------------------
|
||||
$edgeCandidates = @(
|
||||
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
||||
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
|
||||
)
|
||||
$edgePath = $edgeCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
if (-not $edgePath) {
|
||||
Write-Host "Microsoft Edge not found in either Program Files location - skipping default browser config."
|
||||
exit 0
|
||||
}
|
||||
Write-Host "Found Edge at: $edgePath"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# XML: Edge as default for web browsing + PDFs + common image formats
|
||||
# ----------------------------------------------------------------------------
|
||||
$xml = @'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<DefaultAssociations>
|
||||
<Association Identifier=".htm" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".html" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".mht" ProgId="MSEdgeMHT" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".mhtml" ProgId="MSEdgeMHT" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".pdf" ProgId="MSEdgePDF" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".svg" ProgId="MSEdgeSVG" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".webp" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier="http" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier="https" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier="ftp" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier="microsoft-edge" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
</DefaultAssociations>
|
||||
'@
|
||||
|
||||
# Write to a stable location that persists across reimaging (C:\Enrollment is
|
||||
# the canonical staging dir in this repo, survives logoff/logon, referenced
|
||||
# by the GP policy below so it must be a path every user-session can read).
|
||||
$xmlDir = 'C:\Enrollment'
|
||||
$xmlPath = Join-Path $xmlDir 'edge-default-associations.xml'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $xmlDir)) {
|
||||
New-Item -ItemType Directory -Path $xmlDir -Force | Out-Null
|
||||
}
|
||||
Set-Content -LiteralPath $xmlPath -Value $xml -Encoding UTF8 -Force
|
||||
Write-Host "Wrote associations XML to: $xmlPath"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Layer 1: DISM import into the Default User profile template
|
||||
# ----------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "Layer 1: dism /Online /Import-DefaultAppAssociations..."
|
||||
& dism.exe /Online /Import-DefaultAppAssociations:"$xmlPath" | Out-Null
|
||||
$dismExit = $LASTEXITCODE
|
||||
if ($dismExit -eq 0) {
|
||||
Write-Host " OK - defaults imported into Default User template."
|
||||
} else {
|
||||
Write-Warning " dism returned exit code $dismExit - import may not have applied."
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Layer 2: Group Policy registry key - enforces XML on every logon
|
||||
# ----------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "Layer 2: Setting DefaultAssociationsConfiguration policy..."
|
||||
$polKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||
if (-not (Test-Path $polKey)) {
|
||||
New-Item -Path $polKey -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $polKey -Name 'DefaultAssociationsConfiguration' -Value $xmlPath -Type String -Force
|
||||
Write-Host " OK - HKLM:\SOFTWARE\Policies\Microsoft\Windows\System\DefaultAssociationsConfiguration = $xmlPath"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================"
|
||||
Write-Host "Edge default browser config applied."
|
||||
Write-Host "================================================================"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Edge startup tabs + homepage
|
||||
#
|
||||
# End users should see Plant Apps, the Shop Floor Homepage, and the
|
||||
# Shopfloor Dashboard open in three tabs the moment Edge launches. Rather
|
||||
# than hardcoding URLs in this script, we read them out of the .url files
|
||||
# that already live on the Public Desktop (copied there by ppkg). This
|
||||
# way, if an admin renames or retargets a .url file later, the script
|
||||
# picks up the new URL on the next imaging run without a code change.
|
||||
#
|
||||
# Fallback: if the .url file is missing, we fall back to a hardcoded URL
|
||||
# (only for the ones we know for sure). Plant Apps has no fallback because
|
||||
# I don't have its URL memorized.
|
||||
#
|
||||
# Machine policies (all under HKLM:\SOFTWARE\Policies\Microsoft\Edge):
|
||||
# RestoreOnStartup = 4 (open a specific set of URLs)
|
||||
# RestoreOnStartupURLs = subkey with "1","2","3"... = URLs in tab order
|
||||
# HomepageLocation = first startup URL (home button opens same)
|
||||
# HomepageIsNewTabPage = 0 (home button opens HomepageLocation, not NTP)
|
||||
# ShowHomeButton = 1 (show the home button in the toolbar)
|
||||
# ----------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "Configuring Edge startup tabs..."
|
||||
|
||||
function Get-UrlFromFile {
|
||||
param([string]$Path)
|
||||
try {
|
||||
$content = Get-Content -LiteralPath $Path -ErrorAction Stop
|
||||
$urlLine = $content | Where-Object { $_ -match '^URL=' } | Select-Object -First 1
|
||||
if ($urlLine) {
|
||||
return ($urlLine -replace '^URL=', '').Trim()
|
||||
}
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-StartupUrl {
|
||||
param(
|
||||
[string]$BaseName, # .url filename without extension
|
||||
[string]$Fallback = ''
|
||||
)
|
||||
# Look in Public Desktop root first, then Web Links subfolder (06's
|
||||
# sweeper may have moved it there already).
|
||||
$candidates = @(
|
||||
"C:\Users\Public\Desktop\$BaseName.url",
|
||||
"C:\Users\Public\Desktop\Web Links\$BaseName.url"
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path -LiteralPath $c) {
|
||||
$url = Get-UrlFromFile $c
|
||||
if ($url) {
|
||||
Write-Host " resolved $BaseName -> $url (from $c)"
|
||||
return $url
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($Fallback) {
|
||||
Write-Host " fallback $BaseName -> $Fallback (.url file not found)" -ForegroundColor DarkGray
|
||||
return $Fallback
|
||||
}
|
||||
Write-Warning " $BaseName : no .url file found and no fallback - will be skipped"
|
||||
return $null
|
||||
}
|
||||
|
||||
# Resolve startup tabs: profile > site-wide > hardcoded fallback
|
||||
# Different PC types can have different tabs (e.g. Lab has Webmail
|
||||
# instead of Plant Apps, Standard-Machine has Plant Apps).
|
||||
$cfgTabs = Get-ProfileValue 'edgeStartupTabs'
|
||||
|
||||
$startupTabs = @()
|
||||
|
||||
# Empty configured list = "no startup tabs", and must be honoured. $null (key
|
||||
# absent from both profile and site-config) is the only "not configured" signal.
|
||||
# Requiring .Count -gt 0 would make "edgeStartupTabs": [] fall through to the
|
||||
# Plant Apps + WJ Shopfloor + Dashboard fallback below - the same
|
||||
# configuring-none-gives-everything bug fixed in Configure-PC.ps1,
|
||||
# 06-OrganizeDesktop.ps1 and 07-TaskbarLayout.ps1.
|
||||
if ($null -ne $cfgTabs) {
|
||||
foreach ($tab in $cfgTabs) {
|
||||
$fallback = if ($tab.fallbackUrlKey -and $siteConfig.urls) { $siteConfig.urls.$($tab.fallbackUrlKey) } else { '' }
|
||||
$url = Resolve-StartupUrl -BaseName $tab.baseName -Fallback $fallback
|
||||
if ($url) { $startupTabs += $url }
|
||||
}
|
||||
} else {
|
||||
$plantApps = Resolve-StartupUrl -BaseName 'Plant Apps' -Fallback 'https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications'
|
||||
if ($plantApps) { $startupTabs += $plantApps }
|
||||
|
||||
$shopFloorHome = Resolve-StartupUrl -BaseName 'WJ Shop Floor Homepage' -Fallback 'https://tsgwp00525.wjs.geaerospace.net'
|
||||
if ($shopFloorHome) { $startupTabs += $shopFloorHome }
|
||||
|
||||
$dashboard = Resolve-StartupUrl -BaseName 'Shopfloor Dashboard' -Fallback 'https://tsgwp00525.wjs.geaerospace.net/shopdb/shopfloor'
|
||||
if ($dashboard) { $startupTabs += $dashboard }
|
||||
}
|
||||
|
||||
# Resolve homepage: profile can override (e.g. Lab has a different homepage
|
||||
# than Standard). Falls back to the first startup tab if not specified.
|
||||
$edgeHomepage = if ($pcProfile -and $pcProfile.edgeHomepage) { $pcProfile.edgeHomepage } else { $null }
|
||||
|
||||
if ($startupTabs.Count -eq 0) {
|
||||
Write-Warning "No startup tab URLs resolved - skipping Edge startup config."
|
||||
} else {
|
||||
$edgePolKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge'
|
||||
if (-not (Test-Path $edgePolKey)) {
|
||||
New-Item -Path $edgePolKey -Force | Out-Null
|
||||
}
|
||||
|
||||
# RestoreOnStartup = 4 means "open a specific set of URLs" (the list
|
||||
# lives under the RestoreOnStartupURLs subkey below).
|
||||
Set-ItemProperty -Path $edgePolKey -Name 'RestoreOnStartup' -Value 4 -Type DWord -Force
|
||||
|
||||
# Build RestoreOnStartupURLs subkey: numbered string values "1","2","3"
|
||||
$urlsKey = Join-Path $edgePolKey 'RestoreOnStartupURLs'
|
||||
if (Test-Path $urlsKey) {
|
||||
# Wipe any prior entries so we don't leak stale tabs if the list
|
||||
# shrinks between runs
|
||||
Remove-Item -Path $urlsKey -Recurse -Force
|
||||
}
|
||||
New-Item -Path $urlsKey -Force | Out-Null
|
||||
for ($i = 0; $i -lt $startupTabs.Count; $i++) {
|
||||
$name = [string]($i + 1)
|
||||
Set-ItemProperty -Path $urlsKey -Name $name -Value $startupTabs[$i] -Type String -Force
|
||||
}
|
||||
|
||||
# Homepage: profile override > first startup tab. Home button opens it, not NTP.
|
||||
$homepageUrl = if ($edgeHomepage) { $edgeHomepage } else { $startupTabs[0] }
|
||||
Write-Host "Homepage set to: $homepageUrl"
|
||||
Set-ItemProperty -Path $edgePolKey -Name 'HomepageLocation' -Value $homepageUrl -Type String -Force
|
||||
Set-ItemProperty -Path $edgePolKey -Name 'HomepageIsNewTabPage' -Value 0 -Type DWord -Force
|
||||
Set-ItemProperty -Path $edgePolKey -Name 'ShowHomeButton' -Value 1 -Type DWord -Force
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Edge startup tabs set ($($startupTabs.Count) tab(s)):"
|
||||
for ($i = 0; $i -lt $startupTabs.Count; $i++) {
|
||||
Write-Host " $($i + 1). $($startupTabs[$i])"
|
||||
}
|
||||
Write-Host "Homepage set to: $($startupTabs[0])"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Chrome: same homepage + startup tabs as Edge
|
||||
#
|
||||
# The PPKG installs Chrome alongside Edge. Apply the same profile-driven
|
||||
# homepage and startup tabs so both browsers behave identically. Uses the
|
||||
# same $startupTabs and $homepageUrl resolved above.
|
||||
# HKLM:\SOFTWARE\Policies\Google\Chrome mirrors the Edge policy structure.
|
||||
# ============================================================================
|
||||
Write-Host ""
|
||||
Write-Host "Configuring Chrome startup tabs..."
|
||||
|
||||
$chromePath = @(
|
||||
'C:\Program Files\Google\Chrome\Application\chrome.exe',
|
||||
'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
if (-not $chromePath) {
|
||||
Write-Host " Chrome not installed - skipping." -ForegroundColor DarkGray
|
||||
} elseif ($startupTabs.Count -eq 0) {
|
||||
Write-Host " No startup tabs resolved - skipping Chrome config." -ForegroundColor DarkGray
|
||||
} else {
|
||||
$chromePolKey = 'HKLM:\SOFTWARE\Policies\Google\Chrome'
|
||||
if (-not (Test-Path $chromePolKey)) {
|
||||
New-Item -Path $chromePolKey -Force | Out-Null
|
||||
}
|
||||
|
||||
Set-ItemProperty -Path $chromePolKey -Name 'RestoreOnStartup' -Value 4 -Type DWord -Force
|
||||
|
||||
$urlsKey = Join-Path $chromePolKey 'RestoreOnStartupURLs'
|
||||
if (Test-Path $urlsKey) {
|
||||
Remove-Item -Path $urlsKey -Recurse -Force
|
||||
}
|
||||
New-Item -Path $urlsKey -Force | Out-Null
|
||||
for ($i = 0; $i -lt $startupTabs.Count; $i++) {
|
||||
Set-ItemProperty -Path $urlsKey -Name ([string]($i + 1)) -Value $startupTabs[$i] -Type String -Force
|
||||
}
|
||||
|
||||
$chromeHomepage = if ($homepageUrl) { $homepageUrl } elseif ($edgeHomepage) { $edgeHomepage } else { $startupTabs[0] }
|
||||
Set-ItemProperty -Path $chromePolKey -Name 'HomepageLocation' -Value $chromeHomepage -Type String -Force
|
||||
Set-ItemProperty -Path $chromePolKey -Name 'HomepageIsNewTabPage' -Value 0 -Type DWord -Force
|
||||
Set-ItemProperty -Path $chromePolKey -Name 'ShowHomeButton' -Value 1 -Type DWord -Force
|
||||
|
||||
Write-Host " Chrome startup tabs set ($($startupTabs.Count) tab(s)), homepage: $chromeHomepage"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================"
|
||||
Write-Host "08-EdgeDefaultBrowser.ps1 complete."
|
||||
Write-Host "Edge + Chrome homepage and startup tabs configured."
|
||||
Write-Host "Machine-wide GPO - applies to all users on next browser launch."
|
||||
Write-Host "================================================================"
|
||||
|
||||
exit 0
|
||||
@@ -1,89 +0,0 @@
|
||||
# 09-Install-PrinterInstallerMap.ps1 - imaging-time placement of the
|
||||
# PrinterInstallerMap.exe tool plus a Public Desktop shortcut.
|
||||
# Runs for every shopfloor PC type (Standard, CMM, Keyence, Display,
|
||||
# etc.) so a tech can launch the printer installer at any bay without
|
||||
# going through Edge. Numbered 09 to run after 06-OrganizeDesktop
|
||||
# (which creates Shopfloor Tools folder) and 08-EdgeDefaultBrowser.
|
||||
#
|
||||
# Why this exists: the legacy printer-deploy flow was a web map
|
||||
# (printerinstallermap.asp) that handed users a per-selection .bat to
|
||||
# download and run. New Edge policy blocks both .bat downloads and
|
||||
# unsigned .exe runs. PrinterInstallerMap is a forked installer with the
|
||||
# map UX baked into a custom Inno wizard page; baking the EXE onto the
|
||||
# image at PXE time means it never goes through a browser (no MOTW, no
|
||||
# SmartScreen prompt). UAC still fires because PrivilegesRequired=admin
|
||||
# and supportuser creds satisfy it - that is the only remaining gate.
|
||||
#
|
||||
# Layout this script produces on a Standard PC:
|
||||
# C:\Tools\PrinterInstallerMap\PrinterInstallerMap.exe
|
||||
# C:\Users\Public\Desktop\Shopfloor Tools\Printer Installer.lnk
|
||||
#
|
||||
# The Shopfloor Tools desktop folder is also created/maintained by
|
||||
# 06-OrganizeDesktop.ps1; this script tolerates either order (it creates
|
||||
# the folder if absent so it can run before 06).
|
||||
#
|
||||
# Idempotent: re-imaging or re-running the script overwrites the EXE and
|
||||
# rewrites the shortcut. Drop-in newer PrinterInstallerMap.exe alongside
|
||||
# this script in the imaging payload, re-image the PC, the desktop link
|
||||
# now points to the new build.
|
||||
#
|
||||
# Source EXE expected at $PSScriptRoot\PrinterInstallerMap.exe (placed
|
||||
# next to this script when the imaging payload lands). Until the Inno
|
||||
# compile + sign+ship step is wired in (see ../../docs/ + the inno repo
|
||||
# feature/printer-map branch), this script no-ops with a warning if the
|
||||
# EXE is missing - re-image will pick it up once the build artifact is
|
||||
# dropped in.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$src = Join-Path $PSScriptRoot 'PrinterInstallerMap.exe'
|
||||
$installDir = 'C:\Tools\PrinterInstallerMap'
|
||||
$installExe = Join-Path $installDir 'PrinterInstallerMap.exe'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$shopfloorTools = Join-Path $publicDesktop 'Shopfloor Tools'
|
||||
$shortcutPath = Join-Path $shopfloorTools 'Printer Installer.lnk'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
Write-Warning "Install-PrinterInstallerMap: source EXE not found at $src - skipping. Re-image after the build artifact is dropped alongside this script."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Stage EXE
|
||||
if (-not (Test-Path -LiteralPath $installDir)) {
|
||||
New-Item -Path $installDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $installExe -Force -ErrorAction Stop
|
||||
Write-Host "Install-PrinterInstallerMap: staged EXE -> $installExe"
|
||||
} catch {
|
||||
Write-Error "Install-PrinterInstallerMap: failed to copy EXE: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure Shopfloor Tools folder exists. 06-OrganizeDesktop.ps1 also
|
||||
# creates this; we create it here so this script does not depend on
|
||||
# 06 having run first.
|
||||
if (-not (Test-Path -LiteralPath $shopfloorTools)) {
|
||||
try {
|
||||
New-Item -Path $shopfloorTools -ItemType Directory -Force | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Install-PrinterInstallerMap: could not create $shopfloorTools : $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Create / overwrite shortcut
|
||||
try {
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
$lnk = $wsh.CreateShortcut($shortcutPath)
|
||||
$lnk.TargetPath = $installExe
|
||||
$lnk.IconLocation = "$installExe,0"
|
||||
$lnk.WorkingDirectory = $installDir
|
||||
$lnk.Description = 'Install network printers from the WJ site map'
|
||||
$lnk.Save()
|
||||
Write-Host "Install-PrinterInstallerMap: shortcut -> $shortcutPath"
|
||||
} catch {
|
||||
Write-Error "Install-PrinterInstallerMap: failed to create shortcut: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,115 +0,0 @@
|
||||
# Apply-MachineNumber.ps1 - SYSTEM-context worker for the two-task machine
|
||||
# number flow. Triggered on-demand by Prompt-MachineNumber.ps1 via
|
||||
# `schtasks /run /tn "WT-Apply-MachineNumber"`. Reads the requested number
|
||||
# from a file the GUI script wrote, invokes Update-MachineNumber as SYSTEM
|
||||
# (full HKLM + ProgramData access), writes a result JSON for the GUI to
|
||||
# display, then cleans up.
|
||||
#
|
||||
# Why SYSTEM:
|
||||
# The eDNC reg key (HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\
|
||||
# General\MachineNo) and UDC settings JSON live in HKLM + ProgramData
|
||||
# respectively - both require admin to write. The OLD design granted
|
||||
# BUILTIN\Users SetValue + Modify via 02-MachineNumberACLs.ps1, but that
|
||||
# was fragile (timing race with eDNC install, ACL silently failed on
|
||||
# some bays) AND a security hole (any user could mess with the machine
|
||||
# identity). Two-task design: GUI gathers input as logged-in user, SYSTEM
|
||||
# does the actual write.
|
||||
#
|
||||
# Files:
|
||||
# C:\Logs\SFLD\machine-number-request.txt - input, single line, new number
|
||||
# C:\Logs\SFLD\machine-number-result.json - output, status fields for GUI
|
||||
# C:\Logs\SFLD\Apply-MachineNumber.log - transcript
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path -LiteralPath $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcript = Join-Path $logDir 'Apply-MachineNumber.log'
|
||||
try { Start-Transcript -Path $transcript -Append -Force | Out-Null } catch {}
|
||||
|
||||
$requestFile = Join-Path $logDir 'machine-number-request.txt'
|
||||
$resultFile = Join-Path $logDir 'machine-number-result.json'
|
||||
|
||||
function Write-Result {
|
||||
param([hashtable]$Body)
|
||||
$Body | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $resultFile -Encoding ascii -Force
|
||||
}
|
||||
|
||||
Write-Host "Apply-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $requestFile)) {
|
||||
Write-Warning "No request file at $requestFile - nothing to apply."
|
||||
Write-Result @{ Status = 'NoRequest'; Errors = @("request file missing: $requestFile") }
|
||||
exit 0
|
||||
}
|
||||
$newNumber = (Get-Content -LiteralPath $requestFile -First 1 -ErrorAction Stop).Trim()
|
||||
Write-Host "Requested new machine number: $newNumber"
|
||||
|
||||
if ($newNumber -notmatch '^\d+$') {
|
||||
Write-Warning "Request is not digits-only: '$newNumber'"
|
||||
Write-Result @{ Status = 'BadInput'; Requested = $newNumber; Errors = @("Not digits only: '$newNumber'") }
|
||||
Remove-Item -LiteralPath $requestFile -Force -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Dot-source the shared helper. Update-MachineNumber.ps1 now has
|
||||
# -ErrorAction Stop on the writes so failures actually throw.
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
$mnResult = Update-MachineNumber -NewNumber $newNumber -Site $site
|
||||
|
||||
$resultBody = @{
|
||||
Status = if ($mnResult.Errors.Count -eq 0) { 'OK' } else { 'PartialErrors' }
|
||||
Requested = $newNumber
|
||||
Site = $site
|
||||
UdcUpdated = [bool]$mnResult.UdcUpdated
|
||||
EdncUpdated = [bool]$mnResult.EdncUpdated
|
||||
OldUdc = $mnResult.OldUdc
|
||||
OldEdnc = $mnResult.OldEdnc
|
||||
UdcSettingsRestored = [bool]$mnResult.UdcSettingsRestored
|
||||
UdcRestored = [bool]$mnResult.UdcRestored
|
||||
MTConnectUpdated = $mnResult.MTConnectUpdated
|
||||
MachineNumberTxtUpdated = [bool]$mnResult.MachineNumberTxtUpdated
|
||||
Errors = $mnResult.Errors
|
||||
AppliedAt = (Get-Date -Format 'o')
|
||||
AppliedAs = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
}
|
||||
Write-Result -Body $resultBody
|
||||
|
||||
Write-Host "Update-MachineNumber result:"
|
||||
Write-Host " UdcUpdated = $($mnResult.UdcUpdated)"
|
||||
Write-Host " EdncUpdated = $($mnResult.EdncUpdated)"
|
||||
Write-Host " Errors = $($mnResult.Errors.Count)"
|
||||
if ($mnResult.Errors) { $mnResult.Errors | ForEach-Object { Write-Host " FAILED: $_" } }
|
||||
|
||||
Remove-Item -LiteralPath $requestFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# On clean success, also unregister the Prompt logon task. Prompt itself
|
||||
# tries to self-unregister but it runs as a Limited user (BUILTIN\Users)
|
||||
# and silently fails on Unregister-ScheduledTask (no delete right on a
|
||||
# SYSTEM-registered task). We're running as SYSTEM here, so we can.
|
||||
# Idempotent if Prompt already unregistered itself somehow.
|
||||
if ($mnResult.Errors.Count -eq 0 -and $mnResult.EdncUpdated) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName 'Prompt Machine Number' -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName 'Prompt Machine Number' -Confirm:$false -ErrorAction Stop
|
||||
Write-Host "Unregistered 'Prompt Machine Number' task (SYSTEM cleanup)."
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Could not unregister 'Prompt Machine Number': $_"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Apply-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
} catch {
|
||||
Write-Warning "Apply threw: $_"
|
||||
Write-Result @{ Status = 'Exception'; Errors = @("$_") }
|
||||
} finally {
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
# Check-MachineNumber.ps1 - Logon-triggered check for placeholder machine
|
||||
# number. If UDC or eDNC are still at 9999, pops an InputBox for the user
|
||||
# to enter the real number. On success, unregisters the scheduled task so
|
||||
# the prompt never appears again.
|
||||
#
|
||||
# Runs as the LOGGED-IN USER (not SYSTEM) because it needs to show GUI.
|
||||
# Writing to ProgramData + HKLM is possible because 02-MachineNumberACLs.ps1
|
||||
# pre-granted BUILTIN\Users write access on those specific targets during
|
||||
# imaging.
|
||||
#
|
||||
# Registered/unregistered by Configure-PC.ps1 (item 6 in the toggle list).
|
||||
|
||||
# --- Transcript logging ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcriptPath = Join-Path $logDir 'Check-MachineNumber.log'
|
||||
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
||||
Write-Host "Check-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
Add-Type -AssemblyName Microsoft.VisualBasic
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
$taskName = 'Check Machine Number'
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
|
||||
# --- Read current values ---
|
||||
$currentMN = Get-CurrentMachineNumber
|
||||
$currentUdc = $currentMN.Udc
|
||||
$currentEdnc = $currentMN.Ednc
|
||||
|
||||
# --- Check if placeholder ---
|
||||
Write-Host "UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not found)' })"
|
||||
Write-Host "eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not found)' })"
|
||||
|
||||
if ($currentUdc -ne '9999' -and $currentEdnc -ne '9999') {
|
||||
Write-Host "Machine number is set (not 9999). Unregistering task and exiting."
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
} catch {}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Placeholder 9999 detected - showing prompt."
|
||||
|
||||
# --- Show prompt ---
|
||||
$promptLines = @()
|
||||
$promptLines += "The machine number on this PC is still set to the"
|
||||
$promptLines += "placeholder value (9999). Please enter the correct"
|
||||
$promptLines += "machine number for this workstation."
|
||||
$promptLines += ""
|
||||
if ($currentUdc) { $promptLines += "Current UDC: $currentUdc" }
|
||||
if ($currentEdnc) { $promptLines += "Current eDNC: $currentEdnc" }
|
||||
$promptLines += ""
|
||||
$promptLines += "Enter the new Machine Number:"
|
||||
|
||||
$prompt = $promptLines -join "`n"
|
||||
$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($new)) {
|
||||
Write-Host "User cancelled. Will prompt again next logon."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
$new = $new.Trim()
|
||||
|
||||
# --- Validate ---
|
||||
if ($new -notmatch '^\d+$') {
|
||||
Write-Host "Invalid input: '$new' (not digits only). Will prompt again next logon."
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
"Machine number must be digits only.`n`nYou entered: '$new'`n`nThe prompt will appear again at next logon.",
|
||||
"Invalid Machine Number",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
) | Out-Null
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Update via shared helper ---
|
||||
$mnResult = Update-MachineNumber -NewNumber $new -Site $site
|
||||
|
||||
$results = @()
|
||||
if ($mnResult.UdcUpdated) { $results += "UDC updated to $new" }
|
||||
if ($mnResult.EdncUpdated) { $results += "eDNC updated to $new" }
|
||||
foreach ($err in $mnResult.Errors) { $results += $err -replace '^', 'FAILED: ' }
|
||||
|
||||
# --- Show result ---
|
||||
$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe.`n`nFull log: C:\Logs\SFLD\Check-MachineNumber.log"
|
||||
# Force the MessageBox to topmost + take focus so it isn't hidden behind
|
||||
# other windows. Without this, the result dialog can render off-screen or
|
||||
# behind the FormTracePak / DNC windows and the tech misses it.
|
||||
$tmpForm = New-Object System.Windows.Forms.Form
|
||||
$tmpForm.TopMost = $true
|
||||
$tmpForm.WindowState = 'Minimized'
|
||||
$tmpForm.ShowInTaskbar = $false
|
||||
$tmpForm.Opacity = 0
|
||||
$tmpForm.Show()
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
$summary,
|
||||
"Machine Number Updated",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Information
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
|
||||
# --- Unregister task on success ---
|
||||
Write-Host "Results: $($results -join '; ')"
|
||||
$anyFail = $results | Where-Object { $_ -match 'FAILED' }
|
||||
if (-not $anyFail) {
|
||||
Write-Host "All updates succeeded. Unregistering logon task."
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
} catch {}
|
||||
} else {
|
||||
Write-Host "Some updates failed. Task stays registered - will prompt again next logon."
|
||||
}
|
||||
|
||||
Write-Host "Check-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,23 +0,0 @@
|
||||
@echo off
|
||||
REM Configure-PC.bat - Launches Configure-PC.ps1 in an elevated PowerShell.
|
||||
REM SupportUser desktop tool for setting machine number + auto-startup items.
|
||||
|
||||
title Configure PC
|
||||
|
||||
set "SCRIPT=%~dp0Configure-PC.ps1"
|
||||
if exist "%SCRIPT%" goto :found
|
||||
|
||||
set "SCRIPT=C:\Users\SupportUser\Desktop\Configure-PC.ps1"
|
||||
if exist "%SCRIPT%" goto :found
|
||||
|
||||
set "SCRIPT=C:\Enrollment\shopfloor-setup\Shopfloor\Configure-PC.ps1"
|
||||
if exist "%SCRIPT%" goto :found
|
||||
|
||||
echo ERROR: Configure-PC.ps1 not found
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:found
|
||||
echo Launching: %SCRIPT%
|
||||
powershell -NoProfile -Command "Start-Process powershell.exe -Verb RunAs -ArgumentList '-NoProfile','-NoExit','-ExecutionPolicy','Bypass','-File','%SCRIPT%'"
|
||||
exit /b
|
||||
@@ -1,518 +0,0 @@
|
||||
# Configure-PC.ps1 - Interactive tool for SupportUser to configure a
|
||||
# shopfloor PC after imaging. Two sections:
|
||||
#
|
||||
# 1. Machine number - if UDC/eDNC are still at placeholder 9999, prompt
|
||||
# to set the real number right now.
|
||||
#
|
||||
# 2. Auto-startup items + machine-number logon prompt - toggle which apps
|
||||
# start automatically for all users, and optionally register a logon
|
||||
# task that prompts STANDARD users for the machine number if it's
|
||||
# still 9999 when they log in (for the case where SupportUser skips
|
||||
# it here and the end user needs to set it themselves).
|
||||
#
|
||||
# Startup .lnk files go in the AllUsers Startup folder:
|
||||
# C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\
|
||||
#
|
||||
# Run via Configure-PC.bat on the SupportUser desktop.
|
||||
|
||||
param(
|
||||
# When set, only show the machine number section (skip startup items).
|
||||
# Used by sync_intune -AsTask after completion -- startup items are
|
||||
# already auto-applied by 06-OrganizeDesktop from the PC profile.
|
||||
[switch]$MachineNumberOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# --- Transcript logging ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcriptPath = Join-Path $logDir 'Configure-PC.log'
|
||||
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
||||
Write-Host "Transcript: $transcriptPath"
|
||||
Write-Host "Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
# Load site config + PC profile (resolves pc-type.txt + pc-subtype.txt
|
||||
# into a profile from site-config.json's pcProfiles section)
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$overridesPath = 'C:\ProgramData\GE\Shopfloor\startup-overrides.json'
|
||||
|
||||
# Persist user-toggled startup overrides so the logon-triggered sweep
|
||||
# (06-OrganizeDesktop.ps1 Phase 3) doesn't re-create .lnks the tech
|
||||
# explicitly removed. Labels in $suppressed are the ones currently OFF.
|
||||
function Get-SuppressedStartup {
|
||||
if (-not (Test-Path -LiteralPath $overridesPath)) { return @() }
|
||||
try {
|
||||
$data = Get-Content -LiteralPath $overridesPath -Raw | ConvertFrom-Json
|
||||
if ($data.suppressed) { return @($data.suppressed) }
|
||||
} catch {
|
||||
Write-Warning " Failed to parse $overridesPath : $_"
|
||||
}
|
||||
return @()
|
||||
}
|
||||
|
||||
function Set-SuppressedStartup {
|
||||
param([string[]]$Labels)
|
||||
$dir = Split-Path -Parent $overridesPath
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
}
|
||||
$payload = @{ suppressed = @($Labels | Sort-Object -Unique) }
|
||||
$payload | ConvertTo-Json | Set-Content -LiteralPath $overridesPath -Encoding UTF8
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Helpers
|
||||
# ============================================================================
|
||||
|
||||
function Get-UrlFromFile {
|
||||
param([string]$BaseName)
|
||||
$candidates = @(
|
||||
(Join-Path $publicDesktop "$BaseName.url"),
|
||||
(Join-Path (Join-Path $publicDesktop 'Web Links') "$BaseName.url")
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (-not (Test-Path -LiteralPath $c)) { continue }
|
||||
try {
|
||||
$content = Get-Content -LiteralPath $c -ErrorAction Stop
|
||||
$urlLine = $content | Where-Object { $_ -match '^URL=' } | Select-Object -First 1
|
||||
if ($urlLine) { return ($urlLine -replace '^URL=', '').Trim() }
|
||||
} catch {}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function New-StartupLnk {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Target,
|
||||
[string]$Arguments = '',
|
||||
[string]$WorkingDirectory = ''
|
||||
)
|
||||
$path = Join-Path $startupDir "$Name.lnk"
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
try {
|
||||
$sc = $wsh.CreateShortcut($path)
|
||||
$sc.TargetPath = $Target
|
||||
if ($Arguments) { $sc.Arguments = $Arguments }
|
||||
if ($WorkingDirectory) { $sc.WorkingDirectory = $WorkingDirectory }
|
||||
elseif ($Target) {
|
||||
$parent = Split-Path -Parent $Target
|
||||
if ($parent) { $sc.WorkingDirectory = $parent }
|
||||
}
|
||||
$sc.Save()
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to create startup shortcut '$Name': $_"
|
||||
return $false
|
||||
} finally {
|
||||
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wsh) | Out-Null } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Machine number - read current state
|
||||
# ============================================================================
|
||||
|
||||
$currentMN = Get-CurrentMachineNumber
|
||||
$currentUdc = $currentMN.Udc
|
||||
$currentEdnc = $currentMN.Ednc
|
||||
|
||||
$needsMachineNumber = ($currentUdc -eq '9999' -or $currentEdnc -eq '9999')
|
||||
|
||||
# ============================================================================
|
||||
# Edge path (for URL-based startup items)
|
||||
# ============================================================================
|
||||
|
||||
$edgePath = @(
|
||||
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
||||
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
# ============================================================================
|
||||
# Startup item definitions
|
||||
# Resolved from: pcProfile.startupItems > siteConfig.startupItems > hardcoded
|
||||
# ============================================================================
|
||||
|
||||
$cfgItems = Get-ProfileValue 'startupItems'
|
||||
|
||||
# An EMPTY configured list means "no startup items" and must be honoured.
|
||||
# Get-ProfileValue returns $null only when the key is absent from BOTH the
|
||||
# profile and site-config, so $null is the real "not configured" signal.
|
||||
#
|
||||
# The old test also required .Count -gt 0, so "startupItems": [] fell through to
|
||||
# the hardcoded list below and a Display kiosk was given UDC, eDNC, Defect
|
||||
# Tracker, WJ Shopfloor and Plant Apps startup shortcuts - Plant Apps launching
|
||||
# Edge in a new window on a kiosk. Confirmed on 579C144 2026-08-06:
|
||||
#
|
||||
# C:\ProgramData\...\StartUp\Defect Tracker.lnk 16:46:03
|
||||
# C:\ProgramData\...\StartUp\Plant Apps.lnk 16:46:03
|
||||
#
|
||||
# Configuring "none" produced "everything". Same bug as desktopApps in
|
||||
# 06-OrganizeDesktop.ps1 and taskbarPins in 07-TaskbarLayout.ps1.
|
||||
if ($null -ne $cfgItems) {
|
||||
$items = @()
|
||||
$num = 0
|
||||
foreach ($si in $cfgItems) {
|
||||
$num++
|
||||
switch ($si.type) {
|
||||
'exe' {
|
||||
$target = $si.target
|
||||
$items += @{
|
||||
Num = $num
|
||||
Label = $si.label
|
||||
Detail = (Split-Path -Leaf $target)
|
||||
Available = (Test-Path $target)
|
||||
CreateLnk = [scriptblock]::Create("return (New-StartupLnk -Name '$($si.label)' -Target '$target')")
|
||||
}
|
||||
}
|
||||
'existing' {
|
||||
$srcLnk = $si.sourceLnk
|
||||
$label = $si.label
|
||||
$items += @{
|
||||
Num = $num
|
||||
Label = $label
|
||||
Detail = 'shortcut'
|
||||
Available = $true
|
||||
CreateLnk = [scriptblock]::Create(@"
|
||||
`$src = @(
|
||||
(Join-Path `$publicDesktop '$srcLnk'),
|
||||
(Join-Path (Join-Path `$publicDesktop 'Shopfloor Tools') '$srcLnk')
|
||||
) | Where-Object { Test-Path -LiteralPath `$_ } | Select-Object -First 1
|
||||
if (`$src) {
|
||||
`$dst = Join-Path `$startupDir '$label.lnk'
|
||||
try { Copy-Item -LiteralPath `$src -Destination `$dst -Force; return `$true }
|
||||
catch { Write-Warning "Failed to copy $label`: `$_"; return `$false }
|
||||
} else {
|
||||
Write-Warning "$srcLnk not found on desktop"
|
||||
return `$false
|
||||
}
|
||||
"@)
|
||||
}
|
||||
}
|
||||
'url' {
|
||||
$urlKey = $si.urlKey
|
||||
$label = $si.label
|
||||
$fallback = if ($urlKey -and $siteConfig.urls) { $siteConfig.urls.$urlKey } else { '' }
|
||||
$items += @{
|
||||
Num = $num
|
||||
Label = $label
|
||||
Detail = 'opens in Edge'
|
||||
Available = [bool]$edgePath
|
||||
CreateLnk = [scriptblock]::Create(@"
|
||||
`$fallback = '$fallback'
|
||||
`$url = Get-UrlFromFile '$label'
|
||||
if (-not `$url) {
|
||||
Write-Host " $label .url not found on desktop, using known URL."
|
||||
`$url = `$fallback
|
||||
}
|
||||
Write-Host " URL: `$url"
|
||||
return (New-StartupLnk -Name '$label' -Target `$edgePath -Arguments "--new-window ```"`$url```"")
|
||||
"@)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$items = @(
|
||||
@{
|
||||
Num = 1
|
||||
Label = 'UDC'
|
||||
Detail = 'UDC.exe'
|
||||
Available = (Test-Path 'C:\Program Files\UDC\UDC.exe')
|
||||
CreateLnk = {
|
||||
return (New-StartupLnk -Name 'UDC' -Target 'C:\Program Files\UDC\UDC.exe')
|
||||
}
|
||||
}
|
||||
@{
|
||||
Num = 2
|
||||
Label = 'eDNC'
|
||||
Detail = 'DncMain.exe'
|
||||
Available = (Test-Path 'C:\Program Files (x86)\Dnc\bin\DncMain.exe')
|
||||
CreateLnk = {
|
||||
return (New-StartupLnk -Name 'eDNC' -Target 'C:\Program Files (x86)\Dnc\bin\DncMain.exe')
|
||||
}
|
||||
}
|
||||
@{
|
||||
Num = 3
|
||||
Label = 'Defect Tracker'
|
||||
Detail = 'ClickOnce app'
|
||||
Available = $true
|
||||
CreateLnk = {
|
||||
$src = @(
|
||||
(Join-Path $publicDesktop 'Defect_Tracker.lnk'),
|
||||
(Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'Defect_Tracker.lnk')
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
if ($src) {
|
||||
$dst = Join-Path $startupDir 'Defect Tracker.lnk'
|
||||
try { Copy-Item -LiteralPath $src -Destination $dst -Force; return $true }
|
||||
catch { Write-Warning "Failed to copy Defect Tracker: $_"; return $false }
|
||||
} else {
|
||||
Write-Warning "Defect_Tracker.lnk not found on desktop"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
}
|
||||
@{
|
||||
Num = 4
|
||||
Label = 'WJ Shopfloor'
|
||||
Detail = 'HostExplorer session'
|
||||
Available = $true
|
||||
CreateLnk = {
|
||||
$src = @(
|
||||
(Join-Path $publicDesktop 'WJ Shopfloor.lnk'),
|
||||
(Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'WJ Shopfloor.lnk')
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
if ($src) {
|
||||
$dst = Join-Path $startupDir 'WJ Shopfloor.lnk'
|
||||
try { Copy-Item -LiteralPath $src -Destination $dst -Force; return $true }
|
||||
catch { Write-Warning "Failed to copy WJ Shopfloor: $_"; return $false }
|
||||
} else {
|
||||
Write-Warning "WJ Shopfloor.lnk not found on desktop"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
}
|
||||
@{
|
||||
Num = 5
|
||||
Label = 'Plant Apps'
|
||||
Detail = 'opens in Edge'
|
||||
Available = [bool]$edgePath
|
||||
CreateLnk = {
|
||||
$fallback = 'https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications'
|
||||
$url = Get-UrlFromFile 'Plant Apps'
|
||||
if (-not $url) {
|
||||
Write-Host " Plant Apps .url not found on desktop, using known URL."
|
||||
$url = $fallback
|
||||
}
|
||||
Write-Host " URL: $url"
|
||||
return (New-StartupLnk -Name 'Plant Apps' -Target $edgePath -Arguments "--new-window `"$url`"")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# Machine-number logon tasks (item 6 toggle controls both)
|
||||
# 2026-05-24: split into user-context Prompt + SYSTEM-context Apply.
|
||||
# 'Check Machine Number' is the legacy single-task name kept for
|
||||
# backward-detection on bays imaged before the split.
|
||||
$machineNumPromptTask = 'Prompt Machine Number'
|
||||
$machineNumApplyTask = 'Apply Machine Number'
|
||||
$machineNumLegacyTask = 'Check Machine Number'
|
||||
|
||||
# ============================================================================
|
||||
# Interactive UI
|
||||
# ============================================================================
|
||||
|
||||
Clear-Host
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " Configure PC" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
|
||||
# --- Section 1: Machine number ---
|
||||
Write-Host ""
|
||||
Write-Host " MACHINE NUMBER" -ForegroundColor Yellow
|
||||
Write-Host " ----------------------------------------"
|
||||
|
||||
if ($needsMachineNumber) {
|
||||
if ($currentUdc) { Write-Host " UDC : $currentUdc" -ForegroundColor Red }
|
||||
if ($currentEdnc) { Write-Host " eDNC : $currentEdnc" -ForegroundColor Red }
|
||||
Write-Host ""
|
||||
$newNum = Read-Host " Enter new machine number (digits only, Enter to skip)"
|
||||
$newNum = $newNum.Trim()
|
||||
|
||||
if ($newNum -and $newNum -match '^\d+$') {
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
$mnResult = Update-MachineNumber -NewNumber $newNum -Site $site
|
||||
|
||||
if ($mnResult.UdcUpdated) {
|
||||
Write-Host " UDC : $currentUdc -> $newNum" -ForegroundColor Green
|
||||
$currentUdc = $newNum
|
||||
}
|
||||
if ($mnResult.EdncUpdated) {
|
||||
Write-Host " eDNC : $currentEdnc -> $newNum" -ForegroundColor Green
|
||||
$currentEdnc = $newNum
|
||||
}
|
||||
foreach ($err in $mnResult.Errors) { Write-Warning " $err" }
|
||||
if ($mnResult.UdcUpdated) { Write-Host " UDC.exe relaunched." }
|
||||
|
||||
$needsMachineNumber = ($currentUdc -eq '9999' -or $currentEdnc -eq '9999')
|
||||
} elseif ($newNum) {
|
||||
Write-Host " Invalid (digits only). Skipped." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host " Skipped (still 9999)." -ForegroundColor DarkGray
|
||||
}
|
||||
} else {
|
||||
if ($currentUdc) { Write-Host " UDC : $currentUdc" -ForegroundColor Green }
|
||||
if ($currentEdnc) { Write-Host " eDNC : $currentEdnc" -ForegroundColor Green }
|
||||
}
|
||||
|
||||
# --- Section 2: Startup items + machine-number logon task ---
|
||||
# Skipped when -MachineNumberOnly (called from sync_intune -AsTask after
|
||||
# completion -- startup items already auto-applied by 06-OrganizeDesktop).
|
||||
if (-not $MachineNumberOnly) {
|
||||
Write-Host ""
|
||||
Write-Host " AUTO-STARTUP ITEMS" -ForegroundColor Yellow
|
||||
Write-Host " ----------------------------------------"
|
||||
Write-Host " Toggle which items start at user logon:"
|
||||
Write-Host ""
|
||||
|
||||
$existingStartup = @(Get-ChildItem -LiteralPath $startupDir -Filter '*.lnk' -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BaseName)
|
||||
|
||||
foreach ($item in $items) {
|
||||
$on = if ($existingStartup -contains $item.Label) { '[ON]' } else { '[ ]' }
|
||||
$avail = if ($item.Available) { '' } else { ' (not installed)' }
|
||||
Write-Host " $($item.Num). $on $($item.Label) - $($item.Detail)$avail"
|
||||
}
|
||||
|
||||
# Item 6: machine number logon prompt. "ON" if EITHER the new Prompt task OR
|
||||
# the legacy Check Machine Number task is registered.
|
||||
$machineNumTaskExists = [bool](
|
||||
(Get-ScheduledTask -TaskName $machineNumPromptTask -ErrorAction SilentlyContinue) -or
|
||||
(Get-ScheduledTask -TaskName $machineNumLegacyTask -ErrorAction SilentlyContinue)
|
||||
)
|
||||
$mnOn = if ($machineNumTaskExists) { '[ON]' } else { '[ ]' }
|
||||
Write-Host " 6. $mnOn Prompt standard user for machine number if 9999"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Enter numbers to toggle (e.g. 1,2,6), Enter to keep current:"
|
||||
$selection = Read-Host " Selection"
|
||||
$selection = $selection.Trim()
|
||||
|
||||
if ($selection) {
|
||||
$selected = $selection -split '[,\s]+' | Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ }
|
||||
|
||||
# Process startup items 1-5
|
||||
$suppressed = @(Get-SuppressedStartup)
|
||||
$suppressedChanged = $false
|
||||
|
||||
foreach ($item in $items) {
|
||||
if ($selected -notcontains $item.Num) { continue }
|
||||
|
||||
$existingLnk = Join-Path $startupDir "$($item.Label).lnk"
|
||||
if (Test-Path -LiteralPath $existingLnk) {
|
||||
# Toggling OFF: remove the .lnk AND record the suppression so
|
||||
# the logon sweep in 06-OrganizeDesktop doesn't re-create it.
|
||||
try {
|
||||
Remove-Item -LiteralPath $existingLnk -Force
|
||||
Write-Host " $($item.Label): REMOVED from startup" -ForegroundColor Yellow
|
||||
} catch { Write-Warning " Failed to remove $($item.Label): $_" }
|
||||
|
||||
if ($suppressed -notcontains $item.Label) {
|
||||
$suppressed += $item.Label
|
||||
$suppressedChanged = $true
|
||||
}
|
||||
} else {
|
||||
# Toggling ON: drop any suppression first so the sweep won't
|
||||
# fight us next logon, then create the .lnk.
|
||||
if ($suppressed -contains $item.Label) {
|
||||
$suppressed = @($suppressed | Where-Object { $_ -ne $item.Label })
|
||||
$suppressedChanged = $true
|
||||
}
|
||||
|
||||
if (-not $item.Available) {
|
||||
Write-Host " $($item.Label): not installed, cannot add to startup" -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
|
||||
$result = & $item.CreateLnk
|
||||
if ($result) {
|
||||
Write-Host " $($item.Label): ADDED to startup" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($suppressedChanged) {
|
||||
try {
|
||||
Set-SuppressedStartup -Labels $suppressed
|
||||
Write-Host " (saved override state to $overridesPath)" -ForegroundColor DarkGray
|
||||
} catch {
|
||||
Write-Warning " Failed to write override state: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Process item 6: machine number logon task
|
||||
if ($selected -contains 6) {
|
||||
if ($machineNumTaskExists) {
|
||||
# Toggle OFF - remove Prompt + Apply (new design) AND the legacy
|
||||
# Check Machine Number task name (in case this bay was imaged
|
||||
# before the split and never re-imaged).
|
||||
$removed = @()
|
||||
foreach ($t in @($machineNumPromptTask, $machineNumApplyTask, $machineNumLegacyTask)) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction Stop
|
||||
$removed += $t
|
||||
}
|
||||
} catch { Write-Warning " Failed to remove '$t': $_" }
|
||||
}
|
||||
if ($removed) {
|
||||
Write-Host " Machine number logon prompt: REMOVED ($($removed -join ', '))" -ForegroundColor Yellow
|
||||
}
|
||||
$machineNumTaskExists = $false
|
||||
} else {
|
||||
# Toggle ON - register logon task
|
||||
# Defer task registration to the shared registrar so this code
|
||||
# path always matches the imaging-time path. Registrar installs
|
||||
# BOTH the user-context "Prompt Machine Number" task and the
|
||||
# SYSTEM-context "Apply Machine Number" task, sets the SDDL on
|
||||
# Apply so Limited users can schtasks /run it, and cleans up
|
||||
# any legacy "Check Machine Number" task name.
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$registrar = Join-Path $scriptDir 'Register-CheckMachineNumberTask.ps1'
|
||||
if (-not (Test-Path -LiteralPath $registrar)) {
|
||||
$registrar = 'C:\Enrollment\shopfloor-setup\Shopfloor\Register-CheckMachineNumberTask.ps1'
|
||||
}
|
||||
if (Test-Path -LiteralPath $registrar) {
|
||||
try {
|
||||
& $registrar
|
||||
Write-Host " Machine number logon prompt: ENABLED" -ForegroundColor Green
|
||||
Write-Host " (Prompt user-task + Apply SYSTEM-task registered;" -ForegroundColor DarkGray
|
||||
Write-Host " will auto-disable after machine number is set)" -ForegroundColor DarkGray
|
||||
$machineNumTaskExists = $true
|
||||
} catch {
|
||||
Write-Warning " Register-CheckMachineNumberTask failed: $_"
|
||||
}
|
||||
} else {
|
||||
Write-Warning " Register-CheckMachineNumberTask.ps1 not found at $registrar"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host " No changes." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
} # end if (-not $MachineNumberOnly)
|
||||
|
||||
# --- Summary ---
|
||||
Write-Host ""
|
||||
Write-Host " CURRENT STATE" -ForegroundColor Cyan
|
||||
Write-Host " ----------------------------------------"
|
||||
$final = @(Get-ChildItem -LiteralPath $startupDir -Filter '*.lnk' -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BaseName)
|
||||
Write-Host " Startup items:"
|
||||
if ($final.Count -eq 0) {
|
||||
Write-Host " (none)"
|
||||
} else {
|
||||
foreach ($f in $final) { Write-Host " - $f" }
|
||||
}
|
||||
$mnState = if ($machineNumTaskExists) { 'ON (prompts at logon if 9999)' } else { 'OFF' }
|
||||
Write-Host " Machine number logon prompt: $mnState"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " Done" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
|
||||
Write-Host "Press any key to close..."
|
||||
try { [void][Console]::ReadKey($true) } catch { [void](Read-Host) }
|
||||
364
playbook/shopfloor-setup/Shopfloor/Install-AppList.ps1
Normal file
364
playbook/shopfloor-setup/Shopfloor/Install-AppList.ps1
Normal file
@@ -0,0 +1,364 @@
|
||||
# Install-AppList.ps1 - install the apps in a site app list (shopfloor).
|
||||
#
|
||||
# -ListPath C:\PreInstall\preinstall.json during setup (Run-ShopfloorSetup,
|
||||
# same logon as the PPKG)
|
||||
# C:\PreInstall\postppkg.json after the post-PPKG reboot
|
||||
# (Invoke-PostPpkg.ps1, SYSTEM)
|
||||
# -Phase name for the log folder: C:\Logs\<Phase>\install.log
|
||||
#
|
||||
# Both lists are EMPTY in the generic build; a site adds its apps:
|
||||
# { "Name": "Foo 1.2", "Installer": "foo\\Foo.msi", "Type": "MSI" | "EXE",
|
||||
# "InstallArgs": "/qn /norestart",
|
||||
# "DetectionMethod": "Registry" | "File", "DetectionPath": "...",
|
||||
# optional: "DetectionName", "DetectionValue", "LogFile",
|
||||
# "KillAfterDetection": true, "PCTypes": ["gea-shopfloor-standard"] }
|
||||
# Installers come from <list folder>\installers\ (staged from the PXE server's
|
||||
# enrollment share, pre-install\installers\). Detection runs first, so an app
|
||||
# that is already there is skipped and re-runs are safe.
|
||||
#
|
||||
# Never fails the caller: a missing or empty list, a bad JSON file or a failed
|
||||
# install is logged and the script exits 0.
|
||||
|
||||
param(
|
||||
[string]$ListPath = 'C:\PreInstall\preinstall.json',
|
||||
[string]$Phase = 'PreInstall'
|
||||
)
|
||||
|
||||
$installerDir = Join-Path (Split-Path -Parent $ListPath) 'installers'
|
||||
$logDir = "C:\Logs\$Phase"
|
||||
$logFile = Join-Path $logDir 'install.log'
|
||||
|
||||
# Appended, never wiped: an installer-triggered reboot can kill a run, and the
|
||||
# next run must not destroy that record.
|
||||
if (-not (Test-Path $logDir)) {
|
||||
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
function Write-ListLog {
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)]
|
||||
[string]$Message,
|
||||
[Parameter(Position=1)]
|
||||
[ValidateSet('INFO','WARN','ERROR')]
|
||||
[string]$Level = 'INFO'
|
||||
)
|
||||
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$line = "[$stamp] [$Level] $Message"
|
||||
Write-Host $line
|
||||
# Write-through, so each line is on disk even if a reboot follows.
|
||||
try {
|
||||
$fs = New-Object System.IO.FileStream(
|
||||
$logFile,
|
||||
[System.IO.FileMode]::Append,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::Read,
|
||||
4096,
|
||||
[System.IO.FileOptions]::WriteThrough
|
||||
)
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n")
|
||||
$fs.Write($bytes, 0, $bytes.Length)
|
||||
$fs.Flush()
|
||||
$fs.Dispose()
|
||||
}
|
||||
catch {
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Write-ListLog "================================================================"
|
||||
Write-ListLog "=== $Phase session start (PID $PID): $ListPath ==="
|
||||
Write-ListLog "================================================================"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ListPath)) {
|
||||
Write-ListLog "No app list at $ListPath - nothing to install."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$pcType = ''
|
||||
$pcTypeFile = 'C:\Enrollment\pc-type.txt'
|
||||
if (Test-Path $pcTypeFile) {
|
||||
$pcType = "$(Get-Content $pcTypeFile -First 1 -ErrorAction SilentlyContinue)".Trim()
|
||||
}
|
||||
Write-ListLog "PC type: $(if ($pcType) { $pcType } else { '(unknown)' })"
|
||||
|
||||
try {
|
||||
$config = Get-Content -LiteralPath $ListPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-ListLog "Failed to parse $ListPath : $_" "ERROR"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$apps = @($config.Applications | Where-Object { $_ })
|
||||
if ($apps.Count -eq 0) {
|
||||
Write-ListLog "The list has no applications - nothing to install."
|
||||
exit 0
|
||||
}
|
||||
Write-ListLog "Installer dir: $installerDir"
|
||||
Write-ListLog "$($apps.Count) app entries"
|
||||
|
||||
# --- Detection helper ---
|
||||
function Test-AppInstalled {
|
||||
param($App)
|
||||
|
||||
if (-not $App.DetectionMethod) {
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($App.DetectionMethod) {
|
||||
"Registry" {
|
||||
if (-not (Test-Path $App.DetectionPath)) {
|
||||
return $false
|
||||
}
|
||||
if ($App.DetectionName) {
|
||||
$value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue
|
||||
if (-not $value) {
|
||||
return $false
|
||||
}
|
||||
if ($App.DetectionValue) {
|
||||
return ($value.$($App.DetectionName) -eq $App.DetectionValue)
|
||||
}
|
||||
return $true
|
||||
}
|
||||
return $true
|
||||
}
|
||||
"File" {
|
||||
return Test-Path $App.DetectionPath
|
||||
}
|
||||
default {
|
||||
Write-ListLog " Unknown detection method: $($App.DetectionMethod)" "WARN"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-ListLog " Detection check threw: $_" "WARN"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- Iterate apps ---
|
||||
$installed = 0
|
||||
$skipped = 0
|
||||
$failed = 0
|
||||
|
||||
foreach ($app in $apps) {
|
||||
# Cancel any reboot a previous installer scheduled (some respect /norestart, some
|
||||
# don't - VC++ 2008's bootstrapper sometimes triggers an immediate Windows reboot
|
||||
# despite the flag). Doing this BEFORE each install protects the rest of the loop.
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
Write-ListLog "==> $($app.Name)"
|
||||
|
||||
# PCTypes: absent, '*' or the PC's type = install. (One generic type,
|
||||
# gea-shopfloor-standard; the filter is there for sites that add types.)
|
||||
$allowedTypes = @($app.PCTypes | Where-Object { $_ })
|
||||
if ($allowedTypes.Count -gt 0 -and -not ($allowedTypes -contains '*') -and -not ($allowedTypes -icontains $pcType)) {
|
||||
Write-ListLog " PCTypes excludes '$pcType' (allowed: $($allowedTypes -join ', ')) - skipping"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
# Detection check
|
||||
if (Test-AppInstalled -App $app) {
|
||||
Write-ListLog " Already installed - skipping"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
# Locate installer file
|
||||
$installerPath = Join-Path $installerDir $app.Installer
|
||||
if (-not (Test-Path $installerPath)) {
|
||||
Write-ListLog " Installer file not found: $installerPath" "ERROR"
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
Write-ListLog " Installing from $installerPath"
|
||||
if ($app.InstallArgs) {
|
||||
Write-ListLog " InstallArgs: $($app.InstallArgs)"
|
||||
}
|
||||
|
||||
# Per-app verbose msiexec log path - written by /L*v on MSI installs and tailed
|
||||
# into the runner log on failure so we don't need to dig through C:\Logs manually.
|
||||
$safeName = $app.Name -replace '[^a-zA-Z0-9]','_'
|
||||
$msiLog = Join-Path $logDir "msi-$safeName.log"
|
||||
if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
try {
|
||||
# We use [System.Diagnostics.Process]::Start() directly instead of Start-Process
|
||||
# because PowerShell 5.1's Start-Process -PassThru has a bug where the returned
|
||||
# Process object's OS handle is disposed when control returns to the script,
|
||||
# causing ExitCode to always read as $null even after WaitForExit() succeeds.
|
||||
# Calling Process.Start() directly returns a Process object with a live handle
|
||||
# that exposes ExitCode correctly. We poll HasExited so UDC_Setup.exe (which
|
||||
# hangs forever after spawning a hidden WPF window) can be killed via the
|
||||
# detection-during-install path.
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
||||
|
||||
if ($app.Type -eq "MSI") {
|
||||
$psi.FileName = "msiexec.exe"
|
||||
$psi.Arguments = "/i `"$installerPath`""
|
||||
if ($app.InstallArgs) {
|
||||
$psi.Arguments += " " + $app.InstallArgs
|
||||
}
|
||||
$psi.Arguments += " /L*v `"$msiLog`""
|
||||
Write-ListLog " msiexec verbose log: $msiLog"
|
||||
}
|
||||
elseif ($app.Type -eq "EXE") {
|
||||
$psi.FileName = $installerPath
|
||||
if ($app.InstallArgs) {
|
||||
$psi.Arguments = $app.InstallArgs
|
||||
}
|
||||
# If the JSON entry specifies a LogFile (per-installer log written by
|
||||
# the EXE itself - e.g. Setup-OpenText.ps1 -> C:\Logs\PreInstall\Setup-
|
||||
# OpenText.log), surface it on failure the same way we surface the
|
||||
# msiexec /L*v verbose log for MSI installs. Lets EXE wrappers actually
|
||||
# report what went wrong inside.
|
||||
if ($app.LogFile) {
|
||||
Write-ListLog " Installer log: $($app.LogFile)"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-ListLog " Unsupported Type: $($app.Type) - skipping" "ERROR"
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
# 1618 = another MSI install is running (after the PPKG its own
|
||||
# installs can still be going). Wait and retry, up to 5 times.
|
||||
for ($try = 1; $app.Type -eq "MSI" -and $try -le 5; $try++) {
|
||||
$proc.WaitForExit(15000) | Out-Null
|
||||
if (-not $proc.HasExited -or $proc.ExitCode -ne 1618) { break }
|
||||
Write-ListLog " Another installation is in progress (1618) - retry $try of 5 in 60 s" "WARN"
|
||||
Start-Sleep -Seconds 60
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
}
|
||||
|
||||
# Poll for completion: process exit OR detection success (whichever happens first)
|
||||
$timeoutSec = 600 # 10 min hard cap per app
|
||||
$pollInterval = 5
|
||||
$elapsed = 0
|
||||
$killedAfterDetect = $false
|
||||
|
||||
while ($elapsed -lt $timeoutSec) {
|
||||
if ($proc.HasExited) { break }
|
||||
|
||||
# Detection-during-install kill is OPT-IN via "KillAfterDetection: true"
|
||||
# in the JSON entry. It's only safe for installers that hang forever after
|
||||
# creating their detection markers (UDC_Setup.exe spawns a hidden WPF window
|
||||
# and never exits). For normal installers (Oracle, msiexec, etc.) the
|
||||
# detection registry key often gets written midway through the install AND
|
||||
# msiexec is still doing post-install cleanup AND msiserver is still holding
|
||||
# the install mutex - killing msiexec at that point leaves the system in a
|
||||
# bad state and the NEXT msiexec call returns 1618 ERROR_INSTALL_ALREADY_
|
||||
# RUNNING. So opt-out by default; only kill apps that explicitly need it.
|
||||
if ($app.KillAfterDetection -and (Test-AppInstalled -App $app)) {
|
||||
Write-ListLog " Detection passed at $elapsed s - killing installer to advance (KillAfterDetection)"
|
||||
try { $proc.Kill(); $proc.WaitForExit(5000) | Out-Null } catch { }
|
||||
|
||||
$killedAfterDetect = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $pollInterval
|
||||
$elapsed += $pollInterval
|
||||
}
|
||||
|
||||
if (-not $proc.HasExited -and -not $killedAfterDetect) {
|
||||
Write-ListLog " TIMEOUT after $timeoutSec seconds - killing installer" "ERROR"
|
||||
try { $proc.Kill() } catch { }
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
if ($killedAfterDetect) {
|
||||
Write-ListLog " SUCCESS (verified via detection during install)"
|
||||
$installed++
|
||||
}
|
||||
else {
|
||||
# Start-Process -PassThru returns a Process object whose ExitCode is null
|
||||
# until WaitForExit() is called - even if HasExited is already true. Without
|
||||
# this, every install that goes through the exit-code branch gets reported
|
||||
# as "Exit code - FAILED" (empty) regardless of the real result.
|
||||
try { $proc.WaitForExit() } catch { }
|
||||
$exitCode = $proc.ExitCode
|
||||
|
||||
if ($exitCode -eq 0 -or $exitCode -eq 3010) {
|
||||
Write-ListLog " Exit code $exitCode after $elapsed s - SUCCESS"
|
||||
if ($exitCode -eq 3010) {
|
||||
Write-ListLog " (Reboot required for $($app.Name))"
|
||||
}
|
||||
$installed++
|
||||
}
|
||||
else {
|
||||
Write-ListLog " Exit code $exitCode - FAILED" "ERROR"
|
||||
|
||||
# If the JSON entry specifies a LogFile for an EXE install (e.g.
|
||||
# Setup-OpenText.ps1 writes its own log at C:\Logs\PreInstall\Setup-
|
||||
# OpenText.log), tail it here so the runner log surfaces the actual
|
||||
# cause without us having to dig through C:\Logs manually.
|
||||
if ($app.Type -eq "EXE" -and $app.LogFile -and (Test-Path $app.LogFile)) {
|
||||
Write-ListLog " --- last 30 lines of $($app.LogFile) ---"
|
||||
Get-Content $app.LogFile -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-ListLog " $_"
|
||||
}
|
||||
Write-ListLog " --- end installer log tail ---"
|
||||
}
|
||||
|
||||
# Surface the *meaningful* lines from the verbose msiexec log (not just
|
||||
# the tail, which is rollback/cleanup noise). The actual root-cause lines
|
||||
# are: MSI error notes (Note: 1: <code>), failed action returns (return
|
||||
# value 3), errors logged by custom actions, and any line containing the
|
||||
# word "Error". Grep these out so the runner log shows the smoking gun.
|
||||
if ($app.Type -eq "MSI" -and (Test-Path $msiLog)) {
|
||||
Write-ListLog " --- meaningful lines from $msiLog ---"
|
||||
$patterns = @(
|
||||
'Note: 1: ',
|
||||
'return value 3',
|
||||
'Error \d+\.',
|
||||
'CustomAction .* returned actual error',
|
||||
'Failed to ',
|
||||
'Installation failed',
|
||||
'Calling custom action',
|
||||
'1: 2262', # Database not found
|
||||
'1: 2203', # Cannot open database
|
||||
'1: 2330' # Insufficient permissions
|
||||
)
|
||||
$regex = ($patterns -join '|')
|
||||
$matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 30
|
||||
if ($matches) {
|
||||
foreach ($m in $matches) {
|
||||
Write-ListLog " $($m.Line.Trim())"
|
||||
}
|
||||
} else {
|
||||
Write-ListLog " (no error patterns matched - falling back to last 25 lines)"
|
||||
Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-ListLog " $_"
|
||||
}
|
||||
}
|
||||
Write-ListLog " --- end MSI log scan ---"
|
||||
}
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-ListLog " Install threw: $_" "ERROR"
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
|
||||
Write-ListLog "============================================"
|
||||
Write-ListLog "$Phase complete: $installed installed, $skipped skipped, $failed failed"
|
||||
Write-ListLog "============================================"
|
||||
|
||||
# Final reboot cancel - if the last installer in the loop scheduled one, the
|
||||
# caller's later `shutdown /a` may not come in time. Cancel here so control
|
||||
# returns cleanly to the caller.
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
exit 0
|
||||
86
playbook/shopfloor-setup/Shopfloor/Invoke-PostPpkg.ps1
Normal file
86
playbook/shopfloor-setup/Shopfloor/Invoke-PostPpkg.ps1
Normal file
@@ -0,0 +1,86 @@
|
||||
# Invoke-PostPpkg.ps1 - run by the "GE Shopfloor Post-PPKG" task (SYSTEM,
|
||||
# at startup and logon). Three jobs, each done once:
|
||||
#
|
||||
# 1. Install the site's post-PPKG app list, C:\PreInstall\postppkg.json
|
||||
# (Install-AppList.ps1; empty in the generic build). Only once the PPKG
|
||||
# has been applied (C:\Enrollment\.ppkg-applied, written by
|
||||
# run-enrollment.ps1), i.e. on the boot after the PPKG reboot.
|
||||
# Done marker: C:\Enrollment\postppkg-done.txt
|
||||
# 2. After the ShopFloor handover (Winlogon DefaultUserName ShopFloor*),
|
||||
# delete the imaging AutoLogonCount. Left in place, ShopFloor inherits
|
||||
# it and Windows turns autologon off when it reaches 0: the bay stops at
|
||||
# a ShopFloor password prompt a few reboots later. The monitor tries the
|
||||
# same, but it runs as Users and the HKLM write can be denied.
|
||||
# 3. Once Monitor-IntuneProgress has written C:\Enrollment\sync-complete.txt,
|
||||
# remove the "Shopfloor Intune Sync" logon task.
|
||||
# When all three are done the task unregisters itself.
|
||||
#
|
||||
# Never throws: every step logs and the script exits 0.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
New-Item -Path $logDir -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
try { Start-Transcript -Path (Join-Path $logDir 'postppkg.log') -Append -Force | Out-Null } catch {}
|
||||
|
||||
$appliedMarker = 'C:\Enrollment\.ppkg-applied'
|
||||
$doneMarker = 'C:\Enrollment\postppkg-done.txt'
|
||||
$syncComplete = 'C:\Enrollment\sync-complete.txt'
|
||||
$listPath = 'C:\PreInstall\postppkg.json'
|
||||
$taskName = 'GE Shopfloor Post-PPKG'
|
||||
$syncTask = 'Shopfloor Intune Sync'
|
||||
|
||||
Write-Host "=== Invoke-PostPpkg $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
|
||||
|
||||
# 1. post-PPKG app list
|
||||
if (Test-Path -LiteralPath $doneMarker) {
|
||||
Write-Host "Post-PPKG list already done ($(Get-Content $doneMarker -First 1))."
|
||||
} elseif (-not (Test-Path -LiteralPath $appliedMarker)) {
|
||||
Write-Host "PPKG not applied yet ($appliedMarker missing) - waiting for the next boot."
|
||||
} else {
|
||||
$runner = Join-Path $PSScriptRoot 'Install-AppList.ps1'
|
||||
if (Test-Path -LiteralPath $runner) {
|
||||
try { & $runner -ListPath $listPath -Phase 'PostPpkg' } catch { Write-Warning "Install-AppList failed: $_" }
|
||||
Set-Content -Path $doneMarker -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -ErrorAction SilentlyContinue
|
||||
Write-Host "Post-PPKG list done (log: C:\Logs\PostPpkg\install.log)."
|
||||
} else {
|
||||
Write-Warning "Install-AppList.ps1 not found next to this script ($runner)."
|
||||
}
|
||||
}
|
||||
|
||||
# 2. AutoLogonCount after the ShopFloor handover
|
||||
$handedOver = $false
|
||||
$countGone = $true
|
||||
try {
|
||||
$wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
||||
$wl = Get-ItemProperty $wlKey -ErrorAction Stop
|
||||
$handedOver = ($wl.DefaultUserName -like 'ShopFloor*')
|
||||
if ($handedOver -and $null -ne $wl.AutoLogonCount) {
|
||||
Remove-ItemProperty $wlKey -Name AutoLogonCount -ErrorAction Stop
|
||||
Write-Host "Removed leftover AutoLogonCount ($($wl.AutoLogonCount)) after the ShopFloor handover."
|
||||
}
|
||||
$countGone = ($null -eq (Get-ItemProperty $wlKey -ErrorAction Stop).AutoLogonCount)
|
||||
} catch {
|
||||
Write-Warning "AutoLogonCount check failed: $_"
|
||||
$countGone = $false
|
||||
}
|
||||
|
||||
# 3. the monitor's logon task, once it has finished
|
||||
$syncDone = Test-Path -LiteralPath $syncComplete
|
||||
if ($syncDone -and (Get-ScheduledTask -TaskName $syncTask -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $syncTask -Confirm:$false -ErrorAction Stop
|
||||
Write-Host "Removed the '$syncTask' task (sync-complete.txt present)."
|
||||
} catch {
|
||||
Write-Warning "Could not remove '$syncTask': $_"
|
||||
}
|
||||
}
|
||||
|
||||
if ((Test-Path -LiteralPath $doneMarker) -and $handedOver -and $countGone -and $syncDone) {
|
||||
Write-Host "All done - unregistering '$taskName'."
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction Stop } catch { Write-Warning "Could not unregister: $_" }
|
||||
} else {
|
||||
Write-Host ("Still to do: list={0} handover={1} autologoncount_cleared={2} sync_complete={3}" -f `
|
||||
(Test-Path -LiteralPath $doneMarker), $handedOver, $countGone, $syncDone)
|
||||
}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,200 +0,0 @@
|
||||
# Prompt-MachineNumber.ps1 - User-context GUI script for the two-task
|
||||
# machine number flow. Triggered AtLogOn for any BUILTIN\Users member.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Read current UDC + eDNC values (read-only - no privileges needed).
|
||||
# 2. If neither is 9999, unregister self and exit (this PC is set up).
|
||||
# 3. Show InputBox for new machine number.
|
||||
# 4. Write number to C:\Logs\SFLD\machine-number-request.txt.
|
||||
# 5. Trigger the SYSTEM-context Apply-MachineNumber task via
|
||||
# schtasks /run. SYSTEM has full HKLM + ProgramData access so the
|
||||
# actual write happens with proper privileges - the prompted user
|
||||
# never needs HKLM write rights (security improvement over the old
|
||||
# 02-MachineNumberACLs.ps1 ACL-grant hack).
|
||||
# 6. Poll for C:\Logs\SFLD\machine-number-result.json (30s timeout).
|
||||
# 7. Show result MessageBox. Unregister self on success.
|
||||
#
|
||||
# Why this script doesn't do the writes itself: GUI is required (InputBox),
|
||||
# but GUI requires user-context (SYSTEM can't render to user desktop on
|
||||
# modern Windows). The user-context dialog gathers input; the SYSTEM task
|
||||
# does privileged writes.
|
||||
|
||||
# --- Transcript ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path -LiteralPath $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcript = Join-Path $logDir 'Prompt-MachineNumber.log'
|
||||
try { Start-Transcript -Path $transcript -Append -Force | Out-Null } catch {}
|
||||
Write-Host "Prompt-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
Add-Type -AssemblyName Microsoft.VisualBasic
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
$taskName = 'Prompt Machine Number'
|
||||
$applyTaskName = 'Apply Machine Number'
|
||||
$requestFile = Join-Path $logDir 'machine-number-request.txt'
|
||||
$resultFile = Join-Path $logDir 'machine-number-result.json'
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
|
||||
# --- Read current values (read-only, no perms needed) ---
|
||||
$currentMN = Get-CurrentMachineNumber
|
||||
$currentUdc = $currentMN.Udc
|
||||
$currentEdnc = $currentMN.Ednc
|
||||
Write-Host "UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not found)' })"
|
||||
Write-Host "eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not found)' })"
|
||||
|
||||
if ($currentUdc -ne '9999' -and $currentEdnc -ne '9999') {
|
||||
Write-Host "Machine number is set (not 9999). Unregistering Prompt task and exiting."
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Placeholder 9999 detected - showing prompt."
|
||||
|
||||
# --- Show prompt ---
|
||||
$promptLines = @()
|
||||
$promptLines += "The machine number on this PC is still set to the"
|
||||
$promptLines += "placeholder value (9999). Please enter the correct"
|
||||
$promptLines += "machine number for this workstation."
|
||||
$promptLines += ""
|
||||
if ($currentUdc) { $promptLines += "Current UDC: $currentUdc" }
|
||||
if ($currentEdnc) { $promptLines += "Current eDNC: $currentEdnc" }
|
||||
$promptLines += ""
|
||||
$promptLines += "Enter the new Machine Number:"
|
||||
$prompt = $promptLines -join "`n"
|
||||
$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($new)) {
|
||||
Write-Host "User cancelled. Will prompt again next logon."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
$new = $new.Trim()
|
||||
|
||||
if ($new -notmatch '^\d+$') {
|
||||
Write-Host "Invalid input: '$new' (not digits only). Showing error and re-prompting next logon."
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
"Machine number must be digits only.`n`nYou entered: '$new'`n`nThe prompt will appear again at next logon.",
|
||||
"Invalid Machine Number",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
) | Out-Null
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Hand off to SYSTEM task ---
|
||||
# Clean any stale request / result files first so we read fresh ones.
|
||||
Remove-Item -LiteralPath $requestFile, $resultFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
try {
|
||||
Set-Content -LiteralPath $requestFile -Value $new -Encoding ascii -Force -ErrorAction Stop
|
||||
} catch {
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
"Could not write request file at $requestFile`n`n$_`n`nThe prompt will appear again at next logon.",
|
||||
"Machine Number Request Failed",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
) | Out-Null
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Wrote $requestFile with '$new'. Triggering SYSTEM apply task..."
|
||||
& schtasks.exe /run /tn $applyTaskName 2>&1 | ForEach-Object { Write-Host " schtasks: $_" }
|
||||
|
||||
# --- Wait for result ---
|
||||
$deadline = (Get-Date).AddSeconds(60)
|
||||
$result = $null
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path -LiteralPath $resultFile) {
|
||||
try {
|
||||
$result = Get-Content -LiteralPath $resultFile -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
break
|
||||
} catch { Start-Sleep -Milliseconds 200 }
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
# Make the result MessageBox topmost so it shows above the FormTracePak /
|
||||
# DNC windows and isn't missed.
|
||||
$tmpForm = New-Object System.Windows.Forms.Form
|
||||
$tmpForm.TopMost = $true
|
||||
$tmpForm.WindowState = 'Minimized'
|
||||
$tmpForm.ShowInTaskbar = $false
|
||||
$tmpForm.Opacity = 0
|
||||
$tmpForm.Show()
|
||||
|
||||
if (-not $result) {
|
||||
Write-Host "Timed out waiting for SYSTEM apply task to produce result file ($resultFile)."
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
"Timed out waiting for the SYSTEM update task to complete.`n`nCheck:`n C:\Logs\SFLD\Apply-MachineNumber.log`n C:\Logs\SFLD\Prompt-MachineNumber.log`n`nThe prompt will appear again at next logon.",
|
||||
"Machine Number Update Timed Out",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Warning
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build summary from result JSON
|
||||
$lines = @()
|
||||
$lines += "Requested: $($result.Requested)"
|
||||
$lines += ""
|
||||
if ($result.UdcUpdated) { $lines += "UDC updated to $($result.Requested)" } else { $lines += "UDC: not updated (UDC may not be installed)" }
|
||||
if ($result.EdncUpdated) { $lines += "eDNC updated to $($result.Requested)" } else { $lines += "eDNC: not updated" }
|
||||
if ($result.UdcSettingsRestored) { $lines += "UDC settings restored from SFLD" }
|
||||
if ($result.UdcRestored) { $lines += "UDC live data restored from SFLD" }
|
||||
if ($result.MachineNumberTxtUpdated) { $lines += "machine-number.txt updated" }
|
||||
if ($result.MTConnectUpdated -and $result.MTConnectUpdated.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "MTConnect Devices.xml updates:"
|
||||
$result.MTConnectUpdated | ForEach-Object { $lines += " - $_" }
|
||||
}
|
||||
if ($result.Errors -and $result.Errors.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "FAILURES:"
|
||||
$result.Errors | ForEach-Object { $lines += " - $_" }
|
||||
}
|
||||
$lines += ""
|
||||
$lines += "Status: $($result.Status)"
|
||||
$lines += "Logs: C:\Logs\SFLD\Apply-MachineNumber.log"
|
||||
$lines += " C:\Logs\SFLD\Prompt-MachineNumber.log"
|
||||
$lines += ""
|
||||
$lines += "To apply eDNC changes, restart any running DncMain.exe."
|
||||
$summary = $lines -join "`n"
|
||||
|
||||
$icon = if ($result.Status -eq 'OK') { [System.Windows.Forms.MessageBoxIcon]::Information } else { [System.Windows.Forms.MessageBoxIcon]::Warning }
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
$summary,
|
||||
"Machine Number Update Result",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
$icon
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
|
||||
# Clean up result file for the next round.
|
||||
Remove-Item -LiteralPath $resultFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Only unregister the Prompt task on full success (no errors AND eDNC
|
||||
# updated to the requested value). If anything failed, leave it registered
|
||||
# for next logon retry.
|
||||
if ($result.Status -eq 'OK' -and $result.EdncUpdated) {
|
||||
Write-Host "All updates succeeded. Unregistering Prompt task."
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||
} else {
|
||||
Write-Host "Some updates failed or skipped. Prompt task stays registered for next logon retry."
|
||||
}
|
||||
|
||||
Write-Host "Prompt-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,179 +0,0 @@
|
||||
# Register-CheckMachineNumberTask.ps1 - Register the two-task machine
|
||||
# number flow at imaging time:
|
||||
#
|
||||
# 1. "Prompt Machine Number" - AtLogOn, BUILTIN\Users, Limited.
|
||||
# Shows InputBox + writes new number to a request file, then triggers
|
||||
# the SYSTEM task via schtasks /run.
|
||||
#
|
||||
# 2. "Apply Machine Number" - on-demand only (no trigger), SYSTEM,
|
||||
# RunLevel Highest. Reads the request file, calls Update-MachineNumber
|
||||
# with full HKLM + ProgramData access, writes a result JSON, removes
|
||||
# the request file. No GUI - the Prompt task polls the result file
|
||||
# and displays the dialog.
|
||||
#
|
||||
# Replaces the old single-task design that ran as the logged-in user with
|
||||
# pre-granted BUILTIN\Users HKLM ACLs (02-MachineNumberACLs.ps1). That
|
||||
# approach was fragile (timing race with eDNC install, silent ACL skip)
|
||||
# and a security hole (any user could write to the machine-identity reg
|
||||
# key). With SYSTEM doing the actual writes, no ACL grants needed.
|
||||
#
|
||||
# Idempotent: safe to re-run. Existing tasks are overwritten.
|
||||
#
|
||||
# File kept named Register-CheckMachineNumberTask.ps1 (rather than
|
||||
# Register-MachineNumberTasks.ps1) so Run-ShopfloorSetup's existing
|
||||
# discovery doesn't need editing.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
||||
$logFile = Join-Path $logDir 'register-checkmn.log'
|
||||
|
||||
function Write-RegLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [INFO] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask start ==='
|
||||
|
||||
$promptTaskName = 'Prompt Machine Number'
|
||||
$applyTaskName = 'Apply Machine Number'
|
||||
$oldTaskName = 'Check Machine Number' # legacy, removed below
|
||||
|
||||
# Clean up the legacy single-task name from prior imaging cycles.
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $oldTaskName -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $oldTaskName -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Unregistered legacy task '$oldTaskName'"
|
||||
}
|
||||
} catch { Write-RegLog "Could not unregister legacy '$oldTaskName': $_" }
|
||||
|
||||
# PC types that have no machine number by design. Arming the prompt on these
|
||||
# guarantees a dialog nobody can answer: startnet only collects a machine number
|
||||
# for the machine-tool types, so everything else is left on the 9999 placeholder
|
||||
# and the check below would otherwise fire on every one of them.
|
||||
#
|
||||
# Observed on 579C144 (a Display) 2026-08-06: PreInstall logged
|
||||
# "Machine number: 9999 (default placeholder)" and the logon prompt armed itself
|
||||
# on a kiosk with no keyboard.
|
||||
$noMachineNumberTypes = @(
|
||||
'gea-shopfloor-display'
|
||||
)
|
||||
$pcTypeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$pcType = ''
|
||||
if (Test-Path -LiteralPath $pcTypeFile) {
|
||||
$pcType = (Get-Content -LiteralPath $pcTypeFile -First 1 -ErrorAction SilentlyContinue)
|
||||
if ($pcType) { $pcType = $pcType.Trim() }
|
||||
}
|
||||
if ($pcType -and ($noMachineNumberTypes -contains $pcType)) {
|
||||
Write-RegLog "PC type '$pcType' has no machine number by design. Not registering the prompt."
|
||||
foreach ($t in @($promptTaskName, $applyTaskName)) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Unregistered stale task '$t'"
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# Only arm the tasks if the bay was imaged with the 9999 placeholder. If
|
||||
# the tech entered a real machine number during PXE imaging it's already
|
||||
# in C:\Enrollment\machine-number.txt; no prompt needed on first logon.
|
||||
$mnFile = 'C:\Enrollment\machine-number.txt'
|
||||
$mnAtImaging = '9999'
|
||||
if (Test-Path -LiteralPath $mnFile) {
|
||||
$raw = (Get-Content -LiteralPath $mnFile -First 1 -ErrorAction SilentlyContinue)
|
||||
if ($raw) { $mnAtImaging = $raw.Trim() }
|
||||
}
|
||||
Write-RegLog "Imaging-time machine-number.txt = '$mnAtImaging'"
|
||||
if ($mnAtImaging -ne '9999') {
|
||||
Write-RegLog "Machine number is real ('$mnAtImaging' != 9999). Not registering tasks."
|
||||
foreach ($t in @($promptTaskName, $applyTaskName)) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Unregistered stale task '$t'"
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask end (no-op) ==='
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Resolve script paths. Prefer the staged shopfloor-setup tree on C:
|
||||
# (where Run-ShopfloorSetup ran from); fall back to the same dir as this
|
||||
# Register script if invoked standalone.
|
||||
function Resolve-Script {
|
||||
param([string]$LeafName)
|
||||
$p = Join-Path $PSScriptRoot $LeafName
|
||||
if (Test-Path -LiteralPath $p) { return $p }
|
||||
$p = "C:\Enrollment\shopfloor-setup\Shopfloor\$LeafName"
|
||||
if (Test-Path -LiteralPath $p) { return $p }
|
||||
return $null
|
||||
}
|
||||
|
||||
$promptScript = Resolve-Script 'Prompt-MachineNumber.ps1'
|
||||
$applyScript = Resolve-Script 'Apply-MachineNumber.ps1'
|
||||
if (-not $promptScript) { Write-RegLog "Prompt-MachineNumber.ps1 not found - cannot register"; exit 1 }
|
||||
if (-not $applyScript) { Write-RegLog "Apply-MachineNumber.ps1 not found - cannot register"; exit 1 }
|
||||
Write-RegLog "Prompt script: $promptScript"
|
||||
Write-RegLog "Apply script: $applyScript"
|
||||
|
||||
# --- Prompt task (user-context, GUI) ---
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$promptScript`""
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
# Group SID S-1-5-32-545 = BUILTIN\Users (catches ShopFloor + support/admin
|
||||
# users that log in interactively). RunLevel Limited - no elevation; the
|
||||
# actual writes happen in the SYSTEM Apply task below.
|
||||
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
||||
Register-ScheduledTask -TaskName $promptTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null
|
||||
Write-RegLog "Registered scheduled task '$promptTaskName' (AtLogOn, BUILTIN\Users, Limited)"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register '$promptTaskName': $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Apply task (SYSTEM, on-demand) ---
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$applyScript`""
|
||||
# No trigger - the Prompt task starts this via schtasks /run /tn.
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
||||
Register-ScheduledTask -TaskName $applyTaskName -Action $action -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null
|
||||
Write-RegLog "Registered scheduled task '$applyTaskName' (on-demand, SYSTEM, Highest)"
|
||||
|
||||
# Default SDDL on a SYSTEM-owned task only grants Admins + SYSTEM
|
||||
# FullAccess - BUILTIN\Users can't see or run it via schtasks /run.
|
||||
# Add an ACE granting BUILTIN\Users GenericRead + GenericExecute so the
|
||||
# user-context Prompt task can trigger this Apply task on demand. They
|
||||
# still can't modify/delete it - only read+execute.
|
||||
try {
|
||||
$svc = New-Object -ComObject Schedule.Service
|
||||
$svc.Connect()
|
||||
$taskObj = $svc.GetFolder('\').GetTask($applyTaskName)
|
||||
# GenericRead = 0x80000000 (GR), GenericExecute = 0x20000000 (GX)
|
||||
# BU = BUILTIN\Users
|
||||
$newSd = 'O:BAG:BAD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;GRGX;;;BU)'
|
||||
# SetSecurityDescriptor flag 0 = default, persists DACL change.
|
||||
$taskObj.SetSecurityDescriptor($newSd, 0)
|
||||
Write-RegLog "Granted BUILTIN\Users GR+GX on '$applyTaskName' (so Limited users can schtasks /run)"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to set task SDDL on '$applyTaskName': $_ (Limited users may not be able to trigger Apply)"
|
||||
}
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register '$applyTaskName': $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask end ==='
|
||||
exit 0
|
||||
@@ -1,115 +0,0 @@
|
||||
# Register-MapSfldShare.ps1 - Stage Map-SfldShare.ps1 + register an
|
||||
# HKLM\Run entry that maps S: for any interactive user (SupportUser,
|
||||
# ShopFloor, any future end-user accounts).
|
||||
#
|
||||
# Why HKLM\Run instead of a scheduled task: Run fires at Explorer
|
||||
# startup in the logged-in user's interactive session with their full
|
||||
# token + HKCU mounted. No principal/LogonType/group-SID plumbing, no
|
||||
# "task fires in session 0 but drive not visible to Explorer" class of
|
||||
# bugs. Works for every BUILTIN\Users member with no extra logic.
|
||||
#
|
||||
# Why not the vendor's ConsumeCredentials.ps1: it calls
|
||||
# New-StoredCredential -Persist LocalMachine (needs admin) before net
|
||||
# use. ShopFloor is non-admin, so the cred-store fails and net use has
|
||||
# no auth. Our Map-SfldShare.ps1 reads HKLM creds directly and passes
|
||||
# them inline to net use /user: -- no Credential Manager needed.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# PC types that are self-contained and must NOT map S:. A Display kiosk is
|
||||
# Entra-joined with local accounts and no SFLD credentials, so the mapping can
|
||||
# only ever fail - once per logon, forever.
|
||||
#
|
||||
# Run-ShopfloorSetup.ps1 already gates the CALL to this script on the same list,
|
||||
# but on 579C144 (2026-08-06) the Run entry was registered on a Display anyway:
|
||||
#
|
||||
# [15:07:55] Set HKLM:\...\Run\GE Map SFLD Share = ...Map-SfldShare.ps1
|
||||
#
|
||||
# with no "Skipping S: drive logon mapper" in the log, so something in the
|
||||
# finalization phase reaches this script past the call-site gate. Gating here as
|
||||
# well makes the outcome correct regardless of who invokes it.
|
||||
$selfContainedTypes = @('Display', 'gea-shopfloor-display')
|
||||
$pcTypeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$pcType = ''
|
||||
if (Test-Path -LiteralPath $pcTypeFile) {
|
||||
$pcType = (Get-Content -LiteralPath $pcTypeFile -First 1 -ErrorAction SilentlyContinue)
|
||||
if ($pcType) { $pcType = $pcType.Trim() }
|
||||
}
|
||||
if ($pcType -and ($selfContainedTypes -contains $pcType)) {
|
||||
$d = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force -EA SilentlyContinue | Out-Null }
|
||||
Add-Content -Path (Join-Path $d 'register-mapshare.log') -EA SilentlyContinue `
|
||||
-Value ("[{0}] [INFO] PC type '{1}' is self-contained - not registering the S: mapper." -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $pcType)
|
||||
Write-Host "PC type '$pcType' is self-contained - skipping S: drive mapper."
|
||||
# Remove a stale entry from an earlier image or an earlier code path.
|
||||
try {
|
||||
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
|
||||
if ((Get-ItemProperty -Path $runKey -Name 'GE Map SFLD Share' -EA SilentlyContinue)) {
|
||||
Remove-ItemProperty -Path $runKey -Name 'GE Map SFLD Share' -Force -EA Stop
|
||||
Write-Host " removed stale 'GE Map SFLD Share' Run entry."
|
||||
}
|
||||
} catch { }
|
||||
return
|
||||
}
|
||||
|
||||
$installRoot = 'C:\Program Files\GE\SfldShare'
|
||||
$mapScript = Join-Path $installRoot 'Map-SfldShare.ps1'
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
$logFile = Join-Path $logDir 'register-mapshare.log'
|
||||
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
|
||||
$runValue = 'GE Map SFLD Share'
|
||||
$legacyTask = 'GE Shopfloor Map S: Drive'
|
||||
|
||||
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
||||
|
||||
function Write-RegLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [INFO] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-MapSfldShare start ==='
|
||||
|
||||
# Stage our Map-SfldShare.ps1 to a persistent location
|
||||
if (-not (Test-Path $installRoot)) {
|
||||
New-Item -Path $installRoot -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$src = Join-Path $PSScriptRoot 'lib\Map-SfldShare.ps1'
|
||||
if (Test-Path $src) {
|
||||
Copy-Item -Path $src -Destination $mapScript -Force
|
||||
Write-RegLog "Staged $src -> $mapScript"
|
||||
} else {
|
||||
Write-RegLog "Map-SfldShare.ps1 not found at $src - cannot register"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Remove the legacy scheduled task if it exists (left behind by older
|
||||
# imaging runs that used the scheduled-task approach).
|
||||
if (Get-ScheduledTask -TaskName $legacyTask -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $legacyTask -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Removed legacy scheduled task '$legacyTask'"
|
||||
} catch {
|
||||
Write-RegLog "Failed to remove legacy task '$legacyTask': $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Register HKLM\Run entry. Runs at Explorer startup for every
|
||||
# interactive user in that user's session.
|
||||
try {
|
||||
$command = '"{0}" -NoProfile -ExecutionPolicy Bypass -File "{1}"' -f `
|
||||
"$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe", $mapScript
|
||||
|
||||
if (-not (Test-Path $runKey)) {
|
||||
New-Item -Path $runKey -Force | Out-Null
|
||||
}
|
||||
New-ItemProperty -Path $runKey -Name $runValue -Value $command -PropertyType String -Force | Out-Null
|
||||
Write-RegLog "Set $runKey\$runValue = $command"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register Run key: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-MapSfldShare end ==='
|
||||
exit 0
|
||||
32
playbook/shopfloor-setup/Shopfloor/Register-PostPpkgTask.ps1
Normal file
32
playbook/shopfloor-setup/Shopfloor/Register-PostPpkgTask.ps1
Normal file
@@ -0,0 +1,32 @@
|
||||
# Register-PostPpkgTask.ps1 - register the "GE Shopfloor Post-PPKG" task.
|
||||
#
|
||||
# Called by Run-ShopfloorSetup.ps1 BEFORE the PPKG step (the PPKG reboot can
|
||||
# kill the caller). Copies the runner out of C:\Enrollment (the bulk-enroll
|
||||
# cleanup may remove staging folders) to C:\Program Files\GE\Shopfloor and
|
||||
# registers a SYSTEM task that runs Invoke-PostPpkg.ps1 at every startup and
|
||||
# logon until it has nothing left to do; then it unregisters itself.
|
||||
#
|
||||
# Idempotent: re-registering replaces the task.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$runtimeDir = 'C:\Program Files\GE\Shopfloor'
|
||||
$taskName = 'GE Shopfloor Post-PPKG'
|
||||
|
||||
New-Item -Path $runtimeDir -ItemType Directory -Force | Out-Null
|
||||
foreach ($f in @('Install-AppList.ps1', 'Invoke-PostPpkg.ps1')) {
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $f) -Destination (Join-Path $runtimeDir $f) -Force
|
||||
}
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$runtimeDir\Invoke-PostPpkg.ps1`""
|
||||
$triggers = @(
|
||||
(New-ScheduledTaskTrigger -AtStartup),
|
||||
(New-ScheduledTaskTrigger -AtLogOn)
|
||||
)
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $triggers `
|
||||
-Principal $principal -Settings $settings -Force | Out-Null
|
||||
Write-Host "Registered '$taskName' (SYSTEM, at startup and logon): $runtimeDir\Invoke-PostPpkg.ps1"
|
||||
@@ -1,51 +0,0 @@
|
||||
'----------------------------------------------------------------------
|
||||
' This macro was created by the macro recorder.
|
||||
' Macro File: Office.ebs
|
||||
' Date: Wed May 18 09:55:44 2016
|
||||
' Recorded for profile: WJ_Office
|
||||
'----------------------------------------------------------------------
|
||||
|
||||
Sub Main
|
||||
Dim HostExplorer as Object
|
||||
Dim MyHost as Object
|
||||
Dim Rc as Integer
|
||||
|
||||
Dim iPSUpdateTimeout
|
||||
Dim iWaitForStringTimeout
|
||||
|
||||
On Error goto GenericErrorHandler
|
||||
|
||||
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||
Set MyHost = HostExplorer.HostFromProfile("WJ_Office") ' Set object for the desired session
|
||||
If MyHost is Nothing Then Goto NoSession
|
||||
|
||||
iPSUpdateTimeout = 60 ' WaitPSUpdated timeout set to 60 seconds
|
||||
iWaitForStringTimeout = 60 ' WaitForString timeout set to 60 seconds
|
||||
|
||||
Rc = MyHost.WaitForString( "Username:", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Rc = MyHost.Keys("shop_pc^M")
|
||||
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||
Rc = MyHost.WaitForString( "Do you have a barcode reader?", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Exit Sub
|
||||
|
||||
'-------------------- Runtime Error Handlers --------------------
|
||||
GenericErrorHandler:
|
||||
Msgbox "Error " & Err & " : """ & Error(Err) & """ has occurred on line " & Erl-1 & "." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
NoSession:
|
||||
Msgbox "Profile ""WJ_Office"" is not running." & Chr(10) & "Unable to execute macro.", 16, "HostExplorer Macro Error"
|
||||
Exit Sub
|
||||
|
||||
OnWaitPSUpdatedTimeout:
|
||||
Msgbox "Timeout occured waiting for host to update screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
OnWaitForStringTimeout:
|
||||
Msgbox "Timeout occured waiting for string on host screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
End Sub
|
||||
@@ -1,35 +0,0 @@
|
||||
'----------------------------------------------------------------------
|
||||
' This macro was created by the macro recorder.
|
||||
' Macro File: mmcs.ebs
|
||||
' Date: Tue Jun 04 08:56:51 2013
|
||||
' Recorded for profile: mmcs
|
||||
'----------------------------------------------------------------------
|
||||
|
||||
Sub Main
|
||||
Dim HostExplorer as Object
|
||||
Dim MyHost as Object
|
||||
Dim iIdleTime
|
||||
|
||||
Dim iPSUpdateTime
|
||||
|
||||
On Error goto ErrorCheck
|
||||
|
||||
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||
Set MyHost = HostExplorer.HostFromProfile("mmcs") ' Set object for the desired session
|
||||
|
||||
iPSUpdateTime = 60 ' PS Update wait time set to 60 seconds
|
||||
iIdleTime = 2000 ' Idle time set to 2000 milliseconds
|
||||
|
||||
MyHost.WaitPSUpdated(iPSUpdateTime)
|
||||
Rc = MyHost.WaitForString("Username:", -1, 9999, TRUE)
|
||||
MyHost.Keys("mmcswj^M")
|
||||
MyHost.WaitPSUpdated(iPSUpdateTime)
|
||||
Rc = MyHost.WaitForString("| Badge : |", -1, 9999, TRUE)
|
||||
Exit Sub
|
||||
|
||||
ErrorCheck:
|
||||
if (Err = 440) Then
|
||||
Msgbox "The specified session is not running.", 16, "Hummingbird Macro Error"
|
||||
End If
|
||||
Exit Sub
|
||||
End Sub
|
||||
@@ -1,56 +0,0 @@
|
||||
'----------------------------------------------------------------------
|
||||
' This macro was created by the macro recorder.
|
||||
' Macro File: shopfloor.ebs
|
||||
' Date: Wed May 18 09:57:00 2016
|
||||
' Recorded for profile: WJ Shopfloor
|
||||
'----------------------------------------------------------------------
|
||||
|
||||
Sub Main
|
||||
Dim HostExplorer as Object
|
||||
Dim MyHost as Object
|
||||
Dim Rc as Integer
|
||||
|
||||
Dim iPSUpdateTimeout
|
||||
Dim iWaitForStringTimeout
|
||||
|
||||
On Error goto GenericErrorHandler
|
||||
|
||||
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||
Set MyHost = HostExplorer.HostFromProfile("WJ Shopfloor") ' Set object for the desired session
|
||||
If MyHost is Nothing Then Goto NoSession
|
||||
|
||||
iPSUpdateTimeout = 60 ' WaitPSUpdated timeout set to 60 seconds
|
||||
iWaitForStringTimeout = 60 ' WaitForString timeout set to 60 seconds
|
||||
|
||||
Rc = MyHost.WaitForString( "Username:", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Rc = MyHost.Keys("shop_xmi^M")
|
||||
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||
Rc = MyHost.WaitForString( "Password:", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Rc = MyHost.Keys("dnc123^M")
|
||||
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||
Rc = MyHost.WaitForString( "| EXIT | |", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Exit Sub
|
||||
|
||||
'-------------------- Runtime Error Handlers --------------------
|
||||
GenericErrorHandler:
|
||||
Msgbox "Error " & Err & " : """ & Error(Err) & """ has occurred on line " & Erl-1 & "." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
NoSession:
|
||||
Msgbox "Profile ""WJ Shopfloor"" is not running." & Chr(10) & "Unable to execute macro.", 16, "HostExplorer Macro Error"
|
||||
Exit Sub
|
||||
|
||||
OnWaitPSUpdatedTimeout:
|
||||
Msgbox "Timeout occured waiting for host to update screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
OnWaitForStringTimeout:
|
||||
Msgbox "Timeout occured waiting for string on host screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
End Sub
|
||||
@@ -1,9 +0,0 @@
|
||||
[KEYMAP]
|
||||
Normal Entries=1
|
||||
Normal0=Backspace,Delete
|
||||
Shift Entries=0
|
||||
Ctrl Entries=0
|
||||
ShiftCtrl Entries=0
|
||||
Alt Entries=0
|
||||
ShiftAlt Entries=0
|
||||
AltCtrl Entries=0
|
||||
@@ -1,13 +0,0 @@
|
||||
[KEYMAP]
|
||||
Normal Entries=5
|
||||
Normal0=F4,Pf4
|
||||
Normal1=Subtract,Num-Pad-Minus
|
||||
Normal2=Multiply,Num-Pad-*
|
||||
Normal3=Divide,Num-Pad-/
|
||||
Normal4=Backspace,Delete
|
||||
Shift Entries=0
|
||||
Ctrl Entries=0
|
||||
ShiftCtrl Entries=0
|
||||
Alt Entries=0
|
||||
ShiftAlt Entries=0
|
||||
AltCtrl Entries=0
|
||||
@@ -1,23 +0,0 @@
|
||||
[Version]
|
||||
AssemblyVersion=1
|
||||
[Console]
|
||||
Title=HostExplorer Menu Editor
|
||||
IconPath=HumCSSPlugins.HumCSSActiveTunnelsPlugin
|
||||
IconID=IDI_CSS_CONSOLE
|
||||
[Attributes]
|
||||
SupportAssemblyEditing=1
|
||||
SupportSeparator=1
|
||||
ParentLevels=100
|
||||
[Parent Node]
|
||||
name=Menu
|
||||
[SubParent Node]
|
||||
name=Submenu
|
||||
[Child Node]
|
||||
name=Menu Option
|
||||
[Messages]
|
||||
Create New Parent=Create New Menu
|
||||
Create New SubParent=Create New Submenu
|
||||
Insert New Parent=Insert New Menu
|
||||
Insert New SubParent=Insert New Submenu
|
||||
[Root Group]
|
||||
Name=Menu
|
||||
@@ -1,474 +0,0 @@
|
||||
[version]
|
||||
AssemblyVersion=1
|
||||
|
||||
[nls]
|
||||
filename=menunls$LANGUAGE_EXTENSION$.hma
|
||||
|
||||
[Console]
|
||||
Title=HostExplorer Menu Editor
|
||||
IconPath=HumCSSPlugins.HumCSSActiveTunnelsPlugin
|
||||
IconID=IDI_CSS_CONSOLE
|
||||
|
||||
[Attributes]
|
||||
SupportAssemblyEditing=1
|
||||
SupportEnablingDisablingItems=0
|
||||
SupportSeparator=1
|
||||
|
||||
[Parent Node]
|
||||
Nameid=Parent
|
||||
|
||||
[SubParent Node]
|
||||
Nameid=SubParent
|
||||
|
||||
[Child Node]
|
||||
Nameid=Child
|
||||
|
||||
[Messages]
|
||||
Nameid1=Create New Parent
|
||||
Nameid2=Create New SubParent
|
||||
Nameid3=Insert New Parent
|
||||
Nameid4=Insert New SubParent
|
||||
|
||||
[root group]
|
||||
Name=Menu
|
||||
item1=File
|
||||
item2=Edit
|
||||
item3=Transfer
|
||||
item4=Fonts
|
||||
item5=options
|
||||
item6=tools
|
||||
item7=view
|
||||
item8=window
|
||||
item9=help
|
||||
|
||||
[Separator]
|
||||
name=
|
||||
id=0
|
||||
|
||||
[File]
|
||||
Nameid=File
|
||||
id=4071
|
||||
Parent=1
|
||||
item1=new session
|
||||
item2=duplicate session
|
||||
item3=open session
|
||||
item4=open session in same window
|
||||
item5=save session profile
|
||||
item6=close session
|
||||
item7=separator
|
||||
item8=open layout
|
||||
item9=save layout
|
||||
item10=separator
|
||||
item11=connect
|
||||
item12=disconnect
|
||||
item13=separator
|
||||
item14=print screen
|
||||
item15=print multiple screens
|
||||
item16=save screen
|
||||
item17=send screen
|
||||
item18=separator
|
||||
item19=screen capture
|
||||
item20=separator
|
||||
item21=Recent Sessions Submenu
|
||||
item22=separator
|
||||
item23=exit all
|
||||
|
||||
[New Session]
|
||||
nameid=New Session
|
||||
id=Dlg-New-Session
|
||||
|
||||
[Duplicate Session]
|
||||
nameid=Duplicate Session
|
||||
id=Duplicate-Session
|
||||
|
||||
[Open Session]
|
||||
nameid=Open Session
|
||||
id=Dlg-Open-Session
|
||||
|
||||
[Open Session In Same Window]
|
||||
nameid=Open Session In Same Window
|
||||
id=Dlg-Open-Session-In-Same-Window
|
||||
|
||||
[Save Session Profile]
|
||||
nameid=Save Session Profile
|
||||
id=Dlg-Save-Profile
|
||||
|
||||
[Close Session]
|
||||
nameid=Close Session
|
||||
id=Dlg-Close-Session
|
||||
|
||||
[open layout]
|
||||
nameid=Open Layout
|
||||
id=Dlg-Open-Layout
|
||||
|
||||
[save layout]
|
||||
nameid=Save Layout
|
||||
id=Dlg-Save-Layout
|
||||
|
||||
[connect]
|
||||
nameid=connect
|
||||
id=Connect
|
||||
|
||||
[disconnect]
|
||||
nameid=disconnect
|
||||
id=Disconnect
|
||||
|
||||
[print screen]
|
||||
nameid=print screen
|
||||
id=Dlg-Print-Screen
|
||||
|
||||
[print multiple screens]
|
||||
nameid=print multiple screens
|
||||
id=Dlg-Print-Multiple-Screens
|
||||
|
||||
|
||||
[save screen]
|
||||
nameid=Save Screen
|
||||
id=Dlg-Save-Screen
|
||||
|
||||
[send screen]
|
||||
nameid=Send Screen
|
||||
id=Send-Screen
|
||||
|
||||
[screen capture]
|
||||
nameid=Screen Capture
|
||||
id=Toggle-Capture
|
||||
|
||||
[Recent Sessions Submenu]
|
||||
nameid=Recent Sessions
|
||||
id=4187
|
||||
Parent=1
|
||||
item1=recent sessions
|
||||
|
||||
[recent sessions]
|
||||
nameid=Recent Sessions
|
||||
id=Recent-Sessions
|
||||
|
||||
[Exit all]
|
||||
nameid=exit all
|
||||
id=Dlg-Exit
|
||||
|
||||
|
||||
[Edit]
|
||||
Nameid=Edit
|
||||
id=4072
|
||||
Parent=1
|
||||
item1=copy
|
||||
item2=copy append
|
||||
item3=paste
|
||||
item4=separator
|
||||
item5=select all
|
||||
item6=separator
|
||||
item7=edit find
|
||||
item8=separator
|
||||
item9=clear display
|
||||
item10=clear all
|
||||
item11=soft terminal reset
|
||||
|
||||
[undo]
|
||||
nameid=undo
|
||||
id=Edit-Undo
|
||||
|
||||
[redo]
|
||||
nameid=redo
|
||||
id=Edit-Redo
|
||||
|
||||
[cut]
|
||||
nameid=cut
|
||||
id=Edit-Cut
|
||||
|
||||
[Copy]
|
||||
nameid=copy
|
||||
id=Edit-Copy
|
||||
|
||||
[Copy append]
|
||||
nameid=Copy Append
|
||||
id=Edit-Copy-Append
|
||||
|
||||
[Paste]
|
||||
nameid=Paste
|
||||
id=Edit-Paste
|
||||
|
||||
[Paste continue]
|
||||
nameid=Paste Continue
|
||||
id=Edit-Paste-Continue
|
||||
|
||||
[Select All]
|
||||
nameid=Select All
|
||||
id=Edit-SelectAll
|
||||
|
||||
[Edit Options]
|
||||
nameid=Options
|
||||
id=Dlg-Options
|
||||
|
||||
[edit find]
|
||||
nameid=Find
|
||||
id=Dlg-Find
|
||||
|
||||
[clear display]
|
||||
nameid=Clear Display
|
||||
id=Clear-Display
|
||||
|
||||
[clear all]
|
||||
nameid=Clear All
|
||||
id=Clear-Buffer
|
||||
|
||||
[soft terminal reset]
|
||||
nameid=Soft Terminal Reset
|
||||
id=Power-On-Reset
|
||||
|
||||
|
||||
[Transfer]
|
||||
Nameid=Transfer
|
||||
id=4073
|
||||
Parent=1
|
||||
item1=send
|
||||
item2=receive
|
||||
|
||||
[send]
|
||||
nameid=Send File to Host
|
||||
id=Dlg-Upload
|
||||
|
||||
[receive]
|
||||
nameid=Receive File from Host
|
||||
id=Dlg-Download
|
||||
|
||||
|
||||
[Fonts]
|
||||
Nameid=Fonts
|
||||
id=4074
|
||||
Parent=1
|
||||
item1=next larger
|
||||
item2=next smaller
|
||||
item3=choose font
|
||||
item4=maximize font
|
||||
|
||||
[next larger]
|
||||
nameid=next larger font
|
||||
id=Font-Larger
|
||||
|
||||
[next smaller]
|
||||
nameid=Next smaller font
|
||||
id=Font-Smaller
|
||||
|
||||
[choose font]
|
||||
nameid=font
|
||||
id=Dlg-Font-Select
|
||||
|
||||
[maximize font]
|
||||
nameid=Maximize font
|
||||
id=Maximize-Font
|
||||
|
||||
|
||||
[options]
|
||||
nameid=Options
|
||||
id=4075
|
||||
Parent=1
|
||||
item1=global options
|
||||
item2=api settings
|
||||
item3=separator
|
||||
item4=keyboard mapping
|
||||
item5=quick keys
|
||||
item6=separator
|
||||
item7=session properties
|
||||
|
||||
[global options]
|
||||
nameid=Global Options
|
||||
id=Dlg-Global
|
||||
|
||||
[api settings]
|
||||
nameid=API Settings
|
||||
id=Dlg-API-Settings
|
||||
|
||||
[keyboard mapping]
|
||||
nameid=Keyboard Mapping
|
||||
id=Dlg-Keyboard-Mapper
|
||||
|
||||
[quick keys]
|
||||
nameid=Quick Keys
|
||||
id=Dlg-Quick-Key-Editor
|
||||
|
||||
[session properties]
|
||||
nameid=session Properties
|
||||
id=Dlg-Edit-Session-Profile
|
||||
|
||||
|
||||
[tools]
|
||||
nameid=Tools
|
||||
id=4076
|
||||
Parent=1
|
||||
item1=Macro Submenu
|
||||
item2=Quick Script Submenu
|
||||
item3=separator
|
||||
item4=Customizetoolbar
|
||||
item5=Customizemenu
|
||||
item6=Customizesessionproperties
|
||||
|
||||
[Macro Submenu]
|
||||
nameid=Macro
|
||||
id=4177
|
||||
Parent=1
|
||||
item1=edit macro
|
||||
item2=run macro
|
||||
item3=separator
|
||||
item4=start recording
|
||||
item5=pause recording
|
||||
item6=resume recording
|
||||
item7=stop recording
|
||||
item8=cancel recording
|
||||
|
||||
[Edit macro]
|
||||
nameid=Macro Edit
|
||||
id=Dlg-Edit-Macro
|
||||
|
||||
[run macro]
|
||||
nameid=run
|
||||
id=Dlg-Run-Macro
|
||||
|
||||
[start recording]
|
||||
nameid=Start Recording
|
||||
id=Record-Macro
|
||||
|
||||
[pause recording]
|
||||
nameid=Pause Recording
|
||||
id=Toggle-Recording-Pause
|
||||
|
||||
[resume recording]
|
||||
nameid=Resume Recording
|
||||
id=Resume-Recording-Macro
|
||||
|
||||
[stop recording]
|
||||
nameid=Stop Recording
|
||||
id=End-Recording
|
||||
|
||||
[cancel recording]
|
||||
nameid=Cancel Recording
|
||||
id=Cancel-Macro-Recording
|
||||
|
||||
[Quick Script Submenu]
|
||||
nameid=Quick Script
|
||||
id=4091
|
||||
Parent=1
|
||||
item1=edit qs
|
||||
item2=run qs
|
||||
item3=stop qs
|
||||
item4=separator
|
||||
item5=start recording qs
|
||||
item6=pause recording qs
|
||||
item7=resume recording qs
|
||||
item8=stop recording qs
|
||||
item9=cancel recording qs
|
||||
|
||||
[Customizetoolbar]
|
||||
nameid=Customize Toolbar
|
||||
id=Dlg-Toolbars
|
||||
|
||||
[Customizemenu]
|
||||
nameid=Customize Menu
|
||||
id=Dlg-Customize-Menus
|
||||
|
||||
[CustomizeSessionProperties]
|
||||
nameid=Customize Session Properties
|
||||
id=Dlg-Customize-Session-Properties
|
||||
|
||||
[Edit qs]
|
||||
nameid=Edit
|
||||
id=Dlg-QuickScript-Editor
|
||||
|
||||
[run qs]
|
||||
nameid=Run
|
||||
id=Dlg-Run-QuickScript
|
||||
|
||||
[stop qs]
|
||||
nameid=Stop
|
||||
id=Stop-QuickScript
|
||||
|
||||
[start recording qs]
|
||||
nameid=Start Recording
|
||||
id=Record-QuickScript
|
||||
|
||||
[pause recording qs]
|
||||
nameid=Pause Recording
|
||||
id=Pause-Recording-QuickScript
|
||||
|
||||
[resume recording qs]
|
||||
nameid=Resume Recording
|
||||
id=Resume-Recording-QuickScript
|
||||
|
||||
[stop recording qs]
|
||||
nameid=Stop Recording
|
||||
id=Stop-Recording-QuickScript
|
||||
|
||||
[cancel recording qs]
|
||||
nameid=Cancel Recording
|
||||
id=Cancel-Recording-QuickScript
|
||||
|
||||
|
||||
[view]
|
||||
nameid=view
|
||||
id=4178
|
||||
Parent=1
|
||||
item1=hotspots
|
||||
item2=row and column indicator
|
||||
item3=cross hair cursor
|
||||
item4=full screen
|
||||
|
||||
[hotspots]
|
||||
nameid=Hotspots
|
||||
id=Toggle-View-Hotspots
|
||||
|
||||
[Row and Column Indicator]
|
||||
nameid=Row and Column Indicator
|
||||
id=Toggle-Row-And-Column-Indicator
|
||||
|
||||
[cross hair cursor]
|
||||
nameid=Cross-Hair Cursor
|
||||
id=Toggle-CrossHair-Cursor
|
||||
|
||||
[full screen]
|
||||
nameid=Full Screen
|
||||
id=Toggle-Full-Screen
|
||||
|
||||
|
||||
[window]
|
||||
nameid=Window
|
||||
id=4179
|
||||
Parent=1
|
||||
item1=cascade
|
||||
item2=next session
|
||||
item3=previous session
|
||||
item4=separator
|
||||
item5=session window list
|
||||
|
||||
[cascade]
|
||||
nameid=Cascade
|
||||
id=Cascade-Session-Windows
|
||||
|
||||
[next session]
|
||||
nameid=next session
|
||||
id=Next-Session
|
||||
|
||||
[previous session]
|
||||
nameid=previous session
|
||||
id=Prev-Session
|
||||
|
||||
[session window list]
|
||||
name=<List Of Windows>
|
||||
id=4142
|
||||
|
||||
|
||||
[help]
|
||||
nameid=Help
|
||||
id=4180
|
||||
Parent=1
|
||||
item1=contents
|
||||
item2=separator
|
||||
item3=about
|
||||
|
||||
[contents]
|
||||
nameid=Contents
|
||||
id=Help-Index
|
||||
|
||||
[about]
|
||||
nameid=about
|
||||
id=Help-About
|
||||
Binary file not shown.
@@ -1,106 +0,0 @@
|
||||
[PROFILE]
|
||||
Session Properties=Default 3270
|
||||
Menu=Default 3270
|
||||
Auto Run Macro Delay Time=1
|
||||
Gateway Name=ibmqks.ae.ge.com
|
||||
Session Keep Alive Interval=600
|
||||
PC Keyboard Type=5
|
||||
IND$FILE Parms=0,0,1,1,0,0,0,257,253,2048,200,430,430,0,1252,"IND$FILE","","","",0
|
||||
SSL/TLS Current User Certificate=1
|
||||
Window Title=%s - %p (%h)
|
||||
WinInfo Default Restored=0 0 0 0 70 405 822 595 0 0 0=20 10 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
WinInfo Default Maximized=0 0 0 0 -8 -8 1936 1048 0 0 0=26 12 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
Print Header2=Page: %# Document Name: %F
|
||||
QuickKey=Default
|
||||
Never Run Profile=Off
|
||||
Profile File Version=200
|
||||
Space Allocation Parms=0,0,0,0
|
||||
Kerberos Use Hummingbird Kerberos=On
|
||||
Cryptographic Mode=1
|
||||
IP Port=992
|
||||
Security Option=2
|
||||
SSL/TLS User Cipher Suites=DEFAULT
|
||||
[HEBar-59440]
|
||||
sizeFloatCX=505
|
||||
sizeFloatCY=28
|
||||
[Toolbars Layout-Summary]
|
||||
Bars=3
|
||||
ScreenCX=1920
|
||||
ScreenCY=1080
|
||||
[Session Options]
|
||||
Last Selected Page=139
|
||||
[Toolbars]
|
||||
ToolBarVersion=6
|
||||
bar0=Default 3270 ToolBar.tb3
|
||||
[Toolbars Layout-Bar0]
|
||||
BarID=59393
|
||||
[Toolbars Layout-Bar1]
|
||||
BarID=59419
|
||||
Bars=3
|
||||
Bar#0=0
|
||||
Bar#1=59440
|
||||
Bar#2=0
|
||||
[Toolbars Layout-Bar2]
|
||||
BarID=59440
|
||||
XPos=-2
|
||||
YPos=-2
|
||||
Docking=1
|
||||
MRUDockID=59419
|
||||
MRUDockLeftPos=-2
|
||||
MRUDockTopPos=-2
|
||||
MRUDockRightPos=512
|
||||
MRUDockBottomPos=26
|
||||
MRUFloatStyle=8196
|
||||
MRUFloatXPos=-1
|
||||
MRUFloatYPos=1025
|
||||
[Hotspots]
|
||||
ShowTips=1
|
||||
[ShortcutReplacers]
|
||||
Size=0
|
||||
[Site Info]
|
||||
EnableCache=Yes
|
||||
RefreshCacheOnConnect=0
|
||||
AlwaysUploadUsing=0
|
||||
AlwaysDownloadUsing=0
|
||||
ProfileVersion=5
|
||||
SecurityType=0
|
||||
Port=21
|
||||
SSL Implicit Port=990
|
||||
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
SFTPProxyAddress=localhost
|
||||
SFTPProxyPort=0
|
||||
SFTPWriteSize=256
|
||||
GssApiServiceName=host
|
||||
GssApiProtLevel=P
|
||||
GssApiIssueCCC=No
|
||||
GssApiPBSZ=16000
|
||||
GssApiKerbClient=0
|
||||
GssApiUseHMS2MIT=No
|
||||
PassiveMode=2
|
||||
AllowServer2ServerTransfer=0
|
||||
KeepAlive=No
|
||||
AutoDetectServerType=Yes
|
||||
KeepAliveTime=0
|
||||
FirewallPort=21
|
||||
FirewallType=0
|
||||
NetworkTimeout=120
|
||||
ConvertGMTTime=0
|
||||
ResolveLinks=1
|
||||
RootToInitialDir=0
|
||||
ChangeDirChangesRoot=0
|
||||
DropVmsVersion=0
|
||||
LocalHourOffset=0
|
||||
LocalMinuteOffset=0
|
||||
TemporaryProfile=0
|
||||
SSLVersion=3
|
||||
SSLReuse=0
|
||||
SSLUserCertificateMode=0
|
||||
SSLCloseOnNegotiateionFail=0
|
||||
SSLAcceptServerSelfSignedCertificates=Yes
|
||||
SSLAcceptServerUnverifiedCertificates=Yes
|
||||
SSLDataChannelProtected=Yes
|
||||
SSLCryptographicMode=1
|
||||
AllowResume=No
|
||||
EPSV4=0
|
||||
[Stats]
|
||||
NumVisits=0
|
||||
@@ -1,122 +0,0 @@
|
||||
[PROFILE]
|
||||
Session Terminal Type=2
|
||||
Session Properties=Default VT
|
||||
Menu=Default VT
|
||||
Auto Run Macro Delay Time=1
|
||||
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||
Session Keep Alive Interval=600
|
||||
PC Keyboard Type=5
|
||||
Tabstops='8 16 24 32 40 48 56 64 72 '
|
||||
VT BG Colors='1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 8 7 7 9 15 14 10 2 10 '
|
||||
Default VT Recv Path=$MYDOCUMENTS$\
|
||||
SSL/TLS Version=3
|
||||
SSL/TLS Current User Certificate=1
|
||||
Window Title=%s - %p (%h)
|
||||
WinInfo Default Restored=0 0 0 0 176 21 892 619 0 0 0=-17 0 0 0 400 0 0 0 0 3 2 2 0 Courier New| 0 0
|
||||
WinInfo Default Maximized=0 0 0 0 -8 -8 1634 1066 0 0 0=30 15 0 0 700 0 0 0 1 3 2 2 0 HE_Bitmap| 0 0
|
||||
Print Header2=Page: %# Document Name: %F
|
||||
QuickKey=Super Key F6
|
||||
Never Run Profile=Off
|
||||
dwVTFlags1=29369344
|
||||
VT FG Colors='7 7 9 15 14 10 2 10 0 1 2 3 4 5 15 7 8 9 10 11 12 13 14 15 15 0 0 1 0 0 0 0 0 0 0 '
|
||||
Connect Timeout=60
|
||||
Disable Secure Connection Warning=Yes
|
||||
Profile File Version=200
|
||||
Cryptographic Mode=1
|
||||
Display RowCol Indicator=Off
|
||||
VT Scrollback Save Attribs=Off
|
||||
VT True Display Mode=1
|
||||
Status Line=1
|
||||
Kerberos Use Hummingbird Kerberos=On
|
||||
Keyboard=Office
|
||||
Auto Start Quick-Key=shopfloor.ebs
|
||||
On Disconnect=0
|
||||
Allow sleep while connected=0
|
||||
Toolbar Scheme Name=Default VT
|
||||
Save Profile on Window Close=Off
|
||||
Toolbar Scheme Modified=Default VT:30026745--1815298816
|
||||
Save Toolbar Info on Exit=Off
|
||||
Save Mode=0
|
||||
[Session Options]
|
||||
Last Selected Page=4000
|
||||
[HEBar-59440]
|
||||
[Toolbars Layout-Summary]
|
||||
Bars=3
|
||||
ScreenCX=1600
|
||||
ScreenCY=1200
|
||||
[Toolbars]
|
||||
ToolBarVersion=5
|
||||
bar0=Default VT ToolBar.tbv
|
||||
[Toolbars Layout-Bar0]
|
||||
BarID=59393
|
||||
Visible=0
|
||||
[Toolbars Layout-Bar1]
|
||||
BarID=59419
|
||||
Bars=3
|
||||
Bar#0=0
|
||||
Bar#1=59440
|
||||
Bar#2=0
|
||||
[Toolbars Layout-Bar2]
|
||||
BarID=59440
|
||||
XPos=-2
|
||||
YPos=-2
|
||||
Docking=1
|
||||
MRUDockID=59419
|
||||
MRUDockLeftPos=-2
|
||||
MRUDockTopPos=-2
|
||||
MRUDockRightPos=541
|
||||
MRUDockBottomPos=26
|
||||
MRUFloatStyle=8196
|
||||
MRUFloatXPos=-2147483648
|
||||
MRUFloatYPos=0
|
||||
[Hotspots]
|
||||
ShowTips=1
|
||||
[ShortcutReplacers]
|
||||
Size=0
|
||||
[Site Info]
|
||||
EnableCache=Yes
|
||||
RefreshCacheOnConnect=0
|
||||
AlwaysUploadUsing=0
|
||||
AlwaysDownloadUsing=0
|
||||
ProfileVersion=5
|
||||
SecurityType=0
|
||||
Port=21
|
||||
SSL Implicit Port=990
|
||||
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
SFTPProxyAddress=localhost
|
||||
SFTPProxyPort=0
|
||||
SFTPWriteSize=256
|
||||
GssApiServiceName=host
|
||||
GssApiProtLevel=P
|
||||
GssApiIssueCCC=No
|
||||
GssApiPBSZ=16000
|
||||
GssApiKerbClient=0
|
||||
GssApiUseHMS2MIT=No
|
||||
PassiveMode=2
|
||||
AllowServer2ServerTransfer=0
|
||||
KeepAlive=No
|
||||
AutoDetectServerType=Yes
|
||||
KeepAliveTime=0
|
||||
FirewallPort=21
|
||||
FirewallType=0
|
||||
NetworkTimeout=120
|
||||
ConvertGMTTime=0
|
||||
ResolveLinks=1
|
||||
RootToInitialDir=0
|
||||
ChangeDirChangesRoot=0
|
||||
DropVmsVersion=0
|
||||
LocalHourOffset=0
|
||||
LocalMinuteOffset=0
|
||||
TemporaryProfile=0
|
||||
SSLVersion=3
|
||||
SSLReuse=0
|
||||
SSLUserCertificateMode=0
|
||||
SSLCloseOnNegotiateionFail=0
|
||||
SSLAcceptServerSelfSignedCertificates=Yes
|
||||
SSLAcceptServerUnverifiedCertificates=Yes
|
||||
SSLDataChannelProtected=Yes
|
||||
SSLCryptographicMode=1
|
||||
AllowResume=No
|
||||
EPSV4=0
|
||||
[Stats]
|
||||
NumVisits=0
|
||||
@@ -1,122 +0,0 @@
|
||||
[PROFILE]
|
||||
Session Terminal Type=2
|
||||
Session Properties=Default VT
|
||||
Menu=Default VT
|
||||
Auto Run Macro Delay Time=1
|
||||
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||
Session Keep Alive Interval=600
|
||||
PC Keyboard Type=5
|
||||
Tabstops='8 16 24 32 40 48 56 64 72 '
|
||||
VT BG Colors='1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 8 7 7 9 15 14 10 2 10 '
|
||||
Default VT Recv Path=$MYDOCUMENTS$\
|
||||
SSL/TLS Version=3
|
||||
SSL/TLS Current User Certificate=1
|
||||
Window Title=%s - %p (%h)
|
||||
WinInfo Default Restored=0 0 0 0 22 22 892 619 0 0 0=20 10 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
WinInfo Default Maximized=0 0 0 0 -8 -8 1634 1066 0 0 0=30 15 0 0 700 0 0 0 1 3 2 2 0 HE_Bitmap| 0 0
|
||||
Print Header2=Page: %# Document Name: %F
|
||||
QuickKey=Super Key F6
|
||||
Never Run Profile=Off
|
||||
dwVTFlags1=29369344
|
||||
VT FG Colors='7 7 9 15 14 10 2 10 0 1 2 3 4 5 15 7 8 9 10 11 12 13 14 15 15 0 0 1 0 0 0 0 0 0 0 '
|
||||
Connect Timeout=60
|
||||
Disable Secure Connection Warning=Yes
|
||||
Profile File Version=200
|
||||
Cryptographic Mode=1
|
||||
Display RowCol Indicator=Off
|
||||
VT Scrollback Save Attribs=Off
|
||||
VT True Display Mode=1
|
||||
Status Line=1
|
||||
Kerberos Use Hummingbird Kerberos=On
|
||||
Keyboard=Office
|
||||
Auto Start Quick-Key=Office.ebs
|
||||
On Disconnect=0
|
||||
Allow sleep while connected=0
|
||||
Toolbar Scheme Name=Default VT
|
||||
Save Profile on Window Close=Off
|
||||
Toolbar Scheme Modified=Default VT:30026745--1815298816
|
||||
Save Toolbar Info on Exit=Off
|
||||
Save Mode=0
|
||||
[Session Options]
|
||||
Last Selected Page=4000
|
||||
[HEBar-59440]
|
||||
[Toolbars Layout-Summary]
|
||||
Bars=3
|
||||
ScreenCX=1600
|
||||
ScreenCY=1200
|
||||
[Toolbars]
|
||||
ToolBarVersion=5
|
||||
bar0=Default VT ToolBar.tbv
|
||||
[Toolbars Layout-Bar0]
|
||||
BarID=59393
|
||||
Visible=0
|
||||
[Toolbars Layout-Bar1]
|
||||
BarID=59419
|
||||
Bars=3
|
||||
Bar#0=0
|
||||
Bar#1=59440
|
||||
Bar#2=0
|
||||
[Toolbars Layout-Bar2]
|
||||
BarID=59440
|
||||
XPos=-2
|
||||
YPos=-2
|
||||
Docking=1
|
||||
MRUDockID=59419
|
||||
MRUDockLeftPos=-2
|
||||
MRUDockTopPos=-2
|
||||
MRUDockRightPos=541
|
||||
MRUDockBottomPos=26
|
||||
MRUFloatStyle=8196
|
||||
MRUFloatXPos=-2147483648
|
||||
MRUFloatYPos=0
|
||||
[Hotspots]
|
||||
ShowTips=1
|
||||
[ShortcutReplacers]
|
||||
Size=0
|
||||
[Site Info]
|
||||
EnableCache=Yes
|
||||
RefreshCacheOnConnect=0
|
||||
AlwaysUploadUsing=0
|
||||
AlwaysDownloadUsing=0
|
||||
ProfileVersion=5
|
||||
SecurityType=0
|
||||
Port=21
|
||||
SSL Implicit Port=990
|
||||
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
SFTPProxyAddress=localhost
|
||||
SFTPProxyPort=0
|
||||
SFTPWriteSize=256
|
||||
GssApiServiceName=host
|
||||
GssApiProtLevel=P
|
||||
GssApiIssueCCC=No
|
||||
GssApiPBSZ=16000
|
||||
GssApiKerbClient=0
|
||||
GssApiUseHMS2MIT=No
|
||||
PassiveMode=2
|
||||
AllowServer2ServerTransfer=0
|
||||
KeepAlive=No
|
||||
AutoDetectServerType=Yes
|
||||
KeepAliveTime=0
|
||||
FirewallPort=21
|
||||
FirewallType=0
|
||||
NetworkTimeout=120
|
||||
ConvertGMTTime=0
|
||||
ResolveLinks=1
|
||||
RootToInitialDir=0
|
||||
ChangeDirChangesRoot=0
|
||||
DropVmsVersion=0
|
||||
LocalHourOffset=0
|
||||
LocalMinuteOffset=0
|
||||
TemporaryProfile=0
|
||||
SSLVersion=3
|
||||
SSLReuse=0
|
||||
SSLUserCertificateMode=0
|
||||
SSLCloseOnNegotiateionFail=0
|
||||
SSLAcceptServerSelfSignedCertificates=Yes
|
||||
SSLAcceptServerUnverifiedCertificates=Yes
|
||||
SSLDataChannelProtected=Yes
|
||||
SSLCryptographicMode=1
|
||||
AllowResume=No
|
||||
EPSV4=0
|
||||
[Stats]
|
||||
NumVisits=0
|
||||
@@ -1,128 +0,0 @@
|
||||
[PROFILE]
|
||||
Session Terminal Type=2
|
||||
Never Run Profile=Off
|
||||
Session Properties=Default VT
|
||||
Menu=Default VT
|
||||
Auto Run Macro Delay Time=1
|
||||
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||
Session Keep Alive Interval=600
|
||||
dwVTFlags1=29369344
|
||||
PC Keyboard Type=5
|
||||
Keyboard=ATM-VT420
|
||||
Tabstops='8 16 24 32 40 48 56 64 72 80 88 96 104 112 120 128 136 144 152 160 168 176 184 192 200 208 216 224 232 240 248 256 264 272 280 288 296 '
|
||||
Default VT Recv Path=$MYDOCUMENTS$\
|
||||
SSL/TLS Version=3
|
||||
SSL/TLS Current User Certificate=1
|
||||
Window Title=%s - %p (%h)
|
||||
WinInfo Default Restored=0 0 0 0 10 10 757 491 0 0 0=16 9 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
WinInfo Default Maximized=0 0 0 0 -8 -8 1382 744 0 0 0=26 13 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
Window State=2
|
||||
Use Specific PRTSCR Printer=On
|
||||
PrtScr DevMode=1025,1024,156,1497,33565,1,256,2794,1240,0,1,4,300,1,1,300,1,0,0,1,0,0,0,0,ZDesigner TLP 3842,User defined
|
||||
PrtScr Driver=winspool
|
||||
PrtScr Device=ZDesigner TLP 3842
|
||||
PrtScr Output=LPT1:
|
||||
Print Header2=Page: %# Document Name: %F
|
||||
Use Specific VT Printer=On
|
||||
VT DevMode=1025,1024,156,1497,323347,1,256,190,914,0,1,4,300,1,1,300,1,0,0,1,0,0,0,0,ZDesigner TLP 3842,User defined
|
||||
VT Driver=winspool
|
||||
VT Device=ZDesigner TLP 3842
|
||||
VT Output=USB001
|
||||
Printer Target=1
|
||||
VT Printer Bypass Windows Print Driver=Off
|
||||
VT Printer Use Default Font=On
|
||||
VT Printer Margins Left=320
|
||||
VT Printer Margins Right=0
|
||||
VT Printer Margins Bottom=0
|
||||
VT Printer Margins Top=0
|
||||
QuickKey=ID
|
||||
Auto Start Quick-Key=mmcs.ebs
|
||||
VT Auto Wrap=On
|
||||
Profile File Version=200
|
||||
Kerberos Use Hummingbird Kerberos=On
|
||||
Cryptographic Mode=1
|
||||
Save Mode=0
|
||||
[HEBar-59440]
|
||||
sizeFloatCX=534
|
||||
sizeFloatCY=28
|
||||
[Toolbars Layout-Summary]
|
||||
Bars=3
|
||||
ScreenCX=1366
|
||||
ScreenCY=768
|
||||
[Session Options]
|
||||
Last Selected Page=4000
|
||||
[Toolbars]
|
||||
ToolBarVersion=5
|
||||
bar0=Default VT ToolBar.tbv
|
||||
[Toolbars Layout-Bar0]
|
||||
BarID=59393
|
||||
[Toolbars Layout-Bar1]
|
||||
BarID=59419
|
||||
Bars=3
|
||||
Bar#0=0
|
||||
Bar#1=59440
|
||||
Bar#2=0
|
||||
[Toolbars Layout-Bar2]
|
||||
BarID=59440
|
||||
XPos=-2
|
||||
YPos=-2
|
||||
Docking=1
|
||||
MRUDockID=59419
|
||||
MRUDockLeftPos=-2
|
||||
MRUDockTopPos=-2
|
||||
MRUDockRightPos=541
|
||||
MRUDockBottomPos=26
|
||||
MRUFloatStyle=8196
|
||||
MRUFloatXPos=-1
|
||||
MRUFloatYPos=0
|
||||
[Hotspots]
|
||||
ShowTips=1
|
||||
[ShortcutReplacers]
|
||||
Size=0
|
||||
[Site Info]
|
||||
EnableCache=Yes
|
||||
RefreshCacheOnConnect=0
|
||||
AlwaysUploadUsing=0
|
||||
AlwaysDownloadUsing=0
|
||||
ProfileVersion=5
|
||||
SecurityType=0
|
||||
Port=21
|
||||
SSL Implicit Port=990
|
||||
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
SFTPProxyAddress=localhost
|
||||
SFTPProxyPort=0
|
||||
SFTPWriteSize=256
|
||||
GssApiServiceName=host
|
||||
GssApiProtLevel=P
|
||||
GssApiIssueCCC=No
|
||||
GssApiPBSZ=16000
|
||||
GssApiKerbClient=0
|
||||
GssApiUseHMS2MIT=No
|
||||
PassiveMode=2
|
||||
AllowServer2ServerTransfer=0
|
||||
KeepAlive=No
|
||||
AutoDetectServerType=Yes
|
||||
KeepAliveTime=0
|
||||
FirewallPort=21
|
||||
FirewallType=0
|
||||
NetworkTimeout=120
|
||||
ConvertGMTTime=0
|
||||
ResolveLinks=1
|
||||
RootToInitialDir=0
|
||||
ChangeDirChangesRoot=0
|
||||
DropVmsVersion=0
|
||||
LocalHourOffset=0
|
||||
LocalMinuteOffset=0
|
||||
TemporaryProfile=0
|
||||
SSLVersion=1
|
||||
SSLReuse=0
|
||||
SSLUserCertificateMode=0
|
||||
SSLCloseOnNegotiateionFail=0
|
||||
SSLAcceptServerSelfSignedCertificates=Yes
|
||||
SSLAcceptServerUnverifiedCertificates=Yes
|
||||
SSLDataChannelProtected=Yes
|
||||
SSLCryptographicMode=1
|
||||
AllowResume=No
|
||||
EPSV4=0
|
||||
[Stats]
|
||||
NumVisits=0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,144 +0,0 @@
|
||||
# Get-PCProfile.ps1 - Shared helper for resolving the active PC profile
|
||||
# from site-config.json. Dot-source this from any script that needs
|
||||
# profile-aware configuration:
|
||||
#
|
||||
# . "$PSScriptRoot\lib\Get-PCProfile.ps1" (from Shopfloor\ scripts)
|
||||
# . "$PSScriptRoot\Get-PCProfile.ps1" (from lib\ scripts)
|
||||
#
|
||||
# After dot-sourcing, these variables are available:
|
||||
# $siteConfig - full parsed site-config.json (or $null)
|
||||
# $pcType - from pc-type.txt (e.g. "Standard", "CMM", "Display")
|
||||
# $pcSubtype - from pc-subtype.txt (e.g. "Timeclock", "Machine", "Lobby")
|
||||
# $profileKey - lookup key (e.g. "Standard-Machine", "CMM", "Display-Lobby")
|
||||
# $pcProfile - the matched profile object from pcProfiles, or $null
|
||||
#
|
||||
# Resolution order for any config key (e.g. startupItems):
|
||||
# 1. $pcProfile.startupItems (profile-specific override)
|
||||
# 2. $siteConfig.startupItems (site-wide default)
|
||||
# 3. (hardcoded fallback in the calling script)
|
||||
#
|
||||
# Usage pattern in consuming scripts:
|
||||
# $items = Get-ProfileValue 'startupItems'
|
||||
# if ($null -ne $items) { ... } else { <hardcoded fallback> }
|
||||
#
|
||||
# Get-ProfileValue distinguishes "not set" ($null) from "explicitly empty"
|
||||
# (@()) so a profile can opt out of a site-wide default by setting the
|
||||
# key to []. Do NOT use truthiness checks like `if ($pcProfile.startupItems)`
|
||||
# because an empty array is falsy and would fall through to site defaults.
|
||||
|
||||
function Get-ProfileValue {
|
||||
param([Parameter(Mandatory)][string]$Key)
|
||||
if ($pcProfile -and ($pcProfile.PSObject.Properties.Name -contains $Key)) {
|
||||
return ,@($pcProfile.$Key)
|
||||
}
|
||||
if ($siteConfig -and ($siteConfig.PSObject.Properties.Name -contains $Key)) {
|
||||
return ,@($siteConfig.$Key)
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-SiteConfig {
|
||||
$configPath = 'C:\Enrollment\site-config.json'
|
||||
if (-not (Test-Path -LiteralPath $configPath)) {
|
||||
Write-Host "site-config.json not found - using defaults" -ForegroundColor DarkGray
|
||||
return $null
|
||||
}
|
||||
try {
|
||||
return (Get-Content -LiteralPath $configPath -Raw -ErrorAction Stop | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Warning "Failed to parse site-config.json: $_"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$siteConfig = Get-SiteConfig
|
||||
|
||||
# Read PC type + sub-type from the files startnet.cmd wrote during imaging
|
||||
$pcType = ''
|
||||
$pcSubtype = ''
|
||||
$typeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$subtypeFile = 'C:\Enrollment\pc-subtype.txt'
|
||||
|
||||
if (Test-Path -LiteralPath $typeFile) {
|
||||
$pcType = (Get-Content -LiteralPath $typeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
if (Test-Path -LiteralPath $subtypeFile) {
|
||||
$pcSubtype = (Get-Content -LiteralPath $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
|
||||
# Display sub-type fallback: if pc-subtype.txt is absent (post-rename-reorg
|
||||
# default) but display-type.txt exists, use it as the subtype. Lets the
|
||||
# Display-Lobby / Display-Dashboard / gea-shopfloor-display-{lobby,dashboard}
|
||||
# profile keys resolve correctly for Display PCs.
|
||||
$displayTypeFile = 'C:\Enrollment\display-type.txt'
|
||||
if (-not $pcSubtype -and ($pcType -ieq 'gea-shopfloor-display' -or $pcType -ieq 'Display') -and (Test-Path -LiteralPath $displayTypeFile)) {
|
||||
$pcSubtype = (Get-Content -LiteralPath $displayTypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
|
||||
# Build the profile key: "Standard-Machine", "CMM", "Display-Lobby", etc.
|
||||
$profileKey = if ($pcSubtype) { "$pcType-$pcSubtype" } else { $pcType }
|
||||
|
||||
# 2026-05-03 rename: when the legacy key (e.g. "Standard-Machine") is not
|
||||
# present in pcProfiles, fall through to the new gea-shopfloor-* equivalent
|
||||
# (e.g. "gea-shopfloor-collections"). Vice versa: a fleet PC that already
|
||||
# writes "gea-shopfloor-collections" finds its profile under either key.
|
||||
$pcProfileAliasGroups = @(
|
||||
@('Standard-Machine', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections'),
|
||||
@('Standard-Timeclock', 'gea-shopfloor-common'),
|
||||
@('Lab', 'gea-shopfloor-common'),
|
||||
@('CMM', 'gea-shopfloor-cmm'),
|
||||
@('Keyence', 'gea-shopfloor-keyence'),
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Display-Lobby', 'gea-shopfloor-display-Lobby', 'gea-shopfloor-display-lobby'),
|
||||
@('Display-Dashboard', 'gea-shopfloor-display-Dashboard', 'gea-shopfloor-display-dashboard'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat')
|
||||
)
|
||||
|
||||
# Look up the profile in pcProfiles. Fall back to $null (callers use
|
||||
# site-wide defaults or hardcoded values).
|
||||
$pcProfile = $null
|
||||
if ($siteConfig -and $siteConfig.pcProfiles -and $profileKey) {
|
||||
$pcProfile = $siteConfig.pcProfiles.$profileKey
|
||||
if (-not $pcProfile) {
|
||||
foreach ($g in $pcProfileAliasGroups) {
|
||||
if ($g -icontains $profileKey) {
|
||||
foreach ($alias in $g) {
|
||||
if ($alias -ieq $profileKey) { continue }
|
||||
$candidate = $siteConfig.pcProfiles.$alias
|
||||
if ($candidate) { $pcProfile = $candidate; break }
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
# Subtype-strip fallback: a compound key like "gea-shopfloor-cmm-doda" (DODA
|
||||
# CMM bays set pc-subtype.txt=doda) has no profile of its own. Fall back to
|
||||
# the BARE pc-type ("gea-shopfloor-cmm" -> "CMM"). Without this the profile
|
||||
# resolves to $null and callers silently use hardcoded defaults (wrong
|
||||
# desktop icons + unwanted startup items).
|
||||
if (-not $pcProfile -and $pcSubtype) {
|
||||
$bare = $pcType
|
||||
$pcProfile = $siteConfig.pcProfiles.$bare
|
||||
if (-not $pcProfile) {
|
||||
foreach ($g in $pcProfileAliasGroups) {
|
||||
if ($g -icontains $bare) {
|
||||
foreach ($alias in $g) {
|
||||
if ($alias -ieq $bare) { continue }
|
||||
$candidate = $siteConfig.pcProfiles.$alias
|
||||
if ($candidate) { $pcProfile = $candidate; break }
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($pcProfile) { $profileKey = $bare }
|
||||
}
|
||||
}
|
||||
|
||||
if ($pcProfile) {
|
||||
Write-Host "PC profile: $profileKey" -ForegroundColor Cyan
|
||||
} elseif ($profileKey) {
|
||||
Write-Host "PC profile '$profileKey' not found in site-config.json - using site defaults" -ForegroundColor DarkGray
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
# Invoke-FilteredReportIP.ps1
|
||||
# Reports this bay's GE corp IP to the GE Tines webhook with a tight
|
||||
# subnet filter so PXE LAN (10.9.100.x) or any other 10.x stray address
|
||||
# never leaks. Mirrors the payload shape of GE's Intune-deployed
|
||||
# ReportIPAddresses_v2.ps1 so the receiving Tines workflow accepts it.
|
||||
#
|
||||
# WHY:
|
||||
# GE's signed Report IP script filters Get-NetIPAddress with
|
||||
# $_.StartsWith("10.") - too broad for WJ where PXE LAN is also 10.x.
|
||||
# Calling our own POST with a narrow filter guarantees the webhook
|
||||
# only ever sees the bay's AESFMA corp 10.x address.
|
||||
#
|
||||
# ALLOWED RANGES (WJ-specific - update if site moves to a new VLAN):
|
||||
# 10.134.48.0/23 (10.134.48.0 - 10.134.49.255)
|
||||
# 10.48.249.0/26 (10.48.249.0 - 10.48.249.63)
|
||||
|
||||
param(
|
||||
[switch]$Force,
|
||||
[int]$MaxAttempts = 6,
|
||||
[int]$RetrySeconds = 10
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$logFile = 'C:\Logs\FilteredReportIP.log'
|
||||
New-Item -ItemType Directory -Path 'C:\Logs' -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
function Log([string]$msg) {
|
||||
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
"$ts $msg" | Tee-Object -FilePath $logFile -Append
|
||||
}
|
||||
|
||||
# Allowed ranges (CIDR -> first IP int + prefix bits)
|
||||
$allowedRanges = @(
|
||||
@{ Network = '10.134.48.0'; PrefixLen = 23 },
|
||||
@{ Network = '10.48.249.0'; PrefixLen = 26 }
|
||||
)
|
||||
|
||||
function ConvertTo-Uint32($ip) {
|
||||
$bytes = ([System.Net.IPAddress]::Parse($ip)).GetAddressBytes()
|
||||
[Array]::Reverse($bytes)
|
||||
return [BitConverter]::ToUInt32($bytes, 0)
|
||||
}
|
||||
|
||||
function Test-InAllowedRange([string]$ip) {
|
||||
try {
|
||||
$ipInt = ConvertTo-Uint32 $ip
|
||||
foreach ($r in $allowedRanges) {
|
||||
$netInt = ConvertTo-Uint32 $r.Network
|
||||
$mask = [uint32]([math]::Pow(2, 32) - [math]::Pow(2, 32 - $r.PrefixLen))
|
||||
if (($ipInt -band $mask) -eq ($netInt -band $mask)) { return $true }
|
||||
}
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
|
||||
Log "=== Filtered Report IP fire ==="
|
||||
$allIPs = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.IPAddress -notmatch '^169\.254' -and $_.IPAddress -ne '127.0.0.1' } |
|
||||
Select-Object -ExpandProperty IPAddress
|
||||
Log "All IPv4 addresses on this bay: $($allIPs -join ', ')"
|
||||
|
||||
$match = @($allIPs | Where-Object { Test-InAllowedRange $_ })
|
||||
if ($match.Count -eq 0) {
|
||||
Log "No IP in allowed ranges (10.134.48.0/23 or 10.48.249.0/26). Not POSTing."
|
||||
exit 2
|
||||
}
|
||||
$reportIP = $match[0]
|
||||
Log "Reporting IP: $reportIP"
|
||||
|
||||
$statusFile = "$env:ProgramData\GEA\FilteredReportIP\last-ip.txt"
|
||||
$lastIP = ''
|
||||
if (Test-Path $statusFile) { $lastIP = (Get-Content $statusFile -ErrorAction SilentlyContinue) -join '' }
|
||||
if (-not $Force -and $lastIP -eq $reportIP) {
|
||||
Log "IP unchanged from last POST ($lastIP). Skipping."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$webhookUrl = 'https://tines.apps.geaerospace.com/webhook/aa891efac08bab3d077d9956ab6614d0/c687048cb0ee35c33760ad14cb9257eb'
|
||||
$body = @{
|
||||
host = $env:COMPUTERNAME
|
||||
fqdn = "$env:COMPUTERNAME.device.geaerospace.net"
|
||||
IP = $reportIP
|
||||
force_update = if ($Force) { 'True' } else { 'False' }
|
||||
}
|
||||
|
||||
for ($i = 1; $i -le $MaxAttempts; $i++) {
|
||||
try {
|
||||
Log "POST attempt $i/$MaxAttempts ..."
|
||||
$resp = Invoke-WebRequest -Uri $webhookUrl -Method POST -Body $body -TimeoutSec 60 -UseBasicParsing -ErrorAction Stop
|
||||
Log "POST succeeded. StatusCode=$($resp.StatusCode) Content=$($resp.Content)"
|
||||
New-Item -ItemType Directory -Path (Split-Path $statusFile) -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
Set-Content -Path $statusFile -Value $reportIP -Force
|
||||
exit 0
|
||||
} catch {
|
||||
Log "POST attempt $i failed: $_"
|
||||
if ($i -lt $MaxAttempts) { Start-Sleep -Seconds $RetrySeconds }
|
||||
}
|
||||
}
|
||||
Log "All $MaxAttempts attempts failed - giving up."
|
||||
exit 1
|
||||
@@ -1,68 +0,0 @@
|
||||
# Map-SfldShare.ps1 - Map S: drive on user logon using SFLD creds from HKLM.
|
||||
#
|
||||
# Runs as the interactive user (BUILTIN\Users, Limited) so the drive
|
||||
# mapping lands in the logged-in user's session. Reads username/password
|
||||
# directly from HKLM:\SOFTWARE\GE\SFLD\Credentials\* and passes them
|
||||
# inline to net use -- no Windows Credential Manager involvement.
|
||||
#
|
||||
# Why not the vendor's ConsumeCredentials.ps1: it calls
|
||||
# New-StoredCredential -Persist LocalMachine which requires admin.
|
||||
# ShopFloor is a non-admin user, so the cred-store step fails silently
|
||||
# and the subsequent net use (which relies on those stored creds) has
|
||||
# no authentication. Direct net use /user: bypasses the issue entirely.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$logFile = Join-Path $logDir 'map-share.log'
|
||||
|
||||
function Write-MapLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $env:USERNAME, $Message
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-MapLog '=== Map-SfldShare start ==='
|
||||
|
||||
$credsBase = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
|
||||
if (-not (Test-Path $credsBase)) {
|
||||
Write-MapLog 'No HKLM SFLD credentials yet - exiting'
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($entry in (Get-ChildItem -Path $credsBase -ErrorAction SilentlyContinue)) {
|
||||
$p = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
|
||||
if (-not $p -or -not $p.TargetHost -or -not $p.Username -or -not $p.Password) { continue }
|
||||
|
||||
$drive = $null
|
||||
$share = $null
|
||||
try { $drive = $p.DriveLetter } catch {}
|
||||
try { $share = $p.ShareName } catch {}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($drive) -or [string]::IsNullOrWhiteSpace($share)) { continue }
|
||||
|
||||
$drive = $drive.TrimEnd(':') + ':'
|
||||
$share = $share.TrimStart('\')
|
||||
$uncPath = "\\$($p.TargetHost)\$share"
|
||||
|
||||
# Skip if already mapped to the right target
|
||||
$existing = & net use $drive 2>&1
|
||||
if ($LASTEXITCODE -eq 0 -and ($existing -join "`n") -match [regex]::Escape($uncPath)) {
|
||||
Write-MapLog "$drive already mapped to $uncPath - skipping"
|
||||
continue
|
||||
}
|
||||
|
||||
& net use $drive /delete /y 2>$null | Out-Null
|
||||
$out = & net use $drive $uncPath /user:$($p.Username) $($p.Password) /persistent:yes 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-MapLog "Mapped $drive -> $uncPath"
|
||||
} else {
|
||||
Write-MapLog "FAILED $drive -> $uncPath (exit $LASTEXITCODE): $out"
|
||||
}
|
||||
}
|
||||
|
||||
Write-MapLog '=== Map-SfldShare end ==='
|
||||
exit 0
|
||||
@@ -251,14 +251,14 @@ function Get-Phase1 {
|
||||
# dashboard card flips to 100% green.
|
||||
|
||||
# Lockdown handover also ends the imaging autologon budget.
|
||||
# Run-ShopfloorSetup and Stage-Dispatcher top AutoLogonCount up to 10 for
|
||||
# SupportUser, and the Sysinternals Autologon run that switches the bay to
|
||||
# ShopFloor leaves it in place. ShopFloor then inherited the remainder and
|
||||
# Windows turned autologon off when it reached 0 - the bay parked at a
|
||||
# ShopFloor password prompt a few reboots after imaging. Delete it as soon
|
||||
# as Winlogon shows ShopFloor. Proven in the win11 VM 2026-10-02. GE-Enforce
|
||||
# common carries the same cleanup (Remove-AutoLogonCount.ps1) for bays
|
||||
# imaged before this.
|
||||
# Run-ShopfloorSetup tops AutoLogonCount up to 10 for SupportUser, and the
|
||||
# Sysinternals Autologon run that switches the bay to ShopFloor leaves it
|
||||
# in place. ShopFloor then inherited the remainder and Windows turned
|
||||
# autologon off when it reached 0 - the bay parked at a ShopFloor password
|
||||
# prompt a few reboots after imaging. Delete it as soon as Winlogon shows
|
||||
# ShopFloor. This task runs as Users (Limited) and the HKLM write can be
|
||||
# denied; the SYSTEM "GE Shopfloor Post-PPKG" task (Invoke-PostPpkg.ps1)
|
||||
# does the same cleanup at every startup until it has succeeded.
|
||||
try {
|
||||
$wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
||||
$wlNow = Get-ItemProperty $wlKey -ErrorAction Stop
|
||||
@@ -947,12 +947,9 @@ function Format-Snapshot {
|
||||
@{ Ok = $Snap.Phase3.InstallComplete; Failed = $false }
|
||||
)
|
||||
# Phase 4 (per-script Application Install) was retired 2026-04-28:
|
||||
# apps now deploy via the GE-Enforce manifest engine off
|
||||
# \\tsgwp00525\...\machineapps\, gated on AESFMA WiFi + S: mount
|
||||
# which both happen post-reboot. The monitor's responsibility ends
|
||||
# at "DSC bootstrap + creds + lockdown landed, time to reboot."
|
||||
# The manifest engine takes over from there on the next ShopFloor
|
||||
# logon.
|
||||
# site apps install from the post-PPKG list after the reboot
|
||||
# (Invoke-PostPpkg.ps1). The monitor's responsibility ends at
|
||||
# "DSC bootstrap + creds + lockdown landed, time to reboot."
|
||||
|
||||
$p4Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
|
||||
$p4Status = Get-PhaseStatus @(
|
||||
@@ -1250,7 +1247,7 @@ if ($AsTask -and (Test-Path -LiteralPath $syncCompleteMarker)) {
|
||||
# For these types, enrollment is "done" once Phase 1 (Identity) completes
|
||||
# and policies start arriving - there is no Phase 2-5 to wait for.
|
||||
$pcTypeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$noDscTypes = @('Display', 'gea-shopfloor-display')
|
||||
$noDscTypes = @()
|
||||
$skipDsc = $false
|
||||
if (Test-Path $pcTypeFile) {
|
||||
$pcType = (Get-Content $pcTypeFile -First 1).Trim()
|
||||
@@ -1308,29 +1305,14 @@ try {
|
||||
$qrRefreshed = [bool]($qrText -notmatch 'not yet Azure AD joined')
|
||||
}
|
||||
|
||||
# Filtered Report IP shim - POSTs this bay's GE corp IP to the
|
||||
# Tines webhook directly with a tight subnet filter. Fires each
|
||||
# tick until it succeeds once (script's own status-file dedup
|
||||
# prevents duplicate POSTs). Bypasses the timing window where
|
||||
# GE's Intune-deployed Report IP misses (it may run before the
|
||||
# SCEP cert + AESFMA join land).
|
||||
if (-not $filteredReportIpSucceeded) {
|
||||
$filteredScript = Join-Path $PSScriptRoot 'Invoke-FilteredReportIP.ps1'
|
||||
if (Test-Path $filteredScript) {
|
||||
$rc = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $filteredScript 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { $filteredReportIpSucceeded = $true }
|
||||
}
|
||||
}
|
||||
|
||||
Clear-Host
|
||||
Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger
|
||||
Write-Host ""
|
||||
Write-Host $qrText
|
||||
|
||||
# Final state: every phase landed. Apps are no longer in the gate -
|
||||
# they deploy via the GE-Enforce manifest engine post-reboot, gated
|
||||
# on AESFMA WiFi + S: drive mount which only happen after we hand
|
||||
# off to the ShopFloor autologon session. So this gate is now:
|
||||
# the site's post-PPKG list installs them after the reboot. So this
|
||||
# gate is now:
|
||||
# - CredsPopulated: SFLD share creds in HKLM (Machine-Enforce,
|
||||
# Acrobat-Enforce, CMM-Enforce, manifest
|
||||
# engine all need these)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Restore-EDncReg.ps1 - Import a per-machine eDNC .reg backup from a folder.
|
||||
#
|
||||
# Two callers:
|
||||
# 1. Standard/03-RestoreEDncConfig.ps1 (imaging-time, source = local PXE copy)
|
||||
# 2. Shopfloor/lib/Update-MachineNumber.ps1 (post-image 9999 -> real, source
|
||||
# = tsgwp00525 SFLD share mounted via existing SFLD credential pattern)
|
||||
#
|
||||
# The .reg files live as <num>-<host>.reg or <num>.reg. Returns the path
|
||||
# that was imported, or $null if nothing was found/imported. Errors are
|
||||
# caught and logged via Write-Host (no throwing); callers continue.
|
||||
#
|
||||
# Dot-source with:
|
||||
# . "$PSScriptRoot\lib\Restore-EDncReg.ps1" (from Shopfloor\)
|
||||
# . "$PSScriptRoot\..\Shopfloor\lib\Restore-EDncReg.ps1" (from Standard\)
|
||||
|
||||
function Mount-SFLDShare {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Mounts a \\server\share path using SFLD creds from
|
||||
HKLM:\SOFTWARE\GE\SFLD\Credentials\*. Returns $true on success,
|
||||
$false if creds are missing or mount fails.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SharePath,
|
||||
[string]$DriveLetter = 'V:'
|
||||
)
|
||||
|
||||
$server = ($SharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1
|
||||
$basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
|
||||
if (-not (Test-Path $basePath)) { return $false }
|
||||
|
||||
$cred = $null
|
||||
foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) {
|
||||
$props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
|
||||
if (-not $props -or -not $props.TargetHost) { continue }
|
||||
if ($props.TargetHost -eq $server -or
|
||||
$props.TargetHost -like "$server.*" -or
|
||||
$server -like "$($props.TargetHost).*") {
|
||||
$cred = $props; break
|
||||
}
|
||||
}
|
||||
if (-not $cred -or -not $cred.Username -or -not $cred.Password) { return $false }
|
||||
|
||||
& net use $DriveLetter /delete /y 2>$null | Out-Null
|
||||
$null = & net use $DriveLetter $SharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
}
|
||||
|
||||
function Import-EDncRegBackup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Looks for <MachineNumber>-*.reg or <MachineNumber>.reg in SourceRoot
|
||||
and runs `reg import` on it. Caller is responsible for mounting any
|
||||
network share first; this function works on whatever local or drive-
|
||||
letter path is handed in.
|
||||
.PARAMETER SourceRoot
|
||||
Directory containing the .reg backups.
|
||||
.PARAMETER MachineNumber
|
||||
The target machine number (digits). Used as a filename prefix.
|
||||
.OUTPUTS
|
||||
Path of the imported file, or $null if none found / import failed.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SourceRoot,
|
||||
[Parameter(Mandatory)][string]$MachineNumber
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $SourceRoot)) {
|
||||
Write-Host " Restore-EDncReg: source root not found: $SourceRoot"
|
||||
return $null
|
||||
}
|
||||
|
||||
$candidate = Get-ChildItem -Path $SourceRoot -Filter "$MachineNumber.reg" -File -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
if (-not $candidate) {
|
||||
Write-Host " Restore-EDncReg: no backup for machine $MachineNumber in $SourceRoot"
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Host " Restore-EDncReg: importing $($candidate.FullName)"
|
||||
$out = & reg.exe import "$($candidate.FullName)" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " Restore-EDncReg: reg import failed (exit $LASTEXITCODE): $out"
|
||||
return $null
|
||||
}
|
||||
Write-Host " Restore-EDncReg: imported OK"
|
||||
return $candidate.FullName
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
# Set-OpenTextAutoStart.ps1 - place WJ Shopfloor.lnk in the All Users
|
||||
# Startup folder so HostExplorer's "WJ Shopfloor" session launches at
|
||||
# every login. Idempotent: re-running is a no-op when the .lnk already
|
||||
# exists at the same path.
|
||||
#
|
||||
# Used by per-pc-type 09-Setup scripts for shopfloor types whose only
|
||||
# business app is OpenText (common, waxtrace, genspect, heattreat).
|
||||
# collections + nocollections do NOT auto-start OpenText - their techs
|
||||
# pick which apps via Configure-PC.ps1.
|
||||
#
|
||||
# Source .lnk is created by the OpenText preinstall (Setup-OpenText.ps1)
|
||||
# on the public desktop. If the .lnk is missing, log a warning and exit
|
||||
# 0 - imaging chain still continues; auto-start can be re-attempted on a
|
||||
# subsequent login by re-running this script.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
|
||||
$candidates = @(
|
||||
Join-Path $publicDesktop 'WJ Shopfloor.lnk'
|
||||
Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'WJ Shopfloor.lnk'
|
||||
)
|
||||
$src = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
if (-not $src) {
|
||||
Write-Warning "WJ Shopfloor.lnk not found on public desktop - OpenText auto-start NOT configured."
|
||||
Write-Warning " Searched: $($candidates -join ' ; ')"
|
||||
Write-Warning " Setup-OpenText.ps1 should create it during preinstall - check OpenText install state."
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $startupDir)) {
|
||||
New-Item -Path $startupDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$dst = Join-Path $startupDir 'WJ Shopfloor.lnk'
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||
Write-Host "OpenText auto-start enabled: $dst (source: $src)"
|
||||
} catch {
|
||||
Write-Warning "Failed to copy WJ Shopfloor.lnk to startup: $_"
|
||||
}
|
||||
@@ -31,7 +31,7 @@ $ErrorActionPreference = 'Continue'
|
||||
# 1. -QRCoderDllPath if explicitly passed
|
||||
# 2. Next to this script ($PSScriptRoot\QRCoder.dll)
|
||||
# 3. Current working directory
|
||||
# 4. WJ canonical staging path (kept for cross-site portability)
|
||||
# 4. The standard staging path (C:\Enrollment\shopfloor-setup\Shopfloor)
|
||||
# ----------------------------------------------------------------------------
|
||||
function Resolve-QRCoderDll {
|
||||
param([string]$Override)
|
||||
|
||||
@@ -1,531 +0,0 @@
|
||||
# Update-MachineNumber.ps1 - Shared helper for reading and updating the
|
||||
# machine number in UDC and eDNC. Dot-source from any script that needs
|
||||
# machine-number operations:
|
||||
#
|
||||
# . "$PSScriptRoot\lib\Update-MachineNumber.ps1" (from Shopfloor\ scripts)
|
||||
# . "$PSScriptRoot\..\Shopfloor\lib\Update-MachineNumber.ps1" (from Standard\ scripts)
|
||||
# . "$PSScriptRoot\Update-MachineNumber.ps1" (from lib\ scripts)
|
||||
#
|
||||
# Exported functions:
|
||||
# Get-CurrentMachineNumber - returns @{ Udc = $string_or_null; Ednc = $string_or_null }
|
||||
# Update-MachineNumber - updates both, returns @{ UdcUpdated = $bool; EdncUpdated = $bool; Errors = @() }
|
||||
#
|
||||
# Both handle missing files/keys gracefully (app not installed = skip, not error).
|
||||
|
||||
$script:UdcSettingsPath = 'C:\ProgramData\UDC\udc_settings.json'
|
||||
$script:UdcExePath = 'C:\Program Files\UDC\UDC.exe'
|
||||
$script:EdncRegPath = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General'
|
||||
|
||||
function Get-CurrentMachineNumber {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reads the current machine number from UDC settings JSON and eDNC registry.
|
||||
.OUTPUTS
|
||||
Hashtable with keys Udc ($string or $null) and Ednc ($string or $null).
|
||||
#>
|
||||
$result = @{ Udc = $null; Ednc = $null }
|
||||
|
||||
if (Test-Path $script:UdcSettingsPath) {
|
||||
try {
|
||||
$json = Get-Content $script:UdcSettingsPath -Raw | ConvertFrom-Json
|
||||
$result.Udc = $json.GeneralSettings.MachineNumber
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (Test-Path $script:EdncRegPath) {
|
||||
try {
|
||||
$result.Ednc = (Get-ItemProperty -Path $script:EdncRegPath -Name MachineNo -ErrorAction Stop).MachineNo
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Update-MTConnectVariantName {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Renames an MTConnect device across every file of a variant that carries it.
|
||||
|
||||
.DESCRIPTION
|
||||
Makino and Fanuc do not fit the one-file-one-service shape the other
|
||||
variants do.
|
||||
Per the vendor's "Installation Configuration and Trouble Shooting" guide,
|
||||
the device name lives in several files that must be identical or the agent
|
||||
may not start, and the Adapter Manager service rewrites the COMPLETE
|
||||
configuration when it stops - so an edit made while it runs is thrown away.
|
||||
|
||||
Makino, from a real bay capture:
|
||||
Agent\Devices.xml <Device name="7502" uuid="7502" id="MC41">
|
||||
Agent\Devices.EDM.xml <Device name="MAKINO-1" uuid="MAKINO-1" id="EDM41">
|
||||
Data\AdapterDataFile.xml <Name>MAKINO-1</Name>
|
||||
Agent\Agent.cfg Adapters { MAKINO-1 { Host=localhost ... } }
|
||||
|
||||
Fanuc, likewise:
|
||||
Agent\devices.xml <Device id="3101" name="3101" uuid="3101">
|
||||
Adapter\devices.xml the adapter's own generated copy
|
||||
Adapter\adapter.xml <DeviceName>/<DeviceID>/<DeviceUUID>3101
|
||||
|
||||
Editing only the agent's copy leaves the adapter streaming under the old
|
||||
identity. Seven of thirty-four captured Fanuc bays are in that state -
|
||||
agent on the right machine number, adapter still on the bay it was
|
||||
imaged from (4007 on a 7801 Toshulin, 3031 on a 7804).
|
||||
|
||||
The vendor's own path is ConfigurationManager.exe, a GUI tool, which a
|
||||
remote renumber cannot drive. This does what it does, in the required
|
||||
order: stop the manager and the agent, rewrite, start again.
|
||||
|
||||
Anything that cannot be renamed is reported rather than half-applied -
|
||||
a bay whose files disagree is worse than one still on its old name.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$NewNumber,
|
||||
[Parameter(Mandatory)][hashtable[]]$Targets,
|
||||
# Overridable so the rename can be exercised against a captured bay.
|
||||
[Parameter(Mandatory)][string]$Root,
|
||||
# Whatever owns the files goes down first - for Makino the Adapter
|
||||
# Manager rewrites its configuration from memory as it stops, and for
|
||||
# Fanuc the adapter holds its own copy. Overridable only so a test can
|
||||
# drive the stop/start ordering with real services.
|
||||
[string[]]$Services = @()
|
||||
)
|
||||
|
||||
$out = @{ Updated = @(); Errors = @() }
|
||||
|
||||
$present = @($Targets |
|
||||
ForEach-Object { @{ Path = (Join-Path $Root $_.Path); Kind = $_.Kind } } |
|
||||
Where-Object { Test-Path -LiteralPath $_.Path })
|
||||
if (-not $present) { return $out }
|
||||
|
||||
# Collect EVERY name in play, not just one. A bay half-renamed by the old
|
||||
# code carries two at once - Devices.xml already on the machine number while
|
||||
# the adapter files still say MAKINO-n - and replacing only the name we
|
||||
# happened to discover leaves the other behind, which is the very state this
|
||||
# is meant to end. Gather them all, then converge every file on the target.
|
||||
$names = New-Object System.Collections.Generic.HashSet[string]
|
||||
foreach ($t in $present) {
|
||||
$raw = Get-Content -LiteralPath $t.Path -Raw -ErrorAction SilentlyContinue
|
||||
if (-not $raw) { continue }
|
||||
switch ($t.Kind) {
|
||||
'attr' { if ($raw -match '<Device[^>]+name="([^"]+)"') { [void]$names.Add($matches[1]) } }
|
||||
'name' { if ($raw -match '<Name>\s*([^<]+?)\s*</Name>') { [void]$names.Add($matches[1]) } }
|
||||
'cfg' {
|
||||
# The adapter block's key is the bare line inside Adapters { }.
|
||||
if ($raw -match '(?ms)Adapters\s*\{\s*\r?\n\s*([^\s{]+)') { [void]$names.Add($matches[1]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
[void]$names.Remove($NewNumber)
|
||||
if ($names.Count -eq 0) { return $out } # every file already on the target
|
||||
|
||||
# Agent is stopped too, so it re-reads Agent.cfg on the way back up.
|
||||
$stopped = @()
|
||||
foreach ($name in $Services) {
|
||||
$svc = Get-Service -Name $name -ErrorAction SilentlyContinue
|
||||
if (-not $svc) { continue }
|
||||
if ($svc.Status -ne 'Stopped') {
|
||||
try {
|
||||
Stop-Service -Name $name -Force -ErrorAction Stop
|
||||
$stopped += $name
|
||||
} catch {
|
||||
$out.Errors += "MTConnect: could not stop '$name' ($_); nothing was changed"
|
||||
foreach ($back in $stopped) {
|
||||
try { Start-Service -Name $back -ErrorAction Stop } catch {}
|
||||
}
|
||||
return $out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($t in $present) {
|
||||
try {
|
||||
$content = Get-Content -LiteralPath $t.Path -Raw -ErrorAction Stop
|
||||
$updated = $content
|
||||
$from = @()
|
||||
foreach ($old in $names) {
|
||||
$escaped = [regex]::Escape($old)
|
||||
switch ($t.Kind) {
|
||||
'attr' { $next = $updated -replace ('"' + $escaped + '"'), ('"' + $NewNumber + '"') }
|
||||
'name' { $next = $updated -replace ('<Name>\s*' + $escaped + '\s*</Name>'), ("<Name>$NewNumber</Name>") }
|
||||
# The adapter block is keyed by a bare name on its own line.
|
||||
'cfg' { $next = $updated -replace ('(?m)^(\s*)' + $escaped + '(\s*)$'), ("`${1}$NewNumber`${2}") }
|
||||
# <DeviceName>/<DeviceID>/<DeviceUUID> in the Fanuc adapter config
|
||||
'elem' { $next = $updated -replace ('(<Device(?:Name|ID|UUID)>)\s*' + $escaped + '\s*(</Device(?:Name|ID|UUID)>)'), ("`${1}$NewNumber`${2}") }
|
||||
}
|
||||
if ($next -ne $updated) { $from += $old; $updated = $next }
|
||||
}
|
||||
if ($updated -eq $content) { continue } # this file was already done
|
||||
Set-Content -LiteralPath $t.Path -Value $updated -NoNewline -ErrorAction Stop
|
||||
$out.Updated += "$($t.Path) ($($from -join ', ') -> $NewNumber)"
|
||||
} catch {
|
||||
$out.Errors += "MTConnect: rewrite failed for $($t.Path): $_"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($name in $stopped) {
|
||||
try { Start-Service -Name $name -ErrorAction Stop }
|
||||
catch { $out.Errors += "MTConnect: restart of '$name' failed: $_" }
|
||||
}
|
||||
|
||||
return $out
|
||||
}
|
||||
|
||||
function Update-MachineNumber {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Updates UDC + eDNC machine number in one step. Stops UDC before writing,
|
||||
relaunches after.
|
||||
.PARAMETER NewNumber
|
||||
The new machine number (digits only - caller validates).
|
||||
.PARAMETER Site
|
||||
Site name passed to UDC.exe -site argument. Defaults to 'West Jefferson'.
|
||||
.OUTPUTS
|
||||
Hashtable: @{ UdcUpdated = $bool; EdncUpdated = $bool; Errors = @() }
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$NewNumber,
|
||||
|
||||
[string]$Site = 'West Jefferson'
|
||||
)
|
||||
|
||||
$out = @{ UdcUpdated = $false; EdncUpdated = $false; Errors = @(); RegImported = $null; OldUdc = $null; OldEdnc = $null }
|
||||
|
||||
# If the machine number is changing (placeholder->real OR real->real
|
||||
# reassignment after a duplicate-image), pull per-machine state for the
|
||||
# new number from the SFLD share: NTLARS .reg, UDC settings, UDC live
|
||||
# data. The live-data restore is idempotent via one-shot migrated/
|
||||
# consumption, so it stays safe on reassign too.
|
||||
$current = Get-CurrentMachineNumber
|
||||
$out.OldUdc = $current.Udc
|
||||
$out.OldEdnc = $current.Ednc
|
||||
$isChanging = ($current.Udc -ne $NewNumber) -or ($current.Ednc -ne $NewNumber)
|
||||
|
||||
if ($isChanging -and $NewNumber -ne '9999') {
|
||||
$sharePath = $null
|
||||
$siteCfgPath = 'C:\Enrollment\site-config.json'
|
||||
if (Test-Path $siteCfgPath) {
|
||||
try {
|
||||
$cfg = Get-Content $siteCfgPath -Raw | ConvertFrom-Json
|
||||
# Alias-aware lookup: prefer new key, fall back to legacy.
|
||||
# PowerShell 5.1 has no null-coalesce operator.
|
||||
$sharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.ntlarsBackupSharePath
|
||||
if (-not $sharePath) { $sharePath = $cfg.pcProfiles.'Standard-Machine'.ntlarsBackupSharePath }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if ($sharePath) {
|
||||
try {
|
||||
. (Join-Path $PSScriptRoot 'Restore-EDncReg.ps1')
|
||||
$mounted = Mount-SFLDShare -SharePath $sharePath -DriveLetter 'V:'
|
||||
if ($mounted) {
|
||||
try {
|
||||
$out.RegImported = Import-EDncRegBackup -SourceRoot 'V:\' -MachineNumber $NewNumber
|
||||
} finally {
|
||||
& net use V: /delete /y 2>$null | Out-Null
|
||||
}
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: SFLD share unreachable - skipping restore."
|
||||
}
|
||||
} catch {
|
||||
$out.Errors += "ntlars restore failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- UDC settings JSON restore: pull udc_settings_<NewNumber>.json
|
||||
# from the SFLD UDC settings_backups share. At imaging time
|
||||
# 00-PreInstall-MachineApps.ps1 pulls this from the local
|
||||
# C:\Enrollment mirror, but a 9999-placeholder PC has no real
|
||||
# pre-stage. Once the tech sets the real number we have SFLD
|
||||
# creds, so go direct to the canonical share. ---
|
||||
$udcSettingsSharePath = $null
|
||||
if ($cfg) {
|
||||
try {
|
||||
$udcSettingsSharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.udcSettingsSharePath
|
||||
if (-not $udcSettingsSharePath) { $udcSettingsSharePath = $cfg.pcProfiles.'Standard-Machine'.udcSettingsSharePath }
|
||||
} catch {}
|
||||
}
|
||||
if ($udcSettingsSharePath) {
|
||||
try {
|
||||
$mountedUdcSet = Mount-SFLDShare -SharePath $udcSettingsSharePath -DriveLetter 'X:'
|
||||
if ($mountedUdcSet) {
|
||||
try {
|
||||
$srcSettings = Join-Path 'X:\' "udc_settings_$NewNumber.json"
|
||||
if (Test-Path -LiteralPath $srcSettings) {
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
$localUdcDir = 'C:\ProgramData\UDC'
|
||||
if (-not (Test-Path $localUdcDir)) { New-Item -ItemType Directory -Path $localUdcDir -Force | Out-Null }
|
||||
Copy-Item -LiteralPath $srcSettings -Destination $script:UdcSettingsPath -Force -ErrorAction Stop
|
||||
$out.UdcSettingsRestored = $true
|
||||
Write-Host " Update-MachineNumber: UDC settings restored from $srcSettings"
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: no udc_settings_$NewNumber.json on settings_backups share"
|
||||
}
|
||||
} finally {
|
||||
& net use X: /delete /y 2>$null | Out-Null
|
||||
}
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: UDC settings_backups share unreachable - skipping settings restore."
|
||||
}
|
||||
} catch {
|
||||
$out.Errors += "UDC settings restore failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- UDC data restore: pull CurrentData.json + ArchivedData/ from
|
||||
# the per-bay backup at <udcBackupSharePath>\<NewNumber>\.
|
||||
# One-shot: after successful restore, the live backup at the root
|
||||
# is moved into <NewNumber>\migrated\<timestamp>\ so it can't be
|
||||
# replayed on subsequent reboots or reused for a future PC at the
|
||||
# same bay. The 'isPlaceholder' guard above ensures this whole
|
||||
# block only ever fires once per PC's lifetime (placeholder->real
|
||||
# transition). ---
|
||||
$udcSharePath = $null
|
||||
if ($cfg) {
|
||||
try {
|
||||
$udcSharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.udcBackupSharePath
|
||||
if (-not $udcSharePath) { $udcSharePath = $cfg.pcProfiles.'Standard-Machine'.udcBackupSharePath }
|
||||
} catch {}
|
||||
}
|
||||
if ($udcSharePath) {
|
||||
try {
|
||||
# N:, never W:. GE-Enforce owns W: for its whole cycle, and this
|
||||
# runs as SYSTEM in the same drive namespace - taking W: here kills
|
||||
# the share out from under an in-flight enforce cycle.
|
||||
$mountedUdc = Mount-SFLDShare -SharePath $udcSharePath -DriveLetter 'N:'
|
||||
if ($mountedUdc) {
|
||||
try {
|
||||
$bayDir = Join-Path 'N:\' $NewNumber
|
||||
$srcCur = Join-Path $bayDir 'CurrentData.json'
|
||||
$srcArc = Join-Path $bayDir 'ArchivedData'
|
||||
if (Test-Path -LiteralPath $srcCur) {
|
||||
Write-Host " Update-MachineNumber: UDC backup found at $bayDir - restoring."
|
||||
|
||||
# Stop UDC pre-emptively so CurrentData.json isn't locked
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
$localUdcDir = 'C:\ProgramData\UDC'
|
||||
if (-not (Test-Path $localUdcDir)) { New-Item -ItemType Directory -Path $localUdcDir -Force | Out-Null }
|
||||
|
||||
$localArc = Join-Path $localUdcDir 'ArchivedData'
|
||||
|
||||
# Copy
|
||||
Copy-Item -LiteralPath $srcCur -Destination (Join-Path $localUdcDir 'CurrentData.json') -Force -ErrorAction Stop
|
||||
if (Test-Path -LiteralPath $srcArc) {
|
||||
if (Test-Path -LiteralPath $localArc) { Remove-Item -LiteralPath $localArc -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
Copy-Item -LiteralPath $srcArc -Destination $localArc -Recurse -Force -ErrorAction Stop
|
||||
}
|
||||
|
||||
# Move live backup -> migrated/<timestamp>/ (one-shot consumption)
|
||||
$stamp = (Get-Date -Format 'yyyy-MM-ddTHH-mm-ssZ')
|
||||
$migDir = Join-Path $bayDir 'migrated'
|
||||
$migStamp = Join-Path $migDir $stamp
|
||||
if (-not (Test-Path -LiteralPath $migDir)) { New-Item -ItemType Directory -Path $migDir -Force | Out-Null }
|
||||
if (-not (Test-Path -LiteralPath $migStamp)) { New-Item -ItemType Directory -Path $migStamp -Force | Out-Null }
|
||||
|
||||
Move-Item -LiteralPath $srcCur -Destination (Join-Path $migStamp 'CurrentData.json') -Force -ErrorAction Stop
|
||||
if (Test-Path -LiteralPath $srcArc) {
|
||||
Move-Item -LiteralPath $srcArc -Destination (Join-Path $migStamp 'ArchivedData') -Force -ErrorAction Stop
|
||||
}
|
||||
$bakManifest = Join-Path $bayDir 'backup.manifest.json'
|
||||
if (Test-Path -LiteralPath $bakManifest) {
|
||||
Move-Item -LiteralPath $bakManifest -Destination (Join-Path $migStamp 'backup.manifest.json') -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Audit manifest in migrated/<stamp>/
|
||||
$localArcInfo = if (Test-Path $localArc) { Get-ChildItem $localArc -Recurse -File -ErrorAction SilentlyContinue } else { @() }
|
||||
$restoreManifest = [ordered]@{
|
||||
RestoredAt = (Get-Date -Format 'o')
|
||||
DestinationHostname = $env:COMPUTERNAME
|
||||
DestinationUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
MachineNumber = $NewNumber
|
||||
CurrentDataBytes = (Get-Item (Join-Path $localUdcDir 'CurrentData.json')).Length
|
||||
ArchivedDataFiles = $localArcInfo.Count
|
||||
ArchivedDataBytes = ($localArcInfo | Measure-Object Length -Sum).Sum
|
||||
RestoredVia = 'Update-MachineNumber.ps1'
|
||||
}
|
||||
$restoreManifest | ConvertTo-Json | Set-Content -Path (Join-Path $migStamp 'restore.manifest.json') -Encoding UTF8
|
||||
|
||||
$out.UdcRestored = $true
|
||||
Write-Host " Update-MachineNumber: UDC restore OK (consumed -> migrated\$stamp\)"
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: no UDC backup at $bayDir (fresh PC, no prior data)"
|
||||
}
|
||||
} finally {
|
||||
& net use N: /delete /y 2>$null | Out-Null
|
||||
}
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: UDC backup share unreachable - skipping UDC restore."
|
||||
}
|
||||
} catch {
|
||||
$out.Errors += "UDC restore failed: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Stop UDC before editing its JSON (avoid stale shutdown write) ---
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
# --- Update UDC settings JSON ---
|
||||
# -ErrorAction Stop on the WRITE so PermissionDenied / IO errors become
|
||||
# terminating and actually hit the catch block. Without this, the cmdlet
|
||||
# writes a non-terminating error (visible in transcript) but flow
|
||||
# continues + $out.UdcUpdated is set to $true, leading the dialog to
|
||||
# report "UDC updated" when the file write actually failed.
|
||||
if (Test-Path $script:UdcSettingsPath) {
|
||||
try {
|
||||
$json = Get-Content $script:UdcSettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
$json.GeneralSettings.MachineNumber = $NewNumber
|
||||
$json | ConvertTo-Json -Depth 99 | Set-Content -Path $script:UdcSettingsPath -Encoding UTF8 -ErrorAction Stop
|
||||
$out.UdcUpdated = $true
|
||||
} catch {
|
||||
$out.Errors += "UDC update failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Update eDNC registry ---
|
||||
# Same -ErrorAction Stop reasoning as above. Set-ItemProperty's
|
||||
# PermissionDenied is non-terminating by default; without -ErrorAction
|
||||
# Stop, the catch block never fires and $out.EdncUpdated=$true gets set
|
||||
# despite the write failing. This is the bug that made the 13:35:39
|
||||
# tech run on FGY07FZ3 report "eDNC updated to 3005 / All updates
|
||||
# succeeded" while the actual reg value stayed at 9999.
|
||||
if (Test-Path $script:EdncRegPath) {
|
||||
try {
|
||||
Set-ItemProperty -Path $script:EdncRegPath -Name MachineNo -Value $NewNumber -Type String -Force -ErrorAction Stop
|
||||
$out.EdncUpdated = $true
|
||||
} catch {
|
||||
$out.Errors += "eDNC update failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Relaunch UDC with new args ---
|
||||
if ((Test-Path $script:UdcExePath) -and $out.UdcUpdated) {
|
||||
try {
|
||||
# UDC.exe arg signature: quoted site name (with space), then
|
||||
# dash-prefixed machine number. Example: UDC.exe "West Jefferson" -7605
|
||||
Start-Process -FilePath $script:UdcExePath -ArgumentList @("`"$Site`"", "-$NewNumber")
|
||||
} catch {
|
||||
$out.Errors += "UDC relaunch failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Update MTConnect Devices.xml (per-variant) + restart agent services ---
|
||||
# MTConnect deploys via the GE-Enforce manifest engine + the
|
||||
# Install_MTConnect_ExisDir_BatConvert.ps1 wrapper. Devices.xml on disk
|
||||
# has the machine number embedded in the <Device name= uuid= id=> attrs.
|
||||
# When the tech changes 9999 -> real number, those attrs need to follow,
|
||||
# OR MTConnect data will keep reporting under the old/wrong identity.
|
||||
#
|
||||
# We do an inline substitution rather than re-running the full wrapper
|
||||
# because the tech may not have credential delegation to the SFLD share
|
||||
# from their interactive session.
|
||||
$out.MTConnectUpdated = @()
|
||||
# Drive this off "is the service installed?" rather than file existence:
|
||||
# Fanuc and Okuma both deploy to C:\MTConnect\Agent\, distinguishable only
|
||||
# by the registered service name (NTFS is case-insensitive so the two
|
||||
# devices.xml / Devices.xml entries collapse to the same file). Without
|
||||
# this filter, the Okuma branch on an Okuma PC sees the file already
|
||||
# rewritten by the (no-op) Fanuc branch and skips the service restart.
|
||||
# Okuma and eDNC keep the name in one file only - verified across 7 Okuma
|
||||
# and 15 eDNC bay captures - so the simple edit is right for them. Fanuc and
|
||||
# Makino are handled below instead.
|
||||
$mtcVariants = @(
|
||||
@{ Service='MTConnect Agent Okuma'; Path='C:\MTConnect\Agent\Devices.xml' },
|
||||
@{ Service='MTConnect eDNC Agent'; Path='C:\MTConnect_eDNC\Agent\Devices.xml' }
|
||||
)
|
||||
foreach ($v in $mtcVariants) {
|
||||
$svc = Get-Service -Name $v.Service -ErrorAction SilentlyContinue
|
||||
if (-not $svc) { continue }
|
||||
if (-not (Test-Path -LiteralPath $v.Path)) { continue }
|
||||
try {
|
||||
$content = Get-Content -LiteralPath $v.Path -Raw -ErrorAction Stop
|
||||
# Find the Device root's name= attr to discover the OLD identifier
|
||||
if ($content -notmatch '<Device[^>]+name="([^"]+)"') { continue }
|
||||
$oldName = $matches[1]
|
||||
# Most variants encode machine number as the trailing digit run:
|
||||
# Fanuc/Makino: name="9999" -> trailing 9999
|
||||
# OKUMA: name="loc9999" -> trailing 9999, prefix 'loc'
|
||||
# eDNC: name="eDNC_OKUMA9999" -> trailing 9999, prefix 'eDNC_OKUMA'
|
||||
if ($oldName -notmatch '^(.*?)(\d+)$') { continue }
|
||||
$prefix = $matches[1]
|
||||
$oldDigits = $matches[2]
|
||||
if ($oldDigits -eq $NewNumber) { continue } # already correct
|
||||
$oldFull = "$prefix$oldDigits"
|
||||
$newFull = "$prefix$NewNumber"
|
||||
$oldEsc = [regex]::Escape($oldFull)
|
||||
# Quoted attr value: name="X" / uuid="X" / id="X"
|
||||
$content = $content -replace ('"' + $oldEsc + '"'), ('"' + $newFull + '"')
|
||||
# OKUMA-style with serial after dot: uuid="locX.<serial>"
|
||||
$content = $content -replace ('"' + $oldEsc + '\.'), ('"' + $newFull + '.')
|
||||
Set-Content -LiteralPath $v.Path -Value $content -NoNewline -ErrorAction Stop
|
||||
$out.MTConnectUpdated += "$($v.Path) ($oldFull -> $newFull)"
|
||||
try { Restart-Service -Name $v.Service -Force -ErrorAction Stop }
|
||||
catch { $out.Errors += "Restart $($v.Service) failed: $_" }
|
||||
} catch {
|
||||
$out.Errors += "MTConnect Devices.xml update failed for $($v.Path): $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Multi-file variants: rename every copy, services stopped first ---
|
||||
# Fanuc and Makino each keep the device name in more than one file, and the
|
||||
# agent's copy alone is not enough - the adapter goes on streaming under the
|
||||
# old identity, which is how seven captured Fanuc bays ended up with the
|
||||
# agent on the right machine number and the adapter on the bay it was
|
||||
# imaged from.
|
||||
$multiFileVariants = @(
|
||||
@{
|
||||
Probe = 'MTConnect Agent Fanuc'
|
||||
Root = 'C:\MTConnect'
|
||||
Services = @('MTConnect Adapter Fanuc', 'MTConnect Agent Fanuc')
|
||||
Targets = @(
|
||||
@{ Path = 'Agent\devices.xml'; Kind = 'attr' },
|
||||
@{ Path = 'Adapter\devices.xml'; Kind = 'attr' },
|
||||
@{ Path = 'Adapter\adapter.xml'; Kind = 'elem' }
|
||||
)
|
||||
},
|
||||
@{
|
||||
Probe = 'Makino MTConnect Agent'
|
||||
Root = 'C:\Makino-MTConnect'
|
||||
Services = @('Makino Adapter Manager', 'Makino MTConnect Agent')
|
||||
Targets = @(
|
||||
@{ Path = 'Agent\Devices.xml'; Kind = 'attr' },
|
||||
@{ Path = 'Agent\Devices.EDM.xml'; Kind = 'attr' },
|
||||
@{ Path = 'Data\AdapterDataFile.xml'; Kind = 'name' },
|
||||
@{ Path = 'Agent\Agent.cfg'; Kind = 'cfg' }
|
||||
)
|
||||
}
|
||||
)
|
||||
foreach ($variant in $multiFileVariants) {
|
||||
if (-not (Get-Service -Name $variant.Probe -ErrorAction SilentlyContinue)) { continue }
|
||||
$renamed = Update-MTConnectVariantName -NewNumber $NewNumber `
|
||||
-Root $variant.Root -Targets $variant.Targets -Services $variant.Services
|
||||
$out.MTConnectUpdated += $renamed.Updated
|
||||
$out.Errors += $renamed.Errors
|
||||
}
|
||||
|
||||
# Keep C:\Enrollment\machine-number.txt in sync. Post-imaging GE-Enforce
|
||||
# prefers eDNC reg, but imaging-time scripts (Install-FromManifest
|
||||
# TargetMachineNumbers filter, 01-eDNC.ps1, 03-RestoreEDncConfig.ps1)
|
||||
# still read this file. Avoid drift on reassign.
|
||||
try {
|
||||
$mnFile = 'C:\Enrollment\machine-number.txt'
|
||||
$mnDir = Split-Path -Parent $mnFile
|
||||
if (-not (Test-Path $mnDir)) { New-Item -ItemType Directory -Path $mnDir -Force | Out-Null }
|
||||
Set-Content -Path $mnFile -Value $NewNumber -Encoding UTF8 -NoNewline
|
||||
$out.MachineNumberTxtUpdated = $true
|
||||
} catch {
|
||||
$out.Errors += "machine-number.txt update failed: $_"
|
||||
}
|
||||
|
||||
return $out
|
||||
}
|
||||
Reference in New Issue
Block a user