Handle the APFS volume not existing but the Service and Fstab being present (#405)

* Handle the APFS volume not existing but the Service and Fstab being present

* Add handling if we need to bootout the service

* Spelling

* Rename enum
This commit is contained in:
Ana Hobden 2023-04-10 13:13:25 -07:00 committed by GitHub
parent 16ddada7a3
commit cb48a7261b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 251 additions and 85 deletions

View file

@ -122,9 +122,17 @@ impl Action for CreateFstabEntry {
existing_entry,
} = self;
let fstab_path = Path::new(FSTAB_PATH);
let uuid = get_uuid_for_label(&apfs_volume_label)
let uuid = match get_uuid_for_label(&apfs_volume_label)
.await
.map_err(Self::error)?;
.map_err(Self::error)?
{
Some(uuid) => uuid,
None => {
return Err(Self::error(CreateFstabEntryError::CannotDetermineUuid(
apfs_volume_label.clone(),
)))?
},
};
let mut fstab = tokio::fs::OpenOptions::new()
.create(true)
@ -227,43 +235,46 @@ impl Action for CreateFstabEntry {
async fn revert(&mut self) -> Result<(), ActionError> {
let fstab_path = Path::new(FSTAB_PATH);
let uuid = get_uuid_for_label(&self.apfs_volume_label)
if let Some(uuid) = get_uuid_for_label(&self.apfs_volume_label)
.await
.map_err(Self::error)?;
.map_err(Self::error)?
{
let fstab_entry = fstab_lines(&uuid, &self.apfs_volume_label);
let fstab_entry = fstab_lines(&uuid, &self.apfs_volume_label);
let mut file = OpenOptions::new()
.create(false)
.write(true)
.read(true)
.open(&fstab_path)
.await
.map_err(|e| Self::error(ActionErrorKind::Open(fstab_path.to_owned(), e)))?;
let mut file = OpenOptions::new()
.create(false)
.write(true)
.read(true)
.open(&fstab_path)
.await
.map_err(|e| Self::error(ActionErrorKind::Open(fstab_path.to_owned(), e)))?;
let mut file_contents = String::default();
file.read_to_string(&mut file_contents)
.await
.map_err(|e| Self::error(ActionErrorKind::Read(fstab_path.to_owned(), e)))?;
let mut file_contents = String::default();
file.read_to_string(&mut file_contents)
.await
.map_err(|e| Self::error(ActionErrorKind::Read(fstab_path.to_owned(), e)))?;
if let Some(start) = file_contents.rfind(fstab_entry.as_str()) {
let end = start + fstab_entry.len();
file_contents.replace_range(start..end, "")
}
if let Some(start) = file_contents.rfind(fstab_entry.as_str()) {
let end = start + fstab_entry.len();
file_contents.replace_range(start..end, "")
file.seek(SeekFrom::Start(0))
.await
.map_err(|e| Self::error(ActionErrorKind::Seek(fstab_path.to_owned(), e)))?;
file.set_len(0)
.await
.map_err(|e| Self::error(ActionErrorKind::Truncate(fstab_path.to_owned(), e)))?;
file.write_all(file_contents.as_bytes())
.await
.map_err(|e| Self::error(ActionErrorKind::Write(fstab_path.to_owned(), e)))?;
file.flush()
.await
.map_err(|e| Self::error(ActionErrorKind::Flush(fstab_path.to_owned(), e)))?;
} else {
return Err(Self::error(CreateFstabEntryError::CannotDetermineFstabLine));
}
file.seek(SeekFrom::Start(0))
.await
.map_err(|e| Self::error(ActionErrorKind::Seek(fstab_path.to_owned(), e)))?;
file.set_len(0)
.await
.map_err(|e| Self::error(ActionErrorKind::Truncate(fstab_path.to_owned(), e)))?;
file.write_all(file_contents.as_bytes())
.await
.map_err(|e| Self::error(ActionErrorKind::Write(fstab_path.to_owned(), e)))?;
file.flush()
.await
.map_err(|e| Self::error(ActionErrorKind::Flush(fstab_path.to_owned(), e)))?;
Ok(())
}
}
@ -289,6 +300,10 @@ pub enum CreateFstabEntryError {
ExistingNixInstallerEntryDisappeared,
#[error("The `/etc/fstab` entry (previously created by the official install scripts) detected during planning disappeared between planning and executing. Cannot update `/etc/fstab` as planned")]
ExistingForeignEntryDisappeared,
#[error("Unable to determine how to add APFS volume `{0}` the `/etc/fstab` line, likely the volume is not yet created or there is some synchronization issue, please report this")]
CannotDetermineUuid(String),
#[error("Unable to reliably determine which `/etc/fstab` line to remove, the volume is likely already deleted, the line involving `/nix` in `/etc/fstab` should be removed manually")]
CannotDetermineFstabLine,
}
impl Into<ActionErrorKind> for CreateFstabEntryError {

View file

@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use tokio::{
fs::{remove_file, OpenOptions},
io::AsyncWriteExt,
process::Command,
};
use crate::action::{
@ -22,6 +23,7 @@ pub struct CreateVolumeService {
mount_service_label: String,
mount_point: PathBuf,
encrypt: bool,
needs_bootout: bool,
}
impl CreateVolumeService {
@ -36,40 +38,98 @@ impl CreateVolumeService {
let path = path.as_ref().to_path_buf();
let mount_point = mount_point.as_ref().to_path_buf();
let mount_service_label = mount_service_label.into();
let this = Self {
let mut this = Self {
path,
apfs_volume_label,
mount_service_label,
mount_point,
encrypt,
needs_bootout: false,
};
// If the service is currently loaded or running, we need to unload it during execute (since we will then recreate it and reload it)
// This `launchctl` command may fail if the service isn't loaded
let mut check_loaded_command = Command::new("launchctl");
check_loaded_command.arg("print");
check_loaded_command.arg(format!("system/{}", this.mount_service_label));
tracing::trace!(
command = format!("{:?}", check_loaded_command.as_std()),
"Executing"
);
let check_loaded_output = check_loaded_command
.output()
.await
.map_err(|e| ActionErrorKind::command(&check_loaded_command, e))
.map_err(Self::error)?;
this.needs_bootout = check_loaded_output.status.success();
if this.needs_bootout {
tracing::debug!(
"Detected loaded service `{}` which needs unload before replacing `{}`",
this.mount_service_label,
this.path.display(),
);
}
if this.path.exists() {
let discovered_plist: LaunchctlMountPlist =
plist::from_file(&this.path).map_err(Self::error)?;
let expected_plist = generate_mount_plist(
&this.mount_service_label,
&this.apfs_volume_label,
&this.mount_point,
encrypt,
)
.await
.map_err(Self::error)?;
if discovered_plist != expected_plist {
tracing::trace!(
?discovered_plist,
?expected_plist,
"Parsed plists not equal"
);
return Err(Self::error(CreateVolumeServiceError::DifferentPlist {
expected: expected_plist,
discovered: discovered_plist,
path: this.path.clone(),
}));
}
match get_uuid_for_label(&this.apfs_volume_label)
.await
.map_err(Self::error)?
{
Some(uuid) => {
let expected_plist = generate_mount_plist(
&this.mount_service_label,
&this.apfs_volume_label,
uuid,
&this.mount_point,
encrypt,
)
.await
.map_err(Self::error)?;
if discovered_plist != expected_plist {
tracing::trace!(
?discovered_plist,
?expected_plist,
"Parsed plists not equal"
);
return Err(Self::error(CreateVolumeServiceError::DifferentPlist {
expected: expected_plist,
discovered: discovered_plist,
path: this.path.clone(),
}));
}
tracing::debug!("Creating file `{}` already complete", this.path.display());
return Ok(StatefulAction::completed(this));
tracing::debug!("Creating file `{}` already complete", this.path.display());
return Ok(StatefulAction::completed(this));
},
None => {
tracing::debug!(
"Detected existing service path `{}` but could not detect a UUID for the volume",
this.path.display()
);
// If there is already a line in `/etc/fstab` with `/nix` in it, the user will likely experience an error during execute,
// so check if there exists a line, which is not a comment, that contains `/nix`
let fstab = PathBuf::from("/etc/fstab");
if fstab.exists() {
let contents = tokio::fs::read_to_string(&fstab)
.await
.map_err(|e| Self::error(ActionErrorKind::Read(fstab, e)))?;
for line in contents.lines() {
if line.starts_with("#") {
continue;
}
let split = line.split_whitespace();
for item in split {
if item == "/nix" {
return Err(Self::error(CreateVolumeServiceError::VolumeDoesNotExistButVolumeServiceAndFstabEntryDoes(this.path.clone(), this.apfs_volume_label)));
}
}
}
}
},
}
}
Ok(StatefulAction::uncompleted(this))
@ -84,8 +144,13 @@ impl Action for CreateVolumeService {
}
fn tracing_synopsis(&self) -> String {
format!(
"Create a `launchctl` plist to mount the APFS volume `{}`",
self.path.display()
"{maybe_unload} a `launchctl` plist to mount the APFS volume `{path}`",
path = self.path.display(),
maybe_unload = if self.needs_bootout {
"Unload, then recreate"
} else {
"Create"
}
)
}
@ -115,11 +180,45 @@ impl Action for CreateVolumeService {
apfs_volume_label,
mount_point,
encrypt,
needs_bootout,
} = self;
if *needs_bootout {
let mut unload_command = Command::new("launchctl");
unload_command.arg("bootout");
unload_command.arg(format!("system/{mount_service_label}"));
tracing::trace!(
command = format!("{:?}", unload_command.as_std()),
"Executing"
);
let unload_output = unload_command
.output()
.await
.map_err(|e| ActionErrorKind::command(&unload_command, e))
.map_err(Self::error)?;
if !unload_output.status.success() {
return Err(Self::error(ActionErrorKind::command_output(
&unload_command,
unload_output,
)));
}
}
let uuid = match get_uuid_for_label(&apfs_volume_label)
.await
.map_err(Self::error)?
{
Some(uuid) => uuid,
None => {
return Err(Self::error(CreateVolumeServiceError::CannotDetermineUuid(
apfs_volume_label.to_string(),
)))
},
};
let generated_plist = generate_mount_plist(
&mount_service_label,
&apfs_volume_label,
uuid,
mount_point,
*encrypt,
)
@ -127,7 +226,7 @@ impl Action for CreateVolumeService {
.map_err(Self::error)?;
let mut options = OpenOptions::new();
options.create_new(true).write(true).read(true);
options.create(true).write(true).read(true);
let mut file = options
.open(&path)
@ -164,11 +263,11 @@ impl Action for CreateVolumeService {
async fn generate_mount_plist(
mount_service_label: &str,
apfs_volume_label: &str,
uuid: uuid::Uuid,
mount_point: &Path,
encrypt: bool,
) -> Result<LaunchctlMountPlist, ActionErrorKind> {
let apfs_volume_label_with_quotes = format!("\"{apfs_volume_label}\"");
let uuid = get_uuid_for_label(&apfs_volume_label).await?;
// The official Nix scripts uppercase the UUID, so we do as well for compatibility.
let uuid_string = uuid.to_string().to_uppercase();
let mount_command = if encrypt {
@ -210,6 +309,10 @@ pub enum CreateVolumeServiceError {
discovered: LaunchctlMountPlist,
path: PathBuf,
},
#[error("UUID for APFS volume labelled `{0}` was not found")]
CannotDetermineUuid(String),
#[error("An APFS volume labelled `{1}` does not exist, but there exists an fstab entry for that volume, as well as a service file at `{0}`. Consider removing the line containing `/nix` from the `/etc/fstab` and running `rm {0}`")]
VolumeDoesNotExistButVolumeServiceAndFstabEntryDoes(PathBuf, String),
}
impl Into<ActionErrorKind> for CreateVolumeServiceError {

View file

@ -76,7 +76,10 @@ impl Action for KickstartLaunchctlService {
ActionTag("kickstart_launchctl_service")
}
fn tracing_synopsis(&self) -> String {
format!("Run `launchctl kickstart -k {}`", self.service)
format!(
"Run `launchctl kickstart -k {}/{}`",
self.domain, self.service
)
}
fn tracing_span(&self) -> Span {

View file

@ -25,30 +25,49 @@ use tokio::process::Command;
pub use unmount_apfs_volume::UnmountApfsVolume;
use uuid::Uuid;
use crate::execute_command;
use super::ActionErrorKind;
async fn get_uuid_for_label(apfs_volume_label: &str) -> Result<Uuid, ActionErrorKind> {
let output = execute_command(
Command::new("/usr/sbin/diskutil")
.process_group(0)
.arg("info")
.arg("-plist")
.arg(apfs_volume_label)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped()),
)
.await?;
async fn get_uuid_for_label(apfs_volume_label: &str) -> Result<Option<Uuid>, ActionErrorKind> {
let mut command = Command::new("/usr/sbin/diskutil");
command.process_group(0);
command.arg("info");
command.arg("-plist");
command.arg(apfs_volume_label);
command.stdin(std::process::Stdio::null());
command.stdout(std::process::Stdio::piped());
let command_str = format!("{:?}", command.as_std());
tracing::trace!(command = command_str, "Executing");
let output = command
.output()
.await
.map_err(|e| ActionErrorKind::command(&command, e))?;
let parsed: DiskUtilApfsInfoOutput = plist::from_bytes(&output.stdout)?;
Ok(parsed.volume_uuid)
if let Some(error_message) = parsed.error_message {
let expected_not_found = format!("Could not find disk: {apfs_volume_label}");
if error_message.contains(&expected_not_found) {
return Ok(None);
} else {
return Err(ActionErrorKind::DiskUtilInfoError {
command: command_str,
message: error_message,
});
}
} else if let Some(uuid) = parsed.volume_uuid {
Ok(Some(uuid))
} else {
Err(ActionErrorKind::command_output(&command, output))
}
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
struct DiskUtilApfsInfoOutput {
#[serde(rename = "ErrorMessage")]
error_message: Option<String>,
#[serde(rename = "VolumeUUID")]
volume_uuid: Uuid,
volume_uuid: Option<Uuid>,
}

View file

@ -97,15 +97,38 @@ impl Action for UnmountApfsVolume {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
execute_command(
Command::new("/usr/sbin/diskutil")
.process_group(0)
.args(["unmount", "force"])
.arg(&self.name)
.stdin(std::process::Stdio::null()),
)
.await
.map_err(Self::error)?;
let Self { disk: _, name } = self;
let currently_mounted = {
let buf = execute_command(
Command::new("/usr/sbin/diskutil")
.process_group(0)
.args(["info", "-plist"])
.arg(&name)
.stdin(std::process::Stdio::null()),
)
.await
.map_err(Self::error)?
.stdout;
let the_plist: DiskUtilInfoOutput =
plist::from_reader(Cursor::new(buf)).map_err(|e| Self::error(e))?;
the_plist.mount_point.is_some()
};
if !currently_mounted {
execute_command(
Command::new("/usr/sbin/diskutil")
.process_group(0)
.args(["unmount", "force"])
.arg(name)
.stdin(std::process::Stdio::null()),
)
.await
.map_err(Self::error)?;
} else {
tracing::debug!("Volume was already unmounted, can skip unmounting")
}
Ok(())
}

View file

@ -531,6 +531,8 @@ pub enum ActionErrorKind {
See https://github.com/DeterminateSystems/nix-installer#without-systemd-linux-only for documentation on usage and drawbacks.\
")]
SystemdMissing,
#[error("`{command}` failed, message: {message}")]
DiskUtilInfoError { command: String, message: String },
}
impl ActionErrorKind {

View file

@ -58,7 +58,8 @@
"apfs_volume_label": "Nix Store",
"mount_service_label": "org.nixos.darwin-store",
"mount_point": "/nix",
"encrypt": false
"encrypt": false,
"needs_bootout": false
},
"state": "Uncompleted"
},