mod builder;
pub mod files;
pub mod init;
use std::ffi::OsString;
use std::fmt::Display;
pub use builder::Spec;
use itertools::{Either, Itertools};
use crate::Tense;
use self::builder::ToAssign;
use self::init::cron::teardown::CrontabChanged;
use self::init::cron::{GetCrontabError, SetCrontabError};
use self::init::systemd::SystemCtlError;
use self::init::SetupError;
#[derive(Debug, Clone, Copy)]
pub enum Mode {
User,
System,
}
impl Mode {
fn is_user(self) -> bool {
match self {
Mode::User => true,
Mode::System => false,
}
}
}
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Mode::User => f.write_str("user"),
Mode::System => f.write_str("system"),
}
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(thiserror::Error, Debug)]
pub enum PrepareInstallError {
#[error("Error setting up init: {0}")]
Init(#[from] init::SetupError),
#[error("Failed to move files: {0}")]
Move(#[from] files::MoveError),
#[error("Need to run as root to install to system")]
NeedRootForSysInstall,
#[error("Need to run as root to setup service to run as another user")]
NeedRootToRunAs,
#[error("Could not find an init system we can set things up for")]
NoInitSystemRecognized,
#[error("Install configured to run as a user: `{0}` however this user does not exist")]
UserDoesNotExist(String),
#[error("All supported init systems found failed, errors: {0:?}")]
SupportedInitSystemFailed(Vec<InitSystemFailure>),
}
#[derive(Debug, thiserror::Error)]
#[error("Init system: {name} ran into error: {error}")]
pub struct InitSystemFailure {
name: String,
error: SetupError,
}
#[derive(thiserror::Error, Debug)]
pub enum PrepareRemoveError {
#[error("Could not find this executable's location: {0}")]
GetExeLocation(std::io::Error),
#[error("Failed to remove files: {0}")]
Move(#[from] files::DeleteError),
#[error("Removing from init system: {0}")]
Init(#[from] init::TearDownError),
#[error("Could not find any installation in any init system")]
NoInstallFound,
#[error("Need to run as root to remove a system install")]
NeedRoot,
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, thiserror::Error)]
pub enum InstallError {
#[error("Could not get crontab, needed to add our lines, error: {0}")]
GetCrontab(#[from] init::cron::GetCrontabError),
#[error("{0}")]
CrontabChanged(#[from] init::cron::teardown::CrontabChanged),
#[error("Could not set crontab, needed to add our lines, error: {0}")]
SetCrontab(#[from] init::cron::SetCrontabError),
#[error("Something went wrong interacting with systemd: {0}")]
Systemd(#[from] init::systemd::Error),
#[error("Could not copy executable: {0}")]
CopyExe(std::io::Error),
#[error("Could not set the owner of the installed executable to be root: {0}")]
SetRootOwner(std::io::Error),
#[error("Could not make the installed executable read only: {0}")]
SetReadOnly(#[from] files::SetReadOnlyError),
#[error("Can not disable Cron service, process will not stop.")]
CouldNotStop,
}
#[allow(clippy::module_name_repetitions)]
pub trait InstallStep {
fn describe(&self, tense: Tense) -> String;
fn describe_detailed(&self, tense: Tense) -> String {
self.describe(tense)
}
fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError>;
}
impl std::fmt::Debug for &dyn InstallStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.describe(Tense::Future))
}
}
impl Display for &dyn InstallStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.describe_detailed(Tense::Future))
}
}
#[derive(Debug, thiserror::Error)]
pub enum RemoveError {
#[error("Could not get crontab, needed tot filter out our added lines, error: {0}")]
GetCrontab(#[from] init::cron::GetCrontabError),
#[error("{0}")]
CrontabChanged(#[from] init::cron::teardown::CrontabChanged),
#[error("Could not set crontab, needed tot filter out our added lines, error: {0}")]
SetCrontab(#[from] init::cron::SetCrontabError),
#[error("Could not remove file(s), error: {0}")]
DeleteError(#[from] files::DeleteError),
#[error("Something went wrong interacting with systemd: {0}")]
Systemd(#[from] init::systemd::Error),
}
pub trait RemoveStep {
fn describe(&self, tense: Tense) -> String;
fn describe_detailed(&self, tense: Tense) -> String {
self.describe(tense)
}
fn perform(&mut self) -> Result<(), RemoveError>;
}
impl std::fmt::Debug for &dyn RemoveStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.describe(Tense::Future))
}
}
impl Display for &dyn RemoveStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.describe_detailed(Tense::Future))
}
}
#[derive(Debug, thiserror::Error)]
pub enum RollbackError {
#[error("Could not rollback, error: {0}")]
Removing(#[from] RemoveError),
#[error("Could not rollback, error restoring file permissions: {0}")]
RestoringPermissions(std::io::Error),
#[error("Could not rollback, error re-enabling service: {0}")]
ReEnabling(#[from] SystemCtlError),
#[error("Can not rollback setting up cron, must be done manually")]
Impossible,
#[error("Crontab changed undoing changes might overwrite the change")]
CrontabChanged(#[from] CrontabChanged),
#[error("Could not get the crontab, needed to undo a change to it: {0}")]
GetCrontab(#[from] GetCrontabError),
#[error("Could not revert to the original crontab: {0}")]
SetCrontab(#[from] SetCrontabError),
}
pub trait RollbackStep {
fn perform(&mut self) -> Result<(), RollbackError>;
fn describe(&self, tense: Tense) -> String;
}
impl std::fmt::Debug for &dyn RollbackStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.describe(Tense::Future))
}
}
impl Display for &dyn RollbackStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.describe(Tense::Future))
}
}
impl<T: RemoveStep> RollbackStep for T {
fn perform(&mut self) -> Result<(), RollbackError> {
Ok(self.perform()?)
}
fn describe(&self, tense: Tense) -> String {
self.describe(tense)
}
}
#[allow(clippy::module_name_repetitions)]
pub struct InstallSteps(pub(crate) Vec<Box<dyn InstallStep>>);
impl std::fmt::Debug for InstallSteps {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for step in self.0.iter().map(|step| step.describe(Tense::Future)) {
write!(f, "{step\n}")?;
}
Ok(())
}
}
impl Display for InstallSteps {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for step in self
.0
.iter()
.map(|step| step.describe_detailed(Tense::Future))
{
write!(f, "{step\n}")?;
}
Ok(())
}
}
impl IntoIterator for InstallSteps {
type Item = Box<dyn InstallStep>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl InstallSteps {
pub fn install(self) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
let mut description = Vec::new();
for mut step in self.0 {
description.push(step.describe(Tense::Past));
step.perform()?;
}
Ok(description.join("\n"))
}
}
impl<T: ToAssign> Spec<builder::Set, builder::Set, builder::Set, T> {
pub fn prepare_install(self) -> Result<InstallSteps, PrepareInstallError> {
let builder::Spec {
mode,
path: Some(source),
service_name: Some(name),
bin_name,
args,
trigger: Some(trigger),
overwrite_existing,
working_dir,
run_as,
description,
..
} = self
else {
unreachable!("type sys guarantees path, name and trigger set")
};
let not_root = matches!(sudo::check(), sudo::RunningAs::User);
if let Mode::System = mode {
if not_root {
return Err(PrepareInstallError::NeedRootForSysInstall);
}
}
if let Some(ref user) = run_as {
let curr_user = uzers::get_current_username()
.ok_or_else(|| PrepareInstallError::UserDoesNotExist(user.clone()))?;
if curr_user != OsString::from(user) && not_root {
return Err(PrepareInstallError::NeedRootToRunAs);
}
}
let init_systems = self.init_systems.unwrap_or_else(init::System::all);
let (mut steps, exe_path) = files::move_files(
source,
mode,
run_as.as_deref(),
overwrite_existing,
&init_systems,
)?;
let params = init::Params {
name,
bin_name,
description,
exe_path,
exe_args: args,
working_dir,
trigger,
run_as,
mode,
};
let mut errors = Vec::new();
for init in init_systems {
if init.not_available().map_err(PrepareInstallError::Init)? {
continue;
}
match init.set_up_steps(¶ms) {
Ok(init_steps) => {
steps.extend(init_steps);
return Ok(InstallSteps(steps));
}
Err(err) => {
#[cfg(feature = "tracing")]
tracing::warn!("Could not set up init using {}, error: {err}", init.name());
errors.push(InitSystemFailure {
name: init.name().to_owned(),
error: err,
});
}
};
}
if errors.is_empty() {
Err(PrepareInstallError::NoInitSystemRecognized)
} else {
Err(PrepareInstallError::SupportedInitSystemFailed(errors))
}
}
}
pub struct RemoveSteps(pub(crate) Vec<Box<dyn RemoveStep>>);
impl std::fmt::Debug for RemoveSteps {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for step in self.0.iter().map(|step| step.describe(Tense::Future)) {
write!(f, "{step\n}")?;
}
Ok(())
}
}
impl Display for RemoveSteps {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for step in self
.0
.iter()
.map(|step| step.describe_detailed(Tense::Future))
{
write!(f, "{step\n}")?;
}
Ok(())
}
}
impl IntoIterator for RemoveSteps {
type Item = Box<dyn RemoveStep>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl RemoveSteps {
pub fn remove(self) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
let mut description = Vec::new();
for mut step in self.0 {
description.push(step.describe(Tense::Past));
step.perform()?;
}
Ok(description.join("\n"))
}
pub fn best_effort_remove(self) -> Result<String, BestEffortRemoveError> {
let (description, failures): (Vec<_>, Vec<_>) =
self.0
.into_iter()
.partition_map(|mut step| match step.perform() {
Ok(()) => Either::Left(step.describe(Tense::Past)),
Err(e) => Either::Right((step.describe_detailed(Tense::Active), e)),
});
if failures.is_empty() {
Ok(description.join("\n"))
} else {
Err(BestEffortRemoveError { failures })
}
}
}
#[derive(Debug, thiserror::Error)]
pub struct BestEffortRemoveError {
failures: Vec<(String, RemoveError)>,
}
impl Display for BestEffortRemoveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Ran into one or more issues trying to remove an install")?;
writeln!(f, "You should resolve/check these issues manually")?;
for (task, error) in &self.failures {
let task = task.to_lowercase();
writeln!(f, "* Tried to {task}\nfailed because: {error}")?;
}
Ok(())
}
}
impl<M: ToAssign, P: ToAssign, T: ToAssign, I: ToAssign> Spec<M, P, T, I> {
pub fn prepare_remove(self) -> Result<RemoveSteps, PrepareRemoveError> {
let builder::Spec {
mode,
service_name: Some(name),
bin_name,
run_as,
..
} = self
else {
unreachable!("type sys guarantees name and trigger set")
};
if let Mode::System = mode {
if let sudo::RunningAs::User = sudo::check() {
return Err(PrepareRemoveError::NeedRoot);
}
}
let mut inits = self.init_systems.unwrap_or(init::System::all()).into_iter();
let (mut steps, path) = loop {
let Some(init) = inits.next() else {
return Err(PrepareRemoveError::NoInstallFound);
};
if let Some(install) = init.tear_down_steps(&name, bin_name, mode, run_as.as_deref())? {
break install;
}
};
let remove_step = files::remove_files(path);
steps.push(Box::new(remove_step));
Ok(RemoveSteps(steps))
}
}