Skip to content

Store upgrade keeps pg-runtime-prev in place instead of deleting and recreating it (prep for #4052) - #4097

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/4052-keep-pg-runtime-prev-in-place
Sep 24, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/4052-keep-pg-runtime-prev-in-place

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Why

Issue #4052 narrows the Darling service account's rights on its install tree: the service keeps Modify on pg-runtime\ and pg-runtime-prev\ themselves, but gets only Read & Execute on the install ROOT, so it can no longer create or delete folders directly under the root.

DarlingStoreUpgrade.TryAdvanceRuntimeAsync deletes and recreates pg-runtime-prev (PreviousRuntimeRootFor(runtimeRoot), a sibling of pg-runtime under the root) before rescuing the current runtime into it, and TryDeleteDirectory(previousRoot) at three cleanup sites does the same thing on success/failure paths. Under the narrowed grant, the delete succeeds (Modify on the folder itself covers that) but the recreate then fails (no rights on the root to add a new child there) — so a runtime rescue fails and the bundled PostgreSQL runtime upgrade stops applying on every host once the narrower ACL ships.

This PR makes the service empty pg-runtime-prev in place instead of deleting and recreating the folder. It is correct on today's (wider) ACLs too — no behavior change except that the folder itself persists (empty) across a rescue/cleanup instead of being deleted and remade.

What changed

  • Darling/PerformanceMonitor.Darling.Service/DarlingStoreUpgrade.cs:
    • Added internal static void EmptyDirectory(string path) — deletes every child file/subfolder recursively, leaves the folder itself; creates the folder only if it's missing, and re-throws UnauthorizedAccessException wrapped with guidance that the installer/upgrade script must be re-run to pre-create it under the narrowed root.
    • Added internal static void TryEmptyDirectory(string path) — best-effort wrapper matching TryDeleteDirectory's never-throws contract, for the cleanup call sites.
    • TryAdvanceRuntimeAsync (~line 1000): replaced if (Directory.Exists(previousRoot)) Directory.Delete(...); Directory.CreateDirectory(previousRoot); with EmptyDirectory(previousRoot);.
    • Replaced the three TryDeleteDirectory(previousRoot) cleanup calls (in the "cannot rescue, defer" catch, the failed-extract catch, and RevertRuntime's post-revert cleanup) with TryEmptyDirectory(previousRoot).
    • Left TryDeleteDirectory itself unchanged and still used for pgsql.failed staging paths (those live INSIDE pg-runtime, which keeps Modify on the root of that subtree — a different shape than the sibling-under-root problem).

Step 2 grep: other create/delete/move directly under the install root

Grepped Darling/PerformanceMonitor.Darling.Service and Darling/PerformanceMonitor.Darling.Storage for Directory.{Delete,CreateDirectory,Move} / File.{Delete,Move}. PerformanceMonitor.Darling.Storage has none. In .Service, everything else found is a different shape — not a delete/recreate of a folder directly under the install root:

  • DarlingCliCommands.cs:1006 Directory.CreateDirectory(targetDirectory) — the viewer-config export directory, which defaults to a subfolder beside darling.json (or an operator-supplied path via --output), not the install root itself.
  • DarlingCliCommands.cs:1427,1447 File.Delete(path) — best-effort cleanup of one file left by a failed viewer export, inside that same export directory.
  • DarlingManagedPostgres.cs:2255 Directory.CreateDirectory(ParentOf(_dataDirectory)) — creates the data-directory's parent under %ProgramData%\PerformanceMonitorDarling\pg, not under the service's install tree at all (it's a separate ProgramData path, always writable by the service).
  • DarlingFileLoggerProvider.cs:98 Directory.CreateDirectory(_logDirectory) — same %ProgramData%\...\logs path, not the install root.
  • DarlingManagedRoles.cs:1589/1712/1716 — compose-container credential directory management (ComposeStoreCredentialDirectory), a container-only path unrelated to a Windows install root.
  • DarlingLogHashKeyFile.cs, DarlingManagedRoles.cs (role/key file writes) — files inside directories already covered above (log-hash key directory, credential directories), not root-level creates/deletes.
  • DarlingStoreUpgrade.cs's other Directory.Move/Directory.Delete calls (pgsql swap/revert, .failed staging, retained rollback data-directory copies) all operate on paths inside pg-runtime or beside the data directory (a ProgramData path), not directly under the service's install root.

None of these match the "delete-then-recreate a folder that is a direct sibling under the narrowed install root" shape that #4052 is fixing. No further fixes made in this lane; nothing else needs one.

Tests

Darling/Darling.Tests/DarlingStoreUpgradeTests.cs:

  • Added RuntimeAdvance_LeavesPgRuntimePrevInPlace_RatherThanDeletingAndRecreatingIt: proves the rescue leaves the same pg-runtime-prev folder in place (creation time unchanged) rather than deleting/recreating it, and that a stale file inside it is gone afterward.
  • RuntimeAdvance_APreviousRuntimeItCanClear_IsReplacedAndTheSwapProceeds (existing) already asserted the stale file is gone after a successful rescue — left as-is; it still passes under the new EmptyDirectory path.

Darling/Darling.Tests/DarlingStoreUpgradeRevertTests.cs:

  • RevertRuntime_Reverts_RecordsTheBlock_AndSaysSo: updated the post-revert assertion from "the folder is gone" to "the folder exists and is empty" (matching the new TryEmptyDirectory cleanup behavior).

Other existing assertions of the shape Assert.False(Directory.Exists(PreviousRuntimeRootFor(...))) (lines 961, 1183, 1994, 2026 in DarlingStoreUpgradeTests.cs) are all on paths where the rescue never ran at all (no-stamp branch / TimescaleDB guard / downgrade refusal) — pg-runtime-prev was never created in those fixtures, so those assertions are unaffected and still correct.

Ran targeted classes iteratively (DarlingStoreUpgradeTests, DarlingStoreUpgradeRevertTests) confirming: no new failures introduced (verified by diffing failures against a pre-change build of the same classes on this branch).

Full suite (once, at the end)

Darling.Tests  Total: 13280, Errors: 0, Failed: 214, Skipped: 710, Not Run: 0, Time: 35.641s

214 failures — consistent with the ~215 Windows-only baseline this Mac rig has on dev (macOS lacks real Windows ACL/process semantics the tests exercise; confirmed by running the same suites against unmodified dev and seeing the identical 5 + 9 failures in the two classes this PR touches). No failure in either touched class is new: DarlingStoreUpgradeTests has the same 5 pre-existing failures on dev and on this branch (117 total on this branch vs 116 on dev, the +1 being this PR's new passing test); DarlingStoreUpgradeRevertTests has the same 9 pre-existing failures on both.

What I did NOT verify

Part of #4052: service-side prep only. The install-script ACL narrowing itself is PR #4090, which must merge after this one. (Worded "Part of" on purpose: the merge watcher closes any issue a body says it closes.)

CHANGELOG entry

  • Internal prep for narrowing the Darling service's install-root permissions: no user-visible behavior change.

@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 24, 2026 00:45
@erikdarlingdata
erikdarlingdata merged commit e50a2aa into dev Sep 24, 2026
17 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4052-keep-pg-runtime-prev-in-place branch September 24, 2026 00:45
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
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