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,
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<Transaction, pkg::Error> {
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<Transaction, pkg::Error> {
super::debian::Apt::haul(&Operation::Uninstall, packages, config)
}
fn manual(&self) -> Result<Vec<Package>, 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<Transaction, pkg::Error> {
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<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,
};
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<Transaction, Error>;
fn uninstall(&self, packages: &[Package], config: &Configuration)
-> Result<Transaction, Error>;
fn manual(&self) -> Result<Vec<Package>, Error>;
fn automatic(&self) -> Result<Vec<Package>, 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<Transaction, Error> {
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<Transaction, Error> {
match self {
Packager::Apt(p) => p.uninstall(packages, config),
Packager::Unknown => Error::unknown_packager(&format!("uninstall {packages:?}")),

View file

@ -74,6 +74,7 @@ pub enum TaskKind {
pub struct Command {
pub base: String,
pub args: Vec<String>,
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<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 message: String,
pub kind: ErrorKind,
}
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
BadSuCommandConfig,
}

View file

@ -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<Package>) -> 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<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)
.args(&command.args)
.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 message: String,
pub kind: ErrorKind,
}
#[derive(Debug)]
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub enum ErrorKind {
CommandNotFound,
RootlessReadOnly,
#[default]
Unknown,
FailedSpawn,
ChildExit,

View file

@ -17,17 +17,19 @@ pub fn fulfill(state: &State) -> Result<(), Error> {
TaskKind::Help => executor::print("<long help>")?,
TaskKind::PackageInstall => {
let packages: Vec<Package> = 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<Package> = 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() {