From a1a78e2ba302ff8f9c9c7e676fec072130dbed9f Mon Sep 17 00:00:00 2001 From: cproudlock Date: Wed, 8 Apr 2026 14:06:26 -0400 Subject: [PATCH] PXE preinstall pipeline + Set-MachineNumber helper for Standard PCs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a local-install pipeline so Standard shopfloor PCs get Oracle, the VC++ redists (2008-2022), and UDC installed during PXE imaging via Samba instead of pulling ~215 MB per device from Azure blob over the corporate WAN. Intune DSC then verifies (already-installed apps are skipped) and the only Azure traffic on the happy path is ~11 KB of CustomScripts wrapper polling. New files: - playbook/preinstall/preinstall.json — curated app list with PCTypes filter and per-app detection rules. Install order puts VC++ 2008 LAST so its (formerly) reboot-triggering bootstrapper doesn't kill the runner mid-loop. (2008 itself now uses extracted vc_red.msi with REBOOT=ReallySuppress; the reorder is defense in depth.) - playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 — the runner. Numbered 00- so it runs first in the baseline sequence. Reads preinstall.json, filters by PCTYPE, polls for completion via detection check (handles UDC's hung WPF process by killing it once detection passes), uses synchronous WriteThrough logging that survives hard reboots, preserves log history across runs. - playbook/shopfloor-setup/Standard/Set-MachineNumber.{ps1,bat} — desktop helper for SupportUser. Reads current UDC + eDNC machine numbers, prompts via VB InputBox, validates digits-only, kills running UDC, edits both C:\ProgramData\UDC\udc_settings.json and HKLM\…\GE Aircraft Engines\DNC\General\MachineNo, relaunches UDC. Lets a tech assign a real machine number to a mass-produced PC without admin/LAPS. - playbook/sync-preinstall.sh — workstation helper to push installer binaries from /home/camp/pxe-images/main/ to the live PXE Samba. Changes: - playbook/startnet.cmd + startnet-template.cmd — add xcopy to stage preinstall bundle from Y:\preinstall\ to W:\PreInstall\ during the WinPE imaging phase, gated on PCTYPE being set. - playbook/pxe_server_setup.yml — create /srv/samba/enrollment/preinstall + installers/ directories and deploy preinstall.json there. - playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 — bump AutoLogonCount to 99 at start (defense against any installer triggering an immediate reboot mid-dispatcher; final line still resets to 2 on successful completion). Copy Set-MachineNumber.{ps1,bat} to SupportUser desktop on Standard PCs. Co-Authored-By: Claude Opus 4.6 (1M context) --- playbook/preinstall/preinstall.json | 72 +++++ playbook/pxe_server_setup.yml | 16 + .../shopfloor-setup/Run-ShopfloorSetup.ps1 | 17 ++ .../Shopfloor/00-PreInstall-MachineApps.ps1 | 284 ++++++++++++++++++ .../Standard/Set-MachineNumber.bat | 6 + .../Standard/Set-MachineNumber.ps1 | 141 +++++++++ playbook/startnet.cmd | 15 + playbook/sync-preinstall.sh | 151 ++++++++++ startnet-template.cmd | 21 +- 9 files changed, 720 insertions(+), 3 deletions(-) create mode 100644 playbook/preinstall/preinstall.json create mode 100644 playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 create mode 100644 playbook/shopfloor-setup/Standard/Set-MachineNumber.bat create mode 100644 playbook/shopfloor-setup/Standard/Set-MachineNumber.ps1 create mode 100755 playbook/sync-preinstall.sh diff --git a/playbook/preinstall/preinstall.json b/playbook/preinstall/preinstall.json new file mode 100644 index 0000000..7b894c3 --- /dev/null +++ b/playbook/preinstall/preinstall.json @@ -0,0 +1,72 @@ +{ + "Version": "1.0", + "Site": "West Jefferson", + "Applications": [ + { + "Name": "Oracle Client 10.2.0.3", + "Installer": "Oracle 10.2.0.3.msi", + "Type": "MSI", + "InstallArgs": "/qn /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\ORACLE\\KEY_OraClientInfra10_2_0", + "DetectionName": "ORACLE_HOME_NAME", + "DetectionValue": "OraClientInfra10_2_0", + "PCTypes": ["*"] + }, + { + "Name": "VC++ Redistributable 2010 x86", + "Installer": "vcredist2010_x86.exe", + "Type": "EXE", + "InstallArgs": "/quiet /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{F0C3E5D1-1ADE-321E-8167-68EF0DE699A5}", + "PCTypes": ["*"] + }, + { + "Name": "VC++ Redistributable 2012 x86", + "Installer": "vcredist2012_x86.exe", + "Type": "EXE", + "InstallArgs": "/quiet /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BD95A8CD-1D9F-35AD-981A-3E7925026EBB}", + "PCTypes": ["*"] + }, + { + "Name": "VC++ Redistributable 2013 x86", + "Installer": "vcredist2013_x86.exe", + "Type": "EXE", + "InstallArgs": "/quiet /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{13A4EE12-23EA-3371-91EE-EFB36DDFFF3E}", + "PCTypes": ["*"] + }, + { + "Name": "VC++ Redistributable 2015-2022 x86", + "Installer": "vcredist2015_2017_2019_2022_x86.exe", + "Type": "EXE", + "InstallArgs": "/install /quiet /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{922480B5-CAEB-4B1B-AAA4-9716EFDCE26B}", + "PCTypes": ["*"] + }, + { + "Name": "UDC", + "Installer": "UDC_Setup.exe", + "Type": "EXE", + "InstallArgs": "\"West Jefferson\" 9999", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\UDC", + "PCTypes": ["Standard"] + }, + { + "_comment": "VC++ 2008 — installed via the extracted vc_red.msi (NOT the bootstrapper) because vcredist2008_x86.exe ignores /norestart (per Aaron Stebner's Microsoft docs). REBOOT=ReallySuppress is a Windows Installer property that hard-blocks reboots even when files are in use; msiexec may return 3010 but won't actually reboot. vc_red.cab must live in the same directory as vc_red.msi or msiexec can't find the data files.", + "Name": "VC++ Redistributable 2008 x86", + "Installer": "vc_red.msi", + "Type": "MSI", + "InstallArgs": "/qn /norestart REBOOT=ReallySuppress", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{9BE518E6-ECC6-35A9-88E4-87755C07200F}", + "PCTypes": ["*"] + } + ] +} diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml index 846624a..17dd309 100644 --- a/playbook/pxe_server_setup.yml +++ b/playbook/pxe_server_setup.yml @@ -313,6 +313,22 @@ directory_mode: '0755' ignore_errors: yes + - name: "Create preinstall bundle directory on enrollment share" + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /srv/samba/enrollment/preinstall + - /srv/samba/enrollment/preinstall/installers + + - name: "Deploy preinstall.json (installer binaries staged separately)" + copy: + src: "{{ usb_mount }}/preinstall/preinstall.json" + dest: /srv/samba/enrollment/preinstall/preinstall.json + mode: '0644' + ignore_errors: yes + - name: "Create BIOS update directory on enrollment share" file: path: /srv/samba/enrollment/BIOS diff --git a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 index 1123c1e..f490d39 100644 --- a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 +++ b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 @@ -1,6 +1,12 @@ # Run-ShopfloorSetup.ps1 — Dispatcher for shopfloor PC type setup # Runs Shopfloor baseline scripts first, then type-specific scripts on top. +# 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 +# everything succeeds. +reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 99 /f | Out-Null + # Cancel any pending reboot so it doesn't interrupt setup shutdown -a 2>$null @@ -77,6 +83,17 @@ if (Test-Path $syncScript) { Write-Host "sync_intune.bat copied to desktop." } +# Standard PCs get the UDC/eDNC machine number helper +if ($pcType -eq "Standard") { + foreach ($helper in @("Set-MachineNumber.bat", "Set-MachineNumber.ps1")) { + $src = Join-Path $setupDir "Standard\$helper" + if (Test-Path $src) { + Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$helper" -Force + Write-Host "$helper copied to SupportUser desktop." + } + } +} + # Set auto-logon to expire after 2 more logins 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." diff --git a/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 b/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 new file mode 100644 index 0000000..c0dd51e --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 @@ -0,0 +1,284 @@ +# 00-PreInstall-MachineApps.ps1 — Install pre-staged apps from C:\PreInstall (Shopfloor baseline) +# +# Reads C:\PreInstall\preinstall.json (staged by startnet.cmd during WinPE phase) and +# installs each app whose PCTypes filter matches the current PC type. Detection runs first +# so already-installed apps are skipped. Numbered 00- so it runs before all other Shopfloor +# baseline scripts — apps need to exist before later scripts (e.g. 01-eDNC.ps1) reference +# them or rely on their state. +# +# Failure mode: log + continue. Intune DSC (Simple-Install.ps1) is the safety net for any +# install that fails here. +# +# Logs to C:\Logs\PreInstall\install.log (wiped each run). + +$preInstallDir = "C:\PreInstall" +$jsonPath = Join-Path $preInstallDir "preinstall.json" +$installerDir = Join-Path $preInstallDir "installers" +$logDir = "C:\Logs\PreInstall" +$logFile = Join-Path $logDir "install.log" + +# --- Setup logging --- +# IMPORTANT: do NOT wipe the log on each run. The runner can get killed by an +# installer-triggered reboot mid-execution. On the next autologon, the dispatcher +# re-runs us — if we wipe the log here, we destroy the forensic record from the +# previous (interrupted) run. Instead, append a session header so runs are visible +# but history is preserved. +if (-not (Test-Path $logDir)) { + New-Item -Path $logDir -ItemType Directory -Force | Out-Null +} + +function Write-PreInstallLog { + param( + [Parameter(Mandatory=$true, Position=0)] + [string]$Message, + [ValidateSet('INFO','WARN','ERROR')] + [string]$Level = 'INFO' + ) + $stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $line = "[$stamp] [$Level] $Message" + Write-Host $line + + # Synchronous write-through so each line hits disk immediately, even if a system + # reboot kills the process right after this call returns. Bypasses the OS write + # cache via FileOptions.WriteThrough. + try { + $fs = New-Object System.IO.FileStream( + $logFile, + [System.IO.FileMode]::Append, + [System.IO.FileAccess]::Write, + [System.IO.FileShare]::Read, + 4096, + [System.IO.FileOptions]::WriteThrough + ) + $bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n") + $fs.Write($bytes, 0, $bytes.Length) + $fs.Flush() + $fs.Dispose() + } + catch { + # Last-resort fallback if the FileStream open fails for any reason + Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue + } +} + +# Session header so multiple runs are visually distinguishable in the log +Write-PreInstallLog "================================================================" +Write-PreInstallLog "=== Runner session start (PID $PID) ===" +Write-PreInstallLog "================================================================" + +# --- Bail early if no preinstall bundle staged --- +if (-not (Test-Path $jsonPath)) { + Write-PreInstallLog "No preinstall.json at $jsonPath - skipping (no bundle staged for this PC)." + exit 0 +} + +# --- Read PCTYPE from C:\Enrollment\pc-type.txt (set by startnet.cmd) --- +$pcTypeFile = "C:\Enrollment\pc-type.txt" +if (-not (Test-Path $pcTypeFile)) { + Write-PreInstallLog "No pc-type.txt at $pcTypeFile - skipping" "WARN" + exit 0 +} +$pcType = (Get-Content $pcTypeFile -First 1).Trim() +if (-not $pcType) { + Write-PreInstallLog "pc-type.txt is empty - skipping" "WARN" + exit 0 +} +Write-PreInstallLog "PC type: $pcType" + +# --- Parse JSON --- +try { + $config = Get-Content $jsonPath -Raw | ConvertFrom-Json +} catch { + Write-PreInstallLog "Failed to parse $jsonPath : $_" "ERROR" + exit 0 +} + +if (-not $config.Applications) { + Write-PreInstallLog "No Applications in preinstall.json" + exit 0 +} + +Write-PreInstallLog "Staged installer dir: $installerDir" +Write-PreInstallLog "Found $($config.Applications.Count) app entries in preinstall.json" + +# --- Detection helper (mirrors Simple-Install.ps1's Test-ApplicationInstalled) --- +function Test-AppInstalled { + param($App) + + if (-not $App.DetectionMethod) { + return $false + } + + try { + switch ($App.DetectionMethod) { + "Registry" { + if (-not (Test-Path $App.DetectionPath)) { + return $false + } + if ($App.DetectionName) { + $value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue + if (-not $value) { + return $false + } + if ($App.DetectionValue) { + return ($value.$($App.DetectionName) -eq $App.DetectionValue) + } + return $true + } + return $true + } + "File" { + return Test-Path $App.DetectionPath + } + default { + Write-PreInstallLog " Unknown detection method: $($App.DetectionMethod)" "WARN" + return $false + } + } + } catch { + Write-PreInstallLog " Detection check threw: $_" "WARN" + return $false + } +} + +# --- Iterate apps --- +$installed = 0 +$skipped = 0 +$failed = 0 + +foreach ($app in $config.Applications) { + # Cancel any reboot a previous installer scheduled (some respect /norestart, some + # don't — VC++ 2008's bootstrapper sometimes triggers an immediate Windows reboot + # despite the flag). Doing this BEFORE each install protects the rest of the loop. + & shutdown.exe /a 2>$null + + Write-PreInstallLog "==> $($app.Name)" + + # Filter by PCTypes + $allowedTypes = @($app.PCTypes) + $matchesType = ($allowedTypes -contains "*") -or ($allowedTypes -contains $pcType) + if (-not $matchesType) { + Write-PreInstallLog " PCTypes filter excludes '$pcType' (allowed: $($allowedTypes -join ', ')) - skipping" + $skipped++ + continue + } + + # Detection check + if (Test-AppInstalled -App $app) { + Write-PreInstallLog " Already installed - skipping" + $skipped++ + continue + } + + # Locate installer file + $installerPath = Join-Path $installerDir $app.Installer + if (-not (Test-Path $installerPath)) { + Write-PreInstallLog " Installer file not found: $installerPath" "ERROR" + $failed++ + continue + } + + Write-PreInstallLog " Installing from $installerPath" + if ($app.InstallArgs) { + Write-PreInstallLog " InstallArgs: $($app.InstallArgs)" + } + + try { + # Start without -Wait so we can poll. UDC_Setup.exe in particular hangs forever + # after install (it spawns UDC.exe and hides its main window without exiting), + # so we can't rely on the process exiting on its own. + if ($app.Type -eq "MSI") { + $msiArgs = "/i `"$installerPath`"" + if ($app.InstallArgs) { + $msiArgs += " " + $app.InstallArgs + } + $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList $msiArgs -PassThru -NoNewWindow + } + elseif ($app.Type -eq "EXE") { + if ($app.InstallArgs) { + $proc = Start-Process -FilePath $installerPath -ArgumentList $app.InstallArgs -PassThru -NoNewWindow + } else { + $proc = Start-Process -FilePath $installerPath -PassThru -NoNewWindow + } + } + else { + Write-PreInstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR" + $failed++ + continue + } + + # Poll for completion: process exit OR detection success (whichever happens first) + $timeoutSec = 600 # 10 min hard cap per app + $pollInterval = 5 + $elapsed = 0 + $killedAfterDetect = $false + + while ($elapsed -lt $timeoutSec) { + if ($proc.HasExited) { break } + + # If detection passes mid-install, the installer already did its job — + # we can kill any zombie process (like UDC_Setup.exe waiting on its hidden + # WPF window) and move on. + if (Test-AppInstalled -App $app) { + Write-PreInstallLog " Detection passed at $elapsed s - killing installer to advance" + try { $proc.Kill(); $proc.WaitForExit(5000) | Out-Null } catch { } + + # UDC's installer auto-launches UDC.exe in silent mode. Kill that too + # so it can't write the placeholder MachineNumber to udc_settings.json. + if ($app.Name -eq "UDC") { + Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object { + try { $_.Kill(); $_.WaitForExit(2000) | Out-Null } catch { } + } + } + + $killedAfterDetect = $true + break + } + + Start-Sleep -Seconds $pollInterval + $elapsed += $pollInterval + } + + if (-not $proc.HasExited -and -not $killedAfterDetect) { + Write-PreInstallLog " TIMEOUT after $timeoutSec seconds - killing installer" "ERROR" + try { $proc.Kill() } catch { } + if ($app.Name -eq "UDC") { + Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object { + try { $_.Kill() } catch { } + } + } + $failed++ + continue + } + + if ($killedAfterDetect) { + Write-PreInstallLog " SUCCESS (verified via detection during install)" + $installed++ + } + elseif ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010) { + Write-PreInstallLog " Exit code $($proc.ExitCode) after $elapsed s - SUCCESS" + if ($proc.ExitCode -eq 3010) { + Write-PreInstallLog " (Reboot required for $($app.Name))" + } + $installed++ + } + else { + Write-PreInstallLog " Exit code $($proc.ExitCode) - FAILED" "ERROR" + $failed++ + } + } catch { + Write-PreInstallLog " Install threw: $_" "ERROR" + $failed++ + } +} + +Write-PreInstallLog "============================================" +Write-PreInstallLog "PreInstall complete: $installed installed, $skipped skipped, $failed failed" +Write-PreInstallLog "============================================" + +# Final reboot cancel — if the last installer in the loop scheduled one, the +# dispatcher's later `shutdown /a` won't fire until the next baseline script starts. +# Cancel here so control returns cleanly to Run-ShopfloorSetup.ps1. +& shutdown.exe /a 2>$null + +exit 0 diff --git a/playbook/shopfloor-setup/Standard/Set-MachineNumber.bat b/playbook/shopfloor-setup/Standard/Set-MachineNumber.bat new file mode 100644 index 0000000..842ce01 --- /dev/null +++ b/playbook/shopfloor-setup/Standard/Set-MachineNumber.bat @@ -0,0 +1,6 @@ +@echo off +REM Set-MachineNumber.bat — Wrapper for Set-MachineNumber.ps1 +REM Runs the PowerShell helper with bypass execution policy so a double-click +REM from the desktop just works. + +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Set-MachineNumber.ps1" diff --git a/playbook/shopfloor-setup/Standard/Set-MachineNumber.ps1 b/playbook/shopfloor-setup/Standard/Set-MachineNumber.ps1 new file mode 100644 index 0000000..d9d0cdb --- /dev/null +++ b/playbook/shopfloor-setup/Standard/Set-MachineNumber.ps1 @@ -0,0 +1,141 @@ +# Set-MachineNumber.ps1 — Update UDC + eDNC machine number on a Standard shopfloor PC +# +# Purpose: +# Both UDC and eDNC use the same per-machine identifier ("Workstation Number" / +# "Machine Number"). On Standard PCs imaged via PXE preinstall, both are installed +# with a placeholder. When the PC is brought to its physical machine and assigned +# a real number, this helper updates both apps in one step. +# +# Persistence locations updated: +# 1. UDC: C:\ProgramData\UDC\udc_settings.json (GeneralSettings.MachineNumber) +# 2. eDNC: HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General\MachineNo +# +# After updating, kills any running UDC.exe and relaunches it with the new args +# so the in-memory state matches the persisted value. +# +# Run as SupportUser (admin). Requires write access to ProgramData and HKLM. + +Add-Type -AssemblyName Microsoft.VisualBasic +Add-Type -AssemblyName System.Windows.Forms + +$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 for display --- +$currentUdc = $null +$currentEdnc = $null + +if (Test-Path $udcSettingsPath) { + try { + $json = Get-Content $udcSettingsPath -Raw | ConvertFrom-Json + $currentUdc = $json.GeneralSettings.MachineNumber + } catch { + $currentUdc = "(unreadable: $_)" + } +} + +if (Test-Path $ednRegPath) { + try { + $currentEdnc = (Get-ItemProperty -Path $ednRegPath -Name MachineNo -ErrorAction Stop).MachineNo + } catch { + $currentEdnc = $null + } +} + +# --- Show prompt with current state --- +$promptLines = @() +$promptLines += "Current UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not set)' })" +$promptLines += "Current eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not set)' })" +$promptLines += "" +$promptLines += "Enter the new Machine Number for this PC:" +$prompt = $promptLines -join "`n" + +$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "") + +if ([string]::IsNullOrWhiteSpace($new)) { + Write-Host "Cancelled." + exit 0 +} + +$new = $new.Trim() + +# --- Validate: digits only (loosen if you need alphanumerics) --- +if ($new -notmatch '^\d+$') { + [System.Windows.Forms.MessageBox]::Show( + "Machine number must be digits only.`n`nYou entered: '$new'", + "Invalid Machine Number", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Error + ) | Out-Null + exit 1 +} + +$results = @() + +# --- 1. Stop UDC.exe before editing its JSON (avoid stale shutdown write) --- +$udcProcs = Get-Process UDC -ErrorAction SilentlyContinue +if ($udcProcs) { + Write-Host "Stopping UDC.exe ($($udcProcs.Count) instance(s))..." + $udcProcs | ForEach-Object { + try { + $_.Kill() + $_.WaitForExit(5000) | Out-Null + } catch { + Write-Warning "Failed to stop UDC.exe (PID $($_.Id)): $_" + } + } + Start-Sleep -Seconds 1 +} + +# --- 2. Update UDC settings JSON --- +if (Test-Path $udcSettingsPath) { + try { + $json = Get-Content $udcSettingsPath -Raw | ConvertFrom-Json + $json.GeneralSettings.MachineNumber = $new + $json | ConvertTo-Json -Depth 99 | Set-Content -Path $udcSettingsPath -Encoding UTF8 + Write-Host "UDC: $currentUdc -> $new" + $results += "UDC updated to $new" + } catch { + Write-Warning "Failed to update UDC settings: $_" + $results += "UDC FAILED: $_" + } +} else { + Write-Warning "UDC settings file not found at $udcSettingsPath. Run UDC.exe at least once to initialize." + $results += "UDC: settings file missing (run UDC.exe once first)" +} + +# --- 3. Update eDNC MachineNo registry value --- +if (Test-Path $ednRegPath) { + try { + Set-ItemProperty -Path $ednRegPath -Name MachineNo -Value $new -Type String -Force + Write-Host "eDNC: $currentEdnc -> $new" + $results += "eDNC updated to $new" + } catch { + Write-Warning "Failed to update eDNC MachineNo: $_" + $results += "eDNC FAILED: $_" + } +} else { + Write-Warning "eDNC registry key not found at $ednRegPath. Is eDNC installed?" + $results += "eDNC: registry key missing (eDNC not installed?)" +} + +# --- 4. Relaunch UDC.exe with new args (if installed) --- +if (Test-Path $udcExePath) { + try { + Start-Process -FilePath $udcExePath -ArgumentList @("-site", "`"$site`"", "-machine", $new) + Write-Host "UDC.exe relaunched." + } catch { + Write-Warning "Failed to relaunch UDC.exe: $_" + } +} + +# --- 5. Show summary --- +$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe." +[System.Windows.Forms.MessageBox]::Show( + $summary, + "Set Machine Number — Done", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Information +) | Out-Null diff --git a/playbook/startnet.cmd b/playbook/startnet.cmd index 3439fc2..6a32409 100644 --- a/playbook/startnet.cmd +++ b/playbook/startnet.cmd @@ -259,6 +259,21 @@ if exist "Y:\shopfloor-setup\%PCTYPE%" ( ) else ( echo WARNING: No setup files found for PC type %PCTYPE%. ) + +REM --- Stage preinstall bundle (apps installed locally to save Azure bandwidth) --- +if exist "Y:\preinstall\preinstall.json" ( + mkdir W:\PreInstall 2>NUL + mkdir W:\PreInstall\installers 2>NUL + copy /Y "Y:\preinstall\preinstall.json" "W:\PreInstall\preinstall.json" + if exist "Y:\preinstall\installers" ( + xcopy /E /Y /I "Y:\preinstall\installers" "W:\PreInstall\installers\" + echo Staged preinstall bundle to W:\PreInstall. + ) else ( + echo WARNING: Y:\preinstall\installers not found - preinstall.json staged without installers. + ) +) else ( + echo No preinstall bundle on PXE server - skipping. +) :pctype_done :cleanup_enroll diff --git a/playbook/sync-preinstall.sh b/playbook/sync-preinstall.sh new file mode 100755 index 0000000..880a050 --- /dev/null +++ b/playbook/sync-preinstall.sh @@ -0,0 +1,151 @@ +#!/bin/bash +# sync-preinstall.sh — Push preinstall.json + installer binaries to the live PXE server. +# +# Run this on the workstation (not on the PXE server) any time: +# - You update preinstall.json in playbook/preinstall/ +# - You update installer binaries in /home/camp/pxe-images/main/ +# +# The PXE server's /srv/samba/enrollment/preinstall/ tree is updated in place. The +# next imaged PC picks up the new files via startnet.cmd's xcopy during WinPE phase. +# +# Usage: +# ./playbook/sync-preinstall.sh +# +# Requires: sshpass (apt install sshpass), scp, ssh + +set -euo pipefail + +# --- Config --- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +PXE_HOST="${PXE_HOST:-10.9.100.1}" +PXE_USER="${PXE_USER:-pxe}" +PXE_PASS="${PXE_PASS:-pxe}" + +PREINSTALL_JSON="$PROJECT_ROOT/playbook/preinstall/preinstall.json" +PXE_IMAGES_DIR="${PXE_IMAGES_DIR:-/home/camp/pxe-images/main}" + +REMOTE_DIR="/srv/samba/enrollment/preinstall" +REMOTE_INSTALLERS="$REMOTE_DIR/installers" +REMOTE_TEMP="/tmp/preinstall-stage" + +# Files to push (source paths under PXE_IMAGES_DIR). +# vc_red.msi + vc_red.cab are extracted from vcredist2008_x86.exe and used directly +# (instead of the bootstrapper) because the 2008 bootstrapper ignores /norestart and +# triggers an immediate reboot. The MSI honors REBOOT=ReallySuppress, the bootstrapper +# does not. The .cab MUST be named "vc_red.cab" exactly because that name is hardcoded +# in the MSI's Media table. +INSTALLERS=( + "dependencies/vc_red.msi" + "dependencies/vc_red.cab" + "dependencies/vcredist2010_x86.exe" + "dependencies/vcredist2012_x86.exe" + "dependencies/vcredist2013_x86.exe" + "dependencies/vcredist2015_2017_2019_2022_x86.exe" + "machineapps/UDC_Setup.exe" + "globalassets/oracleclient/Oracle 10.2.0.3.msi" +) + +# --- Helpers --- +ssh_run() { + sshpass -p "$PXE_PASS" ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR "$PXE_USER@$PXE_HOST" "$@" +} + +scp_to() { + # Remote path is double-escaped: outer ssh layer + inner shell layer. + # Wrap in single-quotes inside the destination so spaces in filenames survive. + sshpass -p "$PXE_PASS" scp -o StrictHostKeyChecking=no -o LogLevel=ERROR "$1" "$PXE_USER@$PXE_HOST:'$2'" +} + +# --- Validate sources --- +echo "Validating source files..." + +if [ ! -f "$PREINSTALL_JSON" ]; then + echo "ERROR: preinstall.json not found at $PREINSTALL_JSON" >&2 + exit 1 +fi + +missing=0 +for rel in "${INSTALLERS[@]}"; do + src="$PXE_IMAGES_DIR/$rel" + if [ ! -f "$src" ]; then + echo " MISSING: $src" >&2 + missing=$((missing + 1)) + else + printf " OK %10d %s\n" "$(stat -c %s "$src")" "$rel" + fi +done + +if [ "$missing" -gt 0 ]; then + echo "ERROR: $missing installer file(s) missing in $PXE_IMAGES_DIR" >&2 + exit 1 +fi + +# --- Verify PXE server reachable --- +echo "Pinging PXE server $PXE_HOST..." +if ! ping -c 1 -W 2 "$PXE_HOST" >/dev/null 2>&1; then + echo "ERROR: PXE server $PXE_HOST not reachable" >&2 + exit 1 +fi + +# --- Stage to /tmp on PXE, then sudo install --- +echo "Staging files to $PXE_HOST:$REMOTE_TEMP..." +ssh_run "mkdir -p $REMOTE_TEMP && rm -f $REMOTE_TEMP/*" + +# preinstall.json +echo " -> preinstall.json" +scp_to "$PREINSTALL_JSON" "$REMOTE_TEMP/preinstall.json" + +# installers (preserve filenames including spaces) +for rel in "${INSTALLERS[@]}"; do + src="$PXE_IMAGES_DIR/$rel" + base="$(basename "$rel")" + echo " -> $base" + scp_to "$src" "$REMOTE_TEMP/$base" +done + +# --- Build remote install script (runs under sudo on PXE) --- +LOCAL_TEMP_SCRIPT="$(mktemp /tmp/sync-preinstall-remote.XXXXXX.sh)" +trap 'rm -f "$LOCAL_TEMP_SCRIPT"' EXIT + +cat > "$LOCAL_TEMP_SCRIPT" < /srv/samba/enrollment/preinstall/preinstall.json +cp "$REMOTE_TEMP/preinstall.json" "$REMOTE_DIR/preinstall.json" +chmod 0644 "$REMOTE_DIR/preinstall.json" +chown root:root "$REMOTE_DIR/preinstall.json" + +# All other files -> installers/ +shopt -s dotglob nullglob +for f in "$REMOTE_TEMP"/*; do + base="\$(basename "\$f")" + if [ "\$base" != "preinstall.json" ] && [ "\$base" != "_install.sh" ]; then + cp "\$f" "$REMOTE_INSTALLERS/\$base" + chmod 0644 "$REMOTE_INSTALLERS/\$base" + chown root:root "$REMOTE_INSTALLERS/\$base" + fi +done + +rm -rf "$REMOTE_TEMP" + +echo +echo "Final state of $REMOTE_DIR:" +ls -la "$REMOTE_DIR" +echo +echo "Final state of $REMOTE_INSTALLERS:" +ls -la "$REMOTE_INSTALLERS" +REMOTE_SCRIPT + +# Stage the install script alongside the data files +scp_to "$LOCAL_TEMP_SCRIPT" "$REMOTE_TEMP/_install.sh" + +# Execute remotely with sudo +echo "Installing files to $REMOTE_DIR (sudo)..." +ssh_run "echo '$PXE_PASS' | sudo -S bash $REMOTE_TEMP/_install.sh" + +echo +echo "Done. Preinstall bundle synced to $PXE_HOST:$REMOTE_DIR" diff --git a/startnet-template.cmd b/startnet-template.cmd index 0116090..6a32409 100644 --- a/startnet-template.cmd +++ b/startnet-template.cmd @@ -86,7 +86,7 @@ echo 2. Wax and Trace echo 3. Keyence echo 4. Genspect echo 5. Display -echo 6. Shopfloor (General) +echo 6. Standard echo. set PCTYPE= set /p pctype_choice=Enter your choice (1-6): @@ -95,7 +95,7 @@ if "%pctype_choice%"=="2" set PCTYPE=WaxAndTrace if "%pctype_choice%"=="3" set PCTYPE=Keyence if "%pctype_choice%"=="4" set PCTYPE=Genspect if "%pctype_choice%"=="5" set PCTYPE=Display -if "%pctype_choice%"=="6" set PCTYPE=Shopfloor +if "%pctype_choice%"=="6" set PCTYPE=Standard if "%PCTYPE%"=="" goto pctype_menu REM --- Display sub-type selection --- @@ -245,13 +245,13 @@ if not "%DISPLAYTYPE%"=="" echo %DISPLAYTYPE%> W:\Enrollment\display-type.txt copy /Y "Y:\shopfloor-setup\Run-ShopfloorSetup.ps1" "W:\Enrollment\Run-ShopfloorSetup.ps1" REM --- Always copy Shopfloor baseline scripts --- mkdir W:\Enrollment\shopfloor-setup 2>NUL +copy /Y "Y:\shopfloor-setup\backup_lockdown.bat" "W:\Enrollment\shopfloor-setup\backup_lockdown.bat" if exist "Y:\shopfloor-setup\Shopfloor" ( mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\" echo Copied Shopfloor baseline setup files. ) REM --- Copy type-specific scripts on top of baseline --- -if "%PCTYPE%"=="Shopfloor" goto pctype_done if exist "Y:\shopfloor-setup\%PCTYPE%" ( mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL xcopy /E /Y /I "Y:\shopfloor-setup\%PCTYPE%" "W:\Enrollment\shopfloor-setup\%PCTYPE%\" @@ -259,6 +259,21 @@ if exist "Y:\shopfloor-setup\%PCTYPE%" ( ) else ( echo WARNING: No setup files found for PC type %PCTYPE%. ) + +REM --- Stage preinstall bundle (apps installed locally to save Azure bandwidth) --- +if exist "Y:\preinstall\preinstall.json" ( + mkdir W:\PreInstall 2>NUL + mkdir W:\PreInstall\installers 2>NUL + copy /Y "Y:\preinstall\preinstall.json" "W:\PreInstall\preinstall.json" + if exist "Y:\preinstall\installers" ( + xcopy /E /Y /I "Y:\preinstall\installers" "W:\PreInstall\installers\" + echo Staged preinstall bundle to W:\PreInstall. + ) else ( + echo WARNING: Y:\preinstall\installers not found - preinstall.json staged without installers. + ) +) else ( + echo No preinstall bundle on PXE server - skipping. +) :pctype_done :cleanup_enroll