Implement transactional operations

This commit is contained in:
Juno Takano 2026-04-04 23:03:49 -03:00
commit f4f135da71
5 changed files with 190 additions and 33 deletions

View file

@ -7,7 +7,7 @@ use crate::{
Kind, OperatingSystem, Kind, OperatingSystem,
pkg::{self, Package, PackagerVariant, Packages}, pkg::{self, Package, PackagerVariant, Packages},
}, },
run::{Command, executor::read}, run::{Command, Transaction, TransactionCommand, executor::read},
}; };
pub const DEBIAN: OperatingSystem = OperatingSystem { pub const DEBIAN: OperatingSystem = OperatingSystem {
@ -25,12 +25,20 @@ pub struct Apt {
} }
impl Packages for Apt { impl Packages for Apt {
fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), pkg::Error> { fn install(
super::debian::Apt::haul("install", packages, config) &self,
packages: &[Package],
config: &Configuration,
) -> Result<Transaction, pkg::Error> {
super::debian::Apt::haul(&Operation::Install, packages, config)
} }
fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), pkg::Error> { fn uninstall(
super::debian::Apt::haul("remove", packages, config) &self,
packages: &[Package],
config: &Configuration,
) -> Result<Transaction, pkg::Error> {
super::debian::Apt::haul(&Operation::Uninstall, packages, config)
} }
fn manual(&self) -> Result<Vec<Package>, pkg::Error> { fn manual(&self) -> Result<Vec<Package>, pkg::Error> {
@ -145,20 +153,64 @@ impl Packages for Apt {
impl Apt { impl Apt {
fn haul( fn haul(
subcommand: &str, operation: &Operation,
packages: &[Package], packages: &[Package],
config: &Configuration, config: &Configuration,
) -> Result<(), pkg::Error> { ) -> Result<Transaction, pkg::Error> {
if packages.is_empty() { if packages.is_empty() {
println!("Package selection is empty: Nothing to {subcommand}"); println!("Package selection is empty: Nothing to {operation}");
return Ok(()); 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())) .chain(packages.iter().map(|p| p.into()))
.collect(); .collect();
let command = Command::new("apt", &args).escalate(config)?; let rollback_args: Vec<&str> = iter::once(rollback_operation.into())
Ok(crate::run::executor::spawn(&command)?) .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<Operation> 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}")
} }
} }

View file

@ -5,11 +5,16 @@ use core::{
fmt::Debug, 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 { pub trait Packages: Clone + Default + Debug + PartialEq + Eq {
fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), Error>; fn install(&self, packages: &[Package], config: &Configuration) -> Result<Transaction, Error>;
fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), Error>; fn uninstall(&self, packages: &[Package], config: &Configuration)
-> Result<Transaction, Error>;
fn manual(&self) -> Result<Vec<Package>, Error>; fn manual(&self) -> Result<Vec<Package>, Error>;
fn automatic(&self) -> Result<Vec<Package>, Error>; fn automatic(&self) -> Result<Vec<Package>, Error>;
fn variant(&self) -> &PackagerVariant; fn variant(&self) -> &PackagerVariant;
@ -23,14 +28,18 @@ pub enum Packager {
} }
impl Packages for Packager { impl Packages for Packager {
fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), Error> { fn install(&self, packages: &[Package], config: &Configuration) -> Result<Transaction, Error> {
match self { match self {
Packager::Apt(p) => p.install(packages, config), Packager::Apt(p) => p.install(packages, config),
Packager::Unknown => Error::unknown_packager(&format!("install {packages:?}")), 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<Transaction, Error> {
match self { match self {
Packager::Apt(p) => p.uninstall(packages, config), Packager::Apt(p) => p.uninstall(packages, config),
Packager::Unknown => Error::unknown_packager(&format!("uninstall {packages:?}")), Packager::Unknown => Error::unknown_packager(&format!("uninstall {packages:?}")),

View file

@ -74,6 +74,7 @@ pub enum TaskKind {
pub struct Command { pub struct Command {
pub base: String, pub base: String,
pub args: Vec<String>, pub args: Vec<String>,
escalated: bool,
} }
impl Command { impl Command {
@ -104,6 +105,7 @@ impl Command {
Ok(Command { Ok(Command {
base: config.su_command.command().clone().base, base: config.su_command.command().clone().base,
escalated: true,
args, args,
}) })
} }
@ -112,17 +114,69 @@ impl Command {
Command { Command {
base: base.to_string(), base: base.to_string(),
args: args.iter().map(|e| e.to_string()).collect(), 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<TransactionCommand>,
}
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<Vec<executor::Error>>,
}
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 struct Error {
pub message: String, pub message: String,
pub kind: ErrorKind, pub kind: ErrorKind,
} }
#[derive(Debug)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind { pub enum ErrorKind {
BadSuCommandConfig, BadSuCommandConfig,
} }

View file

@ -1,22 +1,41 @@
use std::process; 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 mod meta;
pub fn print(message: &str) -> Result<(), Error> { // TODO Should this be a method of Transaction instead?
println!("{message}"); pub fn commit(transaction: &mut Transaction) -> Result<(), Error> {
Ok(()) elog(&format!("Committing transaction: {transaction:#?}"));
} for command in &mut transaction.commands {
if let Err(error) = spawn(&command.run) {
pub fn print_packages(packages: Vec<Package>) -> Result<(), Error> { command.status = TransactionCommandStatus::PendingRollback;
for package in packages { command.push_error(&error);
print(&format!("{package}"))?; 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(()) 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) if let Ok(mut child) = process::Command::new(&command.base)
.args(&command.args) .args(&command.args)
.spawn() .spawn()
@ -44,6 +63,13 @@ pub fn spawn(command: &Command) -> Result<(), Error> {
} }
pub fn read(command: &Command) -> Result<String, Error> { pub fn read(command: &Command) -> Result<String, Error> {
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) if let Ok(output) = process::Command::new(&command.base)
.args(&command.args) .args(&command.args)
.output() .output()
@ -68,15 +94,29 @@ pub fn read(command: &Command) -> Result<String, Error> {
} }
} }
#[derive(Debug)] pub fn print(message: &str) -> Result<(), Error> {
println!("{message}");
Ok(())
}
pub(super) fn print_packages(packages: Vec<Package>) -> Result<(), Error> {
for package in packages {
print(&format!("{package}"))?;
}
Ok(())
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct Error { pub struct Error {
pub message: String, pub message: String,
pub kind: ErrorKind, pub kind: ErrorKind,
} }
#[derive(Debug)] #[derive(Clone, Default, Debug, PartialEq, Eq)]
pub enum ErrorKind { pub enum ErrorKind {
CommandNotFound, CommandNotFound,
RootlessReadOnly,
#[default]
Unknown, Unknown,
FailedSpawn, FailedSpawn,
ChildExit, ChildExit,

View file

@ -17,17 +17,19 @@ pub fn fulfill(state: &State) -> Result<(), Error> {
TaskKind::Help => executor::print("<long help>")?, TaskKind::Help => executor::print("<long help>")?,
TaskKind::PackageInstall => { TaskKind::PackageInstall => {
let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect(); let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect();
state let mut transaction = state
.os() .os()
.packager() .packager()
.install(&packages, state.configuration())?; .install(&packages, state.configuration())?;
executor::commit(&mut transaction)?;
} }
TaskKind::PackageUninstall => { TaskKind::PackageUninstall => {
let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect(); let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect();
state let mut transaction = state
.os() .os()
.packager() .packager()
.uninstall(&packages, state.configuration())?; .uninstall(&packages, state.configuration())?;
executor::commit(&mut transaction)?;
} }
TaskKind::PackageListAuto => { TaskKind::PackageListAuto => {
match state.os().packager().automatic() { match state.os().packager().automatic() {