Cut core/wire and shell/persist comment bloat ~46% (comments only, zero code change)
This commit is contained in:
+89
-128
@@ -1,21 +1,17 @@
|
||||
// prune_fs.cpp — the prune scan + THE SINGLE FILE-DELETION AUTHORITY in ReaSampler
|
||||
// (Q-W5 split of the former persist.cpp; see session.h for the TU map).
|
||||
// prune_fs.cpp — the prune scan + THE SINGLE FILE-DELETION AUTHORITY in ReaSampler.
|
||||
//
|
||||
// deleteOrphanFile below (SHFileOperationW on Windows, std::filesystem::remove on
|
||||
// SWELL platforms) is the ONLY code in the system that deletes USER files — the sole
|
||||
// deletion authority over the bank folder's bytes (the R3 prune; shells removing a
|
||||
// transient scratch file they themselves just created, e.g. the drop path's temp
|
||||
// .vstpreset, are self-cleanup, not authority over user data). It is deliberately
|
||||
// file-local (anonymous namespace): nothing outside this TU can reach it. The Q-W5
|
||||
// split CONCENTRATES the deletion authority here — it must never
|
||||
// spread (CONTEXT.md §Phase Q deletion-authority isolation;
|
||||
// docs/product/code-organization.md §7). The safety-critical "which files are
|
||||
// deleteOrphanFile below (SHFileOperationW on Windows, std::filesystem::remove
|
||||
// on SWELL platforms) is the ONLY code in the system that deletes USER files —
|
||||
// the sole deletion authority over the bank folder's bytes (a shell removing a
|
||||
// transient scratch file it just created, e.g. the drop path's temp
|
||||
// .vstpreset, is self-cleanup, not authority over user data). Deliberately
|
||||
// file-local (anonymous namespace): nothing outside this TU can reach it, and
|
||||
// this concentration must never spread. The safety-critical "which files are
|
||||
// orphans" decision stays in the pure core (prune_reconcile); this TU only
|
||||
// enumerates, resolves, stats, and — after the R3 confirm — executes.
|
||||
// enumerates, resolves, stats, and — after the confirm — executes.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. REAPER-facing only through the
|
||||
// persist_detail helpers (active-project read) and usage_scan (the pS-usage
|
||||
// instance-hold reads); this TU itself calls no REAPER API directly.
|
||||
// Compiled into the reaper_reasampler module. REAPER-facing only through the
|
||||
// persist_detail helpers and usage_scan; this TU itself calls no REAPER API directly.
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -25,12 +21,11 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// Move-to-trash surface (fork R-C, trash-preferred). On Windows the Recycle Bin is
|
||||
// reached via SHFileOperationW + FOF_ALLOWUNDO (verified against the Windows SDK
|
||||
// shellapi.h: SHFILEOPSTRUCTW { hwnd, wFunc, pFrom(double-NUL list), pTo, fFlags, ... },
|
||||
// FO_DELETE=0x3, FOF_ALLOWUNDO=0x40). No portable move-to-trash exists on the SWELL
|
||||
// (macOS/Linux) side of this codebase, so those platforms fall back to unlink behind the
|
||||
// R3 dry-run/confirm guardrail — see deleteOrphanFile below for the per-platform routing.
|
||||
// Move-to-trash surface, trash-preferred. Windows reaches the Recycle Bin via
|
||||
// SHFileOperationW + FOF_ALLOWUNDO (verified against shellapi.h: SHFILEOPSTRUCTW
|
||||
// { hwnd, wFunc, pFrom(double-NUL list), pTo, fFlags, ... }, FO_DELETE=0x3,
|
||||
// FOF_ALLOWUNDO=0x40). No portable move-to-trash exists on SWELL (macOS/Linux),
|
||||
// so those platforms fall back to unlink — see deleteOrphanFile below.
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
@@ -38,7 +33,7 @@
|
||||
|
||||
#include "shell/persist/persist_internal.h"
|
||||
#include "shell/persist/session.h"
|
||||
#include "shell/persist/usage_scan.h" // liveInstanceHeldPaths (pS-usage: instance holds join `referenced`)
|
||||
#include "shell/persist/usage_scan.h" // liveInstanceHeldPaths — instance holds join `referenced`
|
||||
|
||||
#include "core/capture/capture_paths.h" // resolveBankFile / bankRelativeForName / kBankSubfolder
|
||||
#include "core/reclaim/prune_reconcile.h" // the pure orphan decision + report tallies
|
||||
@@ -52,31 +47,24 @@ namespace fs = std::filesystem;
|
||||
using persist_detail::projectDirOf;
|
||||
using persist_detail::readActiveProject;
|
||||
|
||||
// The dry-run file-list display cap: the orphan COUNT and reclaimed SIZE are always
|
||||
// exact (tallied over the full orphan set), but the enumerated file list handed to the
|
||||
// console is clipped to this many entries so a project with thousands of orphans does
|
||||
// not flood the report. PruneReport::truncated flags the clip. R3's confirm surface can
|
||||
// choose its own presentation; this is purely the Wave-2 dry-run readout ceiling.
|
||||
// The dry-run file-list display cap: count and size are always exact
|
||||
// (tallied over the full orphan set), but the enumerated list handed to the
|
||||
// console is clipped so a project with thousands of orphans does not flood
|
||||
// the report. PruneReport::truncated flags the clip.
|
||||
constexpr std::size_t kPruneListDisplayCap = 64;
|
||||
|
||||
// A fresh enumerate + pure-core prune compute for the active project. Shared by the
|
||||
// dry-run report (pruneDryRun), the full-set query (pruneOrphanSet), and the deletion
|
||||
// (pruneReclaim) so all three agree on ONE resolution + enumeration + set-algebra path
|
||||
// (no divergence between what is shown and what is deleted). REAPER-facing (resolves the
|
||||
// active project, enumerates the folder) but writes nothing.
|
||||
// A fresh enumerate + pure-core prune compute for the active project. Shared
|
||||
// by the dry-run report, the full-set query, and the deletion so all three
|
||||
// agree on one resolution + enumeration + set-algebra path — no divergence
|
||||
// between what is shown and what is deleted. REAPER-facing but writes nothing.
|
||||
//
|
||||
// * bankDirAbs — the resolved CURRENT bank folder (absolute, forward-slashed). Empty
|
||||
// when there is no active/saved project, no project dir, or no folder on
|
||||
// disk yet -> the caller treats an empty dir as "nothing to reclaim".
|
||||
// * orphans — the FULL orphan set (owned ∩ present) − referenced, in enumeration
|
||||
// order, untruncated. The pure core decides; this only supplies inputs.
|
||||
// * sizeByRel — per-orphan-relative on-disk byte size (0 when it could not be stat'd).
|
||||
// * abortedUnreadableUsage — true iff a present rsusage_* instance-usage record could
|
||||
// not be read/decoded (pS-usage fail-safe): `orphans` is left EMPTY —
|
||||
// the prune must halt rather than proceed with degraded protection.
|
||||
// An empty orphan set is itself the delete-side guarantee (every
|
||||
// consumer of this scan deletes at most `orphans ∩ ...`), the flag is
|
||||
// what lets the action TELL the user instead of claiming "no orphans".
|
||||
// * bankDirAbs — the resolved current bank folder. Empty when there is no
|
||||
// active/saved project, no project dir, or no folder yet.
|
||||
// * orphans — the full orphan set, untruncated. The pure core decides.
|
||||
// * sizeByRel — per-orphan on-disk byte size (0 when it could not be stat'd).
|
||||
// * abortedUnreadableUsage — true iff a present rsusage_* record could not
|
||||
// be read/decoded: `orphans` is left EMPTY, the prune must
|
||||
// halt rather than proceed with degraded protection.
|
||||
struct PruneScan {
|
||||
std::string bankDirAbs;
|
||||
std::vector<std::string> orphans;
|
||||
@@ -95,10 +83,8 @@ PruneScan scanPruneOrphans(const BankBook& book,
|
||||
void* proj = readActiveProject(rppPath);
|
||||
if (!proj || rppPath.empty()) return scan; // no active/saved project -> empty scan
|
||||
|
||||
// Resolve the CURRENT bank folder the same way the index does (M4): project dir of
|
||||
// the live .rpp + the fixed bank subfolder. Never a stored absolute path, so a
|
||||
// Save-As relocation is followed automatically. resolveBankFile is the shared M4
|
||||
// arithmetic; feeding it the bank subfolder as the "relative path" yields the folder.
|
||||
// Resolve the current bank folder the same way the index does — never a
|
||||
// stored absolute path, so a Save-As relocation is followed automatically.
|
||||
const std::string projectDir = projectDirOf(rppPath);
|
||||
const std::string bankDir =
|
||||
capture::resolveBankFile(projectDir, capture::kBankSubfolder);
|
||||
@@ -109,15 +95,11 @@ PruneScan scanPruneOrphans(const BankBook& book,
|
||||
return scan; // no bank folder captured yet -> nothing to reclaim
|
||||
}
|
||||
|
||||
// Enumerate the folder into project-relative index-spelled paths, spelled the SAME
|
||||
// way the capture path spelled them (bankRelativeForName == deriveBankPaths's
|
||||
// convention) so the pure core's exact-string match lines up with referencedPaths()
|
||||
// and the manifest. Non-recursive: the bank folder is flat (capture writes files
|
||||
// directly here); skip any subdirectory. Size is stat'd here and cached by relative
|
||||
// path so the report's byte tally reuses the same on-disk read.
|
||||
// Manual iterator form (it.increment(ec)) keeps the loop non-throwing: a mid-iteration
|
||||
// failure (file removed, permission flip) breaks out with a best-effort partial list
|
||||
// rather than propagating std::filesystem_error across REAPER's C ABI.
|
||||
// Enumerate into project-relative paths spelled the SAME way the capture
|
||||
// path spells them, so the pure core's exact-string match lines up with
|
||||
// referencedPaths() and the manifest. Non-recursive: the bank folder is
|
||||
// flat. Manual iterator form (it.increment(ec)) keeps the loop
|
||||
// non-throwing on a mid-iteration failure.
|
||||
std::vector<std::string> present;
|
||||
fs::directory_iterator it(bankDir, ec);
|
||||
for (; !ec && it != fs::directory_iterator{}; it.increment(ec)) {
|
||||
@@ -133,24 +115,20 @@ PruneScan scanPruneOrphans(const BankBook& book,
|
||||
scan.sizeByRel[rel] = sz_ec ? 0 : static_cast<std::uint64_t>(sz);
|
||||
}
|
||||
|
||||
// The decision lives in the pure core — read-only inputs from the book and manifest.
|
||||
// referencedPaths() unions across the whole book (pool included); owned().paths() is
|
||||
// the manifest set. pS-usage: the referenced set additionally unions every LIVE
|
||||
// ReaSampler 9000 instance's held captures (usage_scan reads the per-instance
|
||||
// rsusage_* records + the live FX enumeration; sample_usage decides liveness,
|
||||
// including the protect-all net when zero instances were identified) — a capture
|
||||
// any live instance holds can NEVER be an orphan, even when its bank entry was
|
||||
// deleted while the instance kept its ref. liveInstanceHeldPaths is READ-ONLY,
|
||||
// preserving this scan's no-write contract. This shell only enumerates, resolves,
|
||||
// and stats.
|
||||
// The decision lives in the pure core — read-only inputs from the book and
|
||||
// manifest. referencedPaths() unions across the whole book; the referenced
|
||||
// set additionally unions every LIVE ReaSampler 9000 instance's held
|
||||
// captures (usage_scan + sample_usage decide liveness) — a capture any
|
||||
// live instance holds can never be an orphan, even if its bank entry was
|
||||
// deleted while the instance kept its ref. liveInstanceHeldPaths is
|
||||
// read-only; this shell only enumerates, resolves, and stats.
|
||||
scan.bankDirAbs = bankDir;
|
||||
const UsageScanResult usage = liveInstanceHeldPaths(proj);
|
||||
if (usage.abortPrune) {
|
||||
// FAIL-SAFE ABORT: a present rsusage_* record could not be read/decoded, so the
|
||||
// protected set is unknowable. Compute NO orphans — every downstream consumer
|
||||
// (dry-run report, confirm set, fresh-recompute delete plan) then deletes
|
||||
// nothing. The flag + key names surface the reason so the action can name each
|
||||
// offending key for operator recovery.
|
||||
// FAIL-SAFE ABORT: a present rsusage_* record could not be read/decoded,
|
||||
// so the protected set is unknowable. Compute NO orphans — every
|
||||
// downstream consumer then deletes nothing. The key names let the
|
||||
// action tell the user which keys to recover.
|
||||
scan.abortedUnreadableUsage = true;
|
||||
scan.offendingUsageKeys = usage.offendingKeys;
|
||||
return scan;
|
||||
@@ -162,38 +140,31 @@ PruneScan scanPruneOrphans(const BankBook& book,
|
||||
return scan;
|
||||
}
|
||||
|
||||
// Deletes ONE orphan file, trash-preferred (fork R-C, settled). Returns true iff the
|
||||
// file was deleted BY THIS CALL (reclaimed here). Returns false for two distinct cases:
|
||||
// * `outAlreadyAbsent` set true — the file was already gone before we touched it;
|
||||
// the caller folds this into the stale/staleness tally, NOT reclaimedCount.
|
||||
// * `outAlreadyAbsent` left false — a real delete failure (locked, conversion error);
|
||||
// the caller folds this into skippedCount.
|
||||
// `absPath` is the resolved absolute path (forward-slashed). NON-THROWING: no exception
|
||||
// may cross the C ABI.
|
||||
// Deletes ONE orphan file, trash-preferred. Returns true iff deleted by this
|
||||
// call. Returns false with `outAlreadyAbsent` set when the file was already
|
||||
// gone (caller folds into staleness, not reclaimedCount); false with it unset
|
||||
// on a real delete failure (locked, conversion error — folds into
|
||||
// skippedCount). `absPath` is the resolved absolute path. Non-throwing: no
|
||||
// exception may cross the C ABI.
|
||||
//
|
||||
// Per-platform routing:
|
||||
// * Windows — SHFileOperationW(FO_DELETE, pFrom=<double-NUL path>, FOF_ALLOWUNDO |
|
||||
// FOF_NOCONFIRMATION | FOF_SILENT | FOF_NOERRORUI). FOF_ALLOWUNDO routes to the
|
||||
// Recycle Bin (recoverable); the no-UI flags suppress REAPER-blocking dialogs (our
|
||||
// own confirm already happened). Verified against shellapi.h. `outUsedTrash` set true.
|
||||
// * Other (SWELL: macOS/Linux) — no portable move-to-trash surface is available in this
|
||||
// codebase, so fall back to std::filesystem::remove (hard unlink) behind the R3
|
||||
// confirm guardrail. `outUsedTrash` left as-is (false).
|
||||
// Windows routes through SHFileOperationW + FOF_ALLOWUNDO (Recycle Bin,
|
||||
// recoverable); the no-UI flags suppress REAPER-blocking dialogs since our own
|
||||
// confirm already happened. Other platforms (SWELL: macOS/Linux) have no
|
||||
// portable move-to-trash surface, so they fall back to std::filesystem::remove
|
||||
// (hard unlink) behind the confirm guardrail.
|
||||
bool deleteOrphanFile(const std::string& absPath, bool& outUsedTrash,
|
||||
bool& outAlreadyAbsent) {
|
||||
#ifdef _WIN32
|
||||
// Convert forward-slashed UTF-8 to a back-slashed, double-NUL-terminated wide string.
|
||||
// SHFileOperation's pFrom is a list; a single path still needs the extra terminating
|
||||
// NUL. Backslashes are required (shell APIs reject forward slashes in some cases).
|
||||
// Back-slashed, double-NUL-terminated wide string: SHFileOperation's
|
||||
// pFrom is a list (needs the extra terminating NUL) and rejects forward
|
||||
// slashes in some cases.
|
||||
std::string win = absPath;
|
||||
for (char& c : win) if (c == '/') c = '\\';
|
||||
|
||||
const int wlen = MultiByteToWideChar(CP_UTF8, 0, win.c_str(), -1, nullptr, 0);
|
||||
if (wlen <= 0) return false; // conversion failed -> real skip (outAlreadyAbsent stays false)
|
||||
if (wlen <= 0) return false; // conversion failed -> real skip
|
||||
std::vector<wchar_t> wbuf(static_cast<std::size_t>(wlen) + 1, L'\0'); // +1 for list NUL
|
||||
MultiByteToWideChar(CP_UTF8, 0, win.c_str(), -1, wbuf.data(), wlen);
|
||||
// wbuf now holds the path + its NUL at [wlen-1]; the extra trailing L'\0' at [wlen]
|
||||
// makes it the double-NUL-terminated single-element list SHFileOperation wants.
|
||||
|
||||
SHFILEOPSTRUCTW op{};
|
||||
op.hwnd = nullptr;
|
||||
@@ -207,22 +178,20 @@ bool deleteOrphanFile(const std::string& absPath, bool& outUsedTrash,
|
||||
outUsedTrash = true;
|
||||
return true; // deleted this call -> reclaimed
|
||||
}
|
||||
// SHFileOperation failed (e.g. file already gone yields a nonzero code on some
|
||||
// versions, or a lock). Distinguish "already absent" from a real failure so the
|
||||
// caller can tally them separately (absent -> staleness skip; failure -> locked skip).
|
||||
// Distinguish "already absent" (nonzero return on some REAPER versions
|
||||
// for a vanished file) from a real failure so the caller can tally separately.
|
||||
std::error_code ec;
|
||||
if (!fs::exists(absPath, ec)) {
|
||||
outAlreadyAbsent = true; // vanished between scan and delete -> staleness, not reclaim
|
||||
outAlreadyAbsent = true;
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
// No portable trash surface on SWELL platforms -> hard unlink behind the confirm.
|
||||
// No portable trash surface on SWELL platforms -> hard unlink.
|
||||
std::error_code ec;
|
||||
const bool removed = fs::remove(absPath, ec);
|
||||
if (removed) return true; // deleted this call -> reclaimed
|
||||
if (ec) return false; // a real failure (locked / permission) -> skip
|
||||
// remove returned false with no error == the file did not exist -> already gone.
|
||||
outAlreadyAbsent = true; // vanished between scan and delete -> staleness, not reclaim
|
||||
if (removed) return true;
|
||||
if (ec) return false; // real failure (locked/permission) -> skip
|
||||
outAlreadyAbsent = true; // no error, no removal -> already gone
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
@@ -231,53 +200,47 @@ bool deleteOrphanFile(const std::string& absPath, bool& outUsedTrash,
|
||||
|
||||
reclaim::PruneReport ReaSamplerSession::pruneDryRun() const {
|
||||
const PruneScan scan = scanPruneOrphans(book_, owned_);
|
||||
// buildPruneReport tallies count / byte-sum / display-truncation — no report logic
|
||||
// re-implemented here. An empty scan (no project / no folder) yields a zero report.
|
||||
reclaim::PruneReport report =
|
||||
reclaim::buildPruneReport(scan.orphans, scan.sizeByRel, kPruneListDisplayCap);
|
||||
// pS-usage fail-safe: surface the unreadable-record abort so the action halts with
|
||||
// an explicit message instead of reporting "no orphaned files" (the count IS zero —
|
||||
// the scan computed nothing — but the user must know the prune refused to run).
|
||||
// The offending key names propagate so the action can name each one for recovery.
|
||||
// Surface the unreadable-usage abort so the action halts with an explicit
|
||||
// message instead of reporting "no orphaned files" — the count IS zero,
|
||||
// but the user must know the prune refused to run.
|
||||
report.abortedUnreadableUsage = scan.abortedUnreadableUsage;
|
||||
report.offendingUsageKeys = scan.offendingUsageKeys;
|
||||
return report;
|
||||
}
|
||||
|
||||
std::vector<std::string> ReaSamplerSession::pruneOrphanSet() const {
|
||||
return scanPruneOrphans(book_, owned_).orphans; // FULL set, untruncated
|
||||
return scanPruneOrphans(book_, owned_).orphans; // full set, untruncated
|
||||
}
|
||||
|
||||
reclaim::PruneDeletionResult ReaSamplerSession::pruneReclaim(
|
||||
const std::vector<std::string>& confirmed) const {
|
||||
reclaim::PruneDeletionResult result;
|
||||
|
||||
// Re-enumerate + run the pure core FRESH (never a stale set): the deletion targets
|
||||
// exactly `confirmed ∩ freshOrphans` (pruneDeletePlan). A file that vanished or became
|
||||
// referenced between confirm and delete drops out of freshOrphans and is skipped; a
|
||||
// newly-appeared orphan not in `confirmed` is never swept without its own confirm.
|
||||
// Because freshOrphans is itself a pure-core output, the plan can contain NO referenced
|
||||
// and NO hand-dropped file — the R-C/R-D safety survives the recompute.
|
||||
// pS-usage: if THIS fresh scan hits an unreadable rsusage_* record it aborts with an
|
||||
// EMPTY orphan set, so the plan below intersects to empty and nothing is deleted —
|
||||
// the fail-safe holds even in the confirm→delete window, with no extra branch here.
|
||||
// Re-enumerate + run the pure core FRESH (never a stale set): deletion
|
||||
// targets exactly `confirmed ∩ freshOrphans`, so a file that vanished or
|
||||
// became referenced between confirm and delete is skipped, and a newly-
|
||||
// appeared orphan not in `confirmed` is never swept. If this fresh scan
|
||||
// hits an unreadable usage record it aborts with an EMPTY orphan set, so
|
||||
// the plan below intersects to empty and nothing is deleted — the
|
||||
// fail-safe holds even in the confirm-to-delete window.
|
||||
const PruneScan scan = scanPruneOrphans(book_, owned_);
|
||||
if (scan.bankDirAbs.empty()) return result; // no project / no folder -> nothing
|
||||
|
||||
const std::vector<std::string> plan =
|
||||
reclaim::pruneDeletePlan(confirmed, scan.orphans);
|
||||
|
||||
// Staleness skip count: entries the user confirmed that are no longer fresh orphans
|
||||
// (vanished or became referenced between confirm and delete). pruneDeletePlan already
|
||||
// de-dups confirmed internally, so compute the unique-confirmed size to avoid counting
|
||||
// de-duplicated entries as stale — that would be dishonest.
|
||||
// Staleness skip count: confirmed entries no longer fresh orphans.
|
||||
// pruneDeletePlan de-dups confirmed internally, so compare against the
|
||||
// unique-confirmed size to avoid counting de-duped entries as stale.
|
||||
const std::size_t uniqueConfirmedCount =
|
||||
std::unordered_set<std::string>(confirmed.begin(), confirmed.end()).size();
|
||||
result.skippedCount += uniqueConfirmedCount - plan.size();
|
||||
|
||||
for (const std::string& rel : plan) {
|
||||
// Reconstruct the absolute path from the resolved bank dir + the entry's file name.
|
||||
// rel is index-spelled "<kBankSubfolder>/<name>"; the name is the tail after '/'.
|
||||
// rel is index-spelled "<kBankSubfolder>/<name>"; reconstruct the
|
||||
// absolute path from the resolved bank dir + the tail after '/'.
|
||||
const std::string::size_type slash = rel.find_last_of('/');
|
||||
const std::string name = (slash == std::string::npos) ? rel : rel.substr(slash + 1);
|
||||
if (name.empty()) { ++result.skippedCount; continue; }
|
||||
@@ -291,9 +254,7 @@ reclaim::PruneDeletionResult ReaSamplerSession::pruneReclaim(
|
||||
++result.reclaimedCount;
|
||||
result.reclaimedBytes += bytes;
|
||||
} else if (alreadyAbsent) {
|
||||
// File vanished between plan and delete — treat as staleness, same as the
|
||||
// confirm→plan gap above. Does NOT count as reclaimed (we didn't delete it).
|
||||
++result.skippedCount;
|
||||
++result.skippedCount; // vanished between plan and delete -> staleness
|
||||
} else {
|
||||
++result.skippedCount; // locked / conversion failure -> recorded, not thrown
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user