/*! An executable or revertable step, possibly orcestrating sub-[`Action`]s using things like [`JoinSet`](tokio::task::JoinSet)s [`Action`]s should be considered an 'atom' of change. Typically they are either a 'base' or a 'composite' [`Action`]. Base actions are things like: * [`CreateDirectory`](base::CreateDirectory) * [`CreateFile`](base::CreateFile) * [`CreateUser`](base::CreateUser) Composite actions are things like: * [`CreateNixTree`](common::CreateNixTree) * [`CreateUsersAndGroups`](common::CreateUsersAndGroups) During their `plan` phase, [`Planner`](crate::planner::Planner)s call an [`Action`]s `plan` function, which may accept any arguments. For example, several 'composite' actions accept a [`CommonSettings`](crate::settings::CommonSettings). Later, the [`InstallPlan`](crate::InstallPlan) will call [`try_execute`](StatefulAction::try_execute) on the [`StatefulAction`]. You can manually plan, execute, then revert an [`Action`] like so: ```rust,no_run # async fn wrapper() { use nix_installer::action::base::CreateDirectory; let mut action = CreateDirectory::plan("/nix", None, None, 0o0755, true).await.unwrap(); action.try_execute().await.unwrap(); action.try_revert().await.unwrap(); # } ``` A general guidance for what determines how fine-grained an [`Action`] should be is the unit of reversion. The [`ConfigureNixDaemonService`](common::ConfigureNixDaemonService) action is a good example of this, it takes several steps, such as running `systemd-tmpfiles`, and calling `systemctl link` on some systemd units. Where possible, tasks which could break during execution should be broken up, as uninstalling/installing step detection is determined by the wrapping [`StatefulAction`]. If an [`Action`] is a 'composite' its sub-[`Action`]s can be reverted piece-by-piece. So breaking up actions into faillable units is ideal. A custom [`Action`] can be created then used in a custom [`Planner`](crate::planner::Planner): ```rust,no_run use std::{error::Error, collections::HashMap}; use tracing::{Span, span}; use nix_installer::{ InstallPlan, settings::{CommonSettings, InstallSettingsError}, planner::{Planner, PlannerError, linux::SteamDeck}, action::{Action, ActionError, StatefulAction, ActionDescription}, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct MyAction {} impl MyAction { #[tracing::instrument(level = "debug", skip_all)] pub async fn plan() -> Result, ActionError> { Ok(Self {}.into()) } } #[async_trait::async_trait] #[typetag::serde(name = "my_action")] impl Action for MyAction { fn tracing_synopsis(&self) -> String { "My action".to_string() } fn tracing_span(&self) -> Span { span!( tracing::Level::DEBUG, "my_action", // Tracing fields here ... ) } fn execute_description(&self) -> Vec { vec![ActionDescription::new(self.tracing_synopsis(), vec![])] } #[tracing::instrument(level = "debug", skip_all)] async fn execute(&mut self) -> Result<(), ActionError> { // Execute steps ... Ok(()) } fn revert_description(&self) -> Vec { vec![ActionDescription::new(self.tracing_synopsis(), vec![])] } #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { // Revert steps... Ok(()) } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MyPlanner { pub common: CommonSettings, } #[async_trait::async_trait] #[typetag::serde(name = "my-planner")] impl Planner for MyPlanner { async fn default() -> Result { Ok(Self { common: CommonSettings::default()?, }) } async fn plan(&self) -> Result>>, PlannerError> { Ok(vec![ // ... MyAction::plan() .await .map_err(PlannerError::Action)?.boxed(), ]) } fn settings(&self) -> Result, InstallSettingsError> { let Self { common } = self; let mut map = std::collections::HashMap::default(); map.extend(common.settings()?.into_iter()); Ok(map) } } # async fn custom_planner_install() -> color_eyre::Result<()> { let planner = MyPlanner::default().await?; let mut plan = InstallPlan::plan(planner).await?; match plan.install(None).await { Ok(()) => tracing::info!("Done"), Err(e) => { match e.source() { Some(source) => tracing::error!("{e}: {}", source), None => tracing::error!("{e}"), }; plan.uninstall(None).await?; }, }; # Ok(()) # } ``` */ pub mod base; pub mod common; pub mod darwin; pub mod linux; mod stateful; pub use stateful::{ActionState, StatefulAction}; use std::error::Error; use tokio::task::JoinError; use tracing::Span; use crate::error::HasExpectedErrors; /// An action which can be reverted or completed, with an action state /// /// This trait interacts with [`StatefulAction`] which does the [`ActionState`] manipulation and provides some tracing facilities. /// /// Instead of calling [`execute`][Action::execute] or [`revert`][Action::revert], you should prefer [`try_execute`][StatefulAction::try_execute] and [`try_revert`][StatefulAction::try_revert] #[async_trait::async_trait] #[typetag::serde(tag = "action")] pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone { /// A synopsis of the action for tracing purposes fn tracing_synopsis(&self) -> String; /// A tracing span suitable for the action /// /// It should be [`tracing::Level::DEBUG`] and contain the same name as the [`typetag::serde`] entry. /// /// It may contain any fields, and will be attached in the [`StatefulAction::try_execute`] and [`StatefulAction::try_revert`] functions. /// /// See [`tracing::Span`] for more details. fn tracing_span(&self) -> Span; /// A description of what this action would do during execution /// /// If this action calls sub-[`Action`]s, care should be taken to use [`StatefulAction::describe_execute`] on those actions, not [`execute_description`][Action::execute_description]. /// /// This is called by [`InstallPlan::describe_install`](crate::InstallPlan::describe_install) through [`StatefulAction::describe_execute`] which will skip output if the action is completed. fn execute_description(&self) -> Vec; /// A description of what this action would do during revert /// /// If this action calls sub-[`Action`]s, care should be taken to use [`StatefulAction::describe_revert`] on those actions, not [`revert_description`][Action::revert_description]. /// /// This is called by [`InstallPlan::describe_uninstall`](crate::InstallPlan::describe_uninstall) through [`StatefulAction::describe_revert`] which will skip output if the action is completed. fn revert_description(&self) -> Vec; /// Perform any execution steps /// /// If this action calls sub-[`Action`]s, care should be taken to call [`try_execute`][StatefulAction::try_execute], not [`execute`][Action::execute], so that [`ActionState`] is handled correctly and tracing is done. /// /// This is called by [`InstallPlan::install`](crate::InstallPlan::install) through [`StatefulAction::try_execute`] which handles tracing as well as if the action needs to execute based on its `action_state`. async fn execute(&mut self) -> Result<(), ActionError>; /// Perform any revert steps /// /// If this action calls sub-[`Action`]s, care should be taken to call [`try_revert`][StatefulAction::try_revert], not [`revert`][Action::revert], so that [`ActionState`] is handled correctly and tracing is done. /// /// /// This is called by [`InstallPlan::uninstall`](crate::InstallPlan::uninstall) through [`StatefulAction::try_revert`] which handles tracing as well as if the action needs to revert based on its `action_state`. async fn revert(&mut self) -> Result<(), ActionError>; fn stateful(self) -> StatefulAction where Self: Sized, { StatefulAction { action: self, state: ActionState::Uncompleted, } } // They should also have an `async fn plan(args...) -> Result, ActionError>;` } dyn_clone::clone_trait_object!(Action); /** A description of an [`Action`](crate::action::Action), intended for humans to review */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct ActionDescription { pub description: String, pub explanation: Vec, } impl ActionDescription { pub fn new(description: String, explanation: Vec) -> Self { Self { description, explanation, } } } /// An error occurring during an action #[derive(thiserror::Error, Debug)] pub enum ActionError { /// A custom error #[error(transparent)] Custom(Box), /// A child error #[error(transparent)] Child(#[from] Box), /// Several child errors #[error("Multiple errors: {}", .0.iter().map(|v| { if let Some(source) = v.source() { format!("{v} ({source})") } else { format!("{v}") } }).collect::>().join(" & "))] Children(Vec>), /// The path already exists #[error("Path exists `{0}`")] Exists(std::path::PathBuf), #[error("Getting metadata for {0}`")] GettingMetadata(std::path::PathBuf, #[source] std::io::Error), #[error("Creating directory `{0}`")] CreateDirectory(std::path::PathBuf, #[source] std::io::Error), #[error("Symlinking from `{0}` to `{1}`")] Symlink( std::path::PathBuf, std::path::PathBuf, #[source] std::io::Error, ), #[error("Set mode `{0}` on `{1}`")] SetPermissions(u32, std::path::PathBuf, #[source] std::io::Error), #[error("Remove file `{0}`")] Remove(std::path::PathBuf, #[source] std::io::Error), #[error("Copying file `{0}` to `{1}`")] Copy( std::path::PathBuf, std::path::PathBuf, #[source] std::io::Error, ), #[error("Rename `{0}` to `{1}`")] Rename( std::path::PathBuf, std::path::PathBuf, #[source] std::io::Error, ), #[error("Read path `{0}`")] Read(std::path::PathBuf, #[source] std::io::Error), #[error("Open path `{0}`")] Open(std::path::PathBuf, #[source] std::io::Error), #[error("Write path `{0}`")] Write(std::path::PathBuf, #[source] std::io::Error), #[error("Seek path `{0}`")] Seek(std::path::PathBuf, #[source] std::io::Error), #[error("Getting uid for user `{0}`")] UserId(String, #[source] nix::errno::Errno), #[error("Getting user `{0}`")] NoUser(String), #[error("Getting gid for group `{0}`")] GroupId(String, #[source] nix::errno::Errno), #[error("Getting group `{0}`")] NoGroup(String), #[error("Chowning path `{0}`")] Chown(std::path::PathBuf, #[source] nix::errno::Errno), /// Failed to execute command #[error("Failed to execute command")] Command(#[source] std::io::Error), #[error("Joining spawned async task")] Join( #[source] #[from] JoinError, ), #[error("String from UTF-8 error")] FromUtf8( #[source] #[from] std::string::FromUtf8Error, ), } impl HasExpectedErrors for ActionError { fn expected(&self) -> Option> { None } }