From d2db1752b2c480b3e78ddf80dbc3db2a9eaa2c88 Mon Sep 17 00:00:00 2001 From: Nic Dorman Date: Wed, 13 May 2026 15:14:58 +0000 Subject: [PATCH] fix(ant-dev): clean up orphan anvil/antnode and stale node identities on stop ant-devnet keeps anvil alive past Testnet::new scope via std::mem::forget on the AnvilInstance, then relies on graceful Drop at process exit to clean it up. SIGTERM/SIGKILL skip destructors, so every ant dev stop leaks one anvil child and one ~/.local/share/ant/nodes// tree for each of the spawned nodes. After a handful of start/stop or killed-mid-startup cycles, the LXC accumulates orphan anvils plus 100+ stale node dirs, and subsequent ant dev start runs flake or hang. This is a workaround at the ant-dev layer (Option B in #73). The proper fix lives in ant-devnet itself (Option A: tempfile::TempDir + tokio signal handler, mirroring how ant-clients MiniTestnet and ant-nodes tests/e2e/testnet.rs already do it) and will be a separate PR against WithAutonomi/ant-node. In ant dev stop now: - pkill anvil and antnode in addition to ant-devnet - rm -rf ~/.local/share/ant/nodes and ~/.local/share/ant/spill so the next start begins from a clean state - Centralise the pkill calls into a _pkill() helper No behaviour change on Windows (the pkill / rm paths are POSIX-only). Closes #16 (local task); helps mitigate #73 (upstream). --- ant-dev/src/ant_dev/cmd_stop.py | 43 ++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/ant-dev/src/ant_dev/cmd_stop.py b/ant-dev/src/ant_dev/cmd_stop.py index 308d9a7f..4a576756 100644 --- a/ant-dev/src/ant_dev/cmd_stop.py +++ b/ant-dev/src/ant_dev/cmd_stop.py @@ -1,8 +1,9 @@ -"""``ant dev stop`` — Tear down all local processes.""" +"""``ant dev stop`` -- Tear down all local processes.""" from __future__ import annotations import os +import shutil import subprocess import sys @@ -34,6 +35,13 @@ def _kill_pid(pid: int) -> None: pass +def _pkill(pattern: str) -> None: + """Best-effort pkill on POSIX; no-op on Windows.""" + if sys.platform == "win32": + return + subprocess.run(["pkill", "-9", "-f", pattern], capture_output=True) + + def run(args) -> None: state = load_state() @@ -45,24 +53,35 @@ def run(args) -> None: print(yellow("[1/2] Stopping antd...")) if pid := state.get("antd_pid"): _kill_pid(pid) - if sys.platform != "win32": - subprocess.run( - ["pkill", "-f", r"target/(debug|release)/antd"], - capture_output=True, - ) + _pkill(r"target/(debug|release)/antd") print(green(" Done")) - # 2. Kill devnet + # 2. Kill devnet + the orphan children it leaves behind. + # + # ant-devnet does ``std::mem::forget(testnet)`` on the AnvilInstance to + # keep anvil running across the scope, then relies on process exit to + # clean it up. That cleanup only fires on graceful Drop -- SIGTERM/ + # SIGKILL skip destructors, so anvil orphans every time we stop. Reap + # it explicitly, plus any antnode children spawned by ant-devnet. + # Tracked upstream in WithAutonomi/ant-sdk#73. print(yellow("[2/2] Stopping ant devnet...")) if pid := state.get("devnet_pid"): _kill_pid(pid) - if sys.platform != "win32": - subprocess.run( - ["pkill", "-f", r"target/(debug|release)/ant-devnet"], - capture_output=True, - ) + _pkill(r"target/(debug|release)/ant-devnet") + _pkill(r"(^|/)anvil( |$)") + _pkill(r"target/(debug|release)/antnode") print(green(" Done")) + # ant-devnet does not clean up ~/.local/share/ant/ on SIGTERM either + # (same destructor-skip cause). Stale node identities accumulating + # there have caused subsequent ``ant dev start`` runs to flake/hang. + # Wipe known transient data dirs so the next start is from a clean slate. + if sys.platform != "win32": + for sub in ("nodes", "spill"): + path = os.path.expanduser(f"~/.local/share/ant/{sub}") + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + clear_state() print()