diff --git a/src/os/debian.rs b/src/os/debian.rs index aed8cfb..4eace3b 100644 --- a/src/os/debian.rs +++ b/src/os/debian.rs @@ -7,7 +7,7 @@ use crate::{ Kind, OperatingSystem, pkg::{self, Package, PackagerVariant, Packages}, }, - run::{Command, executor::read}, + run::{Command, Transaction, TransactionCommand, executor::read}, }; pub const DEBIAN: OperatingSystem = OperatingSystem { @@ -25,12 +25,20 @@ pub struct Apt { } impl Packages for Apt { - fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), pkg::Error> { - super::debian::Apt::haul("install", packages, config) + fn install( + &self, + packages: &[Package], + config: &Configuration, + ) -> Result { + super::debian::Apt::haul(&Operation::Install, packages, config) } - fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), pkg::Error> { - super::debian::Apt::haul("remove", packages, config) + fn uninstall( + &self, + packages: &[Package], + config: &Configuration, + ) -> Result { + super::debian::Apt::haul(&Operation::Uninstall, packages, config) } fn manual(&self) -> Result, pkg::Error> { @@ -145,20 +153,64 @@ impl Packages for Apt { impl Apt { fn haul( - subcommand: &str, + operation: &Operation, packages: &[Package], config: &Configuration, - ) -> Result<(), pkg::Error> { + ) -> Result { if packages.is_empty() { - println!("Package selection is empty: Nothing to {subcommand}"); - return Ok(()); + println!("Package selection is empty: Nothing to {operation}"); + return Ok(Transaction::default()); } - let args: Vec<&str> = iter::once(subcommand) + // TODO This works as it is stated and is interesting as part of the + // PoC, but doesn't really make sense to install something that wasn't + // installed in the first place as the "rollback" of a failed uninstall + let rollback_operation = match operation { + Operation::Install => Operation::Uninstall, + Operation::Uninstall => Operation::Install, + }; + + let run_args: Vec<&str> = iter::once(operation.into()) .chain(packages.iter().map(|p| p.into())) .collect(); - let command = Command::new("apt", &args).escalate(config)?; - Ok(crate::run::executor::spawn(&command)?) + let rollback_args: Vec<&str> = iter::once(rollback_operation.into()) + .chain(packages.iter().map(|p| p.into())) + .collect(); + + let run = Command::new("apt", &run_args).escalate(config)?; + let rollback = Command::new("apt", &rollback_args).escalate(config)?; + let transaction_command = TransactionCommand::new(run, rollback); + Ok(Transaction::single(&transaction_command)) + } +} + +enum Operation { + Install, + Uninstall, +} + +impl<'s> From for &'s str { + fn from(operation: Operation) -> &'s str { + match operation { + Operation::Install => "install", + Operation::Uninstall => "remove", + } + } +} + +impl<'s> From<&'s Operation> for &'s str { + fn from(operation: &Operation) -> &str { + match *operation { + Operation::Install => "install", + Operation::Uninstall => "remove", + } + } +} + +impl std::fmt::Display for Operation { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s: &str = self.into(); + write!(f, "{s}") } } diff --git a/src/os/pkg.rs b/src/os/pkg.rs index 3d6f6cb..76e95fc 100644 --- a/src/os/pkg.rs +++ b/src/os/pkg.rs @@ -5,11 +5,16 @@ use core::{ fmt::Debug, }; -use crate::{conf::Configuration, os::debian, run}; +use crate::{ + conf::Configuration, + os::debian, + run::{self, Transaction}, +}; pub trait Packages: Clone + Default + Debug + PartialEq + Eq { - fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), Error>; - fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), Error>; + fn install(&self, packages: &[Package], config: &Configuration) -> Result; + fn uninstall(&self, packages: &[Package], config: &Configuration) + -> Result; fn manual(&self) -> Result, Error>; fn automatic(&self) -> Result, Error>; fn variant(&self) -> &PackagerVariant; @@ -23,14 +28,18 @@ pub enum Packager { } impl Packages for Packager { - fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), Error> { + fn install(&self, packages: &[Package], config: &Configuration) -> Result { match self { Packager::Apt(p) => p.install(packages, config), Packager::Unknown => Error::unknown_packager(&format!("install {packages:?}")), } } - fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), Error> { + fn uninstall( + &self, + packages: &[Package], + config: &Configuration, + ) -> Result { match self { Packager::Apt(p) => p.uninstall(packages, config), Packager::Unknown => Error::unknown_packager(&format!("uninstall {packages:?}")), diff --git a/src/run.rs b/src/run.rs index 655ea8a..dcfe4ba 100644 --- a/src/run.rs +++ b/src/run.rs @@ -74,6 +74,7 @@ pub enum TaskKind { pub struct Command { pub base: String, pub args: Vec, + escalated: bool, } impl Command { @@ -104,6 +105,7 @@ impl Command { Ok(Command { base: config.su_command.command().clone().base, + escalated: true, args, }) } @@ -112,17 +114,69 @@ impl Command { Command { base: base.to_string(), args: args.iter().map(|e| e.to_string()).collect(), + escalated: false, + } + } + + pub const fn escalated(&self) -> bool { + self.escalated + } +} + +#[must_use] +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Transaction { + commands: Vec, +} + +impl Transaction { + pub fn single(command: &TransactionCommand) -> Transaction { + Transaction { + commands: vec![command.clone()], } } } -#[derive(Debug)] +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct TransactionCommand { + run: Command, + rollback: Command, + status: TransactionCommandStatus, + errors: Option>, +} + +impl TransactionCommand { + pub const fn new(run: Command, rollback: Command) -> TransactionCommand { + TransactionCommand { + run, + rollback, + status: TransactionCommandStatus::Pending, + errors: None, + } + } + + pub fn push_error(&mut self, error: &executor::Error) { + self.errors.get_or_insert_with(Vec::new).push(error.clone()); + } +} + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub enum TransactionCommandStatus { + #[default] + Pending, + Success, + PendingRollback, + Rolledback, + FailedRollback, +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Error { pub message: String, pub kind: ErrorKind, } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ErrorKind { BadSuCommandConfig, } diff --git a/src/run/executor.rs b/src/run/executor.rs index 7bbb2ed..4b0a165 100644 --- a/src/run/executor.rs +++ b/src/run/executor.rs @@ -1,22 +1,41 @@ use std::process; -use crate::{os::pkg::Package, run::Command}; +use crate::{ + log::elog, + os::pkg::Package, + run::{Command, Transaction, TransactionCommandStatus}, +}; pub mod meta; -pub fn print(message: &str) -> Result<(), Error> { - println!("{message}"); - Ok(()) -} - -pub fn print_packages(packages: Vec) -> Result<(), Error> { - for package in packages { - print(&format!("{package}"))?; +// TODO Should this be a method of Transaction instead? +pub fn commit(transaction: &mut Transaction) -> Result<(), Error> { + elog(&format!("Committing transaction: {transaction:#?}")); + for command in &mut transaction.commands { + if let Err(error) = spawn(&command.run) { + command.status = TransactionCommandStatus::PendingRollback; + command.push_error(&error); + if let Err(rollback_error) = spawn(&command.rollback) { + command.status = TransactionCommandStatus::FailedRollback; + command.push_error(&rollback_error); + elog(&format!("Failed rollback of command {:#?}", &command)); + return Err(rollback_error); + } else { + command.status = TransactionCommandStatus::Rolledback; + elog(&format!("Successfully rolled back command {:#?}", &command)); + return Err(error); + } + } else { + command.status = TransactionCommandStatus::Success; + elog(&format!("Successfully ran command {:#?}", &command)); + } } + + elog("Transaction committed"); Ok(()) } -pub fn spawn(command: &Command) -> Result<(), Error> { +pub(super) fn spawn(command: &Command) -> Result<(), Error> { if let Ok(mut child) = process::Command::new(&command.base) .args(&command.args) .spawn() @@ -44,6 +63,13 @@ pub fn spawn(command: &Command) -> Result<(), Error> { } pub fn read(command: &Command) -> Result { + if command.escalated() { + return Err(Error { + message: "Read function is strictly rootless".to_string(), + kind: ErrorKind::RootlessReadOnly, + }); + } + if let Ok(output) = process::Command::new(&command.base) .args(&command.args) .output() @@ -68,15 +94,29 @@ pub fn read(command: &Command) -> Result { } } -#[derive(Debug)] +pub fn print(message: &str) -> Result<(), Error> { + println!("{message}"); + Ok(()) +} + +pub(super) fn print_packages(packages: Vec) -> Result<(), Error> { + for package in packages { + print(&format!("{package}"))?; + } + Ok(()) +} + +#[derive(Clone, Default, Debug, PartialEq, Eq)] pub struct Error { pub message: String, pub kind: ErrorKind, } -#[derive(Debug)] +#[derive(Clone, Default, Debug, PartialEq, Eq)] pub enum ErrorKind { CommandNotFound, + RootlessReadOnly, + #[default] Unknown, FailedSpawn, ChildExit, diff --git a/src/run/expeditor.rs b/src/run/expeditor.rs index 79a5c91..f1f31a7 100644 --- a/src/run/expeditor.rs +++ b/src/run/expeditor.rs @@ -17,17 +17,19 @@ pub fn fulfill(state: &State) -> Result<(), Error> { TaskKind::Help => executor::print("")?, TaskKind::PackageInstall => { let packages: Vec = task.parameters.iter().map(|s| s.into()).collect(); - state + let mut transaction = state .os() .packager() .install(&packages, state.configuration())?; + executor::commit(&mut transaction)?; } TaskKind::PackageUninstall => { let packages: Vec = task.parameters.iter().map(|s| s.into()).collect(); - state + let mut transaction = state .os() .packager() .uninstall(&packages, state.configuration())?; + executor::commit(&mut transaction)?; } TaskKind::PackageListAuto => { match state.os().packager().automatic() {