sweep: pre-existing drift + matrix UDC entry + ignore 142MB EXE
Bundles drift left uncommitted from prior sessions and the UDC matrix
verify entry added today.
Drift items (all per session-progress.md, completed in earlier sessions
but never staged):
- playbook/check-bios.cmd (deleted, moved to BIOS/check-bios.cmd)
- playbook/migrate-to-wifi.ps1 (made no-op 2026-04-24 after the dnsmasq
no-gateway fix removed the wired-NIC race that motivated it)
- playbook/preinstall/oracle/Install-Oracle11r2.cmd (post-OUI .ora copy
added 2026-04-24)
- playbook/preinstall/oracle/tnsnames.ora (live tnsnames, 469 KB,
deployed alongside the wrapper 2026-04-24)
- playbook/pxe_server_setup.yml (dnsmasq dhcp-option=3,6 commented,
Oracle .ora deploy task added 2026-04-24)
- playbook/shopfloor-setup/BIOS/{check-bios.cmd, models.txt} (BIOS
detection refinements)
- playbook/shopfloor-setup/Shopfloor/Force-Lockdown.bat
- playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1
- playbook/shopfloor-setup/Shopfloor/SetShopfloorAutoLogon.bat (new)
- playbook/shopfloor-setup/Shopfloor/09-Install-PrinterInstallerMap.ps1
(new, places PrinterInstallerMap.exe + Public Desktop shortcut at
imaging time; manifest entry self-heals on tamper)
- playbook/shopfloor-setup/Shopfloor/lib/Show-IntuneDeviceQR.ps1 (new,
standalone QR rendering for site that wanted just that piece)
- playbook/shopfloor-setup/gea-shopfloor-collections/{Install-eMxInfo.cmd.template,
Restore-UDCData.ps1} (these were uncommitted in pre-rename Standard/;
git mv didn't catch them because they were untracked at the time)
- docs/shopfloor-machine-imaging-guide.md (operator-facing how-to)
Matrix:
- common.test/matrix.json: add UDC verify entry to gea-shopfloor-collections
row. Surfaces UDC silent-install issue (item H pending) instead of
letting it pass silently.
.gitignore:
- PrinterInstallerMap.exe (142 MB) excluded. Track via LFS or stage on
PXE server only - too big for regular git history. Untouched on disk
so existing local copy still works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# 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
|
||||
@@ -60,6 +60,18 @@ echo.
|
||||
PowerShell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%"
|
||||
set RC=%errorLevel%
|
||||
|
||||
REM On success, write a marker that Monitor-IntuneProgress.ps1 (Phase 6 /
|
||||
REM Get-LockdownState) recognizes as authoritative. Manual lockdown via
|
||||
REM sfld_autologon.ps1 only flips Winlogon; the Intune Remediation log
|
||||
REM never gets the "Autologon set for ShopFloor" line because Detection
|
||||
REM now passes and Remediation never re-runs. The marker tells the monitor
|
||||
REM to treat (Winlogon registry matches + marker present) as Complete.
|
||||
if "%RC%"=="0" (
|
||||
if not exist "C:\Enrollment" mkdir "C:\Enrollment"
|
||||
> "C:\Enrollment\force-lockdown-applied.txt" echo %DATE% %TIME%
|
||||
echo Marker written: C:\Enrollment\force-lockdown-applied.txt
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ============================================================
|
||||
echo Lockdown script exit code: %RC%
|
||||
|
||||
12
playbook/shopfloor-setup/Shopfloor/SetShopfloorAutoLogon.bat
Executable file
12
playbook/shopfloor-setup/Shopfloor/SetShopfloorAutoLogon.bat
Executable file
@@ -0,0 +1,12 @@
|
||||
REM 1) Check if we are running elevated (admin)
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo Requesting elevation...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass ^
|
||||
-Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
||||
goto :EOF
|
||||
)
|
||||
|
||||
|
||||
|
||||
"C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -File "C:\Program Files\Sysinternals\sfld_autologon.ps1"
|
||||
@@ -303,34 +303,91 @@ function Get-LockdownState {
|
||||
# Autologon_Remediation.log - "Autologon set for ShopFloor user ..."
|
||||
# Autologon_Detection.log - "... matches the expected value: 1"
|
||||
#
|
||||
# These are the TRUE end-of-chain signals. The DefaultUserName flip and
|
||||
# admin rename that we previously checked land at PPKG time (too early);
|
||||
# the IME logs only appear after Intune enrollment + category + DSC +
|
||||
# Remediation cycle, which is the actual lockdown completion.
|
||||
# IME logs are append-only - they never rotate or clear. Earlier logic
|
||||
# used `-match` against the whole raw file, which stayed true forever
|
||||
# once success was ever recorded, even if the policy later changed and
|
||||
# the current state drifted. The fix below:
|
||||
# (a) Walks lines from END to find the MOST RECENT outcome per log
|
||||
# (latest attempt wins, stale successes are ignored)
|
||||
# (b) Cross-checks Winlogon registry for ground truth - catches the
|
||||
# case where IME logs say success but the reg state got rolled
|
||||
# back. All three must agree for Complete to be true.
|
||||
$imeLogs = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs'
|
||||
|
||||
# --- Remediation: scan from tail for the LATEST result line
|
||||
$remLog = Join-Path $imeLogs 'Autologon_Remediation.log'
|
||||
$remDone = $false
|
||||
if (Test-Path $remLog) {
|
||||
try {
|
||||
$content = Get-Content $remLog -Raw -ErrorAction Stop
|
||||
$remDone = ($content -match 'Autologon set for ShopFloor')
|
||||
$lines = Get-Content $remLog -ErrorAction Stop
|
||||
for ($i = $lines.Count - 1; $i -ge 0; $i--) {
|
||||
$ln = $lines[$i]
|
||||
if ($ln -match 'Autologon set for ShopFloor') {
|
||||
$remDone = $true
|
||||
break
|
||||
}
|
||||
if ($ln -match 'Autologon could not be set|Remediation failed|FATAL|Exception') {
|
||||
$remDone = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# --- Detection: scan from tail for the LATEST "matches the expected value: <N>"
|
||||
$detLog = Join-Path $imeLogs 'Autologon_Detection.log'
|
||||
$detDone = $false
|
||||
if (Test-Path $detLog) {
|
||||
try {
|
||||
$content = Get-Content $detLog -Raw -ErrorAction Stop
|
||||
$detDone = ($content -match 'matches the expected value:\s*1')
|
||||
$lines = Get-Content $detLog -ErrorAction Stop
|
||||
for ($i = $lines.Count - 1; $i -ge 0; $i--) {
|
||||
if ($lines[$i] -match 'matches the expected value:\s*(\d+)') {
|
||||
$detDone = ($matches[1] -eq '1')
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# --- Ground truth: does Winlogon actually reflect ShopFloor autologon?
|
||||
# This catches the case where IME logs report historical success but the
|
||||
# reg state has since been changed back (manual tech intervention, policy
|
||||
# rollback, GPO override, etc).
|
||||
$registryMatches = $false
|
||||
try {
|
||||
$wl = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -ErrorAction Stop
|
||||
$registryMatches = (
|
||||
[string]$wl.AutoAdminLogon -eq '1' -and
|
||||
$wl.DefaultUserName -and
|
||||
$wl.DefaultUserName -like 'ShopFloor*'
|
||||
)
|
||||
} catch {}
|
||||
|
||||
# --- Manual override marker written by Force-Lockdown.bat.
|
||||
# When a tech runs sfld_autologon.ps1 directly (vendor-documented escape
|
||||
# hatch when the Intune push hasn't applied within ~30 minutes), only
|
||||
# Winlogon flips - the IME Remediation log never gets a new success
|
||||
# entry because Detection now passes and Remediation isn't invoked.
|
||||
# Without this marker, the strict (rem AND det AND reg) gate stays
|
||||
# false forever even though the device is fully locked down.
|
||||
$forceMarker = 'C:\Enrollment\force-lockdown-applied.txt'
|
||||
$forceApplied = (Test-Path -LiteralPath $forceMarker)
|
||||
|
||||
return @{
|
||||
RemediationApplied = $remDone
|
||||
DetectionPassed = $detDone
|
||||
Complete = ($remDone -and $detDone)
|
||||
RegistryMatches = $registryMatches
|
||||
ForceApplied = $forceApplied
|
||||
# Two paths to Complete:
|
||||
# 1. Normal Intune-driven: all three signals present (closes the
|
||||
# append-only-log false-positive AND rollback-without-log case).
|
||||
# 2. Manual escape hatch: marker file + ground-truth registry
|
||||
# match. Marker is the audit-trail substitute for the missing
|
||||
# Remediation log entry; registry is still the gate.
|
||||
Complete = (
|
||||
($remDone -and $detDone -and $registryMatches) -or
|
||||
($forceApplied -and $registryMatches)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,11 +637,21 @@ function Format-Snapshot {
|
||||
@{ Ok = $Snap.Phase1.PoliciesArriving; Failed = $false }
|
||||
)
|
||||
|
||||
# Phase 6 / Lockdown (shared by both flows, rendered last)
|
||||
$p6Status = Get-PhaseStatus @(
|
||||
@{ Ok = $Snap.Phase6.RemediationApplied; Failed = $false },
|
||||
@{ Ok = $Snap.Phase6.DetectionPassed; Failed = $false }
|
||||
)
|
||||
# Phase 6 / Lockdown (shared by both flows, rendered last).
|
||||
# RegistryMatches is the ground-truth check - if it flips to false after a
|
||||
# historical success, something rolled back Winlogon and lockdown has drifted.
|
||||
# If the manual Force-Lockdown marker is present, treat the row as COMPLETE
|
||||
# to match the Get-LockdownState gate (avoids the table reading IN PROGRESS
|
||||
# while the script is about to advance to setup-complete on this same tick).
|
||||
if ($Snap.Phase6.Complete) {
|
||||
$p6Status = 'COMPLETE'
|
||||
} else {
|
||||
$p6Status = Get-PhaseStatus @(
|
||||
@{ Ok = $Snap.Phase6.RemediationApplied; Failed = $false },
|
||||
@{ Ok = $Snap.Phase6.DetectionPassed; Failed = $false },
|
||||
@{ Ok = $Snap.Phase6.RegistryMatches; Failed = $false }
|
||||
)
|
||||
}
|
||||
|
||||
if (-not $skipDsc) {
|
||||
# ---- Standard / CMM / etc. (DSC flow) ----
|
||||
@@ -600,24 +667,16 @@ function Format-Snapshot {
|
||||
@{ Ok = $Snap.Phase3.DeployComplete; Failed = $false },
|
||||
@{ Ok = $Snap.Phase3.InstallComplete; Failed = $false }
|
||||
)
|
||||
$p4HasFailed = $false; $p4AllDone = $true; $p4AnyStarted = $false
|
||||
$p4HasScripts = ($Snap.Phase4 -and $Snap.Phase4.Count -gt 0)
|
||||
if ($p4HasScripts) {
|
||||
foreach ($s in $Snap.Phase4) {
|
||||
if ($s.Status -eq 'failed') { $p4HasFailed = $true }
|
||||
if ($s.Status -ne 'done') { $p4AllDone = $false }
|
||||
if ($s.Status -ne 'pending') { $p4AnyStarted = $true }
|
||||
}
|
||||
} else {
|
||||
# No scripts discovered. If DSC install is already complete,
|
||||
# there are simply no custom scripts for this image type --
|
||||
# that's COMPLETE, not WAITING.
|
||||
$p4AllDone = $Snap.Phase3.InstallComplete
|
||||
}
|
||||
$p4Status = if ($p4HasFailed) { 'FAILED' } elseif ($p4AllDone) { 'COMPLETE' } elseif ($p4AnyStarted) { 'IN PROGRESS' } else { 'WAITING' }
|
||||
# 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.
|
||||
|
||||
$p5Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
|
||||
$p5Status = Get-PhaseStatus @(
|
||||
$p4Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
|
||||
$p4Status = Get-PhaseStatus @(
|
||||
@{ Ok = $Snap.Phase5.ConsumeCredsTask; Failed = $false },
|
||||
@{ Ok = $Snap.Phase5.CredsPopulated; Failed = $false }
|
||||
)
|
||||
@@ -629,12 +688,11 @@ function Format-Snapshot {
|
||||
}
|
||||
Write-Host ' 2. Device Configuration ' -NoNewline; Format-StatusTag $p2Status; Write-Host ''
|
||||
Write-Host ' 3. Software Deployment ' -NoNewline; Format-StatusTag $p3Status; Write-Host ''
|
||||
Write-Host ' 4. Application Install ' -NoNewline; Format-StatusTag $p4Status; Write-Host ''
|
||||
Write-Host ' 5. Credential Setup ' -NoNewline; Format-StatusTag $p5Status; Write-Host ''
|
||||
if ($p5Done -and $p6Status -ne 'COMPLETE') {
|
||||
Write-Host ' 4. Credential Setup ' -NoNewline; Format-StatusTag $p4Status; Write-Host ''
|
||||
if ($p4Done -and $p6Status -ne 'COMPLETE') {
|
||||
Write-Host ' >> Initiate ARTS Lockdown request' -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ' 6. Lockdown ' -NoNewline; Format-StatusTag $p6Status; Write-Host ''
|
||||
Write-Host ' 5. Lockdown ' -NoNewline; Format-StatusTag $p6Status; Write-Host ''
|
||||
} else {
|
||||
# ---- Display (no DSC, no credentials) ----
|
||||
# Phases: 1 Registration -> 2 Device Configuration -> 3 Lockdown
|
||||
@@ -863,17 +921,21 @@ try {
|
||||
Write-Host ""
|
||||
Write-Host $qrText
|
||||
|
||||
# Final state: every phase landed. The gate is intentionally strict
|
||||
# because each piece is needed for the device to function:
|
||||
# - DscInstallComplete: device-config.yaml apps + custom scripts ran
|
||||
# - CredsPopulated: SFLD share creds in HKLM (Machine-Enforce,
|
||||
# Acrobat-Enforce, CMM-Enforce all need these)
|
||||
# - LockdownComplete: kiosk policy baseline + Winlogon flipped to
|
||||
# ShopFloor autologon + admin renamed
|
||||
# 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:
|
||||
# - CredsPopulated: SFLD share creds in HKLM (Machine-Enforce,
|
||||
# Acrobat-Enforce, CMM-Enforce, manifest
|
||||
# engine all need these)
|
||||
# - LockdownComplete: kiosk policy baseline + Winlogon flipped to
|
||||
# ShopFloor autologon + admin renamed
|
||||
# DscInstallComplete used to be required, but device-config.yaml
|
||||
# is becoming a no-op as apps migrate to the manifest engine; a
|
||||
# 403 on the YAML download (e.g. expired SAS token) shouldn't
|
||||
# block the imaging workflow if creds + lockdown are otherwise OK.
|
||||
# Display PCs skip this whole branch via $skipDsc above.
|
||||
if ($snap.DscInstallComplete -and
|
||||
$snap.Phase5.CredsPopulated -and
|
||||
$snap.LockdownComplete) {
|
||||
if ($snap.Phase5.CredsPopulated -and $snap.LockdownComplete) {
|
||||
Invoke-SetupComplete
|
||||
}
|
||||
|
||||
@@ -912,8 +974,7 @@ try {
|
||||
# Intune Remediation script mid-execution and delay lockdown.
|
||||
$waitingForLockdownOnly = $false
|
||||
if (-not $skipDsc) {
|
||||
$waitingForLockdownOnly = ($snap.DscInstallComplete -and
|
||||
$snap.Phase5.CredsPopulated -and
|
||||
$waitingForLockdownOnly = ($snap.Phase5.CredsPopulated -and
|
||||
-not $snap.LockdownComplete)
|
||||
} else {
|
||||
$waitingForLockdownOnly = ($snap.Phase1.AzureAdJoined -and
|
||||
|
||||
207
playbook/shopfloor-setup/Shopfloor/lib/Show-IntuneDeviceQR.ps1
Normal file
207
playbook/shopfloor-setup/Shopfloor/lib/Show-IntuneDeviceQR.ps1
Normal file
@@ -0,0 +1,207 @@
|
||||
# Show-IntuneDeviceQR.ps1 - Standalone version of the QR-code panel from
|
||||
# Monitor-IntuneProgress.ps1, extracted for use at sites that only need the
|
||||
# device-ID display (no DSC monitoring, no Intune sync triggers, no reboot
|
||||
# orchestration).
|
||||
#
|
||||
# Polls dsregcmd /status every 15s until the device reports an Azure AD
|
||||
# DeviceId, then renders that ID as a half-block QR code in the console
|
||||
# along with the literal text. Stays open until the user presses a key.
|
||||
#
|
||||
# REQUIREMENTS
|
||||
# - Windows PowerShell 5.1 (or PS 7) with .NET Framework available
|
||||
# - QRCoder.dll alongside this script (or at one of the fallback paths
|
||||
# below). Single ~75 KB native dependency, no internet needed.
|
||||
#
|
||||
# USAGE
|
||||
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File Show-IntuneDeviceQR.ps1
|
||||
#
|
||||
# Or double-click via a one-line .bat:
|
||||
# powershell.exe -NoProfile -ExecutionPolicy Bypass -NoExit -File "%~dp0Show-IntuneDeviceQR.ps1"
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$PollSeconds = 15,
|
||||
[string]$QRCoderDllPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Locate QRCoder.dll. Search order:
|
||||
# 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)
|
||||
# ----------------------------------------------------------------------------
|
||||
function Resolve-QRCoderDll {
|
||||
param([string]$Override)
|
||||
|
||||
$candidates = @()
|
||||
if ($Override) { $candidates += $Override }
|
||||
if ($PSScriptRoot) { $candidates += (Join-Path $PSScriptRoot 'QRCoder.dll') }
|
||||
$candidates += (Join-Path (Get-Location) 'QRCoder.dll')
|
||||
$candidates += 'C:\Enrollment\shopfloor-setup\Shopfloor\QRCoder.dll'
|
||||
|
||||
foreach ($c in $candidates) {
|
||||
if ($c -and (Test-Path -LiteralPath $c)) { return (Resolve-Path -LiteralPath $c).Path }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Get the AAD DeviceId from dsregcmd. Returns $null until the device is
|
||||
# Azure AD joined AND dsregcmd has populated the field.
|
||||
# ----------------------------------------------------------------------------
|
||||
function Get-AadDeviceId {
|
||||
try {
|
||||
$dsreg = dsregcmd /status 2>&1
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
$line = $dsreg | Select-String 'DeviceId' | Select-Object -First 1
|
||||
if (-not $line) { return $null }
|
||||
$parts = $line.ToString().Split(':')
|
||||
if ($parts.Count -lt 2) { return $null }
|
||||
$id = $parts[1].Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($id)) { return $null }
|
||||
return $id
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Half-block QR renderer. Each output character is 1 module wide, 2 modules
|
||||
# tall, using:
|
||||
# U+2588 = both top and bottom modules set
|
||||
# U+2580 = top only
|
||||
# U+2584 = bottom only
|
||||
# space = neither
|
||||
# Cuts QR height in half vs full-block rendering. Adds a 4-module quiet
|
||||
# zone manually since QRCoder's ModuleMatrix excludes it by default.
|
||||
# ----------------------------------------------------------------------------
|
||||
function Format-QrHalfBlocks {
|
||||
param([string]$Payload, [string]$DllPath, [int]$IndentSpaces = 8)
|
||||
|
||||
Add-Type -Path $DllPath
|
||||
$gen = New-Object QRCoder.QRCodeGenerator
|
||||
$data = $gen.CreateQrCode($Payload, [QRCoder.QRCodeGenerator+ECCLevel]::L)
|
||||
|
||||
$matrix = $data.ModuleMatrix
|
||||
$size = $matrix.Count
|
||||
$pad = 4
|
||||
$total = $size + 2 * $pad
|
||||
|
||||
$upper = [char]0x2580
|
||||
$lower = [char]0x2584
|
||||
$full = [char]0x2588
|
||||
$left = ' ' * $IndentSpaces
|
||||
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
for ($y = 0; $y -lt $total; $y += 2) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
[void]$sb.Append($left)
|
||||
for ($x = 0; $x -lt $total; $x++) {
|
||||
$mx = $x - $pad
|
||||
$my1 = $y - $pad
|
||||
$my2 = $y + 1 - $pad
|
||||
|
||||
$top = ($my1 -ge 0 -and $my1 -lt $size -and $mx -ge 0 -and $mx -lt $size -and $matrix[$my1].Get($mx))
|
||||
$bot = ($my2 -ge 0 -and $my2 -lt $size -and $mx -ge 0 -and $mx -lt $size -and $matrix[$my2].Get($mx))
|
||||
|
||||
if ($top -and $bot) { [void]$sb.Append($full) }
|
||||
elseif ($top) { [void]$sb.Append($upper) }
|
||||
elseif ($bot) { [void]$sb.Append($lower) }
|
||||
else { [void]$sb.Append(' ') }
|
||||
}
|
||||
$lines.Add($sb.ToString())
|
||||
}
|
||||
return $lines
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Bigger console so the QR + text frame fits in one viewport.
|
||||
# ----------------------------------------------------------------------------
|
||||
try {
|
||||
$rui = $Host.UI.RawUI
|
||||
$maxH = $rui.MaxPhysicalWindowSize.Height
|
||||
$targetWindow = [Math]::Min(58, [int]$maxH)
|
||||
$targetBuffer = [Math]::Max($targetWindow, 200)
|
||||
|
||||
$bs = $rui.BufferSize
|
||||
if ($bs.Height -lt $targetBuffer) { $bs.Height = $targetBuffer; $rui.BufferSize = $bs }
|
||||
|
||||
$ws = $rui.WindowSize
|
||||
if ($ws.Height -lt $targetWindow) { $ws.Height = $targetWindow; $rui.WindowSize = $ws }
|
||||
} catch {}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Wait for QRCoder.dll
|
||||
# ----------------------------------------------------------------------------
|
||||
$dllPath = Resolve-QRCoderDll -Override $QRCoderDllPath
|
||||
|
||||
# Mark-of-the-Web: when files arrive via SMB / zip download / email,
|
||||
# Windows attaches a Zone.Identifier alternate data stream that flags the
|
||||
# file as "from another computer". Add-Type then refuses to load the DLL
|
||||
# with "Operation is not supported" / "Could not load file or assembly".
|
||||
# Unblock-File silently strips the ADS. Best-effort: errors are non-fatal
|
||||
# (file may already be unblocked, or the user may not have write access).
|
||||
if ($dllPath) {
|
||||
try { Unblock-File -LiteralPath $dllPath -ErrorAction SilentlyContinue } catch {}
|
||||
}
|
||||
if ($PSCommandPath) {
|
||||
try { Unblock-File -LiteralPath $PSCommandPath -ErrorAction SilentlyContinue } catch {}
|
||||
}
|
||||
|
||||
if (-not $dllPath) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: QRCoder.dll not found." -ForegroundColor Red
|
||||
Write-Host "Searched (in order):"
|
||||
if ($QRCoderDllPath) { Write-Host " - $QRCoderDllPath (-QRCoderDllPath)" }
|
||||
if ($PSScriptRoot) { Write-Host " - $(Join-Path $PSScriptRoot 'QRCoder.dll')" }
|
||||
Write-Host " - $(Join-Path (Get-Location) 'QRCoder.dll')"
|
||||
Write-Host " - C:\Enrollment\shopfloor-setup\Shopfloor\QRCoder.dll"
|
||||
Write-Host ""
|
||||
Write-Host "Drop QRCoder.dll alongside this script and re-run." -ForegroundColor Yellow
|
||||
exit 2
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Poll loop - retry every $PollSeconds until DeviceId appears
|
||||
# ----------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host " Waiting for Azure AD device registration..." -ForegroundColor Cyan
|
||||
Write-Host " Polling dsregcmd every $PollSeconds s. Press Ctrl+C to abort." -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
$attempt = 0
|
||||
while ($true) {
|
||||
$attempt++
|
||||
$deviceId = Get-AadDeviceId
|
||||
if ($deviceId) { break }
|
||||
|
||||
Write-Host (" [{0}] attempt {1,3} no DeviceId yet, retrying in {2}s..." -f (Get-Date -Format 'HH:mm:ss'), $attempt, $PollSeconds) -ForegroundColor DarkGray
|
||||
Start-Sleep -Seconds $PollSeconds
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Render
|
||||
# ----------------------------------------------------------------------------
|
||||
Clear-Host
|
||||
Write-Host ""
|
||||
Write-Host " GE Aerospace -- Intune Device ID" -ForegroundColor Cyan
|
||||
Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
Write-Host " Device ID: $deviceId" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
try {
|
||||
$qrLines = Format-QrHalfBlocks -Payload $deviceId -DllPath $dllPath
|
||||
foreach ($l in $qrLines) { Write-Host $l }
|
||||
} catch {
|
||||
Write-Host " (QR generation failed: $($_.Exception.Message))" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Scan with phone camera or Intune admin app." -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
Write-Host " Press any key to close..." -ForegroundColor Yellow
|
||||
try { [void][Console]::ReadKey($true) } catch { [void](Read-Host) }
|
||||
exit 0
|
||||
Reference in New Issue
Block a user