Needed for eMxInfo.txt (site-specific eDNC config). The file has no
DisplayVersion in the registry and no canonical MSI; we ship it as a
standalone secret on the SFLD share and key drift correction off its
SHA256. When the yearly replacement drops, bump the hash in
machineapps-manifest.json and every Standard-Machine PC catches up on
next logon.
Patched Install-FromManifest in all three copies (CMM, common, Standard)
for consistency. Also adds the eMxInfo.txt entry to the Standard
machineapps-manifest template and an Install-eMxInfo.cmd template that
copies the file into both 32/64-bit eDNC Program Files paths.
Reason: Intune DSC's main-category YAML was pushing these to every main
device, including Timeclocks - DSC has no awareness of our pc-subtype
distinction. After UDC/eDNC/NTLARS are removed from the DSC YAML, ongoing
version drift would no longer be corrected. This enforcer replaces that,
scoped correctly by subtype.
Structure mirrors CMM (CMM-Enforce.ps1) and common (Acrobat-Enforce.ps1):
- Machine-Enforce.ps1: SYSTEM logon task; mounts SFLD share with HKLM-
backed creds; hands off to Install-FromManifest.
- machineapps-manifest.template.json: repo reference; authoritative copy
lives on the share at \\tsgwp00525.wjs.geaerospace.net\shared\dt\
shopfloor\main\machineapps\machineapps-manifest.json.
- Register-MachineEnforce.ps1: idempotent setup; stages scripts to
C:\Program Files\GE\MachineApps and registers the task.
- lib/Install-FromManifest.ps1: copy of the common/ version (already has
Type=CMD support).
Sub-type gating belt-and-suspenders:
- Run-ShopfloorSetup.ps1 only calls Register-MachineEnforce when
$pcType -eq "Standard" -and $pcSubType -eq "Machine".
- Machine-Enforce.ps1 itself re-reads pc-subtype.txt and exits early if
not "Machine", so a mistakenly-deployed copy no-ops.
site-config.json:
- Added "machineappsSharePath" to Standard-Machine pcProfile.
Drive letter U: to stay clear of CMM (S:) and Acrobat (T:) enforcers
that may run concurrently at logon.
Update workflow:
drop new UDC/eDNC/NTLARS installer on the SFLD share,
bump DetectionValue in machineapps-manifest.json,
every Machine PC catches up on next user logon.
Acrobat Reader enforcement:
- playbook/shopfloor-setup/common/ is the cross-PC-type staging dir. Mirrors
CMM/ structure (enforce script + its Install-FromManifest copy + manifest
template + register script).
- Acrobat-Enforce.ps1 runs as SYSTEM on every logon, reads
acrobatSharePath from site-config.common, mounts the SFLD share with
the same HKLM-backed credential lookup CMM-Enforce uses, hands the
acrobat-manifest.json from the share to Install-FromManifest.
- Install-FromManifest extended with Type=CMD so it can invoke vendor-
supplied .cmd wrappers (Install-AcroReader.cmd does a two-step MSI+MSP
install that does not fit MSI/EXE types cleanly). cmd.exe /c wraps it
because UseShellExecute=false cannot launch .cmd directly.
- Register-AcrobatEnforce.ps1 stages scripts to C:\Program Files\GE\Acrobat
and registers "GE Acrobat Enforce" scheduled task. Called from
Run-ShopfloorSetup.ps1 right before the enrollment (PPKG) step so it
applies to every PC type, not just CMM.
- acrobat-manifest.template.json is the repo reference; the authoritative
copy lives on the SFLD share at
\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\common\acrobat\
Bumping Acrobat updates = drop new MSP on share, bump DetectionValue in
manifest; enforcer catches every PC on next logon.
- site-config.json: add "common": { "acrobatSharePath": ... }. Uses a
new top-level block rather than a PC-type-specific one since Acrobat
applies everywhere.
Initial install still happens via the preinstall flow
(Install-AcroReader.cmd during WinPE). The enforcer is the ongoing-
updates side; on a freshly-imaged PC detection passes and it no-ops.
Also in this commit:
- run-enrollment.ps1: provtool.exe argument syntax fix. First test
returned 0x80004005 E_FAIL in 1s because /ppkg: and /log: are not
valid provtool flags; the cmdlet's internal call used positional
path + /quiet + /source. Switched to that syntax.
Three changes that go together so a re-image never hits "System error 53":
1. dnsmasq dhcp-script hook (playbook/pxe-server-helpers/pxe-dhcp-hook.sh)
Fires on every add/del lease event. Runs conntrack -D and ss -K for the
client IP so any stale ESTABLISHED SMB session from a previous boot is
cleared before the client reconnects. Runs as root (dnsmasq default).
Wired into /etc/dnsmasq.conf via dhcp-script= directive in the playbook.
2. One-source PPKG (playbook/startnet.cmd + startnet-template.cmd)
The 5 per-Office PPKG copies were bit-for-bit identical; only the
filename differs because BPRT parses Office and Region out of the name.
Store one source file (e.g. GCCH_Prod_SFLD_v4.11.ppkg) and construct
the BPRT-tagged target filename at menu-selection time from variables:
SOURCE_PPKG / PPKG_VER / PPKG_EXP / REGION / OFFICE
copy /Y "Y:\ppkgs\%SOURCE_PPKG%" "W:\Enrollment\%PPKG%"
Bumped PPKG_VER v4.10 -> v4.11 and PPKG_EXP 20260430 -> 20270430.
Saves ~30G on disk per version.
3. run-enrollment.ps1 already committed in 5a9c3db uses provtool.exe
directly (no PowerShell cmdlet 180s timeout). Included here because it
is part of the same end-to-end PPKG path.
Observed today on E8FHGDB4: Install-ProvisioningPackage timed out after
the PowerShell cmdlet's hardcoded 180s limit on a 7.6 GB GCCH v4.10
PPKG. The catch-block fell through to Add-ProvisioningPackage, which
returned "success" but the PPKG diagnostic bundle showed the child
provtool.exe was called with empty packagePathsToAdd (session created,
State=Not started, RebootCount=0). The PC was named, OOBE-completed,
and BPRT apps ran, but the bulk enrollment never applied - PC was not
Entra-joined.
Microsoft Docs GitHub issue 502 confirms the 180s cmdlet timeout is
hardcoded with no configuration option. Quest KB 4376269 suggests
rebuilding the PPKG with the latest Windows Configuration Designer,
but that is upstream and not under our control per PPKG.
Switch to Start-Process -FilePath provtool.exe -Wait. The wait is on
the actual child process, no caller-side timeout. provtool.exe is
what the cmdlet was invoking anyway; we just bypass the wrapper that
imposes the limit.
Sources:
https://support.quest.com/on-demand-migration/kb/4376269https://github.com/MicrosoftDocs/windows-powershell-docs/issues/502https://learn.microsoft.com/en-us/windows/configuration/provisioning-packages/provisioning-apply-package
Two new docs cover what the system is (boot chain, services, shares,
enrollment layout, data flow) and what to change per site (every
hardcoded value, where it lives, secrets handling).
scripts/mirror-from-gold.sh replicates content from an existing PXE
server (Operating Systems, drivers, packages, custom installers, BIOS,
PCDMIS, Blancco custom image, site-config) onto a freshly-installed PXE
server. Translates the legacy flat enrollment layout on the source into
the reorganized taxonomy (ppkgs/, pre-install/installers/, installers-
post/cmm/, blancco/, config/) on the destination. Tolerates rsync
exit 23 (permission-denied subdirs like the OpenText W10shortcuts dir
that is pxe-upload-group-only on legacy servers).
Workstation reorganization:
- All build/deploy/helper scripts moved into scripts/ (paths updated to use
REPO_ROOT instead of SCRIPT_DIR so they resolve sibling dirs from the new
depth)
- New config/ directory placeholder for site-specific overrides
- Removed stale: mok-keys/, test-vm.sh, test-lab.sh, setup-guide-original.txt,
unattend/ (duplicate of moved playbook/FlatUnattendW10.xml)
- README.md and SETUP.md structure listings updated, dead "Testing with KVM"
section removed
- .claude/ gitignored
Enrollment share internal taxonomy (forward-looking; existing servers
unaffected since they keep their current boot.wim with flat paths):
- Single SMB share kept (WinPE only mounts one Y: drive), but content now
organised into ppkgs/, scripts/, config/, shopfloor-setup/, pre-install/{bios,
installers}, installers-post/cmm/, blancco/, logs/
- README.md deployed to share root explaining each subdir
- New playbook tasks deploy site-config.json + wait-for-internet.ps1 +
migrate-to-wifi.ps1 explicitly (were ad-hoc on legacy servers)
- BIOS subdir moved into pre-install/bios/, preinstall/ renamed to pre-install/
- startnet.cmd + startnet-template.cmd updated with new Y:\subdir\ paths
- Bumped GCCH PPKG references v4.9 -> v4.10
Blancco USB-build fixes (so next fresh USB install boots Blancco end-to-end
without the manual fixup we did against GOLD):
- grub-blancco.cfg: kernel/initrd switched HTTP -> TFTP (GRUB's HTTP module
times out on multi-MB files); added modprobe.blacklist=iwlwifi,iwlmvm,btusb
(WiFi drivers hang udev on Intel business PCs)
- grubx64.efi rebuilt from updated cfg
- Playbook task added to create /srv/tftp/blancco/ symlinks pointing at the
HTTP-served binaries
run-enrollment.ps1: OOBEComplete is now set AFTER PPKG install (Win11 22H2+
hangs indefinitely if OOBEComplete is set before the bulk-enrollment PPKG runs).
Also includes deploy-bios.sh / pull-bios.sh / busybox-static / models.txt
that were sitting untracked at the repo root.
- autoinstall/user-data: move lvextend/growpart/pvresize BEFORE playbook
so 130GB of drivers+PPKGs fits during first-boot copy. Use
tr -d "[:space:]" to avoid breaking outer bash -c single-quote wrap.
- playbook: add executable: /bin/bash to Dell driver deploy (process
substitution) and Blancco initramfs builder (brace expansion).
- playbook: make "Ensure Samba user for Blancco reports" idempotent via
pdbedit check so re-runs don't abort the play.
- download-packages.sh: also download dist-upgrade package set. Explicit
--simulate misses transitive version bumps (e.g. gnupg 17.4 needs
matching gpgv 17.4) causing offline dpkg "dependency problems" when
ISO baseline is older than noble-updates.
- PreInstall runner reads pc-subtype.txt and matches PCTypes against
both base type (Standard) and composite key (Standard-Machine).
- UDC scoped to Standard-Machine only. eDNC and MachineNumberACLs
skip on Standard-Timeclock sub-type.
- Lab added to OpenText PCTypes.
- build-usb.sh copies enrollment/ (PPKGs) and drivers-staging/ (Dell
driver packs) onto USB for self-contained deployment.
- Playbook deploys PPKGs and drivers from USB to PXE server shares.
- Gitignore enrollment/, drivers-staging/, *.ppkg (large binaries).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Monitor-IntuneProgress: Display PCs skip DSC phases entirely (no SAS
token, no DSCInstall.log), complete after Phase 1 identity. Renderer
hides Phase 2-5 for Display type.
- Playbook: deploy PPKG files and run-enrollment.ps1 from USB to
enrollment share. Bump dnsmasq restart cron from 15s to 30s.
- build-usb.sh: copy enrollment/ directory (PPKGs) onto USB if present.
- user-data: add dpkg --configure -a after offline .deb install to fix
packages left in unconfigured state (cron, systemd-timesyncd).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Install-KioskApp.cmd wrapper reads display-type.txt and runs the
matching Inno Setup installer (Lobby or Dashboard). Replaces the
standalone 09-Setup-Display.ps1 for uniform app install pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same version as GEA-Engineering image. Two-step install via wrapper
script (MSI + enterprise transform, then DC update patch).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rolls up everything from the CMM imaging test iteration tonight. No
single concern - several small, related polish items on the option-3
patched-MSI pipeline and the shopfloor-setup / sync_intune handoff.
- Rename all type-specific "01-Setup-<Type>.ps1" scripts to
"09-Setup-<Type>.ps1" across CMM, Display, Genspect, Keyence, Lab,
and WaxAndTrace. The "01-" prefix implied the script runs first in
the overall sequence when it actually runs between baseline (00, 04)
and finalization (06, 07). Logs now read "Running CMM setup:
09-Setup-CMM.ps1" which matches the real position. Standard/
01-eDNC.ps1 + 02-MachineNumberACLs.ps1 left alone - those digits
represent real within-type ordering.
- playbook/shopfloor-setup/site-config.json CMM profile updates:
- startupItems = [] (empty). Previously had WJ Shopfloor auto-launch
which the user does not want on CMM workstations. Now relies on
the Get-ProfileValue empty-array fix to not fall through to site
defaults.
- desktopApps + taskbarPins gain entries for PC-DMIS 2016, PC-DMIS
2019 R2, CLM Admin, and goCMM so 06-OrganizeDesktop Phase 2
materializes them into C:\\Users\\Public\\Desktop\\Shopfloor Tools\\
and 07-TaskbarLayout pins them. goCMM is under C:\\Program Files
(x86)\\General Electric\\goCMM\\ (GE product, not Hexagon).
- playbook/shopfloor-setup/Run-ShopfloorSetup.ps1: remove the blocking
"UNPLUG ethernet cable, press any key" prompt + the interactive
wired-NIC re-enable. The whole prompt block was a hard blocker on
the imaging chain that required a human to walk to each PC.
- playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1:
re-enable wired NICs unconditionally at the top of the transcript.
This is the new home for the re-enable that used to live behind the
prompt in Run-ShopfloorSetup. By the time sync_intune fires (after
PPKG reboot + auto-login + Stage-Dispatcher), the tech has had
minutes of wall-clock time to physically rewire from PXE to
production without us blocking on a keypress. Tower case is a
no-op because migrate-to-wifi.ps1 already left wired enabled.
- Internal comment updates in 09-Setup-CMM.ps1, cmm-manifest.json,
Install-FromManifest.ps1, and startnet.cmd (+ startnet-template)
to reflect the new filename.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PC-DMIS refuses to install without a valid license in /qn mode; its
BA / MSI custom actions ProcessLicensingFromBundle (which spins for
~13 minutes trying to activate against licensing.wilcoxassoc.com)
and IsLicenseDateValid (which errors 1603 when no license file is
present) are the gate. Bypassed by dark-extracting the chained MSIs
from each Burn bundle and pre-patching both custom actions'
InstallExecuteSequence.Condition columns to '0' via Windows
Installer COM SQL UPDATE. The patched MSIs install cleanly with no
license, PCDLRN.exe loads at runtime, hits its own runtime license
check, and shows the normal "no license" dialog. Tech activates via
clmadmin.exe post-imaging and PC-DMIS launches normally.
- playbook/preinstall/preinstall.json: adds VC++ 2010 x64 and VC++
2012 x64 redistributable entries scoped to all PC types. PC-DMIS
links msvcr100.dll / mfc100u.dll (VS 2010) and msvcr110.dll /
mfc110u.dll (VS 2012); without these the exe gets DLL_NOT_FOUND
(0xC0000135) at launch. Win11 ships VC++ 2022 (covers 2015+) but
not 2010/2012, so we ship these from the dark-extracted bundle
payloads. Small (~13 MB combined), inert on PCs that don't need
them, so the filter is "*".
- playbook/shopfloor-setup/CMM/cmm-manifest.json: version 2.0.
Drops the bundle EXEs, installs patched MSIs directly with
properly quoted INSTALLFOLDER / APPLICATIONFOLDER paths (the
earlier "hangs" were caused by Start-Process splitting unquoted
paths on spaces, not actual msiexec hangs). Skips the chained
CLM Tools 1.5/1.7 MSIs - CLM 1.8.73 standalone provides the same
interfaces and PC-DMIS MSIs have no LaunchCondition requiring
Tools 1.5 / 1.7 specifically. Keeps Protect Viewer from the 2019
R2 bundle as a separate entry. CLM 1.8 and goCMM bundles run
unpatched (no install-time license check).
- playbook/sync-cmm.sh: now also includes *.msi files in the
upload set, not just *.exe.
Known caveats: patched MSIs have HashMismatch signatures (expected
- Windows Installer accepts them in /qn mode on locally-cached
machines). Every Hexagon bundle version bump requires re-dark-
extracting and re-patching. Unsupported by Hexagon; do not call
them for install-related issues without reverting to the original
bundles first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FlatUnattendW10-shopfloor.xml was rejected by Windows OOBE with
"the answer file is invalid" after the earlier tower-no-WiFi fix.
Root cause: the inline PowerShell in <CommandLine> for Orders 4 and
5 exceeded the SynchronousCommand CommandLine length limit (~1024
chars) and/or contained characters the unattend schema validator
dislikes.
Fix: move the logic to two external PS1 scripts and shrink both
CommandLine entries to ~85 chars each that just invoke the scripts.
- playbook/wait-for-internet.ps1: 60s interactive prompt ("connect
production network now"), then poll TCP 443 to login.microsoft-
online.us for up to 10 min with a hard timeout so the loop always
exits. Uses Test-NetConnection -Port 443 (not Test-Connection /
ICMP) because Microsoft 365 edges do not reliably respond to ping.
- playbook/migrate-to-wifi.ps1: Gates the entire wired-disable
migration on "does a WiFi adapter exist?" If not (tower), the
script is a no-op. If yes, disable wired / wait for WiFi internet
with a 5 min timeout / re-enable wired on timeout fallback.
- startnet.cmd stages both new scripts to W:\Enrollment\ next to
run-enrollment.ps1 during the WinPE phase.
- FlatUnattendW10-shopfloor.xml Orders 4 and 5 shrunk to short
invocations of C:\Enrollment\wait-for-internet.ps1 and
C:\Enrollment\migrate-to-wifi.ps1.
- startnet-template.cmd kept in sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FlatUnattendW10-shopfloor.xml Orders 4 and 5 hung forever on
desktops/towers with no WiFi NIC. Two underlying bugs:
1. Order 4 used Test-Connection (ICMP) against login.microsoftonline.us.
Microsoft 365 endpoints do not reliably respond to ICMP, so even
with working TCP 443 internet the ping loop ran forever. Symptom
on a user-facing machine was the PowerShell window permanently
stuck on "Waiting for internet connectivity...".
2. Order 5 unconditionally disabled all wired adapters and waited for
WiFi internet. On a tower with no WiFi NIC this left the machine
completely offline, and the following while loop waited for a WiFi
connection that could never happen.
Fixes:
- Order 4 now emits a 60s interactive prompt asking the user to
connect to the production network (so towers have a window to
unplug PXE and plug into a production port), then uses
Test-NetConnection -Port 443 with a 10 min hard timeout so the
loop always exits.
- Order 5 checks for a physical WiFi adapter first; if none exists
(tower case), it logs "No WiFi adapter - staying on ethernet" and
returns immediately instead of disabling wired. If WiFi is present
and migration times out, wired adapters are re-enabled as a
fallback so the machine is never left offline.
Both orders now use Test-NetConnection -Port 443 instead of
Test-Connection (ICMP) so ICMP-blocking firewalls and non-responsive
cloud endpoints no longer produce infinite waits.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WinPE clients re-imaging the same machine hit "System error 53 -
network path not found" on the second attempt. systemctl restart smbd
did not help; only a full server power cycle cleared the state.
Root cause is kernel nf_conntrack: the default TCP ESTABLISHED timeout
is 5 days (432000s), so a session from the first WinPE run whose
client rebooted abnormally leaves an ASSURED ESTABLISHED entry that
ufw's state-tracking rules then mis-classify the new SYN against.
Fix applied in three layers:
- /etc/sysctl.d/99-pxe-conntrack.conf drops TCP ESTABLISHED timeout
to 1 hour and shortens the half-closed states to 30s each.
- smb.conf gains socket options TCP_NODELAY SO_KEEPALIVE IPTOS_LOWDELAY
plus keepalive = 30 and deadtime = 5. Active sessions refresh the
conntrack timer every 30s via keepalives so they never age out;
dead ones expire in an hour.
- /usr/local/sbin/smb-diag.sh snapshots kernel + Samba state for
remote diagnosis; /usr/local/sbin/smb-soft-reset.sh walks a
progressive recovery (nmbd/smbd restart, conntrack flush, arp
flush, ss -K) as an alternative to power-cycling.
conntrack package added to download-packages.sh and playbook verify
list so the offline .deb bundle ships with it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
Adds a machine number prompt to startnet.cmd after the Standard sub-type
selection. Tech enters the number during the PXE boot process. Defaults
to 9999 if Enter is pressed (existing placeholder behavior).
Written to C:\Enrollment\machine-number.txt alongside pc-type.txt.
Consumers:
00-PreInstall-MachineApps.ps1 - replaces 9999 in UDC InstallArgs with
the entered number, so UDC installs with the correct machine number
from the start (no post-setup Set-MachineNumber needed).
01-eDNC.ps1 - writes the machine number to the DNC\General\MachineNo
registry value during eDNC install.
Configure-PC.ps1 - existing $needsMachineNumber check already skips
the prompt when UDC/eDNC aren't at 9999, so no change needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BUILTIN\Users (Limited RunLevel) can't delete scheduled tasks, so
Unregister-ScheduledTask failed silently and the sync task kept firing
at every logon even after completion.
Fix: write C:\Enrollment\sync-complete.txt on completion. At script
startup in -AsTask mode, check for the marker and exit immediately if
found. The task stays in Task Scheduler but does nothing -- fires at
logon, sees marker, exits in under a second. No visible window.
Manual sync_intune.bat runs (no -AsTask) ignore the marker and always
show the full status display for inventory QR code purposes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chrome (installed by PPKG) now gets the same profile-driven homepage
and startup tabs as Edge. Uses HKLM:\SOFTWARE\Policies\Google\Chrome
with the same policy keys (RestoreOnStartup, RestoreOnStartupURLs,
HomepageLocation, HomepageIsNewTabPage, ShowHomeButton).
Reuses the $startupTabs and $homepageUrl already resolved for Edge
from the PC profile, so both browsers show identical tabs. Skips
cleanly if Chrome isn't installed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three changes to eliminate the redundant startup-item picker during
the imaging chain:
06-OrganizeDesktop.ps1 - new Phase 3: auto-apply startup items
Reads pcProfile.startupItems (or site-wide default) and creates
.lnk files in AllUsers Startup folder. Supports exe, existing, and
url types (same as Configure-PC). Idempotent - skips items that
already exist so manual changes aren't overwritten. Runs during
shopfloor setup finalization, so the tech doesn't need to select
startup items again.
Configure-PC.ps1 - new -MachineNumberOnly switch
When set, skips the entire startup-items section and only shows the
machine number prompt (if UDC/eDNC at 9999). Used by sync_intune
-AsTask after completion. Full startup picker still available when
the tech opens Configure-PC.bat manually from the desktop.
Monitor-IntuneProgress.ps1 - simplified -AsTask completion
After post-reboot DSC complete: unregisters task, launches
Configure-PC -MachineNumberOnly, exits. Tech uses sync_intune.bat
on the desktop to see QR code for inventory purposes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The QR code text was built once at script startup. If the device wasn't
AAD-joined yet, it showed "Device not yet Azure AD joined" forever -
even after Phase 1 checks passed. Now regenerates Build-QRCodeText
when Phase1.AzureAdJoined transitions to true.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Install-ProvisioningPackage triggers an immediate reboot that kills
run-enrollment.ps1 before it can register the sync_intune task or do
any post-install work. BPRT app installs happen on the NEXT boot, not
before the reboot.
Fix: move sync task registration into Run-ShopfloorSetup.ps1, executed
BEFORE calling run-enrollment.ps1. The task is safely registered while
we still have control. Then enrollment installs the PPKG and lets it
reboot. After reboot, BPRT finishes in background, sync task fires at
logon, monitors Intune enrollment (which is independent of BPRT).
Run-ShopfloorSetup.ps1:
- Registers "Shopfloor Intune Sync" @logon task after desktop tool
copies but BEFORE enrollment
- Flushes transcript before calling enrollment (since PPKG reboot
will kill us, ensures log is complete)
- Enrollment is the absolute last call
run-enrollment.ps1:
- Stripped to essentials: find PPKG, rename computer, set OOBE,
Install-ProvisioningPackage
- No BPRT polling (irrelevant - happens after reboot)
- No task registration (already done by caller)
- No shutdown call (PPKG handles it)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The imaging chain needs exactly 2 autologons after the first boot
(sync_intune pre-reboot + post-reboot). Setting to 4 adds 2 extra
reboots of margin for unexpected restarts from Windows Update, PPKG
file operations, or script crashes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Em dashes (U+2014) and arrows (U+2192) break PowerShell 5.1 on
Windows when the file has no UTF-8 BOM -- byte 0x94 gets read as
a right double quote in Windows-1252, silently closing strings
mid-parse. This caused run-enrollment.ps1 to fail on PXE-imaged
machines with "string is missing the terminator" at line 113.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Install-ProvisioningPackage is async — it queues the provisioning engine
and returns immediately. The actual BPRT app installs (Chrome, Office,
Tanium, CyberArk, etc.) run in the background. Without waiting, the
PPKG reboot fires while installs are still in progress, leaving apps
partially installed.
Fix: poll for C:\Logs\BPRT\Remove Staging Locations\Log.txt — the last
BPRT step. When that file exists, all provisioning steps have completed.
Polls every 10 seconds for up to 15 minutes (Office install can be slow).
Progress logged every 30 seconds showing which steps have finished.
If the timeout fires (15 min), logs a warning and proceeds — the SYSTEM
logon task from 06-OrganizeDesktop.ps1 provides self-healing on the next
boot for anything that was incomplete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three final optimization batches:
1. Start-Transcript added to 4 scripts that lacked standalone logging:
04-NetworkAndWinRM.ps1, 05-OfficeShortcuts.ps1, 01-eDNC.ps1,
02-MachineNumberACLs.ps1. Each writes to C:\Logs\SFLD\<name>.log
with append mode. Stop-Transcript added before exit points.
2. preinstall.json: Oracle Client PCTypes changed from ["*"] to
["Standard", "CMM", "Genspect", "Keyence", "WaxAndTrace", "Display"].
Lab Workstations don't need Oracle Client (shopfloor data app
dependency). VC++ redists stay at ["*"] (harmless shared deps).
3. Edge profiles added to all remaining PC types in site-config.json:
CMM, Genspect, Keyence, WaxAndTrace, Standard-Timeclock all get the
standard 3-tab setup (Plant Apps + Homepage + Dashboard) with
homepage = tsgwp00524. Display-Lobby and Display-Dashboard get
Shopfloor Dashboard as both homepage and single tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three optimization batches from the pipeline audit:
1. Shared Update-MachineNumber.ps1 helper (lib/)
Extracts duplicated machine-number update logic from Configure-PC.ps1,
Check-MachineNumber.ps1, and Set-MachineNumber.ps1 into a shared
dot-sourceable helper at Shopfloor/lib/Update-MachineNumber.ps1.
Exports:
Get-CurrentMachineNumber → @{ Udc = $string; Ednc = $string }
Update-MachineNumber -NewNumber <n> [-Site <s>] → @{ UdcUpdated; EdncUpdated; Errors }
All three consumers now dot-source the helper instead of duplicating
~50 lines each. Set-MachineNumber.ps1 also migrated from inline
Get-SiteConfig to dot-sourcing Get-PCProfile.ps1 for consistency.
2. Site-config integration for remaining scripts
Setup-OpenText.ps1: exclude lists (profiles + shortcuts) now read from
site-config.json opentext section, falling back to West Jefferson
defaults. Inline Get-SiteConfig since the script runs from
C:\PreInstall\installers\opentext\ (can't dot-source Get-PCProfile).
00-PreInstall-MachineApps.ps1: after parsing preinstall.json, scans
InstallArgs for "West Jefferson" and replaces with site-config
siteName if different. Inline Get-SiteConfig for same reason.
3. Placeholder type-specific directories
Created skeleton 01-Setup-*.ps1 scripts for all PC types so the
directory structure is in place and Run-ShopfloorSetup's type-specific
loop has something to iterate over:
Genspect/01-Setup-Genspect.ps1
Keyence/01-Setup-Keyence.ps1
WaxAndTrace/01-Setup-WaxAndTrace.ps1
Lab/01-Setup-Lab.ps1
Each logs a "no type-specific apps configured yet" banner and exits.
Fill in app installs when details are finalized; for share-based
installs, copy the CMM/01-Setup-CMM.ps1 pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactored CMM/01-Setup-CMM.ps1 from local-file installer to network-
share-based pattern. CMM apps live on a file share instead of being
pre-staged locally or pulled from Azure Blob.
Framework:
1. Reads share path from site-config.json CMM profile (cmmSharePath),
falls back to West Jefferson default
2. Scans HKLM:\SOFTWARE\GE\SFLD\Credentials\* for a credential entry
whose TargetHost matches the share's server name
3. Mounts the share as S: using net use with the stored creds
4. (Placeholder) Install apps from the share
5. Disconnects the share
The Get-SFLDCredential helper function is generic and will be reused by
Genspect/Keyence scripts when their share-based installs are built. It
matches credentials by TargetHost field, supporting exact match and
domain-suffix matching.
App install blocks are commented out as placeholders — uncomment when
PC-DMIS, CLM License, and other Hexagon app details are finalized.
Added cmmSharePath to site-config.json CMM profile.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a pcProfiles section to site-config.json that lets each PC type (and
optional sub-type) override startupItems, taskbarPins, and desktopApps.
Scripts resolve: pcProfile > site-wide default > hardcoded fallback.
New shared helper: Shopfloor/lib/Get-PCProfile.ps1
Dot-sourced by consuming scripts. Reads pc-type.txt + pc-subtype.txt,
builds a profile key (e.g. "Standard-Machine"), and looks it up in
site-config.json pcProfiles. Exports $siteConfig, $pcType, $pcSubtype,
$profileKey, $pcProfile for the caller to use.
Replaces the inline Get-SiteConfig function that was copy-pasted into
each script. Scripts now do:
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
instead of duplicating the loader.
startnet.cmd changes:
- Added Lab as PC type option (7)
- Standard now has a sub-type menu: Timeclock / Machine
- Display sub-type menu also writes PCSUBTYPE for consistency
- pc-subtype.txt written alongside pc-type.txt when sub-type selected
- site-config.json copied from enrollment share to W:\Enrollment\
site-config.json v2.0:
- New pcProfiles section with profiles for:
Standard-Timeclock, Standard-Machine, CMM, Genspect, Keyence,
WaxAndTrace, Lab, Display-Lobby, Display-Dashboard
- CMM/Genspect/Keyence/WaxAndTrace profiles have TODO comments for
type-specific apps (placeholder with WJ Shopfloor baseline only)
- Lab/Display profiles have empty startupItems and desktopApps
- Top-level startupItems/taskbarPins/desktopApps remain as site-wide
defaults (used when no profile matches)
Updated scripts:
06-OrganizeDesktop.ps1 - desktopApps from profile > site > hardcoded
07-TaskbarLayout.ps1 - taskbarPins from profile > site > hardcoded
08-EdgeDefaultBrowser.ps1 - uses shared profile loader
Configure-PC.ps1 - startupItems from profile > site > hardcoded
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New site-config.json file at C:\Enrollment\ (staged by startnet.cmd from
the enrollment share) contains all West Jefferson-specific values that were
previously hardcoded across 7 scripts. To deploy at a different GE site,
clone site-config.json and change the values - scripts need zero changes.
Config schema (v1.0):
siteName / siteNameCompact - UDC/eDNC site args
urls{} - Edge startup tab fallback URLs
edgeStartupTabs[] - ordered tab list with .url file basenames
opentext{} - excluded .hep profiles and .lnk shortcuts
startupItems[] - Configure-PC toggle list (exe/existing/url)
taskbarPins[] - 07-TaskbarLayout pin order with lnk paths
desktopApps[] - 06-OrganizeDesktop Phase 2 app list
Every script uses the same inline Get-SiteConfig helper that reads the
JSON and returns $null if missing/corrupt. All consumers fall back to the
current hardcoded West Jefferson defaults when $siteConfig is null, so
PXE servers without a site-config.json continue working identically.
Scripts updated:
06-OrganizeDesktop.ps1 - desktopApps array from config
07-TaskbarLayout.ps1 - pinSpec array from config
08-EdgeDefaultBrowser.ps1 - startup tab loop from config
Configure-PC.ps1 - startup items + site name from config
Check-MachineNumber.ps1 - site name from config
Set-MachineNumber.ps1 - site name from config
01-eDNC.ps1 - siteName + siteNameCompact from config
startnet.cmd - copies site-config.json from enrollment share
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related fixes from the pipeline audit:
1. Stage-Dispatcher race condition (critical):
Run-ShopfloorSetup.ps1 called shutdown /r /t 10 and the dispatcher
had to write the next stage + register RunOnce within that 10-second
window. If disk I/O was slow, the reboot fired before RunOnce was
registered, and the chain broke.
Fix: dispatcher now cancels Run-ShopfloorSetup's pending reboot
(shutdown /a) immediately after it returns, then advances the stage
and registers RunOnce with no time pressure, then initiates its own
shutdown /r /t 5.
2. Dispatcher owns all reboots:
Run-ShopfloorSetup.ps1 now checks the -FromDispatcher flag at the
end. When called by the dispatcher, it schedules shutdown /r /t 30
as a safety net (the dispatcher cancels it immediately). When called
standalone (manual run or legacy FirstLogonCommands), it reboots
directly with /t 10 as before.
This means the dispatcher has full control over the reboot lifecycle:
cancel -> advance stage -> register RunOnce -> reboot. No racing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The stage-file gate in Run-ShopfloorSetup.ps1 would fire even when
called by Stage-Dispatcher.ps1 (because the stage file still contains
"shopfloor-setup"), causing an infinite exit loop.
Fix: Run-ShopfloorSetup now accepts -FromDispatcher switch. The gate
only fires when the switch is absent (i.e. when called by the unattend's
FirstLogonCommands). Stage-Dispatcher passes -FromDispatcher when
invoking Run-ShopfloorSetup, bypassing the gate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Run-ShopfloorSetup.ps1 now checks for C:\Enrollment\setup-stage.txt at
the very top. If the stage file exists (written by run-enrollment.ps1),
the script exits immediately with "deferring to Stage-Dispatcher.ps1
on next logon". This prevents the unattend's FirstLogonCommands chain
from running Run-ShopfloorSetup in the same session as run-enrollment,
which was bypassing the entire staged reboot chain.
Without this gate:
FirstLogonCommand #1: run-enrollment.ps1 (sets stage file + RunOnce)
FirstLogonCommand #2: Run-ShopfloorSetup.ps1 (runs immediately, ignoring stage)
PPKG reboot fires after both complete
Next boot: dispatcher has nothing to do (Run-ShopfloorSetup already ran)
With the gate:
FirstLogonCommand #1: run-enrollment.ps1 (sets stage file + RunOnce)
FirstLogonCommand #2: Run-ShopfloorSetup.ps1 (sees stage file, exits)
PPKG reboot fires
Next boot: RunOnce fires dispatcher, reads "shopfloor-setup", runs
Run-ShopfloorSetup properly (stage file deleted by gate on re-entry)
Also: Monitor-IntuneProgress.ps1 RetriggerMinutes bumped from 3 to 5.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the single-session "cancel PPKG reboot and cram everything into
one autologon" flow with a staged chain where each reboot advances to the
next step automatically. The technician touches the keyboard 3 times total
(UNPLUG prompt, Y to reboot, Configure-PC selections).
New Stage-Dispatcher.ps1:
Reads C:\Enrollment\setup-stage.txt and chains through:
shopfloor-setup -> sync-intune -> configure-pc
Each stage re-registers HKLM RunOnce so the dispatcher fires again on
the next logon. Stage file is deleted when the chain completes.
Transcript logged to C:\Logs\SFLD\stage-dispatcher.log.
Stage "shopfloor-setup": runs Run-ShopfloorSetup.ps1 (which reboots via
shutdown /r /t 10). Dispatcher advances stage to sync-intune in the
~10 second window before the machine goes down, re-registers RunOnce.
Stage "sync-intune": launches Monitor-IntuneProgress.ps1 -Unattended.
Exit 2 (pre-reboot done, user confirmed): dispatcher re-registers
RunOnce and initiates shutdown /r /t 5. Stage stays at sync-intune so
the monitor picks up post-reboot state on next boot.
Exit 0 (post-reboot install complete): dispatcher chains directly to
Configure-PC.ps1 in the same session, then deletes the stage file.
Stage "configure-pc": runs Configure-PC.ps1 and deletes the stage file.
Fallback entry point if the post-reboot chain was interrupted.
Modified run-enrollment.ps1:
Removed the shutdown /a that canceled the PPKG reboot. Instead writes
setup-stage.txt = "shopfloor-setup" and registers RunOnce for the
dispatcher. PPKG reboot fires naturally (handles PendingFileRename
operations like Zscaler rename and PPKG self-cleanup). Now tracked in
the git repo at playbook/shopfloor-setup/run-enrollment.ps1.
Modified Monitor-IntuneProgress.ps1:
New -Unattended switch. When set:
Invoke-SetupComplete exits 0 without waiting for keypress.
Invoke-RebootPrompt exits 2 without prompting or rebooting (dispatcher
handles both). Manual sync_intune.bat usage (no flag) unchanged.
RetriggerMinutes bumped from 3 to 5 (user request).
Modified startnet.cmd:
Now also copies Stage-Dispatcher.ps1 from the PXE server to
W:\Enrollment\Stage-Dispatcher.ps1 alongside run-enrollment.ps1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Don't prompt the user to reboot until the enrollment pipeline has
finished its pre-reboot work. Previously Test-RebootState fired as
soon as DSCDeployment.log showed "completed", even if Phase 1 (Identity)
or Phase 2 (SFLD config) checks were still in progress.
Now the reboot prompt requires ALL of these to be green in the snapshot:
Phase 1: AzureAdJoined, IntuneEnrolled, EmTaskExists, PoliciesArriving
Phase 2: SfldRoot, FunctionOk, SasTokenOk
Phase 3: DeployLogExists, DeployComplete
This prevents the edge case where DSCDeployment.log completes but the
user reboots before Intune policies have fully landed, which could leave
the post-reboot DSC install phase without the SAS token or function
assignment it needs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. UDC JSON ACL: set on directory C:\ProgramData\UDC\ with
ContainerInherit+ObjectInherit instead of the file. UDC_Setup.exe
gets killed by KillAfterDetection before UDC.exe creates
udc_settings.json, so the file doesn't exist at ACL-grant time.
Directory-level ACL with inheritance covers any file created later.
2. Set-MachineNumber.ps1 auto-running: the type-specific loop's
Get-ChildItem -Filter "*.ps1" picked up the desktop tool alongside
the numbered installer scripts. Added Where-Object { $_.Name -match
'^\d' } so only numbered-prefix scripts (01-eDNC, 02-ACLs) run.
3. WJ Shopfloor copy-to-self: Phase 1 sweep moved WJ Shopfloor.lnk
into Shopfloor Tools\, then Phase 2's Find-ExistingLnk found it
there and tried to Copy-Item to the same path. Now checks if
resolved source path == destination and prints "exists: (already
in Shopfloor Tools)" instead of erroring.
4. NTLARS missing from taskbar pins: the $pinSpec entry was never
added to 07-TaskbarLayout.ps1 despite the comment update. Added
between eDNC and Defect_Tracker in pin order.
5. shutdown /a stderr noise: 15+ red "Unable to abort system shutdown"
lines in the transcript from shutdown.exe writing to stderr when no
shutdown is pending. Changed all occurrences in Run-ShopfloorSetup,
00-PreInstall-MachineApps to: cmd /c "shutdown /a 2>nul" *>$null
which suppresses both native stderr and PS error stream.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New tools:
Configure-PC.bat/.ps1 - Interactive desktop 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 (updates UDC JSON + eDNC registry,
restarts UDC.exe with new args).
2. Auto-startup toggle: pick which apps start at user logon from a
numbered list (UDC, eDNC, Defect Tracker, WJ Shopfloor, Plant Apps).
Creates/removes .lnk files in AllUsers Startup folder. Toggle UI
shows [ON]/[ ] state, safe to re-run anytime. Plant Apps URL
resolved from .url file at runtime with hardcoded fallback to
https://mes-wjefferson.apps.lr.geaerospace.net/run/...
3. Item 6 in the toggle list: register/unregister a "Check Machine
Number" logon task for standard (non-admin) users. When enabled,
the task fires at every logon, checks for 9999, pops an InputBox
if found, updates both apps, then unregisters itself on success.
Check-MachineNumber.ps1 - The logon task script. Runs as the logged-in
user (needs GUI for InputBox), not SYSTEM. Writing to ProgramData + HKLM
is possible because 02-MachineNumberACLs.ps1 pre-grants BUILTIN\Users
write access on the two specific targets during imaging.
02-MachineNumberACLs.ps1 - Standard type-specific script (runs after
01-eDNC.ps1). Opens C:\ProgramData\UDC\udc_settings.json for Users:Modify
and HKLM:\...\GE Aircraft Engines\DNC\General for Users:SetValue. Narrow
scope, not blanket admin.
Execution order fixes in Run-ShopfloorSetup.ps1:
The dispatcher now has two lists: $skipInBaseline (scripts NOT run in the
alphabetical baseline loop) and $runAfterTypeSpecific (scripts run
explicitly after type-specific scripts complete). This fixes the bug where
06/07 ran before 01-eDNC.ps1 installed DnC, so eDNC/NTLARS shortcuts were
silently skipped.
New execution order:
Baseline: 00-PreInstall, 04-NetworkAndWinRM (skipping 05-08 + tools)
Type-specific: 01-eDNC, 02-MachineNumberACLs
Finalization: 06-OrganizeDesktop, 07-TaskbarLayout
06 internally calls 05 (Office shortcuts, Phase 0) and 08 (Edge config,
Phase 4) as sub-phases, so they also benefit from running late. Office
isn't installed until after the first reboot (ppkg streams C2R), so 05
no-ops at imaging time but succeeds when 06's SYSTEM logon task re-runs
it on the second boot. 08 resolves startup-tab URLs from .url files
delivered by DSC (even later); same self-heal via the logon task.
Other fixes in this commit:
- OpenText Setup-OpenText.ps1 Step 4: exclude WJ_Office.lnk, IBM_qks.lnk,
mmcs.lnk desktop shortcuts (matching the Step 3 .hep profile exclusion
from the previous commit). Removes stale copies from prior installs.
- 05-OfficeShortcuts.ps1: widened Office detection to 6 path variants
covering C2R + MSI + Office15/16, with diagnostic output on miss.
- 06-OrganizeDesktop.ps1: removed Phase 3 (desktop-root pin copies for
eDNC/NTLARS) so shortcuts live in Shopfloor Tools only, not duplicated
at root. Emptied $keepAtRoot. Added Phase 0 (call 05) and Phase 4
(call 08). Lazy folder creation + empty-folder cleanup. Scheduled task
now runs as SYSTEM (was BUILTIN\Users with Limited which failed the
admin check). Added NTLARS to 07's taskbar pin list.
- 08-EdgeDefaultBrowser.ps1: Plant Apps URL fallback hardcoded from
device-config.yaml.
- All new scripts have Start-Transcript logging to C:\Logs\SFLD\ with
timestamps and running-as identity.
- Run-ShopfloorSetup.ps1: Start-Transcript + Stop-Transcript wrapping
entire dispatcher run, writes to C:\Logs\SFLD\shopfloor-setup.log.
Configure-PC.bat added to SupportUser desktop copy list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three new baseline scripts that run during shopfloor imaging to clean up
the end-user Public Desktop. Before this, Azure AD users logged into a
shopfloor PC and saw 20+ loose shortcuts at the desktop root (Office
apps, OpenText sessions, WJ web portals, DNC utilities, Defect Tracker,
plus .url files for every intranet page) with no organization. End users
couldn't find anything.
06-OrganizeDesktop.ps1 - Single source of truth for Public Desktop layout
Phase 1: sweeps loose shortcuts at the desktop root into three category
folders - Office\, Shopfloor Tools\, Web Links\ - by filename regex,
extension, and .lnk target resolution. Allowlists eDNC.lnk and
NTLARS.lnk to stay at root since end users click them too often.
Unknown items are left at the root on purpose (never delete).
Phase 2: materializes specific app shortcuts into Shopfloor Tools\.
UDC / eDNC / NTLARS are built fresh from their .exe paths; WJ
Shopfloor and Defect_Tracker are MSI-advertised (empty TargetPath,
Darwin descriptor) so we copy the existing .lnk from wherever it
lives via a multi-location lookup. Each entry is conditional on its
source being present - script runs cleanly on PC types without DnC.
Phase 3: drops eDNC.lnk and NTLARS.lnk at desktop root from the
Shopfloor Tools\ copies, so end users have both a folder version
and a quick-access root version.
Phase 4: registers an "Organize Public Desktop" scheduled task that
re-runs phase 1 at every logon. Shortcuts added later by DSC /
Intune / msiexec get filed automatically without another imaging
pass. Admin check at the top, -ErrorAction Stop on Register-
ScheduledTask and directory creation so failures are caught
instead of printing false success.
07-TaskbarLayout.ps1 - Minimal taskbar pinner
Checks which .lnk files 06 created in Shopfloor Tools\, then writes
LayoutModification.xml to the Default User profile with taskbar pins
in order: Edge, WJ Shopfloor, UDC, eDNC, Defect_Tracker. No shortcut
creation in this script - all shortcut management lives in 06.
Missing .lnks are skipped (PC types without DnC just get fewer pins).
Applies on first logon of new user profiles (Azure AD users after
enrollment). Existing profiles don't re-read Default User - Windows
design limitation since 1703, no programmatic fix.
08-EdgeDefaultBrowser.ps1 - Edge as default browser + startup tabs
Motivated by the ppkg installing Chrome alongside Edge: new Azure AD
users hit a "Choose your default app" picker on first URL click
because nothing is marked default. Two layers:
1. dism /Online /Import-DefaultAppAssociations:<xml> writes an XML
with Edge ProgIds for http/https/.htm/.html/.pdf/.svg/.webp into
the Default User profile template. New profiles inherit.
2. HKLM:\SOFTWARE\Policies\Microsoft\Windows\System\
DefaultAssociationsConfiguration registry value (the "Set a
default associations configuration file" GPO) points at the same
XML so Windows re-applies on every logon, catching Windows-update
defaults-reset cases.
Leaves Chrome installed, just not the default URL handler.
Also sets Edge startup tabs via machine-wide policies under
HKLM:\SOFTWARE\Policies\Microsoft\Edge:
RestoreOnStartup = 4 (open specific URLs)
RestoreOnStartupURLs = Plant Apps, WJ Shop Floor Homepage, Shopfloor
Dashboard (tab order per spec)
HomepageLocation = first tab (Plant Apps)
HomepageIsNewTabPage = 0
ShowHomeButton = 1
URLs are resolved dynamically from the .url files on the Public
Desktop (or Web Links\ after the sweep), so if WJDT changes a URL
later the script picks it up without a code change. Fallbacks are
hardcoded for the two portals we have URLs memorized for; Plant Apps
has no fallback and will be skipped if the .url file is missing.
Test workflow: admin-check in all three scripts fails fast on non-
elevated runs instead of spamming half-successful Access Denied output
like the first draft did.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per West Jefferson request, those three connection profiles aren't used
on shopfloor PCs and just clutter the HostExplorer session picker.
They stay in the bundled source tree (dependencies/opentext/Profile/)
for rollback, we just don't copy them into the runtime destinations.
Implementation:
- New optional Exclude list on $contentMap entries
- Copy-HummingbirdContent filters files through Exclude before copying
- Also removes any stale excluded files from the destination up-front,
so a PC that got them from an older install gets cleaned up on
re-deploy (defensive - no production PC has the 15.0.SP1.2 marker
yet so this won't actually fire in practice)
- NO version bump: 15.0.SP1.2 stays, per explicit request. First
imaging run picks up the new logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Long debugging round on the shopfloor test PC with several overlapping
bugs. This commit folds all the fixes together.
sync_intune.bat
- Slim down to an elevation thunk that launches a NEW elevated PS
window via Start-Process -Verb RunAs (with -NoExit so the window
doesn't vanish on error). All UI now lives in the PS monitor, not
mixed into the cmd launcher.
- Goto-based control flow. Earlier version had nested if (...) blocks
with literal parens inside echo lines (e.g. "wrappers (Install-eDNC,
...etc)."); cmd parses if-blocks by counting parens character-by-
character, so the ")" in "etc)." closed the outer block early and
the leftover "." threw ". was unexpected at this time.", crashing
the elevated cmd /c window before pause ran.
- Multi-location Monitor-IntuneProgress.ps1 lookup so the user's
quick-test workflow (drop both files on the desktop) works without
manually editing the hardcoded path. Lookup order:
1. %~dp0lib\Monitor-IntuneProgress.ps1
2. %~dp0Monitor-IntuneProgress.ps1
3. C:\Users\SupportUser\Desktop\Monitor-IntuneProgress.ps1
4. C:\Enrollment\shopfloor-setup\Shopfloor\lib\Monitor-IntuneProgress.ps1
- Prints "Launching: <path>" as its first line so you can see which
copy it actually loaded. This caught a bug where a stale desktop
copy was shadowing the canonical file via fallback #2.
Set-MachineNumber.bat
- Same multi-location lookup pattern. Old version used
%~dp0Set-MachineNumber.ps1 and bombed when the bat was copied to
the desktop without its .ps1 sibling.
- Goto-based dispatch, no nested parens, for the same parser reason.
Monitor-IntuneProgress.ps1
- Start-Transcript at the top, writing to C:\Logs\SFLD\ (falls back
to %TEMP% if C:\Logs\SFLD isn't writable yet) with a startup banner
including a timestamp. Every run leaves a captured trace.
- Main polling loop wrapped in try/catch/finally. Unhandled exceptions
print a red report with type, message, position, and stack trace,
then block on Wait-ForAnyKey so the window can't auto-close on a
silent crash.
- Console window resize at startup via $Host.UI.RawUI.WindowSize /
BufferSize, wrapped in try/catch (Windows Terminal ignores it, but
classic conhost honors it).
- Clear-KeyBuffer / Read-SingleKey / Wait-ForAnyKey helpers. Drain any
buffered keystrokes from the polling loop before each prompt so an
accidental keypress can't satisfy a pause prematurely.
- Invoke-SetupComplete / Invoke-RebootPrompt final-state handlers.
The REBOOT REQUIRED branch now shows a yellow 3-line header, a
four-line explanation, and a cyan "Press Y to reboot now, or N to
cancel:" prompt via Read-SingleKey @('Y','N'). Y triggers
Restart-Computer -Force (with shutdown.exe fallback), N falls
through to Wait-ForAnyKey.
- Display order: status table FIRST, QR LAST. The cursor ends below
the QR so the viewport always follows it - keeps the QR on screen
regardless of window height. Works on both classic conhost and
Windows Terminal (neither reliably honors programmatic resize).
- Half-block QR renderer: walks QRCoder's ModuleMatrix directly and
emits U+2580 / U+2584 / U+2588 / space, one output line per two
matrix rows. Halves the rendered height vs AsciiQRCode full-block.
Quiet zone added manually via $pad=4 since QRCoder's ModuleMatrix
doesn't include one. Trade-off: may not be perfectly square on all
fonts, but the user accepted that for the smaller footprint after
multiple iterations comparing full-block vs half-block vs PNG popup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related fixes from a debugging round on the test PC:
1. PreInstall runner: detection-during-install kill is now opt-in via
"KillAfterDetection: true" on JSON entries that need it. Old behavior
killed any installer as soon as its detection passed - which broke
Oracle: Oracle creates its registry key partway through install,
the runner detected it at the 25s poll, killed msiexec mid-install,
and msiserver was still doing rollback when the next install (VC++
2008) started - so VC++ 2008 hit ERROR_INSTALL_ALREADY_RUNNING
(1618). Only UDC needs the detection-kill (its installer spawns a
hidden WPF window and never exits). Other installers exit cleanly
on their own and shouldn't be killed.
2. Track Setup-OpenText scripts in git. The bundled OpenText install
scripts (Setup-OpenText.ps1, Setup-OpenText.cmd, version.txt) live
at runtime in /home/camp/pxe-images/main/dependencies/opentext/
alongside the binary install files (~106 MB of MSI/CAB/MSP/MST plus
profile content). The binaries stay outside git but the script
logic and version stamp are mirrored into playbook/preinstall/
opentext/ here so git history captures changes to the install
logic and version bumps. README.md explains the workflow.
Latest Setup-OpenText.ps1 includes:
- $SourceDir default moved into script body (PowerShell evaluates
param([string]$X = $PSScriptRoot) defaults at parameter-binding
time, when $PSScriptRoot may not yet be populated, so the
default came out as empty string and Join-Path crashed)
- Logging set up FIRST so any startup error gets captured
- REBOOT=ReallySuppress dropped from both msiexec calls (base MSI
and SP1 patch) - OpenText installs shell extensions that hook
explorer.exe, and Restart Manager closes explorer to replace
the shell DLLs. With REBOOT=ReallySuppress, RM closed explorer
but interpreted the relaunch as a "reboot action" and refused
to do it, leaving the user with no desktop. /norestart on its
own prevents the actual Windows reboot but lets RM cleanly
close-and-relaunch explorer mid-install.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the runner runs a Type:MSI install it injects /L*v <log> and tails
that log on failure to show what actually went wrong. Type:EXE installs
had no equivalent - if Setup-OpenText.cmd or any other EXE wrapper
failed, the installlog just showed "Exit code 1 - FAILED" with no clue
what happened inside.
Adds an optional LogFile field to JSON entries. When present on a
Type:EXE entry, the runner:
- Logs "Installer log: <path>" before launching the installer
- On failure, tails the last 30 lines of that file into the runner
log (same pattern as the MSI verbose log scan)
Wired up on the OpenText entry to point at C:\Logs\PreInstall\Setup-
OpenText.log (which Setup-OpenText.ps1 already writes itself). Other
EXE entries can opt in by adding their own LogFile field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Run-ShopfloorSetup.ps1 line 46-47 does:
Get-ChildItem -Path $baselineDir -Filter "*.ps1" -File | Sort-Object Name
foreach ($script in $scripts) { & $script.FullName }
This picks up EVERY *.ps1 in Shopfloor\ and runs it as a baseline
script. Last commit (66d13d8) put Monitor-IntuneProgress.ps1 in that
same directory, which means the dispatcher was running it as the LAST
baseline script (M sorts after 00/04/05). The monitor is an infinite
poll loop that never returns until the SFLD lifecycle is complete -
so the dispatcher hung there forever, and Standard\01-eDNC.ps1 and
Standard\Set-MachineNumber.ps1 never ran.
Symptoms in the test run:
- 00-PreInstall-MachineApps.ps1 ran (10 installed, 1 OpenText fail)
- 04-NetworkAndWinRM.ps1 ran silently
- 05-OfficeShortcuts.ps1 ran silently
- Monitor-IntuneProgress.ps1 started (Clear-Host + status table) and
hung in its main loop
- eDNC + Set-MachineNumber never ran
Fix: move Monitor-IntuneProgress.ps1 into Shopfloor\lib\ so the
dispatcher's non-recursive Get-ChildItem doesn't see it. Update
sync_intune.bat's MONITOR path to the new location, and add a
comment explaining WHY the monitor lives under lib\ to prevent this
mistake from being repeated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the 3-step pass/fail polling that lived in the .bat with a
PowerShell monitor that renders a full status table for the SFLD
enrollment lifecycle and handles the pre-reboot -> reboot -> post-reboot
transition explicitly.
Three structural problems with the old script:
1. Step 3 ("SFLD - Consume Credentials task exists") fired too early.
The task is created by SetupCredentials.log around 08:52 in the
pre-reboot phase, NOT post-reboot, so passing all 3 gates didn't
actually mean "fully done" - it just meant "credential setup ran".
2. No detection of the pre-reboot -> reboot -> post-reboot transition.
The script never read DSCDeployment.log, so it couldn't tell the
user "you need to reboot now to start the install phase". A device
stuck waiting for reboot was indistinguishable from one still
syncing.
3. No visibility into Phase 4 (per-script wrappers like Install-eDNC,
Install-UDC, Install-VCRedists, Install-OpenText). When something
hung you had to manually grep C:\Logs\SFLD\.
New layout:
sync_intune.bat - thin launcher (~50 lines): self-elevate, invoke
Monitor-IntuneProgress.ps1, branch on exit code
(0 = done / 2 = reboot needed / else = error).
Monitor-IntuneProgress.ps1 - the actual monitor (~340 lines):
- 5-phase status table (Identity / SFLD config / DSC deployment +
install / Custom scripts / Final) updated every 30s via Clear-
Host + redraw, with the QR code anchored at the top.
- Phase 4 auto-discovers custom scripts by parsing DSCInstall.log
for "Downloading script: <name>" lines AND scanning C:\Logs\SFLD\
Install-*.log files - so Display PCs running entirely different
scripts surface their own list automatically without hardcoding.
Statuses: pending / running / done / failed (mtime + tail-based).
- Boot-loop-safe reboot detection via Test-RebootState: only signals
'needed' if DSCDeployment.log was modified AFTER LastBootUpTime.
Once we've rebooted past it, just waits for DSCInstall.log.
- Caches monotonic Phase 1 indicators (AzureAdJoined, IntuneEnrolled,
EnterpriseMgmt task) so dsregcmd /status (slow ~1-2s) only runs
until the flag flips true, not on every poll.
- Triggers Intune sync at startup, re-triggers every 3 minutes (was
every 15 seconds in the old loop, which actively interrupted
in-flight CSP work).
Exit codes consumed by sync_intune.bat:
0 - DSCInstall.log shows "Installation completed successfully"
2 - DSCDeployment.log shows "Deployment completed successfully" AND
the deploy log is newer than LastBootUpTime (= reboot needed)
1 - error
Detection markers (decoded from a captured run at /home/camp/pxe-images/
Logs/ - see comment block at top of Monitor-IntuneProgress.ps1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Office Click-to-Run installs the binaries when an Office-bearing ppkg
is selected (e.g. GCCH_Prod_SFLD_StdOffice-x86_*) but doesn't create
desktop shortcuts - operators only see Office in the Start Menu's
Microsoft 365 folder. This baseline script fills that gap.
Self-detects Office by EXE existence at C:\\Program Files\\Microsoft
Office\\root\\Office16\\ or the (x86) equivalent. No Office found =
silent no-op, so it's safe to run on every PC type (Display kiosks,
Wax/Trace, Keyence, etc.) without needing a per-type filter.
Creates Excel.lnk / Word.lnk / PowerPoint.lnk 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 created
on the device (Azure AD operator logons after enrollment)
Numbered 05- so it runs after 00-PreInstall and 04-NetworkAndWinRM
in the Shopfloor baseline sequence. Idempotent - WScript.Shell's
CreateShortcut overwrites existing .lnks each run.
Outlook / OneNote / Access / Publisher intentionally not shortcutted
(scope decision; can be added by extending the $officeApps array).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>