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