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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -77,3 +77,6 @@ secrets.yml
|
|||||||
*_secret
|
*_secret
|
||||||
*_secrets
|
*_secrets
|
||||||
credentials.json
|
credentials.json
|
||||||
|
|
||||||
|
# Pre-staged binary (142 MB) - track via LFS or stage on PXE server, not in regular git
|
||||||
|
playbook/shopfloor-setup/Shopfloor/PrinterInstallerMap.exe
|
||||||
|
|||||||
239
docs/shopfloor-machine-imaging-guide.md
Normal file
239
docs/shopfloor-machine-imaging-guide.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# Shopfloor Machine PC Imaging Guide
|
||||||
|
|
||||||
|
Step-by-step for imaging a new (or replacement) shopfloor PC that will sit at a CNC machine and run UDC, eDNC, NTLARS, MTConnect, and the standard shopfloor toolset.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- PC connected to the **PXE switch** (not the production network yet)
|
||||||
|
- USB mouse + keyboard connected
|
||||||
|
- PXE server is running and reachable (verify by pinging `10.9.100.1` from another PC on the same switch)
|
||||||
|
- **Target machine number** known (e.g., `7605`) — you can enter it at PXE time, or use `9999` as a placeholder if the PC will be configured at the bay later
|
||||||
|
- **ARTS Lockdown request submitted** for this PC (or know that you'll submit one mid-imaging)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: BIOS Configuration
|
||||||
|
|
||||||
|
1. Plug the PC into the **KVM**.
|
||||||
|
2. Power on the PC and begin **tapping F12** to bring up the One-Time-Boot menu.
|
||||||
|
3. Select **BIOS Setup**.
|
||||||
|
4. Toggle **Advanced Setup** to **ENABLED**.
|
||||||
|
5. Click **Boot Configuration**:
|
||||||
|
- Verify **Enable Secure Boot** is **ENABLED**
|
||||||
|
- Verify **Enable Microsoft UEFI CA** is **ENABLED**
|
||||||
|
6. Click **Storage** and verify **SATA/NVMe Operation** is set to **AHCI/NVMe**.
|
||||||
|
7. **If this is a Precision Tower**: click **Security** and **ENABLE "Start Data Wipe"** (wipes existing data on next boot).
|
||||||
|
8. Click **Apply Changes**, then **Exit**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: PXE Boot
|
||||||
|
|
||||||
|
1. Begin **tapping F12** again to return to the One-Time-Boot menu.
|
||||||
|
2. Verify the **network cable is connected to the PXE Server's isolated switch** (NOT the production network).
|
||||||
|
3. From the One-Time-Boot menu, select **ONBOARD NIC (IPV4)**.
|
||||||
|
4. Once the PXE Boot menu appears, select **Windows PE (Image Deployment)**.
|
||||||
|
5. WinPE launches with a command prompt that **automatically updates the BIOS to the latest version** before prompting you to select the image type.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Image + Enrollment Selection
|
||||||
|
|
||||||
|
1. **WinPE Setup Menu**: select `3. GEA Shopfloor`.
|
||||||
|
2. **GCCH Enrollment Profile**: select `1. No Office` (machine PCs don't need Office).
|
||||||
|
3. **Shopfloor PC Type**: select `6. Standard`.
|
||||||
|
4. **Standard PC Sub-Type**: select `1. Machine`.
|
||||||
|
5. **Machine number prompt**:
|
||||||
|
- If the PC's target bay is known: type the machine number (e.g., `7605`) and press Enter.
|
||||||
|
- If the bay isn't known yet: just press Enter to use placeholder `9999`. You'll set the real number after the PC is physically placed at the bay (see Step 9).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Imaging (Automated Phase)
|
||||||
|
|
||||||
|
Once GE Image Setup launches:
|
||||||
|
|
||||||
|
1. Click **Start**.
|
||||||
|
2. The process runs unattended through:
|
||||||
|
- Disk partition + Windows install
|
||||||
|
- PreInstall apps (Oracle Client 11.2, OpenText HostExplorer, VC++ Redists, eDNC if Standard-Machine, UDC, etc.)
|
||||||
|
- GE-Enforce framework registration
|
||||||
|
- First reboot
|
||||||
|
3. Note the **Serial Number** from the screen — log it in your tracking sheet.
|
||||||
|
4. The PC reboots and auto-logs in as `SupportUser`. The "Shopfloor Intune Sync" PowerShell window opens automatically.
|
||||||
|
|
||||||
|
**This whole phase takes ~20-40 minutes** depending on hardware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Monitor Intune Enrollment
|
||||||
|
|
||||||
|
Once the **Shopfloor Intune Sync** window is open, you'll see a 5-phase status table that refreshes every 30 seconds:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Intune Registration [WAITING/IN PROGRESS/COMPLETE]
|
||||||
|
2. Device Configuration [WAITING/IN PROGRESS/COMPLETE]
|
||||||
|
3. Software Deployment [WAITING/IN PROGRESS/COMPLETE]
|
||||||
|
4. Credential Setup [WAITING/IN PROGRESS/COMPLETE]
|
||||||
|
5. Lockdown [WAITING/IN PROGRESS/COMPLETE]
|
||||||
|
```
|
||||||
|
|
||||||
|
Below the table, an **Intune Device ID** + QR code appears. Scan the QR with your phone to copy the device ID into your ARTS Lockdown request.
|
||||||
|
|
||||||
|
### What to do at each phase
|
||||||
|
|
||||||
|
- **Phase 1 → COMPLETE**: a `>> Select Device Category in Intune portal` hint appears. **Action**: in Intune, set the Device Category to `Shopfloor` (or whatever your site uses).
|
||||||
|
- **Phase 2 → COMPLETE**: just keep watching.
|
||||||
|
- **Phase 3 → IN PROGRESS forever**: known issue — the DSC `device-config.yaml` download is currently failing with a 403. **It does NOT block setup-complete** — Phases 4 and 5 are independent. Skip ahead.
|
||||||
|
- **Phase 4 → COMPLETE**: SFLD share creds landed in registry. A `>> Initiate ARTS Lockdown request` hint appears if you haven't already.
|
||||||
|
- **Phase 5 → COMPLETE**: lockdown applied via Intune Remediation. The script auto-fires "Setup Complete" and reboots the PC.
|
||||||
|
|
||||||
|
If Phase 5 stays WAITING for >30 minutes after Phase 4 completes, see Step 6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Force Lockdown (only if needed)
|
||||||
|
|
||||||
|
If Phase 5 is stuck WAITING for 30+ minutes after Phase 4 completed AND the ARTS Lockdown request is approved:
|
||||||
|
|
||||||
|
1. Open an elevated cmd or PowerShell.
|
||||||
|
2. Run:
|
||||||
|
```
|
||||||
|
C:\Enrollment\shopfloor-setup\Shopfloor\Force-Lockdown.bat
|
||||||
|
```
|
||||||
|
3. It self-elevates via UAC, prompts for confirmation:
|
||||||
|
```
|
||||||
|
Type YES (uppercase) to confirm ARTS request is in place: YES
|
||||||
|
```
|
||||||
|
4. The script runs `sfld_autologon.ps1`, flips Winlogon to ShopFloor autologon, and writes `C:\Enrollment\force-lockdown-applied.txt` on success.
|
||||||
|
5. Within 30 seconds, the Intune Sync window's Phase 5 flips to COMPLETE → "Setup Complete" → reboot.
|
||||||
|
|
||||||
|
**WARNING**: Do NOT run Force-Lockdown without an approved ARTS request. It bypasses the normal Intune Lockdown-group push and will be flagged in the audit trail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Post-Reboot — ShopFloor Autologon Phase
|
||||||
|
|
||||||
|
After the lockdown reboot, the PC auto-logs in as `ShopFloor` (instead of SupportUser).
|
||||||
|
|
||||||
|
What happens automatically:
|
||||||
|
|
||||||
|
1. WiFi profile (`AESFMA` SSID) lands via Intune.
|
||||||
|
2. PC connects to AESFMA.
|
||||||
|
3. `S:` drive maps to `\\tsgwp00525.wjs.geaerospace.net\shared`.
|
||||||
|
4. **GE Shopfloor Machine Apps Enforce** scheduled task fires on logon.
|
||||||
|
5. Manifest engine reads `\\tsgwp00525\...\common\manifest.json` AND `\\tsgwp00525\...\standard-machine\manifest.json`, evaluates each app entry against current state, runs installer if not detected.
|
||||||
|
6. Apps installed/verified: Adobe Acrobat Reader DC, WJF Defect Tracker, 3OF9 barcode font, Edge IE-Mode site list + policy, VNC firewall rule, Oracle Client 11.2, OpenText HostExplorer ShopFloor, UDC, eDNC + NTLARS, eMxInfo.txt, MTConnect Fanuc/OKUMA/Makino/eDNC variants (per machine number).
|
||||||
|
7. May take 5-15 minutes on first logon (cold app installs); subsequent logons skip-and-validate in <30 seconds.
|
||||||
|
|
||||||
|
You can watch progress in `C:\GE Aerospace\machineapps-enforce.log`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Move to the Bay
|
||||||
|
|
||||||
|
Physically move the PC to its target machine. Plug into the production ethernet (NOT the PXE switch).
|
||||||
|
|
||||||
|
If the PC doesn't have an assigned machine number yet, or if you used `9999` placeholder at PXE time, continue to Step 9.
|
||||||
|
|
||||||
|
If you entered the real machine number at PXE time, Configure-PC.ps1 already wrote it to UDC, eDNC, the DNC registry, and MTConnect Devices.xml automatically — **skip to Step 10**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 9: Set Machine Number (only if 9999 placeholder was used)
|
||||||
|
|
||||||
|
1. Log in as **SupportUser** (admin).
|
||||||
|
2. Run from Desktop or Start Menu:
|
||||||
|
```
|
||||||
|
Set Machine Number.lnk
|
||||||
|
```
|
||||||
|
(which calls `C:\Enrollment\shopfloor-setup\Standard\Set-MachineNumber.ps1`)
|
||||||
|
3. Type the new machine number (digits only) when the GUI prompts.
|
||||||
|
4. Click OK. The script:
|
||||||
|
- Stops UDC, writes the new number to UDC settings JSON, relaunches UDC
|
||||||
|
- Writes the new number to eDNC registry (`HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General\MachineNo`)
|
||||||
|
- Pulls the per-machine eDNC `.reg` backup from `\\tsgwp00525\...\ntlars-backups\<num>.reg` (restores eFocas/PPDCS/Hssb config for that machine)
|
||||||
|
- Updates MTConnect `Devices.xml` for any installed agent (Fanuc/Okuma/Makino/eDNC) and restarts the agent service
|
||||||
|
5. A summary dialog confirms what was updated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 10: Verify the Machine
|
||||||
|
|
||||||
|
Before signing off, confirm the PC is healthy:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Service health
|
||||||
|
Get-Service | Where-Object { $_.Name -match '^(MTConnect|Makino|MakinoMTConnect|MTConnect eDNC|MTConnect Adapter|UDC|DNC)' } |
|
||||||
|
Format-Table Name, Status, StartType -AutoSize
|
||||||
|
|
||||||
|
# Machine number persisted everywhere
|
||||||
|
"UDC: $((Get-Content 'C:\ProgramData\UDC\udc_settings.json' -Raw | ConvertFrom-Json).GeneralSettings.MachineNumber)"
|
||||||
|
"eDNC: $((Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General' -Name MachineNo).MachineNo)"
|
||||||
|
|
||||||
|
# MTConnect HTTP probe (depends on variant - port 5000 for Fanuc/OKUMA, 5001 for eDNC, 5005 for UDC)
|
||||||
|
Invoke-WebRequest 'http://localhost:5000/probe' -UseBasicParsing -TimeoutSec 3 | Select StatusCode
|
||||||
|
|
||||||
|
# Manifest engine ran cleanly
|
||||||
|
Get-Content 'C:\GE Aerospace\machineapps-enforce.log' -Tail 20
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected healthy state:
|
||||||
|
|
||||||
|
- All MTConnect/UDC/DNC services: **Running** + **Auto** start type
|
||||||
|
- UDC + eDNC machine numbers: **match the assigned bay**
|
||||||
|
- HTTP probe: **HTTP 200** with a `<MTConnectDevices>` XML response
|
||||||
|
- Manifest enforce log: ends with `evaluation complete: N entries, 0 failures` (or similar)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Intune Sync window closes by itself
|
||||||
|
|
||||||
|
It writes `C:\Logs\SFLD\sync_intune_transcript.txt` continuously. Open that log to see what it last reported. Re-launch via:
|
||||||
|
```
|
||||||
|
C:\Enrollment\shopfloor-setup\Shopfloor\sync_intune.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3 stuck at IN PROGRESS
|
||||||
|
|
||||||
|
Known issue — the DSC blob download is 403'ing right now. Doesn't block setup-complete. If you need DSC's wallpaper / start menu pins / FileSystem actions, escalate to IT to fix the SAS token or storage account firewall on `geasfldwestjefferson`. Until then, those visual customizations won't appear — operators won't notice if the start menu pins are absent because they're not the primary workflow.
|
||||||
|
|
||||||
|
### Phase 5 (Lockdown) stays WAITING after 30 minutes
|
||||||
|
|
||||||
|
ARTS request is probably still pending. Confirm approval, then run Force-Lockdown.bat (Step 6).
|
||||||
|
|
||||||
|
### Manifest engine logs show "DllNotFoundException" or "share not reachable"
|
||||||
|
|
||||||
|
PC isn't on AESFMA WiFi yet (or WiFi profile hasn't pushed). Wait 5-10 minutes after the post-lockdown reboot. Verify:
|
||||||
|
```powershell
|
||||||
|
(Get-NetConnectionProfile).Name
|
||||||
|
Test-Path '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\common\manifest.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
If `Test-Path` returns False, WiFi/auth isn't ready. If True, kick the manifest engine manually:
|
||||||
|
```powershell
|
||||||
|
Start-ScheduledTask -TaskName 'GE Shopfloor Machine Apps Enforce'
|
||||||
|
```
|
||||||
|
|
||||||
|
### MTConnect not running after machine-number set
|
||||||
|
|
||||||
|
The wrapper logs land at `C:\GE Aerospace\mtc-install-runservice-batconvert.log`. Common causes: pre-existing Windows Firewall Block rule (rare), Mark-of-the-Web on copied EXEs (the wrapper's Unblock-File sweep handles this), or the bundle isn't on the SFLD share for this variant. Open the log and grep for `ERROR`.
|
||||||
|
|
||||||
|
### Configure-PC machine-number GUI doesn't open
|
||||||
|
|
||||||
|
The script needs a desktop session. Won't run via WinRM/SSH/non-interactive. Make sure you're logged in at the console as SupportUser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- **PXE server**: `10.9.100.1`
|
||||||
|
- **SFLD share**: `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\`
|
||||||
|
- **Manifest engine log**: `C:\GE Aerospace\machineapps-enforce.log`
|
||||||
|
- **Intune sync transcript**: `C:\Logs\SFLD\sync_intune_transcript.txt`
|
||||||
|
- **DSC logs**: `C:\Logs\SFLD\` (DSCDeployment.log, DSCInstall.log, version.txt)
|
||||||
|
- **Per-app install logs**: `C:\Logs\SFLD\Install-*.log`
|
||||||
|
- **Force-Lockdown marker**: `C:\Enrollment\force-lockdown-applied.txt`
|
||||||
|
- **Set-MachineNumber script**: `C:\Enrollment\shopfloor-setup\Standard\Set-MachineNumber.ps1`
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM check-bios.cmd - Check and apply Dell BIOS update from WinPE x64
|
|
||||||
REM Sets BIOS_STATUS for startnet.cmd menu display
|
|
||||||
|
|
||||||
set "BIOSDIR=%~dp0"
|
|
||||||
set "FLASH=%BIOSDIR%Flash64W.exe"
|
|
||||||
set "MANIFEST=%BIOSDIR%models.txt"
|
|
||||||
|
|
||||||
if exist "%FLASH%" goto :flash_ok
|
|
||||||
echo Flash64W.exe not found, skipping BIOS check.
|
|
||||||
set "BIOS_STATUS=Skipped (Flash64W.exe missing)"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:flash_ok
|
|
||||||
if exist "%MANIFEST%" goto :manifest_ok
|
|
||||||
echo models.txt not found, skipping BIOS check.
|
|
||||||
set "BIOS_STATUS=Skipped (models.txt missing)"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:manifest_ok
|
|
||||||
REM --- Get system model from WMI ---
|
|
||||||
set SYSMODEL=
|
|
||||||
for /f "skip=1 tokens=*" %%M in ('wmic csproduct get name 2^>NUL') do (
|
|
||||||
if not defined SYSMODEL set "SYSMODEL=%%M"
|
|
||||||
)
|
|
||||||
for /f "tokens=*" %%a in ("%SYSMODEL%") do set "SYSMODEL=%%a"
|
|
||||||
|
|
||||||
if "%SYSMODEL%"=="" goto :no_model
|
|
||||||
goto :got_model
|
|
||||||
|
|
||||||
:no_model
|
|
||||||
echo Could not detect system model, skipping BIOS check.
|
|
||||||
set "BIOS_STATUS=Skipped (model not detected)"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:got_model
|
|
||||||
echo Model: %SYSMODEL%
|
|
||||||
|
|
||||||
REM --- Get current BIOS version ---
|
|
||||||
set BIOSVER=
|
|
||||||
for /f "skip=1 tokens=*" %%V in ('wmic bios get smbiosbiosversion 2^>NUL') do (
|
|
||||||
if not defined BIOSVER set "BIOSVER=%%V"
|
|
||||||
)
|
|
||||||
for /f "tokens=*" %%a in ("%BIOSVER%") do set "BIOSVER=%%a"
|
|
||||||
echo Current BIOS: %BIOSVER%
|
|
||||||
|
|
||||||
REM --- Read manifest and find matching BIOS file ---
|
|
||||||
set BIOSFILE=
|
|
||||||
set TARGETVER=
|
|
||||||
for /f "usebackq eol=# tokens=1,2,3 delims=|" %%A in ("%MANIFEST%") do (
|
|
||||||
echo "%SYSMODEL%" | find /I "%%A" >NUL
|
|
||||||
if not errorlevel 1 (
|
|
||||||
set "BIOSFILE=%%B"
|
|
||||||
set "TARGETVER=%%C"
|
|
||||||
goto :found_bios
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
echo No BIOS update available for this model.
|
|
||||||
set "BIOS_STATUS=%SYSMODEL% - no update in catalog"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:found_bios
|
|
||||||
if not exist "%BIOSDIR%%BIOSFILE%" goto :bios_file_missing
|
|
||||||
goto :bios_file_ok
|
|
||||||
|
|
||||||
:bios_file_missing
|
|
||||||
echo WARNING: %BIOSFILE% not found in BIOS folder.
|
|
||||||
set "BIOS_STATUS=%SYSMODEL% - %BIOSFILE% missing"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:bios_file_ok
|
|
||||||
REM --- Skip if already at target version ---
|
|
||||||
echo.%BIOSVER%| find /I "%TARGETVER%" >NUL
|
|
||||||
if not errorlevel 1 goto :already_current
|
|
||||||
|
|
||||||
REM --- Compare versions to prevent downgrade ---
|
|
||||||
call :compare_versions "%BIOSVER%" "%TARGETVER%"
|
|
||||||
if "%VERCMP%"=="newer" goto :already_newer
|
|
||||||
goto :do_flash
|
|
||||||
|
|
||||||
:already_current
|
|
||||||
echo BIOS is already up to date - %TARGETVER%
|
|
||||||
set "BIOS_STATUS=%SYSMODEL% v%BIOSVER% (up to date)"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:already_newer
|
|
||||||
echo Current BIOS %BIOSVER% is newer than target %TARGETVER% - skipping.
|
|
||||||
set "BIOS_STATUS=%SYSMODEL% v%BIOSVER% (up to date)"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:do_flash
|
|
||||||
echo Update: %BIOSVER% -^> %TARGETVER%
|
|
||||||
echo Applying BIOS update (this may take a few minutes, do not power off)...
|
|
||||||
|
|
||||||
pushd "%BIOSDIR%"
|
|
||||||
Flash64W.exe /b="%BIOSFILE%" /s /f /l=X:\bios-update.log
|
|
||||||
set FLASHRC=%ERRORLEVEL%
|
|
||||||
popd
|
|
||||||
echo Flash complete (exit code %FLASHRC%).
|
|
||||||
|
|
||||||
if "%FLASHRC%"=="3" goto :already_current
|
|
||||||
if "%FLASHRC%"=="0" goto :flash_done
|
|
||||||
if "%FLASHRC%"=="2" goto :staged
|
|
||||||
if "%FLASHRC%"=="6" goto :staged
|
|
||||||
|
|
||||||
echo WARNING: Flash64W.exe returned unexpected code %FLASHRC%.
|
|
||||||
set "BIOS_STATUS=%SYSMODEL% flash error (code %FLASHRC%)"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:flash_done
|
|
||||||
echo BIOS update complete.
|
|
||||||
set "BIOS_STATUS=%SYSMODEL% updated %BIOSVER% -^> %TARGETVER%"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:staged
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo BIOS update staged successfully.
|
|
||||||
echo It will flash during POST after the
|
|
||||||
echo post-imaging reboot.
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
set "BIOS_STATUS=%SYSMODEL% STAGED %BIOSVER% -^> %TARGETVER% (flashes on reboot)"
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:compare_versions
|
|
||||||
set "VERCMP=equal"
|
|
||||||
set "_CV=%~1"
|
|
||||||
set "_TV=%~2"
|
|
||||||
for /f "tokens=1,2,3 delims=." %%a in ("%_CV%") do (
|
|
||||||
set /a "C1=%%a" 2>NUL
|
|
||||||
set /a "C2=%%b" 2>NUL
|
|
||||||
set /a "C3=%%c" 2>NUL
|
|
||||||
)
|
|
||||||
for /f "tokens=1,2,3 delims=." %%a in ("%_TV%") do (
|
|
||||||
set /a "T1=%%a" 2>NUL
|
|
||||||
set /a "T2=%%b" 2>NUL
|
|
||||||
set /a "T3=%%c" 2>NUL
|
|
||||||
)
|
|
||||||
if %C1% GTR %T1% ( set "VERCMP=newer" & goto :eof )
|
|
||||||
if %C1% LSS %T1% ( set "VERCMP=older" & goto :eof )
|
|
||||||
if %C2% GTR %T2% ( set "VERCMP=newer" & goto :eof )
|
|
||||||
if %C2% LSS %T2% ( set "VERCMP=older" & goto :eof )
|
|
||||||
if %C3% GTR %T3% ( set "VERCMP=newer" & goto :eof )
|
|
||||||
if %C3% LSS %T3% ( set "VERCMP=older" & goto :eof )
|
|
||||||
set "VERCMP=equal"
|
|
||||||
goto :eof
|
|
||||||
@@ -1,43 +1,30 @@
|
|||||||
# migrate-to-wifi.ps1 - Invoked by FlatUnattendW10-shopfloor.xml as Order 5
|
# migrate-to-wifi.ps1 - No-op as of 2026-04-24.
|
||||||
# during first logon, right after wait-for-internet.ps1 and right before
|
|
||||||
# GCCH enrollment. Moves the machine off wired onto WiFi for the rest of
|
|
||||||
# the imaging chain so the PXE ethernet cable can be safely disconnected.
|
|
||||||
#
|
#
|
||||||
# Gated: if there is no physical Wi-Fi adapter on the machine (tower /
|
# Previously this disabled all wired NICs at first logon to keep PPKG /
|
||||||
# desktop case), the whole migration is a no-op. Previously this step
|
# Intune enrollment routing internet traffic via WiFi. The wired NIC was
|
||||||
# disabled all wired adapters unconditionally and then waited for WiFi
|
# preferred by Windows because the PXE dnsmasq was handing out a default
|
||||||
# internet that could never arrive on towers, hanging first logon forever.
|
# gateway (dhcp-option=3,10.9.100.1) which Windows installed as a default
|
||||||
|
# route, and the lower interface metric of wired beat WiFi. Internet-bound
|
||||||
|
# traffic then black-holed at 10.9.100.1 (the PXE server, which doesn't
|
||||||
|
# forward).
|
||||||
|
#
|
||||||
|
# That root cause was fixed by removing the dhcp-option=3 and =6 lines
|
||||||
|
# from /etc/dnsmasq.conf on the PXE server. Without an advertised gateway
|
||||||
|
# on the PXE side, Windows can't add a default route via wired, so all
|
||||||
|
# internet traffic uses WiFi by default and the wired NIC stays harmless
|
||||||
|
# for same-subnet PXE/SMB traffic to 10.9.100.1.
|
||||||
|
#
|
||||||
|
# Side effect of the original behavior was an eDNC race: eDNC autostart
|
||||||
|
# would fire while the wired NIC was still disabled and hit WSAEINVAL
|
||||||
|
# (Winsock 10022) trying to bind to a non-existent local IP, looping its
|
||||||
|
# retry timer until a SYSTEM task re-enabled the NIC after SFLD creds
|
||||||
|
# landed (often ~30+ min later). Keeping the NIC up the whole time
|
||||||
|
# eliminates that race.
|
||||||
|
#
|
||||||
|
# Kept as a no-op file (instead of removed) so the unattend XML's Order 5
|
||||||
|
# RunSynchronousCommand entry does not need to be re-numbered. If the
|
||||||
|
# dhcp-option lines ever come back, this can be reverted to the disable
|
||||||
|
# logic by restoring from git.
|
||||||
|
|
||||||
$wifi = Get-NetAdapter -Physical -ErrorAction SilentlyContinue |
|
Write-Host 'migrate-to-wifi.ps1: no-op (wired NIC kept enabled).'
|
||||||
Where-Object { $_.InterfaceDescription -match 'Wi-?Fi|Wireless|WLAN|802\.11' }
|
|
||||||
|
|
||||||
if (-not $wifi) {
|
|
||||||
Write-Host 'No WiFi adapter - staying on ethernet.' -ForegroundColor Cyan
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Get-NetAdapter -Physical |
|
|
||||||
Where-Object { $_.InterfaceDescription -notmatch 'Wi-?Fi|Wireless|WLAN|802\.11' } |
|
|
||||||
Disable-NetAdapter -Confirm:$false
|
|
||||||
|
|
||||||
$deadline = (Get-Date).AddMinutes(5)
|
|
||||||
$ok = $false
|
|
||||||
while ((Get-Date) -lt $deadline) {
|
|
||||||
try {
|
|
||||||
if (Test-NetConnection -ComputerName login.microsoftonline.us -Port 443 -InformationLevel Quiet -WarningAction SilentlyContinue) {
|
|
||||||
$ok = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
Start-Sleep -Seconds 5
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ok) {
|
|
||||||
Write-Host 'Internet confirmed over WiFi.' -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host 'WiFi internet timeout - re-enabling ethernet.' -ForegroundColor Yellow
|
|
||||||
Get-NetAdapter -Physical |
|
|
||||||
Where-Object { $_.InterfaceDescription -notmatch 'Wi-?Fi|Wireless|WLAN|802\.11' } |
|
|
||||||
Enable-NetAdapter -Confirm:$false
|
|
||||||
}
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -65,6 +65,41 @@ set RC=%ERRORLEVEL%
|
|||||||
|
|
||||||
echo [%STAMP%] OUI exit code: %RC% >> "%LOG%"
|
echo [%STAMP%] OUI exit code: %RC% >> "%LOG%"
|
||||||
|
|
||||||
|
REM --- Drop GE-customized tnsnames.ora / sqlnet.ora / ldap.ora into ORACLE_HOME.
|
||||||
|
REM The zip ships these in client\ora\ but Oracle's setup.exe does not consume
|
||||||
|
REM them - that copy is the GE Wise wrapper's job, which we bypass. Without
|
||||||
|
REM these, every Oracle-dependent app (eDNC/UDC/NTLARS/CMM tooling) fails with
|
||||||
|
REM ORA-12154 "TNS could not resolve the connect identifier specified".
|
||||||
|
REM
|
||||||
|
REM Per-file override: if %~dp0<name>.ora exists (deployed alongside this
|
||||||
|
REM wrapper on the PXE server / SFLD share), use it instead of the bundled
|
||||||
|
REM copy. Lets us push an updated tnsnames.ora without repackaging the zip.
|
||||||
|
REM
|
||||||
|
REM Only run when OUI succeeded (RC 0 or 3). On failure ORACLE_HOME may not
|
||||||
|
REM be fully populated and the copy targets may not exist.
|
||||||
|
if %RC%==0 goto :do_ora_copy
|
||||||
|
if %RC%==3 goto :do_ora_copy
|
||||||
|
goto :skip_ora_copy
|
||||||
|
|
||||||
|
:do_ora_copy
|
||||||
|
set "ORA_DST=C:\Apps\product\11.2.0\client_1\network\admin"
|
||||||
|
if not exist "%ORA_DST%" (
|
||||||
|
echo [%STAMP%] WARN: %ORA_DST% does not exist after OUI - skipping .ora copy >> "%LOG%"
|
||||||
|
goto :skip_ora_copy
|
||||||
|
)
|
||||||
|
for %%F in (tnsnames.ora sqlnet.ora ldap.ora) do (
|
||||||
|
set "ORA_SRC=%~dp0%%F"
|
||||||
|
if not exist "!ORA_SRC!" set "ORA_SRC=%CLIENT_DIR%\ora\%%F"
|
||||||
|
if exist "!ORA_SRC!" (
|
||||||
|
echo [%STAMP%] Copying %%F from !ORA_SRC! to %ORA_DST%\%%F >> "%LOG%"
|
||||||
|
copy /Y "!ORA_SRC!" "%ORA_DST%\%%F" >> "%LOG%" 2>&1
|
||||||
|
) else (
|
||||||
|
echo [%STAMP%] WARN: %%F not found in either %~dp0 or %CLIENT_DIR%\ora >> "%LOG%"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
:skip_ora_copy
|
||||||
|
|
||||||
REM Cleanup staging dir to reclaim ~1.5 GB - OUI copies everything to ORACLE_HOME
|
REM Cleanup staging dir to reclaim ~1.5 GB - OUI copies everything to ORACLE_HOME
|
||||||
echo [%STAMP%] Cleaning up staging dir >> "%LOG%"
|
echo [%STAMP%] Cleaning up staging dir >> "%LOG%"
|
||||||
rmdir /s /q "%STAGING%" >nul 2>&1
|
rmdir /s /q "%STAGING%" >nul 2>&1
|
||||||
|
|||||||
12173
playbook/preinstall/oracle/tnsnames.ora
Executable file
12173
playbook/preinstall/oracle/tnsnames.ora
Executable file
File diff suppressed because it is too large
Load Diff
@@ -148,8 +148,19 @@
|
|||||||
interface={{ pxe_iface }}
|
interface={{ pxe_iface }}
|
||||||
bind-interfaces
|
bind-interfaces
|
||||||
dhcp-range=10.9.100.10,10.9.100.100,12h
|
dhcp-range=10.9.100.10,10.9.100.100,12h
|
||||||
dhcp-option=3,10.9.100.1
|
# No default gateway (option 3) and no DNS (option 6) handed out:
|
||||||
dhcp-option=6,8.8.8.8
|
# the PXE network is isolated and the PXE server does not forward
|
||||||
|
# internet traffic. Previously we set both, which made imaged PCs
|
||||||
|
# add a default route via 10.9.100.1 and prefer it over WiFi (lower
|
||||||
|
# interface metric). PPKG / Intune enrollment then black-holed
|
||||||
|
# internet-bound traffic. The fix used to be migrate-to-wifi.ps1
|
||||||
|
# disabling the wired NIC during first-logon, which created an
|
||||||
|
# eDNC race (10022 socket errors until the SYSTEM task re-enabled
|
||||||
|
# the wired NIC much later). Removing these options entirely lets
|
||||||
|
# Windows route internet via WiFi and same-subnet PXE/SMB traffic
|
||||||
|
# via wired, no migration script needed.
|
||||||
|
# dhcp-option=3,10.9.100.1
|
||||||
|
# dhcp-option=6,8.8.8.8
|
||||||
enable-tftp
|
enable-tftp
|
||||||
tftp-root={{ tftp_dir }}
|
tftp-root={{ tftp_dir }}
|
||||||
dhcp-boot=ipxe.efi
|
dhcp-boot=ipxe.efi
|
||||||
@@ -448,6 +459,18 @@
|
|||||||
mode: '0755'
|
mode: '0755'
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
|
|
||||||
|
# Per-file overrides for the .ora config dropped by the wrapper post-install.
|
||||||
|
# The zip's client/ora/ contains dated defaults; shipping an updated
|
||||||
|
# tnsnames.ora alongside the wrapper lets us refresh the DB catalog without
|
||||||
|
# repackaging the 686 MB zip. sqlnet.ora and ldap.ora can be added here the
|
||||||
|
# same way if they ever need to diverge from the zip's bundled copies.
|
||||||
|
- name: "Deploy updated tnsnames.ora override to pre-install/installers/oracle/"
|
||||||
|
copy:
|
||||||
|
src: "{{ usb_mount }}/preinstall/oracle/tnsnames.ora"
|
||||||
|
dest: /srv/samba/enrollment/pre-install/installers/oracle/tnsnames.ora
|
||||||
|
mode: '0644'
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
- name: "Deploy Oracle 11.2 zip (686 MB) from USB to pre-install/installers/oracle/"
|
- name: "Deploy Oracle 11.2 zip (686 MB) from USB to pre-install/installers/oracle/"
|
||||||
shell: >
|
shell: >
|
||||||
if [ -f "{{ usb_root }}/oracle/Oracle_OracleDatabase_11r2_V03.zip" ]; then
|
if [ -f "{{ usb_root }}/oracle/Oracle_OracleDatabase_11r2_V03.zip" ]; then
|
||||||
@@ -459,21 +482,40 @@
|
|||||||
fi
|
fi
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
|
|
||||||
- name: "Deploy BIOS check script and manifest to pre-install/bios/"
|
- name: "Ensure winpeapps/_shared/BIOS directory exists"
|
||||||
|
file:
|
||||||
|
path: /srv/samba/winpeapps/_shared/BIOS
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: "Deploy BIOS check script + manifest to winpeapps/_shared/BIOS/"
|
||||||
|
# Path matches what startnet.cmd reads at WinPE boot:
|
||||||
|
# net use B: \\10.9.100.1\winpeapps\_shared
|
||||||
|
# if exist B:\BIOS\check-bios.cmd ...
|
||||||
|
# Earlier deploy targeted enrollment/pre-install/bios/ (different share)
|
||||||
|
# which startnet.cmd never read, so BIOS_STATUS perma-stuck on
|
||||||
|
# "No BIOS check (share unavailable)". Corrected 2026-04-28.
|
||||||
copy:
|
copy:
|
||||||
src: "{{ usb_mount }}/shopfloor-setup/BIOS/{{ item }}"
|
src: "{{ usb_mount }}/shopfloor-setup/BIOS/{{ item }}"
|
||||||
dest: "/srv/samba/enrollment/pre-install/bios/{{ item }}"
|
dest: "/srv/samba/winpeapps/_shared/BIOS/{{ item }}"
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
loop:
|
loop:
|
||||||
- check-bios.cmd
|
- check-bios.cmd
|
||||||
- models.txt
|
- models.txt
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
|
|
||||||
- name: "Deploy BIOS update binaries from USB to pre-install/bios/"
|
- name: "Deploy Dell Flash64W.exe to winpeapps/_shared/BIOS/"
|
||||||
|
copy:
|
||||||
|
src: "{{ usb_root }}/bios/Flash64W.exe"
|
||||||
|
dest: /srv/samba/winpeapps/_shared/BIOS/Flash64W.exe
|
||||||
|
mode: '0644'
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: "Deploy BIOS update binaries from USB to winpeapps/_shared/BIOS/"
|
||||||
shell: >
|
shell: >
|
||||||
if [ -d "{{ usb_root }}/bios" ]; then
|
if [ -d "{{ usb_root }}/bios" ]; then
|
||||||
cp -f {{ usb_root }}/bios/*.exe /srv/samba/enrollment/pre-install/bios/ 2>/dev/null || true
|
cp -f {{ usb_root }}/bios/*.exe /srv/samba/winpeapps/_shared/BIOS/ 2>/dev/null || true
|
||||||
count=$(find /srv/samba/enrollment/pre-install/bios -name '*.exe' | wc -l)
|
count=$(find /srv/samba/winpeapps/_shared/BIOS -name '*.exe' | wc -l)
|
||||||
echo "Deployed $count BIOS binaries"
|
echo "Deployed $count BIOS binaries"
|
||||||
else
|
else
|
||||||
echo "No bios/ on USB - skipping"
|
echo "No bios/ on USB - skipping"
|
||||||
|
|||||||
@@ -1,40 +1,39 @@
|
|||||||
@echo off
|
@echo off
|
||||||
REM check-bios.cmd - Check and apply Dell BIOS update from WinPE x64
|
REM check-bios.cmd - Check and apply Dell BIOS update from WinPE x64
|
||||||
REM Called from startnet.cmd before imaging menu
|
REM Sets BIOS_STATUS for startnet.cmd menu display
|
||||||
REM Requires: Flash64W.exe (Dell 64-Bit BIOS Flash Utility) in same directory
|
|
||||||
REM
|
|
||||||
REM Exit behavior:
|
|
||||||
REM - BIOS update applied -> reboots automatically (flashes during POST)
|
|
||||||
REM - Already up to date -> returns to startnet.cmd
|
|
||||||
REM - No match / no files -> returns to startnet.cmd
|
|
||||||
|
|
||||||
set BIOSDIR=%~dp0
|
set "BIOSDIR=%~dp0"
|
||||||
set FLASH=%BIOSDIR%Flash64W.exe
|
set "FLASH=%BIOSDIR%Flash64W.exe"
|
||||||
set MANIFEST=%BIOSDIR%models.txt
|
set "MANIFEST=%BIOSDIR%models.txt"
|
||||||
|
|
||||||
if not exist "%FLASH%" (
|
if exist "%FLASH%" goto :flash_ok
|
||||||
echo Flash64W.exe not found, skipping BIOS check.
|
echo Flash64W.exe not found, skipping BIOS check.
|
||||||
exit /b 0
|
set "BIOS_STATUS=Skipped (Flash64W.exe missing)"
|
||||||
)
|
exit /b 0
|
||||||
|
|
||||||
if not exist "%MANIFEST%" (
|
:flash_ok
|
||||||
echo models.txt not found, skipping BIOS check.
|
if exist "%MANIFEST%" goto :manifest_ok
|
||||||
exit /b 0
|
echo models.txt not found, skipping BIOS check.
|
||||||
)
|
set "BIOS_STATUS=Skipped (models.txt missing)"
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:manifest_ok
|
||||||
REM --- Get system model from WMI ---
|
REM --- Get system model from WMI ---
|
||||||
set SYSMODEL=
|
set SYSMODEL=
|
||||||
for /f "skip=1 tokens=*" %%M in ('wmic csproduct get name 2^>NUL') do (
|
for /f "skip=1 tokens=*" %%M in ('wmic csproduct get name 2^>NUL') do (
|
||||||
if not defined SYSMODEL set "SYSMODEL=%%M"
|
if not defined SYSMODEL set "SYSMODEL=%%M"
|
||||||
)
|
)
|
||||||
REM Trim trailing whitespace
|
|
||||||
for /f "tokens=*" %%a in ("%SYSMODEL%") do set "SYSMODEL=%%a"
|
for /f "tokens=*" %%a in ("%SYSMODEL%") do set "SYSMODEL=%%a"
|
||||||
|
|
||||||
if "%SYSMODEL%"=="" (
|
if "%SYSMODEL%"=="" goto :no_model
|
||||||
echo Could not detect system model, skipping BIOS check.
|
goto :got_model
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
|
:no_model
|
||||||
|
echo Could not detect system model, skipping BIOS check.
|
||||||
|
set "BIOS_STATUS=Skipped (model not detected)"
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:got_model
|
||||||
echo Model: %SYSMODEL%
|
echo Model: %SYSMODEL%
|
||||||
|
|
||||||
REM --- Get current BIOS version ---
|
REM --- Get current BIOS version ---
|
||||||
@@ -46,7 +45,6 @@ for /f "tokens=*" %%a in ("%BIOSVER%") do set "BIOSVER=%%a"
|
|||||||
echo Current BIOS: %BIOSVER%
|
echo Current BIOS: %BIOSVER%
|
||||||
|
|
||||||
REM --- Read manifest and find matching BIOS file ---
|
REM --- Read manifest and find matching BIOS file ---
|
||||||
REM Format: ModelSubstring|BIOSFile|Version
|
|
||||||
set BIOSFILE=
|
set BIOSFILE=
|
||||||
set TARGETVER=
|
set TARGETVER=
|
||||||
for /f "usebackq eol=# tokens=1,2,3 delims=|" %%A in ("%MANIFEST%") do (
|
for /f "usebackq eol=# tokens=1,2,3 delims=|" %%A in ("%MANIFEST%") do (
|
||||||
@@ -59,60 +57,60 @@ for /f "usebackq eol=# tokens=1,2,3 delims=|" %%A in ("%MANIFEST%") do (
|
|||||||
)
|
)
|
||||||
|
|
||||||
echo No BIOS update available for this model.
|
echo No BIOS update available for this model.
|
||||||
|
set "BIOS_STATUS=%SYSMODEL% - no update in catalog"
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:found_bios
|
:found_bios
|
||||||
if not exist "%BIOSDIR%%BIOSFILE%" (
|
if not exist "%BIOSDIR%%BIOSFILE%" goto :bios_file_missing
|
||||||
echo WARNING: %BIOSFILE% not found in BIOS folder.
|
goto :bios_file_ok
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
|
:bios_file_missing
|
||||||
|
echo WARNING: %BIOSFILE% not found in BIOS folder.
|
||||||
|
set "BIOS_STATUS=%SYSMODEL% - %BIOSFILE% missing"
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:bios_file_ok
|
||||||
REM --- Skip if already at target version ---
|
REM --- Skip if already at target version ---
|
||||||
echo.%BIOSVER%| find /I "%TARGETVER%" >NUL
|
echo.%BIOSVER%| find /I "%TARGETVER%" >NUL
|
||||||
if not errorlevel 1 goto :already_current
|
if not errorlevel 1 goto :already_current
|
||||||
|
|
||||||
REM --- Compare versions to prevent downgrade ---
|
REM --- Compare versions to prevent downgrade ---
|
||||||
REM Split current and target into major.minor.patch and compare numerically
|
|
||||||
call :compare_versions "%BIOSVER%" "%TARGETVER%"
|
call :compare_versions "%BIOSVER%" "%TARGETVER%"
|
||||||
if "%VERCMP%"=="newer" goto :already_newer
|
if "%VERCMP%"=="newer" goto :already_newer
|
||||||
goto :do_flash
|
goto :do_flash
|
||||||
|
|
||||||
:already_current
|
:already_current
|
||||||
echo BIOS is already up to date - %TARGETVER%
|
echo BIOS is already up to date - %TARGETVER%
|
||||||
|
set "BIOS_STATUS=%SYSMODEL% v%BIOSVER% (up to date)"
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:already_newer
|
:already_newer
|
||||||
echo Current BIOS %BIOSVER% is newer than target %TARGETVER% - skipping.
|
echo Current BIOS %BIOSVER% is newer than target %TARGETVER% - skipping.
|
||||||
|
set "BIOS_STATUS=%SYSMODEL% v%BIOSVER% (up to date)"
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:do_flash
|
:do_flash
|
||||||
|
|
||||||
echo Update: %BIOSVER% -^> %TARGETVER%
|
echo Update: %BIOSVER% -^> %TARGETVER%
|
||||||
echo Applying BIOS update (this may take a few minutes, do not power off)...
|
echo Applying BIOS update (this may take a few minutes, do not power off)...
|
||||||
|
|
||||||
REM --- Run Flash64W.exe from BIOS directory to avoid UNC path issues ---
|
|
||||||
REM Exit codes: 0=success, 2=reboot needed, 3=already current, 6=reboot needed
|
|
||||||
pushd "%BIOSDIR%"
|
pushd "%BIOSDIR%"
|
||||||
Flash64W.exe /b="%BIOSFILE%" /s /f /l=X:\bios-update.log
|
Flash64W.exe /b="%BIOSFILE%" /s /f /l=X:\bios-update.log
|
||||||
set FLASHRC=%ERRORLEVEL%
|
set FLASHRC=%ERRORLEVEL%
|
||||||
popd
|
popd
|
||||||
echo Flash complete (exit code %FLASHRC%).
|
echo Flash complete (exit code %FLASHRC%).
|
||||||
|
|
||||||
if "%FLASHRC%"=="3" (
|
if "%FLASHRC%"=="3" goto :already_current
|
||||||
echo BIOS is already up to date.
|
if "%FLASHRC%"=="0" goto :flash_done
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%FLASHRC%"=="0" (
|
|
||||||
echo BIOS update complete.
|
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%FLASHRC%"=="2" goto :staged
|
if "%FLASHRC%"=="2" goto :staged
|
||||||
if "%FLASHRC%"=="6" goto :staged
|
if "%FLASHRC%"=="6" goto :staged
|
||||||
|
|
||||||
echo WARNING: Flash64W.exe returned unexpected code %FLASHRC%.
|
echo WARNING: Flash64W.exe returned unexpected code %FLASHRC%.
|
||||||
echo Check X:\bios-update.log for details.
|
set "BIOS_STATUS=%SYSMODEL% flash error (code %FLASHRC%)"
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:flash_done
|
||||||
|
echo BIOS update complete.
|
||||||
|
set "BIOS_STATUS=%SYSMODEL% updated %BIOSVER% -^> %TARGETVER%"
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:staged
|
:staged
|
||||||
@@ -123,31 +121,23 @@ echo It will flash during POST after the
|
|||||||
echo post-imaging reboot.
|
echo post-imaging reboot.
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo.
|
echo.
|
||||||
|
set "BIOS_STATUS=%SYSMODEL% STAGED %BIOSVER% -^> %TARGETVER% (flashes on reboot)"
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
REM ============================================================
|
|
||||||
REM compare_versions - Compare two dotted version strings
|
|
||||||
REM Usage: call :compare_versions "current" "target"
|
|
||||||
REM Sets VERCMP=newer if current > target, older if current < target, equal if same
|
|
||||||
REM ============================================================
|
|
||||||
:compare_versions
|
:compare_versions
|
||||||
set "VERCMP=equal"
|
set "VERCMP=equal"
|
||||||
set "_CV=%~1"
|
set "_CV=%~1"
|
||||||
set "_TV=%~2"
|
set "_TV=%~2"
|
||||||
|
|
||||||
REM Parse current version parts
|
|
||||||
for /f "tokens=1,2,3 delims=." %%a in ("%_CV%") do (
|
for /f "tokens=1,2,3 delims=." %%a in ("%_CV%") do (
|
||||||
set /a "C1=%%a" 2>NUL
|
set /a "C1=%%a" 2>NUL
|
||||||
set /a "C2=%%b" 2>NUL
|
set /a "C2=%%b" 2>NUL
|
||||||
set /a "C3=%%c" 2>NUL
|
set /a "C3=%%c" 2>NUL
|
||||||
)
|
)
|
||||||
REM Parse target version parts
|
|
||||||
for /f "tokens=1,2,3 delims=." %%a in ("%_TV%") do (
|
for /f "tokens=1,2,3 delims=." %%a in ("%_TV%") do (
|
||||||
set /a "T1=%%a" 2>NUL
|
set /a "T1=%%a" 2>NUL
|
||||||
set /a "T2=%%b" 2>NUL
|
set /a "T2=%%b" 2>NUL
|
||||||
set /a "T3=%%c" 2>NUL
|
set /a "T3=%%c" 2>NUL
|
||||||
)
|
)
|
||||||
|
|
||||||
if %C1% GTR %T1% ( set "VERCMP=newer" & goto :eof )
|
if %C1% GTR %T1% ( set "VERCMP=newer" & goto :eof )
|
||||||
if %C1% LSS %T1% ( set "VERCMP=older" & goto :eof )
|
if %C1% LSS %T1% ( set "VERCMP=older" & goto :eof )
|
||||||
if %C2% GTR %T2% ( set "VERCMP=newer" & goto :eof )
|
if %C2% GTR %T2% ( set "VERCMP=newer" & goto :eof )
|
||||||
|
|||||||
@@ -45,3 +45,4 @@ Precision 7865 Tower|Precision_7865_1.6.1.exe|1.6.1
|
|||||||
Precision 7875 Tower|Precision_7875_SHP_02.07.03.exe|2.7.3
|
Precision 7875 Tower|Precision_7875_SHP_02.07.03.exe|2.7.3
|
||||||
Rugged 14 RB14250|Dell_Pro_Rugged_RB14250_RA13250_1.13.1.exe|1.13.1
|
Rugged 14 RB14250|Dell_Pro_Rugged_RB14250_RA13250_1.13.1.exe|1.13.1
|
||||||
Tower Plus 7020|OptiPlex_7020_1.22.1_SEMB.exe|1.22.1
|
Tower Plus 7020|OptiPlex_7020_1.22.1_SEMB.exe|1.22.1
|
||||||
|
Tower Plus QBT1250|Dell_Pro_QBT1250_QBS1250_QBM1250_QCT1250_QCS1250_QCM1250_SEMB_1.12.2.exe|1.12.2
|
||||||
|
|||||||
@@ -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%"
|
PowerShell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%"
|
||||||
set RC=%errorLevel%
|
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 ============================================================
|
echo ============================================================
|
||||||
echo Lockdown script exit code: %RC%
|
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_Remediation.log - "Autologon set for ShopFloor user ..."
|
||||||
# Autologon_Detection.log - "... matches the expected value: 1"
|
# Autologon_Detection.log - "... matches the expected value: 1"
|
||||||
#
|
#
|
||||||
# These are the TRUE end-of-chain signals. The DefaultUserName flip and
|
# IME logs are append-only - they never rotate or clear. Earlier logic
|
||||||
# admin rename that we previously checked land at PPKG time (too early);
|
# used `-match` against the whole raw file, which stayed true forever
|
||||||
# the IME logs only appear after Intune enrollment + category + DSC +
|
# once success was ever recorded, even if the policy later changed and
|
||||||
# Remediation cycle, which is the actual lockdown completion.
|
# 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'
|
$imeLogs = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs'
|
||||||
|
|
||||||
|
# --- Remediation: scan from tail for the LATEST result line
|
||||||
$remLog = Join-Path $imeLogs 'Autologon_Remediation.log'
|
$remLog = Join-Path $imeLogs 'Autologon_Remediation.log'
|
||||||
$remDone = $false
|
$remDone = $false
|
||||||
if (Test-Path $remLog) {
|
if (Test-Path $remLog) {
|
||||||
try {
|
try {
|
||||||
$content = Get-Content $remLog -Raw -ErrorAction Stop
|
$lines = Get-Content $remLog -ErrorAction Stop
|
||||||
$remDone = ($content -match 'Autologon set for ShopFloor')
|
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 {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detection: scan from tail for the LATEST "matches the expected value: <N>"
|
||||||
$detLog = Join-Path $imeLogs 'Autologon_Detection.log'
|
$detLog = Join-Path $imeLogs 'Autologon_Detection.log'
|
||||||
$detDone = $false
|
$detDone = $false
|
||||||
if (Test-Path $detLog) {
|
if (Test-Path $detLog) {
|
||||||
try {
|
try {
|
||||||
$content = Get-Content $detLog -Raw -ErrorAction Stop
|
$lines = Get-Content $detLog -ErrorAction Stop
|
||||||
$detDone = ($content -match 'matches the expected value:\s*1')
|
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 {}
|
} 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 @{
|
return @{
|
||||||
RemediationApplied = $remDone
|
RemediationApplied = $remDone
|
||||||
DetectionPassed = $detDone
|
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 }
|
@{ Ok = $Snap.Phase1.PoliciesArriving; Failed = $false }
|
||||||
)
|
)
|
||||||
|
|
||||||
# Phase 6 / Lockdown (shared by both flows, rendered last)
|
# 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 @(
|
$p6Status = Get-PhaseStatus @(
|
||||||
@{ Ok = $Snap.Phase6.RemediationApplied; Failed = $false },
|
@{ Ok = $Snap.Phase6.RemediationApplied; Failed = $false },
|
||||||
@{ Ok = $Snap.Phase6.DetectionPassed; Failed = $false }
|
@{ Ok = $Snap.Phase6.DetectionPassed; Failed = $false },
|
||||||
|
@{ Ok = $Snap.Phase6.RegistryMatches; Failed = $false }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $skipDsc) {
|
if (-not $skipDsc) {
|
||||||
# ---- Standard / CMM / etc. (DSC flow) ----
|
# ---- Standard / CMM / etc. (DSC flow) ----
|
||||||
@@ -600,24 +667,16 @@ function Format-Snapshot {
|
|||||||
@{ Ok = $Snap.Phase3.DeployComplete; Failed = $false },
|
@{ Ok = $Snap.Phase3.DeployComplete; Failed = $false },
|
||||||
@{ Ok = $Snap.Phase3.InstallComplete; Failed = $false }
|
@{ Ok = $Snap.Phase3.InstallComplete; Failed = $false }
|
||||||
)
|
)
|
||||||
$p4HasFailed = $false; $p4AllDone = $true; $p4AnyStarted = $false
|
# Phase 4 (per-script Application Install) was retired 2026-04-28:
|
||||||
$p4HasScripts = ($Snap.Phase4 -and $Snap.Phase4.Count -gt 0)
|
# apps now deploy via the GE-Enforce manifest engine off
|
||||||
if ($p4HasScripts) {
|
# \\tsgwp00525\...\machineapps\, gated on AESFMA WiFi + S: mount
|
||||||
foreach ($s in $Snap.Phase4) {
|
# which both happen post-reboot. The monitor's responsibility ends
|
||||||
if ($s.Status -eq 'failed') { $p4HasFailed = $true }
|
# at "DSC bootstrap + creds + lockdown landed, time to reboot."
|
||||||
if ($s.Status -ne 'done') { $p4AllDone = $false }
|
# The manifest engine takes over from there on the next ShopFloor
|
||||||
if ($s.Status -ne 'pending') { $p4AnyStarted = $true }
|
# logon.
|
||||||
}
|
|
||||||
} 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' }
|
|
||||||
|
|
||||||
$p5Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
|
$p4Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
|
||||||
$p5Status = Get-PhaseStatus @(
|
$p4Status = Get-PhaseStatus @(
|
||||||
@{ Ok = $Snap.Phase5.ConsumeCredsTask; Failed = $false },
|
@{ Ok = $Snap.Phase5.ConsumeCredsTask; Failed = $false },
|
||||||
@{ Ok = $Snap.Phase5.CredsPopulated; 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 ' 2. Device Configuration ' -NoNewline; Format-StatusTag $p2Status; Write-Host ''
|
||||||
Write-Host ' 3. Software Deployment ' -NoNewline; Format-StatusTag $p3Status; 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 ' 4. Credential Setup ' -NoNewline; Format-StatusTag $p4Status; Write-Host ''
|
||||||
Write-Host ' 5. Credential Setup ' -NoNewline; Format-StatusTag $p5Status; Write-Host ''
|
if ($p4Done -and $p6Status -ne 'COMPLETE') {
|
||||||
if ($p5Done -and $p6Status -ne 'COMPLETE') {
|
|
||||||
Write-Host ' >> Initiate ARTS Lockdown request' -ForegroundColor Yellow
|
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 {
|
} else {
|
||||||
# ---- Display (no DSC, no credentials) ----
|
# ---- Display (no DSC, no credentials) ----
|
||||||
# Phases: 1 Registration -> 2 Device Configuration -> 3 Lockdown
|
# Phases: 1 Registration -> 2 Device Configuration -> 3 Lockdown
|
||||||
@@ -863,17 +921,21 @@ try {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host $qrText
|
Write-Host $qrText
|
||||||
|
|
||||||
# Final state: every phase landed. The gate is intentionally strict
|
# Final state: every phase landed. Apps are no longer in the gate -
|
||||||
# because each piece is needed for the device to function:
|
# they deploy via the GE-Enforce manifest engine post-reboot, gated
|
||||||
# - DscInstallComplete: device-config.yaml apps + custom scripts ran
|
# 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,
|
# - CredsPopulated: SFLD share creds in HKLM (Machine-Enforce,
|
||||||
# Acrobat-Enforce, CMM-Enforce all need these)
|
# Acrobat-Enforce, CMM-Enforce, manifest
|
||||||
|
# engine all need these)
|
||||||
# - LockdownComplete: kiosk policy baseline + Winlogon flipped to
|
# - LockdownComplete: kiosk policy baseline + Winlogon flipped to
|
||||||
# ShopFloor autologon + admin renamed
|
# 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.
|
# Display PCs skip this whole branch via $skipDsc above.
|
||||||
if ($snap.DscInstallComplete -and
|
if ($snap.Phase5.CredsPopulated -and $snap.LockdownComplete) {
|
||||||
$snap.Phase5.CredsPopulated -and
|
|
||||||
$snap.LockdownComplete) {
|
|
||||||
Invoke-SetupComplete
|
Invoke-SetupComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,8 +974,7 @@ try {
|
|||||||
# Intune Remediation script mid-execution and delay lockdown.
|
# Intune Remediation script mid-execution and delay lockdown.
|
||||||
$waitingForLockdownOnly = $false
|
$waitingForLockdownOnly = $false
|
||||||
if (-not $skipDsc) {
|
if (-not $skipDsc) {
|
||||||
$waitingForLockdownOnly = ($snap.DscInstallComplete -and
|
$waitingForLockdownOnly = ($snap.Phase5.CredsPopulated -and
|
||||||
$snap.Phase5.CredsPopulated -and
|
|
||||||
-not $snap.LockdownComplete)
|
-not $snap.LockdownComplete)
|
||||||
} else {
|
} else {
|
||||||
$waitingForLockdownOnly = ($snap.Phase1.AzureAdJoined -and
|
$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
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
{ "$ref": "common.fmsResolver" },
|
{ "$ref": "common.fmsResolver" },
|
||||||
{ "$ref": "common.oracle" },
|
{ "$ref": "common.oracle" },
|
||||||
{ "$ref": "common.openText" },
|
{ "$ref": "common.openText" },
|
||||||
|
{ "name": "UDC", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\UDC", "name": "DisplayVersion", "value": "1.0.34" } },
|
||||||
{ "name": "FMS Primary host", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\GE Aircraft Engines\\Dnc\\FMS", "name": "FMSHostPrimary", "value": "wjfms3.ae.ge.com" } },
|
{ "name": "FMS Primary host", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\GE Aircraft Engines\\Dnc\\FMS", "name": "FMSHostPrimary", "value": "wjfms3.ae.ge.com" } },
|
||||||
{ "name": "FMS Secondary host", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\GE Aircraft Engines\\Dnc\\FMS", "name": "FMSHostSecondary", "value": "10.233.112.158" } },
|
{ "name": "FMS Secondary host", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\GE Aircraft Engines\\Dnc\\FMS", "name": "FMSHostSecondary", "value": "10.233.112.158" } },
|
||||||
{ "name": "eDNC bundles NTLARS", "verify": { "method": "FileVersion", "path": "C:\\Program Files (x86)\\Dnc\\bin\\DncMain.exe", "value": "6.4.5.0" } }
|
{ "name": "eDNC bundles NTLARS", "verify": { "method": "FileVersion", "path": "C:\\Program Files (x86)\\Dnc\\bin\\DncMain.exe", "value": "6.4.5.0" } }
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@echo off
|
||||||
|
REM Install-eMxInfo.cmd - copy the site-specific eMxInfo.txt into both
|
||||||
|
REM DNC Program Files install paths. Run by Install-FromManifest.ps1
|
||||||
|
REM (Type=CMD) when Hash detection on the x86 path fails.
|
||||||
|
REM
|
||||||
|
REM Real install paths confirmed via 01-eDNC.ps1 (PXE preinstall) + a
|
||||||
|
REM real shopfloor install log capture:
|
||||||
|
REM C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt
|
||||||
|
REM C:\Program Files\DNC\Server Files\eMxInfo.txt
|
||||||
|
REM Both required because mxTransactionDll.dll references both.
|
||||||
|
REM
|
||||||
|
REM Earlier versions wrote to C:\Program Files\eDNC\eMxInfo.txt - that
|
||||||
|
REM path doesn't exist on disk; the eDNC product creates \DNC\ (no 'e')
|
||||||
|
REM with a "Server Files" subdir. Corrected 2026-04-28.
|
||||||
|
|
||||||
|
set "SRC=%~dp0eMxInfo.txt"
|
||||||
|
if not exist "%SRC%" (
|
||||||
|
echo Install-eMxInfo: source file not found at %SRC%
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set "DST64=C:\Program Files\DNC\Server Files"
|
||||||
|
set "DST86=C:\Program Files (x86)\DNC\Server Files"
|
||||||
|
|
||||||
|
if not exist "%DST64%\" mkdir "%DST64%" 2>nul
|
||||||
|
if not exist "%DST86%\" mkdir "%DST86%" 2>nul
|
||||||
|
|
||||||
|
copy /Y "%SRC%" "%DST64%\eMxInfo.txt" >nul || exit /b 2
|
||||||
|
copy /Y "%SRC%" "%DST86%\eMxInfo.txt" >nul || exit /b 3
|
||||||
|
|
||||||
|
echo Install-eMxInfo: deployed eMxInfo.txt to both DNC\Server Files paths
|
||||||
|
exit /b 0
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
# Restore-UDCData.ps1 - Idempotent UDC data restore for the manifest engine.
|
||||||
|
#
|
||||||
|
# Triggered by the GE Shopfloor Enforce scheduled task (runs as SYSTEM, every
|
||||||
|
# user logon + every 5 min). Standard-machine manifest entry uses
|
||||||
|
# DetectionMethod=Always so this fires every cycle; the script self-decides
|
||||||
|
# whether there's actually any work to do.
|
||||||
|
#
|
||||||
|
# CONTRACT:
|
||||||
|
# - 99% of cycles: no backup waiting -> exit 0 in ~1 second, ~5 lines of log
|
||||||
|
# - 1 cycle (the one after Backup-UDCData lands a backup for this PC's bay):
|
||||||
|
# stop UDC, copy CurrentData.json + ArchivedData/ to C:\ProgramData\UDC,
|
||||||
|
# move consumed backup to <bay>\migrated\<timestamp>\, write
|
||||||
|
# restore.manifest.json, restart UDC. After this, root is empty so the
|
||||||
|
# check returns "no backup waiting" again on subsequent cycles.
|
||||||
|
#
|
||||||
|
# DESIGNED FOR THE SWAP WORKFLOW:
|
||||||
|
# New PC gets pre-imaged with real machine number + locked down, sits in
|
||||||
|
# storage. Days/weeks later, tech runs Backup-UDCData on old PC -> backup
|
||||||
|
# lands on share. Tech swaps PCs. New PC powers on at the bay -> ShopFloor
|
||||||
|
# autologon -> manifest engine fires this script -> backup detected ->
|
||||||
|
# restored -> UDC opens with prior history intact.
|
||||||
|
#
|
||||||
|
# Replaces the placeholder->real trigger in Update-MachineNumber.ps1 for the
|
||||||
|
# pre-imaged-then-swapped case (where the trigger fired at imaging time, before
|
||||||
|
# the backup existed). Update-MachineNumber.ps1's branch still handles the
|
||||||
|
# secondary case (tech used 9999 placeholder + sets number at bay) - both
|
||||||
|
# triggers safely no-op if the other already consumed the backup.
|
||||||
|
#
|
||||||
|
# LOGGING:
|
||||||
|
# Single rotating log at C:\Logs\UDC\Restore-UDCData.log (1 MB cap, rotated
|
||||||
|
# to .old.log on overflow). Every cycle writes a header line so even the
|
||||||
|
# silent no-op path leaves a trace. Errors include full exception type,
|
||||||
|
# position, and stack trace.
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$BackupShareRoot = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\udc',
|
||||||
|
[string]$UdcDataDir = 'C:\ProgramData\UDC',
|
||||||
|
[string]$UdcExePath = 'C:\Program Files\UDC\UDC.exe',
|
||||||
|
[string]$UdcSettingsPath = 'C:\ProgramData\UDC\udc_settings.json',
|
||||||
|
[string]$Site = 'West Jefferson',
|
||||||
|
# Share can take 20-60s to become reachable from SYSTEM context after a
|
||||||
|
# cold boot or fresh logon. Retry until then before deciding "no backup".
|
||||||
|
[int]$ShareTimeoutSec = 60,
|
||||||
|
[int]$SharePollSec = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
|
||||||
|
# -- Logging setup --------------------------------------------------------
|
||||||
|
$logDir = 'C:\Logs\UDC'
|
||||||
|
try {
|
||||||
|
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
||||||
|
} catch { $logDir = $env:TEMP }
|
||||||
|
$logFile = Join-Path $logDir 'Restore-UDCData.log'
|
||||||
|
$logFileMaxBytes = 1MB
|
||||||
|
|
||||||
|
# Rotate log file if oversized (keeps one prior generation)
|
||||||
|
try {
|
||||||
|
if ((Test-Path $logFile) -and ((Get-Item $logFile).Length -gt $logFileMaxBytes)) {
|
||||||
|
$rotated = Join-Path $logDir 'Restore-UDCData.old.log'
|
||||||
|
if (Test-Path $rotated) { Remove-Item $rotated -Force -ErrorAction SilentlyContinue }
|
||||||
|
Rename-Item -Path $logFile -NewName 'Restore-UDCData.old.log' -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
function Log {
|
||||||
|
param([string]$Msg, [string]$Level = 'INFO')
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
|
||||||
|
$line = "[$ts][$Level] $Msg"
|
||||||
|
try { Add-Content -LiteralPath $logFile -Value $line -ErrorAction SilentlyContinue } catch {}
|
||||||
|
Write-Host $line
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogErr {
|
||||||
|
param($Err)
|
||||||
|
if (-not $Err) { return }
|
||||||
|
$exType = if ($Err.Exception) { $Err.Exception.GetType().FullName } else { '<no exception>' }
|
||||||
|
$exMsg = if ($Err.Exception) { $Err.Exception.Message } else { "$Err" }
|
||||||
|
Log " exception: $exType - $exMsg" 'ERROR'
|
||||||
|
if ($Err.InvocationInfo -and $Err.InvocationInfo.PositionMessage) {
|
||||||
|
$pos = ($Err.InvocationInfo.PositionMessage -replace "`r?`n", ' | ')
|
||||||
|
Log " at: $pos" 'ERROR'
|
||||||
|
}
|
||||||
|
if ($Err.ScriptStackTrace) {
|
||||||
|
$st = ($Err.ScriptStackTrace -replace "`r?`n", ' | ')
|
||||||
|
Log " stack: $st" 'ERROR'
|
||||||
|
}
|
||||||
|
if ($Err.Exception -and $Err.Exception.InnerException) {
|
||||||
|
Log " inner: $($Err.Exception.InnerException.Message)" 'ERROR'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log '==============================================='
|
||||||
|
Log "Restore-UDCData starting (PID $PID)"
|
||||||
|
Log "Hostname: $env:COMPUTERNAME"
|
||||||
|
try {
|
||||||
|
$whoami = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
|
} catch { $whoami = '<unknown>' }
|
||||||
|
Log "User identity: $whoami"
|
||||||
|
Log "PowerShell version: $($PSVersionTable.PSVersion)"
|
||||||
|
Log "BackupShareRoot: $BackupShareRoot"
|
||||||
|
Log "UdcDataDir: $UdcDataDir"
|
||||||
|
Log "UdcSettingsPath: $UdcSettingsPath"
|
||||||
|
Log "ShareTimeoutSec: $ShareTimeoutSec SharePollSec: $SharePollSec"
|
||||||
|
|
||||||
|
# -- Resolve local machine number ----------------------------------------
|
||||||
|
if (-not (Test-Path -LiteralPath $UdcSettingsPath)) {
|
||||||
|
Log "udc_settings.json not present - UDC not installed yet, no work to do."
|
||||||
|
Log 'Exit 0.'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$json = Get-Content -LiteralPath $UdcSettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
$mn = $json.GeneralSettings.MachineNumber
|
||||||
|
Log "Resolved MachineNumber from udc_settings: $mn"
|
||||||
|
} catch {
|
||||||
|
Log "Failed to parse $UdcSettingsPath" 'ERROR'
|
||||||
|
LogErr $_
|
||||||
|
Log 'Exit 0.'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
if (-not $mn -or $mn -eq '9999' -or $mn -notmatch '^\d+$') {
|
||||||
|
Log "Machine number is placeholder/empty/non-numeric ('$mn'). Update-MachineNumber.ps1's branch will catch the placeholder->real transition. No work to do."
|
||||||
|
Log 'Exit 0.'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Mount share with SFLD user creds -----------------------------------
|
||||||
|
# This script runs as NT AUTHORITY\SYSTEM (manifest engine on logon, or
|
||||||
|
# scheduled task). SYSTEM authenticates to remote SMB as the COMPUTER
|
||||||
|
# ACCOUNT (DOMAIN\HOSTNAME$), not as a user. The SFLD share's ACLs grant
|
||||||
|
# top-level enumeration to authenticated computers but file-level read
|
||||||
|
# only to a specific SFLD user. Without explicit user creds, Test-Path
|
||||||
|
# on bay-level files returns False (access denied = same return as not-
|
||||||
|
# found), making the script silently log "absent" when the files in fact
|
||||||
|
# exist. Symptom: Restore-UDCData.log shows endless "no work this cycle"
|
||||||
|
# while another PC (or a user-context invocation) successfully consumes
|
||||||
|
# the backup. Fix: mount the share with explicit SFLD creds from
|
||||||
|
# HKLM:\SOFTWARE\GE\SFLD\Credentials and probe via the drive letter.
|
||||||
|
# Inline Mount-SFLDShare so this script can run from any location -
|
||||||
|
# specifically the SFLD share path \\tsgwp00525...\standard-machine\scripts\
|
||||||
|
# where GE-Enforce executes it. The original Shopfloor\lib\Restore-EDncReg.ps1
|
||||||
|
# is only present in the C:\Enrollment\ imaging-time tree, not on the share,
|
||||||
|
# so dot-sourcing its relative path would silently fail and Mount-SFLDShare
|
||||||
|
# would be undefined at call time.
|
||||||
|
function Mount-SFLDShare {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$SharePath,
|
||||||
|
[string]$DriveLetter = 'V:'
|
||||||
|
)
|
||||||
|
$server = ($SharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1
|
||||||
|
$basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
|
||||||
|
if (-not (Test-Path $basePath)) { return $false }
|
||||||
|
$cred = $null
|
||||||
|
foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) {
|
||||||
|
$props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
|
||||||
|
if (-not $props) { continue }
|
||||||
|
# Match either the value-style TargetHost (test-fixture) or the
|
||||||
|
# production-style layout where the credential subkey NAME is the host.
|
||||||
|
$hosts = @()
|
||||||
|
if ($props.TargetHost) { $hosts += $props.TargetHost }
|
||||||
|
$hosts += $entry.PSChildName
|
||||||
|
$matched = $false
|
||||||
|
foreach ($h in $hosts) {
|
||||||
|
if (-not $h) { continue }
|
||||||
|
if ($h -eq $server -or $h -like "$server.*" -or $server -like "$h.*") {
|
||||||
|
$matched = $true; break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($matched -and $props.Username -and $props.Password) {
|
||||||
|
$cred = $props; break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $cred) { return $false }
|
||||||
|
& net use $DriveLetter /delete /y 2>$null | Out-Null
|
||||||
|
$null = & net use $DriveLetter $SharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1
|
||||||
|
return ($LASTEXITCODE -eq 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log "Mounting share with SFLD creds: $BackupShareRoot -> W:"
|
||||||
|
$shareMounted = $false
|
||||||
|
$sw = [Diagnostics.Stopwatch]::StartNew()
|
||||||
|
while ($sw.Elapsed.TotalSeconds -lt $ShareTimeoutSec) {
|
||||||
|
if (Mount-SFLDShare -SharePath $BackupShareRoot -DriveLetter 'W:') {
|
||||||
|
$shareMounted = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds $SharePollSec
|
||||||
|
}
|
||||||
|
$sw.Stop()
|
||||||
|
if ($shareMounted) {
|
||||||
|
Log ("Share mounted as W: after {0:N1} s" -f $sw.Elapsed.TotalSeconds)
|
||||||
|
} else {
|
||||||
|
Log "Mount-SFLDShare failed after $ShareTimeoutSec s. SFLD creds may be missing in HKLM:\SOFTWARE\GE\SFLD\Credentials, or the share is unreachable. Exiting non-zero so the dispatcher logs a failure." 'ERROR'
|
||||||
|
Log 'Exit 1.'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# All bay-level paths now go through W: (authenticated as SFLD user) so
|
||||||
|
# Test-Path returns the truth, not access-denied-False.
|
||||||
|
$bayDir = Join-Path 'W:\' $mn
|
||||||
|
$srcCur = Join-Path $bayDir 'CurrentData.json'
|
||||||
|
$srcArc = Join-Path $bayDir 'ArchivedData'
|
||||||
|
Log "Probing backup paths for bay $mn"
|
||||||
|
Log " bayDir: $bayDir"
|
||||||
|
$bayDirExists = Test-Path -LiteralPath $bayDir
|
||||||
|
Log " bayDir exists: $bayDirExists"
|
||||||
|
$srcCurExists = Test-Path -LiteralPath $srcCur
|
||||||
|
Log " CurrentData.json src: $(if ($srcCurExists) { 'present' } else { 'absent' }) - $srcCur"
|
||||||
|
$srcArcExists = Test-Path -LiteralPath $srcArc
|
||||||
|
Log " ArchivedData/ src: $(if ($srcArcExists) { 'present' } else { 'absent' }) - $srcArc"
|
||||||
|
|
||||||
|
if (-not $srcCurExists -and -not $srcArcExists) {
|
||||||
|
Log "No backup waiting for bay $mn (neither CurrentData.json nor ArchivedData\ at bay root) - no work to do this cycle."
|
||||||
|
& net use W: /delete /y 2>$null | Out-Null
|
||||||
|
Log 'Exit 0.'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
if (-not $srcCurExists) {
|
||||||
|
Log "Partial backup waiting (ArchivedData\ present, CurrentData.json absent). Will restore ArchivedData\ only. Source PC may have had no live UDC session at backup time, or backup partially failed." 'WARN'
|
||||||
|
}
|
||||||
|
if (-not $srcArcExists) {
|
||||||
|
Log "Partial backup waiting (CurrentData.json present, ArchivedData\ absent). Will restore CurrentData.json only." 'WARN'
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- We have a backup. Restore. ------------------------------------------
|
||||||
|
Log "Backup waiting at $bayDir - proceeding with restore"
|
||||||
|
|
||||||
|
# Stop UDC.exe so CurrentData.json isn't locked
|
||||||
|
$udcProcs = @(Get-Process UDC -ErrorAction SilentlyContinue)
|
||||||
|
Log "UDC processes currently running: $($udcProcs.Count)"
|
||||||
|
foreach ($p in $udcProcs) {
|
||||||
|
try {
|
||||||
|
Log " stopping UDC.exe PID $($p.Id)"
|
||||||
|
$p.Kill()
|
||||||
|
$p.WaitForExit(5000) | Out-Null
|
||||||
|
Log " stopped"
|
||||||
|
} catch {
|
||||||
|
Log " could not stop UDC.exe PID $($p.Id)" 'WARN'
|
||||||
|
LogErr $_
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
|
||||||
|
# Ensure local UDC data dir exists
|
||||||
|
if (-not (Test-Path -LiteralPath $UdcDataDir)) {
|
||||||
|
Log "Creating local UDC data dir: $UdcDataDir"
|
||||||
|
try {
|
||||||
|
New-Item -Path $UdcDataDir -ItemType Directory -Force | Out-Null
|
||||||
|
} catch {
|
||||||
|
Log "Failed to create $UdcDataDir - cannot continue" 'ERROR'
|
||||||
|
LogErr $_
|
||||||
|
& net use W: /delete /y 2>$null | Out-Null
|
||||||
|
Log 'Exit 1.'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$localCur = Join-Path $UdcDataDir 'CurrentData.json'
|
||||||
|
$localArc = Join-Path $UdcDataDir 'ArchivedData'
|
||||||
|
|
||||||
|
# Copy CurrentData.json (only if present at source)
|
||||||
|
$copiedCur = $false
|
||||||
|
if ($srcCurExists) {
|
||||||
|
Log "Copying CurrentData.json"
|
||||||
|
Log " src: $srcCur"
|
||||||
|
Log " dst: $localCur"
|
||||||
|
try {
|
||||||
|
Copy-Item -LiteralPath $srcCur -Destination $localCur -Force -ErrorAction Stop
|
||||||
|
$copiedCur = $true
|
||||||
|
$sz = (Get-Item -LiteralPath $localCur).Length
|
||||||
|
Log " OK ($sz bytes)"
|
||||||
|
} catch {
|
||||||
|
Log " FAILED" 'ERROR'
|
||||||
|
LogErr $_
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log "CurrentData.json not present in backup - skipping that copy step"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy ArchivedData/
|
||||||
|
$copiedArc = $false
|
||||||
|
$arcFiles = 0
|
||||||
|
$arcBytes = 0
|
||||||
|
if ($srcArcExists) {
|
||||||
|
Log "Copying ArchivedData/"
|
||||||
|
Log " src: $srcArc"
|
||||||
|
Log " dst: $localArc"
|
||||||
|
try {
|
||||||
|
if (Test-Path -LiteralPath $localArc) {
|
||||||
|
Log " removing existing $localArc"
|
||||||
|
Remove-Item -LiteralPath $localArc -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
Copy-Item -LiteralPath $srcArc -Destination $localArc -Recurse -Force -ErrorAction Stop
|
||||||
|
$arcItems = Get-ChildItem -LiteralPath $localArc -Recurse -File -ErrorAction SilentlyContinue
|
||||||
|
$arcFiles = @($arcItems).Count
|
||||||
|
$arcBytes = ($arcItems | Measure-Object Length -Sum).Sum
|
||||||
|
$copiedArc = $true
|
||||||
|
Log " OK ($arcFiles files, $arcBytes bytes)"
|
||||||
|
} catch {
|
||||||
|
Log " FAILED" 'ERROR'
|
||||||
|
LogErr $_
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log "ArchivedData/ not present in backup - skipping that copy step"
|
||||||
|
}
|
||||||
|
|
||||||
|
# One-shot consumption: only consume when every present source has been
|
||||||
|
# successfully copied. If a source was absent we don't fault on it; if a
|
||||||
|
# source was present but copy failed, we leave the live backup for retry.
|
||||||
|
# Must have copied at least one thing to consume.
|
||||||
|
$consumeOk = (($copiedCur -or -not $srcCurExists) -and `
|
||||||
|
($copiedArc -or -not $srcArcExists) -and `
|
||||||
|
($copiedCur -or $copiedArc))
|
||||||
|
Log "consumeOk=$consumeOk (copiedCur=$copiedCur, copiedArc=$copiedArc, srcCurExists=$srcCurExists, srcArcExists=$srcArcExists)"
|
||||||
|
|
||||||
|
if ($consumeOk) {
|
||||||
|
try {
|
||||||
|
$stamp = Get-Date -Format 'yyyy-MM-ddTHH-mm-ssZ'
|
||||||
|
$migDir = Join-Path $bayDir 'migrated'
|
||||||
|
$migStamp = Join-Path $migDir $stamp
|
||||||
|
Log "Moving consumed backup to $migStamp"
|
||||||
|
if (-not (Test-Path -LiteralPath $migDir)) { New-Item -ItemType Directory -Path $migDir -Force | Out-Null }
|
||||||
|
if (-not (Test-Path -LiteralPath $migStamp)) { New-Item -ItemType Directory -Path $migStamp -Force | Out-Null }
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $srcCur) {
|
||||||
|
Move-Item -LiteralPath $srcCur -Destination (Join-Path $migStamp 'CurrentData.json') -Force -ErrorAction Stop
|
||||||
|
Log " moved CurrentData.json"
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $srcArc) {
|
||||||
|
Move-Item -LiteralPath $srcArc -Destination (Join-Path $migStamp 'ArchivedData') -Force -ErrorAction Stop
|
||||||
|
Log " moved ArchivedData/"
|
||||||
|
}
|
||||||
|
$bakManifest = Join-Path $bayDir 'backup.manifest.json'
|
||||||
|
if (Test-Path -LiteralPath $bakManifest) {
|
||||||
|
Move-Item -LiteralPath $bakManifest -Destination (Join-Path $migStamp 'backup.manifest.json') -Force -ErrorAction SilentlyContinue
|
||||||
|
Log " moved backup.manifest.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
$restoreManifest = [ordered]@{
|
||||||
|
RestoredAt = (Get-Date -Format 'o')
|
||||||
|
DestinationHostname = $env:COMPUTERNAME
|
||||||
|
DestinationUser = $whoami
|
||||||
|
MachineNumber = $mn
|
||||||
|
CurrentDataPresent = $copiedCur
|
||||||
|
CurrentDataBytes = if ($copiedCur) { (Get-Item -LiteralPath $localCur).Length } else { 0 }
|
||||||
|
ArchivedDataPresent = $copiedArc
|
||||||
|
ArchivedDataFiles = $arcFiles
|
||||||
|
ArchivedDataBytes = $arcBytes
|
||||||
|
RestoredVia = 'Restore-UDCData.ps1 (manifest engine, on logon)'
|
||||||
|
}
|
||||||
|
$restoreManifest | ConvertTo-Json | Set-Content -Path (Join-Path $migStamp 'restore.manifest.json') -Encoding UTF8
|
||||||
|
Log " wrote restore.manifest.json"
|
||||||
|
|
||||||
|
Log "Backup consumed -> migrated\$stamp\"
|
||||||
|
} catch {
|
||||||
|
Log "Move-to-migrated FAILED (data IS restored locally; live backup remains, next cycle will retry consumption)" 'ERROR'
|
||||||
|
LogErr $_
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log "Restore incomplete - leaving live backup at $bayDir for retry next cycle." 'WARN'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Relaunch UDC with the current machine number args. UDC's vendor autostart in
|
||||||
|
# HKLM\Run will also fire on the next user logon, so this is belt-and-suspenders
|
||||||
|
# for the same-session case (e.g. tech is at the keyboard during the restore).
|
||||||
|
if ((Test-Path -LiteralPath $UdcExePath) -and ($copiedCur -or $copiedArc)) {
|
||||||
|
Log "Relaunching UDC.exe: `"$Site`" -$mn"
|
||||||
|
try {
|
||||||
|
Start-Process -FilePath $UdcExePath -ArgumentList @("`"$Site`"", "-$mn")
|
||||||
|
Log " relaunched"
|
||||||
|
} catch {
|
||||||
|
Log " UDC relaunch FAILED" 'WARN'
|
||||||
|
LogErr $_
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unmount the SFLD-creds-mounted drive so we don't leave a stale net-use entry
|
||||||
|
& net use W: /delete /y 2>$null | Out-Null
|
||||||
|
|
||||||
|
Log 'Exit 0.'
|
||||||
|
Log '==============================================='
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user