CMM: add PC-DMIS + combined CMM backup/restore + diagnostic scripts

Adds the PC-DMIS settings/probe backup-restore set alongside the existing
goCMM scripts, plus a single combined CMM backup and the diagnostics built
while debugging the live bays:

- Backup-PCDMISSettings / Install-PCDMISSettings: capture+restore PC-DMIS
  registry + data/probe/cal files per installed version (2016/2019/2026).
  Hardened from real-bay failures: detect install dir via Program Files
  fallback; capture compens.dat (not just comp.dat) + interfac.dll; identify
  the controller by hash-matching interfac.dll to its source DLL AND reading
  the PE OriginalFilename (covers rename-without-copy); EXCLUDE the whole
  Homepage state (Recent/Favorites/DetailsView) which null-refs PC-DMIS on
  launch via stale routine paths; restore routes HKCU into the target user's
  hive (-TargetUser ShopFloor), fails loud on a non-backup path, and applies
  the legacy->new FQDN rewrite across reg + data files incl .bas.
- Backup-CMM: one wrapper running goCMM + PC-DMIS (all versions) into one
  per-CMM folder + index, for staging on PXE and restore-by-machine-number.
- Clear-PCDMISRecent: fixes the Homepage recent-list NullReferenceException
  crash on an already-broken bay.
- pcdmis-probe-debug / Export-PCDMISCrashEvents: diagnostics for the
  custom-probe-not-showing and crash investigations.
- Modify-PCDMISRights / Grant-FullControl: grant the operator the registry +
  filesystem access PC-DMIS needs under lockdown.
- Install-goCMMSettings: add .bas to the FQDN-rewrite include list.

Not yet wired into 09-Setup-CMM auto-restore - staging + the gated restore
block come next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-12 08:42:32 -04:00
parent bfe17fe123
commit 1d65103cc0
15 changed files with 998 additions and 1 deletions

View File

@@ -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 " $_" }