Preinstall: extract MSIs for all VC++ redists to suppress reboot

The Microsoft VC++ bootstrappers (vcredist*_x86.exe) ignore /norestart
and trigger immediate Windows reboots when CRT DLLs are in use, which
in practice is always. We saw this break a live Standard PC imaging
run (installlog showed the manual shutdown -a sequence between runs).

Fix follows the existing 2008 pattern: extract the inner MSIs from
each Burn bundle, run them via msiexec with REBOOT=ReallySuppress
(a hard Windows Installer property the bootstrapper can't override),
and treat exit 3010 as success. Files are now staged per-version
under dependencies/vcredist/<version>/ because each MSI's Media table
hardcodes its CAB filename, so the pairs would otherwise collide.

preinstall.json: 4 EXE entries replaced with 8 MSI entries (Min+Add
for 2012/2013/2022 because each version's Burn bundle ships them
as separate MSIs). 2008 also moved into the same vcredist/2008/
subdir for consistency. ProductCodes verified against the existing
detection paths (the previous "bootstrapper" GUIDs were actually
the Min runtime GUIDs inherited up the chain).

sync-preinstall.sh: now tarballs the dependencies/vcredist/ subtree
to preserve directory structure across the scp+sudo-cp boundary,
flat installers (UDC, Oracle) still copied individually, and the
remote install script now removes the legacy flat vc_red.msi/cab
plus the obsolete vcredist*_x86.exe bootstrappers on every sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-04-08 15:01:23 -04:00
parent ded0a7184b
commit 61e0f3a033
2 changed files with 131 additions and 56 deletions

View File

@@ -14,41 +14,82 @@
"PCTypes": ["*"]
},
{
"_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.",
"Name": "VC++ Redistributable 2008 x86",
"Installer": "vcredist/2008/installer.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{9BE518E6-ECC6-35A9-88E4-87755C07200F}",
"PCTypes": ["*"]
},
{
"_comment": "VC++ 2010 x86 - same fix as 2008. Bootstrapper ignores /norestart; extracted MSI with REBOOT=ReallySuppress does not.",
"Name": "VC++ Redistributable 2010 x86",
"Installer": "vcredist2010_x86.exe",
"Type": "EXE",
"InstallArgs": "/quiet /norestart",
"Installer": "vcredist/2010/installer.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{F0C3E5D1-1ADE-321E-8167-68EF0DE699A5}",
"PCTypes": ["*"]
},
{
"Name": "VC++ Redistributable 2012 x86",
"Installer": "vcredist2012_x86.exe",
"Type": "EXE",
"InstallArgs": "/quiet /norestart",
"_comment": "VC++ 2012 x86 Minimum Runtime - extracted from vcredist2012_x86.exe Burn bundle. Same REBOOT=ReallySuppress fix.",
"Name": "VC++ Redistributable 2012 x86 (Minimum)",
"Installer": "vcredist/2012-min/installer.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BD95A8CD-1D9F-35AD-981A-3E7925026EBB}",
"PCTypes": ["*"]
},
{
"Name": "VC++ Redistributable 2013 x86",
"Installer": "vcredist2013_x86.exe",
"Type": "EXE",
"InstallArgs": "/quiet /norestart",
"Name": "VC++ Redistributable 2012 x86 (Additional)",
"Installer": "vcredist/2012-add/installer.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{B175520C-86A2-35A7-8619-86DC379688B9}",
"PCTypes": ["*"]
},
{
"_comment": "VC++ 2013 x86 Minimum Runtime - extracted from vcredist2013_x86.exe Burn bundle.",
"Name": "VC++ Redistributable 2013 x86 (Minimum)",
"Installer": "vcredist/2013-min/installer.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{13A4EE12-23EA-3371-91EE-EFB36DDFFF3E}",
"PCTypes": ["*"]
},
{
"Name": "VC++ Redistributable 2015-2022 x86",
"Installer": "vcredist2015_2017_2019_2022_x86.exe",
"Type": "EXE",
"InstallArgs": "/install /quiet /norestart",
"Name": "VC++ Redistributable 2013 x86 (Additional)",
"Installer": "vcredist/2013-add/installer.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{F8CFEB22-A2E7-3971-9EDA-4B11EDEFC185}",
"PCTypes": ["*"]
},
{
"_comment": "VC++ 2015-2022 x86 - extracted from vcredist2015_2017_2019_2022_x86.exe Burn bundle. The bundle contains 2022 14.44.35211 plus 8 chained KB updates for older 2015/2017/2019 releases. We install only the 2022 Min+Add MSIs - the CRT v140 ABI is shared across 2015/2017/2019/2022, so the latest pair covers all four versions on Windows 10/11.",
"Name": "VC++ Redistributable 2022 x86 (Minimum)",
"Installer": "vcredist/2022-min/installer.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{922480B5-CAEB-4B1B-AAA4-9716EFDCE26B}",
"PCTypes": ["*"]
},
{
"Name": "VC++ Redistributable 2022 x86 (Additional)",
"Installer": "vcredist/2022-add/installer.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{C18FB403-1E88-43C8-AD8A-CED50F23DE8B}",
"PCTypes": ["*"]
},
{
"Name": "UDC",
"Installer": "UDC_Setup.exe",
@@ -57,16 +98,6 @@
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\UDC",
"PCTypes": ["Standard"]
},
{
"_comment": "VC++ 2008 — installed via the extracted vc_red.msi (NOT the bootstrapper) because vcredist2008_x86.exe ignores /norestart (per Aaron Stebner's Microsoft docs). REBOOT=ReallySuppress is a Windows Installer property that hard-blocks reboots even when files are in use; msiexec may return 3010 but won't actually reboot. vc_red.cab must live in the same directory as vc_red.msi or msiexec can't find the data files.",
"Name": "VC++ Redistributable 2008 x86",
"Installer": "vc_red.msi",
"Type": "MSI",
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress",
"DetectionMethod": "Registry",
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{9BE518E6-ECC6-35A9-88E4-87755C07200F}",
"PCTypes": ["*"]
}
]
}

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# sync-preinstall.sh Push preinstall.json + installer binaries to the live PXE server.
# sync-preinstall.sh - Push preinstall.json + installer binaries to the live PXE server.
#
# Run this on the workstation (not on the PXE server) any time:
# - You update preinstall.json in playbook/preinstall/
@@ -11,7 +11,7 @@
# Usage:
# ./playbook/sync-preinstall.sh
#
# Requires: sshpass (apt install sshpass), scp, ssh
# Requires: sshpass (apt install sshpass), scp, ssh, tar
set -euo pipefail
@@ -30,19 +30,18 @@ REMOTE_DIR="/srv/samba/enrollment/preinstall"
REMOTE_INSTALLERS="$REMOTE_DIR/installers"
REMOTE_TEMP="/tmp/preinstall-stage"
# Files to push (source paths under PXE_IMAGES_DIR).
# vc_red.msi + vc_red.cab are extracted from vcredist2008_x86.exe and used directly
# (instead of the bootstrapper) because the 2008 bootstrapper ignores /norestart and
# triggers an immediate reboot. The MSI honors REBOOT=ReallySuppress, the bootstrapper
# does not. The .cab MUST be named "vc_red.cab" exactly because that name is hardcoded
# in the MSI's Media table.
INSTALLERS=(
"dependencies/vc_red.msi"
"dependencies/vc_red.cab"
"dependencies/vcredist2010_x86.exe"
"dependencies/vcredist2012_x86.exe"
"dependencies/vcredist2013_x86.exe"
"dependencies/vcredist2015_2017_2019_2022_x86.exe"
# Subtrees copied recursively into installers/, preserving directory structure.
# vcredist/ holds per-version MSI+CAB pairs (one subdir per version) extracted from
# the Microsoft bootstrappers. The bootstrappers ignore /norestart and trigger
# immediate Windows reboots; the extracted MSIs honor REBOOT=ReallySuppress and do
# not. Each MSI's CAB filename ('vc_red.cab' for 2008/2010, 'cab1.cab' for the rest)
# is hardcoded in the MSI's Media table - do not rename.
TREE_SUBDIRS=(
"dependencies/vcredist"
)
# Individual files copied flat (basename only) into installers/.
FLAT_INSTALLERS=(
"machineapps/UDC_Setup.exe"
"globalassets/oracleclient/Oracle 10.2.0.3.msi"
)
@@ -53,8 +52,6 @@ ssh_run() {
}
scp_to() {
# Remote path is double-escaped: outer ssh layer + inner shell layer.
# Wrap in single-quotes inside the destination so spaces in filenames survive.
sshpass -p "$PXE_PASS" scp -o StrictHostKeyChecking=no -o LogLevel=ERROR "$1" "$PXE_USER@$PXE_HOST:'$2'"
}
@@ -67,7 +64,19 @@ if [ ! -f "$PREINSTALL_JSON" ]; then
fi
missing=0
for rel in "${INSTALLERS[@]}"; do
for tree in "${TREE_SUBDIRS[@]}"; do
src="$PXE_IMAGES_DIR/$tree"
if [ ! -d "$src" ]; then
echo " MISSING: $src (directory)" >&2
missing=$((missing + 1))
else
size=$(du -sb "$src" | cut -f1)
printf " OK %10d %s/ (tree)\n" "$size" "$tree"
fi
done
for rel in "${FLAT_INSTALLERS[@]}"; do
src="$PXE_IMAGES_DIR/$rel"
if [ ! -f "$src" ]; then
echo " MISSING: $src" >&2
@@ -78,7 +87,7 @@ for rel in "${INSTALLERS[@]}"; do
done
if [ "$missing" -gt 0 ]; then
echo "ERROR: $missing installer file(s) missing in $PXE_IMAGES_DIR" >&2
echo "ERROR: $missing source(s) missing in $PXE_IMAGES_DIR" >&2
exit 1
fi
@@ -89,25 +98,42 @@ if ! ping -c 1 -W 2 "$PXE_HOST" >/dev/null 2>&1; then
exit 1
fi
# --- Build local tarball of all TREE_SUBDIRS ---
LOCAL_TARBALL="$(mktemp /tmp/preinstall-trees.XXXXXX.tar)"
trap 'rm -f "$LOCAL_TARBALL" "${LOCAL_TEMP_SCRIPT:-}"' EXIT
echo "Building local tarball of subtrees..."
tar_args=()
for tree in "${TREE_SUBDIRS[@]}"; do
parent_dir="$PXE_IMAGES_DIR/$(dirname "$tree")"
leaf="$(basename "$tree")"
tar_args+=( -C "$parent_dir" "$leaf" )
done
tar cf "$LOCAL_TARBALL" "${tar_args[@]}"
echo " -> $(stat -c %s "$LOCAL_TARBALL") bytes"
# --- Stage to /tmp on PXE, then sudo install ---
echo "Staging files to $PXE_HOST:$REMOTE_TEMP..."
ssh_run "mkdir -p $REMOTE_TEMP && rm -f $REMOTE_TEMP/*"
ssh_run "rm -rf $REMOTE_TEMP && mkdir -p $REMOTE_TEMP/flat"
# preinstall.json
echo " -> preinstall.json"
scp_to "$PREINSTALL_JSON" "$REMOTE_TEMP/preinstall.json"
# installers (preserve filenames including spaces)
for rel in "${INSTALLERS[@]}"; do
# tarball of subtrees
echo " -> trees.tar"
scp_to "$LOCAL_TARBALL" "$REMOTE_TEMP/trees.tar"
# flat installers (preserve filenames including spaces, all into flat/)
for rel in "${FLAT_INSTALLERS[@]}"; do
src="$PXE_IMAGES_DIR/$rel"
base="$(basename "$rel")"
echo " -> $base"
scp_to "$src" "$REMOTE_TEMP/$base"
scp_to "$src" "$REMOTE_TEMP/flat/$base"
done
# --- Build remote install script (runs under sudo on PXE) ---
LOCAL_TEMP_SCRIPT="$(mktemp /tmp/sync-preinstall-remote.XXXXXX.sh)"
trap 'rm -f "$LOCAL_TEMP_SCRIPT"' EXIT
cat > "$LOCAL_TEMP_SCRIPT" <<REMOTE_SCRIPT
#!/bin/bash
@@ -119,17 +145,32 @@ cp "$REMOTE_TEMP/preinstall.json" "$REMOTE_DIR/preinstall.json"
chmod 0644 "$REMOTE_DIR/preinstall.json"
chown root:root "$REMOTE_DIR/preinstall.json"
# All other files -> installers/
# Remove legacy flat VC++ files (replaced by vcredist/ subdir tree). These were the
# pre-Burn-extraction layout where 2008's MSI/CAB sat at the root and 2010/2012/2013/
# 2015 used bootstrappers directly. All four now live under vcredist/<version>/.
rm -f "$REMOTE_INSTALLERS/vc_red.msi" \
"$REMOTE_INSTALLERS/vc_red.cab" \
"$REMOTE_INSTALLERS/vcredist2008_x86.exe" \
"$REMOTE_INSTALLERS/vcredist2010_x86.exe" \
"$REMOTE_INSTALLERS/vcredist2012_x86.exe" \
"$REMOTE_INSTALLERS/vcredist2013_x86.exe" \
"$REMOTE_INSTALLERS/vcredist2015_2017_2019_2022_x86.exe"
# Extract tree tarball into installers/ (preserves subdirs)
tar xf "$REMOTE_TEMP/trees.tar" -C "$REMOTE_INSTALLERS/"
# Flat installers -> installers/
shopt -s dotglob nullglob
for f in "$REMOTE_TEMP"/*; do
for f in "$REMOTE_TEMP/flat/"*; do
base="\$(basename "\$f")"
if [ "\$base" != "preinstall.json" ] && [ "\$base" != "_install.sh" ]; then
cp "\$f" "$REMOTE_INSTALLERS/\$base"
chmod 0644 "$REMOTE_INSTALLERS/\$base"
chown root:root "$REMOTE_INSTALLERS/\$base"
fi
cp "\$f" "$REMOTE_INSTALLERS/\$base"
done
# Normalize ownership and perms (files 0644, dirs 0755, all root:root)
chown -R root:root "$REMOTE_INSTALLERS"
find "$REMOTE_INSTALLERS" -type f -exec chmod 0644 {} +
find "$REMOTE_INSTALLERS" -type d -exec chmod 0755 {} +
rm -rf "$REMOTE_TEMP"
echo
@@ -138,6 +179,9 @@ ls -la "$REMOTE_DIR"
echo
echo "Final state of $REMOTE_INSTALLERS:"
ls -la "$REMOTE_INSTALLERS"
echo
echo "vcredist tree:"
find "$REMOTE_INSTALLERS/vcredist" -type f -printf '%10s %p\n' 2>/dev/null | sort -k2
REMOTE_SCRIPT
# Stage the install script alongside the data files