Skip to content

fix(cas): fall back from unwritable installation pools (backport of #349) - #350

Merged
bobtista merged 1 commit into
release/alpha-4from
backport/cas-pool-writable-fallback-alpha-4
Aug 3, 2026
Merged

fix(cas): fall back from unwritable installation pools (backport of #349)#350
bobtista merged 1 commit into
release/alpha-4from
backport/cas-pool-writable-fallback-alpha-4

Conversation

@bobtista

@bobtista bobtista commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Backport of #349 into release/alpha-4.

Prevents installation-adjacent CAS storage from breaking content acquisition when
the game is installed in a protected location such as Program Files. GenHub
verifies that it can create and write the actual installation-pool directory
before routing content there. When the location is unavailable, new content uses
the primary user-writable pool while existing readable objects remain
discoverable through read-only legacy pools.

See #349 for the full description of the change.

Backport notes

Testing

  • dotnet test GenHub/GenHub.Tests/GenHub.Tests.Core/GenHub.Tests.Core.csproj -c Release
    • Passed: 1,463
    • Failed: 0
  • dotnet build GenHub/GenHub.Linux/GenHub.Linux.csproj -c Release
    • Passed
  • git diff --check
    • Passed
  • The 28 tests covering pool routing, provenance, migration, actual-directory
    probing, and legacy-content lookup all run and pass on this branch.

Risks and rollback

  • Existing objects are not moved or deleted. Previous readable installation pools
    are retained for lookup only, while new writes use the effective writable pool.
  • The new provenance and legacy-path settings are additive and default to the
    prior behavior when absent.
  • Reverting restores the previous installation-pool selection behavior and its
    protected-path acquisition failures.

Manual validation

Packaged non-administrator Windows validation was completed for #349: GameClient
content acquired with the game under Program Files, GenHub restarted, profile
created, and launched.

That run covers this backport too. The CAS code here is identical to #349, and
the only production difference against release/alpha-4 is the
executable-classifier call in CommunityOutpostDeliverer, which is outside the
storage-routing path.

Backport of #349
Fixes #347

Greptile Summary

This backport makes installation-adjacent CAS routing conditional on actual directory writability and preserves previous installation pools for read-only object lookup.

  • Adds a reusable, cached filesystem writability probe.
  • Centralizes installation-pool selection, fallback, provenance migration, and settings persistence.
  • Extends CAS resolution and lookup across retained legacy pools.
  • Updates acquisition flows and dependency injection to use the centralized pool service.
  • Adds coverage for protected paths, fallback routing, legacy lookup, persistence, and pool refresh behavior.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code-triggered failures identified.

The updated routing probes the actual target directory before enabling installation-pool writes, falls back to primary storage when unavailable, and retains prior readable pools for object lookup.

Important Files Changed

Filename Overview
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs Centralizes writable installation-pool selection, fallback, migration provenance, legacy-root retention, and persistent updates.
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs Refreshes active installation storage and exposes retained legacy pools without enabling writes to them.
GenHub/GenHub/Features/Storage/Services/CasPoolResolver.cs Routes installation-related content only when its configured pool passes the write probe and supplies legacy lookup roots.
GenHub/GenHub/Features/Storage/Services/CasService.cs Extends content-path and existence lookups to search retained legacy CAS pools after active and primary storage.
GenHub/GenHub/Common/Services/StorageWritabilityProbe.cs Adds cached, invalidatable write probing against the actual target directory with probe-file cleanup.
GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs Delegates pre-acquisition installation-pool preparation to the centralized service while allowing primary-pool fallback.
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs Uses the centralized pool preparation service before persisting GameClient manifests.
GenHub/GenHub/Infrastructure/DependencyInjection/CasModule.cs Registers the new singleton services and propagates additive CAS configuration fields.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Acquire installation-related content] --> B[Detect game installations]
    B --> C[Derive installation-adjacent CAS path]
    C --> D{Directory accepts writes?}
    D -->|Yes| E[Persist active installation pool]
    D -->|No| F[Route new objects to primary pool]
    E --> G[Reinitialize CAS pool routing]
    F --> G
    G --> H[Store new content]
    I[Previous installation pools] --> J[Retain as read-only legacy pools]
    J --> K[Lookup active, primary, and legacy pools]
    H --> K
Loading

Reviews (1): Last reviewed commit: "fix(cas): fall back from unwritable inst..." | Re-trigger Greptile

Context used (4)

* fix(cas): fall back from an unwritable installation CAS pool

* fix(cas): preserve fallback pool state safely

* fix(cas): harden writable pool fallback

* fix(cas): honor pool selection cancellation

* fix(cas): normalize legacy pool roots

* fix(cas): avoid duplicate primary legacy storage

* test(cas): tolerate cleanup failures in installation pool tests

* refactor(cas): drop a redundant installation-path check

* fix(cas): retain every previous installation pool root for lookup

(cherry picked from commit b3f5c4a)
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • development

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e1a37540-6da2-4550-b788-5b45a8a5c086

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@bobtista
bobtista merged commit d858d39 into release/alpha-4 Aug 3, 2026
3 checks passed
private bool IsInsideApplicationDirectory(string rootPath)
{
var appBaseDirectory = Path.TrimEndingDirectorySeparator(Path.GetFullPath(AppContext.BaseDirectory));
var normalizedRootPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(rootPath));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Path.GetFullPath can throw unhandled from the DI constructor.

NormalizeRoot and IsInsideApplicationDirectory are reached via RefreshInstallationPools() in the CasPoolManager constructor, and RefreshLegacyInstallationPool calls them once per entry in the user-supplied LegacyInstallationPoolRootPaths. A malformed or illegal-character path in settings raises ArgumentException/PathTooLongException/SecurityException out of constructor resolution, preventing the app from starting. The previous check used Path.TrimEndingDirectorySeparator on the raw value and only checked a single root; wrap these calls in try/catch (treat as "skip/invalid root") or validate first.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if (!saved)
{
logger.LogError("Failed to save installation CAS pool settings");
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Save-failure path leaves in-memory settings and the pool manager inconsistent.

UserSettingsService.TryUpdateAndSaveAsync applies the mutation to the in-memory settings before persisting and returns false if SaveAsync throws (UserSettingsService.cs:113-139). On that branch this code returns false and skips casPoolManager.ReinitializeInstallationPool(), so in-memory settings now point at the new pool while the pool manager still routes to the previous configuration. Additionally, aborting the entire GameClient acquisition for a transient settings-persistence error contradicts the graceful primary-pool fallback intent of this change. Consider rolling back the in-memory mutation, or returning true to continue with the primary pool when persistence fails.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

}
return await installationCasPoolService.EnsurePoolPathAsync(installations, cancellationToken);
}
catch (Exception ex)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Cancellation is swallowed and surfaced as a hard acquisition failure.

