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:
@@ -38,6 +38,11 @@ $ErrorActionPreference = 'Continue'
|
|||||||
# logged; manifests tagged with a newer MINOR are fine.
|
# logged; manifests tagged with a newer MINOR are fine.
|
||||||
#
|
#
|
||||||
# Changelog:
|
# 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, ...)
|
# 2.3 - PCTypes filter accepts old (Standard, Standard-Machine, CMM, ...)
|
||||||
# and new (gea-shopfloor-collections, gea-shopfloor-cmm, ...) names
|
# and new (gea-shopfloor-collections, gea-shopfloor-cmm, ...) names
|
||||||
# interchangeably via alias sets. Transitional for the rename reorg.
|
# 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,
|
# 2.0 - initial Stage 2a: PS1/BAT/File/Registry/INF action types,
|
||||||
# Always/MarkerFile/ValueMatches/pnputil detection, PCTypes filter
|
# Always/MarkerFile/ValueMatches/pnputil detection, PCTypes filter
|
||||||
$LIB_MANIFEST_MAJOR = 2
|
$LIB_MANIFEST_MAJOR = 2
|
||||||
$LIB_MANIFEST_MINOR = 3
|
$LIB_MANIFEST_MINOR = 4
|
||||||
|
|
||||||
$logDir = Split-Path -Parent $LogFile
|
$logDir = Split-Path -Parent $LogFile
|
||||||
if (-not (Test-Path $logDir)) {
|
if (-not (Test-Path $logDir)) {
|
||||||
@@ -258,13 +263,49 @@ function Invoke-InstallerAction {
|
|||||||
Write-InstallLog " EXE not found: $installerPath" 'ERROR'
|
Write-InstallLog " EXE not found: $installerPath" 'ERROR'
|
||||||
return [pscustomobject]@{ ExitCode = -1; LogRef = $null }
|
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 }
|
if ($App.InstallArgs) { $psi.Arguments = $App.InstallArgs }
|
||||||
Write-InstallLog " exe: $installerPath"
|
Write-InstallLog " exe: $runPath"
|
||||||
if ($App.InstallArgs) { Write-InstallLog " args: $($App.InstallArgs)" }
|
if ($App.InstallArgs) { Write-InstallLog " args: $($App.InstallArgs)" }
|
||||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
try {
|
||||||
$proc.WaitForExit()
|
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||||
return [pscustomobject]@{ ExitCode = $proc.ExitCode; LogRef = $App.LogFile }
|
$proc.WaitForExit()
|
||||||
|
$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' } {
|
{ $_ -eq 'CMD' -or $_ -eq 'BAT' } {
|
||||||
$installerPath = Join-Path $InstallerRoot $App.Installer
|
$installerPath = Join-Path $InstallerRoot $App.Installer
|
||||||
|
|||||||
Reference in New Issue
Block a user