diff --git a/playbook/preinstall/opentext/Setup-OpenText.ps1 b/playbook/preinstall/opentext/Setup-OpenText.ps1 index a5ffe24..e71b5e4 100644 --- a/playbook/preinstall/opentext/Setup-OpenText.ps1 +++ b/playbook/preinstall/opentext/Setup-OpenText.ps1 @@ -280,13 +280,40 @@ foreach ($u in $userDirs) { } # --- Step 4: Public Desktop shortcuts --- +# Same exclusion list as the Profile step: these three sessions aren't +# used on shopfloor PCs, so we skip deploying their .lnk files AND +# remove any that a prior install left behind. Write-SetupLog "" Write-SetupLog "Step 4: Deploying public desktop shortcuts..." $shortcutSrc = Join-Path $SourceDir 'W10shortcuts' $publicDesktop = 'C:\Users\Public\Desktop' + +$excludeShortcuts = @( + 'WJ_Office.lnk', + 'IBM_qks.lnk', + 'mmcs.lnk' +) + +# Clean up stale copies from prior installs first +foreach ($name in $excludeShortcuts) { + $stale = Join-Path $publicDesktop $name + if (Test-Path -LiteralPath $stale) { + try { + Remove-Item -LiteralPath $stale -Force -ErrorAction Stop + Write-SetupLog " removed stale desktop shortcut: $name" + } catch { + Write-SetupLog " failed to remove stale $stale : $_" + } + } +} + if (Test-Path $shortcutSrc) { $lnkFiles = Get-ChildItem -Path $shortcutSrc -Filter '*.lnk' -File -ErrorAction SilentlyContinue foreach ($l in $lnkFiles) { + if ($excludeShortcuts -contains $l.Name) { + Write-SetupLog " skip (excluded): $($l.Name)" + continue + } Copy-Item -Path $l.FullName -Destination $publicDesktop -Force Write-SetupLog " $($l.Name) -> $publicDesktop" } diff --git a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 index 3a56fdb..2dcad55 100644 --- a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 +++ b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 @@ -1,6 +1,24 @@ # Run-ShopfloorSetup.ps1 - Dispatcher for shopfloor PC type setup # Runs Shopfloor baseline scripts first, then type-specific scripts on top. +# --- Transcript logging --- +# Captures everything the dispatcher and all child scripts write to host so +# we can diagnose setup failures after the fact. -Append + -Force so repeat +# invocations (e.g. after a reboot mid-setup) accumulate instead of clobbering. +$logDir = 'C:\Logs\SFLD' +if (-not (Test-Path $logDir)) { + try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP } +} +$transcriptPath = Join-Path $logDir 'shopfloor-setup.log' +try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {} + +Write-Host "" +Write-Host "================================================================" +Write-Host "=== Run-ShopfloorSetup.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ===" +Write-Host " Transcript: $transcriptPath" +Write-Host "================================================================" +Write-Host "" + # Bump AutoLogonCount HIGH at the start so reboots during setup (e.g. VC++ 2008 # triggering an immediate ExitWindowsEx) don't exhaust autologin attempts before # the dispatcher can complete. The end-of-script reset puts it back to 2 once @@ -40,11 +58,44 @@ if (-not $pcType) { Write-Host "Shopfloor PC Type: $pcType" -# --- Run Shopfloor baseline scripts first --- +# Scripts to skip in the alphabetical baseline loop. Each is either run +# explicitly in the finalization phase below, or invoked internally by +# another script: +# +# 05 - Office shortcuts. Invoked by 06 as Phase 0. Office isn't installed +# until after the first reboot, so 05 no-ops on the imaging run. +# 06's SYSTEM logon task re-runs it on the second boot. +# 06 - Desktop org. Phase 2 needs eDNC/NTLARS on disk (installed by +# type-specific 01-eDNC.ps1). Run in finalization phase. +# 07 - Taskbar pin layout. Reads 06's output. Run in finalization phase. +# 08 - Edge default browser + startup tabs. Invoked by 06 as Phase 4. +# Reads .url files delivered by DSC (after setup reboots). 06's +# SYSTEM logon task re-runs it to pick them up. +$skipInBaseline = @( + '05-OfficeShortcuts.ps1', + '06-OrganizeDesktop.ps1', + '07-TaskbarLayout.ps1', + '08-EdgeDefaultBrowser.ps1', + 'Check-MachineNumber.ps1', + 'Configure-PC.ps1' +) + +# Scripts run AFTER type-specific scripts complete. 05 and 08 are NOT +# here because 06 calls them internally as sub-phases. +$runAfterTypeSpecific = @( + '06-OrganizeDesktop.ps1', + '07-TaskbarLayout.ps1' +) + +# --- Run Shopfloor baseline scripts first (skipping deferred ones) --- $baselineDir = Join-Path $setupDir "Shopfloor" if (Test-Path $baselineDir) { $scripts = Get-ChildItem -Path $baselineDir -Filter "*.ps1" -File | Sort-Object Name foreach ($script in $scripts) { + if ($skipInBaseline -contains $script.Name) { + Write-Host "Skipping baseline: $($script.Name) (runs in finalization phase)" + continue + } shutdown /a 2>$null Write-Host "Running baseline: $($script.Name)" try { @@ -74,13 +125,36 @@ if ($pcType -ne "Shopfloor") { } } +# --- Finalization: run deferred baseline scripts (desktop org, taskbar pins) +# --- +# These needed to wait until all apps (eDNC, NTLARS, UDC, OpenText) were +# installed by the baseline + type-specific phases above. 06 internally +# calls 05 (Office shortcuts) and 08 (Edge config) as sub-phases, so we +# only need to invoke 06 and 07 explicitly here. +foreach ($name in $runAfterTypeSpecific) { + $script = Join-Path $baselineDir $name + if (-not (Test-Path $script)) { + Write-Warning "Deferred script not found: $script" + continue + } + shutdown /a 2>$null + Write-Host "Running deferred baseline: $name" + try { + & $script + } catch { + Write-Warning "Deferred script $name failed: $_" + } +} + Write-Host "Shopfloor setup complete for $pcType." # Copy utility scripts to SupportUser desktop -$syncScript = Join-Path $setupDir "Shopfloor\sync_intune.bat" -if (Test-Path $syncScript) { - Copy-Item -Path $syncScript -Destination "C:\Users\SupportUser\Desktop\sync_intune.bat" -Force - Write-Host "sync_intune.bat copied to desktop." +foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat')) { + $src = Join-Path $setupDir "Shopfloor\$tool" + if (Test-Path $src) { + Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$tool" -Force + Write-Host "$tool copied to desktop." + } } # Standard PCs get the UDC/eDNC machine number helper @@ -98,5 +172,13 @@ if ($pcType -eq "Standard") { reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 2 /f | Out-Null Write-Host "Auto-logon set to 2 remaining logins." +Write-Host "" +Write-Host "================================================================" +Write-Host "=== Run-ShopfloorSetup.ps1 complete $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ===" +Write-Host "================================================================" + +# Flush transcript before shutdown so the log file is complete on next boot +try { Stop-Transcript | Out-Null } catch {} + Write-Host "Rebooting in 10 seconds..." shutdown /r /t 10 diff --git a/playbook/shopfloor-setup/Shopfloor/05-OfficeShortcuts.ps1 b/playbook/shopfloor-setup/Shopfloor/05-OfficeShortcuts.ps1 index ae6a467..7f3d19f 100644 --- a/playbook/shopfloor-setup/Shopfloor/05-OfficeShortcuts.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/05-OfficeShortcuts.ps1 @@ -21,14 +21,25 @@ $officeApps = @( @{ Exe = 'POWERPNT.EXE'; Name = 'PowerPoint' } ) -# Office binary location depends on x86 vs x64 install. The standard ShopFloor -# Office ppkg is x86 (GCCH_Prod_SFLD_StdOffice-x86_*) so it lands under -# Program Files (x86), but we check both so the script works either way. -$officeRoot = $null -foreach ($base in @( +# Office binary location varies by install type and version: +# - Office 2019+ / M365 / LTSC Click-to-Run: \Microsoft Office\root\Office16\ +# - Office 2016 MSI (legacy): \Microsoft Office\Office16\ (no root\) +# - Office 2013 MSI: \Microsoft Office\Office15\ +# - x86 Office on 64-bit Windows: Program Files (x86)\... +# The standard ShopFloor Office ppkg is x86 Click-to-Run so normal case +# is "Program Files (x86)\Microsoft Office\root\Office16\EXCEL.EXE", but +# we check all variants so the script works across SKUs. +$officeSearchRoots = @( 'C:\Program Files\Microsoft Office\root\Office16', - 'C:\Program Files (x86)\Microsoft Office\root\Office16' -)) { + 'C:\Program Files (x86)\Microsoft Office\root\Office16', + 'C:\Program Files\Microsoft Office\Office16', + 'C:\Program Files (x86)\Microsoft Office\Office16', + 'C:\Program Files\Microsoft Office\Office15', + 'C:\Program Files (x86)\Microsoft Office\Office15' +) + +$officeRoot = $null +foreach ($base in $officeSearchRoots) { if (Test-Path (Join-Path $base 'EXCEL.EXE')) { $officeRoot = $base break @@ -37,6 +48,11 @@ foreach ($base in @( if (-not $officeRoot) { Write-Host "No Office install detected - skipping shortcut creation." + Write-Host "Searched:" + foreach ($p in $officeSearchRoots) { + $hit = if (Test-Path $p) { "(dir exists)" } else { "(missing)" } + Write-Host " $p $hit" + } exit 0 } diff --git a/playbook/shopfloor-setup/Shopfloor/06-OrganizeDesktop.ps1 b/playbook/shopfloor-setup/Shopfloor/06-OrganizeDesktop.ps1 index 0333d62..586fe8e 100644 --- a/playbook/shopfloor-setup/Shopfloor/06-OrganizeDesktop.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/06-OrganizeDesktop.ps1 @@ -49,14 +49,13 @@ if (-not $isAdmin) { $publicDesktop = 'C:\Users\Public\Desktop' $shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools' $scriptPath = $MyInvocation.MyCommand.Path +$scriptDir = Split-Path -Parent $scriptPath # Filenames that always stay at the desktop root regardless of classification. -# End users click these many times a day and an extra folder click is real -# friction. Phase 2 also drops these here as a post-sweep safety net. -$keepAtRoot = @( - 'eDNC.lnk', - 'NTLARS.lnk' -) +# Empty right now - every shortcut lives inside a category folder for a +# clean end-user desktop. Add entries here if a future shortcut needs to +# stay pinned at root. +$keepAtRoot = @() # ============================================================================ # Phase 1: Sweep loose shortcuts at desktop root into category folders @@ -109,17 +108,11 @@ function Invoke-DesktopSweep { } } - # Ensure category folders exist - foreach ($cat in $categories.Keys) { - $dir = Join-Path $DesktopPath $cat - if (-not (Test-Path -LiteralPath $dir)) { - try { - New-Item -ItemType Directory -Path $dir -Force -ErrorAction Stop | Out-Null - } catch { - Write-Warning "Failed to create category folder '$cat' : $_" - } - } - } + # Category folders are created LAZILY - only when we're about to move + # something into them - so a Display / Wax-Trace PC without Office + # doesn't get an empty Office\ folder littering the desktop. See + # Remove-EmptyCategoryFolders at the end of the script for the + # post-sweep cleanup pass that finishes the job. # WScript.Shell for resolving .lnk targets $shell = $null @@ -187,7 +180,15 @@ function Invoke-DesktopSweep { } if ($category) { - $destDir = Join-Path $DesktopPath $category + $destDir = Join-Path $DesktopPath $category + if (-not (Test-Path -LiteralPath $destDir)) { + try { + New-Item -ItemType Directory -Path $destDir -Force -ErrorAction Stop | Out-Null + } catch { + Write-Warning "Failed to create category folder '$category' : $_" + continue + } + } $destPath = Join-Path $destDir $item.Name try { Move-Item -LiteralPath $item.FullName -Destination $destPath -Force -ErrorAction Stop @@ -269,17 +270,10 @@ function Find-ExistingLnk { } function Add-ShopfloorToolsApps { - if (-not (Test-Path -LiteralPath $shopfloorToolsDir)) { - try { - New-Item -ItemType Directory -Path $shopfloorToolsDir -Force -ErrorAction Stop | Out-Null - } catch { - Write-Warning "Failed to create $shopfloorToolsDir : $_" - return - } - } - # App registry - each entry describes how to materialize one shortcut - # into Shopfloor Tools\. + # into Shopfloor Tools\. Folder creation is deferred until we know we + # have at least one app to put in it (lazy creation, so PC types with + # nothing installed don't get an empty Shopfloor Tools folder). # # Kind = 'exe' -> build a fresh .lnk from ExePath # Kind = 'existing' -> copy an existing .lnk via Find-ExistingLnk @@ -291,12 +285,29 @@ function Add-ShopfloorToolsApps { @{ Name = 'Defect_Tracker'; Kind = 'existing'; SourceName = 'Defect_Tracker.lnk' } ) + # Lazy folder creation - only create Shopfloor Tools\ the first time + # we have an app that's actually going to land in it. PC types with + # nothing installed get no empty folder. + $ensureToolsDir = { + if (-not (Test-Path -LiteralPath $shopfloorToolsDir)) { + try { + New-Item -ItemType Directory -Path $shopfloorToolsDir -Force -ErrorAction Stop | Out-Null + return $true + } catch { + Write-Warning "Failed to create $shopfloorToolsDir : $_" + return $false + } + } + return $true + } + foreach ($app in $apps) { $dest = Join-Path $shopfloorToolsDir "$($app.Name).lnk" switch ($app.Kind) { 'exe' { if (Test-Path -LiteralPath $app.ExePath) { + if (-not (& $ensureToolsDir)) { break } if (New-ShopfloorLnk -Path $dest -Target $app.ExePath) { Write-Host " created: $($app.Name) -> $dest" } @@ -308,6 +319,7 @@ function Add-ShopfloorToolsApps { 'existing' { $src = Find-ExistingLnk $app.SourceName if ($src) { + if (-not (& $ensureToolsDir)) { break } try { Copy-Item -LiteralPath $src -Destination $dest -Force -ErrorAction Stop Write-Host " copied: $($app.Name) from $src" @@ -323,30 +335,56 @@ function Add-ShopfloorToolsApps { } # ============================================================================ -# Phase 3: Pin eDNC and NTLARS shortcuts at the desktop root +# Phase 3: Remove empty category folders # -# These are the two shortcuts end users touch most frequently. Allowlisted -# in the sweep, and also force-dropped here (copied from the Shopfloor -# Tools\ versions we just created) so a sweep-and-forget user still ends -# up with them loose at the root. +# If the sweep classified nothing into a given category (e.g. no Office +# on a Display PC, no Shopfloor Tools apps on a kiosk), we don't want an +# empty folder cluttering the desktop. The scheduled-task sweep also runs +# this so categories that go empty between logons self-heal. # ============================================================================ -function Update-DesktopRootPins { - foreach ($name in @('eDNC.lnk', 'NTLARS.lnk')) { - $src = Join-Path $shopfloorToolsDir $name - $dst = Join-Path $publicDesktop $name - if (Test-Path -LiteralPath $src) { +function Remove-EmptyCategoryFolders { + foreach ($cat in @('Office', 'Shopfloor Tools', 'Web Links')) { + $dir = Join-Path $publicDesktop $cat + if (-not (Test-Path -LiteralPath $dir)) { continue } + $contents = @(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue) + if ($contents.Count -eq 0) { try { - Copy-Item -LiteralPath $src -Destination $dst -Force -ErrorAction Stop - Write-Host " root: $name" + Remove-Item -LiteralPath $dir -Force -ErrorAction Stop + Write-Host " removed empty: $cat\" } catch { - Write-Warning "Failed to drop $dst : $_" + Write-Warning "Failed to remove empty $dir : $_" } } } } # ============================================================================ -# Scheduled task registration (phase 1 re-run at every logon) +# Phase 4: Edge default browser + startup tabs (delegated to 08) +# +# 08-EdgeDefaultBrowser.ps1 resolves startup-tab URLs from .url files on +# the Public Desktop (or in Web Links\ after the sweep). The .url files +# are delivered by DSC AFTER this initial shopfloor setup, so the first +# run at imaging time won't find them (falls back to hardcoded URLs, +# Plant Apps gets skipped). By calling 08 from inside 06, every SYSTEM +# scheduled-task logon re-run of 06 also re-runs 08 — so after DSC drops +# the .url files and the next sweep files them into Web Links\, 08 +# picks them up and updates the Edge policy. Self-healing. +# ============================================================================ +function Invoke-EdgeDefaultBrowser { + $edgeScript = Join-Path $scriptDir '08-EdgeDefaultBrowser.ps1' + if (-not (Test-Path -LiteralPath $edgeScript)) { + Write-Host " 08-EdgeDefaultBrowser.ps1 not found - skipping" + return + } + try { + & $edgeScript + } catch { + Write-Warning "08-EdgeDefaultBrowser.ps1 failed: $_" + } +} + +# ============================================================================ +# Scheduled task registration (re-runs 06 at every logon as SYSTEM) # ============================================================================ function Register-SweepScheduledTask { param([string]$ScriptPath) @@ -365,12 +403,16 @@ function Register-SweepScheduledTask { $trigger = New-ScheduledTaskTrigger -AtLogOn - # Run as whichever user logs in, at Limited (standard) rights. Public - # desktop is writable by BUILTIN\Users so no elevation needed. Using - # the well-known Users group SID so this works on non-English Windows. + # Run as SYSTEM so the script can (a) pass its admin check at the + # top, (b) write to Public Desktop / ProgramData Start Menu / the + # Default User profile without Access Denied, and (c) register the + # next iteration of itself idempotently. Earlier versions used + # -GroupId 'BUILTIN\Users' -RunLevel Limited which fired at logon + # but the script exited immediately on the non-admin check. $principal = New-ScheduledTaskPrincipal ` - -GroupId 'S-1-5-32-545' ` - -RunLevel Limited + -UserId 'NT AUTHORITY\SYSTEM' ` + -LogonType ServiceAccount ` + -RunLevel Highest $settings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries ` @@ -399,6 +441,20 @@ function Register-SweepScheduledTask { # ============================================================================ # Main # ============================================================================ +Write-Host "" +Write-Host "=== Phase 0: create Office shortcuts (if Office is installed) ===" +# Delegated to 05-OfficeShortcuts.ps1. Office is installed via ppkg but +# doesn't finish streaming until AFTER the first reboot, so the first +# imaging-time run of 06 finds no Office and this no-ops. On the second +# logon (after reboot), Office is installed and 05 creates the shortcuts +# so Phase 1 below can sweep them into the Office\ folder. +$officeScript = Join-Path $scriptDir '05-OfficeShortcuts.ps1' +if (Test-Path -LiteralPath $officeScript) { + try { & $officeScript } catch { Write-Warning "05-OfficeShortcuts.ps1 failed: $_" } +} else { + Write-Host " 05-OfficeShortcuts.ps1 not found - skipping" +} + Write-Host "" Write-Host "=== Phase 1: sweep loose shortcuts into category folders ===" Invoke-DesktopSweep -DesktopPath $publicDesktop @@ -408,11 +464,15 @@ Write-Host "=== Phase 2: populate Shopfloor Tools with app shortcuts ===" Add-ShopfloorToolsApps Write-Host "" -Write-Host "=== Phase 3: drop eDNC / NTLARS at desktop root ===" -Update-DesktopRootPins +Write-Host "=== Phase 3: remove empty category folders ===" +Remove-EmptyCategoryFolders Write-Host "" -Write-Host "=== Phase 4: register logon sweeper scheduled task ===" +Write-Host "=== Phase 4: Edge default browser + startup tabs ===" +Invoke-EdgeDefaultBrowser + +Write-Host "" +Write-Host "=== Phase 5: register logon sweeper scheduled task ===" Register-SweepScheduledTask -ScriptPath $scriptPath exit 0 diff --git a/playbook/shopfloor-setup/Shopfloor/07-TaskbarLayout.ps1 b/playbook/shopfloor-setup/Shopfloor/07-TaskbarLayout.ps1 index b78d5aa..3b650f2 100644 --- a/playbook/shopfloor-setup/Shopfloor/07-TaskbarLayout.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/07-TaskbarLayout.ps1 @@ -7,9 +7,7 @@ # This script does NOT create, move, or copy any shortcuts - all shortcut # management lives in 06. 07 is the last-mile taskbar config only. # -# Pin order (left to right): Edge, WJ Shopfloor, UDC, eDNC, Defect_Tracker. -# NTLARS is intentionally not pinned - per the original spec it lives on -# the desktop root only. +# Pin order (left to right): Edge, WJ Shopfloor, UDC, eDNC, NTLARS, Defect_Tracker. # # LayoutModification.xml is written to the Default User profile shell # directory, which means the pins apply on FIRST LOGON of any new user diff --git a/playbook/shopfloor-setup/Shopfloor/08-EdgeDefaultBrowser.ps1 b/playbook/shopfloor-setup/Shopfloor/08-EdgeDefaultBrowser.ps1 index f70c2e5..ce7cd35 100644 --- a/playbook/shopfloor-setup/Shopfloor/08-EdgeDefaultBrowser.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/08-EdgeDefaultBrowser.ps1 @@ -196,7 +196,7 @@ function Resolve-StartupUrl { # Tab order as requested: Plant Apps, Shop Floor Homepage, Shopfloor Dashboard $startupTabs = @() -$plantApps = Resolve-StartupUrl -BaseName 'Plant Apps' +$plantApps = Resolve-StartupUrl -BaseName 'Plant Apps' -Fallback 'https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications' if ($plantApps) { $startupTabs += $plantApps } $shopFloorHome = Resolve-StartupUrl -BaseName 'WJ Shop Floor Homepage' -Fallback 'http://tsgwp00524.logon.ds.ge.com/' diff --git a/playbook/shopfloor-setup/Shopfloor/Check-MachineNumber.ps1 b/playbook/shopfloor-setup/Shopfloor/Check-MachineNumber.ps1 new file mode 100644 index 0000000..a9bbfd4 --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/Check-MachineNumber.ps1 @@ -0,0 +1,158 @@ +# Check-MachineNumber.ps1 - Logon-triggered check for placeholder machine +# number. If UDC or eDNC are still at 9999, pops an InputBox for the user +# to enter the real number. On success, unregisters the scheduled task so +# the prompt never appears again. +# +# Runs as the LOGGED-IN USER (not SYSTEM) because it needs to show GUI. +# Writing to ProgramData + HKLM is possible because 02-MachineNumberACLs.ps1 +# pre-granted BUILTIN\Users write access on those specific targets during +# imaging. +# +# Registered/unregistered by Configure-PC.ps1 (item 6 in the toggle list). + +# --- Transcript logging --- +$logDir = 'C:\Logs\SFLD' +if (-not (Test-Path $logDir)) { + try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP } +} +$transcriptPath = Join-Path $logDir 'Check-MachineNumber.log' +try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {} +Write-Host "Check-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" + +Add-Type -AssemblyName Microsoft.VisualBasic +Add-Type -AssemblyName System.Windows.Forms + +$taskName = 'Check Machine Number' +$udcSettingsPath = 'C:\ProgramData\UDC\udc_settings.json' +$udcExePath = 'C:\Program Files\UDC\UDC.exe' +$ednRegPath = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General' +$site = 'West Jefferson' + +# --- Read current values --- +$currentUdc = $null +$currentEdnc = $null + +if (Test-Path $udcSettingsPath) { + try { + $json = Get-Content $udcSettingsPath -Raw | ConvertFrom-Json + $currentUdc = $json.GeneralSettings.MachineNumber + } catch {} +} + +if (Test-Path $ednRegPath) { + try { + $currentEdnc = (Get-ItemProperty -Path $ednRegPath -Name MachineNo -ErrorAction Stop).MachineNo + } catch {} +} + +# --- Check if placeholder --- +Write-Host "UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not found)' })" +Write-Host "eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not found)' })" + +if ($currentUdc -ne '9999' -and $currentEdnc -ne '9999') { + Write-Host "Machine number is set (not 9999). Unregistering task and exiting." + try { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + } catch {} + try { Stop-Transcript | Out-Null } catch {} + exit 0 +} + +Write-Host "Placeholder 9999 detected - showing prompt." + +# --- Show prompt --- +$promptLines = @() +$promptLines += "The machine number on this PC is still set to the" +$promptLines += "placeholder value (9999). Please enter the correct" +$promptLines += "machine number for this workstation." +$promptLines += "" +if ($currentUdc) { $promptLines += "Current UDC: $currentUdc" } +if ($currentEdnc) { $promptLines += "Current eDNC: $currentEdnc" } +$promptLines += "" +$promptLines += "Enter the new Machine Number:" + +$prompt = $promptLines -join "`n" +$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "") + +if ([string]::IsNullOrWhiteSpace($new)) { + Write-Host "User cancelled. Will prompt again next logon." + try { Stop-Transcript | Out-Null } catch {} + exit 0 +} + +$new = $new.Trim() + +# --- Validate --- +if ($new -notmatch '^\d+$') { + Write-Host "Invalid input: '$new' (not digits only). Will prompt again next logon." + [System.Windows.Forms.MessageBox]::Show( + "Machine number must be digits only.`n`nYou entered: '$new'`n`nThe prompt will appear again at next logon.", + "Invalid Machine Number", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Error + ) | Out-Null + try { Stop-Transcript | Out-Null } catch {} + exit 0 +} + +# --- Update UDC --- +$results = @() +if (Test-Path $udcSettingsPath) { + # Stop UDC first + Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object { + try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {} + } + Start-Sleep -Seconds 1 + + try { + $json = Get-Content $udcSettingsPath -Raw | ConvertFrom-Json + $json.GeneralSettings.MachineNumber = $new + $json | ConvertTo-Json -Depth 99 | Set-Content -Path $udcSettingsPath -Encoding UTF8 + $results += "UDC updated to $new" + } catch { + $results += "UDC FAILED: $_" + } +} + +# --- Update eDNC --- +if (Test-Path $ednRegPath) { + try { + Set-ItemProperty -Path $ednRegPath -Name MachineNo -Value $new -Type String -Force + $results += "eDNC updated to $new" + } catch { + $results += "eDNC FAILED: $_" + } +} + +# --- Relaunch UDC --- +if (Test-Path $udcExePath) { + try { + Start-Process -FilePath $udcExePath -ArgumentList @('-site', "`"$site`"", '-machine', $new) + } catch {} +} + +# --- Show result --- +$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe." +[System.Windows.Forms.MessageBox]::Show( + $summary, + "Machine Number Updated", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Information +) | Out-Null + +# --- Unregister task on success --- +Write-Host "Results: $($results -join '; ')" +$anyFail = $results | Where-Object { $_ -match 'FAILED' } +if (-not $anyFail) { + Write-Host "All updates succeeded. Unregistering logon task." + try { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + } catch {} +} else { + Write-Host "Some updates failed. Task stays registered - will prompt again next logon." +} + +Write-Host "Check-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +try { Stop-Transcript | Out-Null } catch {} +exit 0 diff --git a/playbook/shopfloor-setup/Shopfloor/Configure-PC.bat b/playbook/shopfloor-setup/Shopfloor/Configure-PC.bat new file mode 100644 index 0000000..5a9e1ed --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/Configure-PC.bat @@ -0,0 +1,23 @@ +@echo off +REM Configure-PC.bat - Launches Configure-PC.ps1 in an elevated PowerShell. +REM SupportUser desktop tool for setting machine number + auto-startup items. + +title Configure PC + +set "SCRIPT=%~dp0Configure-PC.ps1" +if exist "%SCRIPT%" goto :found + +set "SCRIPT=C:\Users\SupportUser\Desktop\Configure-PC.ps1" +if exist "%SCRIPT%" goto :found + +set "SCRIPT=C:\Enrollment\shopfloor-setup\Shopfloor\Configure-PC.ps1" +if exist "%SCRIPT%" goto :found + +echo ERROR: Configure-PC.ps1 not found +pause +exit /b 1 + +:found +echo Launching: %SCRIPT% +powershell -NoProfile -Command "Start-Process powershell.exe -Verb RunAs -ArgumentList '-NoProfile','-NoExit','-ExecutionPolicy','Bypass','-File','%SCRIPT%'" +exit /b diff --git a/playbook/shopfloor-setup/Shopfloor/Configure-PC.ps1 b/playbook/shopfloor-setup/Shopfloor/Configure-PC.ps1 new file mode 100644 index 0000000..ca75611 --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/Configure-PC.ps1 @@ -0,0 +1,414 @@ +# Configure-PC.ps1 - Interactive tool for SupportUser to configure a +# shopfloor PC after imaging. Two sections: +# +# 1. Machine number - if UDC/eDNC are still at placeholder 9999, prompt +# to set the real number right now. +# +# 2. Auto-startup items + machine-number logon prompt - toggle which apps +# start automatically for all users, and optionally register a logon +# task that prompts STANDARD users for the machine number if it's +# still 9999 when they log in (for the case where SupportUser skips +# it here and the end user needs to set it themselves). +# +# Startup .lnk files go in the AllUsers Startup folder: +# C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\ +# +# Run via Configure-PC.bat on the SupportUser desktop. + +$ErrorActionPreference = 'Continue' + +# --- Transcript logging --- +$logDir = 'C:\Logs\SFLD' +if (-not (Test-Path $logDir)) { + try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP } +} +$transcriptPath = Join-Path $logDir 'Configure-PC.log' +try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {} +Write-Host "Transcript: $transcriptPath" +Write-Host "Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" + +$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup' +$publicDesktop = 'C:\Users\Public\Desktop' + +# ============================================================================ +# Helpers +# ============================================================================ + +function Get-UrlFromFile { + param([string]$BaseName) + $candidates = @( + (Join-Path $publicDesktop "$BaseName.url"), + (Join-Path (Join-Path $publicDesktop 'Web Links') "$BaseName.url") + ) + foreach ($c in $candidates) { + if (-not (Test-Path -LiteralPath $c)) { continue } + try { + $content = Get-Content -LiteralPath $c -ErrorAction Stop + $urlLine = $content | Where-Object { $_ -match '^URL=' } | Select-Object -First 1 + if ($urlLine) { return ($urlLine -replace '^URL=', '').Trim() } + } catch {} + } + return $null +} + +function New-StartupLnk { + param( + [string]$Name, + [string]$Target, + [string]$Arguments = '', + [string]$WorkingDirectory = '' + ) + $path = Join-Path $startupDir "$Name.lnk" + $wsh = New-Object -ComObject WScript.Shell + try { + $sc = $wsh.CreateShortcut($path) + $sc.TargetPath = $Target + if ($Arguments) { $sc.Arguments = $Arguments } + if ($WorkingDirectory) { $sc.WorkingDirectory = $WorkingDirectory } + elseif ($Target) { + $parent = Split-Path -Parent $Target + if ($parent) { $sc.WorkingDirectory = $parent } + } + $sc.Save() + return $true + } catch { + Write-Warning "Failed to create startup shortcut '$Name': $_" + return $false + } finally { + try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wsh) | Out-Null } catch {} + } +} + +# ============================================================================ +# Machine number - read current state +# ============================================================================ + +$udcSettingsPath = 'C:\ProgramData\UDC\udc_settings.json' +$udcExePath = 'C:\Program Files\UDC\UDC.exe' +$ednRegPath = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General' + +$currentUdc = $null +$currentEdnc = $null + +if (Test-Path $udcSettingsPath) { + try { + $json = Get-Content $udcSettingsPath -Raw | ConvertFrom-Json + $currentUdc = $json.GeneralSettings.MachineNumber + } catch {} +} + +if (Test-Path $ednRegPath) { + try { + $currentEdnc = (Get-ItemProperty -Path $ednRegPath -Name MachineNo -ErrorAction Stop).MachineNo + } catch {} +} + +$needsMachineNumber = ($currentUdc -eq '9999' -or $currentEdnc -eq '9999') + +# ============================================================================ +# Edge path (for URL-based startup items) +# ============================================================================ + +$edgePath = @( + 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe', + 'C:\Program Files\Microsoft\Edge\Application\msedge.exe' +) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + +# ============================================================================ +# Startup item definitions +# ============================================================================ + +$items = @( + @{ + Num = 1 + Label = 'UDC' + Detail = 'UDC.exe' + Available = (Test-Path 'C:\Program Files\UDC\UDC.exe') + CreateLnk = { + return (New-StartupLnk -Name 'UDC' -Target 'C:\Program Files\UDC\UDC.exe') + } + } + @{ + Num = 2 + Label = 'eDNC' + Detail = 'DncMain.exe' + Available = (Test-Path 'C:\Program Files (x86)\Dnc\bin\DncMain.exe') + CreateLnk = { + return (New-StartupLnk -Name 'eDNC' -Target 'C:\Program Files (x86)\Dnc\bin\DncMain.exe') + } + } + @{ + Num = 3 + Label = 'Defect Tracker' + Detail = 'ClickOnce app' + Available = $true + CreateLnk = { + $src = @( + (Join-Path $publicDesktop 'Defect_Tracker.lnk'), + (Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'Defect_Tracker.lnk') + ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + if ($src) { + $dst = Join-Path $startupDir 'Defect Tracker.lnk' + try { Copy-Item -LiteralPath $src -Destination $dst -Force; return $true } + catch { Write-Warning "Failed to copy Defect Tracker: $_"; return $false } + } else { + Write-Warning "Defect_Tracker.lnk not found on desktop" + return $false + } + } + } + @{ + Num = 4 + Label = 'WJ Shopfloor' + Detail = 'HostExplorer session' + Available = $true + CreateLnk = { + $src = @( + (Join-Path $publicDesktop 'WJ Shopfloor.lnk'), + (Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'WJ Shopfloor.lnk') + ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + if ($src) { + $dst = Join-Path $startupDir 'WJ Shopfloor.lnk' + try { Copy-Item -LiteralPath $src -Destination $dst -Force; return $true } + catch { Write-Warning "Failed to copy WJ Shopfloor: $_"; return $false } + } else { + Write-Warning "WJ Shopfloor.lnk not found on desktop" + return $false + } + } + } + @{ + Num = 5 + Label = 'Plant Apps' + Detail = 'opens in Edge' + Available = [bool]$edgePath + CreateLnk = { + $fallback = 'https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications' + $url = Get-UrlFromFile 'Plant Apps' + if (-not $url) { + Write-Host " Plant Apps .url not found on desktop, using known URL." + $url = $fallback + } + Write-Host " URL: $url" + return (New-StartupLnk -Name 'Plant Apps' -Target $edgePath -Arguments "--new-window `"$url`"") + } + } +) + +# Machine-number logon task is item 6 +$machineNumTaskName = 'Check Machine Number' + +# ============================================================================ +# Interactive UI +# ============================================================================ + +Clear-Host +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Configure PC" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan + +# --- Section 1: Machine number --- +Write-Host "" +Write-Host " MACHINE NUMBER" -ForegroundColor Yellow +Write-Host " ----------------------------------------" + +if ($needsMachineNumber) { + if ($currentUdc) { Write-Host " UDC : $currentUdc" -ForegroundColor Red } + if ($currentEdnc) { Write-Host " eDNC : $currentEdnc" -ForegroundColor Red } + Write-Host "" + $newNum = Read-Host " Enter new machine number (digits only, Enter to skip)" + $newNum = $newNum.Trim() + + if ($newNum -and $newNum -match '^\d+$') { + $updated = @() + + # Stop UDC before editing its JSON + Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object { + try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {} + } + Start-Sleep -Seconds 1 + + # UDC JSON + if (Test-Path $udcSettingsPath) { + try { + $json = Get-Content $udcSettingsPath -Raw | ConvertFrom-Json + $json.GeneralSettings.MachineNumber = $newNum + $json | ConvertTo-Json -Depth 99 | Set-Content -Path $udcSettingsPath -Encoding UTF8 + Write-Host " UDC : $currentUdc -> $newNum" -ForegroundColor Green + $updated += 'UDC' + $currentUdc = $newNum + } catch { Write-Warning " UDC update failed: $_" } + } + + # eDNC registry + if (Test-Path $ednRegPath) { + try { + Set-ItemProperty -Path $ednRegPath -Name MachineNo -Value $newNum -Type String -Force + Write-Host " eDNC : $currentEdnc -> $newNum" -ForegroundColor Green + $updated += 'eDNC' + $currentEdnc = $newNum + } catch { Write-Warning " eDNC update failed: $_" } + } + + # Relaunch UDC + if ((Test-Path $udcExePath) -and $updated -contains 'UDC') { + try { + Start-Process -FilePath $udcExePath -ArgumentList @('-site', '"West Jefferson"', '-machine', $newNum) + Write-Host " UDC.exe relaunched." + } catch {} + } + + $needsMachineNumber = ($currentUdc -eq '9999' -or $currentEdnc -eq '9999') + } elseif ($newNum) { + Write-Host " Invalid (digits only). Skipped." -ForegroundColor Yellow + } else { + Write-Host " Skipped (still 9999)." -ForegroundColor DarkGray + } +} else { + if ($currentUdc) { Write-Host " UDC : $currentUdc" -ForegroundColor Green } + if ($currentEdnc) { Write-Host " eDNC : $currentEdnc" -ForegroundColor Green } +} + +# --- Section 2: Startup items + machine-number logon task --- +Write-Host "" +Write-Host " AUTO-STARTUP ITEMS" -ForegroundColor Yellow +Write-Host " ----------------------------------------" +Write-Host " Toggle which items start at user logon:" +Write-Host "" + +$existingStartup = @(Get-ChildItem -LiteralPath $startupDir -Filter '*.lnk' -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BaseName) + +foreach ($item in $items) { + $on = if ($existingStartup -contains $item.Label) { '[ON]' } else { '[ ]' } + $avail = if ($item.Available) { '' } else { ' (not installed)' } + Write-Host " $($item.Num). $on $($item.Label) - $($item.Detail)$avail" +} + +# Item 6: machine number logon prompt +$machineNumTaskExists = [bool](Get-ScheduledTask -TaskName $machineNumTaskName -ErrorAction SilentlyContinue) +$mnOn = if ($machineNumTaskExists) { '[ON]' } else { '[ ]' } +Write-Host " 6. $mnOn Prompt standard user for machine number if 9999" + +Write-Host "" +Write-Host " Enter numbers to toggle (e.g. 1,2,6), Enter to keep current:" +$selection = Read-Host " Selection" +$selection = $selection.Trim() + +if ($selection) { + $selected = $selection -split '[,\s]+' | Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ } + + # Process startup items 1-5 + 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) { + try { + Remove-Item -LiteralPath $existingLnk -Force + Write-Host " $($item.Label): REMOVED from startup" -ForegroundColor Yellow + } catch { Write-Warning " Failed to remove $($item.Label): $_" } + } else { + $result = & $item.CreateLnk + if ($result) { + Write-Host " $($item.Label): ADDED to startup" -ForegroundColor Green + } + } + } + + # Process item 6: machine number logon task + if ($selected -contains 6) { + if ($machineNumTaskExists) { + # Toggle OFF + try { + Unregister-ScheduledTask -TaskName $machineNumTaskName -Confirm:$false -ErrorAction Stop + Write-Host " Machine number logon prompt: REMOVED" -ForegroundColor Yellow + $machineNumTaskExists = $false + } catch { Write-Warning " Failed to remove task: $_" } + } else { + # Toggle ON - register logon task + # The task needs to run as the logged-in user (for GUI), but + # writing to HKLM + ProgramData requires the ACLs we pre-grant + # during imaging (see task 7 / ACL pre-grant script). + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + $checkScript = Join-Path $scriptDir 'Check-MachineNumber.ps1' + + if (-not (Test-Path -LiteralPath $checkScript)) { + # Fallback: check enrollment staging dir + $checkScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\Check-MachineNumber.ps1' + } + + if (Test-Path -LiteralPath $checkScript) { + try { + $action = New-ScheduledTaskAction ` + -Execute 'powershell.exe' ` + -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$checkScript`"" + + $trigger = New-ScheduledTaskTrigger -AtLogOn + + # Run as the logged-in user (needs GUI for InputBox), NOT + # SYSTEM (SYSTEM can't show UI to the user's desktop). + $principal = New-ScheduledTaskPrincipal ` + -GroupId 'S-1-5-32-545' ` + -RunLevel Limited + + $settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -ExecutionTimeLimit (New-TimeSpan -Minutes 5) + + Register-ScheduledTask ` + -TaskName $machineNumTaskName ` + -Action $action ` + -Trigger $trigger ` + -Principal $principal ` + -Settings $settings ` + -Force ` + -ErrorAction Stop | Out-Null + + Write-Host " Machine number logon prompt: ENABLED" -ForegroundColor Green + Write-Host " (will auto-disable after machine number is set)" -ForegroundColor DarkGray + $machineNumTaskExists = $true + } catch { + Write-Warning " Failed to register task: $_" + } + } else { + Write-Warning " Check-MachineNumber.ps1 not found at $checkScript" + } + } + } +} else { + Write-Host " No changes." -ForegroundColor DarkGray +} + +# --- Summary --- +Write-Host "" +Write-Host " CURRENT STATE" -ForegroundColor Cyan +Write-Host " ----------------------------------------" +$final = @(Get-ChildItem -LiteralPath $startupDir -Filter '*.lnk' -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BaseName) +Write-Host " Startup items:" +if ($final.Count -eq 0) { + Write-Host " (none)" +} else { + foreach ($f in $final) { Write-Host " - $f" } +} +$mnState = if ($machineNumTaskExists) { 'ON (prompts at logon if 9999)' } else { 'OFF' } +Write-Host " Machine number logon prompt: $mnState" + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Done" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +try { Stop-Transcript | Out-Null } catch {} + +Write-Host "Press any key to close..." +try { [void][Console]::ReadKey($true) } catch { [void](Read-Host) } diff --git a/playbook/shopfloor-setup/Standard/02-MachineNumberACLs.ps1 b/playbook/shopfloor-setup/Standard/02-MachineNumberACLs.ps1 new file mode 100644 index 0000000..acc2104 --- /dev/null +++ b/playbook/shopfloor-setup/Standard/02-MachineNumberACLs.ps1 @@ -0,0 +1,55 @@ +# 02-MachineNumberACLs.ps1 - Pre-grant write access on the UDC settings +# file and eDNC registry key so that STANDARD (non-admin) users can update +# the machine number via the Check-MachineNumber logon task without +# elevation or a UAC prompt. +# +# Runs during imaging as admin (type-specific Standard phase, after +# 01-eDNC.ps1 has installed DnC). Only touches Standard PCs. +# +# What gets opened up (narrow scope, not blanket admin): +# - C:\ProgramData\UDC\udc_settings.json -> BUILTIN\Users : Modify +# - HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General +# -> BUILTIN\Users : SetValue + +Write-Host "02-MachineNumberACLs.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" +Write-Host "" +Write-Host "Setting ACLs for standard-user machine number access..." + +# --- UDC settings JSON --- +$udcJson = 'C:\ProgramData\UDC\udc_settings.json' +if (Test-Path -LiteralPath $udcJson) { + try { + $acl = Get-Acl -LiteralPath $udcJson + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + 'BUILTIN\Users', 'Modify', 'Allow') + $acl.AddAccessRule($rule) + Set-Acl -LiteralPath $udcJson -AclObject $acl -ErrorAction Stop + Write-Host " UDC JSON: BUILTIN\Users granted Modify on $udcJson" + } catch { + Write-Warning " Failed to set ACL on $udcJson : $_" + } +} else { + Write-Host " UDC JSON not found at $udcJson - skipping (UDC not installed?)" -ForegroundColor DarkGray +} + +# --- eDNC registry key --- +$ednRegPathWin = 'SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General' +try { + $regKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($ednRegPathWin, $true) + if ($regKey) { + $regSec = $regKey.GetAccessControl() + $rule = New-Object System.Security.AccessControl.RegistryAccessRule( + 'BUILTIN\Users', 'SetValue', 'Allow') + $regSec.AddAccessRule($rule) + $regKey.SetAccessControl($regSec) + $regKey.Close() + Write-Host " eDNC reg: BUILTIN\Users granted SetValue on HKLM:\$ednRegPathWin" + } else { + Write-Host " eDNC registry key not found - skipping (eDNC not installed?)" -ForegroundColor DarkGray + } +} catch { + Write-Warning " Failed to set ACL on HKLM:\$ednRegPathWin : $_" +} + +Write-Host "ACL setup complete."