From 3d5f877138541d9b96a77f2d133df7026dab52f9 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Thu, 22 Oct 2026 10:36:13 +0000 Subject: [PATCH] create: Keep cleaning up after Ctrl-C while deleting wallet files Ctrl-C during the folder delete or the macOS RAM disk detach escaped before the other step ran and before the user was told what was left. Both steps now catch it like the RPC steps do, so cleanup finishes and anything still on disk is named in the error that stops creation. Claude-Session: https://claude.ai/code/session_01T233rKgZqE5wzDm3EVTHL1 --- src/codex32/_bitcoin_core.py | 16 +++++++++++----- tests/test_bitcoin_core.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/codex32/_bitcoin_core.py b/src/codex32/_bitcoin_core.py index 4742365..360c4ea 100644 --- a/src/codex32/_bitcoin_core.py +++ b/src/codex32/_bitcoin_core.py @@ -557,11 +557,17 @@ def _close(self) -> None: pass except KeyboardInterrupt: interrupted = True - if self._folder: - shutil.rmtree(self._folder, ignore_errors=True) - detach = ("/usr/bin/hdiutil", "detach", "-force", self._disk) - if self._disk and not subprocess.run(detach, capture_output=True, check=False).returncode: - self._disk = "" + try: + if self._folder: + shutil.rmtree(self._folder, ignore_errors=True) + except KeyboardInterrupt: + interrupted = True + try: + detach = ("/usr/bin/hdiutil", "detach", "-force", self._disk) + if self._disk and not subprocess.run(detach, capture_output=True, check=False).returncode: + self._disk = "" + except KeyboardInterrupt: + interrupted = True if not os.path.exists(self._folder): self._folder = "" left = self._folder or self._disk diff --git a/tests/test_bitcoin_core.py b/tests/test_bitcoin_core.py index f4f150f..0be6650 100644 --- a/tests/test_bitcoin_core.py +++ b/tests/test_bitcoin_core.py @@ -982,6 +982,40 @@ def test_core_entropy_stops_creation_when_a_draws_wallet_stays( assert told == ([left] if "addhdkey" in fail else []) and (left in str(stopped.value)) == (not told) +@pytest.mark.parametrize("step", ("rmtree", "detach")) +def test_core_entropy_names_what_ctrl_c_left_while_deleting_files( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, step: str +) -> None: + rpc, disk, detached = _EntropyRPC(), "/dev/disk9", list[str]() + + def ram_folder(self: CoreEntropy) -> str: + self._disk = disk + return tempfile.mkdtemp(dir=tmp_path) + + def run(command: tuple[str, ...], **_options: object) -> subprocess.CompletedProcess[bytes]: + if step == "detach": + raise KeyboardInterrupt + detached.append(command[-1]) + return subprocess.CompletedProcess(command, 0) + + def interrupted(*_arguments: object, **_options: object) -> None: + raise KeyboardInterrupt + + monkeypatch.setattr(BitcoinCore, "_rpc", rpc) + monkeypatch.setattr(CoreEntropy, "_ram_folder", ram_folder) + monkeypatch.setattr("codex32._bitcoin_core.subprocess.run", run) + if step == "rmtree": + monkeypatch.setattr(shutil, "rmtree", interrupted) + entropy = CoreEntropy(BitcoinCore("bitcoin-cli", "main", 320000), lambda _message: None) + + with pytest.raises(BitcoinCoreError, match="creation stopped") as stopped: + entropy(1) + # Ctrl-C while deleting files still runs the next step, then says what to remove by hand. + left = os.path.dirname(rpc.calls[0][0][2].removeprefix("wallet_name=")) if step == "rmtree" else disk + assert f"was not removed. Delete or eject {left}." in str(stopped.value) + assert detached == ([disk] if step == "rmtree" else []) and not rpc.loaded + + @pytest.mark.parametrize("lost", ("createwallet", "unloadwallet")) def test_core_entropy_removes_a_wallet_whose_call_never_answered_then_stops( monkeypatch: pytest.MonkeyPatch, lost: str