Backup-FormtracepakSettings: empirically-grounded version + model detection, HKEY_USERS sweep, manifest evidence stamp

Installed FormTracePak v6.213 on the win11 VM (picking FORMTRACER Avant
in the dialogs) and probed the resulting registry / disk layout to find
out what evidence a real FormTracePak install actually carries. Two
empirical findings:

1. ACTIVE MODEL lives at
   HKLM:\SOFTWARE\WOW6432Node\Mitutoyo\FORMPAK\Config\device map\DeviceName
   (string value). For the AVANT install the value is "FORMTRACER Avant";
   for the CV/SV/CS controllers the value contains the matching model id.
   The Surfpak\FormMes\MachineInfo\Machine\Machine* subtree lists EVERY
   supported machine and is NOT the active selection - the previous
   heuristic that scanned uninstall-entry DisplayName picked up bogus
   WinUSB driver-package entries from "Mitutoyo Corporation" instead.

2. INSTALLED BINARY VERSION is in Formtracepak.exe VersionInfo:
   FileVersion=6.2.0.51, ProductVersion=6.2.0.0. This does NOT match the
   Mitutoyo MSI release label ("6.213") that bay-config.csv uses. The
   uninstall entry's DisplayVersion is empty. So bay-config.csv stays
   canonical for the per-asset marketing version; exe FileVersion is a
   concrete cross-check.

Backup rewrites:
- Replace the previous one-shot version detection with evidence reading:
  bay-config.csv (asset->version+model), Formtracepak.exe VersionInfo,
  device-map\DeviceName. The over-broad uninstall-reg regex is gone.
- Normalize DeviceName ("FORMTRACER Avant" / "CV-4500" / "CV-3200" /
  "Contracer" / "Surftest") to bay-config notation (AVANT / CV-4500 / ...).
- Emit BayConfigMatch flag - true when bay-config-predicted model agrees
  with the device-map\DeviceName on disk. False = drift, tech rechecks
  before restoring to a new bay.
- manifest.json now stamps: AssetNumber, FormtracepakVersion, Model,
  BayConfigVersion, BayConfigModel, BayConfigSource, InstalledExeVersion,
  InstalledExePath, InstalledDeviceName, InstalledModelNormalized,
  BayConfigMatch.

HKEY_USERS sweep:
- Wax/Trace bays log in as a per-site user (lg782713sd at WJ today,
  ShopFloor post-SFLD-2.0, other accounts at other sites). The previous
  HKCU:\ scan only captured the script's running user. Sweep every
  loaded HKEY_USERS hive whose SID matches S-1-5-21-* (real user SIDs)
  for Software\Mitutoyo / FORMTRACEPAK / FORMPAK / SURFPAK subkeys,
  add them to $RegistryRoots. Username-agnostic - works at any site
  without changes.

reg.exe export now also accepts the Registry::HKEY_USERS\<sid>\... PSPath
form by stripping the "Registry::" prefix when building the reg.exe
argument (previously emitted "Invalid key name" errors on HKEY_USERS roots).

Smoke tested against a real v6.213 / FORMTRACER Avant install on win11
VM: bay-config lookup matches, exe FileVersion read, device-map
normalized to AVANT, all four .reg files (HKLM, HKLM-WOW6432Node, HKCU,
HKEY_USERS\<interactive-sid>) exported clean, 0 errors.

Restore-side SID translation (HKEY_USERS\<src-sid>\... -> target user's
SID or HKCU on the new bay) is a follow-up. HKLM tree carries the
critical device-map\DeviceName, controller config, and machine settings;
the HKEY_USERS hive captures per-user UI prefs only.
This commit is contained in:
cproudlock
2026-05-24 09:58:24 -04:00
parent 821e3179d1
commit cb149ed8cd

View File