IInstallationCasPoolService.EnsurePoolPathAsync calls cancellationToken.ThrowIfCancellationRequested(), but this blanket catch (Exception ex) catches the resulting OperationCanceledException, and the newly-added return false then turns a user cancellation into CreateFailure("Could not ensure storage for GameClient content.") at the call site instead of letting it propagate. Re-throw OperationCanceledException (and TaskCanceledException) before the generic handler so cancellation flows correctly.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

var poolPathReady = await EnsureInstallationPoolPathAsync(cancellationToken);
if (!poolPathReady)
{
return OperationResult<ContentManifest>.CreateFailure(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: New early-return path leaks the downloaded archive and extract directory.

By the time this guard runs, archivePath (downloaded archive) and extractPath (extracted content) already exist, but this return executes before CleanupTemporaryFilesAsync(archivePath, extractPath) at the end of the method, and DeliverContentAsync has no finally. Failed GameClient acquisitions will therefore leave those temp files behind on disk. Run the cleanup (or move the pool-ensure and cleanup into a try/finally) before returning.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


try
{
return Path.GetFullPath(path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: NormalizePath diverges from CasPoolManager.NormalizeRoot.

This helper calls Path.GetFullPath without Path.TrimEndingDirectorySeparator, while CasPoolManager.NormalizeRoot trims. A user-configured InstallationPoolRootPath that ends in a separator keeps it, so currentPath never equals effectivePath/candidatePath, making settingsAlreadyMatch perpetually false and triggering a settings rewrite on every run. Align the two normalizers.

Suggested change
return Path.GetFullPath(path);
return Path.TrimEndingDirectorySeparator(Path.GetFullPath(path));

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

CasRootPath = CasRootPath,
InstallationPoolRootPath = InstallationPoolRootPath,
IsInstallationPoolRootPathAutoDerived = IsInstallationPoolRootPathAutoDerived,
LegacyInstallationPoolRootPaths = [.. LegacyInstallationPoolRootPaths],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Clone throws NullReferenceException if the list is null.

If a persisted CasConfiguration deserializes LegacyInstallationPoolRootPaths as null (e.g. hand-edited or corrupted settings), the spread [.. LegacyInstallationPoolRootPaths] throws here, and the equivalent [.. userCasConfig.LegacyInstallationPoolRootPaths] in CasModule.cs runs during startup options binding. Guard the spread.

Suggested change
LegacyInstallationPoolRootPaths = [.. LegacyInstallationPoolRootPaths],
LegacyInstallationPoolRootPaths = [.. (LegacyInstallationPoolRootPaths ?? [])],

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs 319 Path.GetFullPath in NormalizeRoot/IsInsideApplicationDirectory can throw unhandled from the DI constructor on a malformed settings path, crashing app startup.
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs 140 On save failure, in-memory settings are mutated but ReinitializeInstallationPool() is skipped, leaving pool manager and settings inconsistent; also aborts acquisition for a transient persistence error.
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs 644 Blanket catch (Exception) swallows OperationCanceledException; the new return false surfaces a user cancellation as a hard "Could not ensure storage" failure.
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs 337 New early-return CreateFailure skips CleanupTemporaryFilesAsync, leaking the downloaded archive and extract directory.

SUGGESTION

File Line Issue
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs 242 NormalizePath omits TrimEndingDirectorySeparator (diverges from CasPoolManager.NormalizeRoot), so settings are rewritten every run.
GenHub/GenHub.Core/Models/Storage/CasConfiguration.cs 151 [.. LegacyInstallationPoolRootPaths] in Clone throws NRE if the property is null (also CasModule.cs at startup).
Files Reviewed (22 files)
  • GenHub/GenHub.Core/Interfaces/Common/IStorageWritabilityProbe.cs
  • GenHub/GenHub.Core/Interfaces/Storage/ICasPoolResolver.cs
  • GenHub/GenHub.Core/Interfaces/Storage/IInstallationCasPoolService.cs
  • GenHub/GenHub.Core/Models/Storage/CasConfiguration.cs - 1 issue
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/ConfigurationProviderServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/StorageLocationServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/UserSettingsServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/CasPoolWritabilityTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs
  • GenHub/GenHub/Common/Services/ConfigurationProviderService.cs
  • GenHub/GenHub/Common/Services/StorageLocationService.cs
  • GenHub/GenHub/Common/Services/StorageWritabilityProbe.cs
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs - 2 issues
  • GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs - 1 issue
  • GenHub/GenHub/Features/Storage/Services/CasPoolResolver.cs
  • GenHub/GenHub/Features/Storage/Services/CasService.cs
  • GenHub/GenHub/Features/Storage/Services/CasStorage.cs
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs - 2 issues
  • GenHub/GenHub/Infrastructure/DependencyInjection/CasModule.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/ConfigurationModule.cs

Fix these issues in Kilo Cloud


Reviewed by glm-5.2 · Input: 143.5K · Output: 37.8K · Cached: 1.4M

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant