use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::tty::{Level, Notice};
use super::{migrate_config_text, migrate_persona_text, Migration};
static LOCK_CONTENTION_REPORTED: AtomicBool = AtomicBool::new(false);
#[cfg(any(test, feature = "test-util"))]
pub fn reset_lock_contention_warned_for_test() {
LOCK_CONTENTION_REPORTED.store(false, Ordering::Relaxed);
}
pub fn read_persona_file(
path: &Path,
report: &mut dyn FnMut(Notice<'static>),
) -> std::io::Result<String> {
read_migrating(
MigratingPath {
shown: path,
operated: path,
},
"persona",
migrate_persona_text,
true,
|p| std::fs::read_to_string(p),
|p, text| crate::atomic_fs::atomic_write(p, text.as_bytes()),
report,
)
}
pub fn read_config_file(
path: &Path,
rewrite: bool,
held_locks: &[&crate::atomic_fs::LockGuard],
report: &mut dyn FnMut(Notice<'static>),
) -> std::io::Result<String> {
let destination = rewrite
.then(|| crate::atomic_fs::ResolvedPath::resolve(path))
.transpose()
.map_err(|error| {
std::io::Error::other(format!(
"resolving config destination {}: {error:#}",
path.display()
))
})?;
let mut lock_failure: Option<String> = None;
let mut locked: Option<crate::atomic_fs::ResolvedPath> = None;
let mut _own_guard: Option<crate::atomic_fs::LockGuard> = None;
if let Some(destination) = &destination {
let held = held_locks
.iter()
.any(|guard| guard.lock_path() == destination.lock_path());
if !held {
match crate::atomic_fs::acquire_lock(&destination.lock_path()) {
Ok(guard) => {
_own_guard = Some(guard);
locked = Some(destination.clone());
}
Err(error) if !LOCK_CONTENTION_REPORTED.swap(true, Ordering::Relaxed) => {
lock_failure = Some(format!(
"cannot lock config {} for migration: {error:#}",
path.display()
));
}
Err(_) => {}
}
} else {
locked = Some(destination.clone());
}
}
let operated = locked
.as_ref()
.map_or(path, crate::atomic_fs::ResolvedPath::as_path);
let mut migration_reported = false;
let result = read_migrating(
MigratingPath {
shown: path,
operated,
},
"config",
migrate_config_text,
locked.is_some(),
|p| std::fs::read_to_string(p),
|_, text| {
let destination = locked.as_ref().expect("rewrite holds the config lock");
destination.atomic_write(text.as_bytes())
},
&mut |notice| {
if notice.text.contains("translated original text")
|| notice.text.contains("migrated config")
|| notice.text.contains("migration")
{
migration_reported = true;
}
let mut notice = notice;
if let Some(error) = &lock_failure {
notice.text = format!("{}; {error}", notice.text).into();
}
report(notice);
},
);
if !migration_reported {
if let Some(lock_failure) = lock_failure.as_ref() {
let message = match &result {
Ok(_) => format!("newt: warning: {lock_failure}; loaded original text in memory"),
Err(read_error) => format!(
"newt: warning: {lock_failure}; could not read original text: {read_error}"
),
};
report(Notice::new(Level::Warn, "", message));
}
}
result
}
pub(super) struct MigratingPath<'a> {
pub(super) shown: &'a Path,
pub(super) operated: &'a Path,
}
pub(super) fn read_migrating(
path: MigratingPath<'_>,
kind: &str,
migrate: fn(&str) -> Option<Migration>,
rewrite: bool,
mut read: impl FnMut(&Path) -> std::io::Result<String>,
write: impl FnOnce(&Path, &str) -> anyhow::Result<()>,
report: &mut dyn FnMut(Notice<'static>),
) -> std::io::Result<String> {
let MigratingPath { shown, operated } = path;
let raw = read(operated)?;
let Some(migration) = migrate(&raw) else {
return Ok(raw);
};
let changes = migration.changes.join(", ");
let shown = shown.display();
if !rewrite {
report(Notice::new(
Level::Warn,
"",
format!(
"newt: warning: {kind} {shown} uses old psyche labels ({changes}); \
loading translated original text in memory, the file is unchanged"
),
));
return Ok(migration.text);
}
match read(operated) {
Ok(current) if current != raw => {
let latest = migrate(¤t);
let changes = latest.as_ref().map(|m| m.changes.join(", "));
let detail = changes.map(|c| format!(" ({c})")).unwrap_or_default();
report(Notice::new(
Level::Warn,
"",
format!(
"newt: warning: {kind} {shown} changed during migration; \
loading latest text in memory{detail}, the file is unchanged"
),
));
return Ok(latest.map_or(current, |latest| latest.text));
}
Err(error) => {
report(Notice::new(
Level::Warn,
"",
format!(
"newt: warning: cannot re-read {kind} {shown} before migration: {error}; \
loading translated original text ({changes}), the file is unchanged"
),
));
return Ok(migration.text);
}
Ok(_) => {}
}
let notice = match write(operated, &migration.text) {
Ok(()) => Notice::new(
Level::Ok,
"",
format!("newt: migrated {kind} {shown}: {changes}; loaded translated original text"),
),
Err(error) => Notice::new(
Level::Warn,
"",
format!(
"newt: warning: {kind} {shown} uses old psyche labels ({changes}); \
loading translated original text, but migration write did not complete: {error:#}; \
config changes from this command were not saved"
),
),
};
report(notice);
Ok(migration.text)
}
#[cfg(test)]
#[path = "psyche_import_notice_tests.rs"]
mod tests;