cargo_tangle/command/dev/
down.rs1use std::fs;
4use std::path::{Path, PathBuf};
5
6use clap::Args;
7use color_eyre::eyre::{Result, bail};
8
9use crate::workspace::WORKSPACE_FILE;
10
11const DEV_DIR: &str = ".tangle/dev";
12
13#[derive(Args, Debug)]
14pub struct DownArgs {
15 #[arg(long)]
17 pub keep_workspace: bool,
18}
19
20pub fn execute(args: DownArgs) -> Result<()> {
21 let dev_dir = PathBuf::from(DEV_DIR);
22
23 if !dev_dir.exists() && !Path::new(WORKSPACE_FILE).exists() {
24 println!("No devnet found in this directory.");
25 return Ok(());
26 }
27
28 let mut stopped = 0usize;
29 for (label, file) in [("anvil", "anvil.pid"), ("manager", "manager.pid")] {
33 let path = dev_dir.join(file);
34 if let Some(pid) = read_pid(&path) {
35 if pid_alive(pid) {
36 if let Err(e) = kill_pid(pid) {
37 eprintln!("⚠ failed to stop {label} ({pid}): {e}");
38 } else {
39 println!("✓ stopped {label} (PID {pid})");
40 stopped += 1;
41 }
42 }
43 }
44 }
45
46 if dev_dir.exists() {
47 fs::remove_dir_all(&dev_dir)?;
48 println!("✓ removed {}", dev_dir.display());
49 if let Some(parent) = dev_dir.parent() {
51 if parent.as_os_str() != "" && parent.is_dir() {
52 if let Ok(mut iter) = fs::read_dir(parent) {
53 if iter.next().is_none() {
54 let _ = fs::remove_dir(parent);
55 }
56 }
57 }
58 }
59 }
60
61 if !args.keep_workspace && Path::new(WORKSPACE_FILE).exists() {
62 if workspace_is_dev() {
64 fs::remove_file(WORKSPACE_FILE)?;
65 println!("✓ removed {WORKSPACE_FILE}");
66 } else {
67 println!(
68 " leaving {WORKSPACE_FILE} (active network is not 'local'; use --keep-workspace to silence this)"
69 );
70 }
71 }
72
73 if stopped == 0 && dev_dir.exists() {
74 bail!("no running anvil/manager found (already stopped?)");
75 }
76
77 println!("Devnet is down.");
78 Ok(())
79}
80
81fn workspace_is_dev() -> bool {
82 let Ok(s) = fs::read_to_string(WORKSPACE_FILE) else {
83 return false;
84 };
85 s.contains(crate::command::dev::up::MANAGED_MARKER)
89}
90
91fn read_pid(path: &Path) -> Option<u32> {
92 fs::read_to_string(path)
93 .ok()
94 .and_then(|s| s.trim().parse().ok())
95}
96
97fn pid_alive(pid: u32) -> bool {
98 use nix::sys::signal::kill;
99 use nix::unistd::Pid;
100 kill(Pid::from_raw(pid as i32), None).is_ok()
101}
102
103fn kill_pid(pid: u32) -> Result<()> {
104 use nix::errno::Errno;
105 use nix::sys::signal::{Signal, kill};
106 use nix::unistd::Pid;
107 match kill(Pid::from_raw(pid as i32), Signal::SIGTERM) {
108 Ok(()) | Err(Errno::ESRCH) => Ok(()),
109 Err(e) => Err(color_eyre::eyre::eyre!("SIGTERM to {pid} failed: {e}")),
110 }
111}