feat(launching): launch the native BGFX client against retail archives - #332
Conversation
…nstead of copying them
Measured unnecessary: the BGFX build declares every dependency as @executable_path/ with a matching rpath and launches with the variable cleared. Two costs outweighed a fallback for a build we do not ship: dyld consults DYLD_LIBRARY_PATH before @executable_path for leaf-name references, so a same-named library elsewhere silently wins; and the hardened runtime ignores it, so behaviour would change silently once GenHub is signed and notarized. Closes #317.
…gine The engine reaches its main loop with zero content and no error when the archive roots are wrong: no exit code, no log line, nothing a host process can observe. Launch success therefore cannot distinguish a working game from an empty one, so the roots are checked before spawn and the launch fails with the offending root named. Presence of at least one .big archive is the sentinel rather than a specific filename, which varies by localisation and version. An archive that exists but fails to mount still needs a machine-readable failure from the engine. Closes #315.
The buffer was read as soon as the process exited, but the asynchronous stderr handlers had not necessarily delivered their final lines — truncating diagnostics precisely on the path where they explain the failure. Wait via the parameterless WaitForExit overload, which also waits for redirected-output handlers, before reading. The buffer now treats a null line as the end-of-stream signal rather than content, retains the head as well as the tail so startup context survives alongside the failure, and bounds line length and total size so a pathological writer cannot exhaust memory. Only stderr is redirected, so there is no stdout stream to drain. Closes #318.
Adds an end-to-end check that a process failing immediately still reports both the head and tail of its stderr. It does not deterministically pin the end-of-stream drain: removing the WaitForExit() call leaves it passing on macOS and .NET 8, because the handlers complete before the capture is read even at twenty thousand lines. The drain stands on the documented contract instead. The GameLauncher fixture now uses a real retail root holding an archive. It previously pointed at a Windows path that does not exist off Windows, which the new pre-spawn validation correctly rejected. Clears the warnings this stack introduced; development itself builds clean. Two remain, both predating these fixes and needing invasive reordering of long files: SA1204 in GameLauncher and SA1202 in WorkspaceStrategyBase.
…alization change EnsureExecutableAsync became protected when the rethrow was added, which put it after a private helper. Moved it above, and dropped the trailing blank line the earlier reordering left.
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds cross-platform retail archive-root constants and pre-launch validation, bounded stderr diagnostics and Unix permission checks, case-insensitive installation discovery, and native-client integration tests. ChangesNative launch support
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
📋 Issue PlannerLet us write the prompt for your AI agent so you can ship faster (with fewer bugs). ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…alize profile overrides Two defects from review, both introduced by the earlier validation change. Windows never reads CNC_ZH_INSTALLPATH or CNC_GENERALS_INSTALLPATH — it resolves install paths from the registry — so a Windows layout without loose top-level archives is not a misconfiguration. BuildEnvironmentVariables returns before setting them there, but validation reads the installation's declared paths, so it needed the same guard rather than inheriting it. Without it, valid Windows launches were rejected with a misleading message about missing archives. A profile setting a root explicitly chooses the directory, not whether the trailing separator is applied. AddArchiveRoot returned early on an existing value and never normalized it, so the engine concatenated it with the archive filename to produce paths like '/path/toINIZH.big' and silently mounted nothing — precisely the failure this validation exists to prevent, reachable through the override path. The variable names are an external contract with the engine, so they move to RetailArchiveConstants alongside the archive search pattern. The normalization test fails against the previous early-return. The Windows guard asserts only on Windows and is a no-op elsewhere.
|
Addressed the Greptile findings. Both P1s were real and both were introduced by the archive-root validation change in this PR, so thanks — these would have shipped. P1: Windows archive validation blocks launches. Confirmed. P1: profile archive roots remain unnormalized. Confirmed, and it defeated the purpose of the check. P2: centralize archive environment constants. Agreed — these names are an external contract with the engine, not local strings. Moved to Verification: the normalization test fails against the previous early-return, so it pins the fix rather than merely passing. 1423 unit tests pass, 0 warnings. The Windows guard asserts only on Windows and is a no-op elsewhere, so CI's Windows job is what exercises it. |
Two of these asserted that validation rejects a bad root, which stopped being true on Windows once validation was guarded there — the platform guard was added without updating the tests that asserted the previous cross-platform behaviour, and Build Windows caught it. Rejection is non-Windows behaviour by design; the Windows side is asserted separately by Validate_OnWindows_SkipsEntirely. The guards do not hollow the tests out: on non-Windows they still assert, and three still fail if the validation fix is reverted.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (7 files)
Previous Review Summaries (7 snapshots, latest commit 3eccb33)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 3eccb33)Status: No Issues Found | Recommendation: Merge Files Reviewed (3 files)
Previous review (commit 69460ea)Status: No Issues Found | Recommendation: Merge Files Reviewed (4 files)
Previous review (commit 1da85e5)Status: No Issues Found | Recommendation: Merge Files Reviewed (1 file)
Previous review (commit 7e6f8ce)Status: No Issues Found | Recommendation: Merge Files Reviewed (1 file)
Previous review (commit aeaa1c0)Status: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Previous review (commit 00a2bc6)Status: No Issues Found | Recommendation: Merge Files Reviewed (11 files)
Previous review (commit 5ac6b3a)Status: No Issues Found | Recommendation: Merge Files Reviewed (13 files)
Reviewed by glm-4.7 · Input: 53.4K · Output: 4.4K · Cached: 327.9K |
|
@coderabbitai review Your previous approval was dismissed by a push, and the re-review hit a rate limit. Since then:
All checks are green: Build Linux, Build Windows, Build macOS, Build Summary, Detect File Changes, Greptile and Kilo. |
|
✅ Action performedReview finished.
|
|
@coderabbitai full review The incremental review reports the commits as already seen, so no new review is produced and the approval dismissed by the push is not restored. Requesting a full re-review of For context, the two commits since your approval of
All checks are green. |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
GenHub/GenHub/Features/GameProfiles/Infrastructure/GameProcessManager.cs (1)
304-336: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winExit-0 fallback drops the already-captured stderr detail.
When the process exits immediately with code
0and no spawned replacement is found (line 331-336),capturedErrorswas already drained at line 311 but its content is never surfaced — only the non-zero-exit branch (line 318-329) builds and reportsdetail. This narrows the "complete stderr diagnostics" goal for a case that's still a genuine unexplained failure (e.g. a Windows launcher-stub whose spawned process wasn't found, or any immediate zero-exit anomaly).🩹 Proposed fix
else { + var stderrTail = capturedErrors.ToString(); + var suffix = string.IsNullOrWhiteSpace(stderrTail) ? string.Empty : $" {stderrTail}"; // If it exits with 0 and we didn't find a spawned child, still fail the launch // because we don't have a valid process to track, preventing the UI from getting stuck in a 'running' state - return OperationResult<GameProcessInfo>.CreateFailure("Process exited immediately after launch."); + return OperationResult<GameProcessInfo>.CreateFailure($"Process exited immediately after launch.{suffix}"); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@GenHub/GenHub/Features/GameProfiles/Infrastructure/GameProcessManager.cs` around lines 304 - 336, Reuse the captured stderr detail in the exit-code-0 fallback within the immediate-exit handling flow: build the same `stderrTail`/`detail` value from `capturedErrors` before branching, log it with the zero-exit failure, and include it in the returned `OperationResult<GameProcessInfo>` message. Preserve the existing non-zero behavior and “No output was captured.” fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientLaunchIntegrationTests.cs`:
- Around line 31-59: The native-client fixture discovery logic is duplicated
between two integration test classes. In
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientLaunchIntegrationTests.cs#L31-L59,
extract EnvironmentOverride, BinaryName, and NativeClientDirectory into a shared
static helper near NativeClientLaunchCollection, then have
NativeClientLaunchIntegrationTests use it; in
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientManifestLaunchTests.cs#L32-L57,
remove the duplicate definitions and call the same helper while preserving the
existing discovery behavior.
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.cs`:
- Around line 187-193: Update the file filter in StageEngineOnly to include
Linux shared libraries ending in “.so” alongside the existing binary and
“.dylib” matches. Preserve the current top-level enumeration and
case-insensitive extension handling.
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Launching/RetailArchiveRootValidationTests.cs`:
- Around line 206-221: Reorder the CreateRoot and BuildEnvironment members so
the static BuildEnvironment method appears before the instance CreateRoot
method, add the required blank line between methods, and remove the extra blank
line before the class closing brace to satisfy StyleCop ordering and spacing
rules.
In `@GenHub/GenHub/Features/Launching/GameLauncher.cs`:
- Around line 531-594: Update ValidateRetailArchiveRoots to accept the launched
GameType and validate only that game's corresponding install-path variable and
declared path; preserve the existing Windows guard and root/archive checks.
Update the call site in the launch flow to pass gameClient.GameType so sibling
paths on the same installation cannot affect the current launch.
---
Outside diff comments:
In `@GenHub/GenHub/Features/GameProfiles/Infrastructure/GameProcessManager.cs`:
- Around line 304-336: Reuse the captured stderr detail in the exit-code-0
fallback within the immediate-exit handling flow: build the same
`stderrTail`/`detail` value from `capturedErrors` before branching, log it with
the zero-exit failure, and include it in the returned
`OperationResult<GameProcessInfo>` message. Preserve the existing non-zero
behavior and “No output was captured.” fallback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c845348c-4462-49b0-961e-5334deddabde
📒 Files selected for processing (13)
GenHub/GenHub.Core/Constants/RetailArchiveConstants.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/BoundedErrorBufferTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientLaunchCollection.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientLaunchIntegrationTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientManifestLaunchTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeLaunchDiagnosticsTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/StderrCaptureRaceTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/Launching/GameLauncherTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/Launching/RetailArchiveRootValidationTests.csGenHub/GenHub/Features/GameProfiles/Infrastructure/GameProcessManager.csGenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileLauncherViewModel.csGenHub/GenHub/Features/Launching/GameLauncher.cs
…port zero-exit stderr Validation checked both games' declared roots regardless of which was launching, so a stale Zero Hour path could block a Generals launch that would have worked. It now checks only the launching game's root, taking the game type from the client. A process exiting immediately with code 0 and no spawned child returned a bare message while the stderr drained moments earlier went unreported — the one diagnostic available for that case, and contrary to the point of the capture work in this branch. Test changes: the native-client fixture discovery was duplicated across three classes and is now a shared NativeClientFixture, so they cannot diverge on when to skip; engine-only staging includes .so alongside .dylib, having been macOS-only; and SetUnixFileMode calls are guarded so the platform analyzer can follow them. Also clears every warning in GenHub.Tests.Core, including five that reached development through #328 because I had only ever built the main project.
|
Addressed all five findings from the full review. Four were real; one I widened. Validate only the launching game's root — real, and an over-reach I introduced. Validation checked both declared paths regardless of which game was launching, so a stale Zero Hour root could block a Generals launch that would have worked fine. Now takes the game type from the client and checks only that root, keeping the Windows guard. Zero-exit fallback dropped the captured stderr — real. A process exiting immediately with code 0 and no spawned child returned a bare message while the stderr drained moments earlier went unreported. That was the only diagnostic available for the case, and contrary to the point of the capture work in this branch. Duplicated fixture discovery — real, and in three classes rather than two:
StyleCop ordering in the validation tests — real, and it led somewhere worse. Fixing it surfaced that Verification: |
… record the gap The previous comment claimed a Zero Hour installation may carry the base archives itself. That was asserted, not verified, and is not the reason. The actual reason is that the engine mounts archives from the working directory as well, so base content may sit in the workspace rather than a retail root — the arrangement this mechanism replaces, but still valid. Records the residual gap review identified: with no Generals root declared and no base content in the workspace either, Zero Hour starts with nothing to mount and this check cannot tell. Archive filenames are arbitrary, so testing for '*.big' proves nothing about base content; a workspace check was considered and rejected because a Zero Hour workspace always contains archives and it would always pass. Closing it needs the engine to report a failed mount.
|
On the "Generals archives escape validation" finding — you were right and my first fix was incomplete. You listed three conditions and I only handled two. Fixed in Not fixed, and it cannot be from here: the absent case. An absent Generals root is not by itself wrong — the engine mounts archives from the working directory as well, so base content may legitimately sit in the profile workspace rather than a retail root. That is the arrangement the install-path environment replaces, and it remains valid, so failing whenever no root is declared would break it. Telling the two apart needs to know whether base content is actually reachable, and that is not testable from filenames. A real install holds arbitrary archives beside the retail ones: So Closing it properly needs the engine to report a failed mount, which is the same signal #315 already notes it needs for the archive-exists-but-fails-to-mount case. Actions taken rather than leaving it implicit:
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientManifestLaunchTests.cs (1)
57-59: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winInclude and assert Linux shared libraries in this fixture.
The helper only captures
.dylibfiles, so Linux.sodependencies never enter the generated manifest; the current assertion can still pass with onlygeneralszh. Include.sofiles and assert that at least one dynamic library was captured, otherwise Linux packaging/resolution regressions remain untested.Proposed fix
+ Assert.Contains( + manifest.Variants.Single().Files, + file => file.RelativePath.EndsWith(".dylib", StringComparison.OrdinalIgnoreCase) + || file.RelativePath.EndsWith(".so", StringComparison.OrdinalIgnoreCase)); + - if (name != NativeClientFixture.BinaryName && !name.EndsWith(".dylib", StringComparison.OrdinalIgnoreCase)) + if (name != NativeClientFixture.BinaryName + && !name.EndsWith(".dylib", StringComparison.OrdinalIgnoreCase) + && !name.EndsWith(".so", StringComparison.OrdinalIgnoreCase))Based on the PR objective that Linux staging includes
.sofiles.Also applies to: 124-124
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientManifestLaunchTests.cs` around lines 57 - 59, Update the fixture’s native-library discovery helper to include Linux .so files alongside .dylib files, then extend the manifest assertions in the NativeClientManifestLaunchTests fixture to require at least one captured dynamic library. Keep the existing executable assertion and verify the generated manifest contains a matching .so or dynamic-library entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientFixture.cs`:
- Around line 33-37: Update the EnvironmentOverride handling in
NativeClientFixture to return the configured directory only when it contains the
expected native client binary, matching the existing fallback validation;
otherwise return null so optional integration tests are skipped for invalid
overrides.
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.cs`:
- Around line 166-171: Update the Linux shared-library condition in the archive
path filter to accept both unversioned `.so` files and versioned artifacts such
as `.so.0` or `.so.0.1.0`, while preserving the existing exact binary-name and
macOS `.dylib` matches. Use the existing name-based filter around
NativeClientFixture.BinaryName so StageEngineOnly stages all Linux
shared-library variants.
---
Outside diff comments:
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientManifestLaunchTests.cs`:
- Around line 57-59: Update the fixture’s native-library discovery helper to
include Linux .so files alongside .dylib files, then extend the manifest
assertions in the NativeClientManifestLaunchTests fixture to require at least
one captured dynamic library. Keep the existing executable assertion and verify
the generated manifest contains a matching .so or dynamic-library entry.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 03858a95-fd34-48a2-9937-f7aeb31e116e
📒 Files selected for processing (11)
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientFixture.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientLaunchCollection.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientLaunchIntegrationTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientManifestLaunchTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeLaunchDiagnosticsTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/Launching/RetailArchiveRootValidationTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/Manifest/ContentManifestPoolTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/Workspace/WorkspaceValidatorTests.csGenHub/GenHub/Features/GameProfiles/Infrastructure/GameProcessManager.csGenHub/GenHub/Features/Launching/GameLauncher.cs
…ot validation The comments asserted the engine reaches its main loop with zero content and no error, and that no post-spawn check could tell. Verified against bobtista/GeneralsGameCode at bobtista/topic/trunk, that is false for the cases described: a root holding no archives aborts during initialisation with exit 1 and a ReleaseCrashInfo.txt, and an unmountable archive also writes '[ggc] ARCHIVE MOUNT FAILED' to stderr. Neither reaches the main loop. The claim came from the plan and was repeated here without ever being checked against the engine, which is public. Validating before spawn still earns its place, for reasons that are actually true: exit 1 is generic, the stderr sentinel exists only in StdBIGFileSystem and not the Win32 twin, and ReleaseCrash shows a system-modal dialog before _exit(1) so a host-launched child on Windows hangs instead of dying. The comments now say that, and bound what the .big sentinel can detect.
…d Linux libraries Three review findings, all in the integration fixtures. The GENHUB_NATIVE_CLIENT_DIR override only checked the directory existed, while the discovered default also required the engine binary. A directory without one is a misconfigured override and these tests should skip, not fail; both paths now validate the same way. The '.so' suffix test missed versioned libraries such as libSDL3.so.0, which is the common shape of a shipped Linux library, and a second copy of the same filter in the manifest fixture had not been updated at all when '.so' was added earlier. Both now use a shared NativeClientFixture.IsDynamicLibrary. The manifest fixture asserted only that something executable was present, so it would have passed with just the binary and never exercised library handling. It now requires a captured dynamic library. The predicate is unit-tested because every test that uses it skips without a local engine install, so it would otherwise never run in CI. Writing those tests caught a real bug: matching on '.so' found the coincidental one inside '.sound' and stopped, so 'mylib.sound.so.1' was rejected. It matches on '.so.' now, and that case is pinned.
|
All three addressed in Fixture override not validated like the default. Versioned Linux libraries missed. Weak manifest assertion. It required only something executable, so it would have passed with just the binary and never exercised library handling. It now requires a captured dynamic library. I unit-tested the predicate, because every test that consumes it skips without a local engine install and it would otherwise never execute in CI. That paid for itself immediately — the first implementation matched on
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
GenHub/GenHub/Features/Launching/GameLauncher.cs (2)
1201-1204: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winUnregister the placeholder launch before returning validation failure.
This path returns after the placeholder was registered, leaving a stale active launch that can block profile deletion until another launch happens to clean it up.
Proposed fix
if (archiveRootError is not null) { logger.LogError("[GameLauncher] Retail archive root validation failed: {Error}", archiveRootError); + await launchRegistry.UnregisterLaunchAsync(launchId); return LaunchOperationResult<GameLaunchInfo>.CreateFailure(archiveRootError, launchId, profile.Id); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@GenHub/GenHub/Features/Launching/GameLauncher.cs` around lines 1201 - 1204, Before returning the validation failure in the archiveRootError branch of GameLauncher, unregister the placeholder launch that was registered earlier. Ensure the cleanup occurs before CreateFailure is returned, while preserving the existing error logging and failure result.
499-510: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRemove loader-path overrides copied from the profile.
profileEnvironmentis cloned unchanged, so a profile containingDYLD_LIBRARY_PATHorLD_LIBRARY_PATHre-enables precisely the native library override this change removes.Proposed fix
if (OperatingSystem.IsWindows()) { return environment; } + environment.Remove("DYLD_LIBRARY_PATH"); + environment.Remove("LD_LIBRARY_PATH"); AddRetailArchiveRoots(environment, installation);As per PR objectives, native launches must remove
DYLD_LIBRARY_PATHandLD_LIBRARY_PATH.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@GenHub/GenHub/Features/Launching/GameLauncher.cs` around lines 499 - 510, Update the environment construction in the launcher method containing AddRetailArchiveRoots so profileEnvironment cannot reintroduce DYLD_LIBRARY_PATH or LD_LIBRARY_PATH: remove those keys from the cloned environment before returning on Windows or adding retail archive roots. Preserve all other profile variables and existing platform-specific behavior.GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.cs (1)
103-108: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winClear archive-root environment variables for the “without roots” launch.
UseShellExecute = falsechild processes inherit the parent environment, andStartProcessAsynconly adds variables fromconfiguration.EnvironmentVariables; it never removes inherited values. This test omitsCNC_ZH_INSTALLPATHandCNC_GENERALS_INSTALLPATH, so a developer exporting a retail archive root can still make the engine-only launch reach retail archives. For this negative test, explicitly unset those env vars beforeProcess.Startor start from a clean child environment.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.cs` around lines 103 - 108, Update the “without roots” launch setup in the test around GameLaunchConfiguration to explicitly remove or unset CNC_ZH_INSTALLPATH and CNC_GENERALS_INSTALLPATH in the child process environment before Process.Start, ensuring inherited developer environment values cannot expose retail archives while preserving the existing engine-only configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.cs`:
- Around line 103-108: Update the “without roots” launch setup in the test
around GameLaunchConfiguration to explicitly remove or unset CNC_ZH_INSTALLPATH
and CNC_GENERALS_INSTALLPATH in the child process environment before
Process.Start, ensuring inherited developer environment values cannot expose
retail archives while preserving the existing engine-only configuration.
In `@GenHub/GenHub/Features/Launching/GameLauncher.cs`:
- Around line 1201-1204: Before returning the validation failure in the
archiveRootError branch of GameLauncher, unregister the placeholder launch that
was registered earlier. Ensure the cleanup occurs before CreateFailure is
returned, while preserving the existing error logging and failure result.
- Around line 499-510: Update the environment construction in the launcher
method containing AddRetailArchiveRoots so profileEnvironment cannot reintroduce
DYLD_LIBRARY_PATH or LD_LIBRARY_PATH: remove those keys from the cloned
environment before returning on Windows or adding retail archive roots. Preserve
all other profile variables and existing platform-specific behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: eaf55bdb-a359-4c55-9fce-28e8acd8d6c0
📒 Files selected for processing (7)
GenHub/GenHub.Core/Constants/RetailArchiveConstants.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientFixture.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientFixtureTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientManifestLaunchTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/Launching/RetailArchiveRootValidationTests.csGenHub/GenHub/Features/Launching/GameLauncher.cs
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
GenHub/GenHub/Features/Launching/GameLauncher.cs (1)
1201-1206: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winUnregister the placeholder before returning this preflight failure.
Line 1203 returns normally after the placeholder was registered at Line 315, so this path bypasses the cleanup catch and leaves a false active launch record. That can block profile deletion until another launch happens to clean it up.
Proposed fix
if (archiveRootError is not null) { logger.LogError("[GameLauncher] Retail archive root validation failed: {Error}", archiveRootError); + await launchRegistry.UnregisterLaunchAsync(launchId); return LaunchOperationResult<GameLaunchInfo>.CreateFailure(archiveRootError, launchId, profile.Id); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@GenHub/GenHub/Features/Launching/GameLauncher.cs` around lines 1201 - 1206, Before returning the preflight failure from the Retail archive root validation block in GameLauncher, explicitly unregister the placeholder created during launch setup. Ensure the cleanup uses the existing placeholder-unregistration mechanism before CreateFailure returns, while leaving the validation and failure result unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@GenHub/GenHub/Features/Launching/GameLauncher.cs`:
- Around line 1201-1206: Before returning the preflight failure from the Retail
archive root validation block in GameLauncher, explicitly unregister the
placeholder created during launch setup. Ensure the cleanup uses the existing
placeholder-unregistration mechanism before CreateFailure returns, while leaving
the validation and failure result unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c0e40181-a81e-42ce-89e3-dcd2bfb845c9
📒 Files selected for processing (7)
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeLaunchDiagnosticsTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/RetailArchiveRootTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/Launching/GameLauncherTests.csGenHub/GenHub.Tests/GenHub.Tests.Core/Features/Launching/RetailArchiveRootValidationTests.csGenHub/GenHub/Features/GameClients/GameClientDetector.csGenHub/GenHub/Features/Launching/GameLauncher.csdocs/dev/constants.md
|
Recording decisions settled during the 2026-07-30 fork/upstream investigation, so review does not re-open them:
|
Summary
Launches the community's native BGFX Zero Hour client against the user's retail data — no Wine. Final PR of the mac stack, after #328 (platform-neutral paths, manifests and Unix workspaces), #329 (macOS host) and #331 (the macOS service registrations the merged state required).
Changes
Surface native launch failures and set the loader search path. A launch that dies before drawing a window previously reported success.
Reach retail archives via the install-path environment instead of copying them. The engine reads
CNC_ZH_INSTALLPATH/CNC_GENERALS_INSTALLPATHon non-Windows and mounts*.bigfrom those roots. Zero Hour needs its own and the base Generals archives — roughly 3 GB — so without this every profile workspace had to materialise all of them.Validate retail archive roots before spawning. Checked before spawn so a misconfigured root fails with the path named, rather than as a generic engine abort the host has to interpret.
Corrected from this PR's original description, which claimed the engine reaches its main loop with zero content and no error and that nothing was observable. Verified against
bobtista/GeneralsGameCodeatbobtista/topic/trunk, that is false: a root holding no archives aborts during initialisation with exit 1 and aReleaseCrashInfo.txt, and an unmountable archive also writes[ggc] ARCHIVE MOUNT FAILEDto stderr. Neither reaches the main loop.Validating first still earns its place for reasons that hold: exit 1 is generic; the stderr sentinel exists only in
StdBIGFileSystem, notWin32BIGFileSystem;ReleaseCrashshows a system-modal dialog before_exit(1), so a host-launched child on Windows hangs rather than dying; and GenHub's own immediate-exit detection is a 500 ms window. An earlier check with an actionable message avoids depending on any of that. Validation runs against the installation's declared paths, not the filtered environment — the environment builder drops a path that does not exist, so validating it alone would skip the stale-installation case this exists to catch.Stop setting
DYLD_LIBRARY_PATH/LD_LIBRARY_PATH. Measured unnecessary: the BGFX build declares every dependency as@executable_path/…with a matching rpath and launches with the variable cleared. Two costs outweighed a fallback for a build we do not ship — dyld consults it before@executable_pathfor leaf-name references, so a same-named library elsewhere silently wins; and the hardened runtime ignores it, so behaviour would change silently once GenHub is signed and notarized (Playwright exclusion, inside-out signing and notarization (blocker for public macOS builds) #322).Capture complete stderr when a launch fails. The buffer was read as soon as the process exited, but the asynchronous handlers had not necessarily delivered their final lines — truncating diagnostics precisely where they explain the failure. Now drains via the parameterless
WaitForExit(), which also waits for redirected-output handlers. The buffer treats a null line as end-of-stream rather than content, retains the head as well as the tail so startup context survives alongside the symptom, and bounds line length and total size.Testing
GenHub.Tests.Core: 1427 passed, 0 failed.GenHub.Tests.MacOScomposition root: 3 passed.The launch tests pass locally and skip in CI — please read this before trusting a green tick
Six tests exercise the real client: three launch it and assert it is still alive after 14 seconds, three assert a misconfigured launch does not survive. All six pass on a machine with a native install (
GENHUB_NATIVE_CLIENT_DIR, 6/6 in 54s).They skip in CI, which has no native fixture and no retail data. A green CI run on this PR therefore says nothing about whether launching works — it only covers the unit and composition tests. The engine-only CI smoke test that would close this gap is #324, and it is deliberately out of scope here.
Fork dependency worth stating
The install-path mechanism this PR validates is fork-only.
CNC_ZH_INSTALLPATHandCNC_GENERALS_INSTALLPATHare read bybobtista/GeneralsGameCodeonbobtista/topic/trunkand do not exist in
TheSuperHackers/GeneralsGameCode@main— upstream resolves install pathsfrom the Windows registry only.
That is fine, since GenHub launches our client, but it bounds what the validation means:
against an upstream build these variables are ignored and the roots have no effect. The
constants and their rationale live in
RetailArchiveConstantsso this is discoverable ratherthan implicit.
The engine behaviour the validation complements is upstream, though — the
INIfilesRead == 0throw, the unnamed enum that routes it tocatch (...), andReleaseCrashshowing a system-modal dialog before
_exit(1)are all in upstreammain. So the reasonsfor checking before spawn rather than relying on the child's exit hold for any build.
Risks and rollback
Revert-safe; no persisted-data conversion. The archive-root validation stops launches before spawn that would previously have failed only after spawn, with a generic engine crash naming nothing — intended, and the validation message names the offending root.
Related
Part of Alpha 5 (#327).
Closes #315.
Closes #317.
Closes #318.
(One keyword per line: GitHub links only the first reference in
Closes #315, #317, #318, which would have left #317 and #318 open after merge.)Greptile Summary
This PR enables native BGFX clients to launch against retail archive roots and improves startup diagnostics.
Confidence Score: 3/5
The PR is not yet safe to merge because native Zero Hour can still be spawned without any validated source of its required base Generals archives.
The current validation skips an absent GeneralsPath, while supported engine-only workspaces contain no archives and rely entirely on environment-provided retail roots; that leaves the previously reported missing-base-content startup failure reachable.
Files Needing Attention: GenHub/GenHub/Features/Launching/GameLauncher.cs; GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Launching/RetailArchiveRootValidationTests.cs
Important Files Changed
Sequence Diagram
sequenceDiagram participant Launcher as GameLauncher participant Install as GameInstallation participant Process as GameProcessManager participant Engine as Native BGFX Client Launcher->>Install: Read Zero Hour / Generals roots Launcher->>Launcher: Build normalized environment Launcher->>Launcher: Validate declared archive roots alt Validation fails Launcher-->>Launcher: Return actionable launch error else Validation passes Launcher->>Process: StartProcessAsync(configuration) Process->>Engine: Spawn with redirected stderr alt Immediate exit Process->>Process: Drain and bound stderr Process-->>Launcher: Return failure diagnostics else Process survives startup window Process-->>Launcher: Return tracked process end endReviews (10): Last reviewed commit: "chore(launching): reference launch const..." | Re-trigger Greptile
Context used: