From 31b00218fe2d330f76e270ae9bcb07206522cd55 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 27 Jun 2025 16:50:48 +0200 Subject: [PATCH] Show which PID is causing a temp root Example: error: Cannot delete path '/nix/store/klyng5rpdkwi5kbxkncy4gjwb490dlhb-foo.drv' because it's in use by Nix process '{nix-process:3605324}'. --- src/libstore/gc.cc | 26 ++++++++++++------- src/libstore/include/nix/store/gc-store.hh | 5 +++- tests/functional/gc-runtime.nix | 1 + tests/functional/gc-runtime.sh | 9 +++++-- tests/functional/gc.sh | 4 +-- .../local-overlay-store/delete-refs-inner.sh | 10 +++---- 6 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc index 91f2ba43f..3f5c2b39e 100644 --- a/src/libstore/gc.cc +++ b/src/libstore/gc.cc @@ -208,7 +208,7 @@ void LocalStore::findTempRoots(Roots & tempRoots, bool censor) while ((end = contents.find((char) 0, pos)) != std::string::npos) { Path root(contents, pos, end - pos); debug("got temporary root '%s'", root); - tempRoots[parseStorePath(root)].emplace(censor ? censored : fmt("{temp:%d}", pid)); + tempRoots[parseStorePath(root)].emplace(censor ? censored : fmt("{nix-process:%d}", pid)); pos = end + 1; } } @@ -465,7 +465,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) { // The temp roots only store the hash part to make it easier to // ignore suffixes like '.lock', '.chroot' and '.check'. - std::unordered_set tempRoots; + std::unordered_map tempRoots; // Hash part of the store path currently being deleted, if // any. @@ -574,7 +574,8 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) debug("got new GC root '%s'", path); auto hashPart = std::string(storePath->hashPart()); auto shared(_shared.lock()); - shared->tempRoots.insert(hashPart); + // FIXME: could get the PID from the socket. + shared->tempRoots.insert_or_assign(hashPart, "{nix-process:unknown}"); /* If this path is currently being deleted, then we have to wait until deletion is finished to ensure that @@ -618,10 +619,14 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) /* Read the temporary roots created before we acquired the global GC root. Any new roots will be sent to our socket. */ - Roots tempRoots; - findTempRoots(tempRoots, options.censor); - for (auto & root : tempRoots) - _shared.lock()->tempRoots.insert(std::string(root.first.hashPart())); + { + Roots tempRoots; + findTempRoots(tempRoots, options.censor); + for (auto & root : tempRoots) + _shared.lock()->tempRoots.insert_or_assign( + std::string(root.first.hashPart()), + *root.second.begin()); + } /* Synchronisation point for testing, see tests/functional/gc-non-blocking.sh. */ if (auto p = getEnv("_NIX_TEST_GC_SYNC_2")) @@ -735,11 +740,12 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) { auto hashPart = std::string(path->hashPart()); auto shared(_shared.lock()); - if (shared->tempRoots.count(hashPart)) { + if (auto i = shared->tempRoots.find(hashPart); i != shared->tempRoots.end()) { if (options.action == GCOptions::gcDeleteSpecific) throw Error( - "Cannot delete path '%s' because it's in use by a Nix process.", - printStorePath(start)); + "Cannot delete path '%s' because it's in use by '%s'.", + printStorePath(start), + i->second); return markAlive(); } shared->pending = hashPart; diff --git a/src/libstore/include/nix/store/gc-store.hh b/src/libstore/include/nix/store/gc-store.hh index 6b73ffc65..23261f576 100644 --- a/src/libstore/include/nix/store/gc-store.hh +++ b/src/libstore/include/nix/store/gc-store.hh @@ -7,8 +7,11 @@ namespace nix { +// FIXME: should turn this into an std::variant to represent the +// several root types. +using GcRootInfo = std::string; -typedef std::unordered_map> Roots; +typedef std::unordered_map> Roots; struct GCOptions diff --git a/tests/functional/gc-runtime.nix b/tests/functional/gc-runtime.nix index ee5980bdf..df7f8ad16 100644 --- a/tests/functional/gc-runtime.nix +++ b/tests/functional/gc-runtime.nix @@ -9,6 +9,7 @@ mkDerivation { cat > $out/program < \$TEST_ROOT/fifo sleep 10000 EOF diff --git a/tests/functional/gc-runtime.sh b/tests/functional/gc-runtime.sh index 0cccaaf16..34e99415d 100755 --- a/tests/functional/gc-runtime.sh +++ b/tests/functional/gc-runtime.sh @@ -21,11 +21,16 @@ nix-env -p "$profiles/test" -f ./gc-runtime.nix -i gc-runtime outPath=$(nix-env -p "$profiles/test" -q --no-name --out-path gc-runtime) echo "$outPath" +fifo="$TEST_ROOT/fifo" +mkfifo "$fifo" + echo "backgrounding program..." -"$profiles"/test/program & -sleep 2 # hack - wait for the program to get started +"$profiles"/test/program "$fifo" & child=$! echo PID=$child +cat "$fifo" + +expectStderr 1 nix-store --delete "$outPath" | grepQuiet "Cannot delete path.*because it's referenced by the GC root '/proc/" nix-env -p "$profiles/test" -e gc-runtime nix-env -p "$profiles/test" --delete-generations old diff --git a/tests/functional/gc.sh b/tests/functional/gc.sh index c58f47021..66dd12eac 100755 --- a/tests/functional/gc.sh +++ b/tests/functional/gc.sh @@ -23,10 +23,10 @@ if nix-store --gc --print-dead | grep -E "$outPath"$; then false; fi nix-store --gc --print-dead inUse=$(readLink "$outPath/reference-to-input-2") -if nix-store --delete "$inUse"; then false; fi +expectStderr 1 nix-store --delete "$inUse" | grepQuiet "Cannot delete path.*because it's referenced by the GC root " test -e "$inUse" -if nix-store --delete "$outPath"; then false; fi +expectStderr 1 nix-store --delete "$outPath" | grepQuiet "Cannot delete path.*because it's referenced by the GC root " test -e "$outPath" for i in "$NIX_STORE_DIR"/*; do diff --git a/tests/functional/local-overlay-store/delete-refs-inner.sh b/tests/functional/local-overlay-store/delete-refs-inner.sh index 385eeadc9..01b6162c5 100644 --- a/tests/functional/local-overlay-store/delete-refs-inner.sh +++ b/tests/functional/local-overlay-store/delete-refs-inner.sh @@ -22,14 +22,14 @@ input2=$(nix-build ../hermetic.nix --no-out-link --arg busybox "$busybox" --arg input3=$(nix-build ../hermetic.nix --no-out-link --arg busybox "$busybox" --arg withFinalRefs true --arg seed 2 -A passthru.input3 -j0) # Can't delete because referenced -expectStderr 1 nix-store --delete $input1 | grepQuiet "Cannot delete path" -expectStderr 1 nix-store --delete $input2 | grepQuiet "Cannot delete path" -expectStderr 1 nix-store --delete $input3 | grepQuiet "Cannot delete path" +expectStderr 1 nix-store --delete $input1 | grepQuiet "Cannot delete path.*because it's referenced by path" +expectStderr 1 nix-store --delete $input2 | grepQuiet "Cannot delete path.*because it's referenced by path" +expectStderr 1 nix-store --delete $input3 | grepQuiet "Cannot delete path.*because it's referenced by path" # These same paths are referenced in the lower layer (by the seed 1 # build done in `initLowerStore`). -expectStderr 1 nix-store --store "$storeA" --delete $input2 | grepQuiet "Cannot delete path" -expectStderr 1 nix-store --store "$storeA" --delete $input3 | grepQuiet "Cannot delete path" +expectStderr 1 nix-store --store "$storeA" --delete $input2 | grepQuiet "Cannot delete path.*because it's referenced by path" +expectStderr 1 nix-store --store "$storeA" --delete $input3 | grepQuiet "Cannot delete path.*because it's referenced by path" # Can delete nix-store --delete $hermetic