diff --git a/CHANGELOG.md b/CHANGELOG.md index fdcb605ad..2d5950308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve ## [Unreleased] +## [v0.6.12] - 2026-04-01 + +### Fixed + +- Reworked Windows preflight and Docker runtime-manager process capture so + large `docker manifest inspect` payloads no longer deadlock the bounded + timeout path, preserving truthful Windows NI preflight and runtime + determinism outcomes instead of hanging the release-proof surface. + +### Added + +- Regression coverage in `tests/Invoke-DockerRuntimeManager.Tests.ps1` and + `tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1` proving large manifest + output still completes cleanly through the shared timeout helper. + +### Changed + +- Built the `v0.6.12` release line on top of the published `v0.6.11` stable + baseline, preserving the Windows NI proof authority and VI-history + native-path repair while adding the preflight deadlock fix required for a + clean release rail. + ## [v0.6.11] - 2026-04-01 ### Fixed diff --git a/Directory.Build.props b/Directory.Build.props index eb321ef70..936fc2e2e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,9 +7,9 @@ false true true - 0.6.11 - 0.6.11.0 - 0.6.11.0 + 0.6.12 + 0.6.12.0 + 0.6.12.0 $(Version)+local package-first project diff --git a/Invoke-PesterTests.ps1 b/Invoke-PesterTests.ps1 index e4a7ee1c4..4ae31a763 100644 --- a/Invoke-PesterTests.ps1 +++ b/Invoke-PesterTests.ps1 @@ -567,6 +567,9 @@ $ErrorActionPreference = 'Stop' if (-not (Get-Variable -Name includeIntegrationBool -Scope Script -ErrorAction SilentlyContinue)) { $script:includeIntegrationBool = $false } +$script:stuckGuardEnabled = $false +$script:hbPath = $null +$script:partialLogPath = $null $script:executionPackResolved = 'full' $script:executionPackReason = 'default' $script:executionPackBaseIncludePatterns = @() @@ -1204,11 +1207,6 @@ Write-Host " Script Root: $root" Write-Host " Tests Directory: $testsDir"; if ($limitToSingle) { Write-Host " Single Test File: $singleTestFile" } Write-Host " Results Directory: $resultsDir" Write-Host "" -$script:dispatcherEventsPath = Join-Path $resultsDir 'dispatcher-events.ndjson' -Write-DispatcherEventLine -Level info -Phase 'lifecycle' -Message 'Dispatcher session initialized.' -Data @{ - testsDir = $testsDir - resultsDir = $resultsDir -} # Validate tests directory exists if (-not (Test-Path -LiteralPath $testsDir -PathType Container)) { @@ -1306,6 +1304,27 @@ function _Build-Snapshot { return $index } +# Artifact tracking pre-snapshot (optional). Capture before any dispatcher-owned +# result files are written so isolated runs report those files as created rather +# than modified. +$script:artifactTrail = $null +$script:executionFinalizeContextPath = $null +$preIndex = $null +$artifactRoots = @() +if ($TrackArtifacts) { + if (-not $ArtifactGlobs -or $ArtifactGlobs.Count -eq 0) { + $ArtifactGlobs = @('tests/results','results','tmp-agg/results','scratch-schema-test/results') + } + $artifactRoots = _Resolve-ArtifactRoots -Roots $ArtifactGlobs -Base $root + try { $preIndex = _Build-Snapshot -Roots $artifactRoots -Base $root } catch { $preIndex = @{} } +} + +$script:dispatcherEventsPath = Join-Path $resultsDir 'dispatcher-events.ndjson' +Write-DispatcherEventLine -Level info -Phase 'lifecycle' -Message 'Dispatcher session initialized.' -Data @{ + testsDir = $testsDir + resultsDir = $resultsDir +} + # Hard gate: never start tests while LabVIEW.exe is running $labviewOpen = @(_Find-ProcsByPattern -Patterns @('LabVIEW') ) if ($labviewOpen.Count -gt 0) { @@ -1765,19 +1784,6 @@ if ($CleanLabVIEW) { _Report-Procs -Names @('LabVIEW') } -# Artifact tracking pre-snapshot (optional) -$script:artifactTrail = $null -$script:executionFinalizeContextPath = $null -$preIndex = $null -$artifactRoots = @() -if ($TrackArtifacts) { - if (-not $ArtifactGlobs -or $ArtifactGlobs.Count -eq 0) { - $ArtifactGlobs = @('tests/results','results','tmp-agg/results','scratch-schema-test/results') - } - $artifactRoots = _Resolve-ArtifactRoots -Roots $ArtifactGlobs -Base $root - try { $preIndex = _Build-Snapshot -Roots $artifactRoots -Base $root } catch { $preIndex = @{} } -} - # Count test files (respect single file mode) if ($limitToSingle) { $testFiles = @([IO.FileInfo]::new($singleTestFile)) diff --git a/docs/archive/releases/RELEASE_NOTES_v0.6.12.md b/docs/archive/releases/RELEASE_NOTES_v0.6.12.md new file mode 100644 index 000000000..29a575938 --- /dev/null +++ b/docs/archive/releases/RELEASE_NOTES_v0.6.12.md @@ -0,0 +1,38 @@ +# Release Notes v0.6.12 + +`v0.6.12` is a maintenance release that carries the published `v0.6.11` stable +line forward and fixes the Windows preflight process-capture deadlock exposed +when Docker returns large manifest output. + +## Highlights + +- The released backend no longer deadlocks while collecting large Docker + manifest payloads during Windows preflight and runtime-manager checks. +- A shared timeout helper now owns bounded process execution and concurrent + stdout/stderr draining for the Windows preflight seam. +- The stable release keeps the `v0.6.11` Windows NI proof authority and + VI-history native-path repair instead of regressing to an older integration + surface. + +## Included maintenance slice + +- `#2091` fix: Windows preflight process capture deadlock +- carry forward the published `v0.6.11` release-line policy and proof surfaces + +## Validation highlights + +- Focused Pester coverage proves large-manifest replay no longer blocks either + `tools/Invoke-DockerRuntimeManager.ps1` or + `tools/Test-WindowsNI2026q1HostPreflight.ps1`. +- The release branch is prepared from the published `v0.6.11` line and merged + with the post-`#2091` `develop` tip, preserving the already-published stable + fixes while adding the Windows preflight deadlock repair. +- Release helper docs, changelog, and version surfaces align on `0.6.12`. + +## Consumer impact + +- Stable consumers should move from `@v0.6.11` to `@v0.6.12` to pick up the + Windows preflight/runtime-manager deadlock repair. +- `comparevi-history` should repin `comparevi-backend-ref.txt` to `v0.6.12` + before rerunning the clone-backed `ni/labview-icon-editor` proof on the + released backend. diff --git a/docs/release/PR_NOTES.md b/docs/release/PR_NOTES.md index 27026a95f..dc0f0b8f4 100644 --- a/docs/release/PR_NOTES.md +++ b/docs/release/PR_NOTES.md @@ -1,37 +1,41 @@ -# Release v0.6.11 - PR Notes Helper +# Release v0.6.12 - PR Notes Helper -Reference sheet for the `v0.6.11` maintenance release. This cut carries the -`v0.6.10` stable line forward and adds the VI-history native-path repair proven -on the Windows NI Docker-backed proof surface. +Reference sheet for the `v0.6.12` maintenance release. This cut carries the +published `v0.6.11` stable line forward and adds the Windows preflight +process-capture deadlock fix proven on the Windows NI Docker-backed proof +surface. ## 1. Summary -Release `v0.6.11` focuses on three themes: +Release `v0.6.12` focuses on three themes: -- **VI-history native-path correctness**: the released backend now resolves - Windows NI proof-surface file paths without losing the repository-relative VI - target, so hosted and local replay lanes can reach the real history target - instead of a synthetic temp-root mismatch. +- **Deadlock-free Windows preflight capture**: the released backend now drains + large Docker manifest output without stalling the bounded timeout path, so + Windows NI preflight and runtime-manager checks can report real readiness + instead of hanging. - **Windows NI proof continuity**: the LabVIEW Docker-backed Windows proof path - remains the authoritative hosted execution surface, and `v0.6.11` is cut - directly from the `v0.6.10` stable line so the released proof authority is - preserved rather than reintroduced from stale `develop`. + remains the authoritative hosted execution surface, and `v0.6.12` is cut + directly from the published `v0.6.11` stable line so the released proof + authority is preserved rather than reintroduced from stale `develop`. - **Consumer-ready repin path**: the release packet is aligned for the next `comparevi-history` pin bump and the clone-backed `ni/labview-icon-editor` history proof rerun on the newly published backend. ## 2. Maintenance Highlights -- `tools/Compare-VIHistory.ps1`, `tools/Compare-RefsToTemp.ps1`, and - `tools/Render-VIHistoryReport.ps1` now preserve the intended VI-history - target path across the Windows NI proof surface instead of collapsing to a - host-native path that the replay layer cannot certify. -- `tests/TestFileExistsAtRef.Tests.ps1` and - `tests/CompareVI.GitRefs.VI2.Tests.ps1` now cover the backend-side path and - git-ref seams that caused the Windows proof regression. -- Stable release surfaces now pin `0.6.11`, while the helper docs still point - consumers at `v0.6.10` until publication completes. +- `tools/ProcessTimeoutHelper.ps1` now centralizes bounded process execution + and concurrent stdout/stderr capture for the Docker-backed Windows proof + seam. +- `tools/Invoke-DockerRuntimeManager.ps1`, + `tools/Assert-DockerRuntimeDeterminism.ps1`, and + `tools/Test-WindowsNI2026q1HostPreflight.ps1` now share that helper so large + manifest responses cannot deadlock the release-proof path. +- `tests/Invoke-DockerRuntimeManager.Tests.ps1` and + `tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1` now prove large manifest + output stays non-blocking on the authoritative Windows surface. +- Stable release surfaces now pin `0.6.12`, while the helper docs still point + consumers at `v0.6.11` until publication completes. ## 3. Validation Snapshot @@ -44,12 +48,12 @@ Release `v0.6.11` focuses on three themes: - [ ] Hosted Windows NI Docker proof is green on the release branch. - [ ] Local-proof autonomy selector still emits the truthful next proof surface instead of looping or hanging. -- [ ] `node tools/npm/run-script.mjs release:finalize -- 0.6.11` completes from +- [ ] `node tools/npm/run-script.mjs release:finalize -- 0.6.12` completes from a clean helper lane and writes fresh finalize metadata under `tests/results/_agent/release/` -- [ ] Published release `v0.6.11` includes the signed distribution assets, +- [ ] Published release `v0.6.12` includes the signed distribution assets, `SHA256SUMS.txt`, `sbom.spdx.json`, and `provenance.json` -- [ ] `comparevi-history` repins `comparevi-backend-ref.txt` to `v0.6.11` +- [ ] `comparevi-history` repins `comparevi-backend-ref.txt` to `v0.6.12` before the clone-backed `ni/labview-icon-editor` proof is rerun ## 4. Reviewer Focus @@ -58,23 +62,25 @@ Release `v0.6.11` focuses on three themes: - `package.json` - `Directory.Build.props` - `tools/CompareVI.Tools/CompareVI.Tools.psd1` -- Review the released Windows NI Docker proof and VI-history backend surfaces for correctness: - - `.github/workflows/windows-ni-proof-reusable.yml` - - `tools/Compare-VIHistory.ps1` - - `tools/Compare-RefsToTemp.ps1` - - `tools/Render-VIHistoryReport.ps1` +- Review the released Windows preflight/runtime-manager surfaces for + correctness: + - `tools/ProcessTimeoutHelper.ps1` + - `tools/Invoke-DockerRuntimeManager.ps1` + - `tools/Test-WindowsNI2026q1HostPreflight.ps1` + - `tests/Invoke-DockerRuntimeManager.Tests.ps1` + - `tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1` - Review the release helper packet for consistency: - `CHANGELOG.md` - `docs/release/TAG_PREP_CHECKLIST.md` - - `docs/archive/releases/RELEASE_NOTES_v0.6.11.md` + - `docs/archive/releases/RELEASE_NOTES_v0.6.12.md` ## 5. Follow-Up After Stable -1. Re-pin `comparevi-history` from `v0.6.10` to `v0.6.11` and rerun the +1. Re-pin `comparevi-history` from `v0.6.11` to `v0.6.12` and rerun the clone-backed `ni/labview-icon-editor` proof on the released backend. 2. Confirm the Windows NI proof artifacts and the published benchmark packet still agree on the certified backend version after the repin. 3. Re-evaluate the current emitted history surface against the real developer question before treating any mode as decision-ready. ---- Updated: 2026-04-01 (prepared for the `v0.6.11` maintenance cut). +--- Updated: 2026-04-01 (prepared for the `v0.6.12` maintenance cut). diff --git a/docs/release/README.md b/docs/release/README.md index ab9ed6058..0aa8ed8a7 100644 --- a/docs/release/README.md +++ b/docs/release/README.md @@ -7,7 +7,9 @@ work. ## Contents - `PR_NOTES.md` - release PR summary helper -- `RELEASE_EVIDENCE_v0.6.6.md` - latest immutable release and certified-consumer evidence packet retained in-repo while the `v0.6.11` cut supersedes the published `v0.6.10` baseline +- `RELEASE_EVIDENCE_v0.6.6.md` - latest immutable release and + certified-consumer evidence packet retained in-repo while the `v0.6.12` cut + supersedes the published `v0.6.11` baseline - `TAG_PREP_CHECKLIST.md` - tag preparation checklist - `POST_RELEASE_FOLLOWUPS.md` - post-release backlog tracker - `ROLLBACK_PLAN.md` - rollback procedure reference diff --git a/docs/release/TAG_PREP_CHECKLIST.md b/docs/release/TAG_PREP_CHECKLIST.md index a99859896..2e473222b 100644 --- a/docs/release/TAG_PREP_CHECKLIST.md +++ b/docs/release/TAG_PREP_CHECKLIST.md @@ -1,14 +1,14 @@ -# v0.6.11 Tag Preparation Checklist +# v0.6.12 Tag Preparation Checklist -Helper reference for cutting or replaying the `v0.6.11` maintenance release. +Helper reference for cutting or replaying the `v0.6.12` maintenance release. Aligns with the archived release notes -(`../archive/releases/RELEASE_NOTES_v0.6.11.md`) and the checked-in stable +(`../archive/releases/RELEASE_NOTES_v0.6.12.md`) and the checked-in stable release surfaces. ## 1. Pre-flight Verification -- [ ] Work from `release/v0.6.11` and ensure it contains the final maintenance +- [ ] Work from `release/v0.6.12` and ensure it contains the final maintenance changes. - [ ] CI is green on the release branch (`lint`, `pester / normalize`, `smoke-gate`, `Policy Guard (Upstream) / policy-guard`, @@ -22,26 +22,30 @@ release surfaces. ## 2. Version & Metadata Consistency - [ ] `CHANGELOG.md` contains a finalized - `## [v0.6.11] - 2026-04-01` section. -- [ ] Stable docs reference `v0.6.10` consistently until `v0.6.11` publication - completes, and the release helper packet references `v0.6.11` + `## [v0.6.12] - 2026-04-01` section. +- [ ] Stable docs reference `v0.6.11` consistently until `v0.6.12` publication + completes, and the release helper packet references `v0.6.12` consistently. - [ ] `package.json`, `Directory.Build.props`, and - `tools/CompareVI.Tools/CompareVI.Tools.psd1` all report `0.6.11`. + `tools/CompareVI.Tools/CompareVI.Tools.psd1` all report `0.6.12`. - [ ] `docs/action-outputs.md` still matches `action.yml`. - [ ] Update `docs/documentation-manifest.json` if release-doc coverage changed. -## 3. Bundle Contract Regression Validation +## 3. Windows Preflight Regression Validation -- [ ] Focused bundle regression tests pass locally: +- [ ] Focused Windows preflight/runtime-manager regression tests pass locally: ```bash -pwsh -NoLogo -NoProfile -File tools/Test-CompareVIHistoryBundleCertification.ps1 -BundleArchivePath tests/results/_agent/bundle-fix/artifacts/CompareVI.Tools-v0.6.11.zip -ResultsDir tests/results/_agent/bundle-fix/certification -SummaryJsonPath tests/results/_agent/bundle-fix/certification/summary.json +pwsh -NoLogo -NoProfile -File Invoke-PesterTests.ps1 -TestsPath tests/Invoke-DockerRuntimeManager.Tests.ps1 +pwsh -NoLogo -NoProfile -File Invoke-PesterTests.ps1 -TestsPath tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1 ``` - [ ] Confirm the released Windows NI / LabVIEW Docker image proof surface is intact: - [ ] The released surface still includes: + `tools/ProcessTimeoutHelper.ps1` + `tools/Invoke-DockerRuntimeManager.ps1` + and `tools/Run-NIWindowsContainerCompare.ps1` and `tools/Test-WindowsNI2026q1HostPreflight.ps1` @@ -56,9 +60,9 @@ pwsh -NoLogo -NoProfile -File tools/Test-CompareVIHistoryBundleCertification.ps1 ## 4. Release Materials Review - [ ] `PR_NOTES.md`, this checklist, and - `../archive/releases/RELEASE_NOTES_v0.6.11.md` are consistent. -- [ ] `README.md` and `docs/USAGE_GUIDE.md` still treat `v0.6.10` as the - previously released stable pin until `v0.6.11` publication completes. + `../archive/releases/RELEASE_NOTES_v0.6.12.md` are consistent. +- [ ] `README.md` and `docs/USAGE_GUIDE.md` still treat `v0.6.11` as the + previously released stable pin until `v0.6.12` publication completes. ## 5. Tag Creation @@ -66,7 +70,7 @@ pwsh -NoLogo -NoProfile -File tools/Test-CompareVIHistoryBundleCertification.ps1 ```pwsh node tools/npm/run-script.mjs priority:release:signing:readiness -node tools/npm/run-script.mjs priority:release:conductor -- --apply --channel stable --version 0.6.11 +node tools/npm/run-script.mjs priority:release:conductor -- --apply --channel stable --version 0.6.12 ``` - [ ] Confirm `tests/results/_agent/release/release-signing-readiness.json` @@ -77,35 +81,35 @@ node tools/npm/run-script.mjs priority:release:conductor -- --apply --channel st - [ ] Create an annotated stable tag: ```pwsh -git tag -a v0.6.11 -m "v0.6.11: repair VI history native paths on Windows proof surfaces" +git tag -a v0.6.12 -m "v0.6.12: fix Windows preflight process capture deadlock" ``` - [ ] Push the tag: ```pwsh -git push origin v0.6.11 +git push origin v0.6.12 ``` ## 6. Validation After Publish -- [ ] Run `node tools/npm/run-script.mjs release:finalize -- 0.6.11` from a +- [ ] Run `node tools/npm/run-script.mjs release:finalize -- 0.6.12` from a clean helper lane to fast-forward `main` and `develop`, then record the finalize metadata. -- [ ] Install the bundle via `@v0.6.11` in a sample workflow and confirm the +- [ ] Install the bundle via `@v0.6.12` in a sample workflow and confirm the released Windows NI and hosted VI-history contracts execute without a local source-tree override. - [ ] Optional maintainer fast loop: run the local Windows Docker replay lane for `vi-history-scenarios-windows` and confirm it still mirrors the hosted contract without replacing the hosted proof requirement. -- [ ] Re-pin `comparevi-history` to `v0.6.11` and confirm the clone-backed +- [ ] Re-pin `comparevi-history` to `v0.6.12` and confirm the clone-backed `ni/labview-icon-editor` proof reaches real comparisons on the released backend. ## 7. Communication -- [ ] Announce the maintenance cut, calling out the promoted Windows NI Docker - proof surface and the required `comparevi-history` repin. -- [ ] Notify consumers that `v0.6.11` supersedes `v0.6.10` as the supported +- [ ] Announce the maintenance cut, calling out the Windows preflight deadlock + fix and the required `comparevi-history` repin. +- [ ] Notify consumers that `v0.6.12` supersedes `v0.6.11` as the supported stable pin. ---- Updated: 2026-04-01 (prepared for the `v0.6.11` maintenance cut). +--- Updated: 2026-04-01 (prepared for the `v0.6.12` maintenance cut). diff --git a/package-lock.json b/package-lock.json index 7dc06fb2a..c73cc2f1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "compare-vi-cli-action", - "version": "0.6.11", + "version": "0.6.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "compare-vi-cli-action", - "version": "0.6.11", + "version": "0.6.12", "license": "BSD-3-Clause", "dependencies": { "argparse": "^2.0.1", diff --git a/package.json b/package.json index 333ec937c..7516971f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "compare-vi-cli-action", "private": true, - "version": "0.6.11", + "version": "0.6.12", "license": "BSD-3-Clause", "type": "module", "scripts": { diff --git a/tests/Assert-DockerRuntimeDeterminism.Tests.ps1 b/tests/Assert-DockerRuntimeDeterminism.Tests.ps1 index 498c949ff..bffbd1067 100644 --- a/tests/Assert-DockerRuntimeDeterminism.Tests.ps1 +++ b/tests/Assert-DockerRuntimeDeterminism.Tests.ps1 @@ -157,7 +157,7 @@ exit 0 "@ Set-Content -LiteralPath (Join-Path $binDir 'wsl.cmd') -Value $wslCmd -Encoding ascii - $env:PATH = "{0};{1}" -f $binDir, $env:PATH + $env:PATH = "{0}{1}{2}" -f $binDir, [System.IO.Path]::PathSeparator, $env:PATH } } @@ -173,6 +173,7 @@ exit 0 DOCKER_STUB_CONTEXT_USE_FAIL_TARGET = $env:DOCKER_STUB_CONTEXT_USE_FAIL_TARGET DOCKER_COMMAND_OVERRIDE = $env:DOCKER_COMMAND_OVERRIDE DOCKER_HOST = $env:DOCKER_HOST + RUNNER_OS = $env:RUNNER_OS } } @@ -209,11 +210,13 @@ exit 0 Set-Item Env:DOCKER_STUB_INFO_MODE 'daemon-unavailable' Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType windows ` -ExpectedContext desktop-windows ` + -HostPlatformOverride Windows ` -AutoRepair:$false ` -SnapshotPath $snapshotPath ` -GitHubOutputPath '' 2>&1 @@ -259,11 +262,13 @@ exit 9 Set-Item Env:DOCKER_COMMAND_OVERRIDE (Join-Path $work 'bin' 'docker.ps1') Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-linux' Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-linux' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType linux ` -ExpectedContext desktop-linux ` + -HostPlatformOverride Windows ` -AutoRepair:$false ` -SnapshotPath $snapshotPath ` -GitHubOutputPath '' 2>&1 @@ -281,12 +286,14 @@ exit 9 Set-Item Env:DOCKER_STUB_INFO_MODE 'unparseable-success' Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $githubOutput = Join-Path $work 'github-output.txt' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType windows ` -ExpectedContext desktop-windows ` + -HostPlatformOverride Windows ` -AutoRepair:$false ` -SnapshotPath $snapshotPath ` -GitHubOutputPath $githubOutput 2>&1 @@ -308,11 +315,13 @@ exit 9 Set-Item Env:DOCKER_STUB_INFO_MODE 'daemon-unavailable' Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType windows ` -ExpectedContext desktop-windows ` + -HostPlatformOverride Windows ` -AutoRepair:$true ` -ManageDockerEngine:$true ` -SnapshotPath $snapshotPath ` @@ -333,11 +342,13 @@ exit 9 Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-linux' Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-linux' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType windows ` -ExpectedContext desktop-windows ` + -HostPlatformOverride Windows ` -AutoRepair:$true ` -ManageDockerEngine:$true ` -AllowHostEngineMutation:$false ` @@ -365,6 +376,7 @@ exit 9 Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-windows' Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $githubOutput = Join-Path $work 'github-output.txt' @@ -372,6 +384,7 @@ exit 9 $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType windows ` -ExpectedContext desktop-windows ` + -HostPlatformOverride Windows ` -AutoRepair:$true ` -SnapshotPath $snapshotPath ` -GitHubOutputPath $githubOutput 2>&1 @@ -399,11 +412,13 @@ exit 9 Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-windows' Set-Item Env:DOCKER_STUB_CONTEXT 'default' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType windows ` -ExpectedContext desktop-windows ` + -HostPlatformOverride Windows ` -AutoRepair:$true ` -SnapshotPath $snapshotPath ` -GitHubOutputPath '' 2>&1 @@ -424,11 +439,13 @@ exit 9 Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-windows' Set-Item Env:DOCKER_STUB_CONTEXT 'default' Set-Item Env:DOCKER_STUB_INFO_FAIL_CONTEXT 'desktop-windows' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType windows ` -ExpectedContext desktop-windows ` + -HostPlatformOverride Windows ` -AutoRepair:$true ` -SnapshotPath $snapshotPath ` -GitHubOutputPath '' 2>&1 @@ -448,12 +465,14 @@ exit 9 Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-linux' Set-Item Env:DOCKER_STUB_INFO_JSON '{"OSType":"linux","OperatingSystem":"Ubuntu 24.04.1 LTS","Name":"ubuntu-native","Platform":{"Name":"Docker Engine - Community"},"Labels":["maintainer=comparevi"]}' Set-Item Env:DOCKER_HOST 'unix:///var/run/docker.sock' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType linux ` -RuntimeProvider native-wsl ` -ExpectedDockerHost 'unix:///var/run/docker.sock' ` + -HostPlatformOverride Windows ` -AutoRepair:$true ` -SnapshotPath $snapshotPath ` -GitHubOutputPath '' 2>&1 @@ -478,12 +497,14 @@ exit 9 Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-linux' Set-Item Env:DOCKER_STUB_INFO_JSON '{"OSType":"linux","OperatingSystem":"Docker Desktop","Name":"docker-desktop","Platform":{"Name":"Docker Desktop 4.41.0"},"Labels":["com.docker.desktop.address=npipe://"]}' Set-Item Env:DOCKER_HOST 'unix:///var/run/docker.sock' + Set-Item Env:RUNNER_OS 'Windows' $snapshotPath = Join-Path $work 'runtime.json' $output = & pwsh -NoLogo -NoProfile -File $script:GuardScript ` -ExpectedOsType linux ` -RuntimeProvider native-wsl ` -ExpectedDockerHost 'unix:///var/run/docker.sock' ` + -HostPlatformOverride Windows ` -AutoRepair:$true ` -SnapshotPath $snapshotPath ` -GitHubOutputPath '' 2>&1 diff --git a/tests/FunctionShadowing.Nested.Tests.ps1 b/tests/FunctionShadowing.Nested.Tests.ps1 index de638d85e..f9bc87ad3 100644 --- a/tests/FunctionShadowing.Nested.Tests.ps1 +++ b/tests/FunctionShadowing.Nested.Tests.ps1 @@ -41,9 +41,19 @@ Describe "Inner Smoke" { $toolsDir = Join-Path $workspace 'tools' New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null - $trackerModule = Join-Path $repoRoot 'tools' 'LabVIEWPidTracker.psm1' - if (Test-Path -LiteralPath $trackerModule -PathType Leaf) { - Copy-Item -Path $trackerModule -Destination $toolsDir -Force + # Keep the nested workspace aligned with dispatcher-side tool growth. + $supportFiles = @( + 'LabVIEWPidTracker.psm1', + 'PesterExecutionPacks.ps1', + 'Invoke-PesterExecutionPostprocess.ps1', + 'PesterServiceModelSchema.ps1', + 'Get-PesterResultXmlSummary.ps1' + ) + foreach ($supportFile in $supportFiles) { + $supportPath = Join-Path $repoRoot 'tools' $supportFile + if (Test-Path -LiteralPath $supportPath -PathType Leaf) { + Copy-Item -Path $supportPath -Destination $toolsDir -Force + } } $dispatcherTools = Join-Path $repoRoot 'tools' 'Dispatcher' @@ -68,7 +78,12 @@ Describe "Inner Smoke" { $nestedExit | Should -Be 0 $json.failed | Should -Be 0 $json.errors | Should -Be 0 - $json.discoveryFailures | Should -Be 0 -Because 'Shadowing smoke test should not introduce discovery failures' + $discoveryFailures = if ($json.PSObject.Properties.Name -contains 'discoveryFailures') { + [int]$json.discoveryFailures + } else { + 0 + } + $discoveryFailures | Should -Be 0 -Because 'Shadowing smoke test should not introduce discovery failures' if ($json.failed -gt 0 -or $json.errors -gt 0 -or $json.discoveryFailures -gt 0) { Write-Host '[nested-shadow] DEBUG OUTPUT START' -ForegroundColor Yellow ($innerOutput | Out-String) | Write-Host diff --git a/tests/Invoke-DockerRuntimeManager.Tests.ps1 b/tests/Invoke-DockerRuntimeManager.Tests.ps1 index 79753c7b9..f85067d63 100644 --- a/tests/Invoke-DockerRuntimeManager.Tests.ps1 +++ b/tests/Invoke-DockerRuntimeManager.Tests.ps1 @@ -109,8 +109,16 @@ if ($Args[0] -eq 'info') { } if ($Args[0] -eq 'manifest' -and $Args.Count -ge 3 -and $Args[1] -eq 'inspect') { + $paddingBytes = [Environment]::GetEnvironmentVariable('DOCKER_STUB_MANIFEST_PADDING_BYTES') + $padding = '' + if (-not [string]::IsNullOrWhiteSpace($paddingBytes)) { + $padding = ('x' * [int]$paddingBytes) + } $manifest = [ordered]@{ schemaVersion = 2 + annotations = [ordered]@{ + padding = $padding + } manifests = @( [ordered]@{ digest = 'sha256:1111111111111111111111111111111111111111111111111111111111111111' @@ -242,6 +250,7 @@ exit 0 DOCKER_STUB_RUN_FAIL_LINUX = $env:DOCKER_STUB_RUN_FAIL_LINUX DOCKER_STUB_INFO_SLEEP_SECONDS = $env:DOCKER_STUB_INFO_SLEEP_SECONDS DOCKER_STUB_INSPECT_SLEEP_SECONDS = $env:DOCKER_STUB_INSPECT_SLEEP_SECONDS + DOCKER_STUB_MANIFEST_PADDING_BYTES = $env:DOCKER_STUB_MANIFEST_PADDING_BYTES DOCKER_STUB_PULL_SLEEP_WINDOWS = $env:DOCKER_STUB_PULL_SLEEP_WINDOWS DOCKER_STUB_PULL_SLEEP_LINUX = $env:DOCKER_STUB_PULL_SLEEP_LINUX DOCKER_STUB_RUN_SLEEP_WINDOWS = $env:DOCKER_STUB_RUN_SLEEP_WINDOWS @@ -456,6 +465,32 @@ exit 0 $json.probes.windows.probe.status | Should -Be 'timeout' } + It 'handles large manifest output without deadlocking the timeout helper' { + $work = Join-Path $TestDrive 'large-manifest-output' + New-Item -ItemType Directory -Path $work -Force | Out-Null + & $script:CreateDockerStub -WorkRoot $work + + Set-Item Env:DOCKER_STUB_STATE_PATH (Join-Path $work 'docker-state.json') + Set-Item Env:DOCKER_STUB_INITIAL_CONTEXT 'desktop-windows' + Set-Item Env:DOCKER_STUB_MANIFEST_PADDING_BYTES '20000' + Set-Item Env:RUNNER_TEMP (Join-Path $work 'runner-temp') + + $jsonPath = Join-Path $work 'docker-runtime-manager.json' + $output = @(& pwsh -NoLogo -NoProfile -File $script:ManagerScript ` + -ProbeScope windows ` + -OutputJsonPath $jsonPath ` + -CommandTimeoutSeconds 5 ` + -SwitchRetryCount 1 ` + -SwitchTimeoutSeconds 30 2>&1) + + $LASTEXITCODE | Should -Be 0 -Because ($output -join "`n") + + $json = Get-Content -LiteralPath $jsonPath -Raw | ConvertFrom-Json -Depth 30 + $json.status | Should -Be 'success' + $json.probes.windows.status | Should -Be 'success' + $json.probes.windows.digest | Should -Be 'sha256:1111111111111111111111111111111111111111111111111111111111111111' + } + It 'fails with lock timeout when the runtime manager lock is held by another process' { $work = Join-Path $TestDrive 'lock-timeout' New-Item -ItemType Directory -Path $work -Force | Out-Null diff --git a/tests/NestedDispatcher.DiscoveryFailureRegression.Tests.ps1 b/tests/NestedDispatcher.DiscoveryFailureRegression.Tests.ps1 index c80f4da4d..e95c05cb1 100644 --- a/tests/NestedDispatcher.DiscoveryFailureRegression.Tests.ps1 +++ b/tests/NestedDispatcher.DiscoveryFailureRegression.Tests.ps1 @@ -23,12 +23,26 @@ Describe 'Nested Dispatcher Discovery Failure Regression' -Tag 'Unit' { Set-Content -Path (Join-Path $testsDir 'Broken.Tests.ps1') -Value $badLines -Encoding UTF8 $dispatcherCopy = Join-Path $workspace 'Invoke-PesterTests.ps1' - Copy-Item -Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'Invoke-PesterTests.ps1') -Destination $dispatcherCopy + $repoRoot = Split-Path $PSScriptRoot -Parent + Copy-Item -Path (Join-Path $repoRoot 'Invoke-PesterTests.ps1') -Destination $dispatcherCopy $toolsDir = Join-Path $workspace 'tools' - $trackerModule = Join-Path (Split-Path $PSScriptRoot -Parent) 'tools' 'LabVIEWPidTracker.psm1' - if (Test-Path -LiteralPath $trackerModule -PathType Leaf) { - New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null - Copy-Item -Path $trackerModule -Destination $toolsDir -Force + New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null + $supportFiles = @( + 'LabVIEWPidTracker.psm1', + 'PesterExecutionPacks.ps1', + 'Invoke-PesterExecutionPostprocess.ps1', + 'PesterServiceModelSchema.ps1', + 'Get-PesterResultXmlSummary.ps1' + ) + foreach ($supportFile in $supportFiles) { + $supportPath = Join-Path $repoRoot 'tools' $supportFile + if (Test-Path -LiteralPath $supportPath -PathType Leaf) { + Copy-Item -Path $supportPath -Destination $toolsDir -Force + } + } + $dispatcherTools = Join-Path $repoRoot 'tools' 'Dispatcher' + if (Test-Path -LiteralPath $dispatcherTools -PathType Container) { + Copy-Item -Path $dispatcherTools -Destination (Join-Path $toolsDir 'Dispatcher') -Recurse -Force } Push-Location $workspace diff --git a/tests/Render-VIHistoryReport.Tests.ps1 b/tests/Render-VIHistoryReport.Tests.ps1 index d05351cd2..b216080da 100644 --- a/tests/Render-VIHistoryReport.Tests.ps1 +++ b/tests/Render-VIHistoryReport.Tests.ps1 @@ -380,4 +380,30 @@ Describe 'Render-VIHistoryReport.ps1' -Tag 'Unit' { $facade.status | Should -Be 'ok' $facade.reason | Should -Be 'within-limit' } + + It 'renders the checked-in offline corpus suite without an explicit history context' { + $manifestPath = Join-Path $script:repoRoot 'fixtures' 'cross-repo' 'labview-icon-editor' 'settings-init' 'manifest.json' + $resultsRoot = Join-Path $TestDrive 'history-results-offline-corpus' + $markdownPath = Join-Path $resultsRoot 'history-report.md' + $htmlPath = Join-Path $resultsRoot 'history-report.html' + $stepSummaryPath = Join-Path $resultsRoot 'history-summary.md' + + & $script:scriptPath ` + -ManifestPath $manifestPath ` + -OutputDir $resultsRoot ` + -MarkdownPath $markdownPath ` + -EmitHtml ` + -HtmlPath $htmlPath ` + -StepSummaryPath $stepSummaryPath | Out-Null + + Test-Path -LiteralPath $markdownPath | Should -BeTrue + Test-Path -LiteralPath $htmlPath | Should -BeTrue + Test-Path -LiteralPath $stepSummaryPath | Should -BeTrue + + $markdown = Get-Content -LiteralPath $markdownPath -Raw + $markdown | Should -Match '## Observed interpretation' + $markdown | Should -Match '\| Coverage Class \| `[^`]+` \|' + $markdown | Should -Match '\| Mode Sensitivity \| `single-mode-observed` \|' + $markdown | Should -Match '\| Outcome Labels \| `clean`, `signal-diff` \|' + } } diff --git a/tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1 b/tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1 index 09e2dc340..b7510739a 100644 --- a/tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1 +++ b/tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1 @@ -76,6 +76,31 @@ if ($Args[0] -eq 'info') { exit 0 } +if ($Args[0] -eq 'manifest' -and $Args.Count -ge 3 -and $Args[1] -eq 'inspect') { + $paddingBytes = [Environment]::GetEnvironmentVariable('DOCKER_STUB_MANIFEST_PADDING_BYTES') + $padding = '' + if (-not [string]::IsNullOrWhiteSpace($paddingBytes)) { + $padding = ('x' * [int]$paddingBytes) + } + $manifest = [ordered]@{ + schemaVersion = 2 + annotations = [ordered]@{ + padding = $padding + } + manifests = @( + [ordered]@{ + digest = 'sha256:1111111111111111111111111111111111111111111111111111111111111111' + platform = [ordered]@{ + os = 'windows' + architecture = 'amd64' + } + } + ) + } + ($manifest | ConvertTo-Json -Depth 10) | Write-Output + exit 0 +} + if ($Args[0] -eq 'image' -and $Args.Count -ge 2 -and $Args[1] -eq 'inspect') { $inspectSleep = [Environment]::GetEnvironmentVariable('DOCKER_STUB_INSPECT_SLEEP_SECONDS') if (-not [string]::IsNullOrWhiteSpace($inspectSleep)) { @@ -165,6 +190,7 @@ exit 0 DOCKER_STUB_INFO_EXITCODE = $env:DOCKER_STUB_INFO_EXITCODE DOCKER_STUB_RUN_STDERR = $env:DOCKER_STUB_RUN_STDERR DOCKER_STUB_RUN_EXITCODE = $env:DOCKER_STUB_RUN_EXITCODE + DOCKER_STUB_MANIFEST_PADDING_BYTES = $env:DOCKER_STUB_MANIFEST_PADDING_BYTES DOCKER_STUB_INSPECT_SLEEP_SECONDS = $env:DOCKER_STUB_INSPECT_SLEEP_SECONDS DOCKER_STUB_PULL_SLEEP_SECONDS = $env:DOCKER_STUB_PULL_SLEEP_SECONDS DOCKER_STUB_RUN_SLEEP_SECONDS = $env:DOCKER_STUB_RUN_SLEEP_SECONDS @@ -351,4 +377,34 @@ exit 0 $json.bootstrap.attempted | Should -BeFalse $json.probe.attempted | Should -BeFalse } + + It 'keeps desktop-local preflight ready when manifest output is large' { + $work = Join-Path $TestDrive 'desktop-local-large-manifest' + New-Item -ItemType Directory -Path $work -Force | Out-Null + & $script:CreateDockerHostedStubs -WorkRoot $work + + Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows' + Set-Item Env:DOCKER_STUB_OSTYPE 'windows' + Set-Item Env:DOCKER_STUB_IMAGE_EXISTS '1' + Set-Item Env:DOCKER_STUB_MANIFEST_PADDING_BYTES '20000' + Set-Item Env:RUNNER_TEMP (Join-Path $work 'runner-temp') + + $resultsRoot = Join-Path $work 'results' + $outputJsonPath = Join-Path $resultsRoot 'windows-ni-2026q1-host-preflight.json' + + $output = @(& pwsh -NoLogo -NoProfile -File $script:ToolPath ` + -Image 'nationalinstruments/labview:2026q1-windows' ` + -ResultsDir $resultsRoot ` + -ExecutionSurface 'desktop-local' ` + -CommandTimeoutSeconds 5 ` + -OutputJsonPath $outputJsonPath ` + -GitHubOutputPath '' ` + -StepSummaryPath '' 2>&1) + $LASTEXITCODE | Should -Be 0 -Because ($output -join "`n") + + $json = Get-Content -LiteralPath $outputJsonPath -Raw | ConvertFrom-Json -Depth 20 + $json.status | Should -Be 'ready' + $json.bootstrap.imagePresent | Should -BeTrue + $json.probe.status | Should -Be 'success' + } } diff --git a/tests/_helpers/DispatcherTestHelper.psm1 b/tests/_helpers/DispatcherTestHelper.psm1 index d73e50ae2..02d0a8cad 100644 --- a/tests/_helpers/DispatcherTestHelper.psm1 +++ b/tests/_helpers/DispatcherTestHelper.psm1 @@ -86,7 +86,6 @@ function Invoke-DispatcherSafe { $psi.EnvironmentVariables['SINGLE_INVOKER'] = '1' $psi.EnvironmentVariables['SUPPRESS_NESTED_DISCOVERY'] = '1' $psi.EnvironmentVariables['STUCK_GUARD'] = '0' - $psi.EnvironmentVariables['DISABLE_SINGLE_INVOKER'] = '1' $psi.EnvironmentVariables['SUPPRESS_PATTERN_SELFTEST'] = '1' $baseline = Get-PwshProcessIds diff --git a/tools/Assert-DockerRuntimeDeterminism.ps1 b/tools/Assert-DockerRuntimeDeterminism.ps1 index fa6462244..b406bed6c 100644 --- a/tools/Assert-DockerRuntimeDeterminism.ps1 +++ b/tools/Assert-DockerRuntimeDeterminism.ps1 @@ -90,6 +90,8 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'ProcessTimeoutHelper.ps1') + function Write-GitHubOutput { param( [Parameter(Mandatory = $true)][string]$Key, @@ -193,41 +195,13 @@ function Invoke-ProcessWithTimeout { exception = '' } - $psi = [System.Diagnostics.ProcessStartInfo]::new() - $psi.FileName = $resolvedFilePath - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.CreateNoWindow = $true - foreach ($arg in @($effectiveArguments)) { - [void]$psi.ArgumentList.Add([string]$arg) - } - - $proc = [System.Diagnostics.Process]::new() - $proc.StartInfo = $psi - - try { - [void]$proc.Start() - $completed = $proc.WaitForExit($safeTimeout * 1000) - if (-not $completed) { - $result.timedOut = $true - try { $proc.Kill($true) } catch {} - return [pscustomobject]$result - } - - $result.exitCode = [int]$proc.ExitCode - $result.stdout = @(Split-OutputLines -Text $proc.StandardOutput.ReadToEnd()) - $result.stderr = @(Split-OutputLines -Text $proc.StandardError.ReadToEnd()) - } catch { - $result.exception = [string]$_.Exception.Message - try { - if (-not $proc.HasExited) { - $proc.Kill($true) - } - } catch {} - } finally { - $proc.Dispose() - } + $invoke = Invoke-ProcessWithTimeoutCore -FilePath $resolvedFilePath -Arguments @($effectiveArguments) -TimeoutSeconds $safeTimeout + $result.timedOut = [bool]$invoke.TimedOut + $result.exitCode = $invoke.ExitCode + $result.stdout = @($invoke.Stdout | ForEach-Object { [string]$_ }) + $result.stderr = @($invoke.Stderr | ForEach-Object { [string]$_ }) + $result.command = [string]$invoke.Command + $result.exception = [string]$invoke.Exception return [pscustomobject]$result } @@ -1000,9 +974,18 @@ if ($ExpectedOsType -eq 'windows') { $hostAlignmentOk = $false $reason = "RUNNER_OS is '$runnerOsRaw', expected Windows." } -} elseif (-not [string]::IsNullOrWhiteSpace($runnerOsNormalized) -and $runnerOsNormalized -ne 'linux') { - $hostAlignmentOk = $false - $reason = "RUNNER_OS is '$runnerOsRaw', expected Linux for linux lane." +} elseif (-not [string]::IsNullOrWhiteSpace($runnerOsNormalized)) { + # Linux Docker lanes are valid on Windows Docker Desktop / native-wsl hosts, + # but non-Windows hosts still need a Linux runner identity. + if ($hostIsWindows) { + if ($runnerOsNormalized -notin @('windows', 'linux')) { + $hostAlignmentOk = $false + $reason = "RUNNER_OS is '$runnerOsRaw', expected Windows or Linux for linux lane on a Windows host." + } + } elseif ($runnerOsNormalized -ne 'linux') { + $hostAlignmentOk = $false + $reason = "RUNNER_OS is '$runnerOsRaw', expected Linux for linux lane." + } } $observedDockerHost = if ([string]::IsNullOrWhiteSpace($env:DOCKER_HOST)) { $null } else { $env:DOCKER_HOST.Trim() } diff --git a/tools/CompareVI.Tools/CompareVI.Tools.psd1 b/tools/CompareVI.Tools/CompareVI.Tools.psd1 index 6eee6cc22..926dfc0bf 100644 --- a/tools/CompareVI.Tools/CompareVI.Tools.psd1 +++ b/tools/CompareVI.Tools/CompareVI.Tools.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'CompareVI.Tools.psm1' - ModuleVersion = '0.6.11' + ModuleVersion = '0.6.12' GUID = '1f9b5f7f-1ab6-4db9-8e36-6b7a6d5e9c8f' Author = 'LabVIEW Community CI' CompanyName = 'LabVIEW Community' diff --git a/tools/Invoke-DockerRuntimeManager.ps1 b/tools/Invoke-DockerRuntimeManager.ps1 index fa59acfd2..c08920e12 100644 --- a/tools/Invoke-DockerRuntimeManager.ps1 +++ b/tools/Invoke-DockerRuntimeManager.ps1 @@ -44,6 +44,8 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'ProcessTimeoutHelper.ps1') + function Resolve-AbsolutePath { param([Parameter(Mandatory)][string]$Path) if ([System.IO.Path]::IsPathRooted($Path)) { @@ -146,41 +148,13 @@ function Invoke-ProcessWithTimeout { exception = '' } - $psi = [System.Diagnostics.ProcessStartInfo]::new() - $psi.FileName = $resolvedFilePath - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.CreateNoWindow = $true - foreach ($arg in @($effectiveArguments)) { - [void]$psi.ArgumentList.Add([string]$arg) - } - - $proc = [System.Diagnostics.Process]::new() - $proc.StartInfo = $psi - - try { - [void]$proc.Start() - $completed = $proc.WaitForExit($safeTimeout * 1000) - if (-not $completed) { - $result.timedOut = $true - try { $proc.Kill($true) } catch {} - return [pscustomobject]$result - } - - $result.exitCode = [int]$proc.ExitCode - $result.stdout = @(Split-OutputLines -Text $proc.StandardOutput.ReadToEnd()) - $result.stderr = @(Split-OutputLines -Text $proc.StandardError.ReadToEnd()) - } catch { - $result.exception = [string]$_.Exception.Message - try { - if (-not $proc.HasExited) { - $proc.Kill($true) - } - } catch {} - } finally { - $proc.Dispose() - } + $invoke = Invoke-ProcessWithTimeoutCore -FilePath $resolvedFilePath -Arguments @($effectiveArguments) -TimeoutSeconds $safeTimeout + $result.timedOut = [bool]$invoke.TimedOut + $result.exitCode = $invoke.ExitCode + $result.stdout = @($invoke.Stdout | ForEach-Object { [string]$_ }) + $result.stderr = @($invoke.Stderr | ForEach-Object { [string]$_ }) + $result.command = [string]$invoke.Command + $result.exception = [string]$invoke.Exception return [pscustomobject]$result } diff --git a/tools/ProcessTimeoutHelper.ps1 b/tools/ProcessTimeoutHelper.ps1 new file mode 100644 index 000000000..1b62d9a52 --- /dev/null +++ b/tools/ProcessTimeoutHelper.ps1 @@ -0,0 +1,147 @@ +#Requires -Version 7.0 + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if (-not ('CompareVI.ProcessInvokeHelper' -as [type])) { + Add-Type -TypeDefinition @" +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; + +namespace CompareVI { + public sealed class ProcessInvokeResult { + public bool TimedOut { get; set; } + public int? ExitCode { get; set; } + public string[] Stdout { get; set; } = Array.Empty(); + public string[] Stderr { get; set; } = Array.Empty(); + public string Command { get; set; } = ""; + public string Exception { get; set; } = ""; + } + + public static class ProcessInvokeHelper { + private static string[] SplitLines(string value) { + if (string.IsNullOrEmpty(value)) { + return Array.Empty(); + } + + return value + .Replace("\r\n", "\n") + .Replace('\r', '\n') + .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + } + + public static ProcessInvokeResult Run(string filePath, string[] arguments, int timeoutSeconds) { + var safeTimeoutSeconds = Math.Max(5, timeoutSeconds); + var result = new ProcessInvokeResult(); + + using var stdoutClosed = new ManualResetEventSlim(false); + using var stderrClosed = new ManualResetEventSlim(false); + var stdout = new StringBuilder(); + var stderr = new StringBuilder(); + + var psi = new ProcessStartInfo { + FileName = filePath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + if (arguments != null) { + foreach (var argument in arguments) { + psi.ArgumentList.Add(argument ?? string.Empty); + } + } + + result.Command = psi.FileName + (psi.ArgumentList.Count > 0 ? " " + string.Join(" ", psi.ArgumentList) : string.Empty); + + using var process = new Process { + StartInfo = psi, + EnableRaisingEvents = true + }; + + process.OutputDataReceived += (_, eventArgs) => { + if (eventArgs.Data == null) { + stdoutClosed.Set(); + return; + } + + lock (stdout) { + stdout.AppendLine(eventArgs.Data); + } + }; + + process.ErrorDataReceived += (_, eventArgs) => { + if (eventArgs.Data == null) { + stderrClosed.Set(); + return; + } + + lock (stderr) { + stderr.AppendLine(eventArgs.Data); + } + }; + + try { + if (!process.Start()) { + result.Exception = "Process failed to start."; + return result; + } + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + if (!process.WaitForExit(safeTimeoutSeconds * 1000)) { + result.TimedOut = true; + try { + process.Kill(true); + } catch { + } + } + + try { + process.WaitForExit(); + } catch { + } + + stdoutClosed.Wait(2000); + stderrClosed.Wait(2000); + + if (!result.TimedOut) { + result.ExitCode = process.ExitCode; + } + } catch (Exception ex) { + result.Exception = ex.Message; + try { + if (!process.HasExited) { + process.Kill(true); + } + } catch { + } + } + + result.Stdout = SplitLines(stdout.ToString()); + result.Stderr = SplitLines(stderr.ToString()); + return result; + } + } +} +"@ -Language CSharp +} + +function Invoke-ProcessWithTimeoutCore { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$FilePath, + [string[]]$Arguments = @(), + [int]$TimeoutSeconds = 45 + ) + + return [CompareVI.ProcessInvokeHelper]::Run( + [string]$FilePath, + [string[]]@($Arguments), + [Math]::Max(5, [int]$TimeoutSeconds) + ) +} diff --git a/tools/Render-VIHistoryReport.ps1 b/tools/Render-VIHistoryReport.ps1 index 3c7284a4f..6ddf973e9 100644 --- a/tools/Render-VIHistoryReport.ps1 +++ b/tools/Render-VIHistoryReport.ps1 @@ -952,12 +952,14 @@ function Build-FallbackHistoryContext { } $modeComparisons = New-Object System.Collections.Generic.List[object] - foreach ($comparisonEntry in @($modeManifest.comparisons)) { - if ($comparisonEntry) { - $modeComparisons.Add($comparisonEntry) | Out-Null - } + $comparisonEntries = @() + if ($modeManifest.PSObject.Properties['comparisons']) { + $comparisonEntries += @($modeManifest.comparisons) + } + if ($modeManifest.PSObject.Properties['collapsedComparisons']) { + $comparisonEntries += @($modeManifest.collapsedComparisons) } - foreach ($comparisonEntry in @($modeManifest.collapsedComparisons)) { + foreach ($comparisonEntry in @($comparisonEntries)) { if ($comparisonEntry) { $modeComparisons.Add($comparisonEntry) | Out-Null } diff --git a/tools/Test-WindowsNI2026q1HostPreflight.ps1 b/tools/Test-WindowsNI2026q1HostPreflight.ps1 index 81a863e47..4ed56c33b 100644 --- a/tools/Test-WindowsNI2026q1HostPreflight.ps1 +++ b/tools/Test-WindowsNI2026q1HostPreflight.ps1 @@ -39,6 +39,8 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'ProcessTimeoutHelper.ps1') + function Resolve-AbsolutePath { param([Parameter(Mandatory)][string]$Path) return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) @@ -158,41 +160,13 @@ function Invoke-ProcessWithTimeout { exception = '' } - $psi = [System.Diagnostics.ProcessStartInfo]::new() - $psi.FileName = $resolvedFilePath - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.CreateNoWindow = $true - foreach ($arg in @($effectiveArguments)) { - [void]$psi.ArgumentList.Add([string]$arg) - } - - $proc = [System.Diagnostics.Process]::new() - $proc.StartInfo = $psi - - try { - [void]$proc.Start() - $completed = $proc.WaitForExit($safeTimeout * 1000) - if (-not $completed) { - $result.timedOut = $true - try { $proc.Kill($true) } catch {} - return [pscustomobject]$result - } - - $result.exitCode = [int]$proc.ExitCode - $result.stdout = @(Split-OutputLines -Text $proc.StandardOutput.ReadToEnd()) - $result.stderr = @(Split-OutputLines -Text $proc.StandardError.ReadToEnd()) - } catch { - $result.exception = [string]$_.Exception.Message - try { - if (-not $proc.HasExited) { - $proc.Kill($true) - } - } catch {} - } finally { - $proc.Dispose() - } + $invoke = Invoke-ProcessWithTimeoutCore -FilePath $resolvedFilePath -Arguments @($effectiveArguments) -TimeoutSeconds $safeTimeout + $result.timedOut = [bool]$invoke.TimedOut + $result.exitCode = $invoke.ExitCode + $result.stdout = @($invoke.Stdout | ForEach-Object { [string]$_ }) + $result.stderr = @($invoke.Stderr | ForEach-Object { [string]$_ }) + $result.command = [string]$invoke.Command + $result.exception = [string]$invoke.Exception return [pscustomobject]$result }