ant_node_manager/cmd/
local.rs

1// Copyright (C) 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9#![allow(clippy::too_many_arguments)]
10
11use super::get_bin_path;
12use crate::{
13    VerbosityLevel,
14    add_services::config::PortRange,
15    local::{LocalNetworkOptions, kill_network, run_network},
16    print_banner, status_report,
17};
18use ant_bootstrap::InitialPeersConfig;
19use ant_evm::{EvmNetwork, RewardsAddress};
20use ant_logging::LogFormat;
21use ant_releases::{AntReleaseRepoActions, ReleaseType};
22use ant_service_management::{
23    NodeRegistryManager, control::ServiceController, get_local_node_registry_path,
24};
25use color_eyre::{Help, Report, Result, eyre::eyre};
26use std::{
27    path::PathBuf,
28    process::{Command, Stdio},
29};
30use sysinfo::System;
31use tokio::time::{Duration, sleep};
32
33pub async fn join(
34    build: bool,
35    count: u16,
36    enable_metrics_server: bool,
37    interval: u64,
38    metrics_port: Option<PortRange>,
39    node_path: Option<PathBuf>,
40    node_port: Option<PortRange>,
41    node_version: Option<String>,
42    log_format: Option<LogFormat>,
43    _peers_args: InitialPeersConfig,
44    rpc_port: Option<PortRange>,
45    rewards_address: RewardsAddress,
46    evm_network: EvmNetwork,
47    skip_validation: bool,
48    verbosity: VerbosityLevel,
49) -> Result<(), Report> {
50    if verbosity != VerbosityLevel::Minimal {
51        print_banner("Joining Local Network");
52    }
53    info!("Joining local network");
54
55    if (enable_metrics_server || metrics_port.is_some()) && !cfg!(feature = "open-metrics") && build
56    {
57        return Err(eyre!(
58            "Metrics server is not available. Please enable the open-metrics feature flag. Run the command with the --features open-metrics"
59        ));
60    }
61
62    let local_node_reg_path = &get_local_node_registry_path()?;
63    let local_node_registry = NodeRegistryManager::load(local_node_reg_path).await?;
64
65    let release_repo = <dyn AntReleaseRepoActions>::default_config();
66
67    let antnode_bin_path = get_bin_path(
68        build,
69        node_path,
70        ReleaseType::AntNode,
71        node_version,
72        &*release_repo,
73        verbosity,
74    )
75    .await?;
76
77    let options = LocalNetworkOptions {
78        antnode_bin_path,
79        enable_metrics_server,
80        interval,
81        join: true,
82        metrics_port,
83        node_count: count,
84        node_port,
85        peers: None,
86        rpc_port,
87        skip_validation,
88        log_format,
89        rewards_address,
90        evm_network,
91    };
92
93    // Ensure EVM testnet is running before starting the local network
94    ensure_evm_testnet_running(build, verbosity).await?;
95
96    run_network(options, local_node_registry, &ServiceController {}).await?;
97    Ok(())
98}
99
100pub async fn kill(keep_directories: bool, verbosity: VerbosityLevel) -> Result<()> {
101    let local_reg_path = &get_local_node_registry_path()?;
102    let local_node_registry = NodeRegistryManager::load(local_reg_path).await?;
103    if local_node_registry.nodes.read().await.is_empty() {
104        info!("No local network is currently running, cannot kill it");
105        println!("No local network is currently running");
106    } else {
107        if verbosity != VerbosityLevel::Minimal {
108            print_banner("Killing Local Network");
109        }
110        info!("Kill local network");
111        kill_network(local_node_registry, keep_directories).await?;
112        std::fs::remove_file(local_reg_path)?;
113    }
114
115    Ok(())
116}
117
118pub async fn run(
119    build: bool,
120    clean: bool,
121    count: u16,
122    enable_metrics_server: bool,
123    interval: u64,
124    metrics_port: Option<PortRange>,
125    node_path: Option<PathBuf>,
126    node_port: Option<PortRange>,
127    node_version: Option<String>,
128    log_format: Option<LogFormat>,
129    rpc_port: Option<PortRange>,
130    rewards_address: RewardsAddress,
131    evm_network: EvmNetwork,
132    skip_validation: bool,
133    verbosity: VerbosityLevel,
134) -> Result<(), Report> {
135    if (enable_metrics_server || metrics_port.is_some()) && !cfg!(feature = "open-metrics") && build
136    {
137        return Err(eyre!(
138            "Metrics server is not available. Please enable the open-metrics feature flag. Run the command with the --features open-metrics"
139        ));
140    }
141
142    // In the clean case, the node registry must be loaded *after* the existing network has
143    // been killed, which clears it out.
144    let local_node_reg_path = &get_local_node_registry_path()?;
145    let local_node_registry: NodeRegistryManager = if clean {
146        debug!(
147            "Clean set to true, removing client, node dir, local registry and killing the network."
148        );
149        let client_data_path = dirs_next::data_dir()
150            .ok_or_else(|| eyre!("Could not obtain user's data directory"))?
151            .join("autonomi")
152            .join("client");
153        if client_data_path.is_dir() {
154            std::fs::remove_dir_all(client_data_path)?;
155        }
156        if local_node_reg_path.exists() {
157            std::fs::remove_file(local_node_reg_path)?;
158        }
159        kill(false, verbosity).await?;
160        NodeRegistryManager::load(local_node_reg_path).await?
161    } else {
162        let local_node_registry = NodeRegistryManager::load(local_node_reg_path).await?;
163        if !local_node_registry.nodes.read().await.is_empty() {
164            error!("A local network is already running, cannot run a new one");
165            return Err(eyre!("A local network is already running")
166                .suggestion("Use the kill command to destroy the network then try again"));
167        }
168        local_node_registry
169    };
170
171    if verbosity != VerbosityLevel::Minimal {
172        print_banner("Launching Local Network");
173    }
174    info!("Launching local network");
175
176    let release_repo = <dyn AntReleaseRepoActions>::default_config();
177
178    let antnode_bin_path = get_bin_path(
179        build,
180        node_path,
181        ReleaseType::AntNode,
182        node_version,
183        &*release_repo,
184        verbosity,
185    )
186    .await?;
187
188    // Ensure EVM testnet is running before starting the local network
189    ensure_evm_testnet_running(build, verbosity).await?;
190
191    let options = LocalNetworkOptions {
192        antnode_bin_path,
193        enable_metrics_server,
194        join: false,
195        interval,
196        metrics_port,
197        node_port,
198        node_count: count,
199        peers: None,
200        rpc_port,
201        skip_validation,
202        log_format,
203        rewards_address,
204        evm_network,
205    };
206    run_network(options, local_node_registry.clone(), &ServiceController {}).await?;
207
208    local_node_registry.save().await?;
209    Ok(())
210}
211
212/// Get the path to the evm-testnet binary, building it if necessary
213fn get_evm_testnet_bin_path(build: bool, verbosity: VerbosityLevel) -> Result<PathBuf> {
214    if build {
215        // Build the evm-testnet binary from source
216        if verbosity != VerbosityLevel::Minimal {
217            print_banner("Building evm-testnet binary");
218        }
219
220        let mut cmd = Command::new("cargo");
221        cmd.args(["build", "--release", "--bin", "evm-testnet"])
222            .stdout(if verbosity == VerbosityLevel::Minimal {
223                Stdio::null()
224            } else {
225                Stdio::inherit()
226            })
227            .stderr(if verbosity == VerbosityLevel::Minimal {
228                Stdio::null()
229            } else {
230                Stdio::inherit()
231            });
232
233        let output = cmd.output()?;
234        if !output.status.success() {
235            return Err(eyre!("Failed to build evm-testnet binary"));
236        }
237
238        let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
239        Ok(PathBuf::from(target_dir)
240            .join("release")
241            .join("evm-testnet"))
242    } else {
243        // Try to find evm-testnet in PATH
244        match which::which("evm-testnet") {
245            Ok(path) => Ok(path),
246            Err(_) => {
247                // Fallback to building from source
248                if verbosity != VerbosityLevel::Minimal {
249                    println!("evm-testnet not found in PATH, building from source...");
250                }
251
252                let mut cmd = Command::new("cargo");
253                cmd.args(["build", "--release", "--bin", "evm-testnet"])
254                    .stdout(if verbosity == VerbosityLevel::Minimal {
255                        Stdio::null()
256                    } else {
257                        Stdio::inherit()
258                    })
259                    .stderr(if verbosity == VerbosityLevel::Minimal {
260                        Stdio::null()
261                    } else {
262                        Stdio::inherit()
263                    });
264
265                let output = cmd.output()?;
266                if !output.status.success() {
267                    return Err(eyre!("Failed to build evm-testnet binary"));
268                }
269
270                let target_dir =
271                    std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
272                Ok(PathBuf::from(target_dir)
273                    .join("release")
274                    .join("evm-testnet"))
275            }
276        }
277    }
278}
279
280/// Spawn the evm-testnet binary as a child process
281async fn spawn_evm_testnet(build: bool, verbosity: VerbosityLevel) -> Result<()> {
282    let evm_testnet_path = get_evm_testnet_bin_path(build, verbosity)?;
283
284    if verbosity != VerbosityLevel::Minimal {
285        print_banner("Starting EVM testnet");
286    }
287
288    let mut cmd = Command::new(&evm_testnet_path);
289    cmd.stdout(if verbosity == VerbosityLevel::Minimal {
290        Stdio::null()
291    } else {
292        Stdio::inherit()
293    })
294    .stderr(if verbosity == VerbosityLevel::Minimal {
295        Stdio::null()
296    } else {
297        Stdio::inherit()
298    });
299
300    let _ = cmd.spawn()?;
301
302    // Wait a moment for the testnet to start up
303    sleep(Duration::from_millis(2000)).await;
304
305    Ok(())
306}
307
308/// Check if EVM testnet is already running by checking the process list
309fn check_evm_testnet_running() -> bool {
310    let mut system = System::new_all();
311    system.refresh_all();
312
313    // Look for evm-testnet or anvil processes
314    for process in system.processes().values() {
315        let process_name = process.name().to_lowercase();
316        if process_name.contains("evm-testnet") || process_name.contains("anvil") {
317            return true;
318        }
319    }
320    false
321}
322
323/// Ensure an EVM testnet is running, starting one if necessary
324async fn ensure_evm_testnet_running(build: bool, verbosity: VerbosityLevel) -> Result<()> {
325    if check_evm_testnet_running() {
326        if verbosity != VerbosityLevel::Minimal {
327            println!("EVM testnet is already running");
328        }
329        return Ok(());
330    }
331
332    spawn_evm_testnet(build, verbosity).await?;
333
334    // Wait for the testnet to be fully ready
335    let mut attempts = 0;
336    while !check_evm_testnet_running() && attempts < 30 {
337        sleep(Duration::from_millis(1000)).await;
338        attempts += 1;
339    }
340
341    if !check_evm_testnet_running() {
342        return Err(eyre!(
343            "Failed to start EVM testnet - not responding after 30 seconds"
344        ));
345    }
346
347    if verbosity != VerbosityLevel::Minimal {
348        println!("EVM testnet started successfully");
349    }
350
351    Ok(())
352}
353
354pub async fn status(details: bool, fail: bool, json: bool) -> Result<()> {
355    let local_node_registry = NodeRegistryManager::load(&get_local_node_registry_path()?).await?;
356    if !json {
357        print_banner("Local Network");
358    }
359    status_report(
360        &local_node_registry,
361        &ServiceController {},
362        details,
363        json,
364        fail,
365        true,
366    )
367    .await?;
368    local_node_registry.save().await?;
369    Ok(())
370}