2022-11-28 22:57:35 +00:00
/*! 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 ( ) -> Result < ( ) , harmonic ::HarmonicError > {
use harmonic ::action ::base ::CreateDirectory ;
let mut action = CreateDirectory ::plan ( " /nix " , None , None , 0o0755 , true ) . await ? ;
action . try_execute ( ) . await ? ;
action . try_revert ( ) . await ? ;
# Ok ( ( ) )
# }
` ` `
A general guidance for what determines how fine - grained an [ ` Action ` ] should be is the unit of
reversion . The [ ` ConfigureNixDaemonService ` ] ( linux ::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 harmonic ::{
InstallPlan ,
settings ::{ CommonSettings , InstallSettingsError } ,
2022-12-02 15:31:15 +00:00
planner ::{ Planner , PlannerError , linux ::SteamDeck } ,
2022-12-05 16:55:30 +00:00
action ::{ Action , ActionError , StatefulAction , ActionDescription } ,
2022-11-28 22:57:35 +00:00
} ;
#[ derive(Debug, serde::Deserialize, serde::Serialize, Clone) ]
pub struct MyAction { }
impl MyAction {
#[ tracing::instrument(skip_all) ]
2022-12-05 16:55:30 +00:00
pub async fn plan ( ) -> Result < StatefulAction < Self > , ActionError > {
2022-11-28 22:57:35 +00:00
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 execute_description ( & self ) -> Vec < ActionDescription > {
vec! [ ActionDescription ::new ( self . tracing_synopsis ( ) , vec! [ ] ) ]
}
#[ tracing::instrument(skip_all, fields(
// Tracing fields...
) ) ]
2022-12-05 16:55:30 +00:00
async fn execute ( & mut self ) -> Result < ( ) , ActionError > {
2022-11-28 22:57:35 +00:00
// Execute steps ...
Ok ( ( ) )
}
fn revert_description ( & self ) -> Vec < ActionDescription > {
vec! [ ActionDescription ::new ( self . tracing_synopsis ( ) , vec! [ ] ) ]
}
#[ tracing::instrument(skip_all, fields(
// Tracing fields...
) ) ]
2022-12-05 16:55:30 +00:00
async fn revert ( & mut self ) -> Result < ( ) , ActionError > {
2022-11-28 22:57:35 +00:00
// 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 < Self , PlannerError > {
Ok ( Self {
common : CommonSettings ::default ( ) ? ,
} )
}
async fn plan ( & self ) -> Result < Vec < StatefulAction < Box < dyn Action > > > , PlannerError > {
Ok ( vec! [
// ...
MyAction ::plan ( )
. await
. map_err ( PlannerError ::Action ) ? . boxed ( ) ,
] )
}
fn settings ( & self ) -> Result < HashMap < String , serde_json ::Value > , 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 ( ( ) )
# }
` ` `
* /
2022-11-10 14:54:12 +00:00
pub mod base ;
2022-10-28 16:29:15 +00:00
pub mod common ;
pub mod darwin ;
pub mod linux ;
2022-11-28 22:57:35 +00:00
mod stateful ;
2022-09-15 19:11:46 +00:00
2022-11-28 22:57:35 +00:00
pub use stateful ::{ ActionState , StatefulAction } ;
2022-12-05 16:55:30 +00:00
use std ::error ::Error ;
use tokio ::task ::JoinError ;
2022-09-20 18:42:20 +00:00
2022-11-23 17:18:38 +00:00
/// An action which can be reverted or completed, with an action state
///
2022-11-28 22:57:35 +00:00
/// This trait interacts with [`StatefulAction`] which does the [`ActionState`] manipulation and provides some tracing facilities.
2022-11-23 17:18:38 +00:00
///
2022-11-28 22:57:35 +00:00
/// Instead of calling [`execute`][Action::execute] or [`revert`][Action::revert], you should prefer [`try_execute`][StatefulAction::try_execute] and [`try_revert`][StatefulAction::try_revert]
2022-10-26 21:14:53 +00:00
#[ async_trait::async_trait ]
#[ typetag::serde(tag = " action " ) ]
2022-10-26 22:13:42 +00:00
pub trait Action : Send + Sync + std ::fmt ::Debug + dyn_clone ::DynClone {
2022-11-28 22:57:35 +00:00
/// A synopsis of the action for tracing purposes
2022-11-23 17:18:38 +00:00
fn tracing_synopsis ( & self ) -> String ;
2022-11-28 22:57:35 +00:00
/// 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.
2022-11-23 17:18:38 +00:00
fn execute_description ( & self ) -> Vec < ActionDescription > ;
2022-11-28 22:57:35 +00:00
/// 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.
2022-11-23 17:18:38 +00:00
fn revert_description ( & self ) -> Vec < ActionDescription > ;
2022-11-28 22:57:35 +00:00
/// 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`.
2022-12-05 16:55:30 +00:00
async fn execute ( & mut self ) -> Result < ( ) , ActionError > ;
2022-11-28 22:57:35 +00:00
/// 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`.
2022-12-05 16:55:30 +00:00
async fn revert ( & mut self ) -> Result < ( ) , ActionError > ;
2022-09-15 19:11:46 +00:00
2022-11-28 22:57:35 +00:00
fn stateful ( self ) -> StatefulAction < Self >
where
Self : Sized ,
{
StatefulAction {
action : self ,
state : ActionState ::Uncompleted ,
2022-11-23 17:18:38 +00:00
}
}
2022-12-05 16:55:30 +00:00
// They should also have an `async fn plan(args...) -> Result<StatefulAction<Self>, ActionError>;`
2022-11-23 17:18:38 +00:00
}
2022-10-26 22:13:42 +00:00
dyn_clone ::clone_trait_object! ( Action ) ;
2022-10-26 21:14:53 +00:00
2022-11-28 22:57:35 +00:00
/**
A description of an [ ` Action ` ] ( crate ::action ::Action ) , intended for humans to review
* /
2022-09-15 17:29:22 +00:00
#[ derive(Debug, serde::Deserialize, serde::Serialize, Clone) ]
pub struct ActionDescription {
pub description : String ,
pub explanation : Vec < String > ,
}
impl ActionDescription {
2022-11-28 22:57:35 +00:00
pub fn new ( description : String , explanation : Vec < String > ) -> Self {
2022-09-15 19:11:46 +00:00
Self {
description ,
explanation ,
}
2022-09-15 17:29:22 +00:00
}
}
2022-12-05 16:55:30 +00:00
/// An error occurring during an action
#[ derive(thiserror::Error, Debug) ]
pub enum ActionError {
/// A custom error
#[ error(transparent) ]
Custom ( Box < dyn std ::error ::Error + Send + Sync > ) ,
/// A child error
#[ error(transparent) ]
Child ( #[ from ] Box < ActionError > ) ,
/// Several child errors
#[ error( " Multiple errors: {} " , .0.iter().map(|v| {
if let Some ( source ) = v . source ( ) {
format! ( " {v} ( {source} ) " )
} else {
format! ( " {v} " )
}
} ) . collect ::< Vec < _ > > ( ) . join ( " & " ) ) ]
Children ( Vec < Box < ActionError > > ) ,
/// 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( " Remove 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 ,
) ,
}