forked from lix-project/lix-installer
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:
parent
16ddada7a3
commit
cb48a7261b
7 changed files with 251 additions and 85 deletions
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
3
tests/fixtures/macos/macos.json
vendored
3
tests/fixtures/macos/macos.json
vendored
|
@ -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"
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue