Skip to main content

cnf_lib/environment/
mod.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
3// This file is part of cnf-lib, available at <https://gitlab.com/hartang/rust/cnf>
4
5//! # Environment handlers
6//!
7//! This module contains handlers for various [environments](Environment). Generally a handler is
8//! characterized by implementing the [`IsEnvironment`] trait and integrating with the [`current`]
9//! function.
10//!
11//! Especially [`IsEnvironment::execute`] function isn't usually trivial to implement, since the
12//! precise commands to run (and wrap) depend on the *current* and *target* execution environment.
13pub mod container;
14pub mod distrobox;
15pub mod host;
16pub mod toolbx;
17#[cfg(test)]
18use crate::test::prelude::*;
19use futures::TryFutureExt;
20use prelude::*;
21use tracing::Instrument;
22
23#[allow(unused_imports)]
24pub(crate) mod prelude {
25    pub(crate) use crate::{
26        environment::{self, Environment},
27        util::{CommandLine, OutputMatcher, cmd},
28    };
29    pub(crate) use async_trait::async_trait;
30    pub(crate) use displaydoc::Display;
31    pub(crate) use serde_derive::{Deserialize, Serialize};
32    pub(crate) use std::fmt;
33    pub(crate) use thiserror::Error as ThisError;
34    pub(crate) use tokio::process::Command;
35    pub(crate) use tracing::{debug, error, info, span, trace, warn};
36}
37
38/// Trait for usable environments.
39#[async_trait]
40pub trait IsEnvironment: fmt::Debug + fmt::Display {
41    /// Error produced by this environment impl.
42    type Err;
43
44    /// Returns true if the given Env is available at all.
45    ///
46    /// We assume the environment `Host` to be available unconditionally. Other environments, such
47    /// as `toolbx`, can only be available when at least the `toolbx` executable is present, or we
48    /// are currently inside a toolbx.
49    async fn exists(&self) -> bool;
50
51    /// Execute a command within this environment
52    ///
53    /// [`IsProvider`](crate::provider::IsProvider) implementations should prefer calling
54    /// [`output_of()`] instead of interacting with an [`Environment`] instance directly.
55    /// Refer to [`output_of()`] for details.
56    ///
57    /// [`output_of()`]: Environment::output_of()
58    async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err>;
59}
60
61/// All the execution environments known to the applicaiton.
62#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Serialize, Deserialize)]
63pub enum Environment {
64    //Container,
65    /// The host system
66    Host(host::Host),
67    /// A distrobox instance
68    Distrobox(distrobox::Distrobox),
69    /// A toolbx instance
70    Toolbx(toolbx::Toolbx),
71    /// Mock environment (test only)
72    #[cfg(test)]
73    Mock(Mock),
74}
75
76impl Environment {
77    /// Execute a command in this environment and collect stdout, if possible.
78    ///
79    /// Executes `cmd` inside this env, collecting all output. If execution finishes successfully,
80    /// the stdout is returned as [`String`]. Otherwise, an [`ExecutionError`] is returned instead.
81    ///
82    /// The [`ExecutionError`] type takes care of a lot of boilerplate code that is otherwise necessary
83    /// to detect specific error conditions caused by *different* environments (because they can
84    /// produce different output for similar/identical error conditions).
85    ///
86    /// Refer to [`OutputMatcher`] for added convenience when trying to recover from a
87    /// [`NonZero`](ExecutionError::NonZero) error.
88    ///
89    ///
90    /// ### Test integration
91    ///
92    /// In conjunction with the [`Mock`](crate::test::mock::Mock) type, this function allows
93    /// replaying the output of called commands. Use the [`quick_test!`](crate::test::quick_test)
94    /// macro to simulate command outputs inside tests.
95    #[cfg(not(test))]
96    pub async fn output_of(&self, cmd: CommandLine) -> Result<String, ExecutionError> {
97        let main_command = cmd.command();
98        let output = self
99            .execute(cmd)
100            .await
101            .map_err(ExecutionError::Environment)?
102            // NOTE: This bit is very important. If ommitted, **all** `Command` instances spawned
103            // by the application ever will block on each other waiting to read things from stdin.
104            .stdin(std::process::Stdio::null())
105            .output()
106            .await
107            .map_err(|err| match err.kind() {
108                std::io::ErrorKind::NotFound => ExecutionError::NotFound(main_command.clone()),
109                _ => ExecutionError::Unknown(err),
110            })?;
111
112        if output.status.success() {
113            Ok(String::from_utf8_lossy(&output.stdout).to_string())
114        } else {
115            let matcher = OutputMatcher::new(&output);
116            match self {
117                Self::Toolbx(t) if matcher.ends_with(&format!("Error: command {main_command} not found in container {}", t.name())) => Err(ExecutionError::NotFound(main_command)),
118                Self::Host(h) if (matcher.starts_with(
119                    "Portal call failed: Failed to start command: Failed to execute child process"
120                ) && matcher.ends_with("(No such file or directory)")) => Err(ExecutionError::NotFound(main_command)),
121                _ => Err(ExecutionError::NonZero { command: main_command, output }),
122            }
123        }
124    }
125
126    /// Instrumented test version of `output_of`.
127    #[cfg(test)]
128    pub async fn output_of(&self, _command: CommandLine) -> Result<String, ExecutionError> {
129        match self {
130            Environment::Mock(mock) => mock.pop_raw(),
131            _ => panic!("cannot execute commands in tests with regular envs"),
132        }
133    }
134
135    /// Serialize an environment to a JSON string.
136    pub fn to_json(&self) -> String {
137        serde_json::to_string(&self)
138            .unwrap_or_else(|_| panic!("failed to serialize env '{}' to JSON", self))
139    }
140
141    /// Start an environment manually.
142    ///
143    /// This is usually done automatically when creating environment instances through their
144    /// constructors. For some environments, this is a no-op.
145    pub fn start(&self) -> Result<(), anyhow::Error> {
146        match self {
147            Self::Distrobox(val) => Ok(val.start()?),
148            Self::Toolbx(val) => Ok(val.start()?),
149            _ => Ok(()),
150        }
151    }
152}
153
154impl fmt::Display for Environment {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        match self {
157            Self::Host(val) => write!(f, "{}", val),
158            Self::Distrobox(val) => write!(f, "{}", val),
159            Self::Toolbx(val) => write!(f, "{}", val),
160            #[cfg(test)]
161            Self::Mock(val) => write!(f, "{}", val),
162        }
163    }
164}
165
166#[async_trait]
167impl IsEnvironment for Environment {
168    type Err = Error;
169
170    async fn exists(&self) -> bool {
171        match self {
172            Self::Host(val) => val.exists(),
173            Self::Distrobox(val) => val.exists(),
174            Self::Toolbx(val) => val.exists(),
175            #[cfg(test)]
176            Self::Mock(val) => val.exists(),
177        }
178        .await
179    }
180
181    async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err> {
182        async move {
183            match self {
184                Self::Host(val) => val.execute(command).map_err(Error::ExecuteOnHost).await,
185                Self::Distrobox(val) => {
186                    val.execute(command)
187                        .map_err(|e| Self::Err::ExecuteInDistrobox {
188                            distrobox: val.to_string(),
189                            source: e,
190                        })
191                        .await
192                }
193                Self::Toolbx(val) => {
194                    val.execute(command)
195                        .map_err(|e| Self::Err::ExecuteInToolbx {
196                            toolbx: val.to_string(),
197                            source: e,
198                        })
199                        .await
200                }
201                #[cfg(test)]
202                Self::Mock(val) => Ok(val.execute(command).await.unwrap()),
203            }
204        }
205        .in_current_span()
206        .await
207    }
208}
209
210impl From<host::Host> for Environment {
211    fn from(value: host::Host) -> Self {
212        Self::Host(value)
213    }
214}
215
216impl From<distrobox::Distrobox> for Environment {
217    fn from(value: distrobox::Distrobox) -> Self {
218        Self::Distrobox(value)
219    }
220}
221
222impl From<toolbx::Toolbx> for Environment {
223    fn from(value: toolbx::Toolbx) -> Self {
224        Self::Toolbx(value)
225    }
226}
227
228impl std::str::FromStr for Environment {
229    type Err = SerializationError;
230
231    fn from_str(s: &str) -> Result<Self, Self::Err> {
232        let val: Self =
233            serde_json::from_str(s).map_err(|_| SerializationError { raw: s.to_owned() })?;
234        Ok(val)
235    }
236}
237
238/// invalid environment specification '{raw}'
239#[derive(Debug, ThisError, Serialize, Deserialize, Display)]
240pub struct SerializationError {
241    raw: String,
242}
243
244/// Return the current execution environment.
245pub fn current() -> Environment {
246    if toolbx::detect() {
247        Environment::Toolbx(toolbx::Toolbx::current().unwrap())
248    } else if distrobox::detect() {
249        Environment::Distrobox(distrobox::Distrobox::current().unwrap())
250    } else {
251        Environment::Host(host::Host::new())
252    }
253}
254
255/// Preserve the users environments.
256///
257/// Attempts to replicate all environment variables of the current process in the spawned
258/// environment. Refer to the source code to see exactly which variables are preserved. Variables
259/// are returned as a vector of strings with 'KEY=VALUE' notation.
260pub fn read_env_vars() -> Vec<String> {
261    let exclude = [
262        "HOST",
263        "HOSTNAME",
264        "HOME",
265        "LANG",
266        "LC_CTYPE",
267        "PATH",
268        "PROFILEREAD",
269        "SHELL",
270    ];
271
272    std::env::vars()
273        .filter_map(|(mut key, value)| {
274            if exclude.contains(&&key[..])
275                || key.starts_with('_')
276                || (key.starts_with("XDG_") && key.ends_with("_DIRS"))
277            {
278                None
279            } else {
280                key.push('=');
281                key.push_str(&value);
282                Some(key)
283            }
284        })
285        .collect::<Vec<_>>()
286}
287
288/// Common errors from command execution.
289#[derive(Debug, ThisError)]
290pub enum ExecutionError {
291    /// Requested executable cannot be found
292    #[error("command not found: {0}")]
293    NotFound(String),
294
295    /// Error inside an [`Environment`]
296    #[error(transparent)]
297    Environment(#[from] Error),
298
299    /// Error from [`Command`]
300    #[error(transparent)]
301    Unknown(#[from] std::io::Error),
302
303    /// Error from the called command
304    #[error("command '{command}' exited with nonzero code: {code:?}", code = output.status)]
305    NonZero {
306        /// The command that produced the error
307        command: String,
308        /// The output associated with the error.
309        output: std::process::Output,
310    },
311}
312
313/// Error from starting up an environment.
314#[derive(Debug, ThisError)]
315pub enum StartError {
316    /// Underlying Distrobox error.
317    #[error(transparent)]
318    Distrobox(#[from] distrobox::StartDistroboxError),
319
320    /// Underlying Toolbx error.
321    #[error(transparent)]
322    Toolbx(#[from] toolbx::StartToolbxError),
323}
324
325/// Environment execution error.
326#[derive(Debug, ThisError, Display)]
327pub enum Error {
328    /// failed to execute command on host
329    ExecuteOnHost(#[from] <host::Host as IsEnvironment>::Err),
330
331    /// failed to execute command in '{toolbx}': {source}
332    ExecuteInToolbx {
333        /// Toolbx where the error originates.
334        toolbx: String,
335        /// Underlying error source.
336        source: <toolbx::Toolbx as IsEnvironment>::Err,
337    },
338
339    /// failed to execute command in '{distrobox}': {source}
340    ExecuteInDistrobox {
341        /// Distrobox where the error originates.
342        distrobox: String,
343        /// Underlying error source.
344        source: <distrobox::Distrobox as IsEnvironment>::Err,
345    },
346}