@@ -149,6 +149,28 @@ $RegistryRoots = @(
"HKLM:\SOFTWARE\WOW6432Node\FORMPAK"
"HKCU:\SOFTWARE\FORMPAK"
"HKLM:\SOFTWARE\SURFPAK"
)
# Sweep every loaded user hive under HKEY_USERS for Mitutoyo/FORMTRACEPAK/
# FORMPAK/SURFPAK subkeys. Wax/Trace bays run as a domain "Shopfloor" account,
# whose HKCU prefs are NOT visible via HKCU:\ when the script runs as
# SupportUser / SYSTEM. As long as the Shopfloor user has a logged-in session
# (interactive or disconnected) the hive is mounted under HKEY_USERS\<SID>.
# Skip the special SIDs (S-1-5-18 = SYSTEM, S-1-5-19/20 = LocalService /
# NetworkService) and the *_Classes subhives.
$userHiveSubpaths = @('Software\Mitutoyo','Software\FORMTRACEPAK','Software\FORMPAK','Software\SURFPAK')
foreach ($u in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
$sid = $u.PSChildName
if ($sid -notmatch '^S-1-5-21-' ) { continue } # only real user SIDs
if ($sid -match '_Classes$') { continue }
foreach ($sp in $userHiveSubpaths) {
$candidate = "Registry::HKEY_USERS\$sid\$sp"
if (Test-Path -LiteralPath $candidate) {
$RegistryRoots += $candidate
}
}
}
$RegistryRoots = $RegistryRoots + @(
"HKCU:\SOFTWARE\SURFPAK"
)
@@ -304,8 +326,13 @@ foreach ($root in $RegistryRoots) {
$safeName = ($root -replace '[:\\]', '_').Trim('_')
$regFilePath = Join-Path $regStage "${safeName}.reg"
$nativeRoot = $root -replace '^HKLM:', 'HKEY_LOCAL_MACHINE' `
-replace '^HKCU:', 'HKEY_CURRENT_USER'
# Normalize the PSPath-style root into a form reg.exe accepts.
# HKLM:\... -> HKEY_LOCAL_MACHINE\...
# HKCU:\... -> HKEY_CURRENT_USER\...
# Registry::HKEY_USERS\<sid>\... -> HKEY_USERS\<sid>\...
$nativeRoot = $root -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' `
-replace '^HKCU:\\', 'HKEY_CURRENT_USER\' `
-replace '^Registry::', ''
try {
$proc = Start-Process -FilePath 'reg.exe' `
-ArgumentList "export `"$nativeRoot`" `"$regFilePath`" /y" `
@@ -359,27 +386,124 @@ if ($regCsvRows.Count -gt 0) {
-NoTypeInformation -Encoding UTF8
}
# ---------------------------------------------------------------------- 5. Detect installed version ----------------------------------------------------------------------
# ---------------------------------------------------------------------- 5. Detect installed version + model ----------------------------------------------------------------------
#
# Evidence sources, established empirically by installing FormTracePak v6.213
# on the win11 VM and probing the resulting registry / disk layout
# (2026-05-24):
#
# - bay-config.csv: authoritative for the per-asset marketing version
# ("6.213", "6.103", ...) AND model ("AVANT", "CV-4500", "CV-3200").
# This is the source of truth for the 15 known wax/trace bays.
#
# - Formtracepak.exe VersionInfo (FileVersion / ProductVersion): the
# binary version on disk (e.g. 6.2.0.51). Does NOT match the
# bay-config marketing format directly (Mitutoyo numbers their
# binary releases differently from their MSI release labels), but is
# concrete on-disk evidence the install is real.
#
# - HKLM:\SOFTWARE\WOW6432Node\Mitutoyo\FORMPAK\Config\device map\DeviceName:
# the active controller / model picked at install time. Real values
# include "FORMTRACER Avant", "CV-4500", "CV-3200", "Contracer CV-3200".
# The Mitutoyo "Surfpak\FormMes\MachineInfo\Machine\Machine*" rows are
# installer templates listing every supported machine; ignore them.
#
# - Mitutoyo uninstall registry entries: useless for version. The
# "Formtracepak" uninstall entry has DisplayVersion = '' (empty),
# and the WinUSB driver-package entries falsely match a broad
# "Mitutoyo" regex with bogus dates like "01/26/2014 1.0.0.0".
# Not consulted.
#
# manifest.json now records both the bay-config-predicted values and the
# on-disk evidence, plus a Match flag so any drift between what's
# supposed to be installed and what actually is gets surfaced at restore
# time without needing a separate audit.
$detectedVersion = 'Unknown'
$uninstallPaths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
)
foreach ($uPath in $uninstallPaths) {
if (-not (Test-Path $uPath)) { continue }
$entries = Get-ChildItem -Path $uPath -ErrorAction SilentlyContinue
foreach ($entry in $entries) {
# --- bay-config.csv predicted values ---
$bayConfigVersion = $null
$bayConfigModel = $null
$bayConfigSource = $null
if ($AssetNumber) {
$bayCfg = @(
(Join-Path $scriptDir 'bay-config.csv'),
'C:\WaxTrace-Install\bay-config.csv',
'Y:\installers-post\waxtrace\bay-config.csv'
) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1
if ($bayCfg) {
try {
$props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
if ($props.DisplayName -and $props.DisplayName -match 'FORMTRACEPAK|FormTrace') {
$detectedVersion = $props.DisplayVersion
break
$row = Import-Csv -LiteralPath $bayCfg |
Where-Object { $_.asset_tag -ieq $AssetNumber } | Select-Object -First 1
if ($row) {
$bayConfigVersion = $row.ftpak_version
$bayConfigModel = $row.model
$bayConfigSource = $bayCfg
}
} catch { }
}
if ($detectedVersion -ne 'Unknown') { break }
}
# --- Formtracepak.exe FileVersion ---
$installedExeVersion = $null
$installedExePath = $null
$exeCandidates = @(
"${env:ProgramFiles(x86)}\MitutoyoApp\Formtracepak\Formtracepak.exe"
"${env:ProgramFiles}\MitutoyoApp\Formtracepak\Formtracepak.exe"
"${env:ProgramFiles(x86)}\Mitutoyo\Formtracepak\Formtracepak.exe"
"${env:ProgramFiles}\Mitutoyo\Formtracepak\Formtracepak.exe"
'C:\Mitutoyo\Formtracepak\Formtracepak.exe'
'D:\Mitutoyo\Formtracepak\Formtracepak.exe'
)
foreach ($exe in $exeCandidates) {
if (Test-Path -LiteralPath $exe) {
try {
$vi = (Get-Item -LiteralPath $exe).VersionInfo
$installedExeVersion = if ($vi.FileVersion) { $vi.FileVersion.Trim() } elseif ($vi.ProductVersion) { $vi.ProductVersion.Trim() } else { $null }
$installedExePath = $exe
break
} catch { }
}
}
# --- Active device-map DeviceName + normalized model ---
$installedDeviceName = $null
$installedModelNormalized = $null
$deviceMapKey = 'HKLM:\SOFTWARE\WOW6432Node\Mitutoyo\FORMPAK\Config\device map'
if (Test-Path -LiteralPath $deviceMapKey) {
try {
$dm = Get-ItemProperty -LiteralPath $deviceMapKey -ErrorAction Stop
if ($dm.PSObject.Properties['DeviceName']) {
$installedDeviceName = [string]$dm.DeviceName
}
} catch { }
}
# Normalize DeviceName to bay-config.csv notation (AVANT / CV-4500 / CV-3200 / ...).
if ($installedDeviceName) {
switch -Regex ($installedDeviceName) {
'(?i)FORMTRACER\s*Avant|^AVANT$' { $installedModelNormalized = 'AVANT'; break }
'(?i)CV-?4500' { $installedModelNormalized = 'CV-4500'; break }
'(?i)CV-?3200' { $installedModelNormalized = 'CV-3200'; break }
'(?i)CV-?3100' { $installedModelNormalized = 'CV-3100'; break }
'(?i)CV-?4100' { $installedModelNormalized = 'CV-4100'; break }
'(?i)CV-?4200' { $installedModelNormalized = 'CV-4200'; break }
'(?i)Contracer|^CS-' { $installedModelNormalized = 'Contracer'; break }
'(?i)Surftest|^SV-' { $installedModelNormalized = 'Surftest'; break }
default { $installedModelNormalized = $installedDeviceName }
}
}
# --- Reconcile + log ---
$bayConfigMatch = $true
if ($bayConfigModel -and $installedModelNormalized -and ($bayConfigModel -ine $installedModelNormalized)) {
$bayConfigMatch = $false
}
$detectedVersion = if ($bayConfigVersion) { $bayConfigVersion } elseif ($installedExeVersion) { $installedExeVersion } else { 'Unknown' }
$detectedModel = if ($bayConfigModel) { $bayConfigModel } elseif ($installedModelNormalized) { $installedModelNormalized } else { 'Unknown' }
Write-Host "Bay config lookup : $(if ($bayConfigSource) { "$bayConfigSource (asset='$AssetNumber' -> ver='$bayConfigVersion' model='$bayConfigModel')" } else { 'asset not in any bay-config.csv' })"
Write-Host "Installed exe : $(if ($installedExePath) { "$installedExePath FileVersion=$installedExeVersion" } else { 'no Formtracepak.exe found on disk' })"
Write-Host "Installed device-map : $(if ($installedDeviceName) { "DeviceName='$installedDeviceName' -> normalized '$installedModelNormalized'" } else { "$deviceMapKey not present" })"
if (-not $bayConfigMatch) {
Write-Warning "Bay-config says model='$bayConfigModel' but device-map says '$installedModelNormalized' - DRIFT (recapture the bay-config.csv or the live install before restoring to a new bay)"
}
# ---------------------------------------------------------------------- Write manifest ----------------------------------------------------------------------
@@ -387,8 +511,18 @@ Write-Host "[4/4] Writing manifest..." -ForegroundColor Cyan
$manifest = [PSCustomObject]@{
SourceComputer = $env:COMPUTERNAME
AssetNumber = $AssetNumber
BackupTimestamp = (Get-Date -Format 'o')
FormtracepakVersion = $detectedVersion
Model = $detectedModel
BayConfigVersion = $bayConfigVersion
BayConfigModel = $bayConfigModel
BayConfigSource = $bayConfigSource
InstalledExeVersion = $installedExeVersion
InstalledExePath = $installedExePath
InstalledDeviceName = $installedDeviceName
InstalledModelNormalized= $installedModelNormalized
BayConfigMatch = $bayConfigMatch
OSVersion = [System.Environment]::OSVersion.VersionString
PowerShellVersion = $PSVersionTable.PSVersion.ToString()
IncludesConfig = ($counters.Config -gt 0)