Compare commits

...

2 commits

Author SHA1 Message Date
f4f135da71 Implement transactional operations 2026-04-04 23:03:49 -03:00
866b5b5164 Extend manually installed packages test cases 2026-04-04 21:04:51 -03:00
9 changed files with 230 additions and 45 deletions

View file

@ -2,5 +2,6 @@
set -eu
./build.sh "$1" && clear
./build.sh "$1"
clear
./run.sh "$1"

View file

@ -10,11 +10,9 @@ if podman container exists "$tag"; then
podman stop --time 3 "$tag"
fi
if ! [ -f "../target/debug/$binary" ]; then
cd ..
cargo build
cd -
fi
cd ..
cargo build
cd -
cp -v ../target/debug/$binary $binary

View file

@ -9,7 +9,7 @@ fail() { printf " [FAIL] %b\n" "$1"; exit 1; }
try() {
actual="$1"
expected="$2"
expected="$(printf '%b' "$2")"
fail_message="${3:-}"
ok_message="${4:-}"
@ -20,6 +20,8 @@ try() {
fi
}
info "tori version $(tori version)"
announce "Fresh install has no manually installed packages"
tori_manual=$(tori manual)
try "$tori_manual" ""
@ -28,11 +30,13 @@ info "Updating apt packages"
apt-get update >/dev/null
announce "Manually installed package is the only package in 'tori manual'"
info "Installing: figlet"
apt-get install -y figlet >/dev/null 2>&1
tori_manual=$(tori manual)
try "$tori_manual" figlet
announce "Manually installed packages are the only packages in 'tori manual'"
info "Installing: sudo"
apt-get install -y sudo >/dev/null 2>&1
tori_manual=$(tori manual | sort)
tori_manual=$(tori manual)
try "$tori_manual" "$(printf 'figlet\nsudo')"

View file

@ -9,7 +9,7 @@ fail() { printf " [FAIL] %b\n" "$1"; exit 1; }
try() {
actual="$1"
expected="$2"
expected="$(printf '%b' "$2")"
operator="${3:-=}"
fail_message="${3:-}"
ok_message="${4:-}"
@ -23,18 +23,43 @@ try() {
fi
}
info "tori version $(tori version)"
announce "sudo works"
whoami=$(whoami)
sudo_whoami=$(sudo whoami)
echo try "$whoami" "$sudo_whoami" !=
try "$whoami" "$sudo_whoami" !=
echo try "$sudo_whoami" root
try "$sudo_whoami" root
info "Updating apt packages"
sudo apt-get update >/dev/null
announce "Manually installed packages are the only packages in 'tori manual'"
info "Installing: sudo"
sudo apt-get install -y sudo >/dev/null 2>&1
tori_manual=$(tori manual | sort)
tori_manual=$(tori manual)
try "$tori_manual" "sudo"
announce "Manually installed packages change after installing one"
info "Installing: figlet"
sudo apt-get install -y figlet >/dev/null 2>&1
tori_manual=$(tori manual)
try "$tori_manual" "figlet\nsudo"
announce "Manually installed packages change after installing several"
info "Installing: vim-tiny tmux qalc"
sudo apt-get install -y vim-tiny tmux qalc >/dev/null 2>&1
tori_manual=$(tori manual)
try "$tori_manual" "figlet\nqalc\nsudo\ntmux\nvim-tiny"
announce "Manually installed packages change after uninstalling one"
info "Uninstalling: qalc"
sudo apt-get remove -y qalc >/dev/null 2>&1
tori_manual=$(tori manual)
try "$tori_manual" "figlet\nsudo\ntmux\nvim-tiny"
announce "Manually installed packages change after uninstalling several"
info "Uninstalling: figlet tmux vim-tiny"
sudo apt-get remove -y figlet tmux vim-tiny >/dev/null 2>&1
tori_manual=$(tori manual)
try "$tori_manual" "sudo"

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() {