linera_base/
command.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Command functionality used for spanning child processes.
5
6use std::{
7    path::{Path, PathBuf},
8    process::Stdio,
9};
10
11use anyhow::{bail, ensure, Context, Result};
12use async_trait::async_trait;
13use tokio::process::Command;
14use tracing::{debug, error};
15
16/// Attempts to resolve the path and test the version of the given binary against our
17/// package version.
18///
19/// This is meant for binaries of the Linera repository. We use the current running binary
20/// to locate the parent directory where to look for the given name.
21pub async fn resolve_binary(name: &'static str, package: &'static str) -> Result<PathBuf> {
22    let current_binary = std::env::current_exe()?;
23    resolve_binary_in_same_directory_as(&current_binary, name, package).await
24}
25
26/// Obtains the current binary parent
27pub fn current_binary_parent() -> Result<PathBuf> {
28    let current_binary = std::env::current_exe()?;
29    binary_parent(&current_binary)
30}
31
32/// Retrieves the path from the binary parent.
33pub fn binary_parent(current_binary: &Path) -> Result<PathBuf> {
34    let mut current_binary_parent = current_binary
35        .canonicalize()
36        .with_context(|| format!("Failed to canonicalize '{}'", current_binary.display()))?;
37    current_binary_parent.pop();
38
39    #[cfg(with_testing)]
40    // Test binaries are typically in target/debug/deps while crate binaries are in target/debug
41    // (same thing for target/release).
42    let current_binary_parent = if current_binary_parent.ends_with("target/debug/deps")
43        || current_binary_parent.ends_with("target/release/deps")
44    {
45        PathBuf::from(current_binary_parent.parent().unwrap())
46    } else {
47        current_binary_parent
48    };
49
50    Ok(current_binary_parent)
51}
52
53/// Same as [`resolve_binary`] but gives the option to specify a binary path to use as
54/// reference. The path may be relative or absolute but it must point to a valid file on
55/// disk.
56pub async fn resolve_binary_in_same_directory_as<P: AsRef<Path>>(
57    current_binary: P,
58    name: &'static str,
59    package: &'static str,
60) -> Result<PathBuf> {
61    let current_binary = current_binary.as_ref();
62    debug!(
63        "Resolving binary {name} based on the current binary path: {}",
64        current_binary.display()
65    );
66
67    let current_binary_parent =
68        binary_parent(current_binary).expect("Fetching binary directory should not fail");
69
70    let binary = current_binary_parent.join(name);
71    let version = format!("v{}", env!("CARGO_PKG_VERSION"));
72    if !binary.exists() {
73        error!(
74            "Cannot find a binary {name} in the directory {}. \
75             Consider using `cargo install {package}` or `cargo build -p {package}`",
76            current_binary_parent.display()
77        );
78        bail!("Failed to resolve binary {name}");
79    }
80
81    // Quick version check.
82    debug!("Checking the version of {}", binary.display());
83    let version_message = Command::new(&binary)
84        .arg("--version")
85        .output()
86        .await
87        .with_context(|| {
88            format!(
89                "Failed to execute and retrieve version from the binary {name} in directory {}",
90                current_binary_parent.display()
91            )
92        })?
93        .stdout;
94    let version_message = String::from_utf8_lossy(&version_message);
95    let found_version = parse_version_message(&version_message);
96    if version != found_version {
97        error!("The binary {name} in directory {} should have version {version} (found {found_version}). \
98                Consider using `cargo install {package} --version '{version}'` or `cargo build -p {package}`",
99               current_binary_parent.display()
100        );
101        bail!("Incorrect version for binary {name}");
102    }
103    debug!("{} has version {version}", binary.display());
104
105    Ok(binary)
106}
107
108/// Obtains the version from the message.
109pub fn parse_version_message(message: &str) -> String {
110    let mut lines = message.lines();
111    lines.next();
112    lines
113        .next()
114        .unwrap_or_default()
115        .trim()
116        .split(' ')
117        .last()
118        .expect("splitting strings gives non-empty lists")
119        .to_string()
120}
121
122/// Extension trait for [`tokio::process::Command`].
123#[async_trait]
124pub trait CommandExt: std::fmt::Debug {
125    /// Similar to [`tokio::process::Command::spawn`] but sets `kill_on_drop` to `true`.
126    /// Errors are tagged with a description of the command.
127    fn spawn_into(&mut self) -> anyhow::Result<tokio::process::Child>;
128
129    /// Similar to [`tokio::process::Command::output`] but does not capture `stderr` and
130    /// returns the `stdout` as a string. Errors are tagged with a description of the
131    /// command.
132    async fn spawn_and_wait_for_stdout(&mut self) -> anyhow::Result<String>;
133
134    /// Spawns and waits for process to finish executing.
135    /// Will not wait for stdout, use `spawn_and_wait_for_stdout` for that
136    async fn spawn_and_wait(&mut self) -> anyhow::Result<()>;
137
138    /// Description used for error reporting.
139    fn description(&self) -> String {
140        format!("While executing {:?}", self)
141    }
142}
143
144#[async_trait]
145impl CommandExt for tokio::process::Command {
146    fn spawn_into(&mut self) -> anyhow::Result<tokio::process::Child> {
147        self.kill_on_drop(true);
148        debug!("Spawning {:?}", self);
149        let child = tokio::process::Command::spawn(self).with_context(|| self.description())?;
150        Ok(child)
151    }
152
153    async fn spawn_and_wait_for_stdout(&mut self) -> anyhow::Result<String> {
154        debug!("Spawning and waiting for {:?}", self);
155        self.stdout(Stdio::piped());
156        self.stderr(Stdio::inherit());
157        self.kill_on_drop(true);
158
159        let child = self.spawn().with_context(|| self.description())?;
160        let output = child
161            .wait_with_output()
162            .await
163            .with_context(|| self.description())?;
164        ensure!(
165            output.status.success(),
166            "{}: got non-zero error code {}",
167            self.description(),
168            output.status
169        );
170        String::from_utf8(output.stdout).with_context(|| self.description())
171    }
172
173    async fn spawn_and_wait(&mut self) -> anyhow::Result<()> {
174        debug!("Spawning and waiting for {:?}", self);
175        self.kill_on_drop(true);
176
177        let mut child = self.spawn().with_context(|| self.description())?;
178        let status = child.wait().await.with_context(|| self.description())?;
179        ensure!(
180            status.success(),
181            "{}: got non-zero error code {}",
182            self.description(),
183            status
184        );
185
186        Ok(())
187    }
188}