Compare commits
112 Commits
main
...
renumber-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d441abd20f | ||
|
|
9145023440 | ||
|
|
51edf98e7d | ||
|
|
e3a3fa6794 | ||
|
|
a165a79f95 | ||
|
|
41cace17e8 | ||
|
|
e97e5bd049 | ||
|
|
c2538a05c5 | ||
|
|
59deaea714 | ||
|
|
f49fa0f940 | ||
|
|
c8e704b595 | ||
|
|
1d65103cc0 | ||
|
|
bfe17fe123 | ||
|
|
da380fbcd7 | ||
|
|
a935c04e1f | ||
|
|
1e17a0564f | ||
|
|
99e7679e87 | ||
|
|
1e6603d331 | ||
|
|
e844ff367c | ||
|
|
913c807142 | ||
|
|
1853db0903 | ||
|
|
7a67716fcc | ||
|
|
995909042b | ||
|
|
a351160520 | ||
|
|
a380b17112 | ||
|
|
5211861409 | ||
|
|
db6be99d43 | ||
|
|
a6fa21589b | ||
|
|
f570e92847 | ||
|
|
ea136687e0 | ||
|
|
57fae57d3b | ||
|
|
c88b2b0ab8 | ||
|
|
1a5852f7ff | ||
|
|
0b116e3ecf | ||
|
|
f6d970c08d | ||
|
|
59b1a9fb65 | ||
|
|
69a1682a7f | ||
|
|
c74148a222 | ||
|
|
191083e440 | ||
|
|
1860a92afa | ||
|
|
9ad467ba6c | ||
|
|
485fe1c7c4 | ||
|
|
9b46d0279f | ||
|
|
5e13d38512 | ||
|
|
55c1ab4814 | ||
|
|
5c3db71879 | ||
|
|
97b9e58d23 | ||
|
|
7298d433eb | ||
|
|
de7d41f5e5 | ||
|
|
f95d305cca | ||
|
|
8e1f81b942 | ||
|
|
77c917157d | ||
|
|
d0dcce5427 | ||
|
|
6602afde38 | ||
|
|
d359563a4c | ||
|
|
cb149ed8cd | ||
|
|
821e3179d1 | ||
|
|
fce6680c6f | ||
|
|
ed12988591 | ||
|
|
b8bb00e2fe | ||
|
|
a104cfdebb | ||
|
|
b57ba0fb6f | ||
|
|
54dddaa760 | ||
|
|
00d4105956 | ||
|
|
86fbc132dd | ||
|
|
4015adeb33 | ||
|
|
de3018512a | ||
|
|
7f93347f74 | ||
|
|
548d85fed5 | ||
|
|
45f39fd431 | ||
|
|
b86b830568 | ||
|
|
a22d2f0313 | ||
|
|
44554b95b0 | ||
|
|
02499cf74b | ||
|
|
27045d5e4a | ||
|
|
1f60c86ec8 | ||
|
|
44d2f0afd5 | ||
|
|
5891a1966f | ||
|
|
e1ea6b7c62 | ||
|
|
2a0b4885fe | ||
|
|
37357eee43 | ||
|
|
3aabd47571 | ||
|
|
7f097013fc | ||
|
|
9108b495c9 | ||
|
|
d8c64bef2b | ||
|
|
76a3ba513c | ||
|
|
3385bc87aa | ||
|
|
220c5db5b9 | ||
|
|
8debc4ddb3 | ||
|
|
036090348c | ||
|
|
b9f66687ac | ||
|
|
65eeead5a0 | ||
|
|
a9a7478d5a | ||
|
|
1c361e138b | ||
|
|
ca647cb690 | ||
|
|
5f322d1110 | ||
|
|
520d4aa791 | ||
|
|
894305e906 | ||
|
|
1b7e1bfee4 | ||
|
|
d5398bdd74 | ||
|
|
cdb6655e4a | ||
|
|
74ba3d1339 | ||
|
|
4599c85509 | ||
|
|
2d75935dfc | ||
|
|
3fb1d983df | ||
|
|
9beee842f1 | ||
|
|
f013aa2bff | ||
|
|
a9260ecadd | ||
|
|
ab3e1c98f6 | ||
|
|
842ef88ccb | ||
|
|
a17b3fae6a | ||
|
|
ce604adcda |
18
.gitignore
vendored
@@ -101,6 +101,20 @@ playbook/shopfloor-setup/Shopfloor/PrinterInstallerMap.exe
|
||||
# /home/camp/pxe-images/keyence/Logs/Keyence/install.log for the signature).
|
||||
# Canonical source on the GE-Enforce SFLD share:
|
||||
# tsgwp00525\sfld$\v2\shared\dt\shopfloor\gea-shopfloor-keyence\apps\Data1.cab
|
||||
# Stage to playbook/shopfloor-setup/gea-shopfloor-keyence/installers/Data1.cab
|
||||
# before building the USB image.
|
||||
playbook/shopfloor-setup/gea-shopfloor-keyence/installers/Data1.cab
|
||||
playbook/shopfloor-setup/gea-shopfloor-waxtrace/captured-binary/
|
||||
|
||||
# Keyence per-model installer payloads - too big for git, staged via sync-keyence.sh
|
||||
playbook/shopfloor-setup/gea-shopfloor-keyence/vr3000/installers/Data*.cab
|
||||
playbook/shopfloor-setup/gea-shopfloor-keyence/vr3000/installers/*.msi
|
||||
playbook/shopfloor-setup/gea-shopfloor-keyence/vr5000/installers/Data*.cab
|
||||
playbook/shopfloor-setup/gea-shopfloor-keyence/vr5000/installers/*.msi
|
||||
playbook/shopfloor-setup/gea-shopfloor-keyence/vr6000/installers/Data1.cab
|
||||
|
||||
# Part Marker (Telesis) utility password - secret, deployed via the enrollment
|
||||
# share from the working tree, never committed.
|
||||
playbook/shopfloor-setup/gea-shopfloor-partmarker/PartMarker/Mark/utilpassword.txt
|
||||
|
||||
# HeatTreat per-machine DNC .reg exports (6601-6604) - contain DNC FtpPasswd
|
||||
# credentials. Deployed via the enrollment share from the working tree.
|
||||
playbook/shopfloor-setup/gea-shopfloor-heattreat/reg/*.reg
|
||||
|
||||
BIN
Binary/Binary.NewBinary1
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
Binary/Binary.NewBinary10
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
Binary/Binary.NewBinary11
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
Binary/Binary.NewBinary12
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Binary/Binary.NewBinary13
Normal file
|
After Width: | Height: | Size: 766 B |
BIN
Binary/Binary.NewBinary14
Normal file
|
After Width: | Height: | Size: 766 B |
BIN
Binary/Binary.NewBinary15
Normal file
|
After Width: | Height: | Size: 766 B |
BIN
Binary/Binary.NewBinary16
Normal file
|
After Width: | Height: | Size: 766 B |
BIN
Binary/Binary.NewBinary17
Normal file
|
After Width: | Height: | Size: 766 B |
BIN
Binary/Binary.NewBinary18
Normal file
|
After Width: | Height: | Size: 766 B |
40
Binary/Binary.NewBinary19
Normal file
@@ -0,0 +1,40 @@
|
||||
Option Explicit
|
||||
|
||||
|
||||
' アップグレードコードから、製品コードを取得
|
||||
'
|
||||
' 第1引数 : アップグレードコード(「{」、「}」、ハイフンあり)
|
||||
Function GetProductCodeFromUpgradeCode(UpgCode)
|
||||
Dim listProductCode
|
||||
Dim szProductCode
|
||||
|
||||
' アップグレードコードから、関連する製品名のリストを取得
|
||||
Set listProductCode = Session.Installer.RelatedProducts(UpgCode)
|
||||
|
||||
' 基本、1件のみヒットするものとする
|
||||
For Each szProductCode In listProductCode
|
||||
GetProductCodeFromUpgradeCode = szProductCode
|
||||
' 1件目を取得した段階で抜ける
|
||||
Exit For
|
||||
Next
|
||||
End Function
|
||||
|
||||
|
||||
' アップグレードコードから既にインストール済みのアプリケーションのインストールパスを取得する
|
||||
Sub GetInstallPath()
|
||||
Dim WshShell
|
||||
Dim szProductCode
|
||||
Dim szInstallStringKey
|
||||
|
||||
Set WshShell = CreateObject("WScript.Shell")
|
||||
|
||||
' アップグレードコードから、製品コードを取得
|
||||
szProductCode = GetProductCodeFromUpgradeCode(Session.Property("UpgradeCode"))
|
||||
|
||||
' レジストリのInstallLocationを取得
|
||||
szInstallStringKey = WshShell.RegRead("HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" + szProductCode + "\InstallLocation")
|
||||
|
||||
Session.Property("INSTALLDIR_FOR_MAJORUPGRADE") = szInstallStringKey
|
||||
|
||||
Set WshShell = nothing
|
||||
End Sub
|
||||
BIN
Binary/Binary.NewBinary2
Normal file
|
After Width: | Height: | Size: 318 B |
22
Binary/Binary.NewBinary20
Normal file
@@ -0,0 +1,22 @@
|
||||
Option Explicit
|
||||
|
||||
Sub CheckOSVersion
|
||||
Const HKEY_LOCAL_MACHINE = &H80000002
|
||||
Dim WshShell,objRegistry
|
||||
Dim strComputer, strKeyPath, strValue, strValueName
|
||||
|
||||
Set WshShell = CreateObject("WScript.Shell")
|
||||
|
||||
strComputer = "."
|
||||
Set objRegistry = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\default:StdRegProv")
|
||||
strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion"
|
||||
strValueName = "CurrentMajorVersionNumber"
|
||||
objRegistry.GetDWORDValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue
|
||||
|
||||
If (not IsNull(strValue)) and (strValue=10) Then
|
||||
Session.Property("IsWindows10")="1"
|
||||
else
|
||||
Session.Property("IsWindows10")="0"
|
||||
End If
|
||||
|
||||
End Sub
|
||||
13
Binary/Binary.NewBinary21
Normal file
@@ -0,0 +1,13 @@
|
||||
Option Explicit
|
||||
|
||||
'レジストリに登録する日付をプロパティに設定
|
||||
Sub SetInstallDate
|
||||
Session.Property("INSTALLDATE") = YYYYMMDD
|
||||
End Sub
|
||||
|
||||
'YYYYMMDD形式の日付を返す
|
||||
Function YYYYMMDD
|
||||
|
||||
YYYYMMDD = Year(Date) & Right("0" & Month(Date), 2) & Right("0" & Day(Date), 2)
|
||||
|
||||
End Function
|
||||
BIN
Binary/Binary.NewBinary3
Normal file
|
After Width: | Height: | Size: 318 B |
BIN
Binary/Binary.NewBinary4
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Binary/Binary.NewBinary5
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Binary/Binary.NewBinary6
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
Binary/Binary.NewBinary7
Normal file
|
After Width: | Height: | Size: 766 B |
BIN
Binary/Binary.NewBinary8
Normal file
|
After Width: | Height: | Size: 766 B |
BIN
Binary/Binary.NewBinary9
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
Binary/Binary.SetAllUsers.dll
Normal file
BIN
Icon/Icon.ARPPRODUCTICON.exe
Normal file
BIN
Icon/Icon.NewShortcut1_AB8E7834205C40DE9AD7D94845255E55.exe
Normal file
BIN
Icon/Icon.NewShortcut2_1E955C7F522448028AA8419A487CA996.exe
Normal file
BIN
Icon/Icon._052C53DD_2BAF_4769_A1A0_1519CA2B23FB
Normal file
BIN
Icon/Icon._173060F8_E06D_4B06_B289_4656D92A595E
Normal file
BIN
Icon/Icon._3336CEAD_1861_4B26_A1C1_2D2F75C36A27
Normal file
BIN
Icon/Icon._59497049_1008_4427_84DF_744B04B0F075
Normal file
BIN
Icon/Icon._5EF3CFE6_6B6A_49D5_A54E_95222360D405
Normal file
BIN
Icon/Icon._823D3F96_4702_483C_BE0E_6CF23CAA78AD
Normal file
BIN
Icon/Icon._90919B36_D3D7_437D_B050_7D5620C88056
Normal file
BIN
Icon/Icon._E7806B4E_27A4_4270_8F13_3E9E01192773
Normal file
16
README.md
@@ -23,7 +23,7 @@ Client PXE boot (UEFI Secure Boot)
|
||||
|
||||
| Service | Port | Purpose |
|
||||
|-------------|-----------|------------------------------------------|
|
||||
| dnsmasq | 67/udp | DHCP (10.9.100.10-100, 12h lease) |
|
||||
| dnsmasq | 67/udp | DHCP (172.16.9.10-100, 12h lease) |
|
||||
| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
|
||||
| Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
|
||||
| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
|
||||
@@ -32,8 +32,8 @@ Client PXE boot (UEFI Secure Boot)
|
||||
|
||||
### Network
|
||||
|
||||
- **PXE server IP:** `10.9.100.1/24`
|
||||
- **DHCP range:** `10.9.100.10` - `10.9.100.100`
|
||||
- **PXE server IP:** `172.16.9.1/24`
|
||||
- **DHCP range:** `172.16.9.10` - `172.16.9.100`
|
||||
- **Firewall:** UFW deny-by-default, only service ports open (22, 67, 69, 80, 445, 4433, 9009)
|
||||
|
||||
## Quick Start
|
||||
@@ -85,12 +85,12 @@ Creates a bootable USB with two partitions:
|
||||
4. After reboot, the first-boot script:
|
||||
- Installs all offline .deb packages
|
||||
- Runs the Ansible playbook (configures dnsmasq, Apache, Samba, UFW, webapp)
|
||||
- Configures static IP `10.9.100.1/24`
|
||||
- Configures static IP `172.16.9.1/24`
|
||||
5. Move the server's wired NIC to the isolated PXE switch
|
||||
|
||||
### Step 5: Access the Web Interface
|
||||
|
||||
Open `http://10.9.100.1:9009` from any machine on the isolated network.
|
||||
Open `http://172.16.9.1:9009` from any machine on the isolated network.
|
||||
|
||||
## Web Management Interface
|
||||
|
||||
@@ -213,11 +213,11 @@ This creates `pxe-server-proxmox.iso` containing the Ubuntu installer, autoinsta
|
||||
3. Attach the ISO as CD-ROM and start the VM
|
||||
4. Ubuntu auto-installs with zero interaction (~10-15 minutes)
|
||||
5. After reboot, first-boot configures all PXE services automatically
|
||||
6. Access the web interface at `http://10.9.100.1:9009`
|
||||
6. Access the web interface at `http://172.16.9.1:9009`
|
||||
|
||||
### Import WinPE Images
|
||||
|
||||
After the server is running, import deployment images via the web interface at `http://10.9.100.1:9009/import` or by mounting a USB drive with WinPE content.
|
||||
After the server is running, import deployment images via the web interface at `http://172.16.9.1:9009/import` or by mounting a USB drive with WinPE content.
|
||||
|
||||
## Samba Shares
|
||||
|
||||
@@ -235,7 +235,7 @@ All shares use guest access (no authentication) for ease of use on the isolated
|
||||
|
||||
Blancco Drive Eraser 7.15.1 boots via a native Ubuntu kernel with a custom initramfs (`blancco-init.sh`) that downloads and mounts the Blancco rootfs over HTTP. XML erasure reports are automatically saved to the PXE server's Samba share (`blancco-reports`). The server supports BMC cloud licensing for Blancco activation over WiFi.
|
||||
|
||||
Reports are viewable and downloadable from the web interface at `http://10.9.100.1:9009/reports`.
|
||||
Reports are viewable and downloadable from the web interface at `http://172.16.9.1:9009/reports`.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
12
SETUP.md
@@ -18,7 +18,7 @@ Client PXE boot
|
||||
|
||||
| Service | Port | Purpose |
|
||||
|-------------|-----------|------------------------------------------|
|
||||
| dnsmasq | 67/udp | DHCP (10.9.100.10-100) |
|
||||
| dnsmasq | 67/udp | DHCP (172.16.9.10-100) |
|
||||
| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
|
||||
| Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
|
||||
| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
|
||||
@@ -95,7 +95,7 @@ Move the server's wired NIC to the isolated switch for PXE clients.
|
||||
|
||||
### Step 6: Import WinPE Content (if not bundled in Step 3)
|
||||
|
||||
**Option A:** Use the web interface at `http://10.9.100.1:9009` to import from USB.
|
||||
**Option A:** Use the web interface at `http://172.16.9.1:9009` to import from USB.
|
||||
|
||||
**Option B:** Manual copy:
|
||||
```bash
|
||||
@@ -107,7 +107,7 @@ sudo umount /mnt/usb2
|
||||
|
||||
## Web Management Interface
|
||||
|
||||
Access at `http://10.9.100.1:9009` from any machine on the isolated network.
|
||||
Access at `http://172.16.9.1:9009` from any machine on the isolated network.
|
||||
|
||||
| Page | URL Path | Purpose |
|
||||
|-------------------|-------------|-----------------------------------------------|
|
||||
@@ -146,7 +146,7 @@ sudo ./test-vm.sh ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso
|
||||
# Watch progress (Ctrl+] to detach)
|
||||
sudo virsh console pxe-test
|
||||
|
||||
# After install: ssh pxe@10.9.100.1 / http://10.9.100.1:9009
|
||||
# After install: ssh pxe@172.16.9.1 / http://172.16.9.1:9009
|
||||
|
||||
# Clean up
|
||||
sudo ./test-vm.sh --destroy
|
||||
@@ -215,8 +215,8 @@ pxe-server/
|
||||
|
||||
## Network Configuration
|
||||
|
||||
- PXE server static IP: `10.9.100.1/24`
|
||||
- DHCP range: `10.9.100.10` - `10.9.100.100`
|
||||
- PXE server static IP: `172.16.9.1/24`
|
||||
- DHCP range: `172.16.9.10` - `172.16.9.100`
|
||||
- Lease time: 12 hours
|
||||
- DNS: `8.8.8.8` (passed to clients, not used by server)
|
||||
- Firewall: UFW deny-by-default, allow 67/udp 69/udp 80/tcp 445/tcp 4433/tcp 9009/tcp
|
||||
|
||||
@@ -17,7 +17,7 @@ autoinstall:
|
||||
match:
|
||||
name: "en*"
|
||||
addresses:
|
||||
- 10.9.100.1/24
|
||||
- 172.16.9.1/24
|
||||
dhcp4: false
|
||||
dhcp6: false
|
||||
optional: true
|
||||
|
||||
14
boot-tools/blancco/blancco-chain.ipxe
Normal file
@@ -0,0 +1,14 @@
|
||||
#!ipxe
|
||||
|
||||
dhcp
|
||||
echo SAN booting Blancco ISO...
|
||||
sanboot http://172.16.9.1/blancco/blancco.iso || goto failed
|
||||
goto end
|
||||
|
||||
:failed
|
||||
echo
|
||||
echo FAILED!
|
||||
prompt Press any key for iPXE shell...
|
||||
shell
|
||||
|
||||
:end
|
||||
48
boot-tools/blancco/blancco-debug.ipxe
Normal file
@@ -0,0 +1,48 @@
|
||||
#!ipxe
|
||||
|
||||
echo =============================================
|
||||
echo Blancco PXE Debug Boot
|
||||
echo =============================================
|
||||
echo
|
||||
|
||||
echo [1/4] Network configuration...
|
||||
dhcp || echo DHCP FAILED
|
||||
echo MAC: ${net0/mac}
|
||||
echo IP: ${net0/ip}
|
||||
echo GW: ${net0/gateway}
|
||||
echo DNS: ${net0/dns}
|
||||
echo
|
||||
|
||||
set server 172.16.9.1
|
||||
|
||||
echo [2/4] Testing HTTP connectivity...
|
||||
imgfetch --name test http://${server}/blancco/config.img || echo HTTP FETCH FAILED
|
||||
imgfree test
|
||||
echo HTTP to ${server}: OK
|
||||
echo
|
||||
|
||||
echo [3/4] Loading kernel and initrd...
|
||||
echo Fetching vmlinuz-bde-linux...
|
||||
kernel http://${server}/blancco/vmlinuz-bde-linux initrd=initrd-combined.img archisobasedir=arch archiso_http_srv=http://${server}/blancco/ copytoram=y cow_spacesize=50% memtest=00 vmalloc=400M ip=dhcp libata.allow_tpm=1 modprobe.blacklist=iwlwifi,iwlmvm,btusb rd.udev.timeout=10 || goto failed
|
||||
echo Kernel loaded OK.
|
||||
echo
|
||||
echo Fetching initrd-combined.img (43MB, may take a moment)...
|
||||
initrd http://${server}/blancco/initrd-combined.img || goto failed
|
||||
echo Initrd loaded OK.
|
||||
echo
|
||||
|
||||
echo [4/4] About to call boot command...
|
||||
echo
|
||||
echo !! Note the LAST kernel line visible before any freeze !!
|
||||
echo
|
||||
prompt Press any key to boot (or Ctrl-C for iPXE shell)... && goto doboot || shell
|
||||
|
||||
:doboot
|
||||
boot || goto failed
|
||||
|
||||
:failed
|
||||
echo
|
||||
echo !! BOOT FAILED !!
|
||||
echo
|
||||
prompt Press any key for iPXE shell...
|
||||
shell
|
||||
17
boot-tools/blancco/blancco.ipxe
Normal file
@@ -0,0 +1,17 @@
|
||||
#!ipxe
|
||||
|
||||
dhcp
|
||||
set server 172.16.9.1
|
||||
|
||||
echo Loading Blancco kernel...
|
||||
kernel http://${server}/blancco/vmlinuz-bde-linux initrd=initrd-combined.img archisobasedir=arch archiso_http_srv=http://${server}/blancco/ copytoram=y cow_spacesize=50% memtest=00 vmalloc=400M ip=dhcp libata.allow_tpm=1 modprobe.blacklist=iwlwifi,iwlmvm,btusb rd.udev.timeout=10 nomodeset || goto failed
|
||||
echo Loading initrd (combined)...
|
||||
initrd http://${server}/blancco/initrd-combined.img || goto failed
|
||||
echo All files loaded. Booting now...
|
||||
boot || goto failed
|
||||
|
||||
:failed
|
||||
echo
|
||||
echo Blancco boot FAILED.
|
||||
prompt Press any key to drop to iPXE shell...
|
||||
shell
|
||||
@@ -3,22 +3,10 @@ set timeout=0
|
||||
|
||||
insmod efinet
|
||||
insmod net
|
||||
insmod http
|
||||
insmod tftp
|
||||
net_bootp
|
||||
|
||||
# Blancco via Ubuntu-kernel switch_root. This is the cmdline that produces
|
||||
# the slim Ubuntu-kernel-chain grubx64.efi. DO NOT flip this back to
|
||||
# vmlinuz-bde-linux / archiso_http_srv / copytoram=y - that was the Apr-14
|
||||
# regression (commit d6776f7) that put us into Blancco's narrow-NIC-driver
|
||||
# archiso path and hung on Dell Precision hardware. The Ubuntu kernel path
|
||||
# with our verbose, full-drivers/net/-tree kexec-initrd.img is what works.
|
||||
#
|
||||
# kexec-initrd.img is built by the pxe_server_setup.yml "Build Blancco PXE
|
||||
# initramfs" task (sweeps drivers/net/ + depmod). blancco-init.sh inside it
|
||||
# handles the rest: modprobe all common NICs, DHCP, download airootfs.sfs,
|
||||
# overlay mount, switch_root.
|
||||
menuentry "Blancco Drive Eraser" {
|
||||
linux (http,10.9.100.1)/blancco/vmlinuz-ubuntu ip=dhcp
|
||||
initrd (http,10.9.100.1)/blancco/kexec-initrd.img
|
||||
linux (tftp,172.16.9.1)/blancco/vmlinuz-bde-linux archisobasedir=arch archiso_http_srv=http://172.16.9.1/blancco/ copytoram=y cow_spacesize=50% memtest=00 vmalloc=400M ip=dhcp libata.allow_tpm=1 modprobe.blacklist=iwlwifi,iwlmvm,btusb rd.udev.timeout=10
|
||||
initrd (tftp,172.16.9.1)/blancco/intel-ucode.img (tftp,172.16.9.1)/blancco/amd-ucode.img (tftp,172.16.9.1)/blancco/config.img (tftp,172.16.9.1)/blancco/initramfs-bde-linux.img
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ systems.
|
||||
|
||||
## Network layout
|
||||
|
||||
- PXE server static IP: `10.9.100.1/24` on an isolated subnet.
|
||||
- DHCP range served by dnsmasq: `10.9.100.10 - 10.9.100.100`, 12h leases.
|
||||
- PXE server static IP: `172.16.9.1/24` on an isolated subnet.
|
||||
- DHCP range served by dnsmasq: `172.16.9.10 - 172.16.9.100`, 12h leases.
|
||||
- Default gateway and DNS handed out via DHCP point at the PXE server itself.
|
||||
- The subnet has no route to the corporate LAN. Client traffic (Blancco BMC
|
||||
cloud, Intune enrollment) goes out via WiFi after Windows boots; PXE-time
|
||||
@@ -166,7 +166,7 @@ USB installer (2 partitions: ISO + CIDATA)
|
||||
Ubuntu auto-install + first-boot Ansible playbook
|
||||
|
|
||||
v
|
||||
Configured PXE server (10.9.100.1) ----+
|
||||
Configured PXE server (172.16.9.1) ----+
|
||||
|
|
||||
Windows PCs running Upload-Image.ps1 --+--> Image content (SMB, webapp import)
|
||||
|
|
||||
|
||||
@@ -22,9 +22,9 @@ contribute a `config/sites/<sitename>.yaml` template back to the repo.
|
||||
|
||||
| Value | Default | Where it lives |
|
||||
|-------------------|----------------------|--------------------------------------------------------------------------------|
|
||||
| PXE server IP | 10.9.100.1 | `playbook/pxe_server_setup.yml` (dnsmasq config, iPXE script, samba conf, webapp env), `playbook/startnet.cmd` (mount paths), `boot-tools/blancco/grub-blancco.cfg` (TFTP/HTTP URLs) |
|
||||
| PXE subnet | 10.9.100.0/24 | Same as above, plus `playbook/pxe_server_setup.yml` (UFW rules) |
|
||||
| DHCP range | 10.9.100.10-100 | `playbook/pxe_server_setup.yml` (dnsmasq config) |
|
||||
| PXE server IP | 172.16.9.1 | `playbook/pxe_server_setup.yml` (dnsmasq config, iPXE script, samba conf, webapp env), `playbook/startnet.cmd` (mount paths), `boot-tools/blancco/grub-blancco.cfg` (TFTP/HTTP URLs) |
|
||||
| PXE subnet | 172.16.9.0/24 | Same as above, plus `playbook/pxe_server_setup.yml` (UFW rules) |
|
||||
| DHCP range | 172.16.9.10-100 | `playbook/pxe_server_setup.yml` (dnsmasq config) |
|
||||
| Hostname | pxeserver | `autoinstall/user-data` (identity.hostname) |
|
||||
|
||||
### Identity and credentials
|
||||
@@ -143,7 +143,7 @@ Blob storage account.
|
||||
### Image-upload paths on Windows
|
||||
|
||||
`scripts/Upload-Image.ps1` defaults to:
|
||||
- `\\10.9.100.1\image-upload` as the destination
|
||||
- `\\172.16.9.1\image-upload` as the destination
|
||||
- `C:\ProgramData\GEAerospace\MediaCreator\Cache\` as the source
|
||||
|
||||
Update both for a different site.
|
||||
@@ -156,10 +156,10 @@ A site config file should drive substitution at build time. Proposed schema:
|
||||
# config/sites/<sitename>.yaml
|
||||
site:
|
||||
name: westjeff
|
||||
pxe_server_ip: 10.9.100.1
|
||||
pxe_subnet: 10.9.100.0/24
|
||||
dhcp_range_start: 10.9.100.10
|
||||
dhcp_range_end: 10.9.100.100
|
||||
pxe_server_ip: 172.16.9.1
|
||||
pxe_subnet: 172.16.9.0/24
|
||||
dhcp_range_start: 172.16.9.10
|
||||
dhcp_range_end: 172.16.9.100
|
||||
hostname: pxeserver
|
||||
|
||||
credentials:
|
||||
|
||||
144
docs/cyberark-cmm-doda-policy.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# CyberArk EPM - CMM / DODA elevation policy
|
||||
|
||||
Reference for the CyberArk EPM admin. Fixes the "PC-DMIS is elevated but the
|
||||
tools it calls error that they are not running elevated" problem on CMM bays.
|
||||
|
||||
## Problem (root cause)
|
||||
|
||||
CyberArk EPM elevation is per-process and is NOT inherited by child processes.
|
||||
The existing policy elevates PC-DMIS (`PCDLRN.exe`), but the external `.exe`
|
||||
files the PC-DMIS routine spawns (report, geometry, and DODA tools) launch with
|
||||
the standard user token (Medium integrity) and fail their own "must run
|
||||
elevated" check, even though the parent PC-DMIS is elevated. The elevation dies
|
||||
at the process boundary.
|
||||
|
||||
## Flow that breaks
|
||||
|
||||
The PC-DMIS routine drives in-process `.BAS` scripts that shell out to separate
|
||||
executables:
|
||||
|
||||
| Step | Process | Separate process? |
|
||||
|------|---------|-------------------|
|
||||
| Merge / sort report results | `MergeFiles.exe` (.NET), reads `C:\Apps\DODA\PreProcess\` | yes |
|
||||
| Geometry export | `PCDToIGES.exe`, `RotateProbeVector.exe` | yes |
|
||||
| DODA calculation | `DovetailAnalysis.exe` (+ embedded JVM and python) | yes |
|
||||
| RTF -> PDF, display | `winword.exe`, `AcroRd32.exe` | yes (removed by the BAS rework below) |
|
||||
| Folder create / save | `MAKEDIR.BAS`, `MAKEFOLDER.bas`, `SaveAsFolder.bas` | no (in-process, uses PC-DMIS token) |
|
||||
|
||||
The in-process file operations inherit PC-DMIS's elevated token. The separate
|
||||
`.exe` files do not. Those are what get blocked.
|
||||
|
||||
## Fix: one Application Group + one Elevate policy
|
||||
|
||||
Do NOT use "elevate all child processes of PC-DMIS". That would elevate
|
||||
`cmd.exe` and anything PC-DMIS launches, which is a large hole on a locked-down
|
||||
shopfloor PC. Elevate only the named toolchain.
|
||||
|
||||
### Application Group: CMM-DODA-Tools
|
||||
|
||||
These are in-house, unsigned tools, so match by SHA-256 (or by path + filename
|
||||
if the install directory is admin-write-only and the confirmed path is known).
|
||||
|
||||
| App | SHA-256 | Spawns children? |
|
||||
|-----|---------|------------------|
|
||||
| `MergeFiles.exe` | `e58ce7599d3bdba816c7ecb183d4f52b32ad8be0b8e4f41813824d8eb472d723` | no |
|
||||
| `PCDToIGES.exe` | `7bdc961c406f7a0f6f8a10752988a17504bdfd691469c08d20f0d5b6673974cf` | no |
|
||||
| `RotateProbeVector.exe` | `f8a1b5b0025769fe0d28dc12826ef5d1fbcdba3b29383799c1eb04b955abebdc` | no |
|
||||
| `DovetailAnalysis.exe` | `86dcb0898bdef4687427ce339520a9c9f5a582890c2241d784fa985019eaaec1` | yes (JVM + python) |
|
||||
|
||||
### Elevate policy
|
||||
|
||||
- Target: the `CMM-DODA-Tools` Application Group
|
||||
- Action: Elevate (run with administrator rights)
|
||||
- Applies to: the CMM computer set + the `ShopFloor` user
|
||||
- Child processes: elevate ONLY for `DovetailAnalysis.exe` (it launches the
|
||||
embedded JVM and python scripts that do the actual file work). The other
|
||||
three have no children.
|
||||
- Match basis: SHA-256 hash
|
||||
|
||||
## Explicitly NOT in the policy
|
||||
|
||||
`winword.exe`, `AcroRd32.exe`, `cmd.exe`. The `CREATE_PDF_FROM_RTF.BAS` rework
|
||||
(Word writes the PDF to user `%TEMP%`, PC-DMIS moves it to the final path with
|
||||
its own elevated in-process token, display via the default PDF handler) removes
|
||||
their need for elevation. Keep Office and Reader out of the elevation set.
|
||||
|
||||
## What the EPM admin needs from GE
|
||||
|
||||
- The computer group = the CMM bays (hostname list, OU, or AD group)
|
||||
- The user = `ShopFloor` (local account)
|
||||
- If using path matching instead of hash: the confirmed install directory of the
|
||||
four exes on a bay (`where MergeFiles.exe`)
|
||||
|
||||
## Verify
|
||||
|
||||
- Before: from elevated PC-DMIS, spawn a child `cmd.exe`, then run
|
||||
`whoami /groups | findstr Label`. Medium Mandatory Level confirms children are
|
||||
not elevated (the bug).
|
||||
- After: the four tools run at High Mandatory Level; report generation plus
|
||||
copy/move/delete to C: and S: succeed; no "not elevated" error.
|
||||
|
||||
## Caveats
|
||||
|
||||
- Hash churn: rebuilding a tool changes its hash, so the Application Group hash
|
||||
must be re-stamped. Path + filename matching avoids this IF the install
|
||||
directory is admin-write-only (so a same-named spoof cannot be dropped there).
|
||||
- Not an ACL fix: the tools hard-check elevation and bail before touching the
|
||||
filesystem, so pre-granting NTFS ACLs alone will not unblock them. The EPM
|
||||
elevation is the actual lever.
|
||||
- Scope tight: match by hash and scope to the CMM computer group + ShopFloor so
|
||||
this elevation never applies fleet-wide.
|
||||
|
||||
## Related code fixes (PXE imaging side)
|
||||
|
||||
- `09-Setup-CMM.ps1` Step 2.5 ACL list corrected from `C:\Program Files\DODA`
|
||||
(nonexistent) to `C:\Apps\DODA` (where `Install-DODA.ps1` actually extracts).
|
||||
- `MergeFiles.exe` expects `C:\Apps\DODA\PreProcess\`, which the DODA zip does
|
||||
not create (it extracts flat). A missing `PreProcess` directory is the likely
|
||||
cause of the historical `MergeFiles.GetDoDAFolder` `DirectoryNotFoundException`
|
||||
crash (see `cmm-utilities` repo `dotNET event.txt`). Have `Install-DODA.ps1`
|
||||
create the `PreProcess` subdir, or have whoever deploys the toolchain own it.
|
||||
- The CMM tool chain (`MergeFiles.exe`, `PCDToIGES.exe`, `RotateProbeVector.exe`,
|
||||
the `.BAS` scripts) lives in the separate `cmm-utilities` repo and is NOT
|
||||
deployed by PXE imaging today. Decide whether imaging should own it so the
|
||||
install path, ACLs, and `PreProcess` directory are consistent.
|
||||
|
||||
## CREATE_PDF_FROM_RTF.BAS rework (removes Word/Reader from the elevation set)
|
||||
|
||||
```vb
|
||||
' CREATE_PDF_FROM_RTF.BAS - rev 1.0: convert in user temp, then move with
|
||||
' PC-DMIS's own token; display via default handler (no elevation needed).
|
||||
Sub Main(filename As String, displayReport As String)
|
||||
Dim rtfFile As String, finalPdf As String, tempPdf As String
|
||||
rtfFile = filename & ".RTF"
|
||||
finalPdf = filename & ".PDF"
|
||||
Dim base As String
|
||||
base = Mid(filename, InStrRev(filename, "\") + 1)
|
||||
tempPdf = Environ$("TEMP") & "\" & base & ".PDF"
|
||||
|
||||
' Word (un-elevated COM) can write to %TEMP% - a user-writable path.
|
||||
Dim word As Object
|
||||
Set word = CreateObject("word.application")
|
||||
word.Visible = False
|
||||
word.Documents.Open rtfFile
|
||||
word.ActiveDocument.SaveAs2 tempPdf, 17 ' 17 = wdFormatPDF
|
||||
word.Quit
|
||||
Set word = Nothing
|
||||
|
||||
' Move temp -> final using PC-DMIS's in-process token (elevated via
|
||||
' CyberArk), so the protected/S: destination is written without needing
|
||||
' Word itself elevated. FileCopy works across volumes; Name does not.
|
||||
If Dir(finalPdf) <> "" Then Kill finalPdf
|
||||
FileCopy tempPdf, finalPdf
|
||||
Kill tempPdf
|
||||
|
||||
' Display via the default PDF handler in the user's own context.
|
||||
If UCase(displayReport) = "TRUE" Then
|
||||
Dim sh As Object
|
||||
Set sh = CreateObject("Shell.Application")
|
||||
sh.ShellExecute finalPdf, "", "", "open", 1
|
||||
End If
|
||||
|
||||
Kill rtfFile
|
||||
End Sub
|
||||
```
|
||||
@@ -196,7 +196,7 @@ Two separate copies of overlapping content with different roles:
|
||||
|
||||
| Path | Source | Used by | Updated when |
|
||||
|------|--------|---------|--------------|
|
||||
| `C:\Enrollment\shopfloor-setup\` | PXE imaging copy from `\\10.9.100.1\enrollment\shopfloor-setup\` | Imaging-flow scripts: `Run-ShopfloorSetup.ps1`, `Stage-Dispatcher.ps1`, `Set-MachineNumber.ps1` -> `Update-MachineNumber.ps1` | Re-image only |
|
||||
| `C:\Enrollment\shopfloor-setup\` | PXE imaging copy from `\\172.16.9.1\enrollment\shopfloor-setup\` | Imaging-flow scripts: `Run-ShopfloorSetup.ps1`, `Stage-Dispatcher.ps1`, `Set-MachineNumber.ps1` -> `Update-MachineNumber.ps1` | Re-image only |
|
||||
| SFLD share `\<scope>\` | Direct upload | GE-Enforce.ps1 / Install-FromManifest.ps1 (every logon) | Direct file upload to share |
|
||||
|
||||
Implication for hot-fixing scripts: a fix to `Restore-UDCData.ps1` needs to
|
||||
|
||||
@@ -52,11 +52,11 @@ Add a new entry (insert before the existing `D12 OptiPlex Family / 7090` entry):
|
||||
the actual driver pack from Dell's catalog by model name (`extract_model_ids`
|
||||
matches "7080") and downloads the latest pack at run time.
|
||||
|
||||
### Side artifacts already on the live PXE server (10.9.100.1)
|
||||
### Side artifacts already on the live PXE server (172.16.9.1)
|
||||
|
||||
- `\\10.9.100.1\winpeapps\_shared\BIOS\OptiPlex_7080_1.37.0.exe` (39.8 MB, BIOS update)
|
||||
- `\\10.9.100.1\image-upload\Deploy\Out-of-box Drivers\Dell_11\OptiPlex\D11 OptiPlex Family\win11_70809ntr8_a09.zip` (Win11 driver pack, 2.6 GB)
|
||||
- `\\10.9.100.1\winpeapps\_shared\BIOS\models.txt` includes the 7080 line.
|
||||
- `\\172.16.9.1\winpeapps\_shared\BIOS\OptiPlex_7080_1.37.0.exe` (39.8 MB, BIOS update)
|
||||
- `\\172.16.9.1\image-upload\Deploy\Out-of-box Drivers\Dell_11\OptiPlex\D11 OptiPlex Family\win11_70809ntr8_a09.zip` (Win11 driver pack, 2.6 GB)
|
||||
- `\\172.16.9.1\winpeapps\_shared\BIOS\models.txt` includes the 7080 line.
|
||||
|
||||
These persist regardless of `geastandardpbr/` rebuilds. Only the model-registry
|
||||
edits need to be re-applied after a USB re-import.
|
||||
|
||||
47
docs/post-deploy-checklist.live.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html><head>
|
||||
<meta charset="utf-8">
|
||||
<title>Post-Deploy Verification Checklist</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.css">
|
||||
<style>
|
||||
body { box-sizing: border-box; max-width: 980px; margin: 2em auto; padding: 0 2em; }
|
||||
.markdown-body img { max-width: 100%; }
|
||||
.markdown-body ul.task-list { list-style: none; padding-left: 0; }
|
||||
.markdown-body li.task-list-item { list-style: none; }
|
||||
.markdown-body li.task-list-item input[type=checkbox] { margin-right: .5em; transform: scale(1.2); }
|
||||
@media print { body { max-width: none; margin: 0; padding: 1em; } }
|
||||
</style>
|
||||
</head><body class="markdown-body">
|
||||
<h1 id="post-deploy-checklist">Post-Deploy Checklist</h1>
|
||||
<p>Run after first boot. Sign off when all boxes checked. Fail any step, see <a href="post-deploy-debug-flowchart.md">post-deploy-debug-flowchart.md</a>.</p>
|
||||
<h2 id="1-common-shop-floor">1. Common Shop Floor</h2>
|
||||
<ul class="task-list">
|
||||
<li class="task-list-item"><input type="checkbox" disabled> <code>Shopfloor Tools</code> then <code>WJ Shopfloor</code> opens</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Login prompt and menu screen render</li>
|
||||
</ul>
|
||||
<p>Fail: check corp network, ping <code>WJFMS3.AE.GE.COM</code>.</p>
|
||||
<h2 id="2-controller-skip-if-standalone-pc">2. Controller (skip if standalone PC)</h2>
|
||||
<ul class="task-list">
|
||||
<li class="task-list-item"><input type="checkbox" disabled> <code>ping 192.168.1.1</code> returns 4 of 4 replies</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> NTLARS General tab populated</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> NTLARS FMS Host Primary set to <code>WJFMS3.AE.GE.COM</code> (FQDN)</li>
|
||||
</ul>
|
||||
<p>Fail ping, see <a href="post-deploy-debug-flowchart.md#2b-controller-nic-has-no-static-ip">2B NIC</a>.
|
||||
Blank General, see <a href="post-deploy-debug-flowchart.md#2a-ntlars-reg-file-never-imported-blank-general-tab-fields">2A reg load</a>.</p>
|
||||
<h2 id="3-udc-com-port">3. UDC COM port</h2>
|
||||
<ul class="task-list">
|
||||
<li class="task-list-item"><input type="checkbox" disabled> UDC opens with no machine-communication error dialog</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> <code>Tools</code> then <code>Retry Connection</code> succeeds (no error)</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Port Name matches physical port: onboard COM1, PCIe card COM2 or COM4</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Data lines populate after reopen</li>
|
||||
</ul>
|
||||
<p>Fail, see <a href="post-deploy-debug-flowchart.md#step-4-set-the-correct-com-port">Step 4</a>.</p>
|
||||
<h2 id="4-printers-genspect-all-nearby">4. Printers (Genspect: all nearby)</h2>
|
||||
<ul class="task-list">
|
||||
<li class="task-list-item"><input type="checkbox" disabled> <code>Install Printers</code> shortcut opens</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Each nearby printer installed, status Ready</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Test page prints</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Genspect: default printer set</li>
|
||||
</ul>
|
||||
<p>Fail, see <a href="../printer-mapping.md">printer-mapping.md</a>.</p>
|
||||
</body></html>
|
||||
37
docs/post-deploy-checklist.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Post-Deploy Checklist
|
||||
|
||||
Run after first boot. Sign off when all boxes checked. Fail any step, see [post-deploy-debug-flowchart.md](post-deploy-debug-flowchart.md).
|
||||
|
||||
## 1. Common Shop Floor
|
||||
|
||||
- [ ] `Shopfloor Tools` then `WJ Shopfloor` opens
|
||||
- [ ] Login prompt and menu screen render
|
||||
|
||||
Fail: check corp network, ping `WJFMS3.AE.GE.COM`.
|
||||
|
||||
## 2. Controller (skip if standalone PC)
|
||||
|
||||
- [ ] `ping 192.168.1.1` returns 4 of 4 replies
|
||||
- [ ] NTLARS General tab populated
|
||||
- [ ] NTLARS FMS Host Primary set to `WJFMS3.AE.GE.COM` (FQDN)
|
||||
|
||||
Fail ping, see [2B NIC](post-deploy-debug-flowchart.md#2b-controller-nic-has-no-static-ip).
|
||||
Blank General, see [2A reg load](post-deploy-debug-flowchart.md#2a-ntlars-reg-file-never-imported-blank-general-tab-fields).
|
||||
|
||||
## 3. UDC COM port
|
||||
|
||||
- [ ] UDC opens with no machine-communication error dialog
|
||||
- [ ] `Tools` then `Retry Connection` succeeds (no error)
|
||||
- [ ] Port Name matches physical port: onboard COM1, PCIe card COM2 or COM4
|
||||
- [ ] Data lines populate after reopen
|
||||
|
||||
Fail, see [Step 4](post-deploy-debug-flowchart.md#step-4-set-the-correct-com-port).
|
||||
|
||||
## 4. Printers (Genspect: all nearby)
|
||||
|
||||
- [ ] `Install Printers` shortcut opens
|
||||
- [ ] Each nearby printer installed, status Ready
|
||||
- [ ] Test page prints
|
||||
- [ ] Genspect: default printer set
|
||||
|
||||
Fail, see [printer-mapping.md](../printer-mapping.md).
|
||||
59
docs/post-deploy-checklist.static.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!doctype html>
|
||||
<html><head>
|
||||
<meta charset="utf-8">
|
||||
<title>Post-Deploy Verification Checklist</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, system-ui, "Segoe UI", sans-serif; max-width: 980px; margin: 2em auto; padding: 0 2em; line-height: 1.5; color: #24292f; }
|
||||
h1, h2, h3 { border-bottom: 1px solid #d0d7de; padding-bottom: .3em; scroll-margin-top: 1em; }
|
||||
h1 { font-size: 2em; }
|
||||
code { background: #f6f8fa; padding: .2em .4em; border-radius: 6px; font-size: 85%; }
|
||||
pre { background: #f6f8fa; padding: 1em; border-radius: 6px; overflow: auto; }
|
||||
pre code { background: none; padding: 0; }
|
||||
table { border-collapse: collapse; }
|
||||
table th, table td { border: 1px solid #d0d7de; padding: 6px 13px; }
|
||||
table tr:nth-child(2n) { background: #f6f8fa; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
blockquote { border-left: .25em solid #d0d7de; padding: 0 1em; color: #57606a; }
|
||||
ul.task-list, li.task-list-item { list-style: none; }
|
||||
ul.task-list { padding-left: 0; }
|
||||
li.task-list-item input[type=checkbox] { margin-right: .5em; transform: scale(1.2); }
|
||||
hr { border: 0; border-top: 1px solid #d0d7de; margin: 2em 0; }
|
||||
@media print {
|
||||
body { max-width: none; margin: 0; padding: 1em; }
|
||||
input[type=checkbox] { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
}
|
||||
</style>
|
||||
</head><body>
|
||||
<h1 id="post-deploy-checklist">Post-Deploy Checklist</h1>
|
||||
<p>Run after first boot. Sign off when all boxes checked. Fail any step, see <a href="post-deploy-debug-flowchart.md">post-deploy-debug-flowchart.md</a>.</p>
|
||||
<h2 id="1-common-shop-floor">1. Common Shop Floor</h2>
|
||||
<ul class="task-list">
|
||||
<li class="task-list-item"><input type="checkbox" disabled> <code>Shopfloor Tools</code> then <code>WJ Shopfloor</code> opens</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Login prompt and menu screen render</li>
|
||||
</ul>
|
||||
<p>Fail: check corp network, ping <code>WJFMS3.AE.GE.COM</code>.</p>
|
||||
<h2 id="2-controller-skip-if-standalone-pc">2. Controller (skip if standalone PC)</h2>
|
||||
<ul class="task-list">
|
||||
<li class="task-list-item"><input type="checkbox" disabled> <code>ping 192.168.1.1</code> returns 4 of 4 replies</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> NTLARS General tab populated</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> NTLARS FMS Host Primary set to <code>WJFMS3.AE.GE.COM</code> (FQDN)</li>
|
||||
</ul>
|
||||
<p>Fail ping, see <a href="post-deploy-debug-flowchart.md#2b-controller-nic-has-no-static-ip">2B NIC</a>.
|
||||
Blank General, see <a href="post-deploy-debug-flowchart.md#2a-ntlars-reg-file-never-imported-blank-general-tab-fields">2A reg load</a>.</p>
|
||||
<h2 id="3-udc-com-port">3. UDC COM port</h2>
|
||||
<ul class="task-list">
|
||||
<li class="task-list-item"><input type="checkbox" disabled> UDC opens with no machine-communication error dialog</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> <code>Tools</code> then <code>Retry Connection</code> succeeds (no error)</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Port Name matches physical port: onboard COM1, PCIe card COM2 or COM4</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Data lines populate after reopen</li>
|
||||
</ul>
|
||||
<p>Fail, see <a href="post-deploy-debug-flowchart.md#step-4-set-the-correct-com-port">Step 4</a>.</p>
|
||||
<h2 id="4-printers-genspect-all-nearby">4. Printers (Genspect: all nearby)</h2>
|
||||
<ul class="task-list">
|
||||
<li class="task-list-item"><input type="checkbox" disabled> <code>Install Printers</code> shortcut opens</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Each nearby printer installed, status Ready</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Test page prints</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled> Genspect: default printer set</li>
|
||||
</ul>
|
||||
<p>Fail, see <a href="../printer-mapping.md">printer-mapping.md</a>.</p>
|
||||
</body></html>
|
||||
@@ -24,7 +24,7 @@ flowchart TD
|
||||
U2 --> U3[Tools - Options - Serial tab]
|
||||
U3 --> U4{Which physical COM port is the cable in?}
|
||||
U4 -->|Intel / onboard| U5[Set Port Name = COM 1]
|
||||
U4 -->|PCIe add-in card| U6[Set Port Name = COM 2]
|
||||
U4 -->|PCIe add-in card| U6[Set Port Name = COM 2 or COM 4]
|
||||
U5 --> U7[Save - File - Exit - reopen UDC]
|
||||
U6 --> U7
|
||||
U7 --> U8([Verify data lines populate])
|
||||
@@ -51,7 +51,7 @@ flowchart TD
|
||||
click U3 "#step-3-open-options" "Step 3 - Options"
|
||||
click U4 "#step-4-set-the-correct-com-port" "Step 4 - COM port"
|
||||
click U5 "#step-4-set-the-correct-com-port" "Step 4 - COM 1"
|
||||
click U6 "#step-4-set-the-correct-com-port" "Step 4 - COM 2"
|
||||
click U6 "#step-4-set-the-correct-com-port" "Step 4 - COM 2 or COM 4"
|
||||
click U7 "#step-5-exit-to-apply" "Step 5 - Exit"
|
||||
|
||||
click D2 "#2a-ntlars-reg-file-never-imported-blank-general-tab-fields" "2A - Load reg backup"
|
||||
@@ -113,7 +113,7 @@ Click the **Serial** tab on the left. Set **Port Name** to match the **physical*
|
||||
|
||||

|
||||
|
||||
- **COM 2** = PCIe add-in serial card
|
||||
- **COM 2 or COM 4** = PCIe add-in serial card (Windows enumeration varies by hardware - check Device Manager -> Ports if unsure)
|
||||
|
||||
Logical:
|
||||
|
||||
@@ -138,7 +138,7 @@ Reopen UDC. Data lines should start populating as the machine runs.
|
||||
### If data still does not appear
|
||||
|
||||
Once the COM port is correct, ~~rule out~~ check these in order:
|
||||
- Wrong physical cable - cable is in COM1 socket but Port Name set to COM 2 (or vice versa). Re-check Step 4.
|
||||
- Wrong physical cable - cable is in COM1 socket but Port Name set to COM 2 / COM 4 (or vice versa). Re-check Step 4.
|
||||
- Cable / connector damaged - swap with a known-good cable.
|
||||
- Machine controller side not transmitting - confirm at the controller HMI.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Step-by-step for imaging a new (or replacement) shopfloor PC that will sit at a
|
||||
|
||||
- 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)
|
||||
- PXE server is running and reachable (verify by pinging `172.16.9.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)
|
||||
|
||||
@@ -229,7 +229,7 @@ The script needs a desktop session. Won't run via WinRM/SSH/non-interactive. Mak
|
||||
|
||||
## Reference
|
||||
|
||||
- **PXE server**: `10.9.100.1`
|
||||
- **PXE server**: `172.16.9.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`
|
||||
|
||||
@@ -156,21 +156,36 @@
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Fetch-StagingPayload.ps1"</CommandLine>
|
||||
<Description>Fetch bulk staging (shopfloor-setup tree + preinstall bundle) from the PXE share on a fresh mount, BEFORE the production-network switch takes the bay off the imaging LAN. Detailed log at C:\Logs\Fetch\.</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Verify-And-Heal-Staging.ps1"</CommandLine>
|
||||
<Description>Verify every imaging payload arrived and re-pull anything missing from the PXE share (incl the CMM bundle + selected-bay backup) while still on the imaging LAN, BEFORE wait-for-internet switches the bay to the production network. Log at C:\Logs\Fetch\.</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\wait-for-internet.ps1"</CommandLine>
|
||||
<Description>Prompt to connect production network then wait for TCP 443 connectivity</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<Order>7</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\migrate-to-wifi.ps1"</CommandLine>
|
||||
<Description>Migrate from wired to WiFi if WiFi adapter present, else stay on wired</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<Order>8</Order>
|
||||
<CommandLine>msiexec.exe /i "C:\PreInstall\installers\powershell7\PowerShell-7.5.4-win-x64.msi" /qn /norestart ADD_PATH=1 USE_MU=0 ENABLE_MU=0 DISABLE_TELEMETRY=1</CommandLine>
|
||||
<Description>Install PowerShell 7 BEFORE PPKG so Intune SetupCredentials Win32App finds pwsh.exe (race fix)</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>9</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"</CommandLine>
|
||||
<Description>Run GCCH Enrollment</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<Order>10</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
|
||||
<Description>Run shopfloor PC type setup</Description>
|
||||
</SynchronousCommand>
|
||||
|
||||
@@ -80,10 +80,10 @@ echo " IFACE=$IFACE, bringing up..."
|
||||
ip link set "$IFACE" up || ifconfig "$IFACE" up
|
||||
sleep 2
|
||||
|
||||
SERVER=10.9.100.1
|
||||
ifconfig "$IFACE" 10.9.100.250 netmask 255.255.255.0 up
|
||||
SERVER=172.16.9.1
|
||||
ifconfig "$IFACE" 172.16.9.250 netmask 255.255.255.0 up
|
||||
sleep 1
|
||||
echo " IP: 10.9.100.250 SERVER: $SERVER"
|
||||
echo " IP: 172.16.9.250 SERVER: $SERVER"
|
||||
ip addr
|
||||
|
||||
echo "[3/5] Downloading airootfs.sfs (~756 MB)..."
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
<username encrypted="false">blancco</username>
|
||||
<password encrypted="false">blancco</password>
|
||||
<domain/>
|
||||
<hostname>10.9.100.1</hostname>
|
||||
<hostname>172.16.9.1</hostname>
|
||||
<path>blancco-reports</path>
|
||||
<protocols key="protocol" type="array">
|
||||
<protocol selected="true">smb</protocol>
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
# Previously this disabled all wired NICs at first logon to keep PPKG /
|
||||
# Intune enrollment routing internet traffic via WiFi. The wired NIC was
|
||||
# preferred by Windows because the PXE dnsmasq was handing out a default
|
||||
# gateway (dhcp-option=3,10.9.100.1) which Windows installed as a default
|
||||
# gateway (dhcp-option=3,172.16.9.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
|
||||
# traffic then black-holed at 172.16.9.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.
|
||||
# for same-subnet PXE/SMB traffic to 172.16.9.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
|
||||
|
||||
@@ -3,17 +3,28 @@
|
||||
"Site": "West Jefferson",
|
||||
"Applications": [
|
||||
{
|
||||
"_comment": "Oracle Client 11.2 Administrator - installed first because downstream apps (eDNC/NTLARS/UDC and CMM tooling) link against the Oracle home and fail cold if it's missing. Installer is a .cmd wrapper (Type=EXE is the preinstall runner's shim for non-MSI launchers, same pattern as OpenText Setup-OpenText.cmd). The wrapper expects Oracle_OracleDatabase_11r2_V03.zip (686 MB) staged next to it, unpacks to %TEMP%, runs Oracle Universal Installer silently with ge_client_install.rsp, then cleans up the staging dir. OUI exit 3 is treated as success (warnings-but-ok). Detection via the registered home key; downstream upgrades or version pins are handled by the runtime enforcer's Oracle Client 11.2 manifest entry in common/manifest.json.",
|
||||
"_comment": "PowerShell 7.5.4 - installed BEFORE PPKG via FlatUnattendW10-shopfloor.xml FirstLogonCommand Order 6 (race fix: Intune SetupCredentials Win32App install command starts with pwsh.exe; if PS7 not yet installed when that Win32App fires, it errors with FILE_NOT_FOUND 0x80070002 and IME's GRS retry never re-fires under V3Processor). This entry is a backstop - no-op via ProductCode detection if unattend Order 6 already installed it. PreEnrollment flag is informational; runner does not currently filter on it.",
|
||||
"Name": "PowerShell 7.5.4",
|
||||
"Installer": "powershell7\\PowerShell-7.5.4-win-x64.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart ADD_PATH=1 USE_MU=0 ENABLE_MU=0 DISABLE_TELEMETRY=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{E8159677-ACF8-4D64-9D36-5C36B8BBEA39}",
|
||||
"PreEnrollment": true,
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "Oracle Client 11.2 Administrator - installed first because downstream apps (eDNC/NTLARS/UDC and CMM tooling) link against the Oracle home and fail cold if it's missing. Installer is a .cmd wrapper (Type=EXE is the preinstall runner's shim for non-MSI launchers, same pattern as OpenText Setup-OpenText.cmd). The wrapper expects Oracle_OracleDatabase_11r2_V03.zip (686 MB) staged next to it, unpacks to %TEMP%, runs Oracle Universal Installer silently with ge_client_install.rsp, then cleans up the staging dir. OUI exit 3 is treated as success (warnings-but-ok). Detection via the registered home key; downstream upgrades or version pins are handled by the runtime enforcer's Oracle Client 11.2 manifest entry in common/manifest.json. Scoped to the DNC-bearing PC types (collections, nocollections, partmarker, heattreat) plus CMM, whose metrology tooling links the Oracle home; non-DNC types (Genspect, Keyence, WaxAndTrace, Display, Timeclock, Lab) do not get it.",
|
||||
"Name": "Oracle Client 11.2",
|
||||
"Installer": "oracle\\Install-Oracle11r2.cmd",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "",
|
||||
"LogFile": "C:\\Logs\\OracleClient\\install.log",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Oracle\\KEY_OraClient11g_home1",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Oracle\\KEY_OraClient11g_home1",
|
||||
"DetectionName": "ORACLE_HOME_NAME",
|
||||
"DetectionValue": "OraClient11g_home1",
|
||||
"PCTypes": ["Standard", "CMM", "Genspect", "Keyence", "WaxAndTrace", "Display"]
|
||||
"PCTypes": ["gea-shopfloor-collections", "gea-shopfloor-nocollections", "gea-shopfloor-partmarker", "gea-shopfloor-heattreat", "gea-shopfloor-cmm"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2008 SP1 x86 - the bootstrapper (vcredist2008_x86.exe) ignores /norestart and triggers an immediate Windows reboot when files are in use (per Aaron Stebner's MSDN docs). Fix: install the extracted vc_red.msi directly with REBOOT=ReallySuppress, which IS hard-honored by Windows Installer. msiexec may return 3010 (would-have-rebooted-but-suppressed) but won't actually reboot. cab name 'vc_red.cab' is hardcoded in the MSI's Media table - do not rename.",
|
||||
@@ -153,7 +164,7 @@
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "",
|
||||
"LogFile": "C:\\Logs\\PreInstall\\Setup-OpenText.log",
|
||||
"PCTypes": ["Standard", "CMM", "Keyence", "Genspect", "WaxAndTrace", "Lab"]
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "UDC_Setup.exe spawns a hidden WPF window (UDC.exe) after install and never exits, so the runner needs KillAfterDetection: true to terminate UDC_Setup.exe + UDC.exe once the registry detection passes. This is an OPT-IN flag - normal installers should NOT set it because killing msiexec mid-install leaves msiserver holding the install mutex and the next msiexec call returns 1618 (Oracle hit this exact bug).",
|
||||
@@ -164,7 +175,9 @@
|
||||
"KillAfterDetection": true,
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\UDC",
|
||||
"PCTypes": ["Standard-Machine"]
|
||||
"PCTypes": ["gea-shopfloor-collections"],
|
||||
"PCTypesStrict": true,
|
||||
"_pcTypesNote": "UDC = the C in 'collections'. nocollections does NOT collect data so MUST NOT install UDC. PCTypesStrict bypasses the alias-expansion matcher so a nocollections PC's myNames (which transitively contains gea-shopfloor-collections via the Standard group) still won't match this entry."
|
||||
},
|
||||
{
|
||||
"_comment": "Display kiosk app (Lobby Display or Dashboard). Install-KioskApp.cmd wrapper reads C:\\Enrollment\\display-type.txt to determine which installer to run. Both GEAerospaceLobbyDisplaySetup.exe and GEAerospaceDashboardSetup.exe must be staged in the display\\ subtree alongside the wrapper. Inno Setup /VERYSILENT is idempotent so no detection needed.",
|
||||
@@ -186,7 +199,7 @@
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "Shopfloor Standard serial-port drivers: StarTech PCIe serial adapter (MosChip-based) + Prolific PL2303 USB-to-serial. Install-Drivers.cmd runs pnputil /add-driver with /subdirs /install so every bundled INF under drivers/ lands in the Windows driver store and auto-binds to matching hardware present now or plugged in later. Scoped to Standard PCs (both Machine + Timeclock) because the PCTypes filter is type-level only; installing a serial driver on a Timeclock without the hardware is harmless - it just sits in the driver store.",
|
||||
"_comment": "Shopfloor Standard serial-port drivers: StarTech PCIe serial adapter (MosChip-based) + Prolific PL2303 USB-to-serial. Install-Drivers.cmd runs pnputil /add-driver with /subdirs /install so every bundled INF under drivers/ lands in the Windows driver store and auto-binds to matching hardware present now or plugged in later. Installed on every PC type (PCTypes ['*']) because serial hardware turns up across bays; a serial driver on a PC without the hardware is harmless - it just sits in the driver store until matching hardware is plugged in.",
|
||||
"Name": "Shopfloor Serial Drivers",
|
||||
"Installer": "drivers\\Install-Drivers.cmd",
|
||||
"Type": "EXE",
|
||||
@@ -194,7 +207,7 @@
|
||||
"LogFile": "C:\\Logs\\PreInstall\\Install-Drivers.log",
|
||||
"DetectionMethod": "File",
|
||||
"DetectionPath": "C:\\ProgramData\\PXEDrivers\\drivers-installed.marker",
|
||||
"PCTypes": ["Standard"]
|
||||
"PCTypes": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# pxe-dhcp-hook.sh - dnsmasq dhcp-script hook.
|
||||
#
|
||||
# Runs every time a PXE client gets/changes/releases a DHCP lease on
|
||||
# 10.9.100.0/24. Flushes conntrack entries and drops any lingering
|
||||
# 172.16.9.0/24. Flushes conntrack entries and drops any lingering
|
||||
# TCP sockets for that client IP. Prevents stale server-side state from
|
||||
# causing "System error 53 - network path not found" when a WinPE client
|
||||
# re-images the same machine without a clean SMB session teardown.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# Step 2: restart nmbd (NetBIOS daemon - separate from smbd)
|
||||
# Step 3: restart smbd (full smbd restart, kills all child sessions)
|
||||
# Step 4: kill any leftover smbd child processes that survived restart
|
||||
# Step 5: flush conntrack for 10.9.100.0/24 (kernel connection tracking)
|
||||
# Step 5: flush conntrack for 172.16.9.0/24 (kernel connection tracking)
|
||||
# Step 6: flush ARP / neighbour cache on br-pxe
|
||||
# Step 7: drop TCP sockets on port 445 via ss -K
|
||||
# Step 8: restart dnsmasq (DHCP/TFTP state as a last resort before reboot)
|
||||
@@ -56,10 +56,10 @@ sleep 1
|
||||
systemctl start smbd 2>&1
|
||||
pause "Step 4 done"
|
||||
|
||||
echo "=== Step 5/8: flush conntrack entries for 10.9.100.0/24 ==="
|
||||
echo "=== Step 5/8: flush conntrack entries for 172.16.9.0/24 ==="
|
||||
if command -v conntrack >/dev/null 2>&1; then
|
||||
conntrack -D -s 10.9.100.0/24 2>&1 || true
|
||||
conntrack -D -d 10.9.100.0/24 2>&1 || true
|
||||
conntrack -D -s 172.16.9.0/24 2>&1 || true
|
||||
conntrack -D -d 172.16.9.0/24 2>&1 || true
|
||||
else
|
||||
echo " conntrack tool not installed - skipping (apt install conntrack)"
|
||||
fi
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
loop: "{{ ansible_interfaces | select('match','^e(th|n)') | list }}"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Find interface with 10.9.100.1 already configured"
|
||||
- name: "Find interface with 172.16.9.1 already configured"
|
||||
set_fact:
|
||||
preconfigured_iface: >-
|
||||
{{ ansible_interfaces
|
||||
@@ -80,7 +80,7 @@
|
||||
| map('regex_replace','^(.*)$','ansible_\1')
|
||||
| map('extract', hostvars[inventory_hostname])
|
||||
| selectattr('ipv4','defined')
|
||||
| selectattr('ipv4.address','equalto','10.9.100.1')
|
||||
| selectattr('ipv4.address','equalto','172.16.9.1')
|
||||
| map(attribute='device')
|
||||
| list
|
||||
| first
|
||||
@@ -147,11 +147,11 @@
|
||||
port=0
|
||||
interface={{ pxe_iface }}
|
||||
bind-interfaces
|
||||
dhcp-range=10.9.100.10,10.9.100.100,12h
|
||||
dhcp-range=172.16.9.10,172.16.9.100,12h
|
||||
# No default gateway (option 3) and no DNS (option 6) handed out:
|
||||
# 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
|
||||
# add a default route via 172.16.9.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
|
||||
@@ -163,7 +163,7 @@
|
||||
# Important: dnsmasq DEFAULTS to sending its own listening address as
|
||||
# both router and DNS when these options are unset. Commenting them
|
||||
# out is NOT the same as disabling - imaged PCs (and Blancco PXE
|
||||
# clients) end up with 10.9.100.1 as gateway. The empty-value form
|
||||
# clients) end up with 172.16.9.1 as gateway. The empty-value form
|
||||
# below explicitly suppresses both options.
|
||||
dhcp-option=3
|
||||
dhcp-option=6
|
||||
@@ -227,7 +227,7 @@
|
||||
content: |
|
||||
#!ipxe
|
||||
|
||||
set server 10.9.100.1
|
||||
set server 172.16.9.1
|
||||
|
||||
:menu
|
||||
menu GE Aerospace PXE Boot Menu
|
||||
@@ -505,7 +505,7 @@
|
||||
|
||||
- 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
|
||||
# net use B: \\172.16.9.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
|
||||
@@ -571,7 +571,11 @@
|
||||
# the short-lived flows that PXE imaging produces.
|
||||
socket options = TCP_NODELAY SO_KEEPALIVE IPTOS_LOWDELAY
|
||||
keepalive = 30
|
||||
deadtime = 5
|
||||
# deadtime=0 (disabled): WinPE maps the enrollment share early then
|
||||
# idles for minutes during the WIM apply. A non-zero deadtime drops
|
||||
# that idle session, so the post-apply staging copies failed (bay
|
||||
# left with only site-config.json). 0 = never auto-disconnect idle.
|
||||
deadtime = 0
|
||||
|
||||
- name: "Configure Samba shares"
|
||||
blockinfile:
|
||||
@@ -681,7 +685,11 @@
|
||||
src: "{{ usb_mount }}/FlatUnattendW10-shopfloor.xml"
|
||||
dest: "{{ samba_share }}/{{ item }}/Deploy/FlatUnattendW10.xml"
|
||||
mode: '0644'
|
||||
force: no
|
||||
# force: yes - repo is source of truth. force: no let the live shopfloor
|
||||
# unattend go stale (missing the Fetch + Verify-And-Heal staging steps),
|
||||
# and a playbook run never repaired it. Keep it in sync like the standard
|
||||
# /engineer unattend below.
|
||||
force: yes
|
||||
loop: "{{ shopfloor_types }}"
|
||||
ignore_errors: yes
|
||||
|
||||
@@ -899,7 +907,7 @@
|
||||
shell: |
|
||||
set -e
|
||||
python3 -c 'import xml.etree.ElementTree as ET; ET.parse("{{ web_root }}/blancco/preferences.xml")'
|
||||
grep -q '<hostname>10.9.100.1</hostname>' "{{ web_root }}/blancco/preferences.xml"
|
||||
grep -q '<hostname>172.16.9.1</hostname>' "{{ web_root }}/blancco/preferences.xml"
|
||||
grep -q '<path>blancco-reports</path>' "{{ web_root }}/blancco/preferences.xml"
|
||||
changed_when: false
|
||||
|
||||
@@ -1089,7 +1097,7 @@
|
||||
# Single-NIC fresh-deploy default. Boxes that need higher throughput
|
||||
# (e.g. WJF prod uses a USB-C 5 Gbps NIC) override this with a bridge
|
||||
# config bonding the USB NIC + onboard NIC into br-pxe. Live override
|
||||
# currently deployed on 10.9.100.1 (do NOT re-run this task there
|
||||
# currently deployed on 172.16.9.1 (do NOT re-run this task there
|
||||
# without first reviewing /etc/netplan/50-cloud-init.yaml.pre-gold-swap):
|
||||
#
|
||||
# network:
|
||||
@@ -1101,7 +1109,7 @@
|
||||
# bridges:
|
||||
# br-pxe:
|
||||
# interfaces: [enp128s31f6, enx34c8d6b11010]
|
||||
# addresses: [10.9.100.1/24]
|
||||
# addresses: [172.16.9.1/24]
|
||||
# parameters:
|
||||
# stp: false
|
||||
#
|
||||
@@ -1120,7 +1128,7 @@
|
||||
ethernets:
|
||||
{{ pxe_iface }}:
|
||||
dhcp4: no
|
||||
addresses: [10.9.100.1/24]
|
||||
addresses: [172.16.9.1/24]
|
||||
notify: "Apply netplan"
|
||||
|
||||
handlers:
|
||||
|
||||
142
playbook/select-waxtrace-asset.ps1
Normal file
@@ -0,0 +1,142 @@
|
||||
# select-waxtrace-asset.ps1 - Arrow-key bay picker for wax/trace imaging.
|
||||
#
|
||||
# Reads bay-config.csv on the PXE share to build the menu of known bays.
|
||||
# Falls back to INDEX.csv (cal-disc index) if bay-config.csv is missing.
|
||||
# Operator picks with Up/Down arrows + Enter. Always appends an
|
||||
# "Other (new bay)" option at the end for unlisted bays - selecting it
|
||||
# falls back to a free-text prompt.
|
||||
#
|
||||
# Writes the chosen asset tag to $OutFile (one line, no trailing newline).
|
||||
# startnet.cmd reads that file back into the MACHINENUM batch var.
|
||||
#
|
||||
# Runs in WinPE PowerShell. Win10/11 WinPE ships powershell.exe with
|
||||
# System.Console.ReadKey support. Tested 2026-05-18.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = asset tag written to $OutFile
|
||||
# 1 = user cancelled (Esc) - $OutFile not written
|
||||
# 2 = no readable bay source AND no fallback entered
|
||||
|
||||
param(
|
||||
[string]$IndexPath = 'Y:\installers-post\waxtrace\bay-config.csv',
|
||||
[Parameter(Mandatory=$true)][string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
function Read-BayList {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return @() }
|
||||
try {
|
||||
$rows = @(Import-Csv -LiteralPath $Path)
|
||||
# bay-config.csv has asset_tag,ftpak_version,model,user_id,hw_sn,hw_id,host,notes
|
||||
# INDEX.csv (legacy) has asset_tag,unit_serial,probe_part,...
|
||||
$isBayCfg = $rows.Count -gt 0 -and ($rows[0].PSObject.Properties.Name -contains 'ftpak_version')
|
||||
return $rows | Sort-Object -Property asset_tag | ForEach-Object {
|
||||
if ($isBayCfg) {
|
||||
[PSCustomObject]@{
|
||||
asset_tag = $_.asset_tag
|
||||
col1 = $_.ftpak_version
|
||||
col2 = $_.model
|
||||
col3 = $_.user_id
|
||||
schema = 'bay-config'
|
||||
}
|
||||
} else {
|
||||
[PSCustomObject]@{
|
||||
asset_tag = $_.asset_tag
|
||||
col1 = $_.unit_serial
|
||||
col2 = $_.probe_part
|
||||
col3 = ''
|
||||
schema = 'index'
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return @()
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Menu {
|
||||
param([object[]]$Items, [int]$Selected, [string]$Title, [string]$Schema)
|
||||
Clear-Host
|
||||
Write-Host ""
|
||||
Write-Host " ============================================================"
|
||||
Write-Host " $Title"
|
||||
Write-Host " ============================================================"
|
||||
Write-Host ""
|
||||
Write-Host " Up / Down arrows = navigate, Enter = select, Esc = cancel"
|
||||
Write-Host ""
|
||||
if ($Schema -eq 'bay-config') {
|
||||
Write-Host (" {0,-10} {1,-8} {2,-10} {3}" -f 'ASSET','FTPAK','MODEL','USER ID')
|
||||
Write-Host (" {0,-10} {1,-8} {2,-10} {3}" -f '-----','-----','-----','-------')
|
||||
} else {
|
||||
Write-Host (" {0,-10} {1,-14} {2}" -f 'ASSET','SERIAL','PROBE')
|
||||
Write-Host (" {0,-10} {1,-14} {2}" -f '-----','------','-----')
|
||||
}
|
||||
for ($i = 0; $i -lt $Items.Count; $i++) {
|
||||
$item = $Items[$i]
|
||||
if ($item -is [string]) {
|
||||
$line = $item
|
||||
} elseif ($Schema -eq 'bay-config') {
|
||||
$line = "{0,-10} {1,-8} {2,-10} {3}" -f $item.asset_tag, $item.col1, $item.col2, $item.col3
|
||||
} else {
|
||||
$line = "{0,-10} {1,-14} {2}" -f $item.asset_tag, $item.col1, $item.col2
|
||||
}
|
||||
if ($i -eq $Selected) {
|
||||
Write-Host (" > " + $line) -ForegroundColor Black -BackgroundColor White
|
||||
} else {
|
||||
Write-Host (" " + $line)
|
||||
}
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Try bay-config.csv first; fall back to INDEX.csv if missing OR if the
|
||||
# explicit -IndexPath argument points to INDEX.csv (legacy callers).
|
||||
$bays = @(Read-BayList -Path $IndexPath)
|
||||
if ($bays.Count -eq 0 -and $IndexPath -notmatch 'INDEX\.csv$') {
|
||||
$fallback = 'Y:\installers-post\waxtrace\calibrations\INDEX.csv'
|
||||
if (Test-Path -LiteralPath $fallback) {
|
||||
Write-Host " (no bay-config.csv at $IndexPath - falling back to $fallback)"
|
||||
$bays = @(Read-BayList -Path $fallback)
|
||||
}
|
||||
}
|
||||
$schema = if ($bays.Count -gt 0) { $bays[0].schema } else { 'bay-config' }
|
||||
|
||||
$menuItems = @()
|
||||
foreach ($b in $bays) { $menuItems += $b }
|
||||
$menuItems += '** Other (new bay - enter asset tag manually) **'
|
||||
|
||||
$sel = 0
|
||||
while ($true) {
|
||||
Show-Menu -Items $menuItems -Selected $sel -Title 'Wax/Trace Asset Tag' -Schema $schema
|
||||
$key = [System.Console]::ReadKey($true)
|
||||
switch ($key.Key) {
|
||||
'UpArrow' { if ($sel -gt 0) { $sel-- } }
|
||||
'DownArrow' { if ($sel -lt ($menuItems.Count - 1)) { $sel++ } }
|
||||
'Enter' {
|
||||
if ($sel -eq ($menuItems.Count - 1)) {
|
||||
Write-Host ""
|
||||
$manual = Read-Host " Enter asset tag (e.g. WJRP9999) or blank to abort"
|
||||
if ($manual) {
|
||||
$manual = $manual.Trim().ToUpper()
|
||||
Set-Content -LiteralPath $OutFile -Value $manual -NoNewline -Encoding ascii
|
||||
Write-Host ""
|
||||
Write-Host " Saved asset tag: $manual"
|
||||
Start-Sleep -Seconds 1
|
||||
exit 0
|
||||
} else {
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
$pick = $bays[$sel].asset_tag
|
||||
Set-Content -LiteralPath $OutFile -Value $pick -NoNewline -Encoding ascii
|
||||
Write-Host ""
|
||||
Write-Host " Selected: $pick"
|
||||
Start-Sleep -Seconds 1
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
'Escape' { exit 1 }
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,8 @@ exit /b 0
|
||||
|
||||
:flash_done
|
||||
echo BIOS update complete.
|
||||
set "BIOS_STATUS=%SYSMODEL% updated %BIOSVER% -^> %TARGETVER%"
|
||||
set "BIOS_STATUS=%SYSMODEL% updated %BIOSVER% to %TARGETVER%"
|
||||
echo flash_done %SYSMODEL% %BIOSVER% to %TARGETVER%> X:\bios-fired.flag
|
||||
exit /b 0
|
||||
|
||||
:staged
|
||||
@@ -121,7 +122,8 @@ echo It will flash during POST after the
|
||||
echo post-imaging reboot.
|
||||
echo ========================================
|
||||
echo.
|
||||
set "BIOS_STATUS=%SYSMODEL% STAGED %BIOSVER% -^> %TARGETVER% (flashes on reboot)"
|
||||
set "BIOS_STATUS=%SYSMODEL% STAGED %BIOSVER% to %TARGETVER% (flashes on reboot)"
|
||||
echo staged %SYSMODEL% %BIOSVER% to %TARGETVER%> X:\bios-fired.flag
|
||||
exit /b 0
|
||||
|
||||
:compare_versions
|
||||
|
||||
181
playbook/shopfloor-setup/Fetch-StagingPayload.ps1
Normal file
@@ -0,0 +1,181 @@
|
||||
# Fetch-StagingPayload.ps1 - post-boot bulk staging fetch (first-logon).
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# WinPE used to stage the whole shopfloor-setup tree + preinstall bundle to
|
||||
# the target disk DURING the WinPE phase. But WinPE maps the enrollment share
|
||||
# (Y:) early, then idles for many minutes while the full Windows image applies.
|
||||
# Samba's `deadtime` drops idle sessions, so by the time WinPE reached the
|
||||
# copies the Y: mount was dead and most copies failed (symptom: a bay with
|
||||
# only site-config.json staged, then nothing). Doing the bulk copy here - at
|
||||
# first logon, in full Windows, on a FRESH share mount with no prior idle -
|
||||
# sidesteps that entirely.
|
||||
#
|
||||
# WHEN IT RUNS
|
||||
# The unattend FirstLogonCommands runs this BEFORE the PowerShell 7 MSI install
|
||||
# (which needs C:\PreInstall\installers\powershell7\) and before
|
||||
# Run-ShopfloorSetup.ps1 (which needs C:\Enrollment\shopfloor-setup\). So this
|
||||
# must populate both trees before those steps fire.
|
||||
#
|
||||
# WHAT IT FETCHES (generic bulk - Phase 1)
|
||||
# \\<server>\enrollment\shopfloor-setup\Run-ShopfloorSetup.ps1 -> C:\Enrollment\
|
||||
# \\<server>\enrollment\shopfloor-setup\{backup_lockdown.bat,Shopfloor,common,
|
||||
# _ntlars-backups,gea-shopfloor-<pctype>} -> C:\Enrollment\shopfloor-setup\
|
||||
# \\<server>\enrollment\pre-install\{preinstall.json,installers,udc-backups}
|
||||
# -> C:\PreInstall\
|
||||
# (Heavy per-type payloads - CMM/Keyence/WaxTrace - are still staged in WinPE
|
||||
# for now; Phase 2 moves those here too.)
|
||||
#
|
||||
# LOGGING
|
||||
# Verbose transcript + a per-item table to C:\Logs\Fetch\. Every robocopy logs
|
||||
# its exit code, file/dir counts, byte total, and elapsed time, so a failed
|
||||
# fetch is fully diagnosable (unlike the old opaque WinPE staging).
|
||||
#
|
||||
# Always exits 0 - a fetch failure must not abort the FirstLogonCommands chain;
|
||||
# the log carries the truth and Run-ShopfloorSetup surfaces missing pieces.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# --- Logging setup ---
|
||||
$logDir = 'C:\Logs\Fetch'
|
||||
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
|
||||
$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||
$logFile = Join-Path $logDir "fetch-staging-$stamp.log"
|
||||
try { Start-Transcript -Path $logFile -Append -Force | Out-Null } catch {}
|
||||
|
||||
function Log {
|
||||
param([string]$Message, [string]$Level = 'INFO')
|
||||
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
Write-Host "[$ts] [$Level] $Message"
|
||||
}
|
||||
|
||||
Log "================================================================"
|
||||
Log "=== Fetch-StagingPayload start (PID $PID) ==="
|
||||
Log "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
Log "Host: $env:COMPUTERNAME"
|
||||
Log "================================================================"
|
||||
|
||||
# --- Resolve the share source + creds (written by startnet to fetch-source.txt;
|
||||
# falls back to the historical defaults if absent) ---
|
||||
$shareUnc = '\\172.16.9.1\enrollment'
|
||||
$shareUser = 'pxe-upload'
|
||||
$sharePass = 'pxe'
|
||||
$srcFile = 'C:\Enrollment\fetch-source.txt'
|
||||
if (Test-Path -LiteralPath $srcFile) {
|
||||
# Format: line1=UNC, line2=user, line3=pass
|
||||
$lines = @(Get-Content -LiteralPath $srcFile -ErrorAction SilentlyContinue)
|
||||
if ($lines.Count -ge 1 -and $lines[0].Trim()) { $shareUnc = $lines[0].Trim() }
|
||||
if ($lines.Count -ge 2 -and $lines[1].Trim()) { $shareUser = $lines[1].Trim() }
|
||||
if ($lines.Count -ge 3 -and $lines[2].Trim()) { $sharePass = $lines[2].Trim() }
|
||||
Log "fetch-source.txt: UNC=$shareUnc user=$shareUser"
|
||||
} else {
|
||||
Log "fetch-source.txt absent - using defaults: UNC=$shareUnc user=$shareUser"
|
||||
}
|
||||
|
||||
# --- pc-type (drives which gea-shopfloor-<type> dir to fetch) ---
|
||||
$pcType = ''
|
||||
if (Test-Path -LiteralPath 'C:\Enrollment\pc-type.txt') {
|
||||
$pcType = (Get-Content -LiteralPath 'C:\Enrollment\pc-type.txt' -First 1 -EA 0).Trim()
|
||||
}
|
||||
Log "PC type: $(if ($pcType) { $pcType } else { '(none)' })"
|
||||
|
||||
# --- Status push (best-effort) ---
|
||||
$pxeStatusLib = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Send-PxeStatus.ps1'
|
||||
# (lib not fetched yet on first run; ignore if absent)
|
||||
if (Test-Path $pxeStatusLib) { try { . $pxeStatusLib; Send-PxeStatus -Stage 'Fetch-StagingPayload: starting' -StageIndex 1 -StageTotal 8 } catch {} }
|
||||
|
||||
# --- Mount the share fresh (use Z:; retry to ride out a brief blip) ---
|
||||
$drive = 'Z:'
|
||||
function Mount-Share {
|
||||
# Pre-clear any stale Z: mapping. Wrap in cmd.exe (output to nul INSIDE cmd)
|
||||
# so net.exe's "network connection could not be found" stderr - emitted when
|
||||
# Z: is not mapped (the normal first-attempt case) - never reaches PowerShell
|
||||
# as a NativeCommandError. PS 2>$null does not reliably suppress that.
|
||||
cmd /c "net use $drive /delete /y >nul 2>&1"
|
||||
$r = & net use $drive $shareUnc /user:$shareUser $sharePass /persistent:no 2>&1
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
}
|
||||
$mounted = $false
|
||||
for ($attempt = 1; $attempt -le 5; $attempt++) {
|
||||
Log "Mounting $shareUnc as $drive (attempt $attempt/5)..."
|
||||
if (Mount-Share) { $mounted = $true; Log "Mounted OK"; break }
|
||||
Log "Mount failed (exit $LASTEXITCODE) - waiting 10s" 'WARN'
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
if (-not $mounted) {
|
||||
Log "Could not mount $shareUnc after 5 attempts - ABORTING fetch. Bay will be under-provisioned; re-run this script once the share is reachable." 'ERROR'
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Fetch helper: robocopy one item, log exit + counts + timing ---
|
||||
$results = @()
|
||||
function Fetch-Item {
|
||||
param(
|
||||
[string]$Label,
|
||||
[string]$SrcDir, # under $drive
|
||||
[string]$DstDir,
|
||||
[string[]]$Files, # named files for a flat copy; empty = whole-dir /E
|
||||
[switch]$Recurse # /E whole directory
|
||||
)
|
||||
$src = Join-Path $drive $SrcDir
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
Log "[SKIP] $Label - source not on share: $src" 'WARN'
|
||||
$script:results += [pscustomobject]@{ Item=$Label; Exit='n/a'; Result='SOURCE-MISSING' }
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $DstDir)) { New-Item -ItemType Directory -Path $DstDir -Force | Out-Null }
|
||||
$args = @($src, $DstDir)
|
||||
if ($Recurse) { $args += '/E' } else { $args += $Files }
|
||||
$args += @('/R:2','/W:3','/NFL','/NDL','/NP')
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
Log "[COPY] $Label : robocopy $src -> $DstDir $(if ($Recurse){'/E'}else{$Files -join ','})"
|
||||
$out = & robocopy @args 2>&1
|
||||
$rc = $LASTEXITCODE
|
||||
$sw.Stop()
|
||||
# robocopy 0-7 = success, 8+ = failure
|
||||
$ok = ($rc -lt 8)
|
||||
# pull the summary counts robocopy prints
|
||||
$summary = ($out | Select-String -Pattern 'Files :|Dirs :|Bytes :' ) -join ' | '
|
||||
Log "[$(if($ok){'OK'}else{'FAIL'})] $Label exit=$rc time=$([math]::Round($sw.Elapsed.TotalSeconds,1))s $summary"
|
||||
$script:results += [pscustomobject]@{ Item=$Label; Exit=$rc; Result=$(if($ok){'OK'}else{'FAIL'}) }
|
||||
}
|
||||
|
||||
# --- Generic bulk fetch ---
|
||||
$ENR = 'C:\Enrollment'
|
||||
$SFD = 'C:\Enrollment\shopfloor-setup'
|
||||
$PIN = 'C:\PreInstall'
|
||||
|
||||
Fetch-Item -Label 'Run-ShopfloorSetup.ps1' -SrcDir 'shopfloor-setup' -DstDir $ENR -Files @('Run-ShopfloorSetup.ps1')
|
||||
# Verify-And-Heal-Staging runs as its own unattend step (right after this Fetch,
|
||||
# before the production-network switch) to re-pull anything that did not arrive -
|
||||
# including the heavy CMM payload Fetch does not carry. Pull the small script
|
||||
# itself here so it is on disk for that step.
|
||||
Fetch-Item -Label 'Verify-And-Heal-Staging.ps1' -SrcDir 'shopfloor-setup' -DstDir $ENR -Files @('Verify-And-Heal-Staging.ps1')
|
||||
Fetch-Item -Label 'backup_lockdown.bat' -SrcDir 'shopfloor-setup' -DstDir $SFD -Files @('backup_lockdown.bat')
|
||||
Fetch-Item -Label 'Shopfloor baseline' -SrcDir 'shopfloor-setup\Shopfloor' -DstDir (Join-Path $SFD 'Shopfloor') -Recurse
|
||||
Fetch-Item -Label 'common' -SrcDir 'shopfloor-setup\common' -DstDir (Join-Path $SFD 'common') -Recurse
|
||||
Fetch-Item -Label '_ntlars-backups' -SrcDir 'shopfloor-setup\_ntlars-backups' -DstDir (Join-Path $SFD '_ntlars-backups') -Recurse
|
||||
if ($pcType) {
|
||||
Fetch-Item -Label "type:$pcType" -SrcDir "shopfloor-setup\$pcType" -DstDir (Join-Path $SFD $pcType) -Recurse
|
||||
}
|
||||
# preinstall bundle
|
||||
Fetch-Item -Label 'preinstall.json' -SrcDir 'pre-install' -DstDir $PIN -Files @('preinstall.json')
|
||||
Fetch-Item -Label 'preinstall installers' -SrcDir 'pre-install\installers' -DstDir (Join-Path $PIN 'installers') -Recurse
|
||||
Fetch-Item -Label 'udc-backups' -SrcDir 'pre-install\udc-backups' -DstDir (Join-Path $PIN 'udc-backups') -Recurse
|
||||
|
||||
# --- Unmount ---
|
||||
cmd /c "net use $drive /delete /y >nul 2>&1"
|
||||
|
||||
# --- Summary table ---
|
||||
Log "================================================================"
|
||||
Log "FETCH SUMMARY:"
|
||||
foreach ($r in $results) { Log (" {0,-28} exit={1,-4} {2}" -f $r.Item, $r.Exit, $r.Result) }
|
||||
$failed = @($results | Where-Object { $_.Result -eq 'FAIL' })
|
||||
if ($failed.Count -gt 0) {
|
||||
Log "$($failed.Count) item(s) FAILED: $(( $failed | ForEach-Object { $_.Item }) -join ', ')" 'ERROR'
|
||||
} else {
|
||||
Log "All fetched items OK." 'INFO'
|
||||
}
|
||||
Log "=== Fetch-StagingPayload complete ==="
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -27,7 +27,7 @@ Write-Host "================================================================"
|
||||
Write-Host ""
|
||||
|
||||
# Imaging-progress reporter. Posts coarse stage updates to the PXE webapp
|
||||
# at http://10.9.100.1:9009/imaging/status so the operator can watch
|
||||
# at http://172.16.9.1:9009/imaging/status so the operator can watch
|
||||
# progress in a browser. Best-effort: failures never block imaging.
|
||||
$pxeStatusLib = Join-Path $PSScriptRoot 'shopfloor-setup\Shopfloor\lib\Send-PxeStatus.ps1'
|
||||
if (Test-Path $pxeStatusLib) {
|
||||
@@ -50,6 +50,35 @@ Report-Stage -Stage 'Run-ShopfloorSetup: starting' -Index 2
|
||||
# Cancel any pending reboot so it doesn't interrupt setup
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
# Self-resume: register this script as a RunOnce so a vendor-installer-
|
||||
# forced reboot mid-flight (FormTracePak Setup.exe, eDNC MSI, etc) auto-
|
||||
# resumes the chain after the next SupportUser auto-login. RunOnce is
|
||||
# single-shot - if we complete normally we remove this key at end of
|
||||
# script. If we're killed mid-flight by a forced reboot, the key
|
||||
# survives and fires after reboot.
|
||||
#
|
||||
# Idempotent design throughout this script: every step checks detection
|
||||
# before installing, so a forced-reboot re-entry just skips the already-
|
||||
# done work and continues from where it left off.
|
||||
#
|
||||
# Also top up AutoLogonCount so the SupportUser autologon budget
|
||||
# (LogonCount=7 from unattend XML) survives extra unplanned reboots.
|
||||
$selfResumeKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
|
||||
$selfResumeName = 'ResumeRunShopfloorSetup'
|
||||
$selfResumeCmd = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + $PSCommandPath + '"'
|
||||
try {
|
||||
Set-ItemProperty -Path $selfResumeKey -Name $selfResumeName -Value $selfResumeCmd -Type String -Force -ErrorAction Stop
|
||||
Write-Host "Self-resume RunOnce registered: will re-fire $PSCommandPath if interrupted"
|
||||
} catch {
|
||||
Write-Warning "Failed to register self-resume RunOnce: $_"
|
||||
}
|
||||
try {
|
||||
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name 'AutoLogonCount' -Value 10 -Type DWord -Force -ErrorAction Stop
|
||||
Write-Host "AutoLogonCount topped up to 10 (vendor-forced reboot resilience)"
|
||||
} catch {
|
||||
Write-Warning "Failed to top up AutoLogonCount: $_"
|
||||
}
|
||||
|
||||
# Wired NIC state handling moved to sync_intune (Monitor-IntuneProgress.ps1).
|
||||
# Previously this script prompted the tech to unplug the PXE cable and
|
||||
# then re-enabled wired adapters interactively - that blocked the whole
|
||||
@@ -102,6 +131,12 @@ $skipInBaseline = @(
|
||||
'06-OrganizeDesktop.ps1',
|
||||
'07-TaskbarLayout.ps1',
|
||||
'08-EdgeDefaultBrowser.ps1',
|
||||
# Machine number flow: split into two scripts registered as scheduled
|
||||
# tasks by Register-CheckMachineNumberTask.ps1. Prompt runs as the
|
||||
# logged-in user (GUI), Apply runs as SYSTEM (privileged writes).
|
||||
# Neither should run in baseline pass.
|
||||
'Prompt-MachineNumber.ps1',
|
||||
'Apply-MachineNumber.ps1',
|
||||
'Check-MachineNumber.ps1',
|
||||
'Configure-PC.ps1'
|
||||
)
|
||||
@@ -299,63 +334,10 @@ if (Test-Path -LiteralPath $monitorScript) {
|
||||
# These run on every logon regardless of PC type, mounting the SFLD share
|
||||
# for version-pinned app enforcement. Initial install already handled by
|
||||
# preinstall flow; enforcers only kick in when detection fails.
|
||||
# --- Re-enable wired NICs once lockdown completes (Phase 6) ---
|
||||
# migrate-to-wifi.ps1 disables wired NICs so the PPKG runs over WiFi.
|
||||
# Keep them disabled through the entire Intune sync + DSC + lockdown
|
||||
# chain so nothing interrupts the WiFi-based enrollment. Only re-enable
|
||||
# after lockdown lands (Autologon_Remediation.log confirms ShopFloor
|
||||
# autologon set). Monitor-IntuneProgress runs as Limited and can't call
|
||||
# Enable-NetAdapter (needs admin). This SYSTEM task fires at logon,
|
||||
# polls for lockdown completion, re-enables wired NICs, and self-deletes.
|
||||
$reEnableTask = 'GE Re-enable Wired NICs'
|
||||
try {
|
||||
$script = @'
|
||||
# Poll for the GE Report-IP Proactive Remediation log file. Its appearance
|
||||
# means the Report IP script has fired with WiFi-only IPs (because we
|
||||
# disabled wired post-PPKG) - which is the exact moment we want to bring
|
||||
# wired back up so Monitor-IntuneProgress can push idx=7 with the
|
||||
# DeviceId / QR code before the Intune-triggered LAPS-prompt reboot lands.
|
||||
# Extension is .LOG (not .txt) observed in field; match any extension.
|
||||
$ip = Get-ChildItem 'C:\Logs\GE_Report_IP_Address*' -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $ip) { exit 0 }
|
||||
|
||||
# Vendor-agnostic wired-NIC re-enable. NetAdapter "Name" varies wildly
|
||||
# ("Ethernet", "Ethernet 2", "Network", per-vendor names like "Realtek
|
||||
# Gaming GbE", "Intel(R) Ethernet Connection (10) I219-V") so filtering
|
||||
# by Name is unreliable. Filter by PhysicalMediaType instead, with a
|
||||
# keyword-negative guard for drivers that mis-report PhysicalMediaType.
|
||||
# Captures Realtek, Intel, Broadcom, Marvell, Aquantia, etc.
|
||||
Get-NetAdapter -Physical -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
$_.HardwareInterface -eq $true -and
|
||||
$_.PhysicalMediaType -ne 'Native 802.11' -and
|
||||
$_.PhysicalMediaType -ne 'Wireless WAN' -and
|
||||
$_.PhysicalMediaType -ne 'BlueTooth' -and
|
||||
$_.InterfaceDescription -notmatch '(?i)Wi-?Fi|Wireless|WLAN|802\.11|Bluetooth'
|
||||
} |
|
||||
Enable-NetAdapter -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName 'GE Re-enable Wired NICs' -Confirm:$false -ErrorAction SilentlyContinue
|
||||
'@
|
||||
$scriptPath = 'C:\Program Files\GE\ReEnableNIC.ps1'
|
||||
if (-not (Test-Path 'C:\Program Files\GE')) {
|
||||
New-Item -Path 'C:\Program Files\GE' -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
Set-Content -Path $scriptPath -Value $script -Force
|
||||
|
||||
$reEnableAction = New-ScheduledTaskAction -Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
|
||||
$reEnableTrigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
$reEnableTrigger.Repetition = (New-ScheduledTaskTrigger -Once -At (Get-Date) `
|
||||
-RepetitionInterval (New-TimeSpan -Minutes 5)).Repetition
|
||||
$reEnablePrincipal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$reEnableSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 2)
|
||||
Register-ScheduledTask -TaskName $reEnableTask -Action $reEnableAction -Trigger $reEnableTrigger `
|
||||
-Principal $reEnablePrincipal -Settings $reEnableSettings -Force -ErrorAction Stop | Out-Null
|
||||
Write-Host "Registered '$reEnableTask' task (waits for SFLD creds, then re-enables wired NICs)."
|
||||
} catch {
|
||||
Write-Warning "Failed to register NIC re-enable task: $_"
|
||||
}
|
||||
# Wired-disable / re-enable dance retired after PXE LAN renumber to
|
||||
# 172.16.9.0/24. GE Report IP filters Get-NetIPAddress on StartsWith("10.")
|
||||
# so PXE LAN addresses are no longer caught - wired NIC can stay up
|
||||
# through the whole imaging chain without leaking to the GE webhook.
|
||||
|
||||
$commonSetupDir = Join-Path $setupDir 'common'
|
||||
|
||||
@@ -413,6 +395,26 @@ if ($noEnforceTypes -contains $pcType) {
|
||||
Write-Host "Register-MapSfldShare.ps1 not found (optional) - skipping"
|
||||
}
|
||||
|
||||
# --- Check Machine Number logon prompt ---
|
||||
# Auto-register the "Check Machine Number" scheduled task. Bays imaged with
|
||||
# the 9999 placeholder will prompt the first ShopFloor end-user logon to
|
||||
# enter the real machine number; on success Update-MachineNumber.ps1 pulls
|
||||
# the per-machine NTLARS .reg + UDC settings JSON + UDC data backup from
|
||||
# SFLD and the task self-unregisters. Self-disables once the number is
|
||||
# real, so safe to always register here.
|
||||
# Skipped for self-contained types (Display) that have no machine number.
|
||||
$registerCheckMN = Join-Path $setupDir 'Shopfloor\Register-CheckMachineNumberTask.ps1'
|
||||
if ($noEnforceTypes -contains $pcType) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Skipping Check Machine Number task ($pcType has no machine number) ==="
|
||||
} elseif (Test-Path -LiteralPath $registerCheckMN) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Registering Check Machine Number logon task ==="
|
||||
try { & $registerCheckMN } catch { Write-Warning "Check-MachineNumber registration failed: $_" }
|
||||
} else {
|
||||
Write-Host "Register-CheckMachineNumberTask.ps1 not found (optional) - skipping"
|
||||
}
|
||||
|
||||
# --- Run enrollment (PPKG install) ---
|
||||
# Enrollment is the LAST thing we do. Install-ProvisioningPackage triggers
|
||||
# an immediate reboot -- everything after this call is unlikely to execute.
|
||||
@@ -474,31 +476,19 @@ if (Test-Path -LiteralPath $enrollScript) {
|
||||
Write-Host "=== Running enrollment (PPKG install) ==="
|
||||
Write-Host "NOTE: PPKG schedules a near-immediate reboot. We will cancel"
|
||||
Write-Host " it and hand off to Monitor-IntuneProgress -PostPpkg, which"
|
||||
Write-Host " runs a 60s settle (giving MDM time to push baseline"
|
||||
Write-Host " runs a 120s settle (giving MDM time to push baseline"
|
||||
Write-Host " policy) and then performs a clean reboot."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
& $enrollScript
|
||||
|
||||
# idx=6 push happens BEFORE wired disable so the dashboard captures
|
||||
# the handoff stage. Disable-WiredNics comes right after - kills wired
|
||||
# before PostPpkg settle's Schedule #3 hammer hits Intune endpoints,
|
||||
# before the PPKG-driven reboot, and before IME starts firing the
|
||||
# Report IP script. Goal: GE's Report IP webhook only ever sees the
|
||||
# corp-WiFi IP, never PXE LAN (10.9.100.x). Monitor-IntuneProgress
|
||||
# re-enables wired once C:\Logs\GE_Report_IP_Address*.txt shows up
|
||||
# (proof of clean Report IP fire) and then pushes idx=7.
|
||||
Write-Host ""
|
||||
Report-Stage -Stage 'Run-ShopfloorSetup: handoff to Monitor-IntuneProgress' -Index 6
|
||||
|
||||
$disableWiredScript = Join-Path $PSScriptRoot 'shopfloor-setup\Shopfloor\lib\Disable-WiredNics.ps1'
|
||||
if (Test-Path -LiteralPath $disableWiredScript) {
|
||||
try { & $disableWiredScript } catch { Write-Warning "Disable-WiredNics threw: $_" }
|
||||
} else {
|
||||
Write-Warning "Disable-WiredNics.ps1 not found at $disableWiredScript - wired stays up (Report IP leak risk)"
|
||||
}
|
||||
|
||||
Write-Host "=== Handing off to Monitor-IntuneProgress -PostPpkg ==="
|
||||
cmd /c "shutdown /a 2>nul" | Out-Null
|
||||
# Made it past all the reboot-prone vendor installers. Clear the
|
||||
# self-resume RunOnce so a normal completion + reboot does not re-fire
|
||||
# this script post-PPKG (PPKG install owns the reboot chain from here).
|
||||
try { Remove-ItemProperty -Path $selfResumeKey -Name $selfResumeName -ErrorAction SilentlyContinue } catch {}
|
||||
$monitor = Join-Path $setupDir 'Shopfloor\lib\Monitor-IntuneProgress.ps1'
|
||||
if (Test-Path -LiteralPath $monitor) {
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitor -PostPpkg
|
||||
@@ -512,6 +502,7 @@ if (Test-Path -LiteralPath $enrollScript) {
|
||||
Write-Host "================================================================"
|
||||
Write-Host "=== Run-ShopfloorSetup.ps1 complete $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
|
||||
Write-Host "================================================================"
|
||||
try { Remove-ItemProperty -Path $selfResumeKey -Name $selfResumeName -ErrorAction SilentlyContinue } catch {}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
Write-Host "Rebooting in 10 seconds..."
|
||||
shutdown /r /t 10
|
||||
|
||||
@@ -165,8 +165,12 @@ if (Test-Path -LiteralPath $machineNumFile) {
|
||||
# before UDC_Setup.exe runs means the installer's File.Copy (overwrite:true)
|
||||
# would overwrite it IF the share were reachable, but since it isn't, our
|
||||
# pre-staged file survives and UDC launches with correct settings.
|
||||
# UDC payload (settings backups + webserver settings) lives only in the
|
||||
# collections per-pc-type dir - UDC is the "C" of "collections". On nocoll
|
||||
# bays the dir doesn't exist; Test-Path skips silently.
|
||||
$udcCollDir = Join-Path (Split-Path $PSScriptRoot -Parent) 'gea-shopfloor-collections'
|
||||
if ($machineNum -and $machineNum -ne '9999') {
|
||||
$udcBackupDir = 'C:\Enrollment\shopfloor-setup\Standard\udc-backups'
|
||||
$udcBackupDir = Join-Path $udcCollDir 'udc-backups'
|
||||
$udcBackup = Join-Path $udcBackupDir "udc_settings_$machineNum.json"
|
||||
$udcTarget = 'C:\ProgramData\UDC\udc_settings.json'
|
||||
if (Test-Path -LiteralPath $udcBackup) {
|
||||
@@ -176,11 +180,11 @@ if ($machineNum -and $machineNum -ne '9999') {
|
||||
Copy-Item -Path $udcBackup -Destination $udcTarget -Force
|
||||
Write-PreInstallLog "Pre-staged UDC settings from $udcBackup -> $udcTarget"
|
||||
} else {
|
||||
Write-PreInstallLog "No UDC settings backup for machine $machineNum in $udcBackupDir"
|
||||
Write-PreInstallLog "No UDC settings backup for machine $machineNum at $udcBackup (skipping - normal for nocoll bays)"
|
||||
}
|
||||
}
|
||||
|
||||
$udcWebSrc = 'C:\Enrollment\shopfloor-setup\Standard\udc_webserver_settings.json'
|
||||
$udcWebSrc = Join-Path $udcCollDir 'udc_webserver_settings.json'
|
||||
$udcWebDst = 'C:\ProgramData\UDC\udc_webserver_settings.json'
|
||||
if (Test-Path -LiteralPath $udcWebSrc) {
|
||||
if (-not (Test-Path 'C:\ProgramData\UDC')) {
|
||||
@@ -189,7 +193,7 @@ if (Test-Path -LiteralPath $udcWebSrc) {
|
||||
Copy-Item -Path $udcWebSrc -Destination $udcWebDst -Force
|
||||
Write-PreInstallLog "Pre-staged UDC webserver settings from $udcWebSrc -> $udcWebDst"
|
||||
} else {
|
||||
Write-PreInstallLog "No UDC webserver settings file at $udcWebSrc" "WARN"
|
||||
Write-PreInstallLog "No UDC webserver settings file at $udcWebSrc (skipping - normal for nocoll bays)"
|
||||
}
|
||||
|
||||
# --- Suppress Windows Defender Firewall "Allow access" prompts globally for
|
||||
@@ -317,7 +321,8 @@ foreach ($app in $config.Applications) {
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat')
|
||||
@('Heattreat', 'gea-shopfloor-heattreat'),
|
||||
@('PartMarker', 'gea-shopfloor-partmarker')
|
||||
)
|
||||
$myNames = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($n in @($pcType, $pcProfileKey) | Where-Object { $_ }) {
|
||||
@@ -326,8 +331,19 @@ foreach ($app in $config.Applications) {
|
||||
if ($g -icontains $n) { foreach ($x in $g) { [void]$myNames.Add($x) } }
|
||||
}
|
||||
}
|
||||
# PCTypesStrict=true bypasses the alias-expansion matcher and requires
|
||||
# the actual pcType (or composite pcProfileKey) to literally equal one
|
||||
# of the allowedTypes entries. Used by UDC because the alias graph
|
||||
# transitively connects gea-shopfloor-collections <-> nocollections via
|
||||
# the legacy 'Standard' group, which would otherwise cause UDC to install
|
||||
# on nocoll bays even with PCTypes=['gea-shopfloor-collections'].
|
||||
$matchesType = ($allowedTypes -contains '*')
|
||||
if (-not $matchesType) {
|
||||
if ($app.PCTypesStrict) {
|
||||
foreach ($t in $allowedTypes) {
|
||||
if (($pcType -ieq $t) -or ($pcProfileKey -ieq $t)) { $matchesType = $true; break }
|
||||
}
|
||||
} else {
|
||||
foreach ($t in $allowedTypes) {
|
||||
if ($myNames.Contains($t)) { $matchesType = $true; break }
|
||||
foreach ($g in $aliasGroups) {
|
||||
@@ -338,6 +354,7 @@ foreach ($app in $config.Applications) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $matchesType) {
|
||||
Write-PreInstallLog " PCTypes filter excludes '$pcProfileKey' (allowed: $($allowedTypes -join ', ')) - skipping"
|
||||
$skipped++
|
||||
|
||||
@@ -132,8 +132,12 @@ function Invoke-DesktopSweep {
|
||||
Name = @(
|
||||
'^UDC',
|
||||
'eDNC', '\bDNC\b', 'DncMain', 'GE DNC', 'NTLARS',
|
||||
'Host\s*Explorer', 'ShopFloor', 'TN3270', 'TN5250', 'HE\s*3270', 'HE\s*5250',
|
||||
'OpenText',
|
||||
'Host\s*Explorer', 'TN3270', 'TN5250', 'HE\s*3270', 'HE\s*5250',
|
||||
# OpenText / 'WJ Shopfloor' / 'ShopFloor' shortcuts left on
|
||||
# the desktop intentionally. The actual filename varies by
|
||||
# OpenText profile (e.g. 'WJ Shopfloor OpenText.lnk') so the
|
||||
# taskbar pin path mismatch silently skipped these. Leaving
|
||||
# them at the public desktop top level instead.
|
||||
'Defect[_\s-]?Tracker',
|
||||
'MarkZebra', 'Zebra',
|
||||
'PC-?DMIS',
|
||||
|
||||
@@ -213,7 +213,7 @@ if ($null -ne $cfgTabs -and $cfgTabs.Count -gt 0) {
|
||||
$plantApps = Resolve-StartupUrl -BaseName 'Plant Apps' -Fallback 'https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications'
|
||||
if ($plantApps) { $startupTabs += $plantApps }
|
||||
|
||||
$shopFloorHome = Resolve-StartupUrl -BaseName 'WJ Shop Floor Homepage' -Fallback 'http://tsgwp00524.logon.ds.ge.com/'
|
||||
$shopFloorHome = Resolve-StartupUrl -BaseName 'WJ Shop Floor Homepage' -Fallback 'https://tsgwp00525.wjs.geaerospace.net'
|
||||
if ($shopFloorHome) { $startupTabs += $shopFloorHome }
|
||||
|
||||
$dashboard = Resolve-StartupUrl -BaseName 'Shopfloor Dashboard' -Fallback 'https://tsgwp00525.wjs.geaerospace.net/shopdb/shopfloor-dashboard/'
|
||||
|
||||
115
playbook/shopfloor-setup/Shopfloor/Apply-MachineNumber.ps1
Normal file
@@ -0,0 +1,115 @@
|
||||
# Apply-MachineNumber.ps1 - SYSTEM-context worker for the two-task machine
|
||||
# number flow. Triggered on-demand by Prompt-MachineNumber.ps1 via
|
||||
# `schtasks /run /tn "WT-Apply-MachineNumber"`. Reads the requested number
|
||||
# from a file the GUI script wrote, invokes Update-MachineNumber as SYSTEM
|
||||
# (full HKLM + ProgramData access), writes a result JSON for the GUI to
|
||||
# display, then cleans up.
|
||||
#
|
||||
# Why SYSTEM:
|
||||
# The eDNC reg key (HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\
|
||||
# General\MachineNo) and UDC settings JSON live in HKLM + ProgramData
|
||||
# respectively - both require admin to write. The OLD design granted
|
||||
# BUILTIN\Users SetValue + Modify via 02-MachineNumberACLs.ps1, but that
|
||||
# was fragile (timing race with eDNC install, ACL silently failed on
|
||||
# some bays) AND a security hole (any user could mess with the machine
|
||||
# identity). Two-task design: GUI gathers input as logged-in user, SYSTEM
|
||||
# does the actual write.
|
||||
#
|
||||
# Files:
|
||||
# C:\Logs\SFLD\machine-number-request.txt - input, single line, new number
|
||||
# C:\Logs\SFLD\machine-number-result.json - output, status fields for GUI
|
||||
# C:\Logs\SFLD\Apply-MachineNumber.log - transcript
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path -LiteralPath $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcript = Join-Path $logDir 'Apply-MachineNumber.log'
|
||||
try { Start-Transcript -Path $transcript -Append -Force | Out-Null } catch {}
|
||||
|
||||
$requestFile = Join-Path $logDir 'machine-number-request.txt'
|
||||
$resultFile = Join-Path $logDir 'machine-number-result.json'
|
||||
|
||||
function Write-Result {
|
||||
param([hashtable]$Body)
|
||||
$Body | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $resultFile -Encoding ascii -Force
|
||||
}
|
||||
|
||||
Write-Host "Apply-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $requestFile)) {
|
||||
Write-Warning "No request file at $requestFile - nothing to apply."
|
||||
Write-Result @{ Status = 'NoRequest'; Errors = @("request file missing: $requestFile") }
|
||||
exit 0
|
||||
}
|
||||
$newNumber = (Get-Content -LiteralPath $requestFile -First 1 -ErrorAction Stop).Trim()
|
||||
Write-Host "Requested new machine number: $newNumber"
|
||||
|
||||
if ($newNumber -notmatch '^\d+$') {
|
||||
Write-Warning "Request is not digits-only: '$newNumber'"
|
||||
Write-Result @{ Status = 'BadInput'; Requested = $newNumber; Errors = @("Not digits only: '$newNumber'") }
|
||||
Remove-Item -LiteralPath $requestFile -Force -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Dot-source the shared helper. Update-MachineNumber.ps1 now has
|
||||
# -ErrorAction Stop on the writes so failures actually throw.
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
$mnResult = Update-MachineNumber -NewNumber $newNumber -Site $site
|
||||
|
||||
$resultBody = @{
|
||||
Status = if ($mnResult.Errors.Count -eq 0) { 'OK' } else { 'PartialErrors' }
|
||||
Requested = $newNumber
|
||||
Site = $site
|
||||
UdcUpdated = [bool]$mnResult.UdcUpdated
|
||||
EdncUpdated = [bool]$mnResult.EdncUpdated
|
||||
OldUdc = $mnResult.OldUdc
|
||||
OldEdnc = $mnResult.OldEdnc
|
||||
UdcSettingsRestored = [bool]$mnResult.UdcSettingsRestored
|
||||
UdcRestored = [bool]$mnResult.UdcRestored
|
||||
MTConnectUpdated = $mnResult.MTConnectUpdated
|
||||
MachineNumberTxtUpdated = [bool]$mnResult.MachineNumberTxtUpdated
|
||||
Errors = $mnResult.Errors
|
||||
AppliedAt = (Get-Date -Format 'o')
|
||||
AppliedAs = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
}
|
||||
Write-Result -Body $resultBody
|
||||
|
||||
Write-Host "Update-MachineNumber result:"
|
||||
Write-Host " UdcUpdated = $($mnResult.UdcUpdated)"
|
||||
Write-Host " EdncUpdated = $($mnResult.EdncUpdated)"
|
||||
Write-Host " Errors = $($mnResult.Errors.Count)"
|
||||
if ($mnResult.Errors) { $mnResult.Errors | ForEach-Object { Write-Host " FAILED: $_" } }
|
||||
|
||||
Remove-Item -LiteralPath $requestFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# On clean success, also unregister the Prompt logon task. Prompt itself
|
||||
# tries to self-unregister but it runs as a Limited user (BUILTIN\Users)
|
||||
# and silently fails on Unregister-ScheduledTask (no delete right on a
|
||||
# SYSTEM-registered task). We're running as SYSTEM here, so we can.
|
||||
# Idempotent if Prompt already unregistered itself somehow.
|
||||
if ($mnResult.Errors.Count -eq 0 -and $mnResult.EdncUpdated) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName 'Prompt Machine Number' -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName 'Prompt Machine Number' -Confirm:$false -ErrorAction Stop
|
||||
Write-Host "Unregistered 'Prompt Machine Number' task (SYSTEM cleanup)."
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Could not unregister 'Prompt Machine Number': $_"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Apply-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
} catch {
|
||||
Write-Warning "Apply threw: $_"
|
||||
Write-Result @{ Status = 'Exception'; Errors = @("$_") }
|
||||
} finally {
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
}
|
||||
@@ -93,13 +93,24 @@ if ($mnResult.EdncUpdated) { $results += "eDNC updated to $new" }
|
||||
foreach ($err in $mnResult.Errors) { $results += $err -replace '^', 'FAILED: ' }
|
||||
|
||||
# --- Show result ---
|
||||
$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe."
|
||||
$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe.`n`nFull log: C:\Logs\SFLD\Check-MachineNumber.log"
|
||||
# Force the MessageBox to topmost + take focus so it isn't hidden behind
|
||||
# other windows. Without this, the result dialog can render off-screen or
|
||||
# behind the FormTracePak / DNC windows and the tech misses it.
|
||||
$tmpForm = New-Object System.Windows.Forms.Form
|
||||
$tmpForm.TopMost = $true
|
||||
$tmpForm.WindowState = 'Minimized'
|
||||
$tmpForm.ShowInTaskbar = $false
|
||||
$tmpForm.Opacity = 0
|
||||
$tmpForm.Show()
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
$summary,
|
||||
"Machine Number Updated",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Information
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
|
||||
# --- Unregister task on success ---
|
||||
Write-Host "Results: $($results -join '; ')"
|
||||
|
||||
@@ -285,8 +285,13 @@ if ($null -ne $cfgItems -and $cfgItems.Count -gt 0) {
|
||||
)
|
||||
}
|
||||
|
||||
# Machine-number logon task is item 6
|
||||
$machineNumTaskName = 'Check Machine Number'
|
||||
# Machine-number logon tasks (item 6 toggle controls both)
|
||||
# 2026-05-24: split into user-context Prompt + SYSTEM-context Apply.
|
||||
# 'Check Machine Number' is the legacy single-task name kept for
|
||||
# backward-detection on bays imaged before the split.
|
||||
$machineNumPromptTask = 'Prompt Machine Number'
|
||||
$machineNumApplyTask = 'Apply Machine Number'
|
||||
$machineNumLegacyTask = 'Check Machine Number'
|
||||
|
||||
# ============================================================================
|
||||
# Interactive UI
|
||||
@@ -354,8 +359,12 @@ foreach ($item in $items) {
|
||||
Write-Host " $($item.Num). $on $($item.Label) - $($item.Detail)$avail"
|
||||
}
|
||||
|
||||
# Item 6: machine number logon prompt
|
||||
$machineNumTaskExists = [bool](Get-ScheduledTask -TaskName $machineNumTaskName -ErrorAction SilentlyContinue)
|
||||
# Item 6: machine number logon prompt. "ON" if EITHER the new Prompt task OR
|
||||
# the legacy Check Machine Number task is registered.
|
||||
$machineNumTaskExists = [bool](
|
||||
(Get-ScheduledTask -TaskName $machineNumPromptTask -ErrorAction SilentlyContinue) -or
|
||||
(Get-ScheduledTask -TaskName $machineNumLegacyTask -ErrorAction SilentlyContinue)
|
||||
)
|
||||
$mnOn = if ($machineNumTaskExists) { '[ON]' } else { '[ ]' }
|
||||
Write-Host " 6. $mnOn Prompt standard user for machine number if 9999"
|
||||
|
||||
@@ -419,62 +428,47 @@ if ($selection) {
|
||||
# Process item 6: machine number logon task
|
||||
if ($selected -contains 6) {
|
||||
if ($machineNumTaskExists) {
|
||||
# Toggle OFF
|
||||
# Toggle OFF - remove Prompt + Apply (new design) AND the legacy
|
||||
# Check Machine Number task name (in case this bay was imaged
|
||||
# before the split and never re-imaged).
|
||||
$removed = @()
|
||||
foreach ($t in @($machineNumPromptTask, $machineNumApplyTask, $machineNumLegacyTask)) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $machineNumTaskName -Confirm:$false -ErrorAction Stop
|
||||
Write-Host " Machine number logon prompt: REMOVED" -ForegroundColor Yellow
|
||||
if (Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction Stop
|
||||
$removed += $t
|
||||
}
|
||||
} catch { Write-Warning " Failed to remove '$t': $_" }
|
||||
}
|
||||
if ($removed) {
|
||||
Write-Host " Machine number logon prompt: REMOVED ($($removed -join ', '))" -ForegroundColor Yellow
|
||||
}
|
||||
$machineNumTaskExists = $false
|
||||
} catch { Write-Warning " Failed to remove task: $_" }
|
||||
} else {
|
||||
# Toggle ON - register logon task
|
||||
# The task needs to run as the logged-in user (for GUI), but
|
||||
# writing to HKLM + ProgramData requires the ACLs we pre-grant
|
||||
# during imaging (see task 7 / ACL pre-grant script).
|
||||
# Defer task registration to the shared registrar so this code
|
||||
# path always matches the imaging-time path. Registrar installs
|
||||
# BOTH the user-context "Prompt Machine Number" task and the
|
||||
# SYSTEM-context "Apply Machine Number" task, sets the SDDL on
|
||||
# Apply so Limited users can schtasks /run it, and cleans up
|
||||
# any legacy "Check Machine Number" task name.
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$checkScript = Join-Path $scriptDir 'Check-MachineNumber.ps1'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $checkScript)) {
|
||||
# Fallback: check enrollment staging dir
|
||||
$checkScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\Check-MachineNumber.ps1'
|
||||
$registrar = Join-Path $scriptDir 'Register-CheckMachineNumberTask.ps1'
|
||||
if (-not (Test-Path -LiteralPath $registrar)) {
|
||||
$registrar = 'C:\Enrollment\shopfloor-setup\Shopfloor\Register-CheckMachineNumberTask.ps1'
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $checkScript) {
|
||||
if (Test-Path -LiteralPath $registrar) {
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$checkScript`""
|
||||
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
# Run as the logged-in user (needs GUI for InputBox), NOT
|
||||
# SYSTEM (SYSTEM can't show UI to the user's desktop).
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-GroupId 'S-1-5-32-545' `
|
||||
-RunLevel Limited
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
||||
|
||||
Register-ScheduledTask `
|
||||
-TaskName $machineNumTaskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Principal $principal `
|
||||
-Settings $settings `
|
||||
-Force `
|
||||
-ErrorAction Stop | Out-Null
|
||||
|
||||
& $registrar
|
||||
Write-Host " Machine number logon prompt: ENABLED" -ForegroundColor Green
|
||||
Write-Host " (will auto-disable after machine number is set)" -ForegroundColor DarkGray
|
||||
Write-Host " (Prompt user-task + Apply SYSTEM-task registered;" -ForegroundColor DarkGray
|
||||
Write-Host " will auto-disable after machine number is set)" -ForegroundColor DarkGray
|
||||
$machineNumTaskExists = $true
|
||||
} catch {
|
||||
Write-Warning " Failed to register task: $_"
|
||||
Write-Warning " Register-CheckMachineNumberTask failed: $_"
|
||||
}
|
||||
} else {
|
||||
Write-Warning " Check-MachineNumber.ps1 not found at $checkScript"
|
||||
Write-Warning " Register-CheckMachineNumberTask.ps1 not found at $registrar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
200
playbook/shopfloor-setup/Shopfloor/Prompt-MachineNumber.ps1
Normal file
@@ -0,0 +1,200 @@
|
||||
# Prompt-MachineNumber.ps1 - User-context GUI script for the two-task
|
||||
# machine number flow. Triggered AtLogOn for any BUILTIN\Users member.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Read current UDC + eDNC values (read-only - no privileges needed).
|
||||
# 2. If neither is 9999, unregister self and exit (this PC is set up).
|
||||
# 3. Show InputBox for new machine number.
|
||||
# 4. Write number to C:\Logs\SFLD\machine-number-request.txt.
|
||||
# 5. Trigger the SYSTEM-context Apply-MachineNumber task via
|
||||
# schtasks /run. SYSTEM has full HKLM + ProgramData access so the
|
||||
# actual write happens with proper privileges - the prompted user
|
||||
# never needs HKLM write rights (security improvement over the old
|
||||
# 02-MachineNumberACLs.ps1 ACL-grant hack).
|
||||
# 6. Poll for C:\Logs\SFLD\machine-number-result.json (30s timeout).
|
||||
# 7. Show result MessageBox. Unregister self on success.
|
||||
#
|
||||
# Why this script doesn't do the writes itself: GUI is required (InputBox),
|
||||
# but GUI requires user-context (SYSTEM can't render to user desktop on
|
||||
# modern Windows). The user-context dialog gathers input; the SYSTEM task
|
||||
# does privileged writes.
|
||||
|
||||
# --- Transcript ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path -LiteralPath $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcript = Join-Path $logDir 'Prompt-MachineNumber.log'
|
||||
try { Start-Transcript -Path $transcript -Append -Force | Out-Null } catch {}
|
||||
Write-Host "Prompt-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
Add-Type -AssemblyName Microsoft.VisualBasic
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
$taskName = 'Prompt Machine Number'
|
||||
$applyTaskName = 'Apply Machine Number'
|
||||
$requestFile = Join-Path $logDir 'machine-number-request.txt'
|
||||
$resultFile = Join-Path $logDir 'machine-number-result.json'
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
|
||||
# --- Read current values (read-only, no perms needed) ---
|
||||
$currentMN = Get-CurrentMachineNumber
|
||||
$currentUdc = $currentMN.Udc
|
||||
$currentEdnc = $currentMN.Ednc
|
||||
Write-Host "UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not found)' })"
|
||||
Write-Host "eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not found)' })"
|
||||
|
||||
if ($currentUdc -ne '9999' -and $currentEdnc -ne '9999') {
|
||||
Write-Host "Machine number is set (not 9999). Unregistering Prompt task and exiting."
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Placeholder 9999 detected - showing prompt."
|
||||
|
||||
# --- Show prompt ---
|
||||
$promptLines = @()
|
||||
$promptLines += "The machine number on this PC is still set to the"
|
||||
$promptLines += "placeholder value (9999). Please enter the correct"
|
||||
$promptLines += "machine number for this workstation."
|
||||
$promptLines += ""
|
||||
if ($currentUdc) { $promptLines += "Current UDC: $currentUdc" }
|
||||
if ($currentEdnc) { $promptLines += "Current eDNC: $currentEdnc" }
|
||||
$promptLines += ""
|
||||
$promptLines += "Enter the new Machine Number:"
|
||||
$prompt = $promptLines -join "`n"
|
||||
$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($new)) {
|
||||
Write-Host "User cancelled. Will prompt again next logon."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
$new = $new.Trim()
|
||||
|
||||
if ($new -notmatch '^\d+$') {
|
||||
Write-Host "Invalid input: '$new' (not digits only). Showing error and re-prompting next logon."
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
"Machine number must be digits only.`n`nYou entered: '$new'`n`nThe prompt will appear again at next logon.",
|
||||
"Invalid Machine Number",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
) | Out-Null
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Hand off to SYSTEM task ---
|
||||
# Clean any stale request / result files first so we read fresh ones.
|
||||
Remove-Item -LiteralPath $requestFile, $resultFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
try {
|
||||
Set-Content -LiteralPath $requestFile -Value $new -Encoding ascii -Force -ErrorAction Stop
|
||||
} catch {
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
"Could not write request file at $requestFile`n`n$_`n`nThe prompt will appear again at next logon.",
|
||||
"Machine Number Request Failed",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
) | Out-Null
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Wrote $requestFile with '$new'. Triggering SYSTEM apply task..."
|
||||
& schtasks.exe /run /tn $applyTaskName 2>&1 | ForEach-Object { Write-Host " schtasks: $_" }
|
||||
|
||||
# --- Wait for result ---
|
||||
$deadline = (Get-Date).AddSeconds(60)
|
||||
$result = $null
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path -LiteralPath $resultFile) {
|
||||
try {
|
||||
$result = Get-Content -LiteralPath $resultFile -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
break
|
||||
} catch { Start-Sleep -Milliseconds 200 }
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
# Make the result MessageBox topmost so it shows above the FormTracePak /
|
||||
# DNC windows and isn't missed.
|
||||
$tmpForm = New-Object System.Windows.Forms.Form
|
||||
$tmpForm.TopMost = $true
|
||||
$tmpForm.WindowState = 'Minimized'
|
||||
$tmpForm.ShowInTaskbar = $false
|
||||
$tmpForm.Opacity = 0
|
||||
$tmpForm.Show()
|
||||
|
||||
if (-not $result) {
|
||||
Write-Host "Timed out waiting for SYSTEM apply task to produce result file ($resultFile)."
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
"Timed out waiting for the SYSTEM update task to complete.`n`nCheck:`n C:\Logs\SFLD\Apply-MachineNumber.log`n C:\Logs\SFLD\Prompt-MachineNumber.log`n`nThe prompt will appear again at next logon.",
|
||||
"Machine Number Update Timed Out",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Warning
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build summary from result JSON
|
||||
$lines = @()
|
||||
$lines += "Requested: $($result.Requested)"
|
||||
$lines += ""
|
||||
if ($result.UdcUpdated) { $lines += "UDC updated to $($result.Requested)" } else { $lines += "UDC: not updated (UDC may not be installed)" }
|
||||
if ($result.EdncUpdated) { $lines += "eDNC updated to $($result.Requested)" } else { $lines += "eDNC: not updated" }
|
||||
if ($result.UdcSettingsRestored) { $lines += "UDC settings restored from SFLD" }
|
||||
if ($result.UdcRestored) { $lines += "UDC live data restored from SFLD" }
|
||||
if ($result.MachineNumberTxtUpdated) { $lines += "machine-number.txt updated" }
|
||||
if ($result.MTConnectUpdated -and $result.MTConnectUpdated.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "MTConnect Devices.xml updates:"
|
||||
$result.MTConnectUpdated | ForEach-Object { $lines += " - $_" }
|
||||
}
|
||||
if ($result.Errors -and $result.Errors.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "FAILURES:"
|
||||
$result.Errors | ForEach-Object { $lines += " - $_" }
|
||||
}
|
||||
$lines += ""
|
||||
$lines += "Status: $($result.Status)"
|
||||
$lines += "Logs: C:\Logs\SFLD\Apply-MachineNumber.log"
|
||||
$lines += " C:\Logs\SFLD\Prompt-MachineNumber.log"
|
||||
$lines += ""
|
||||
$lines += "To apply eDNC changes, restart any running DncMain.exe."
|
||||
$summary = $lines -join "`n"
|
||||
|
||||
$icon = if ($result.Status -eq 'OK') { [System.Windows.Forms.MessageBoxIcon]::Information } else { [System.Windows.Forms.MessageBoxIcon]::Warning }
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
$summary,
|
||||
"Machine Number Update Result",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
$icon
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
|
||||
# Clean up result file for the next round.
|
||||
Remove-Item -LiteralPath $resultFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Only unregister the Prompt task on full success (no errors AND eDNC
|
||||
# updated to the requested value). If anything failed, leave it registered
|
||||
# for next logon retry.
|
||||
if ($result.Status -eq 'OK' -and $result.EdncUpdated) {
|
||||
Write-Host "All updates succeeded. Unregistering Prompt task."
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||
} else {
|
||||
Write-Host "Some updates failed or skipped. Prompt task stays registered for next logon retry."
|
||||
}
|
||||
|
||||
Write-Host "Prompt-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -0,0 +1,149 @@
|
||||
# Register-CheckMachineNumberTask.ps1 - Register the two-task machine
|
||||
# number flow at imaging time:
|
||||
#
|
||||
# 1. "Prompt Machine Number" - AtLogOn, BUILTIN\Users, Limited.
|
||||
# Shows InputBox + writes new number to a request file, then triggers
|
||||
# the SYSTEM task via schtasks /run.
|
||||
#
|
||||
# 2. "Apply Machine Number" - on-demand only (no trigger), SYSTEM,
|
||||
# RunLevel Highest. Reads the request file, calls Update-MachineNumber
|
||||
# with full HKLM + ProgramData access, writes a result JSON, removes
|
||||
# the request file. No GUI - the Prompt task polls the result file
|
||||
# and displays the dialog.
|
||||
#
|
||||
# Replaces the old single-task design that ran as the logged-in user with
|
||||
# pre-granted BUILTIN\Users HKLM ACLs (02-MachineNumberACLs.ps1). That
|
||||
# approach was fragile (timing race with eDNC install, silent ACL skip)
|
||||
# and a security hole (any user could write to the machine-identity reg
|
||||
# key). With SYSTEM doing the actual writes, no ACL grants needed.
|
||||
#
|
||||
# Idempotent: safe to re-run. Existing tasks are overwritten.
|
||||
#
|
||||
# File kept named Register-CheckMachineNumberTask.ps1 (rather than
|
||||
# Register-MachineNumberTasks.ps1) so Run-ShopfloorSetup's existing
|
||||
# discovery doesn't need editing.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
||||
$logFile = Join-Path $logDir 'register-checkmn.log'
|
||||
|
||||
function Write-RegLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [INFO] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask start ==='
|
||||
|
||||
$promptTaskName = 'Prompt Machine Number'
|
||||
$applyTaskName = 'Apply Machine Number'
|
||||
$oldTaskName = 'Check Machine Number' # legacy, removed below
|
||||
|
||||
# Clean up the legacy single-task name from prior imaging cycles.
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $oldTaskName -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $oldTaskName -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Unregistered legacy task '$oldTaskName'"
|
||||
}
|
||||
} catch { Write-RegLog "Could not unregister legacy '$oldTaskName': $_" }
|
||||
|
||||
# Only arm the tasks if the bay was imaged with the 9999 placeholder. If
|
||||
# the tech entered a real machine number during PXE imaging it's already
|
||||
# in C:\Enrollment\machine-number.txt; no prompt needed on first logon.
|
||||
$mnFile = 'C:\Enrollment\machine-number.txt'
|
||||
$mnAtImaging = '9999'
|
||||
if (Test-Path -LiteralPath $mnFile) {
|
||||
$raw = (Get-Content -LiteralPath $mnFile -First 1 -ErrorAction SilentlyContinue)
|
||||
if ($raw) { $mnAtImaging = $raw.Trim() }
|
||||
}
|
||||
Write-RegLog "Imaging-time machine-number.txt = '$mnAtImaging'"
|
||||
if ($mnAtImaging -ne '9999') {
|
||||
Write-RegLog "Machine number is real ('$mnAtImaging' != 9999). Not registering tasks."
|
||||
foreach ($t in @($promptTaskName, $applyTaskName)) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Unregistered stale task '$t'"
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask end (no-op) ==='
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Resolve script paths. Prefer the staged shopfloor-setup tree on C:
|
||||
# (where Run-ShopfloorSetup ran from); fall back to the same dir as this
|
||||
# Register script if invoked standalone.
|
||||
function Resolve-Script {
|
||||
param([string]$LeafName)
|
||||
$p = Join-Path $PSScriptRoot $LeafName
|
||||
if (Test-Path -LiteralPath $p) { return $p }
|
||||
$p = "C:\Enrollment\shopfloor-setup\Shopfloor\$LeafName"
|
||||
if (Test-Path -LiteralPath $p) { return $p }
|
||||
return $null
|
||||
}
|
||||
|
||||
$promptScript = Resolve-Script 'Prompt-MachineNumber.ps1'
|
||||
$applyScript = Resolve-Script 'Apply-MachineNumber.ps1'
|
||||
if (-not $promptScript) { Write-RegLog "Prompt-MachineNumber.ps1 not found - cannot register"; exit 1 }
|
||||
if (-not $applyScript) { Write-RegLog "Apply-MachineNumber.ps1 not found - cannot register"; exit 1 }
|
||||
Write-RegLog "Prompt script: $promptScript"
|
||||
Write-RegLog "Apply script: $applyScript"
|
||||
|
||||
# --- Prompt task (user-context, GUI) ---
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$promptScript`""
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
# Group SID S-1-5-32-545 = BUILTIN\Users (catches ShopFloor + support/admin
|
||||
# users that log in interactively). RunLevel Limited - no elevation; the
|
||||
# actual writes happen in the SYSTEM Apply task below.
|
||||
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
||||
Register-ScheduledTask -TaskName $promptTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null
|
||||
Write-RegLog "Registered scheduled task '$promptTaskName' (AtLogOn, BUILTIN\Users, Limited)"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register '$promptTaskName': $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Apply task (SYSTEM, on-demand) ---
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$applyScript`""
|
||||
# No trigger - the Prompt task starts this via schtasks /run /tn.
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
||||
Register-ScheduledTask -TaskName $applyTaskName -Action $action -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null
|
||||
Write-RegLog "Registered scheduled task '$applyTaskName' (on-demand, SYSTEM, Highest)"
|
||||
|
||||
# Default SDDL on a SYSTEM-owned task only grants Admins + SYSTEM
|
||||
# FullAccess - BUILTIN\Users can't see or run it via schtasks /run.
|
||||
# Add an ACE granting BUILTIN\Users GenericRead + GenericExecute so the
|
||||
# user-context Prompt task can trigger this Apply task on demand. They
|
||||
# still can't modify/delete it - only read+execute.
|
||||
try {
|
||||
$svc = New-Object -ComObject Schedule.Service
|
||||
$svc.Connect()
|
||||
$taskObj = $svc.GetFolder('\').GetTask($applyTaskName)
|
||||
# GenericRead = 0x80000000 (GR), GenericExecute = 0x20000000 (GX)
|
||||
# BU = BUILTIN\Users
|
||||
$newSd = 'O:BAG:BAD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;GRGX;;;BU)'
|
||||
# SetSecurityDescriptor flag 0 = default, persists DACL change.
|
||||
$taskObj.SetSecurityDescriptor($newSd, 0)
|
||||
Write-RegLog "Granted BUILTIN\Users GR+GX on '$applyTaskName' (so Limited users can schtasks /run)"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to set task SDDL on '$applyTaskName': $_ (Limited users may not be able to trigger Apply)"
|
||||
}
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register '$applyTaskName': $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask end ==='
|
||||
exit 0
|
||||
@@ -1,45 +0,0 @@
|
||||
# Disable-WiredNics.ps1
|
||||
# Disables every Up wired (MediaType 802.3) NIC and records their names to
|
||||
# C:\Enrollment\disabled-wired-nics.txt so Monitor-IntuneProgress can
|
||||
# re-enable them once Report IP has run on WiFi-only.
|
||||
#
|
||||
# Reason: GE's Intune Proactive-Remediation "Report IP" script enumerates
|
||||
# Get-NetIPAddress and POSTs every IP it finds to a GE webhook. When a
|
||||
# shopfloor bay is still cabled to the air-gapped PXE LAN (10.9.100.0/24),
|
||||
# the webhook sees 10.9.100.x as one of the device's IPs and tags the bay
|
||||
# "not on corp net". A dynamic group / assignment-filter at GE then excludes
|
||||
# the bay from receiving the SFLD ConfigurationProfile (Function + SasToken
|
||||
# OMA-URI) -> Phase 2 "Device Configuration" never closes.
|
||||
#
|
||||
# Killing the wired NIC after stage 2 reports + before AAD-join makes the
|
||||
# bay's first Report IP fire see corp-WiFi IP only. The bay is tagged
|
||||
# clean, dynamic group eligibility flips, SFLD policy delivers normally.
|
||||
# Monitor-IntuneProgress re-enables the NIC once Report IP's log file
|
||||
# appears at C:\Logs\GE_Report_IP_Address*.txt.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$stateFile = 'C:\Enrollment\disabled-wired-nics.txt'
|
||||
|
||||
try {
|
||||
$wired = Get-NetAdapter -ErrorAction Stop |
|
||||
Where-Object {
|
||||
$_.Status -eq 'Up' -and
|
||||
$_.MediaType -eq '802.3' -and
|
||||
$_.HardwareInterface -eq $true
|
||||
}
|
||||
|
||||
if (-not $wired) {
|
||||
Write-Host "Disable-WiredNics: no Up wired NICs found - nothing to disable."
|
||||
return
|
||||
}
|
||||
|
||||
$names = $wired | ForEach-Object { $_.Name }
|
||||
$names | Out-File -FilePath $stateFile -Encoding ASCII -Force
|
||||
Write-Host ("Disable-WiredNics: persisted {0} NIC name(s) -> {1}" -f $names.Count, $stateFile)
|
||||
foreach ($n in $names) { Write-Host " - $n" }
|
||||
|
||||
$wired | Disable-NetAdapter -Confirm:$false -ErrorAction Continue
|
||||
Write-Host "Disable-WiredNics: NICs disabled. Re-enable triggered by Monitor when GE_Report_IP_Address log appears."
|
||||
} catch {
|
||||
Write-Warning "Disable-WiredNics: failed: $_"
|
||||
}
|
||||
@@ -66,6 +66,15 @@ if (Test-Path -LiteralPath $subtypeFile) {
|
||||
$pcSubtype = (Get-Content -LiteralPath $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
|
||||
# Display sub-type fallback: if pc-subtype.txt is absent (post-rename-reorg
|
||||
# default) but display-type.txt exists, use it as the subtype. Lets the
|
||||
# Display-Lobby / Display-Dashboard / gea-shopfloor-display-{lobby,dashboard}
|
||||
# profile keys resolve correctly for Display PCs.
|
||||
$displayTypeFile = 'C:\Enrollment\display-type.txt'
|
||||
if (-not $pcSubtype -and ($pcType -ieq 'gea-shopfloor-display' -or $pcType -ieq 'Display') -and (Test-Path -LiteralPath $displayTypeFile)) {
|
||||
$pcSubtype = (Get-Content -LiteralPath $displayTypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
|
||||
# Build the profile key: "Standard-Machine", "CMM", "Display-Lobby", etc.
|
||||
$profileKey = if ($pcSubtype) { "$pcType-$pcSubtype" } else { $pcType }
|
||||
|
||||
@@ -82,6 +91,8 @@ $pcProfileAliasGroups = @(
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Display-Lobby', 'gea-shopfloor-display-Lobby', 'gea-shopfloor-display-lobby'),
|
||||
@('Display-Dashboard', 'gea-shopfloor-display-Dashboard', 'gea-shopfloor-display-dashboard'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat')
|
||||
)
|
||||
|
||||
|
||||
@@ -80,11 +80,11 @@ param(
|
||||
# The persistent @logon sync_intune task takes over after reboot.
|
||||
[switch]$PostPpkg,
|
||||
# -PostPpkgSettleSec: how long to wait before the clean reboot when
|
||||
# in -PostPpkg mode. 60s empirically gives MDM enough time to push
|
||||
# in -PostPpkg mode. 120s empirically gives MDM enough time to push
|
||||
# the baseline policy (4 -> ~30 PolicyManager subkeys) so when techs
|
||||
# see sync_intune resume after reboot, the readiness signals are
|
||||
# already meaningful instead of "policy still pulling".
|
||||
[int]$PostPpkgSettleSec = 60
|
||||
[int]$PostPpkgSettleSec = 120
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
@@ -186,7 +186,10 @@ $script:cache = @{
|
||||
EnrollmentId = $null
|
||||
DeviceId = $null
|
||||
DeviceIdReported = $false
|
||||
SfldPolicyPushed = $false
|
||||
CredsReadyPushed = $false
|
||||
LockdownCompletePushed = $false
|
||||
ReportIpForced = $false
|
||||
InternetAccessDeleted = $false
|
||||
}
|
||||
|
||||
@@ -213,7 +216,12 @@ function Get-Phase1 {
|
||||
# on-screen QR works but the dashboard QR did not.
|
||||
if (-not $script:cache.AzureAdJoined -or -not $script:cache.DeviceId) {
|
||||
try {
|
||||
$dsreg = dsregcmd /status 2>&1
|
||||
# dsregcmd on Win11 emits ANSI escape codes (\x1B[7m...\x1B[0m)
|
||||
# around field names when its output is treated as a terminal.
|
||||
# Captured output then contains those codes between e.g.
|
||||
# "DeviceId" and ":", breaking a tight regex like
|
||||
# 'DeviceId\s*:\s*<value>'. Strip ANSI sequences before matching.
|
||||
$dsreg = (dsregcmd /status 2>&1 | Out-String) -replace '\x1B\[[0-9;]*[A-Za-z]', ''
|
||||
if (-not $script:cache.AzureAdJoined -and $dsreg -match 'AzureAdJoined\s*:\s*YES') {
|
||||
$script:cache.AzureAdJoined = $true
|
||||
}
|
||||
@@ -223,61 +231,12 @@ function Get-Phase1 {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# Report IP log presence drives two independent actions that USED to be
|
||||
# bundled inside the DeviceId-push gate. Splitting them so re-enable
|
||||
# fires even if DeviceId hasn't been captured yet (e.g. AAD join lag,
|
||||
# dsregcmd parse miss):
|
||||
#
|
||||
# 1. Re-enable wired NICs as soon as the log lands + state file exists.
|
||||
# 2. Push idx=7 once DeviceId is captured AND the log exists.
|
||||
$reportIpLog = Get-ChildItem -Path 'C:\Logs\GE_Report_IP_Address*' -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
$nicListFile = 'C:\Enrollment\disabled-wired-nics.txt'
|
||||
$justReEnabled = $false
|
||||
if ($reportIpLog -and (Test-Path $nicListFile)) {
|
||||
try {
|
||||
$nicNames = Get-Content $nicListFile -ErrorAction Stop
|
||||
foreach ($n in $nicNames) {
|
||||
if ([string]::IsNullOrWhiteSpace($n)) { continue }
|
||||
try { Enable-NetAdapter -Name $n -Confirm:$false -ErrorAction Stop }
|
||||
catch { Write-Warning "Enable-NetAdapter '$n' failed: $_" }
|
||||
}
|
||||
# Wait for DHCP renewal + route table update + reachability to
|
||||
# PXE server. 1 second wasn't enough in field testing - the
|
||||
# subsequent idx=7 push fired into the void before the wired
|
||||
# NIC was carrying traffic.
|
||||
Start-Sleep -Seconds 5
|
||||
Remove-Item $nicListFile -Force -ErrorAction SilentlyContinue
|
||||
$justReEnabled = $true
|
||||
} catch {
|
||||
Write-Warning "Re-enable wired NICs failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Push DeviceId / idx=7 once, when both DeviceId is captured and the
|
||||
# Report IP log has landed (dashboard QR renders from DeviceId).
|
||||
# Retry up to 6x with backoff because the imminent LAPS-prompt reboot
|
||||
# gives us only seconds and the wired NIC may still be settling.
|
||||
if ($script:cache.DeviceId -and -not $script:cache.DeviceIdReported -and $reportIpLog) {
|
||||
Ensure-SendPxeStatus
|
||||
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
|
||||
$attempts = if ($justReEnabled) { 6 } else { 1 }
|
||||
for ($i = 0; $i -lt $attempts; $i++) {
|
||||
$err = $null
|
||||
try {
|
||||
Send-PxeStatus -Stage 'Monitor-IntuneProgress: Intune Device ID captured' `
|
||||
-StageIndex 7 -StageTotal 8 `
|
||||
-IntuneDeviceId $script:cache.DeviceId -ErrorAction Stop
|
||||
$script:cache.DeviceIdReported = $true
|
||||
break
|
||||
} catch { $err = $_ }
|
||||
if ($i -lt $attempts - 1) { Start-Sleep -Seconds 2 }
|
||||
}
|
||||
if (-not $script:cache.DeviceIdReported -and $err) {
|
||||
Write-Warning "idx=7 push failed after $attempts attempts: $err"
|
||||
}
|
||||
}
|
||||
}
|
||||
# idx=7 push happens later in Get-Phase1 when Intune-registration
|
||||
# essentials are all green (see WiFi-swap block). The legacy
|
||||
# wired-NIC re-enable + reportIpLog-gated idx=7 retry was retired
|
||||
# after the PXE LAN renumber to 172.16.9.0/24 - PXE LAN addresses
|
||||
# no longer pass GE Report IP's StartsWith("10.") filter, so the
|
||||
# wired-disable / re-enable dance is unnecessary.
|
||||
|
||||
# Lockdown-applied auto-completion. Fleet-wide reality: bays use a LOCAL
|
||||
# ShopFloor account, so AzureAdPrt stays NO and user-scoped Intune policies
|
||||
@@ -358,35 +317,99 @@ function Get-Phase1 {
|
||||
} catch {}
|
||||
|
||||
# Once Intune registration is fully landed (AAD-joined + Intune-enrolled
|
||||
# + EnterpriseMgmt task present + baseline policies arrived), three
|
||||
# things must happen together:
|
||||
# 1. Delete INTERNETACCESS WiFi profile (gets bay off 172.16.x)
|
||||
# 2. Connect AESFMA (gets bay onto corp 10.x via EAP-TLS - cert is
|
||||
# already in LocalMachine\My thanks to Intune SCEP)
|
||||
# 3. Push idx=7 to the PXE dashboard with the captured DeviceId so
|
||||
# the dashboard card shows the QR for the Intune device id.
|
||||
# All three fire in one shot per Monitor lifetime via cache flags.
|
||||
# + EnterpriseMgmt task present + baseline policies arrived):
|
||||
# - Push idx=7 to PXE dashboard with the DeviceId / QR.
|
||||
# The INTERNETACCESS -> AESFMA WiFi swap uses a VERIFY-BEFORE-DELETE
|
||||
# pattern so the bay never ends up with no path:
|
||||
# 1. Phase 1 essentials must be COMPLETE (Intune registration done).
|
||||
# 2. Attempt netsh wlan connect AESFMA while INTERNETACCESS still up.
|
||||
# 3. Wait ~8s, parse netsh wlan show interfaces for SSID=AESFMA +
|
||||
# State=connected.
|
||||
# 4. ONLY after operationally connected to AESFMA, delete INTERNETACCESS.
|
||||
# 5. If connect fails (cert not provisioned yet, etc), keep
|
||||
# INTERNETACCESS, retry next tick.
|
||||
$phase1Essential = ($script:cache.AzureAdJoined -and
|
||||
$script:cache.IntuneEnrolled -and
|
||||
$script:cache.EmTaskExists -and
|
||||
$policiesBaselineReady)
|
||||
if ($phase1Essential -and -not $script:cache.InternetAccessDeleted) {
|
||||
try {
|
||||
Write-Host "Intune registration complete - deleting INTERNETACCESS profile + reconnecting to AESFMA..."
|
||||
$delOut = netsh wlan delete profile name="INTERNETACCESS" 2>&1 | Out-String
|
||||
Write-Host $delOut
|
||||
Start-Sleep -Seconds 2
|
||||
$conOut = netsh wlan connect name="AESFMA" ssid="AESFMA" 2>&1 | Out-String
|
||||
Write-Host $conOut
|
||||
# Helper: split netsh wlan show interfaces output into one block
|
||||
# per adapter (delimited by lines starting with "Name :"), then
|
||||
# check whether any block contains SSID=AESFMA AND State=connected
|
||||
# in either order.
|
||||
function Test-AESFMAConnected {
|
||||
$out = netsh wlan show interfaces 2>$null | Out-String
|
||||
if (-not $out) { return $false }
|
||||
$blocks = ($out -split '(?ms)(?=^\s*Name\s*:\s*)')
|
||||
foreach ($b in $blocks) {
|
||||
if (($b -match 'SSID\s*:\s*AESFMA\b') -and ($b -match 'State\s*:\s*connected\b')) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
if (Test-AESFMAConnected) {
|
||||
# Already connected (either via WLAN auto-join, prior tick's
|
||||
# attempt, or an operator manual connect). Clean up
|
||||
# INTERNETACCESS, force a Report IP push from the AESFMA-attached
|
||||
# corp address, and stop trying.
|
||||
Write-Host "AESFMA connected - cleaning up INTERNETACCESS..."
|
||||
$null = netsh wlan delete profile name="INTERNETACCESS" 2>&1 | Out-String
|
||||
$script:cache.InternetAccessDeleted = $true
|
||||
# Force the GE Report IP exe to post the new (AESFMA corp) IP
|
||||
# to the Tines webhook immediately - default trigger is on
|
||||
# DHCP event + slow interval, this skips the wait.
|
||||
if (-not $script:cache.ReportIpForced) {
|
||||
$rip = 'C:\ProgramData\ReportIP\GE_ReportIP_3_v1.EXE'
|
||||
if (Test-Path $rip) {
|
||||
try {
|
||||
Start-Process -FilePath $rip -ArgumentList '/ForceUpdate=True','/S' -WindowStyle Hidden -ErrorAction Stop
|
||||
Write-Host "Forced GE Report IP push (corp-AESFMA IP)."
|
||||
$script:cache.ReportIpForced = $true
|
||||
} catch {
|
||||
Write-Warning "WiFi swap (INTERNETACCESS -> AESFMA) failed: $_"
|
||||
Write-Warning "Force GE Report IP failed: $_"
|
||||
}
|
||||
}
|
||||
if ($phase1Essential -and $script:cache.DeviceId -and -not $script:cache.DeviceIdReported) {
|
||||
}
|
||||
} else {
|
||||
# Not connected. Try without pre-gating on a cert chain check -
|
||||
# the X509Chain.Build can return a partial chain (e.g. missing
|
||||
# intermediate) which made the strict root-thumbprint match
|
||||
# false even when EAP-TLS would actually succeed. Let netsh
|
||||
# itself be the source of truth via the connect attempt.
|
||||
# Rate-limit: at most one attempt every 30 seconds to avoid
|
||||
# spam when AESFMA isn't actually reachable.
|
||||
$now = Get-Date
|
||||
if (-not $script:cache.AesfmaNextAttempt -or $now -ge $script:cache.AesfmaNextAttempt) {
|
||||
try {
|
||||
Write-Host "Attempting AESFMA connect (INTERNETACCESS stays up as fallback)..."
|
||||
$null = netsh wlan connect name="AESFMA" ssid="AESFMA" 2>&1 | Out-String
|
||||
Start-Sleep -Seconds 15
|
||||
if (Test-AESFMAConnected) {
|
||||
Write-Host "AESFMA connected. Deleting INTERNETACCESS profile..."
|
||||
$null = netsh wlan delete profile name="INTERNETACCESS" 2>&1 | Out-String
|
||||
$script:cache.InternetAccessDeleted = $true
|
||||
} else {
|
||||
Write-Host "AESFMA connect not yet operational - will retry in 30s."
|
||||
$script:cache.AesfmaNextAttempt = $now.AddSeconds(30)
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "AESFMA connect/swap attempt failed: $_"
|
||||
$script:cache.AesfmaNextAttempt = $now.AddSeconds(30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# idx=7 push fires AS SOON AS DeviceId is captured. We want the QR
|
||||
# to render on the PXE dashboard BEFORE the Intune-driven LAPS-prompt
|
||||
# reboot lands (~1 min after GE Report IP posts its log). Phase 1
|
||||
# essentials, SCEP cert delivery, and AESFMA connection all take
|
||||
# longer than DeviceId capture, so don't gate on any of those.
|
||||
if ($script:cache.DeviceId -and -not $script:cache.DeviceIdReported) {
|
||||
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
Send-PxeStatus -Stage 'Monitor-IntuneProgress: Intune registration complete' `
|
||||
Send-PxeStatus -Stage 'Monitor-IntuneProgress: Intune Device ID captured' `
|
||||
-StageIndex 7 -StageTotal 8 `
|
||||
-IntuneDeviceId $script:cache.DeviceId -ErrorAction Stop
|
||||
$script:cache.DeviceIdReported = $true
|
||||
@@ -863,21 +886,13 @@ function Format-Snapshot {
|
||||
# not just "arriving". Stops the category prompt firing pre-first-reboot
|
||||
# when only ~4 subkeys are present (we tested this empirically; clicking
|
||||
# "assign category" at 4 subkeys = imaging stalls + re-image required).
|
||||
# Report IP log presence is part of Phase 1 completion. Without that log
|
||||
# we know GE's Proactive-Remediation script hasn't fired on WiFi-only
|
||||
# yet, which means the SFLD ConfigurationProfile assignment filter still
|
||||
# sees a leaked 10.9.100.x IP and Phase 2 won't unblock. Don't call
|
||||
# registration "done" until Report IP has cleared.
|
||||
$reportIpDone = [bool](Get-ChildItem -Path 'C:\Logs\GE_Report_IP_Address*' -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
$p1Done = ($Snap.Phase1.AzureAdJoined -and $Snap.Phase1.IntuneEnrolled -and
|
||||
$Snap.Phase1.EmTaskExists -and $Snap.Phase1.PoliciesBaselineReady -and
|
||||
$reportIpDone)
|
||||
$Snap.Phase1.EmTaskExists -and $Snap.Phase1.PoliciesBaselineReady)
|
||||
$p1Status = Get-PhaseStatus @(
|
||||
@{ Ok = $Snap.Phase1.AzureAdJoined; Failed = $false },
|
||||
@{ Ok = $Snap.Phase1.IntuneEnrolled; Failed = $false },
|
||||
@{ Ok = $Snap.Phase1.EmTaskExists; Failed = $false },
|
||||
@{ Ok = $Snap.Phase1.PoliciesBaselineReady; Failed = $false },
|
||||
@{ Ok = $reportIpDone; Failed = $false }
|
||||
@{ Ok = $Snap.Phase1.PoliciesBaselineReady; Failed = $false }
|
||||
)
|
||||
|
||||
# Phase 6 / Lockdown (shared by both flows, rendered last).
|
||||
@@ -1235,6 +1250,35 @@ try {
|
||||
while ($true) {
|
||||
$snap = Get-Snapshot
|
||||
|
||||
# Push sub-stage transitions to PXE dashboard so the operator sees
|
||||
# whether the bay is waiting on category assignment, or has
|
||||
# progressed past it. idx stays 7 across all three; the stage
|
||||
# string drives the friendly label in imaging.html.
|
||||
if (-not $script:cache.SfldPolicyPushed -and
|
||||
$snap.Phase2.SfldRoot -and $snap.Phase2.FunctionOk -and $snap.Phase2.SasTokenOk) {
|
||||
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
Send-PxeStatus -Stage 'Monitor-IntuneProgress: Phase 2 SFLD policy delivered (device configuration)' `
|
||||
-StageIndex 7 -StageTotal 8 `
|
||||
-IntuneDeviceId $script:cache.DeviceId -ErrorAction SilentlyContinue
|
||||
$script:cache.SfldPolicyPushed = $true
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
if (-not $script:cache.CredsReadyPushed -and
|
||||
$snap.Phase4.CredsPopulated -and
|
||||
$snap.Phase3.InstallComplete -and
|
||||
$snap.Phase2.SfldRoot -and $snap.Phase2.FunctionOk -and $snap.Phase2.SasTokenOk) {
|
||||
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
Send-PxeStatus -Stage 'Monitor-IntuneProgress: Phases 1-4 complete - ready for lockdown (ARTS request)' `
|
||||
-StageIndex 7 -StageTotal 8 `
|
||||
-IntuneDeviceId $script:cache.DeviceId -ErrorAction SilentlyContinue
|
||||
$script:cache.CredsReadyPushed = $true
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
# Retry QR code every cycle until it actually renders. dsregcmd
|
||||
# may report AzureAdJoined=YES before DeviceId is populated, so
|
||||
# a single-shot refresh misses the window.
|
||||
@@ -1338,8 +1382,16 @@ try {
|
||||
$nextRetrigger = $lastSync.AddMinutes($currentInterval)
|
||||
}
|
||||
|
||||
# Tight poll while DeviceId still missing - it may take a few
|
||||
# minutes after PPKG for dsregcmd to return a DeviceId, and we
|
||||
# need to catch it ASAP to push idx=7 before the LAPS reboot.
|
||||
# Once captured + reported, fall back to the normal cadence.
|
||||
if (-not $script:cache.DeviceIdReported) {
|
||||
Start-Sleep -Seconds 5
|
||||
} else {
|
||||
Start-Sleep -Seconds $PollSecs
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Any unhandled exception in the main loop lands here. Write the error
|
||||
|
||||
@@ -19,7 +19,7 @@ function Send-PxeStatus {
|
||||
# Only available post-AAD-join; pass it from Monitor-IntuneProgress
|
||||
# once captured. The dashboard renders a QR of this value.
|
||||
[string]$IntuneDeviceId = '',
|
||||
[string]$PxeServer = '10.9.100.1',
|
||||
[string]$PxeServer = '172.16.9.1',
|
||||
[int]$Port = 9009,
|
||||
[int]$TimeoutSec = 5
|
||||
)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Set-OpenTextAutoStart.ps1 - place WJ Shopfloor.lnk in the All Users
|
||||
# Startup folder so HostExplorer's "WJ Shopfloor" session launches at
|
||||
# every login. Idempotent: re-running is a no-op when the .lnk already
|
||||
# exists at the same path.
|
||||
#
|
||||
# Used by per-pc-type 09-Setup scripts for shopfloor types whose only
|
||||
# business app is OpenText (common, waxtrace, genspect, heattreat).
|
||||
# collections + nocollections do NOT auto-start OpenText - their techs
|
||||
# pick which apps via Configure-PC.ps1.
|
||||
#
|
||||
# Source .lnk is created by the OpenText preinstall (Setup-OpenText.ps1)
|
||||
# on the public desktop. If the .lnk is missing, log a warning and exit
|
||||
# 0 - imaging chain still continues; auto-start can be re-attempted on a
|
||||
# subsequent login by re-running this script.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
|
||||
$candidates = @(
|
||||
Join-Path $publicDesktop 'WJ Shopfloor.lnk'
|
||||
Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'WJ Shopfloor.lnk'
|
||||
)
|
||||
$src = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
if (-not $src) {
|
||||
Write-Warning "WJ Shopfloor.lnk not found on public desktop - OpenText auto-start NOT configured."
|
||||
Write-Warning " Searched: $($candidates -join ' ; ')"
|
||||
Write-Warning " Setup-OpenText.ps1 should create it during preinstall - check OpenText install state."
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $startupDir)) {
|
||||
New-Item -Path $startupDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$dst = Join-Path $startupDir 'WJ Shopfloor.lnk'
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||
Write-Host "OpenText auto-start enabled: $dst (source: $src)"
|
||||
} catch {
|
||||
Write-Warning "Failed to copy WJ Shopfloor.lnk to startup: $_"
|
||||
}
|
||||
@@ -244,11 +244,16 @@ function Update-MachineNumber {
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
# --- Update UDC settings JSON ---
|
||||
# -ErrorAction Stop on the WRITE so PermissionDenied / IO errors become
|
||||
# terminating and actually hit the catch block. Without this, the cmdlet
|
||||
# writes a non-terminating error (visible in transcript) but flow
|
||||
# continues + $out.UdcUpdated is set to $true, leading the dialog to
|
||||
# report "UDC updated" when the file write actually failed.
|
||||
if (Test-Path $script:UdcSettingsPath) {
|
||||
try {
|
||||
$json = Get-Content $script:UdcSettingsPath -Raw | ConvertFrom-Json
|
||||
$json = Get-Content $script:UdcSettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
$json.GeneralSettings.MachineNumber = $NewNumber
|
||||
$json | ConvertTo-Json -Depth 99 | Set-Content -Path $script:UdcSettingsPath -Encoding UTF8
|
||||
$json | ConvertTo-Json -Depth 99 | Set-Content -Path $script:UdcSettingsPath -Encoding UTF8 -ErrorAction Stop
|
||||
$out.UdcUpdated = $true
|
||||
} catch {
|
||||
$out.Errors += "UDC update failed: $_"
|
||||
@@ -256,9 +261,15 @@ function Update-MachineNumber {
|
||||
}
|
||||
|
||||
# --- Update eDNC registry ---
|
||||
# Same -ErrorAction Stop reasoning as above. Set-ItemProperty's
|
||||
# PermissionDenied is non-terminating by default; without -ErrorAction
|
||||
# Stop, the catch block never fires and $out.EdncUpdated=$true gets set
|
||||
# despite the write failing. This is the bug that made the 13:35:39
|
||||
# tech run on FGY07FZ3 report "eDNC updated to 3005 / All updates
|
||||
# succeeded" while the actual reg value stayed at 9999.
|
||||
if (Test-Path $script:EdncRegPath) {
|
||||
try {
|
||||
Set-ItemProperty -Path $script:EdncRegPath -Name MachineNo -Value $NewNumber -Type String -Force
|
||||
Set-ItemProperty -Path $script:EdncRegPath -Name MachineNo -Value $NewNumber -Type String -Force -ErrorAction Stop
|
||||
$out.EdncUpdated = $true
|
||||
} catch {
|
||||
$out.Errors += "eDNC update failed: $_"
|
||||
|
||||
@@ -86,6 +86,39 @@ switch ($stage) {
|
||||
break
|
||||
}
|
||||
|
||||
# Defensive: top up AutoLogonCount so SupportUser keeps auto-logging
|
||||
# in across any vendor-installer-forced reboots during this stage.
|
||||
# The unattend XML sets LogonCount=7 at install; typical imaging burns
|
||||
# through several reboots (Office, Oracle, FormTracePak forced reboot,
|
||||
# Run-ShopfloorSetup explicit reboot, stage advances) and the unplanned
|
||||
# FormTracePak reboot can push the counter past 0 - clearing
|
||||
# AutoAdminLogon and leaving the bay parked at the login screen with
|
||||
# the dispatcher unable to fire. Set the counter to 10 every time this
|
||||
# stage runs so the budget is restored. When sync-intune finishes the
|
||||
# whole pipeline, AutoAdminLogon is left to decrement to 0 naturally;
|
||||
# by then lockdown's own Autologon.exe has taken over for ShopFloor.
|
||||
try {
|
||||
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' `
|
||||
-Name 'AutoLogonCount' -Value 10 -Type DWord -ErrorAction Stop
|
||||
Write-Host "Topped up AutoLogonCount to 10 for SupportUser autologon resilience."
|
||||
} catch {
|
||||
Write-Warning "Failed to top up AutoLogonCount: $_"
|
||||
}
|
||||
|
||||
# Defensive: re-register RunOnce BEFORE calling Run-ShopfloorSetup.
|
||||
# Setup chains we don't control (FormTracePak Setup.exe, eDNC MSI,
|
||||
# any vendor installer that forces an immediate reboot) can cut
|
||||
# the script off mid-flight. Without this, the dispatcher never
|
||||
# returns from & $script and the post-call Register-NextRun never
|
||||
# fires, leaving the next boot with no RunOnce + a stalled image.
|
||||
# With this defensive register the next boot re-fires the same
|
||||
# dispatcher, which re-reads the still-'shopfloor-setup' stage
|
||||
# file, re-runs Run-ShopfloorSetup (every step is idempotent +
|
||||
# detects already-installed state), and converges. Once
|
||||
# Run-ShopfloorSetup returns normally we re-register again below
|
||||
# before advancing to the next stage - cheap, idempotent.
|
||||
Register-NextRun
|
||||
|
||||
# -FromDispatcher bypasses the stage-file gate at the top of
|
||||
# Run-ShopfloorSetup (which would otherwise see the stage file
|
||||
# and exit immediately thinking it should defer to us).
|
||||
|
||||
27
playbook/shopfloor-setup/Verify-And-Heal-Staging.bat
Normal file
@@ -0,0 +1,27 @@
|
||||
@echo off
|
||||
REM ==========================================================================
|
||||
REM Verify-And-Heal-Staging.bat - check every imaging payload arrived on this PC
|
||||
REM and re-pull whatever is missing from the enrollment share.
|
||||
REM
|
||||
REM Usage (run on the PC):
|
||||
REM Verify-And-Heal-Staging.bat verify + heal anything missing
|
||||
REM Verify-And-Heal-Staging.bat /verifyonly report only, do not pull
|
||||
REM ==========================================================================
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo Requesting administrator elevation...
|
||||
powershell -NoProfile -Command "Start-Process -Verb RunAs -FilePath '%~f0' -ArgumentList '%*'"
|
||||
exit /b
|
||||
)
|
||||
|
||||
set "PS=%~dp0Verify-And-Heal-Staging.ps1"
|
||||
set "ARGS="
|
||||
if /I "%~1"=="/verifyonly" set "ARGS=-VerifyOnly"
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS%" %ARGS%
|
||||
echo.
|
||||
echo Exit code: %errorlevel% (0=all present/healed, 1=still missing)
|
||||
pause
|
||||
endlocal
|
||||
150
playbook/shopfloor-setup/Verify-And-Heal-Staging.ps1
Normal file
@@ -0,0 +1,150 @@
|
||||
<#
|
||||
Verify-And-Heal-Staging.ps1
|
||||
|
||||
Post-boot check that every payload the imaging flow is supposed to stage onto a
|
||||
shopfloor PC actually arrived - and re-pull (heal) anything missing from the
|
||||
enrollment share. Runs in full Windows (reliable network), so it is immune to the
|
||||
WinPE samba-idle-drop that loses copies during the WIM apply.
|
||||
|
||||
Covers the generic Fetch payload (shopfloor-setup tree + preinstall bundle) AND
|
||||
the heavy per-type payload that Fetch-StagingPayload does NOT pull today: the CMM
|
||||
bundle (C:\CMM-Install) and the selected bay's backup set
|
||||
(C:\CMM-Install\backups\<cmmid>). That is the one that silently goes missing when
|
||||
WinPE staging runs out of time before reboot.
|
||||
|
||||
Designed to be:
|
||||
- run manually on a problem PC (Verify-And-Heal-Staging.bat), or
|
||||
- called from the pre-install phase before 00-PreInstall-MachineApps so a bay is
|
||||
never left under-provisioned.
|
||||
|
||||
Idempotent. Uses robocopy per item, which compares size + timestamp on every
|
||||
file, so it re-pulls anything MISSING or PARTIAL (e.g. a truncated MSI that
|
||||
"exists" but is incomplete and would fail to install) and skips files already
|
||||
complete. Heals use /R:3 /W:5 (resilient), not the WinPE fail-fast /R:1 /W:1.
|
||||
|
||||
Share + creds: read from C:\Enrollment\fetch-source.txt (line1=UNC, line2=user,
|
||||
line3=pass) - same file Fetch-StagingPayload uses - else the defaults below.
|
||||
|
||||
Run as administrator. Exit 0 = everything present or healed; 1 = something still
|
||||
missing after heal attempts (read the table).
|
||||
#>
|
||||
param(
|
||||
[string]$ShareUnc,
|
||||
[string]$ShareUser,
|
||||
[string]$SharePass,
|
||||
[switch]$VerifyOnly # report only, do not heal
|
||||
)
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$logDir = 'C:\Logs\Fetch'
|
||||
New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
$log = Join-Path $logDir "verify-heal-$ts.log"
|
||||
function Log($m,$lvl='INFO'){ $line="[$(Get-Date -Format 'HH:mm:ss')] [$lvl] $m"; Write-Host $line; Add-Content -Path $log -Value $line -EA SilentlyContinue }
|
||||
|
||||
# --- share + creds (mirror Fetch-StagingPayload) ---
|
||||
$defUnc='\\172.16.9.1\enrollment'; $defUser='pxe-upload'; $defPass='pxe'
|
||||
$srcFile='C:\Enrollment\fetch-source.txt'
|
||||
if ((-not $ShareUnc) -and (Test-Path -LiteralPath $srcFile)) {
|
||||
$l=@(Get-Content -LiteralPath $srcFile -EA SilentlyContinue)
|
||||
if ($l.Count -ge 1 -and $l[0].Trim()) { $ShareUnc=$l[0].Trim() }
|
||||
if ($l.Count -ge 2 -and $l[1].Trim()) { $ShareUser=$l[1].Trim() }
|
||||
if ($l.Count -ge 3 -and $l[2].Trim()) { $SharePass=$l[2].Trim() }
|
||||
}
|
||||
if (-not $ShareUnc) { $ShareUnc=$defUnc }
|
||||
if (-not $ShareUser) { $ShareUser=$defUser }
|
||||
if (-not $SharePass) { $SharePass=$defPass }
|
||||
|
||||
# --- identity ---
|
||||
function ReadTxt($p){ if (Test-Path -LiteralPath $p) { (Get-Content -LiteralPath $p -First 1 -EA 0).Trim() } else { '' } }
|
||||
$pcType = ReadTxt 'C:\Enrollment\pc-type.txt'
|
||||
$cmmid = ReadTxt 'C:\Enrollment\cmm\cmmid.txt'
|
||||
|
||||
Log "=== Verify-And-Heal-Staging ==="
|
||||
Log "share=$ShareUnc user=$ShareUser pcType=$(if($pcType){$pcType}else{'(none)'}) cmmid=$(if($cmmid){$cmmid}else{'(none)'}) verifyOnly=$VerifyOnly"
|
||||
|
||||
# --- expected payload manifest -------------------------------------------------
|
||||
# Each: Label, Src (under share), Dst, Mode (File|Dir), Verify (path that must
|
||||
# exist to count as present), Optional (missing-and-no-source is not a failure),
|
||||
# Files (for Mode=File), Xd (robocopy /XD dirs to exclude on heal).
|
||||
$items = New-Object System.Collections.Generic.List[object]
|
||||
function Add-Item($Label,$Src,$Dst,$Mode,$Verify,$Files=$null,$Optional=$false,$Xd=$null){
|
||||
$items.Add([pscustomobject]@{Label=$Label;Src=$Src;Dst=$Dst;Mode=$Mode;Verify=$Verify;Files=$Files;Optional=$Optional;Xd=$Xd})
|
||||
}
|
||||
$ENR='C:\Enrollment'; $SFD='C:\Enrollment\shopfloor-setup'; $PIN='C:\PreInstall'
|
||||
Add-Item 'Run-ShopfloorSetup.ps1' 'shopfloor-setup' $ENR 'File' (Join-Path $ENR 'Run-ShopfloorSetup.ps1') @('Run-ShopfloorSetup.ps1')
|
||||
Add-Item 'Shopfloor baseline' 'shopfloor-setup\Shopfloor' (Join-Path $SFD 'Shopfloor') 'Dir' (Join-Path $SFD 'Shopfloor')
|
||||
Add-Item 'common' 'shopfloor-setup\common' (Join-Path $SFD 'common') 'Dir' (Join-Path $SFD 'common')
|
||||
Add-Item '_ntlars-backups' 'shopfloor-setup\_ntlars-backups' (Join-Path $SFD '_ntlars-backups') 'Dir' (Join-Path $SFD '_ntlars-backups') $null $true
|
||||
if ($pcType) {
|
||||
Add-Item "type:$pcType" "shopfloor-setup\$pcType" (Join-Path $SFD $pcType) 'Dir' (Join-Path $SFD $pcType)
|
||||
}
|
||||
Add-Item 'preinstall.json' 'pre-install' $PIN 'File' (Join-Path $PIN 'preinstall.json') @('preinstall.json')
|
||||
Add-Item 'preinstall installers' 'pre-install\installers' (Join-Path $PIN 'installers') 'Dir' (Join-Path $PIN 'installers')
|
||||
Add-Item 'udc-backups' 'pre-install\udc-backups' (Join-Path $PIN 'udc-backups') 'Dir' (Join-Path $PIN 'udc-backups') $null $true
|
||||
# --- heavy CMM payload (the gap) ---
|
||||
if ($pcType -eq 'gea-shopfloor-cmm') {
|
||||
Add-Item 'CMM bundle' 'installers-post\cmm' 'C:\CMM-Install' 'Dir' 'C:\CMM-Install\cmm-manifest.json' $null $false 'backups'
|
||||
if ($cmmid) {
|
||||
Add-Item "CMM backup ($cmmid)" "installers-post\cmm\backups\$cmmid" "C:\CMM-Install\backups\$cmmid" 'Dir' "C:\CMM-Install\backups\$cmmid" $null $true
|
||||
}
|
||||
}
|
||||
|
||||
# --- robocopy-based verify/heal -----------------------------------------------
|
||||
# Presence alone is NOT trusted: a partially transferred file (e.g. a truncated
|
||||
# MSI) exists but is incomplete and breaks install. Instead robocopy runs per
|
||||
# item and compares size + timestamp on EVERY file, re-pulling any that are
|
||||
# missing OR differ (partial/truncated) and skipping ones already complete (a
|
||||
# cheap metadata scan). So it scans all files, not just checks a folder is
|
||||
# non-empty. VerifyOnly adds /L (list-only): it reports what WOULD be re-pulled
|
||||
# without changing anything.
|
||||
$drive='Z:'; $mounted=$false
|
||||
function Mount-Share { cmd /c "net use $drive /delete /y >nul 2>&1"; & net use $drive $ShareUnc /user:$ShareUser $SharePass /persistent:no 2>&1 | Out-Null; return ($LASTEXITCODE -eq 0) }
|
||||
|
||||
$report = New-Object System.Collections.Generic.List[object]
|
||||
for ($a=1; $a -le 5 -and -not $mounted; $a++){ if (Mount-Share){$mounted=$true;Log "Mounted $ShareUnc as $drive"} else {Log "mount attempt $a/5 failed - 10s" 'WARN'; Start-Sleep 10} }
|
||||
if (-not $mounted) {
|
||||
Log "Could not mount $ShareUnc after 5 attempts - cannot verify/heal. Bay may be under-provisioned; re-run once the share is reachable." 'ERROR'
|
||||
foreach ($it in $items) { $report.Add([pscustomobject]@{Item=$it.Label;Status='NO-MOUNT'}) }
|
||||
} else {
|
||||
foreach ($it in $items) {
|
||||
$src = Join-Path $drive $it.Src
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
$report.Add([pscustomobject]@{Item=$it.Label;Status=$(if($it.Optional){'ABSENT(opt)'}else{'NO-SOURCE'})})
|
||||
Log "[$($it.Label)] source not on share ($src)$(if($it.Optional){' - optional'})" $(if($it.Optional){'INFO'}else{'WARN'})
|
||||
continue
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $it.Dst)) { New-Item -ItemType Directory -Path $it.Dst -Force | Out-Null }
|
||||
$args=@($src,$it.Dst)
|
||||
if ($it.Mode -eq 'Dir') { $args+='/E' } else { $args+=$it.Files }
|
||||
if ($it.Xd) { $args+=@('/XD',(Join-Path $src $it.Xd)) }
|
||||
$args+=@('/R:3','/W:5','/NFL','/NDL','/NP')
|
||||
if ($VerifyOnly) { $args+='/L' } # list-only: detect missing/partial, change nothing
|
||||
$out = & robocopy @args 2>&1
|
||||
$rc = $LASTEXITCODE
|
||||
# robocopy exit bits: 1=copied, 2=extra, 4=mismatch, 8+=failure (<8 success).
|
||||
$copied = (($rc -band 1) -ne 0) -or (($rc -band 4) -ne 0)
|
||||
$files = ($out | Select-String -Pattern '^\s*Files :' | Select-Object -First 1)
|
||||
if ($rc -ge 8) { $status='HEAL-FAIL' }
|
||||
elseif (-not $copied) { $status='COMPLETE' } # in sync, nothing to do
|
||||
elseif ($VerifyOnly) { $status='INCOMPLETE' } # would re-pull (missing/partial)
|
||||
else { $status='HEALED' } # actually re-pulled missing/partial
|
||||
$report.Add([pscustomobject]@{Item=$it.Label;Status=$status})
|
||||
Log "[$($it.Label)] robocopy rc=$rc -> $status $(("$files").Trim())"
|
||||
}
|
||||
cmd /c "net use $drive /delete /y >nul 2>&1"
|
||||
}
|
||||
|
||||
# --- report --------------------------------------------------------------------
|
||||
Log '================ STAGING VERIFY/HEAL REPORT ================'
|
||||
foreach ($r in $report) { Log (" {0,-26} {1}" -f $r.Item, $r.Status) }
|
||||
$bad = @($report | Where-Object { $_.Status -in @('NO-SOURCE','HEAL-FAIL','NO-MOUNT','INCOMPLETE') })
|
||||
if ($bad.Count -gt 0) {
|
||||
Log "RESULT: $($bad.Count) item(s) need attention: $(($bad|ForEach-Object{$_.Item+'='+$_.Status}) -join ', ')" 'ERROR'
|
||||
Log "Log: $log"
|
||||
exit 1
|
||||
} else {
|
||||
Log 'RESULT: all required payloads complete (or healed).'
|
||||
Log "Log: $log"
|
||||
exit 0
|
||||
}
|
||||
@@ -76,6 +76,40 @@ $pcType = (Get-Content -LiteralPath $pcTypeFile -First 1 -ErrorAction Silentl
|
||||
$pcSubType = if (Test-Path $pcSubTypeFile) {
|
||||
(Get-Content -LiteralPath $pcSubTypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
} else { '' }
|
||||
|
||||
# Backfill pc-subtype.txt on Keyence PCs imaged before 2026-05 (startnet.cmd
|
||||
# didn't write pc-subtype.txt for Keyence then). Without a subtype, the share
|
||||
# manifest's per-model PCTypes gate falls back to installing the default model
|
||||
# (VR-6000) on top of VR-3000 / VR-5000 boxes. Detect the installed model from
|
||||
# its uninstall ProductCode and persist the subtype so subsequent GE-Enforce
|
||||
# cycles + the share manifest gate route correctly.
|
||||
if ($pcType -ieq 'keyence' -and -not $pcSubType) {
|
||||
$keyenceProducts = @(
|
||||
@{ Subtype = 'vr3000'; Path = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{9CC9A062-2A93-4D3B-AECA-F70C691A46F2}' },
|
||||
@{ Subtype = 'vr5000'; Path = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{AF7E8B93-DBEB-4DB1-91CB-4DA592D8E222}' },
|
||||
@{ Subtype = 'vr6000'; Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{058E7194-BDF8-4FA2-9D69-978BB0F25214}' }
|
||||
)
|
||||
foreach ($p in $keyenceProducts) {
|
||||
if (Test-Path -LiteralPath $p.Path) {
|
||||
$pcSubType = $p.Subtype
|
||||
try {
|
||||
$enrollDir = Split-Path -Parent $pcSubTypeFile
|
||||
if (-not (Test-Path -LiteralPath $enrollDir)) {
|
||||
New-Item -Path $enrollDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
Set-Content -LiteralPath $pcSubTypeFile -Value $pcSubType -Encoding ascii -Force
|
||||
Write-EnforceLog "Backfilled pc-subtype.txt = $pcSubType from installed product code"
|
||||
} catch {
|
||||
Write-EnforceLog "pc-subtype.txt backfill write failed: $_" 'WARN'
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $pcSubType) {
|
||||
Write-EnforceLog "Keyence PC with no pc-subtype.txt and no recognized VR product installed - skipping model-gated apps until imaging populates subtype" 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
Write-EnforceLog "PCType: $pcType$(if ($pcSubType) { " / $pcSubType" })"
|
||||
|
||||
# --- site-config ---
|
||||
|
||||
@@ -38,6 +38,12 @@ $ErrorActionPreference = 'Continue'
|
||||
# logged; manifests tagged with a newer MINOR are fine.
|
||||
#
|
||||
# Changelog:
|
||||
# 2.6 - added _CmmVersion filter. Entry tagged _CmmVersion only applies when
|
||||
# it equals C:\Enrollment\cmm\version.txt (the bay's resolved PC-DMIS
|
||||
# version, written at imaging from cmm-bay-config.csv). Untagged entries
|
||||
# always pass; missing/empty version file is a no-op (legacy install-all
|
||||
# + non-CMM scopes unaffected). Lifted out of 09-Setup-CMM so the gate
|
||||
# lives in one place both the imaging and enforce paths share.
|
||||
# 2.5 - Type=EXE handler honors optional WaitTimeoutSec on the manifest
|
||||
# entry. WiX Burn bootstrappers (UDC_Setup.exe) install the MSI
|
||||
# successfully but the wrapper process never exits (waits on a
|
||||
@@ -58,7 +64,7 @@ $ErrorActionPreference = 'Continue'
|
||||
# 2.0 - initial Stage 2a: PS1/BAT/File/Registry/INF action types,
|
||||
# Always/MarkerFile/ValueMatches/pnputil detection, PCTypes filter
|
||||
$LIB_MANIFEST_MAJOR = 2
|
||||
$LIB_MANIFEST_MINOR = 5
|
||||
$LIB_MANIFEST_MINOR = 6
|
||||
|
||||
$logDir = Split-Path -Parent $LogFile
|
||||
if (-not (Test-Path $logDir)) {
|
||||
@@ -354,9 +360,18 @@ function Invoke-InstallerAction {
|
||||
return [pscustomobject]@{ ExitCode = $proc.ExitCode; LogRef = $App.LogFile }
|
||||
}
|
||||
'PS1' {
|
||||
$scriptPath = Join-Path $InstallerRoot ($App.Script)
|
||||
# Accept either Script or Installer as the relative path, and never
|
||||
# feed a null into Join-Path/Test-Path (that throws a cryptic
|
||||
# 'LiteralPath is null'). Log the resolved value so a bad/empty
|
||||
# entry is obvious in the log instead of crashing the entry.
|
||||
$rel = if ($App.Script) { $App.Script } elseif ($App.Installer) { $App.Installer } else { $null }
|
||||
if ([string]::IsNullOrWhiteSpace([string]$rel)) {
|
||||
Write-InstallLog (" PS1 entry '{0}' has no Script/Installer value (Script={1}, Installer={2}) - skipping" -f $App.Name, $App.Script, $App.Installer) 'ERROR'
|
||||
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
|
||||
}
|
||||
$scriptPath = Join-Path $InstallerRoot $rel
|
||||
if (-not (Test-Path -LiteralPath $scriptPath)) {
|
||||
Write-InstallLog " PS1 not found: $scriptPath" 'ERROR'
|
||||
Write-InstallLog " PS1 not found: $scriptPath (from rel '$rel')" 'ERROR'
|
||||
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
|
||||
}
|
||||
$psi.FileName = 'powershell.exe'
|
||||
@@ -455,7 +470,8 @@ $script:_pcTypeAliasGroups = @(
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat')
|
||||
@('Heattreat', 'gea-shopfloor-heattreat'),
|
||||
@('PartMarker', 'gea-shopfloor-partmarker')
|
||||
)
|
||||
|
||||
# Returns every alias set (each itself a string array) that contains $name.
|
||||
@@ -519,29 +535,35 @@ function Test-HostnameMatches {
|
||||
}
|
||||
|
||||
# Machine-number filter. Stable identifier tied to the bay; survives PC
|
||||
# replacement at the same machine. Source of truth = the value the tech
|
||||
# entered at the PXE menu, persisted to C:\Enrollment\machine-number.txt
|
||||
# by startnet.cmd. Falls back to the DNC registry if that file is missing
|
||||
# (covers PCs that pre-date this filter being introduced).
|
||||
# replacement at the same machine.
|
||||
#
|
||||
# Source of truth = the eDNC/DNC registry MachineNo. That is what the
|
||||
# reassignment flow (Set-MachineNumber -> Update-MachineNumber) actually
|
||||
# rewrites when a bay is re-numbered (e.g. 9999 placeholder -> 7501). The
|
||||
# imaging-time C:\Enrollment\machine-number.txt is written ONCE by startnet.cmd
|
||||
# at the PXE menu and is NOT updated on reassignment, so it goes stale. Read
|
||||
# the registry FIRST so TargetMachineNumbers gating follows reassignment; fall
|
||||
# back to the txt only when the registry has no value (covers non-DNC PCs or a
|
||||
# bay where eDNC has not populated MachineNo yet).
|
||||
$script:_cachedMachineNumber = $null
|
||||
function Get-CurrentMachineNumber {
|
||||
if ($null -ne $script:_cachedMachineNumber) { return $script:_cachedMachineNumber }
|
||||
$candidates = @(
|
||||
'C:\Enrollment\machine-number.txt'
|
||||
)
|
||||
foreach ($p in $candidates) {
|
||||
if (Test-Path -LiteralPath $p) {
|
||||
$v = (Get-Content -LiteralPath $p -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
if ($v) { $script:_cachedMachineNumber = $v.Trim(); return $script:_cachedMachineNumber }
|
||||
}
|
||||
}
|
||||
foreach ($r in @(
|
||||
'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General',
|
||||
'HKLM:\SOFTWARE\GE Aircraft Engines\DNC\General'
|
||||
)) {
|
||||
if (Test-Path $r) {
|
||||
$p = Get-ItemProperty -Path $r -ErrorAction SilentlyContinue
|
||||
if ($p.MachineNo) { $script:_cachedMachineNumber = [string]$p.MachineNo; return $script:_cachedMachineNumber }
|
||||
if ($p.MachineNo) {
|
||||
$v = ([string]$p.MachineNo).Trim()
|
||||
if ($v) { $script:_cachedMachineNumber = $v; return $script:_cachedMachineNumber }
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($p in @('C:\Enrollment\machine-number.txt')) {
|
||||
if (Test-Path -LiteralPath $p) {
|
||||
$v = (Get-Content -LiteralPath $p -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
if ($v) { $script:_cachedMachineNumber = $v.Trim(); return $script:_cachedMachineNumber }
|
||||
}
|
||||
}
|
||||
$script:_cachedMachineNumber = ''
|
||||
@@ -559,6 +581,42 @@ function Test-MachineNumberMatches {
|
||||
return $false
|
||||
}
|
||||
|
||||
# CMM PC-DMIS version filter. The bay's PC-DMIS version (2016/2019/2026) is
|
||||
# resolved at imaging by resolve-cmm-bay-config.ps1 from cmm-bay-config.csv (the
|
||||
# single bay -> version map) and persisted to C:\Enrollment\cmm\version.txt. An
|
||||
# entry tagged _CmmVersion applies only when it equals that file; untagged
|
||||
# entries (CLM, goCMM, Protect Viewer, DODA, the PDF converter) always pass.
|
||||
# When the file is absent/empty - a bay imaged before the picker, or any
|
||||
# non-CMM PC running a different scope - the filter is a no-op so every tagged
|
||||
# entry passes. That preserves the legacy "install all versions" behavior for
|
||||
# pre-picker bays and leaves non-CMM scopes untouched.
|
||||
#
|
||||
# This is the SINGLE place the version gate lives. Both the imaging path
|
||||
# (09-Setup-CMM) and the runtime path (GE-Enforce) call this lib, so the gate
|
||||
# cannot apply in one path and not the other. The 2016-installed-on-a-2019-bay
|
||||
# bug was exactly that drift: the imaging path filtered by _CmmVersion but the
|
||||
# enforce path did not, so enforce reinstalled every version it did not detect.
|
||||
$script:_cachedCmmVersion = $null
|
||||
$script:_cmmVersionRead = $false
|
||||
function Get-CurrentCmmVersion {
|
||||
if ($script:_cmmVersionRead) { return $script:_cachedCmmVersion }
|
||||
$script:_cmmVersionRead = $true
|
||||
$f = 'C:\Enrollment\cmm\version.txt'
|
||||
if (Test-Path -LiteralPath $f) {
|
||||
$v = (Get-Content -LiteralPath $f -First 1 -ErrorAction SilentlyContinue)
|
||||
if ($v) { $script:_cachedCmmVersion = $v.Trim() }
|
||||
}
|
||||
return $script:_cachedCmmVersion
|
||||
}
|
||||
|
||||
function Test-CmmVersionMatches {
|
||||
param($App)
|
||||
if (-not $App._CmmVersion) { return $true } # untagged entry always applies
|
||||
$myVer = Get-CurrentCmmVersion
|
||||
if (-not $myVer) { return $true } # no resolved version -> legacy install-all
|
||||
return ([string]$App._CmmVersion -ieq $myVer)
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -574,6 +632,11 @@ foreach ($app in $config.Applications) {
|
||||
|
||||
Write-InstallLog "==> $($app.Name)"
|
||||
|
||||
# Per-entry guard: a single entry that throws must NOT abort the whole
|
||||
# scope (and silently skip every later entry + the status write). Catch,
|
||||
# log, count as failed, move on.
|
||||
try {
|
||||
|
||||
if (-not (Test-PCTypeMatches -App $app -Type $PCType -SubType $PCSubType)) {
|
||||
Write-InstallLog " PCTypes filter: entry targets $($app.PCTypes -join ',') but PC is $PCType$(if ($PCSubType) { "-$PCSubType" }) - skipping"
|
||||
$pcFiltered++
|
||||
@@ -593,6 +656,13 @@ foreach ($app in $config.Applications) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not (Test-CmmVersionMatches -App $app)) {
|
||||
$myVer = Get-CurrentCmmVersion
|
||||
Write-InstallLog " _CmmVersion filter: entry targets $($app._CmmVersion) but bay version is $(if ($myVer) { $myVer } else { '(none)' }) - skipping"
|
||||
$pcFiltered++
|
||||
continue
|
||||
}
|
||||
|
||||
if (Test-AppInstalled -App $app) {
|
||||
Write-InstallLog ' Already installed at expected version - skipping'
|
||||
$skipped++
|
||||
@@ -680,6 +750,11 @@ foreach ($app in $config.Applications) {
|
||||
|
||||
$failed++
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-InstallLog (" UNCAUGHT error processing {0}: {1} | at {2}" -f $app.Name, $_.Exception.Message, ($_.ScriptStackTrace -replace '\s+',' ')) 'ERROR'
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
|
||||
Write-InstallLog '============================================'
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# Deploy-ShopfloorStartLayout.ps1
|
||||
#
|
||||
# Local-DSC port of the Intune SFLD desktop/Start-menu deployment. Creates the
|
||||
# Public Desktop weblinks (.url) + app/folder shortcuts (.lnk) AND pins them to
|
||||
# the Windows 11 Start menu - using the exact same mechanism Simple-Install.ps1
|
||||
# uses: shortcuts in the All-Users Start Menu, a ConfigureStartPins JSON policy
|
||||
# in the registry, and a StartMenuExperienceHost reset so it applies on next
|
||||
# logon. Nothing here needs Intune/MDM - it is all file + registry-policy.
|
||||
#
|
||||
# Designed to run from the GE-Enforce manifest engine as a Type=PS1 entry
|
||||
# (DetectionMethod=Always, or Hash on the pins.json). Idempotent.
|
||||
#
|
||||
# Usage:
|
||||
# powershell -ExecutionPolicy Bypass -File Deploy-ShopfloorStartLayout.ps1
|
||||
# -AssetsDir <globalassets dir on the share>
|
||||
#
|
||||
# AssetsDir holds the prebuilt .url/.lnk (the "globalassets" folder). The pin
|
||||
# list + order below mirrors device-config.yaml StartMenuPins; entries with a
|
||||
# Target are created on the fly (app/folder pins), the rest are copied from
|
||||
# AssetsDir.
|
||||
|
||||
param(
|
||||
[string]$AssetsDir = (Join-Path $PSScriptRoot 'globalassets'),
|
||||
[string]$DesktopDir = 'C:\Users\Public\Desktop',
|
||||
[switch]$NoShellRestart
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$logDir = 'C:\Logs\Shopfloor'
|
||||
New-Item -ItemType Directory -Path $logDir -Force -EA SilentlyContinue | Out-Null
|
||||
$log = Join-Path $logDir ('start-layout-{0}.log' -f (Get-Date -Format 'yyyyMMdd'))
|
||||
function Log($m){ "$([DateTime]::Now.ToString('s')) $m" | Tee-Object -FilePath $log -Append | Out-Null }
|
||||
|
||||
# Ordered pin set - mirrors device-config.yaml StartMenuPins. Name = the file
|
||||
# in the All-Users Start Menu (and AssetsDir for prebuilt ones). Target set =>
|
||||
# create the shortcut; Target empty => copy the prebuilt file from AssetsDir.
|
||||
$Pins = @(
|
||||
@{ Name = 'Shopfloor Dashboard.url' }
|
||||
@{ Name = 'PN & SN Label Printing.url' }
|
||||
@{ Name = 'WJ Shop Floor Homepage.url' }
|
||||
@{ Name = 'WJ Web Reports.url' }
|
||||
@{ Name = 'Blueprint PDF Viewer.url' }
|
||||
@{ Name = 'Central CSF Web Reports.url' }
|
||||
@{ Name = 'Plant Apps.url' }
|
||||
@{ Name = 'Safety Good Catch Form.url' }
|
||||
@{ Name = 'WJ IT Help Desk.url' }
|
||||
@{ Name = 'OneIDM.url' }
|
||||
@{ Name = 'M365 Webmail.url' }
|
||||
@{ Name = 'HR Central.url' }
|
||||
@{ Name = 'Defect_Tracker.lnk' }
|
||||
@{ Name = 'Calculator.lnk' }
|
||||
@{ Name = 'Notepad.lnk' }
|
||||
@{ Name = 'eDNC.lnk'; Target = 'C:\Program Files\eDNC\eDNC.exe' }
|
||||
@{ Name = 'NTLARS.lnk'; Target = 'C:\Program Files (x86)\NTLARS\NTLARS.exe' }
|
||||
@{ Name = 'Shopfloor Tools.lnk'; Target = 'C:\Users\Public\Desktop\Shopfloor Tools' }
|
||||
)
|
||||
|
||||
$startMenuDir = Join-Path $env:ALLUSERSPROFILE 'Microsoft\Windows\Start Menu\Programs'
|
||||
|
||||
function New-UrlShortcut([string]$Path,[string]$Url){
|
||||
@('[InternetShortcut]', "URL=$Url") | Set-Content -LiteralPath $Path -Encoding ASCII
|
||||
}
|
||||
function New-LnkShortcut([string]$Path,[string]$Target,[string]$Args,[string]$Icon){
|
||||
$sh = New-Object -ComObject WScript.Shell
|
||||
$sc = $sh.CreateShortcut($Path)
|
||||
$sc.TargetPath = $Target
|
||||
if ($Args) { $sc.Arguments = $Args }
|
||||
# working dir: parent of target for files, the folder itself for folder pins
|
||||
$sc.WorkingDirectory = if (Test-Path -LiteralPath $Target -PathType Container) { $Target } else { Split-Path -Parent $Target }
|
||||
if ($Icon) { $sc.IconLocation = $Icon }
|
||||
$sc.Save()
|
||||
}
|
||||
|
||||
Log "=== Deploy shopfloor start layout (assets: $AssetsDir) ==="
|
||||
New-Item -ItemType Directory -Path $startMenuDir -Force -EA SilentlyContinue | Out-Null
|
||||
New-Item -ItemType Directory -Path $DesktopDir -Force -EA SilentlyContinue | Out-Null
|
||||
|
||||
$pinnedList = @()
|
||||
foreach ($pin in $Pins) {
|
||||
$leaf = $pin.Name
|
||||
$dst = Join-Path $startMenuDir $leaf
|
||||
try {
|
||||
if ($pin.Target) {
|
||||
# create app/folder shortcut from Target
|
||||
New-LnkShortcut -Path $dst -Target $pin.Target -Args $pin.Arguments -Icon $pin.IconLocation
|
||||
Log "created (target) $leaf -> $($pin.Target)"
|
||||
} else {
|
||||
# copy prebuilt asset (.url/.lnk) from globalassets
|
||||
$src = Join-Path $AssetsDir $leaf
|
||||
if (-not (Test-Path -LiteralPath $src)) { Log "MISSING asset, skipping pin: $src"; continue }
|
||||
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||
# also drop on the Public Desktop
|
||||
Copy-Item -LiteralPath $src -Destination (Join-Path $DesktopDir $leaf) -Force
|
||||
Log "copied $leaf (start menu + desktop)"
|
||||
}
|
||||
$pinnedList += @{ desktopAppLink = "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\$leaf" }
|
||||
} catch {
|
||||
Log "ERROR pin ${leaf}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# ConfigureStartPins JSON -> HKLM policy (same shape Simple-Install.ps1 writes)
|
||||
$jsonDir = 'C:\ProgramData\SFLD\StartMenu'
|
||||
New-Item -ItemType Directory -Path $jsonDir -Force -EA SilentlyContinue | Out-Null
|
||||
$jsonPath = Join-Path $jsonDir 'pins.json'
|
||||
([ordered]@{ applyOnce = $false; pinnedList = $pinnedList } | ConvertTo-Json -Depth 6) |
|
||||
Set-Content -LiteralPath $jsonPath -Encoding UTF8
|
||||
$reg = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer'
|
||||
if (-not (Test-Path $reg)) { New-Item -Path $reg -Force | Out-Null }
|
||||
New-ItemProperty -Path $reg -Name 'ConfigureStartPins' -PropertyType String `
|
||||
-Value (Get-Content -LiteralPath $jsonPath -Raw -Encoding UTF8) -Force | Out-Null
|
||||
Log "ConfigureStartPins policy written ($($pinnedList.Count) pins) -> $reg"
|
||||
|
||||
# Apply now: clear each real user's cached start layout + restart the shell.
|
||||
if (-not $NoShellRestart) {
|
||||
Get-ChildItem 'C:\Users' -Directory -EA SilentlyContinue |
|
||||
Where-Object { $_.Name -notin @('Public','Default','Default User','All Users') } |
|
||||
ForEach-Object {
|
||||
$sb = Join-Path $_.FullName 'AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin'
|
||||
if (Test-Path -LiteralPath $sb) { Remove-Item -LiteralPath $sb -Force -EA SilentlyContinue; Log "cleared start2.bin: $($_.Name)" }
|
||||
}
|
||||
Get-Process -Name 'StartMenuExperienceHost' -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue
|
||||
Log 'StartMenuExperienceHost restarted (pins apply on next shell load)'
|
||||
}
|
||||
|
||||
Log '=== done ==='
|
||||
exit 0
|
||||
@@ -7,7 +7,7 @@
|
||||
{ "name": "Adobe Acrobat Reader DC", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{AC76BA86-7AD7-1033-7B44-AC0F074E4100}", "name": "DisplayVersion", "value": "25.001.20531" } },
|
||||
{ "name": "WJF Defect Tracker", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{CC1B4D32-1606-4A3F-8F24-31312F723D5C}", "name": "DisplayVersion", "value": "01.00.0102" } },
|
||||
{ "name": "3OF9 barcode font", "verify": { "method": "File", "path": "C:\\Windows\\Fonts\\3OF9.ttf" } },
|
||||
{ "name": "Edge IE-Mode site list", "verify": { "method": "Hash", "path": "C:\\ProgramData\\Edge\\enterprise-mode-site-list.xml", "value": "16F2A6E45EFA19ED7B1C54B264D6B33597678D3A5303255BC7CEB7E8510C60FC" } }
|
||||
{ "name": "Edge IE-Mode site list", "verify": { "method": "Hash", "path": "C:\\ProgramData\\Edge\\enterprise-mode-site-list.xml", "value": "E13073B2D89E120560AF638F08519E94CC1DC880FFEF5D6A4C7011430E21E4EA" } }
|
||||
],
|
||||
"fmsResolver": [
|
||||
{ "name": "FMS hosts pin", "verify": { "method": "FileGrep", "path": "C:\\Windows\\System32\\drivers\\etc\\hosts", "pattern": "10\\.233\\.112\\.158\\s+wjfms3\\.ae\\.ge\\.com" } }
|
||||
|
||||
@@ -119,8 +119,20 @@ elseif (-not (Test-Path $libSource)) {
|
||||
Write-CMMLog "Shared library not found at $libSource" "ERROR"
|
||||
}
|
||||
else {
|
||||
Write-CMMLog "Running Install-FromManifest against $stagingRoot"
|
||||
& $libSource -ManifestPath $stagingMani -InstallerRoot $stagingRoot -LogFile $logFile
|
||||
$pcType = ''
|
||||
$pcSubType = ''
|
||||
if (Test-Path 'C:\Enrollment\pc-type.txt') { $pcType = (Get-Content 'C:\Enrollment\pc-type.txt' -First 1 -EA 0).Trim() }
|
||||
if (Test-Path 'C:\Enrollment\pc-subtype.txt') { $pcSubType = (Get-Content 'C:\Enrollment\pc-subtype.txt' -First 1 -EA 0).Trim() }
|
||||
|
||||
# PC-DMIS version gating (drop entries whose _CmmVersion does not match the
|
||||
# bay's C:\Enrollment\cmm\version.txt) is now owned by the shared lib
|
||||
# Install-FromManifest.ps1 (>= 2.6). Both this imaging path and the runtime
|
||||
# GE-Enforce path call that lib, so the gate is applied identically in one
|
||||
# place and the two paths cannot drift. We pass the full manifest unfiltered
|
||||
# and let the lib filter per entry. The bug this prevents: enforce lacked
|
||||
# the gate and reinstalled the wrong PC-DMIS version on an already-imaged bay.
|
||||
Write-CMMLog "Running Install-FromManifest against $stagingRoot (PCType=$pcType, PCSubType=$pcSubType)"
|
||||
& $libSource -ManifestPath $stagingMani -InstallerRoot $stagingRoot -LogFile $logFile -PCType $pcType -PCSubType $pcSubType
|
||||
$rc = $LASTEXITCODE
|
||||
Write-CMMLog "Install-FromManifest returned $rc"
|
||||
}
|
||||
@@ -131,15 +143,15 @@ else {
|
||||
# PC-DMIS writes settings, probe configs, and measurement data to its own
|
||||
# install directory at runtime. Without Modify permission for BUILTIN\Users,
|
||||
# non-admin accounts get a UAC elevation prompt on every launch. Granting
|
||||
# the ACL here is the Hexagon-documented approach for non-admin deployment
|
||||
# and avoids the need for a first-run-as-admin (which hits a license dialog
|
||||
# and can't be automated silently).
|
||||
# the ACL here is the Hexagon-documented approach for non-admin deployment.
|
||||
# Step 2.6 below handles the required first-run-as-admin initialization.
|
||||
$pcdmisDirs = @(
|
||||
'C:\Program Files\Hexagon\PC-DMIS 2016.0 64-bit',
|
||||
'C:\Program Files\Hexagon\PC-DMIS 2019 R2 64-bit',
|
||||
'C:\Program Files\Hexagon\PC-DMIS 2026.1 64-bit',
|
||||
'C:\ProgramData\Hexagon',
|
||||
'C:\Program Files (x86)\General Electric\goCMM',
|
||||
'C:\Program Files\DODA'
|
||||
'C:\Apps\DODA'
|
||||
)
|
||||
foreach ($dir in $pcdmisDirs) {
|
||||
if (-not (Test-Path -LiteralPath $dir)) {
|
||||
@@ -164,18 +176,191 @@ foreach ($dir in $pcdmisDirs) {
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Step 3: Clean up the bootstrap staging dir
|
||||
# Step 2.6: First-run-as-admin for each installed PC-DMIS version
|
||||
# ============================================================================
|
||||
# ~2 GB reclaimed. From here on, GE-Enforce takes over from the tsgwp00525
|
||||
# share for ongoing updates.
|
||||
if (Test-Path $stagingRoot) {
|
||||
Write-CMMLog "Deleting bootstrap staging at $stagingRoot"
|
||||
# PC-DMIS performs one-time initialization on first launch (COM registration,
|
||||
# config file creation, internal setup). This must happen with admin rights
|
||||
# before the PPKG locks the machine down. Launch each installed version,
|
||||
# wait for it to initialize, then kill it.
|
||||
$pcdmisExes = @(
|
||||
'C:\Program Files\Hexagon\PC-DMIS 2016.0 64-bit\PCDLRN.exe',
|
||||
'C:\Program Files\Hexagon\PC-DMIS 2019 R2 64-bit\PCDLRN.exe',
|
||||
'C:\Program Files\Hexagon\PC-DMIS 2026.1 64-bit\PCDLRN.exe'
|
||||
)
|
||||
foreach ($exe in $pcdmisExes) {
|
||||
if (-not (Test-Path -LiteralPath $exe)) { continue }
|
||||
$ver = Split-Path (Split-Path $exe -Parent) -Leaf
|
||||
Write-CMMLog "First-run init: launching $ver"
|
||||
try {
|
||||
$proc = Start-Process -FilePath $exe -PassThru -ErrorAction Stop
|
||||
$initTimeout = 45
|
||||
Write-CMMLog " PID $($proc.Id) started, waiting ${initTimeout}s for initialization..."
|
||||
Start-Sleep -Seconds $initTimeout
|
||||
if (-not $proc.HasExited) {
|
||||
$proc.Kill()
|
||||
$proc.WaitForExit(10000)
|
||||
Write-CMMLog " Killed after ${initTimeout}s (first-run init complete)"
|
||||
} else {
|
||||
Write-CMMLog " Exited on its own (exit $($proc.ExitCode))"
|
||||
}
|
||||
} catch {
|
||||
Write-CMMLog " First-run launch failed: $_" 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Step 2.7: Seed goCMM registry path values + grant Users write on the key
|
||||
# ============================================================================
|
||||
# goCMM (.NET x86 WPF app) stores its config in the registry at
|
||||
# HKLM\SOFTWARE\WOW6432Node\General Electric\goCMM (32-bit MSI / 32-bit
|
||||
# process, so install seed and runtime reads both land in the WOW6432Node
|
||||
# view). A capture from a working CMM4 bay shows the two values that matter:
|
||||
# Shared Data Directory = C:\geaofi\ (constant)
|
||||
# Selected Part Group = \\tsgwp00525.wjs.geaerospace.net\SHARED\CMM\CMM4\Spool (per-bay UNC)
|
||||
# i.e. the PER-BAY program path is "Selected Part Group" (a UNC to the
|
||||
# tsgwp00525 SHARED share), and "Shared Data Directory" is the constant local
|
||||
# C:\geaofi\. Both live in HKLM, so a non-admin shopfloor user cannot set
|
||||
# them (nor save a part-group switch) without elevation. So in admin context
|
||||
# we: seed both values, and grant BUILTIN\Users write on the key so runtime
|
||||
# switches succeed without UAC. Mirrors Step 2.5 (install-dir ACL grant).
|
||||
$goCmmKey = 'HKLM:\SOFTWARE\WOW6432Node\General Electric\goCMM'
|
||||
|
||||
# Constant local data dir on every bay.
|
||||
$goCmmDataDir = 'C:\geaofi\'
|
||||
|
||||
# Host that S: maps to. Selected Part Group is stored as a UNC to this host's
|
||||
# SHARED share. Kept in one place so a domain/host change is a one-line edit.
|
||||
$partGroupShareRoot = '\\tsgwp00525.wjs.geaerospace.net\SHARED'
|
||||
|
||||
# Per-bay part group, resolved by resolve-cmm-bay-config.ps1 into
|
||||
# C:\Enrollment\cmm\partgroup.txt as a friendly S:\... path. Convert the S:
|
||||
# drive prefix to the UNC share root. Get-Content + Trim keeps internal spaces
|
||||
# (e.g. CMM8 "Venture CMM8"); the value is passed as a single -Value arg,
|
||||
# never through a command line, so the space cannot split the path.
|
||||
$partGroup = ''
|
||||
$pgFile = 'C:\Enrollment\cmm\partgroup.txt'
|
||||
if (Test-Path -LiteralPath $pgFile) {
|
||||
$raw = (Get-Content -LiteralPath $pgFile -First 1 -EA 0).Trim()
|
||||
if ($raw) {
|
||||
# ^S:\ -> \\host\SHARED\ (case-insensitive). Leave non-S: values as-is.
|
||||
$partGroup = $raw -replace '(?i)^S:\\', "$partGroupShareRoot\"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $goCmmKey)) {
|
||||
Write-CMMLog "goCMM key absent ($goCmmKey) - goCMM not installed or install failed; creating key so the seed/ACL still land" 'WARN'
|
||||
try { New-Item -Path $goCmmKey -Force | Out-Null } catch { Write-CMMLog "Could not create $goCmmKey : $_" 'WARN' }
|
||||
}
|
||||
|
||||
# Shared Data Directory (constant)
|
||||
try {
|
||||
New-ItemProperty -Path $goCmmKey -Name 'Shared Data Directory' -Value $goCmmDataDir -PropertyType String -Force | Out-Null
|
||||
Write-CMMLog "Set goCMM 'Shared Data Directory' = $goCmmDataDir"
|
||||
} catch {
|
||||
Write-CMMLog "Failed to set goCMM 'Shared Data Directory': $_" 'WARN'
|
||||
}
|
||||
|
||||
# Selected Part Group (per-bay UNC)
|
||||
if ($partGroup) {
|
||||
try {
|
||||
New-ItemProperty -Path $goCmmKey -Name 'Selected Part Group' -Value $partGroup -PropertyType String -Force | Out-Null
|
||||
Write-CMMLog "Set goCMM 'Selected Part Group' = $partGroup"
|
||||
} catch {
|
||||
Write-CMMLog "Failed to set goCMM 'Selected Part Group': $_" 'WARN'
|
||||
}
|
||||
} else {
|
||||
Write-CMMLog "No partgroup.txt (bay not in bay-config, or manual CMM ID) - leaving 'Selected Part Group' unset" 'WARN'
|
||||
}
|
||||
|
||||
# Grant BUILTIN\Users ReadKey+WriteKey (WriteKey = SetValue + CreateSubKey).
|
||||
# Registry ACEs use ContainerInherit only (no leaf objects in the registry).
|
||||
if (Test-Path $goCmmKey) {
|
||||
try {
|
||||
$racl = Get-Acl -Path $goCmmKey
|
||||
$rrule = New-Object System.Security.AccessControl.RegistryAccessRule(
|
||||
'BUILTIN\Users',
|
||||
'ReadKey,WriteKey',
|
||||
'ContainerInherit',
|
||||
'None',
|
||||
'Allow'
|
||||
)
|
||||
$racl.AddAccessRule($rrule)
|
||||
Set-Acl -Path $goCmmKey -AclObject $racl -ErrorAction Stop
|
||||
Write-CMMLog "Granted BUILTIN\Users write on $goCmmKey"
|
||||
} catch {
|
||||
Write-CMMLog "Failed to set ACL on $goCmmKey : $_" 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Step 2.8: Restore this bay's PC-DMIS + goCMM settings from its backup
|
||||
# ============================================================================
|
||||
# Runs AFTER app install + first-run-init (so a restored config is not clobbered
|
||||
# by PC-DMIS's first-launch defaults) and BEFORE the Step 3 cleanup (the backup
|
||||
# set lives under $stagingRoot\backups\<cmmid>, deleted by cleanup). Restore-CMM
|
||||
# self-gates: it skips DODA bays and bays with no staged backup, and restores
|
||||
# ONLY the config-version PC-DMIS zip. Same-bay restore -> no CommPort clobber.
|
||||
# Best-effort: Restore-CMM always exits 0, so imaging never fails on a restore.
|
||||
$restoreScript = Join-Path $PSScriptRoot 'scripts\Restore-CMM.ps1'
|
||||
if (Test-Path -LiteralPath $restoreScript) {
|
||||
Write-CMMLog "Running per-bay settings restore (Restore-CMM.ps1)"
|
||||
try {
|
||||
& $restoreScript -BackupRoot (Join-Path $stagingRoot 'backups') *>&1 | ForEach-Object { Write-CMMLog " $_" }
|
||||
Write-CMMLog "Restore-CMM returned $LASTEXITCODE"
|
||||
} catch {
|
||||
Write-CMMLog "Restore-CMM threw (non-fatal): $_" 'WARN'
|
||||
}
|
||||
} else {
|
||||
Write-CMMLog "Restore-CMM.ps1 not found at $restoreScript - skipping settings restore" 'WARN'
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Step 3: Conditional cleanup of the bootstrap staging dir
|
||||
# ============================================================================
|
||||
# Only delete C:\CMM-Install when EVERY manifest entry detected as installed.
|
||||
# A vendor installer that forces an unplanned mid-install reboot would
|
||||
# otherwise leave us with no recovery path on the self-resumed re-run
|
||||
# (Run-ShopfloorSetup's new RunOnce would fire, but Step 2 would log
|
||||
# "$stagingRoot does not exist" and bail). Leaving the staging dir in
|
||||
# place until the manifest fully converges means a re-fire just re-runs
|
||||
# the partial installs and completes.
|
||||
$allDetected = $true
|
||||
if (Test-Path $stagingMani) {
|
||||
try {
|
||||
$cfg = Get-Content $stagingMani -Raw | ConvertFrom-Json
|
||||
foreach ($app in $cfg.Applications) {
|
||||
if (-not $app.DetectionMethod -or -not $app.DetectionPath) { continue }
|
||||
# Honor PCTypes filter when checking detection.
|
||||
if ($app.PCTypes -and $app.PCTypes.Count -gt 0) {
|
||||
$myNames = @($pcType)
|
||||
if ($pcSubType) { $myNames += "$pcType-$pcSubType" }
|
||||
$match = $false
|
||||
foreach ($t in $app.PCTypes) { if ($myNames -contains $t) { $match = $true; break } }
|
||||
if (-not $match) { continue } # not applicable to this PC, skip detection
|
||||
}
|
||||
if (-not (Test-Path $app.DetectionPath)) { $allDetected = $false; Write-CMMLog "Not installed: $($app.Name)"; break }
|
||||
if ($app.DetectionName) {
|
||||
$val = (Get-ItemProperty -Path $app.DetectionPath -Name $app.DetectionName -EA 0).$($app.DetectionName)
|
||||
if (-not $val) { $allDetected = $false; Write-CMMLog "Not installed (no value): $($app.Name)"; break }
|
||||
if ($app.DetectionValue -and $val -ne $app.DetectionValue) { $allDetected = $false; Write-CMMLog "Wrong version: $($app.Name) got $val expected $($app.DetectionValue)"; break }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-CMMLog "Could not parse manifest for cleanup-gate check: $_" 'WARN'
|
||||
$allDetected = $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($allDetected -and (Test-Path $stagingRoot)) {
|
||||
Write-CMMLog "All manifest entries installed. Deleting bootstrap staging at $stagingRoot"
|
||||
try {
|
||||
Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction Stop
|
||||
Write-CMMLog "Bootstrap cleanup complete"
|
||||
} catch {
|
||||
Write-CMMLog "Failed to delete $stagingRoot : $_" "WARN"
|
||||
}
|
||||
} elseif (Test-Path $stagingRoot) {
|
||||
Write-CMMLog "Bootstrap staging retained at $stagingRoot (not all entries installed yet - will retry on next self-resumed run)"
|
||||
}
|
||||
|
||||
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
|
||||
|
||||
48
playbook/shopfloor-setup/gea-shopfloor-cmm/Install-DODA.ps1
Normal file
@@ -0,0 +1,48 @@
|
||||
# Install-DODA.ps1 - Extract DODA zip to C:\Apps\DODA\.
|
||||
#
|
||||
# Called by Install-FromManifest as a Type=PS1 entry. The zip is staged
|
||||
# alongside this script in C:\CMM-Install\ by startnet.cmd.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$installDir = 'C:\Apps\DODA'
|
||||
$zipPattern = 'doda_build*.zip'
|
||||
$stagingRoot = Split-Path $PSScriptRoot -ErrorAction SilentlyContinue
|
||||
if (-not $stagingRoot) { $stagingRoot = 'C:\CMM-Install' }
|
||||
|
||||
$zip = Get-ChildItem -Path $stagingRoot -Filter $zipPattern -File -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $zip) {
|
||||
Write-Host "DODA zip not found in $stagingRoot (pattern: $zipPattern)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $installDir)) {
|
||||
New-Item -Path $installDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "Extracting $($zip.Name) to $installDir..."
|
||||
try {
|
||||
Expand-Archive -LiteralPath $zip.FullName -DestinationPath $installDir -Force -ErrorAction Stop
|
||||
Write-Host "DODA extracted to $installDir"
|
||||
} catch {
|
||||
Write-Host "ERROR: Extract failed - $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# MergeFiles.exe (cmm-utilities toolchain) reads C:\Apps\DODA\PreProcess\ as
|
||||
# its working dir. The DODA zip extracts flat without it, so create it here -
|
||||
# a missing PreProcess dir is the known cause of MergeFiles.GetDoDAFolder
|
||||
# throwing DirectoryNotFoundException (see cmm-utilities dotNET event.txt).
|
||||
$preProcess = Join-Path $installDir 'PreProcess'
|
||||
if (-not (Test-Path $preProcess)) {
|
||||
New-Item -Path $preProcess -ItemType Directory -Force | Out-Null
|
||||
Write-Host "Created $preProcess"
|
||||
}
|
||||
|
||||
if (Test-Path (Join-Path $installDir 'DovetailAnalysis.exe')) {
|
||||
Write-Host "DovetailAnalysis.exe verified present"
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "ERROR: DovetailAnalysis.exe not found after extract"
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<#
|
||||
Install-PCDMISPDFConverter.ps1
|
||||
|
||||
Installs the PC-DMIS PDF converter (Amyuni Document Converter 500, printer name
|
||||
"PC-DMIS 50 Converter").
|
||||
|
||||
Why this exists: our CMM image installs PC-DMIS from a patched STANDALONE MSI,
|
||||
bypassing Hexagon's Burn bundle. The Amyuni PDF converter is NOT a custom action
|
||||
in the main MSI (INSTALLPDFCONVERTER is a bundle property the MSI never reads).
|
||||
The bundle would have run the Amyuni install as a separate chained step - which
|
||||
we skip. The MSI does lay the installer down on disk at:
|
||||
C:\Program Files\Hexagon\PC-DMIS <ver> 64-bit\PDFDriverInstallFiles\BatFileInstallPDF50.zip
|
||||
but nothing ever executes it. This script does.
|
||||
|
||||
The zip ships InstallPDF50.exe + the Amyuni driver (amyuni.inf, acfpdf*.dll,
|
||||
cdintf*.dll, atpdf500.cat) + InstallPDF50.bat. We do NOT run the .bat (it ends in
|
||||
`pause` and hangs under /qn) - we parse its InstallPDF50.exe invocation (printer
|
||||
name + Wilcox licensee + license code) and run that directly from the extracted
|
||||
folder so the sibling DLLs resolve.
|
||||
|
||||
The converter is ONE system printer shared by every PC-DMIS version, so we install
|
||||
from the first PDFDriverInstallFiles we find and stop once the printer exists.
|
||||
|
||||
Idempotent: if the "PC-DMIS 50 Converter" printer already exists, exits 0 without
|
||||
reinstalling. Run as administrator / SYSTEM (driver install needs it).
|
||||
|
||||
Exit: 0 = printer present (installed or already there), 1 = failed.
|
||||
#>
|
||||
param(
|
||||
[string]$PrinterName = 'PC-DMIS 50 Converter',
|
||||
[string]$OutDir = 'C:\Logs\CMM'
|
||||
)
|
||||
$ErrorActionPreference = 'Continue'
|
||||
New-Item -ItemType Directory -Path $OutDir -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$log = Join-Path $OutDir "pdfconverter-$ts.log"
|
||||
function Log($m){ $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $m"; Write-Host $line; $line | Out-File -FilePath $log -Append -Encoding ascii }
|
||||
|
||||
function Test-ConverterPresent {
|
||||
# Get-Printer is the authoritative check. Fall back to the printer-name key
|
||||
# in the registry for old hosts where the Printing cmdlets are absent.
|
||||
try { if (Get-Printer -Name $PrinterName -ErrorAction SilentlyContinue) { return $true } } catch {}
|
||||
$k = 'HKLM:\SYSTEM\CurrentControlSet\Control\Print\Printers\' + $PrinterName
|
||||
return (Test-Path $k)
|
||||
}
|
||||
|
||||
Log "==== PC-DMIS PDF converter install on $env:COMPUTERNAME ===="
|
||||
|
||||
if (Test-ConverterPresent) {
|
||||
Log "Printer '$PrinterName' already present - nothing to do."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Find the Amyuni installer the MSI laid down. PC-DMIS 2016 (vendor Wai) and
|
||||
# 2019/2026 (vendor Hexagon) all install under Program Files\Hexagon, but scan
|
||||
# both Hexagon and Wai trees to be safe.
|
||||
$zips = @()
|
||||
foreach ($root in @("$env:ProgramFiles\Hexagon","$env:ProgramFiles\Wai","${env:ProgramFiles(x86)}\Hexagon","${env:ProgramFiles(x86)}\Wai")) {
|
||||
if (-not (Test-Path $root)) { continue }
|
||||
$zips += Get-ChildItem -Path $root -Recurse -Filter 'BatFileInstallPDF50.zip' -ErrorAction SilentlyContinue
|
||||
}
|
||||
$zips = $zips | Sort-Object FullName -Unique
|
||||
if (-not $zips) {
|
||||
Log "ERROR: no BatFileInstallPDF50.zip found under any PC-DMIS install dir."
|
||||
Log " PC-DMIS may not be installed yet, or PDFDriverInstallFiles is missing."
|
||||
exit 1
|
||||
}
|
||||
Log ("Found {0} Amyuni installer zip(s):" -f $zips.Count)
|
||||
$zips | ForEach-Object { Log " $($_.FullName)" }
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
||||
foreach ($zip in $zips) {
|
||||
$stage = Join-Path $env:TEMP ("amyuni-pdf-" + $ts + "-" + [Guid]::NewGuid().ToString('N').Substring(0,6))
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $stage -Force | Out-Null
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($zip.FullName, $stage)
|
||||
Log "Extracted $($zip.Name) -> $stage"
|
||||
|
||||
$exe = Join-Path $stage 'InstallPDF50.exe'
|
||||
$bat = Join-Path $stage 'InstallPDF50.bat'
|
||||
if (-not (Test-Path $exe)) { Log " no InstallPDF50.exe in zip - skipping"; continue }
|
||||
|
||||
# Parse InstallPDF50.bat for the exact InstallPDF50.exe arguments (printer
|
||||
# name + Wilcox licensee + license code). License code can differ per
|
||||
# version, so read it rather than hardcode. Strip the leading exe token.
|
||||
$args = $null
|
||||
if (Test-Path $bat) {
|
||||
$cmd = (Get-Content $bat | Where-Object { $_ -match 'InstallPDF50\.exe' } | Select-Object -First 1)
|
||||
if ($cmd) { $args = ($cmd -replace '(?i)^\s*[^"]*InstallPDF50\.exe\s*','').Trim() }
|
||||
}
|
||||
if (-not $args) {
|
||||
# Fallback to the known-good invocation if the bat is missing/odd.
|
||||
$args = '"' + $PrinterName + '" -n "Wilcox Associates, Inc."'
|
||||
Log " WARN: could not parse args from bat - using fallback (no license code): $args"
|
||||
} else {
|
||||
Log " parsed install args from bat"
|
||||
}
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $exe
|
||||
$psi.Arguments = $args
|
||||
$psi.WorkingDirectory = $stage # sibling DLLs (cdintf64.dll, acfpdf*, amyuni.inf, atpdf500.cat) must resolve
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
||||
Log " running: InstallPDF50.exe $args (cwd=$stage)"
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
# InstallPDF50.exe creates the printer in a few seconds but then hangs
|
||||
# (does not self-exit, same as Hexagon's Burn bundle). Poll for the
|
||||
# printer instead of blocking; once it appears, kill the hung exe and
|
||||
# move on. Hard cap at 90s as a backstop for a genuinely stuck install.
|
||||
$deadline = (Get-Date).AddSeconds(90)
|
||||
$appeared = $false
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if ($proc.HasExited) { Log " InstallPDF50.exe exited on its own (code $($proc.ExitCode))"; break }
|
||||
if (Test-ConverterPresent) { $appeared = $true; break }
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
if (-not $proc.HasExited) {
|
||||
Log (" printer {0} - killing InstallPDF50.exe (it does not self-exit)" -f $(if ($appeared) { 'present' } else { 'NOT present after 90s' }))
|
||||
try { $proc.Kill() } catch {}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
if (Test-ConverterPresent) {
|
||||
Log "SUCCESS: printer '$PrinterName' is now present."
|
||||
Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue
|
||||
exit 0
|
||||
}
|
||||
Log " printer not present after this attempt - trying next zip if any."
|
||||
} catch {
|
||||
Log " ERROR during install from $($zip.Name): $($_.Exception.Message)"
|
||||
} finally {
|
||||
Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-ConverterPresent) { Log "Printer '$PrinterName' present."; exit 0 }
|
||||
Log "ERROR: printer '$PrinterName' still not present after all attempts."
|
||||
exit 1
|
||||
@@ -0,0 +1,13 @@
|
||||
cmm_id,pcdmis_version,doda,part_group
|
||||
CMM1,2019,no,S:\CMM\CMM1\HPTCMM1
|
||||
CMM2,2019,no,S:\CMM\CMM2\HPT
|
||||
CMM3,2019,no,S:\CMM\CMM3\VENTURE_CMM3
|
||||
CMM4,2016,no,S:\CMM\CMM4\Spool
|
||||
CMM5,2019,no,S:\CMM\CMM5\BLISKCMM5
|
||||
CMM6,2019,no,S:\CMM\CMM6\BLISKCMM6
|
||||
CMM7,2019,no,S:\CMM\CMM7\VENTURE_CMM7
|
||||
CMM8,2019,no,S:\CMM\CMM8\Venture CMM8
|
||||
CMM9,2019,no,S:\CMM\CMM9\BLISKCMM9
|
||||
CMM10,2016,no,S:\CMM\CMM10\Spool
|
||||
CMM11,2026,no,S:\CMM\CMM11\Spool
|
||||
CMM12,2026,no,S:\CMM\CMM12\Spool
|
||||
|
@@ -1,31 +1,44 @@
|
||||
{
|
||||
"Version": "2.0",
|
||||
"_comment": "CMM machine-app manifest, imaging-time only. Consumed by 09-Setup-CMM.ps1 reading from C:\\CMM-Install\\. Ongoing enforcement is handled separately by GE-Enforce reading cmm/manifest.json from the tsgwp00525 share. Option 3 (patched-MSI) install strategy: we bypass Hexagon's Burn bundle entirely for PC-DMIS 2016 and 2019 R2. The main PC-DMIS MSIs have been patched via COM SQL UPDATE (msibuild-style) to force the Condition column to '0' for two custom actions: ProcessLicensingFromBundle (which would otherwise spin for ~13 minutes trying to activate against licensing.wilcoxassoc.com with empty credentials) and IsLicenseDateValid (which would fail the install with 'no valid license'). With both CAs disabled, the MSI installs cleanly with no license present; PCDLRN.exe installs and loads at runtime and the tech activates a real license via clmadmin.exe after imaging. VS 2010/2012 x64 runtime prereqs are handled by the shared preinstall.json VC++ x64 entries (which run before this manifest). CLM Tools 1.5/1.7 chained MSIs from the original bundles are intentionally SKIPPED; CLM 1.8.73 standalone provides the admin + runtime interfaces. Protect Viewer is kept because it's useful alongside PC-DMIS 2019 R2.",
|
||||
"_comment": "CMM machine-app manifest, imaging-time only. Consumed by 09-Setup-CMM.ps1 reading from C:\\CMM-Install\\. 09-Setup reads C:\\Enrollment\\cmm\\version.txt (written by resolve-cmm-bay-config.ps1) and filters PC-DMIS entries by the _CmmVersion tag before passing to Install-FromManifest. Entries without _CmmVersion are always installed (CLM, goCMM, DODA). Ongoing enforcement is handled separately by GE-Enforce reading cmm/manifest.json from the tsgwp00525 share. Patched-MSI install strategy: we bypass Hexagon's Burn bundle entirely. The main PC-DMIS MSIs have been patched via COM SQL UPDATE to force the Condition column to '0' for licensing custom actions (ProcessLicensingFromBundle, IsLicenseDateValid, IsLicenseExpired, IsLmsLicenseServerConnectionError, IsLmsLicenseError). With CAs disabled, the MSI installs cleanly with no license present; the tech activates a real license via clmadmin.exe after imaging.",
|
||||
"Applications": [
|
||||
{
|
||||
"_comment": "PC-DMIS 2016 main MSI (PATCHED). ProcessLicensingFromBundle + IsLicenseDateValid custom actions have been pre-disabled by SQL UPDATE of InstallExecuteSequence.Condition to '0'. Install args: INSTALLFOLDER/APPLICATIONFOLDER paths have embedded double quotes to survive the runner's command-line concatenation when the path contains spaces. USINGWPFINSTALLER=1 mirrors the Burn bundle default and ensures HandleLicenseChoice CA (seq 783) stays skipped. HEIP=0 disables Hexagon telemetry. INSTALLPDFCONVERTER=0 skips the Nitro PDF converter. The patched MSI has a HashMismatch signature, which is expected and accepted by Windows Installer in /qn mode.",
|
||||
"Name": "PC-DMIS 2016",
|
||||
"_CmmVersion": "2016",
|
||||
"Installer": "pcdmis2016-main-patched.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart ALLUSERS=1 MSIFASTINSTALL=7 INSTALLFOLDER=\"C:\\Program Files\\Hexagon\\PC-DMIS 2016.0 64-bit\" APPLICATIONFOLDER=\"C:\\Program Files\\Hexagon\\PC-DMIS 2016.0 64-bit\" USINGWPFINSTALLER=1 HEIP=0 INSTALLPDFCONVERTER=0 REBOOT=ReallySuppress LICENSETYPE=LMSEntitlement",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{5389B196-81F0-44A9-A073-4C1D72041F09}",
|
||||
"DetectionName": "DisplayVersion",
|
||||
"DetectionValue": "11.0.1179.0"
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{5389B196-81F0-44A9-A073-4C1D72041F09}"
|
||||
},
|
||||
{
|
||||
"_comment": "PC-DMIS 2019 R2 main MSI (PATCHED). Same patch strategy as 2016. Adds INSTALLOFFLINEHELP=0 (saves ~1.5 GB) and INSTALLUNIVERSALUPDATER=0 (disables Hexagon's auto-updater which we do not want on air-gapped shopfloor machines). Protect Viewer is a separate MSI installed next.",
|
||||
"Name": "PC-DMIS 2019 R2",
|
||||
"_CmmVersion": "2019",
|
||||
"Installer": "pcdmis2019-main-patched.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart ALLUSERS=1 MSIFASTINSTALL=7 INSTALLFOLDER=\"C:\\Program Files\\Hexagon\\PC-DMIS 2019 R2 64-bit\" APPLICATIONFOLDER=\"C:\\Program Files\\Hexagon\\PC-DMIS 2019 R2 64-bit\" USINGWPFINSTALLER=1 HEIP=0 INSTALLPDFCONVERTER=0 INSTALLOFFLINEHELP=0 INSTALLUNIVERSALUPDATER=0 REBOOT=ReallySuppress LICENSETYPE=LMSEntitlement",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{49DBE7F9-228A-4E66-8BB5-DB5A446DCAE7}",
|
||||
"DetectionName": "DisplayVersion",
|
||||
"DetectionValue": "14.2.728.0"
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{49DBE7F9-228A-4E66-8BB5-DB5A446DCAE7}"
|
||||
},
|
||||
{
|
||||
"_comment": "Protect Viewer - companion tool bundled with PC-DMIS 2019 R2. Separate MSI with no license check of its own. Dark-extracted from the 2019 R2 Burn bundle and shipped as-is.",
|
||||
"Name": "PC-DMIS 2026.1",
|
||||
"_CmmVersion": "2026",
|
||||
"Installer": "pcdmis2026-main-patched.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart ALLUSERS=1 MSIFASTINSTALL=7 INSTALLFOLDER=\"C:\\Program Files\\Hexagon\\PC-DMIS 2026.1 64-bit\" APPLICATIONFOLDER=\"C:\\Program Files\\Hexagon\\PC-DMIS 2026.1 64-bit\" USINGWPFINSTALLER=1 HEIP=0 INSTALLPDFCONVERTER=0 INSTALLOFFLINEHELP=0 INSTALLUNIVERSALUPDATER=0 REBOOT=ReallySuppress LICENSETYPE=LMSEntitlement",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{81BACE1B-FB08-4DCF-8100-79911AD3EC1E}"
|
||||
},
|
||||
{
|
||||
"_comment": "PC-DMIS PDF converter (Amyuni Document Converter 500; system printer 'PC-DMIS 50 Converter'). NOT installed by the PC-DMIS MSI: INSTALLPDFCONVERTER is a Burn-bundle property the standalone MSI never reads (0 of 153 custom actions reference it), and our patched-MSI strategy bypasses the bundle that would have chained the Amyuni install. The MSI only lays the installer down at <installdir>\\PDFDriverInstallFiles\\BatFileInstallPDF50.zip; this entry runs it (InstallPDF50.exe directly - the shipped .bat ends in `pause` and hangs under /qn). MUST stay AFTER the PC-DMIS entries so the files exist on disk. One shared printer covers every installed version; script is idempotent and scans Program Files\\Hexagon (and Wai). MarkerFile keeps it one-shot at imaging.",
|
||||
"Name": "PC-DMIS PDF Converter",
|
||||
"Type": "PS1",
|
||||
"Script": "Install-PCDMISPDFConverter.ps1",
|
||||
"DetectionMethod": "MarkerFile",
|
||||
"DetectionPath": "C:\\Logs\\CMM\\pdfconverter.installed"
|
||||
},
|
||||
{
|
||||
"_comment": "Protect Viewer - companion tool. Install for all versions.",
|
||||
"Name": "Protect Viewer",
|
||||
"Installer": "ProtectViewer.msi",
|
||||
"Type": "MSI",
|
||||
@@ -34,28 +47,33 @@
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7DE6B8AF-F580-4CDE-845F-FBE46C1FCF69}"
|
||||
},
|
||||
{
|
||||
"_comment": "CLM 1.8.73 standalone bundle - provides clmadmin.exe and the runtime licensing libraries that both PC-DMIS 2016 and 2019 R2 use. Unlike the PC-DMIS bundles, CLM's WiX Burn bundle has no install-time license check (it IS the license tool), so we run it via its original EXE with no patches. The tech uses clmadmin.exe to activate a real license post-imaging, which unlocks both PC-DMIS versions.",
|
||||
"_comment": "CLM 1.8.73 standalone - provides clmadmin.exe for license activation. Install for all versions.",
|
||||
"Name": "CLM 1.8.73",
|
||||
"Installer": "CLM_1.8.73.0_x64.exe",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "/quiet /norestart /log \"C:\\Logs\\CMM\\CLM.log\"",
|
||||
"LogFile": "C:\\Logs\\CMM\\CLM.log",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{a55fecde-0776-474e-a5b3-d57ea93d6a9f}",
|
||||
"DetectionName": "DisplayVersion",
|
||||
"DetectionValue": "1.8.73.0"
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{a55fecde-0776-474e-a5b3-d57ea93d6a9f}"
|
||||
},
|
||||
{
|
||||
"_comment": "goCMM 1.1.6718 - Hexagon's CMM job launcher utility. No license check. Unpatched bundle EXE runs as-is.",
|
||||
"_comment": "goCMM - Hexagon CMM job launcher. Install for all versions.",
|
||||
"Name": "goCMM",
|
||||
"Installer": "goCMM_1.1.6718.31289.exe",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "/quiet /norestart /log \"C:\\Logs\\CMM\\goCMM.log\"",
|
||||
"LogFile": "C:\\Logs\\CMM\\goCMM.log",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{94f02b85-bbca-422e-9b8b-0c16a769eced}",
|
||||
"DetectionName": "DisplayVersion",
|
||||
"DetectionValue": "1.1.6710.18601"
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{94f02b85-bbca-422e-9b8b-0c16a769eced}"
|
||||
},
|
||||
{
|
||||
"_comment": "DODA - Dovetail Digital Analysis. Deployed as a flat file extract to C:\\Apps\\DODA\\. Only installed when doda.txt=yes (pc-subtype.txt=doda gates this via PCTypes).",
|
||||
"Name": "DODA",
|
||||
"PCTypes": ["cmm-doda"],
|
||||
"Type": "PS1",
|
||||
"Script": "Install-DODA.ps1",
|
||||
"DetectionMethod": "File",
|
||||
"DetectionPath": "C:\\Apps\\DODA\\DovetailAnalysis.exe"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# resolve-cmm-bay-config.ps1 - Resolve CMM bay config from cmm-bay-config.csv.
|
||||
#
|
||||
# Called by startnet.cmd after the bay picker. Reads the CSV from the PXE
|
||||
# enrollment share, looks up the selected CMM ID, and writes:
|
||||
# W:\Enrollment\cmm\version.txt (e.g. "2019")
|
||||
# W:\Enrollment\cmm\doda.txt (e.g. "yes" or "no")
|
||||
#
|
||||
# 09-Setup-CMM.ps1 reads these at install time to gate which PC-DMIS
|
||||
# version gets installed and whether DODA is deployed.
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ConfigPath,
|
||||
[Parameter(Mandatory=$true)][string]$CmmId,
|
||||
[Parameter(Mandatory=$true)][string]$OutDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
||||
Write-Host "ERROR: CSV not found at $ConfigPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
$bays = Import-Csv -LiteralPath $ConfigPath
|
||||
} catch {
|
||||
Write-Host "ERROR: Failed to parse $ConfigPath - $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$match = $bays | Where-Object { $_.cmm_id -ieq $CmmId }
|
||||
if (-not $match) {
|
||||
Write-Host "WARNING: $CmmId not found in bay-config. No version/doda resolution."
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not (Test-Path $OutDir)) {
|
||||
New-Item -Path $OutDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$version = $match.pcdmis_version.Trim()
|
||||
$doda = $match.doda.Trim().ToLower()
|
||||
# part_group is the goCMM "Selected Part Group" path. It may legitimately
|
||||
# contain spaces (e.g. CMM8 "Venture CMM8"); Trim() strips only leading/
|
||||
# trailing whitespace, never internal spaces. Stored in the friendly S:\
|
||||
# form; 09-Setup-CMM converts it to the tsgwp00525 UNC at apply time.
|
||||
$partGroup = ''
|
||||
if ($match.PSObject.Properties['part_group'] -and $match.part_group) {
|
||||
$partGroup = $match.part_group.Trim()
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText((Join-Path $OutDir 'version.txt'), $version)
|
||||
[System.IO.File]::WriteAllText((Join-Path $OutDir 'doda.txt'), $doda)
|
||||
# cmmid.txt: the resolved CMM id, so 09-Setup-CMM can locate this bay's staged
|
||||
# backup set (installers-post\cmm\backups\<cmm_id>\) for restore-by-machine-#.
|
||||
[System.IO.File]::WriteAllText((Join-Path $OutDir 'cmmid.txt'), $CmmId)
|
||||
if ($partGroup) {
|
||||
[System.IO.File]::WriteAllText((Join-Path $OutDir 'partgroup.txt'), $partGroup)
|
||||
}
|
||||
|
||||
Write-Host "Resolved $CmmId -> PC-DMIS $version, DODA=$doda, PartGroup=$(if ($partGroup) { $partGroup } else { '(none)' })"
|
||||
Write-Host " version.txt -> $OutDir\version.txt"
|
||||
Write-Host " doda.txt -> $OutDir\doda.txt"
|
||||
if ($partGroup) { Write-Host " partgroup.txt -> $OutDir\partgroup.txt" }
|
||||
exit 0
|
||||
@@ -0,0 +1,19 @@
|
||||
@echo off
|
||||
REM Backup-CMM.bat - ONE backup for a whole CMM bay (goCMM + PC-DMIS, all versions).
|
||||
REM
|
||||
REM Run AS ADMINISTRATOR on the live CMM. Do NOT run on DODA bays.
|
||||
REM
|
||||
REM Backup-CMM.bat -CmmId CMM3 (names the folder by CMM #)
|
||||
REM Backup-CMM.bat (uses the computer name)
|
||||
REM
|
||||
REM Output: C:\Logs\CMM\cmm-backup\<CmmId>\ (gocmm + pcdmis zips + index)
|
||||
REM Copy that whole folder to the PXE staging area for restore-by-machine-#.
|
||||
|
||||
setlocal
|
||||
set "HERE=%~dp0"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%HERE%Backup-CMM.ps1" %*
|
||||
echo.
|
||||
echo ---------------------------------------------------------------
|
||||
echo Backup folder is under C:\Logs\CMM\cmm-backup\
|
||||
echo ---------------------------------------------------------------
|
||||
pause
|
||||
@@ -0,0 +1,82 @@
|
||||
<#
|
||||
Backup-CMM.ps1
|
||||
|
||||
ONE backup for a whole CMM bay - runs both:
|
||||
- Backup-goCMMSettings.ps1 (HKLM goCMM key + C:\geaofi, minus LocalProgramCopies/logs)
|
||||
- Backup-PCDMISSettings.ps1 (PC-DMIS registry + data/probe/cal + interfac.dll,
|
||||
every installed version; Homepage state excluded)
|
||||
|
||||
All zips land together in one per-CMM folder so they can be staged on PXE and
|
||||
restored by CMM machine-# at imaging.
|
||||
|
||||
Run as ADMINISTRATOR on the live CMM. Do NOT run on DODA bays (handled separately).
|
||||
|
||||
Output (default): S:\2 WJ Scans Record Retention\backup\cmm\<CmmId>\
|
||||
gocmm_backup_<PC>_<ts>.zip
|
||||
pcdmis_backup_<PC>_<ver>_<ts>.zip (one per PC-DMIS version)
|
||||
cmm-backup-index.json
|
||||
If S: is not mapped/reachable, falls back to C:\Logs\CMM\cmm-backup\<CmmId>\.
|
||||
|
||||
Params:
|
||||
-CmmId <id> e.g. CMM3 - names the folder + index. If omitted, the script
|
||||
PROMPTS for it.
|
||||
-OutDir <p> base output folder (default the S: record-retention path above)
|
||||
#>
|
||||
param(
|
||||
[string]$CmmId,
|
||||
[string]$OutDir = 'S:\2 WJ Scans Record Retention\backup\cmm'
|
||||
)
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
|
||||
# Prompt for the CMM number if not passed. It names the backup folder, so we
|
||||
# need a real value - loop until non-empty.
|
||||
if (-not $CmmId) {
|
||||
while (-not $CmmId) {
|
||||
$CmmId = (Read-Host "Enter the CMM number for this bay (e.g. CMM3)").Trim()
|
||||
if (-not $CmmId) { Write-Host " CMM number is required." -ForegroundColor Yellow }
|
||||
}
|
||||
}
|
||||
|
||||
# Default OutDir is on S: (record retention). If S: is not mapped or that base
|
||||
# path is unreachable, fall back to a local folder so the backup still runs -
|
||||
# the operator can copy it up to S: afterward.
|
||||
$fallback = 'C:\Logs\CMM\cmm-backup'
|
||||
$baseRoot = Split-Path -Qualifier $OutDir -ErrorAction SilentlyContinue
|
||||
if ($baseRoot -and -not (Test-Path "$baseRoot\")) {
|
||||
Write-Host "WARNING: $baseRoot is not reachable (S: not mapped?). Falling back to $fallback" -ForegroundColor Yellow
|
||||
$OutDir = $fallback
|
||||
}
|
||||
$dest = Join-Path $OutDir $CmmId
|
||||
New-Item -ItemType Directory -Path $dest -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
$log = Join-Path $dest "cmm-backup-$ts.log"
|
||||
function Log($m){ Write-Host $m; $m | Out-File -FilePath $log -Append }
|
||||
|
||||
Log "================ CMM backup: $CmmId on $env:COMPUTERNAME at $(Get-Date) ================"
|
||||
|
||||
foreach ($s in 'Backup-goCMMSettings.ps1','Backup-PCDMISSettings.ps1') {
|
||||
$p = Join-Path $here $s
|
||||
if (-not (Test-Path $p)) { Log "MISSING sibling script: $p - skipping"; continue }
|
||||
Log "---- running $s ----"
|
||||
try { & $p -OutDir $dest *>&1 | ForEach-Object { Log " $_" } }
|
||||
catch { Log " ERROR in $s : $($_.Exception.Message)" }
|
||||
}
|
||||
|
||||
# index of what we captured
|
||||
$zips = Get-ChildItem $dest -Filter '*.zip' -File -ErrorAction SilentlyContinue
|
||||
[pscustomobject]@{
|
||||
CmmId = $CmmId
|
||||
Computer = $env:COMPUTERNAME
|
||||
Timestamp = (Get-Date -Format o)
|
||||
goCMM = @($zips | Where-Object { $_.Name -like 'gocmm_backup_*' } | Select-Object -Expand Name)
|
||||
PCDMIS = @($zips | Where-Object { $_.Name -like 'pcdmis_backup_*' } | Select-Object -Expand Name)
|
||||
} | ConvertTo-Json | Out-File (Join-Path $dest 'cmm-backup-index.json') -Encoding ascii
|
||||
|
||||
Log "================ DONE ================"
|
||||
Log "Folder: $dest"
|
||||
$zips | ForEach-Object { Log (" {0} ({1} KB)" -f $_.Name, [math]::Round($_.Length/1KB)) }
|
||||
Write-Host ""
|
||||
Write-Host "CMM backup for $CmmId complete:" -ForegroundColor Green
|
||||
Write-Host " $dest"
|
||||
$zips | ForEach-Object { Write-Host " $($_.Name)" }
|
||||
@@ -0,0 +1,19 @@
|
||||
@echo off
|
||||
REM Backup-PCDMISSettings.bat - capture PC-DMIS settings / probes / calibration
|
||||
REM for every installed version (2016 / 2019 / 2026), headless.
|
||||
REM
|
||||
REM Run on a LIVE bay AS ADMINISTRATOR (reg export of HKLM + read of install dir).
|
||||
REM
|
||||
REM Backup-PCDMISSettings.bat (all detected versions)
|
||||
REM Backup-PCDMISSettings.bat -Version 2026.1 (one version)
|
||||
REM
|
||||
REM Output: C:\Logs\CMM\pcdmis-backup\pcdmis_backup_<PC>_<ver>_<ts>.zip
|
||||
|
||||
setlocal
|
||||
set "HERE=%~dp0"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%HERE%Backup-PCDMISSettings.ps1" %*
|
||||
echo.
|
||||
echo ---------------------------------------------------------------
|
||||
echo Backups under C:\Logs\CMM\pcdmis-backup\ (one zip per version)
|
||||
echo ---------------------------------------------------------------
|
||||
pause
|
||||
@@ -0,0 +1,180 @@
|
||||
<#
|
||||
Backup-PCDMISSettings.ps1
|
||||
|
||||
Manual PC-DMIS settings/probe/calibration backup - replicates what the Settings
|
||||
Editor captures (registry + data/probe files), but headless and scriptable,
|
||||
because SettingsEditor.exe /b only works through the GUI and fails non-interactively.
|
||||
|
||||
Works across PC-DMIS 2016 / 2019 / 2026 (auto-detects version + vendor hive:
|
||||
'Hexagon' on 2019/2026, 'Wai' on older 2016 builds).
|
||||
|
||||
Captures, per installed version, into one zip:
|
||||
- registry: HKLM + HKCU <vendor>\PC-DMIS\<ver> (settings, probe search paths)
|
||||
- install-dir probe/cal master files: PROBE.DAT, usrprobe.dat, comp.dat,
|
||||
tool.dat, *.prb (top level + Configuration\)
|
||||
- the per-version data folders under ProgramData, Public\Documents, and
|
||||
per-user AppData (Roaming + Local)
|
||||
|
||||
Run as administrator on a LIVE bay. One zip per installed version.
|
||||
Output: C:\Logs\CMM\pcdmis-backup\pcdmis_backup_<PC>_<ver>_<ts>.zip
|
||||
|
||||
Params:
|
||||
-Version <x> back up only this version (e.g. 2026.1). Default: every detected.
|
||||
-OutDir <p> output folder (default C:\Logs\CMM\pcdmis-backup)
|
||||
#>
|
||||
param(
|
||||
[string]$Version,
|
||||
[string]$OutDir = 'C:\Logs\CMM\pcdmis-backup'
|
||||
)
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
New-Item -ItemType Directory -Path $OutDir -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
$log = Join-Path $OutDir "pcdmis-backup-$ts.log"
|
||||
function Log($m){ Write-Host $m; $m | Out-File -FilePath $log -Append }
|
||||
|
||||
# --- discover installed PC-DMIS versions: vendor (Hexagon/Wai), version, install dir ---
|
||||
function Get-PCDMISInstalls {
|
||||
$found = @()
|
||||
$roots = @(
|
||||
@{ Path='HKLM:\SOFTWARE\WOW6432Node\Hexagon\PC-DMIS'; Vendor='Hexagon' },
|
||||
@{ Path='HKLM:\SOFTWARE\Hexagon\PC-DMIS'; Vendor='Hexagon' },
|
||||
@{ Path='HKLM:\SOFTWARE\WOW6432Node\Wai\PC-DMIS'; Vendor='Wai' },
|
||||
@{ Path='HKLM:\SOFTWARE\Wai\PC-DMIS'; Vendor='Wai' }
|
||||
)
|
||||
foreach ($r in $roots) {
|
||||
if (-not (Test-Path $r.Path)) { continue }
|
||||
Get-ChildItem $r.Path -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match '^\d' } | ForEach-Object {
|
||||
$p = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
|
||||
$ver = $_.PSChildName
|
||||
$instDir = $p.Directory; if (-not $instDir) { $instDir = $p.InstallDir }
|
||||
# Fallback: the registry Directory value is often blank - find the install dir on disk
|
||||
if (-not $instDir) {
|
||||
foreach ($pf in "$env:ProgramFiles\$($r.Vendor)","${env:ProgramFiles(x86)}\$($r.Vendor)") {
|
||||
if (-not (Test-Path $pf)) { continue }
|
||||
$cand = Get-ChildItem $pf -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "PC-DMIS*$ver*" -and (Test-Path (Join-Path $_.FullName 'PCDLRN.exe')) } | Select-Object -First 1
|
||||
if ($cand) { $instDir = $cand.FullName; break }
|
||||
}
|
||||
}
|
||||
$found += [pscustomobject]@{ Vendor=$r.Vendor; Version=$ver; HiveRoot=$r.Path; InstallDir=$instDir }
|
||||
}
|
||||
}
|
||||
$found
|
||||
}
|
||||
|
||||
function Backup-OneVersion($inst) {
|
||||
$ver = $inst.Version; $vendor = $inst.Vendor
|
||||
Log "==== Backing up PC-DMIS $vendor $ver ===="
|
||||
$stage = Join-Path $env:TEMP "pcd-bk-$ver-$ts"
|
||||
New-Item -ItemType Directory -Path $stage,"$stage\registry","$stage\install","$stage\ProgramData","$stage\PublicDocs","$stage\AppData" -Force | Out-Null
|
||||
|
||||
# registry: HKLM + HKCU under both Hexagon and Wai (export whichever exists)
|
||||
foreach ($hk in @('HKLM','HKCU')) {
|
||||
foreach ($v in @('Hexagon','Wai')) {
|
||||
$regPath = "$hk\SOFTWARE\$(if($hk -eq 'HKLM'){'WOW6432Node\'} )$v\PC-DMIS\$ver"
|
||||
$regPathNative = "$hk\SOFTWARE\$v\PC-DMIS\$ver"
|
||||
foreach ($rp in @($regPath,$regPathNative)) {
|
||||
$test = $rp -replace '^HKLM','HKLM:' -replace '^HKCU','HKCU:'
|
||||
if (Test-Path $test) {
|
||||
$f = "$stage\registry\$hk-$v-$ver.reg"
|
||||
reg export $rp "$f" /y 2>&1 | Out-Null
|
||||
if (Test-Path $f) { Log " reg export $rp" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# install-dir master probe/cal files
|
||||
if ($inst.InstallDir -and (Test-Path $inst.InstallDir)) {
|
||||
# interfac.dll = the active controller's interface DLL, renamed to interfac.dll
|
||||
# by PC-DMIS per the machine's controller. Machine-specific - capture it.
|
||||
foreach ($pat in 'PROBE.DAT','usrprobe.dat','comp.dat','compens.dat','tool.dat','machine.dat','interfac.dll') {
|
||||
Get-ChildItem $inst.InstallDir -Filter $pat -ErrorAction SilentlyContinue | ForEach-Object { Copy-Item $_.FullName "$stage\install\" -Force }
|
||||
}
|
||||
Get-ChildItem $inst.InstallDir -Filter '*.prb' -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$rel = $_.FullName.Substring($inst.InstallDir.TrimEnd('\').Length).TrimStart('\')
|
||||
$dst = Join-Path "$stage\install" $rel; New-Item -ItemType Directory -Path (Split-Path $dst) -Force -EA SilentlyContinue | Out-Null
|
||||
Copy-Item $_.FullName $dst -Force
|
||||
}
|
||||
Log " copied install-dir probe/cal files"
|
||||
}
|
||||
|
||||
# --- Identify the controller: which DLL became interfac.dll ---
|
||||
# PC-DMIS makes the active controller's interface DLL into interfac.dll. If it
|
||||
# was COPIED, an identical sibling .dll still exists -> hash match names it. If
|
||||
# it was RENAMED (no copy), no sibling matches - so we also read the PE version
|
||||
# resource's OriginalFilename, which survives a rename and names the source.
|
||||
$controllerInfo = $null
|
||||
$ifPath = if ($inst.InstallDir) { Join-Path $inst.InstallDir 'interfac.dll' } else { $null }
|
||||
if ($ifPath -and (Test-Path $ifPath)) {
|
||||
try {
|
||||
$ifItem = Get-Item $ifPath
|
||||
$ifHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $ifPath).Hash
|
||||
$vi = $ifItem.VersionInfo
|
||||
# size pre-filter so we don't hash every DLL in the install dir
|
||||
$match = Get-ChildItem $inst.InstallDir -Filter '*.dll' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -ne 'interfac.dll' -and $_.Length -eq $ifItem.Length } |
|
||||
Where-Object { try { (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash -eq $ifHash } catch { $false } } |
|
||||
Select-Object -First 1
|
||||
$controllerInfo = [pscustomobject]@{
|
||||
InterfacSha256 = $ifHash
|
||||
MatchedSourceDll = if ($match) { $match.Name } else { $null } # null = renamed, no copy
|
||||
OriginalFilename = $vi.OriginalFilename
|
||||
FileDescription = $vi.FileDescription
|
||||
ProductName = $vi.ProductName
|
||||
FileVersion = $vi.FileVersion
|
||||
}
|
||||
Log (" controller: interfac.dll source=" + $(if ($match) { $match.Name } else { '(renamed, no copy)' }) +
|
||||
" origName=$($vi.OriginalFilename) desc=$($vi.FileDescription)")
|
||||
} catch { Log " WARN: controller identification failed: $($_.Exception.Message)" }
|
||||
} else { Log " (no interfac.dll in install dir - controller not identified)" }
|
||||
|
||||
# per-version data folders
|
||||
$dataMap = @(
|
||||
@{ Src="$env:ProgramData\$vendor\PC-DMIS\$ver"; Dst="$stage\ProgramData" },
|
||||
@{ Src="$env:PUBLIC\Documents\$vendor\PC-DMIS\$ver"; Dst="$stage\PublicDocs" },
|
||||
@{ Src="$env:APPDATA\$vendor\PC-DMIS\$ver"; Dst="$stage\AppData\Roaming" },
|
||||
@{ Src="$env:LOCALAPPDATA\$vendor\PC-DMIS\$ver"; Dst="$stage\AppData\Local" }
|
||||
)
|
||||
foreach ($d in $dataMap) {
|
||||
if (Test-Path $d.Src) {
|
||||
New-Item -ItemType Directory -Path $d.Dst -Force | Out-Null
|
||||
# Exclude bay/path-specific Homepage state. Recent + Favorites store
|
||||
# absolute routine paths (S:\..., C:\geaofi\LocalProgramCopies\...).
|
||||
# Restoring them onto another bay makes PC-DMIS null-ref on launch
|
||||
# (RecentExecutedItem.LoadRealNode) trying to resolve missing paths.
|
||||
# Exclude the whole Homepage start-screen state (Recent, Favorites,
|
||||
# DetailsView) - it stores absolute routine paths (S:\..., C:\geaofi\...)
|
||||
# and PC-DMIS null-refs on launch resolving them (LoadRealNode). Rebuilt
|
||||
# on use. Also skip regenerable caches/logs.
|
||||
robocopy $d.Src $d.Dst /E /XD Cache Caches Temp logs Logs Homepage /R:1 /W:1 /NFL /NDL /NJH /NJS | Out-Null
|
||||
Log " copied $($d.Src)"
|
||||
}
|
||||
}
|
||||
|
||||
# manifest
|
||||
[pscustomobject]@{
|
||||
Computer=$env:COMPUTERNAME; Timestamp=(Get-Date -Format o)
|
||||
Vendor=$vendor; Version=$ver; InstallDir=$inst.InstallDir
|
||||
ControllerInterface=$controllerInfo
|
||||
} | ConvertTo-Json -Depth 4 | Out-File "$stage\manifest.json" -Encoding ascii
|
||||
|
||||
# zip
|
||||
$zip = Join-Path $OutDir "pcdmis_backup_${env:COMPUTERNAME}_${ver}_$ts.zip"
|
||||
if (Test-Path $zip) { Remove-Item $zip -Force }
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::CreateFromDirectory($stage,$zip)
|
||||
Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Log "==== DONE: $zip ===="
|
||||
return $zip
|
||||
}
|
||||
|
||||
Log "PC-DMIS backup on $env:COMPUTERNAME at $(Get-Date)"
|
||||
$installs = Get-PCDMISInstalls | Where-Object { $_.Version -match '^\d' } | Sort-Object Version -Unique
|
||||
if ($Version) { $installs = $installs | Where-Object { $_.Version -eq $Version } }
|
||||
if (-not $installs) { Log "No PC-DMIS installs detected (Hexagon/Wai). Nothing to back up."; return }
|
||||
|
||||
$made = @()
|
||||
foreach ($inst in $installs) { $made += (Backup-OneVersion $inst) }
|
||||
Write-Host ""
|
||||
Write-Host "PC-DMIS backups written:" -ForegroundColor Green
|
||||
$made | ForEach-Object { Write-Host " $_" }
|
||||
@@ -0,0 +1,19 @@
|
||||
@echo off
|
||||
REM Backup-goCMMSettings.bat - capture this bay's goCMM settings.
|
||||
REM
|
||||
REM Run on a LIVE legacy goCMM device, AS ADMINISTRATOR (reg export of HKLM +
|
||||
REM read of C:\geaofi needs admin). Captures the registry pointers + the whole
|
||||
REM Shared Data Directory (ApplicationSettings.xml = all 7 Settings tabs) into
|
||||
REM one zip under C:\Logs\CMM\gocmm-backup\.
|
||||
REM
|
||||
REM Backup-goCMMSettings.bat (default output dir)
|
||||
REM Backup-goCMMSettings.bat -OutDir D:\path
|
||||
|
||||
setlocal
|
||||
set "HERE=%~dp0"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%HERE%Backup-goCMMSettings.ps1" %*
|
||||
echo.
|
||||
echo ---------------------------------------------------------------
|
||||
echo Backup zip is under C:\Logs\CMM\gocmm-backup\ (gocmm_backup_*.zip)
|
||||
echo ---------------------------------------------------------------
|
||||
pause
|
||||
@@ -0,0 +1,91 @@
|
||||
<#
|
||||
Backup-goCMMSettings.ps1
|
||||
|
||||
Capture a goCMM bay's COMPLETE settings into one zip. Run on a LIVE legacy
|
||||
goCMM device. Mirrors Backup-FormtracepakSettings.ps1.
|
||||
|
||||
goCMM stores settings in two places:
|
||||
1. Registry pointers: HKLM\SOFTWARE\WOW6432Node\General Electric\goCMM
|
||||
Shared Data Directory, Selected Part Group, Installation Directory
|
||||
2. The Shared Data Directory (default C:\geaofi\) holds ApplicationSettings.xml
|
||||
= the real content of ALL 7 Settings tabs: PC-DMIS, Quindos, Modus,
|
||||
Machine Definition, User Input, Notifications, Part Groups.
|
||||
|
||||
This zip captures both so a re-imaged bay can be restored to identical settings.
|
||||
|
||||
Output: C:\Logs\CMM\gocmm-backup\gocmm_backup_<PC>_<timestamp>.zip
|
||||
Run as administrator (reg export of HKLM + read of the data dir).
|
||||
#>
|
||||
param(
|
||||
[string]$OutDir = 'C:\Logs\CMM\gocmm-backup'
|
||||
)
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
New-Item -ItemType Directory -Path $OutDir -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
$log = Join-Path $OutDir "gocmm-backup-$ts.log"
|
||||
function Log($m){ Write-Host $m; $m | Out-File -FilePath $log -Append }
|
||||
|
||||
$stage = Join-Path $env:TEMP "gocmm-bk-$ts"
|
||||
New-Item -ItemType Directory -Path $stage, "$stage\registry", "$stage\geaofi" -Force | Out-Null
|
||||
|
||||
Log "==== goCMM backup on $env:COMPUTERNAME at $(Get-Date) ===="
|
||||
|
||||
# --- Read the 3 registry pointers (32-bit / WOW6432Node view, as the 32-bit app sees them) ---
|
||||
$sharedDir = 'C:\geaofi'; $partGroup = ''; $installDir = ''
|
||||
try {
|
||||
$base32 = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine','Registry32')
|
||||
$k = $base32.OpenSubKey('SOFTWARE\General Electric\goCMM', $false)
|
||||
if ($k) {
|
||||
$sharedDir = ([string]$k.GetValue('Shared Data Directory','C:\geaofi')).TrimEnd('\')
|
||||
$partGroup = [string]$k.GetValue('Selected Part Group','')
|
||||
$installDir = [string]$k.GetValue('Installation Directory','')
|
||||
$k.Close()
|
||||
Log "Shared Data Directory = $sharedDir"
|
||||
Log "Selected Part Group = $partGroup"
|
||||
Log "Installation Directory= $installDir"
|
||||
} else { Log "WARN: goCMM registry key not found - defaulting Shared Data Directory to $sharedDir" }
|
||||
} catch { Log "WARN: reading goCMM registry failed: $($_.Exception.Message)" }
|
||||
|
||||
# --- Export the registry key ---
|
||||
reg export 'HKLM\SOFTWARE\WOW6432Node\General Electric\goCMM' "$stage\registry\goCMM.reg" /y 2>&1 | Out-Null
|
||||
if (Test-Path "$stage\registry\goCMM.reg") { Log "Exported registry key" } else { Log "WARN: registry export produced no file" }
|
||||
|
||||
# --- Copy the Shared Data Directory (skip transient LocalProgramCopies + logs) ---
|
||||
if (Test-Path $sharedDir) {
|
||||
robocopy $sharedDir "$stage\geaofi" /E /XD LocalProgramCopies logs /R:1 /W:1 /NFL /NDL /NJH /NJS | Out-Null
|
||||
Log "Copied $sharedDir (excluded LocalProgramCopies, logs)"
|
||||
if (Test-Path "$stage\geaofi\ApplicationSettings.xml") {
|
||||
Log "ApplicationSettings.xml captured (PC-DMIS + all Settings tabs)"
|
||||
} else {
|
||||
Log "WARN: ApplicationSettings.xml NOT found under $sharedDir - settings tabs may be unconfigured"
|
||||
}
|
||||
} else {
|
||||
Log "WARN: Shared Data Directory $sharedDir does not exist - only the registry key will be in this backup"
|
||||
}
|
||||
|
||||
# --- Manifest ---
|
||||
$ver = ''
|
||||
if ($installDir) {
|
||||
$exe = Join-Path ($installDir.TrimEnd('\')) 'goCMM.exe'
|
||||
if (Test-Path $exe) { $ver = (Get-Item $exe).VersionInfo.FileVersion }
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Computer = $env:COMPUTERNAME
|
||||
Timestamp = (Get-Date -Format o)
|
||||
SharedDataDirectory = $sharedDir
|
||||
SelectedPartGroup = $partGroup
|
||||
InstallationDirectory = $installDir
|
||||
goCMMVersion = $ver
|
||||
} | ConvertTo-Json | Out-File "$stage\manifest.json" -Encoding ascii
|
||||
|
||||
# --- Zip ---
|
||||
$zip = Join-Path $OutDir "gocmm_backup_${env:COMPUTERNAME}_$ts.zip"
|
||||
if (Test-Path $zip) { Remove-Item $zip -Force }
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::CreateFromDirectory($stage, $zip)
|
||||
Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Log "==== DONE: $zip ===="
|
||||
Write-Host ""
|
||||
Write-Host "goCMM backup written:" -ForegroundColor Green
|
||||
Write-Host " $zip"
|
||||
@@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
REM Clear-PCDMISRecent.bat - fix the PC-DMIS startup crash (NullReferenceException
|
||||
REM in RecentExecutedItem.LoadRealNode) caused by a stale Homepage "Recent
|
||||
REM Executed Files" list pointing at routine paths that don't resolve on this bay.
|
||||
REM
|
||||
REM Deletes the Recent (and Favorites) Homepage state for every user + PC-DMIS
|
||||
REM version; PC-DMIS rebuilds an empty list on next launch.
|
||||
REM
|
||||
REM Run AS ADMINISTRATOR.
|
||||
REM
|
||||
REM Clear-PCDMISRecent.bat (all users, Recent + Favorites)
|
||||
REM Clear-PCDMISRecent.bat -RecentOnly (keep Favorites)
|
||||
REM Clear-PCDMISRecent.bat -User ShopFloor
|
||||
|
||||
setlocal
|
||||
set "HERE=%~dp0"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%HERE%Clear-PCDMISRecent.ps1" %*
|
||||
echo.
|
||||
echo ---------------------------------------------------------------
|
||||
echo Done. Relaunch PC-DMIS - the recent-list crash should be gone.
|
||||
echo Log: C:\Logs\CMM\pcdmis-clearrecent-*.log
|
||||
echo ---------------------------------------------------------------
|
||||
pause
|
||||
@@ -0,0 +1,75 @@
|
||||
<#
|
||||
Clear-PCDMISRecent.ps1
|
||||
|
||||
Fixes the PC-DMIS startup crash:
|
||||
System.NullReferenceException
|
||||
at Adapter.Service.Recent.Models.RecentExecutedItem.LoadRealNode()
|
||||
at Adapter.Service.Recent.Models.RecentExecutedFiles.LoadAllWhenReady()
|
||||
|
||||
Cause: the Homepage "Recent Executed Files" list contains absolute routine
|
||||
paths (S:\..., C:\geaofi\LocalProgramCopies\...). When a path does not resolve
|
||||
in the running user's context (drive not visible, cache not yet rebuilt, file
|
||||
gone), PC-DMIS dereferences a null while async-loading the list and crashes on
|
||||
launch. A list carried over from another bay (e.g. a settings restore) is the
|
||||
common trigger.
|
||||
|
||||
Fix: delete the Recent (and Favorites) Homepage state for every user profile
|
||||
and every PC-DMIS version. PC-DMIS rebuilds an empty list on next start.
|
||||
|
||||
Run as ADMINISTRATOR (to reach all user profiles).
|
||||
Output: C:\Logs\CMM\pcdmis-clearrecent-<PC>-<ts>.log
|
||||
|
||||
Params:
|
||||
-RecentOnly only clear the Recent list, keep Favorites
|
||||
-User <name> only this user's profile (default: all profiles)
|
||||
#>
|
||||
param(
|
||||
[switch]$RecentOnly,
|
||||
[string]$User
|
||||
)
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$dir = 'C:\Logs\CMM'
|
||||
New-Item -ItemType Directory -Path $dir -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
$log = Join-Path $dir "pcdmis-clearrecent-$env:COMPUTERNAME-$ts.log"
|
||||
function Log($m){ Write-Host $m; $m | Out-File -FilePath $log -Append }
|
||||
|
||||
Log "==== Clear PC-DMIS Recent/Favorites on $env:COMPUTERNAME at $(Get-Date) ===="
|
||||
|
||||
# which user profiles
|
||||
$userDirs = @()
|
||||
if ($User) {
|
||||
if (Test-Path "C:\Users\$User") { $userDirs += "C:\Users\$User" } else { Log "User profile C:\Users\$User not found" }
|
||||
} else {
|
||||
$userDirs = (Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue).FullName
|
||||
}
|
||||
|
||||
$targets = @('Homepage\Recent\RecentExecutedFiles.xml')
|
||||
if (-not $RecentOnly) { $targets += 'Homepage\Favorites\Favorites.xml' }
|
||||
|
||||
$deleted = 0; $scanned = 0
|
||||
foreach ($u in $userDirs) {
|
||||
foreach ($vendor in 'Hexagon','WAI') {
|
||||
$base = Join-Path $u "AppData\Local\$vendor\PC-DMIS"
|
||||
if (-not (Test-Path $base)) { continue }
|
||||
foreach ($vdir in (Get-ChildItem $base -Directory -ErrorAction SilentlyContinue)) {
|
||||
$scanned++
|
||||
foreach ($rel in $targets) {
|
||||
$f = Join-Path $vdir.FullName $rel
|
||||
if (Test-Path $f) {
|
||||
try { Remove-Item $f -Force -ErrorAction Stop; $deleted++
|
||||
Log " DELETED $f"
|
||||
} catch { Log " FAILED $f : $($_.Exception.Message)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log ""
|
||||
Log "Scanned $scanned PC-DMIS version folder(s) across $($userDirs.Count) profile(s); deleted $deleted file(s)."
|
||||
Log "PC-DMIS will rebuild an empty recent list on next launch."
|
||||
Write-Host ""
|
||||
if ($deleted -gt 0) { Write-Host "Cleared $deleted stale Homepage file(s). Relaunch PC-DMIS - the crash should be gone." -ForegroundColor Green }
|
||||
else { Write-Host "Nothing to clear (no RecentExecutedFiles.xml found). If it still crashes, the cause is elsewhere - grab the event log." -ForegroundColor Yellow }
|
||||
Log "Log: $log"
|
||||
@@ -0,0 +1,21 @@
|
||||
@echo off
|
||||
REM Export-PCDMISCrashEvents.bat - pull the Windows event-log entries that
|
||||
REM explain the PC-DMIS crash (the full .NET exception + stack, which the
|
||||
REM crash minidump does NOT contain).
|
||||
REM
|
||||
REM Run AS ADMINISTRATOR on the bay that crashed, soon after the crash.
|
||||
REM
|
||||
REM Export-PCDMISCrashEvents.bat (last 72 hours)
|
||||
REM Export-PCDMISCrashEvents.bat -Hours 12 (narrower window)
|
||||
REM
|
||||
REM Output: C:\Logs\CMM\pcdmis-crash-events-<PC>-<ts>.txt
|
||||
REM Send that file back - the first .NET Runtime (ID 1026) event is the answer.
|
||||
|
||||
setlocal
|
||||
set "HERE=%~dp0"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%HERE%Export-PCDMISCrashEvents.ps1" %*
|
||||
echo.
|
||||
echo ---------------------------------------------------------------
|
||||
echo Wrote C:\Logs\CMM\pcdmis-crash-events-*.txt - send it back.
|
||||
echo ---------------------------------------------------------------
|
||||
pause
|
||||