Install-FromManifest: InUseCheck ForceClose / CloseAndReopen

Adds partial Stage 2b support for InUseCheck entries in manifests. When
an entry declares InUseCheck.Behavior = ForceClose or CloseAndReopen and
the listed processes are running at install time, the lib now:

  1. Calls CloseMainWindow() on each matching Process handle (polite WM_CLOSE).
  2. Waits GracefulCloseTimeoutSec (default 10) for exit.
  3. Hard-kills the process if it did not exit gracefully.
  4. Proceeds with the install.

"CloseAndReopen" is currently treated the same as ForceClose - no reopen
happens today. Stage 2b will add the user-session scheduled-task trick
to relaunch the closed app in the logged-in user's session. In practice
for the 24/7 ShopFloor persistent-user pattern the operator relaunches
the app manually (or the app is registered as Startup and reopens on
the next reboot), which is acceptable.

Concrete impact: the eDNC entry in standard-machine/manifest.json lists
InUseCheck.Processes = DncMain + NTLARS with Behavior=CloseAndReopen. On
a retrofit or upgrade cycle that finds eDNC 6.4.3 needs to go to 6.4.5,
the lib now force-closes DncMain and NTLARS before msiexec rather than
risking Restart Manager silently scheduling a pending-file-replace that
does not actually upgrade until the next reboot (which on a 24/7 PC
might be never).

Verified in the Win11 analyzer VM against manifests declaring InUseCheck
on eDNC - logs show "InUseCheck: DncMain (PID ...) asked to close"
followed by either graceful exit or the force-kill path, then install
proceeds without 3010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-04-22 12:17:34 -04:00
parent eb68793e79
commit 70a36711c3

View File

@@ -416,6 +416,33 @@ foreach ($app in $config.Applications) {
continue continue
} }
# --- InUseCheck (partial Stage 2b): ForceClose / CloseAndReopen ---
# Before install, if the entry declares processes that would block the
# install, close them. Stage 2a supports ForceClose: polite WM_CLOSE
# with a timeout then hard Kill. "CloseAndReopen" is currently treated
# the same (Stage 2b will add the user-session relaunch trick). No
# reopen happens today; operator relaunches the app or the enforcer
# leaves it for Windows Explorer / Start-Menu shortcut behavior.
if ($app.InUseCheck -and $app.InUseCheck.Behavior -in @('ForceClose','CloseAndReopen')) {
foreach ($p in ($app.InUseCheck.Processes | Where-Object { $_ })) {
$procs = Get-Process -Name $p.Name -ErrorAction SilentlyContinue
foreach ($proc in $procs) {
$timeout = if ($p.GracefulCloseTimeoutSec) { [int]$p.GracefulCloseTimeoutSec } else { 10 }
try {
Write-InstallLog " InUseCheck: $($p.Name) (PID $($proc.Id)) asked to close (timeout ${timeout}s)"
$null = $proc.CloseMainWindow()
if (-not $proc.WaitForExit([int]($timeout * 1000))) {
Write-InstallLog " InUseCheck: $($p.Name) did not exit gracefully - killing" 'WARN'
$proc.Kill()
$proc.WaitForExit(5000) | Out-Null
}
} catch {
Write-InstallLog " InUseCheck: close/kill of $($p.Name) threw: $_" 'WARN'
}
}
}
}
$result = Invoke-InstallerAction -App $app $result = Invoke-InstallerAction -App $app
$rc = $result.ExitCode $rc = $result.ExitCode