Install-FromManifest: stage network-share EXE to local before invoking

Lib v2.4. Process.Start of an EXE that lives on a network share fails
with "Access is denied" when the dispatcher runs as SYSTEM, even when
the share is properly mounted via cmdkey + net use. Empirically
confirmed 2026-05-02 with UDC_Setup.exe via qga.

Fix: when the resolved EXE path is on a UNC or PSDrive-with-DisplayRoot
mount, copy the file into a per-cycle temp dir under $env:TEMP and run
from there. Cleanup happens in finally regardless of run outcome.
Cost is one transit per fire, which is rare in practice because most
EXE entries skip on subsequent cycles via DetectionMethod.

Validated on win11 VM with UDC_Setup.exe: dispatcher previously
returned blank exit code with "Access is denied" in stderr; now logs
"staged network EXE -> C:\WINDOWS\TEMP\ge-enforce-exe-..." and the
process runs to Exit 0 in ~18 seconds. UDC's separate "exit 0 without
actually installing" issue is a wrong-silent-flag in InstallArgs, not
this dispatcher fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-05-04 08:42:33 -04:00
parent 6dcf96e40a
commit 64169819b3

View File

@@ -38,6 +38,11 @@ $ErrorActionPreference = 'Continue'
# logged; manifests tagged with a newer MINOR are fine.
#
# Changelog:
# 2.4 - Type=EXE handler stages network-share EXEs to a local temp dir
# before invoking Process.Start. SYSTEM-context Process.Start
# fails with "Access is denied" on \\share or mapped-drive EXE
# paths (empirically confirmed with UDC_Setup.exe 2026-05-02).
# Local invocation works. Cleanup is best-effort in finally.
# 2.3 - PCTypes filter accepts old (Standard, Standard-Machine, CMM, ...)
# and new (gea-shopfloor-collections, gea-shopfloor-cmm, ...) names
# interchangeably via alias sets. Transitional for the rename reorg.
@@ -47,7 +52,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 = 3
$LIB_MANIFEST_MINOR = 4
$logDir = Split-Path -Parent $LogFile
if (-not (Test-Path $logDir)) {
@@ -258,13 +263,49 @@ function Invoke-InstallerAction {
Write-InstallLog " EXE not found: $installerPath" 'ERROR'
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
}
$psi.FileName = $installerPath
# Process.Start of an EXE that lives on a network share fails
# with "Access is denied" when the dispatcher runs as SYSTEM,
# even with a valid mount and no Mark-of-the-Web. Empirically
# confirmed 2026-05-02 with UDC_Setup.exe via qga: same path
# works once copied to a local C:\ dir. Stage to a per-cycle
# temp dir, run from there, clean up afterwards. Costs the
# transit time once per fire (most EXEs skip on subsequent
# cycles via detection so this rarely repeats).
$runPath = $installerPath
$isNetworkPath = ($installerPath -match '^\\\\') -or ($installerPath -match '^[A-Z]:\\' -and (Get-PSDrive -Name $installerPath.Substring(0,1) -ErrorAction SilentlyContinue).DisplayRoot)
$stagedPath = $null
if ($isNetworkPath) {
$stagedDir = Join-Path $env:TEMP ("ge-enforce-exe-" + [Guid]::NewGuid().ToString('N').Substring(0,8))
try {
New-Item -ItemType Directory -Path $stagedDir -Force | Out-Null
$leaf = Split-Path -Leaf $installerPath
$stagedPath = Join-Path $stagedDir $leaf
Copy-Item -LiteralPath $installerPath -Destination $stagedPath -Force -ErrorAction Stop
$runPath = $stagedPath
Write-InstallLog " staged network EXE -> $runPath"
} catch {
Write-InstallLog " failed to stage EXE locally: $_ - attempting direct invocation" 'WARN'
}
}
$psi.FileName = $runPath
if ($App.InstallArgs) { $psi.Arguments = $App.InstallArgs }
Write-InstallLog " exe: $installerPath"
Write-InstallLog " exe: $runPath"
if ($App.InstallArgs) { Write-InstallLog " args: $($App.InstallArgs)" }
try {
$proc = [System.Diagnostics.Process]::Start($psi)
$proc.WaitForExit()
return [pscustomobject]@{ ExitCode = $proc.ExitCode; LogRef = $App.LogFile }
$exitCode = $proc.ExitCode
} catch {
Write-InstallLog " Process.Start failed: $_" 'ERROR'
$exitCode = -1
} finally {
if ($stagedPath) {
try { Remove-Item -LiteralPath (Split-Path -Parent $stagedPath) -Recurse -Force -ErrorAction Stop } catch {}
}
}
return [pscustomobject]@{ ExitCode = $exitCode; LogRef = $App.LogFile }
}
{ $_ -eq 'CMD' -or $_ -eq 'BAT' } {
$installerPath = Join-Path $InstallerRoot $App.Installer