From 64169819b387d5ff686e31b04bcf7c0072c11497 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Mon, 4 May 2026 08:42:33 -0400 Subject: [PATCH] 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) --- .../common/lib/Install-FromManifest.ps1 | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 b/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 index 4f37433..91b616d 100644 --- a/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 +++ b/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 @@ -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)" } - $proc = [System.Diagnostics.Process]::Start($psi) - $proc.WaitForExit() - return [pscustomobject]@{ ExitCode = $proc.ExitCode; LogRef = $App.LogFile } + try { + $proc = [System.Diagnostics.Process]::Start($psi) + $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' } { $installerPath = Join-Path $InstallerRoot $App.Installer