Shopfloor imaging: CMM type, Configure-PC override fix, serial drivers

- CMM imaging pipeline: WinPE-staged bootstrap + on-logon enforcer
  against tsgwp00525 share, manifest-driven installer runner shared via
  Install-FromManifest.ps1. Installs PC-DMIS 2016/2019 R2, CLM 1.8,
  goCMM; enables .NET 3.5 prereq; registers GE CMM Enforce logon task
  for ongoing version enforcement.
- Shopfloor serial drivers: StarTech PCIe serial + Prolific PL2303
  USB-to-serial via Install-Drivers.cmd wrapper calling pnputil
  /add-driver /subdirs /install. Scoped to Standard PCs.
- OpenText extended to CMM/Keyence/Genspect/WaxAndTrace via
  preinstall.json PCTypes; Defect Tracker added to CMM profile
  desktopApps + taskbarPins.
- Configure-PC startup-item toggle now persists across the logon
  sweep via C:\\ProgramData\\GE\\Shopfloor\\startup-overrides.json;
  06-OrganizeDesktop Phase 3 respects suppressed items.
- Get-ProfileValue helper added to Shopfloor/lib/Get-PCProfile.ps1;
  distinguishes explicit empty array from missing key (fixes Lab
  getting Plant Apps in startup because empty array was falsy).
- 06-OrganizeDesktop gains transcript logging at C:\\Logs\\SFLD\\
  06-OrganizeDesktop.log and now deletes the stale Shopfloor Intune
  Sync task when C:\\Enrollment\\sync-complete.txt is present (task
  was registered with Limited principal and couldn't self-unregister).
- startnet.cmd CMM xcopy block (gated on pc-type=CMM) stages the
  bundle to W:\\CMM-Install during WinPE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-04-11 12:58:47 -04:00
parent bc123c1066
commit ee7d3bad66
17 changed files with 1069 additions and 196 deletions

View File

@@ -46,6 +46,50 @@ if (-not $isAdmin) {
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"
@@ -280,11 +324,9 @@ function Add-ShopfloorToolsApps {
#
# Kind = 'exe' -> build a fresh .lnk from ExePath
# Kind = 'existing' -> copy an existing .lnk via Find-ExistingLnk
$cfgApps = if ($pcProfile -and $pcProfile.desktopApps) { $pcProfile.desktopApps }
elseif ($siteConfig -and $siteConfig.desktopApps) { $siteConfig.desktopApps }
else { $null }
$cfgApps = Get-ProfileValue 'desktopApps'
if ($cfgApps) {
if ($null -ne $cfgApps -and $cfgApps.Count -gt 0) {
$apps = @($cfgApps | ForEach-Object {
$entry = @{ Name = $_.name; Kind = $_.kind }
if ($_.kind -eq 'exe') { $entry.ExePath = $_.exePath }
@@ -490,16 +532,27 @@ 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 is the auto-apply step that eliminates the need
# for Configure-PC to ask about startup items during the imaging chain.
# Configure-PC.bat on the desktop still lets the tech modify them later.
# 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'
$cfgStartup = if ($pcProfile -and $pcProfile.startupItems) { $pcProfile.startupItems }
elseif ($siteConfig -and $siteConfig.startupItems) { $siteConfig.startupItems }
else { $null }
$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 : $_"
}
}
if ($cfgStartup -and $cfgStartup.Count -gt 0) {
$cfgStartup = Get-ProfileValue 'startupItems'
if ($null -ne $cfgStartup -and $cfgStartup.Count -gt 0) {
if (-not (Test-Path $startupDir)) {
New-Item -ItemType Directory -Path $startupDir -Force | Out-Null
}
@@ -509,6 +562,10 @@ if ($cfgStartup -and $cfgStartup.Count -gt 0) {
) | 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) {
@@ -567,4 +624,7 @@ 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

View File

@@ -45,11 +45,9 @@ $layoutXmlPath = Join-Path $defaultUserShell 'LayoutModification.xml'
# it; all other entries reference C:\Users\Public\Desktop\Shopfloor Tools\
# which 06-OrganizeDesktop.ps1 populates.
# ============================================================================
$cfgPins = if ($pcProfile -and $pcProfile.taskbarPins) { $pcProfile.taskbarPins }
elseif ($siteConfig -and $siteConfig.taskbarPins) { $siteConfig.taskbarPins }
else { $null }
$cfgPins = Get-ProfileValue 'taskbarPins'
if ($cfgPins) {
if ($null -ne $cfgPins -and $cfgPins.Count -gt 0) {
$pinSpec = @($cfgPins | ForEach-Object {
@{
Name = $_.name

View File

@@ -199,13 +199,11 @@ function Resolve-StartupUrl {
# 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 = if ($pcProfile -and $pcProfile.edgeStartupTabs) { $pcProfile.edgeStartupTabs }
elseif ($siteConfig -and $siteConfig.edgeStartupTabs) { $siteConfig.edgeStartupTabs }
else { $null }
$cfgTabs = Get-ProfileValue 'edgeStartupTabs'
$startupTabs = @()
if ($cfgTabs) {
if ($null -ne $cfgTabs -and $cfgTabs.Count -gt 0) {
foreach ($tab in $cfgTabs) {
$fallback = if ($tab.fallbackUrlKey -and $siteConfig.urls) { $siteConfig.urls.$($tab.fallbackUrlKey) } else { '' }
$url = Resolve-StartupUrl -BaseName $tab.baseName -Fallback $fallback

View File

@@ -42,6 +42,31 @@ Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurren
$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
@@ -116,11 +141,9 @@ $edgePath = @(
# Resolved from: pcProfile.startupItems > siteConfig.startupItems > hardcoded
# ============================================================================
$cfgItems = if ($pcProfile -and $pcProfile.startupItems) { $pcProfile.startupItems }
elseif ($siteConfig -and $siteConfig.startupItems) { $siteConfig.startupItems }
else { $null }
$cfgItems = Get-ProfileValue 'startupItems'
if ($cfgItems) {
if ($null -ne $cfgItems -and $cfgItems.Count -gt 0) {
$items = @()
$num = 0
foreach ($si in $cfgItems) {
@@ -345,21 +368,38 @@ 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 }
if (-not $item.Available) {
Write-Host " $($item.Label): not installed, skipping" -ForegroundColor DarkGray
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
@@ -367,6 +407,15 @@ if ($selection) {
}
}
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) {

View File

@@ -18,9 +18,24 @@
# 3. (hardcoded fallback in the calling script)
#
# Usage pattern in consuming scripts:
# $items = if ($pcProfile -and $pcProfile.startupItems) { $pcProfile.startupItems }
# elseif ($siteConfig -and $siteConfig.startupItems) { $siteConfig.startupItems }
# else { $null } # trigger hardcoded fallback
# $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'

View File

@@ -561,9 +561,11 @@ function Invoke-SetupComplete {
if ($AsTask) {
# Write completion marker so future logon-triggered runs exit
# immediately. We can't Unregister-ScheduledTask because the task
# runs as BUILTIN\Users (Limited) which lacks permission to delete
# tasks. The marker file makes the task a harmless no-op.
# immediately. We can't Unregister-ScheduledTask ourselves because
# this task runs as BUILTIN\Users (Limited) which lacks permission
# to delete tasks. 06-OrganizeDesktop.ps1 (which runs as SYSTEM via
# its own sweep task on every logon) watches for this marker and
# deletes the stale Shopfloor Intune Sync task when it sees us.
try {
Set-Content -LiteralPath $syncCompleteMarker -Value (Get-Date -Format 'o') -Force
Write-Host "Sync complete marker written." -ForegroundColor Green