Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pre install/uninstall checks to planners #561

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/action/base/create_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ impl Action for CreateUser {
Some(40) if stderr.contains("-14120") => {
// The user is on an ephemeral Mac, like detsys uses
// These Macs cannot always delete users, as sometimes there is no graphical login
tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}", self.name);
tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}`", self.name);
},
_ => {
// Something went wrong
Expand Down
2 changes: 1 addition & 1 deletion src/action/base/delete_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl Action for DeleteUser {
Some(40) if stderr.contains("-14120") => {
// The user is on an ephemeral Mac, like detsys uses
// These Macs cannot always delete users, as sometimes there is no graphical login
tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}", self.name);
tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}`", self.name);
},
_ => {
// Something went wrong
Expand Down
8 changes: 8 additions & 0 deletions src/cli/subcommand/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ impl CommandExecute for Install {
(Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")),
};

if let Err(err) = install_plan.pre_install_check().await {
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
Err(err)?
}

if !no_confirm {
let mut currently_explaining = explain;
loop {
Expand Down
8 changes: 8 additions & 0 deletions src/cli/subcommand/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ impl CommandExecute for Uninstall {
.wrap_err("Reading receipt")?;
let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?;

if let Err(err) = plan.pre_uninstall_check().await {
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
Err(err)?
}

if !no_confirm {
let mut currently_explaining = explain;
loop {
Expand Down
18 changes: 18 additions & 0 deletions src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ impl InstallPlan {
#[cfg(feature = "diagnostics")]
let diagnostic_data = Some(planner.diagnostic_data().await?);

// Some Action `plan` calls may fail if we don't do these checks
planner.pre_install_check().await?;

let actions = planner.plan().await?;
Ok(Self {
planner: planner.boxed(),
Expand All @@ -62,6 +65,17 @@ impl InstallPlan {
diagnostic_data,
})
}

pub async fn pre_uninstall_check(&self) -> Result<(), NixInstallerError> {
self.planner.pre_uninstall_check().await?;
Ok(())
}

pub async fn pre_install_check(&self) -> Result<(), NixInstallerError> {
self.planner.pre_install_check().await?;
Ok(())
}

#[tracing::instrument(level = "debug", skip_all)]
pub async fn describe_install(&self, explain: bool) -> Result<String, NixInstallerError> {
let Self {
Expand Down Expand Up @@ -143,6 +157,8 @@ impl InstallPlan {
cancel_channel: impl Into<Option<Receiver<()>>>,
) -> Result<(), NixInstallerError> {
self.check_compatible()?;
self.planner.pre_install_check().await?;

let Self { actions, .. } = self;
let mut cancel_channel = cancel_channel.into();

Expand Down Expand Up @@ -313,6 +329,8 @@ impl InstallPlan {
cancel_channel: impl Into<Option<Receiver<()>>>,
) -> Result<(), NixInstallerError> {
self.check_compatible()?;
self.planner.pre_uninstall_check().await?;

let Self { actions, .. } = self;
let mut cancel_channel = cancel_channel.into();
let mut errors = vec![];
Expand Down
43 changes: 28 additions & 15 deletions src/planner/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,8 @@ impl Planner for Linux {
}

async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
check_not_nixos()?;

check_nix_not_already_installed().await?;

check_not_wsl1()?;

let has_selinux = detect_selinux().await?;

if self.init.init == InitSystem::Systemd && self.init.start_daemon {
check_systemd_active()?;
}

let mut plan = vec![];

plan.push(
Expand Down Expand Up @@ -141,6 +131,29 @@ impl Planner for Linux {
self.settings.ssl_cert_file.clone(),
)?)
}
async fn pre_uninstall_check(&self) -> Result<(), PlannerError> {
cole-h marked this conversation as resolved.
Show resolved Hide resolved
check_not_wsl1()?;

if self.init.init == InitSystem::Systemd && self.init.start_daemon {
check_systemd_active()?;
}

Ok(())
}

async fn pre_install_check(&self) -> Result<(), PlannerError> {
check_not_nixos()?;

check_nix_not_already_installed().await?;

check_not_wsl1()?;

if self.init.init == InitSystem::Systemd && self.init.start_daemon {
check_systemd_active()?;
}

Ok(())
}
}

impl Into<BuiltinPlanner> for Linux {
Expand All @@ -150,23 +163,23 @@ impl Into<BuiltinPlanner> for Linux {
}

// If on NixOS, running `nix_installer` is pointless
fn check_not_nixos() -> Result<(), PlannerError> {
pub(crate) fn check_not_nixos() -> Result<(), PlannerError> {
// NixOS always sets up this file as part of setting up /etc itself: https://github.com/NixOS/nixpkgs/blob/bdd39e5757d858bd6ea58ed65b4a2e52c8ed11ca/nixos/modules/system/etc/setup-etc.pl#L145
if Path::new("/etc/NIXOS").exists() {
return Err(PlannerError::NixOs);
}
Ok(())
}

fn check_not_wsl1() -> Result<(), PlannerError> {
pub(crate) fn check_not_wsl1() -> Result<(), PlannerError> {
// Detection strategies: https://patrickwu.space/wslconf/
if std::env::var("WSL_DISTRO_NAME").is_ok() && std::env::var("WSL_INTEROP").is_err() {
return Err(PlannerError::Wsl1);
}
Ok(())
}

async fn detect_selinux() -> Result<bool, PlannerError> {
pub(crate) async fn detect_selinux() -> Result<bool, PlannerError> {
if Path::new("/sys/fs/selinux").exists() && which("sestatus").is_ok() {
// We expect systems with SELinux to have the normal SELinux tools.
let has_semodule = which("semodule").is_ok();
Expand All @@ -181,7 +194,7 @@ async fn detect_selinux() -> Result<bool, PlannerError> {
}
}

async fn check_nix_not_already_installed() -> Result<(), PlannerError> {
pub(crate) async fn check_nix_not_already_installed() -> Result<(), PlannerError> {
// For now, we don't try to repair the user's Nix install or anything special.
if let Ok(_) = Command::new("nix-env")
.arg("--version")
Expand All @@ -195,7 +208,7 @@ async fn check_nix_not_already_installed() -> Result<(), PlannerError> {
Ok(())
}

fn check_systemd_active() -> Result<(), PlannerError> {
pub(crate) fn check_systemd_active() -> Result<(), PlannerError> {
if !Path::new("/run/systemd/system").exists() {
if std::env::var("WSL_DISTRO_NAME").is_ok() {
return Err(LinuxErrorKind::Wsl2SystemdNotActive)?;
Expand Down
56 changes: 53 additions & 3 deletions src/planner/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ use std::{collections::HashMap, io::Cursor, path::PathBuf};
#[cfg(feature = "cli")]
use clap::ArgAction;
use tokio::process::Command;
use which::which;

use super::ShellProfileLocations;
use crate::planner::HasExpectedErrors;

use crate::{
action::{
Expand Down Expand Up @@ -89,8 +91,6 @@ impl Planner for Macos {
}

async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
ensure_not_running_in_rosetta().await?;

let root_disk = match &self.root_disk {
root_disk @ Some(_) => root_disk.clone(),
None => {
Expand Down Expand Up @@ -219,6 +219,18 @@ impl Planner for Macos {
self.settings.ssl_cert_file.clone(),
)?)
}

async fn pre_uninstall_check(&self) -> Result<(), PlannerError> {
check_nix_darwin_not_installed().await?;

Ok(())
}

async fn pre_install_check(&self) -> Result<(), PlannerError> {
check_not_running_in_rosetta()?;

Ok(())
}
}

impl Into<BuiltinPlanner> for Macos {
Expand All @@ -227,7 +239,30 @@ impl Into<BuiltinPlanner> for Macos {
}
}

async fn ensure_not_running_in_rosetta() -> Result<(), PlannerError> {
async fn check_nix_darwin_not_installed() -> Result<(), PlannerError> {
let has_darwin_rebuild = which("darwin-rebuild").is_ok();
let has_darwin_option = which("darwin-option").is_ok();

let activate_system_present = Command::new("launchctl")
.arg("print")
.arg("system/org.nixos.activate-system")
.process_group(0)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map(|v| v.success())
.unwrap_or(false);

if activate_system_present || has_darwin_rebuild || has_darwin_option {
return Err(MacosError::UninstallNixDarwin).map_err(|e| PlannerError::Custom(Box::new(e)));
};

Ok(())
}

fn check_not_running_in_rosetta() -> Result<(), PlannerError> {
use sysctl::{Ctl, Sysctl};
const CTLNAME: &str = "sysctl.proc_translated";

Expand All @@ -246,3 +281,18 @@ async fn ensure_not_running_in_rosetta() -> Result<(), PlannerError> {

Ok(())
}

#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum MacosError {
#[error("`nix-darwin` installation detected, it must be removed before uninstalling Nix. Please refer to https://github.com/LnL7/nix-darwin#uninstalling for instructions how to uninstall `nix-darwin`.")]
UninstallNixDarwin,
}

impl HasExpectedErrors for MacosError {
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
match self {
this @ MacosError::UninstallNixDarwin => Some(Box::new(this)),
}
}
}
12 changes: 12 additions & 0 deletions src/planner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
Box::new(self)
}

async fn pre_uninstall_check(&self) -> Result<(), PlannerError> {
Ok(())
}

async fn pre_install_check(&self) -> Result<(), PlannerError> {
Ok(())
}

#[cfg(feature = "diagnostics")]
async fn diagnostic_data(&self) -> Result<crate::diagnostics::DiagnosticData, PlannerError>;
}
Expand Down Expand Up @@ -415,6 +423,10 @@ impl HasExpectedErrors for PlannerError {
if let Some(err) = _e.downcast_ref::<linux::LinuxErrorKind>() {
return err.expected();
}
#[cfg(target_os = "macos")]
if let Some(err) = _e.downcast_ref::<macos::MacosError>() {
return err.expected();
}
None
},
this @ PlannerError::NixOs => Some(Box::new(this)),
Expand Down
22 changes: 22 additions & 0 deletions src/planner/steam_deck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,28 @@ impl Planner for SteamDeck {
self.settings.ssl_cert_file.clone(),
)?)
}

async fn pre_uninstall_check(&self) -> Result<(), PlannerError> {
super::linux::check_not_wsl1()?;

// Unlike the Linux planner, the steam deck planner requires systemd
super::linux::check_systemd_active()?;

Ok(())
}

async fn pre_install_check(&self) -> Result<(), PlannerError> {
super::linux::check_not_nixos()?;

super::linux::check_nix_not_already_installed().await?;

super::linux::check_not_wsl1()?;

// Unlike the Linux planner, the steam deck planner requires systemd
super::linux::check_systemd_active()?;

Ok(())
}
}

impl Into<BuiltinPlanner> for SteamDeck {
Expand Down