use crate::{AdoptedIdentity, IdentityCheck, OwnedChild, OwnedCommand, StartToken, SweepReport};
use std::{
fs::{File, OpenOptions},
io::{self, Write},
num::NonZeroU32,
path::{Path, PathBuf},
time::Duration,
};
const MAGIC: &str = "RKJ1";
#[derive(Debug)]
pub struct WorkerJournal {
path: PathBuf,
file: File,
owner: AdoptedIdentity,
}
impl WorkerJournal {
pub fn open(path: impl Into<PathBuf>) -> io::Result<Self> {
let path = path.into();
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
std::fs::create_dir_all(parent)?;
}
let owner = own_identity()?;
let file = OpenOptions::new().create(true).append(true).open(&path)?;
Ok(Self { path, file, owner })
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn record(&mut self, worker: &AdoptedIdentity) -> io::Result<()> {
let start = worker.start_token();
let owner = self.owner.start_token();
let exe = worker.exe().map_or_else(
|| "-".to_owned(),
|exe| hex(exe.to_string_lossy().as_bytes()),
);
self.append(&format!(
"{MAGIC} S {} {} {} {} {} {} {exe}\n",
worker.pid(),
start.major,
start.minor,
self.owner.pid(),
owner.major,
owner.minor,
))
}
pub fn record_child(&mut self, child: &OwnedChild) -> io::Result<AdoptedIdentity> {
let captured = child.while_unreaped(|pid| {
NonZeroU32::new(pid).map_or(Ok(None), AdoptedIdentity::capture)
})?;
let identity = captured.transpose()?.flatten().ok_or_else(|| {
io::Error::other("the child could not be identified for the worker journal")
})?;
self.record(&identity)?;
Ok(identity)
}
pub fn finish(&mut self, worker: &AdoptedIdentity) -> io::Result<()> {
let start = worker.start_token();
self.append(&format!(
"{MAGIC} E {} {} {}\n",
worker.pid(),
start.major,
start.minor
))
}
pub fn reset(&mut self, keep: &[AdoptedIdentity]) -> io::Result<()> {
let mut temporary = self.path.clone().into_os_string();
temporary.push(".new");
let temporary = PathBuf::from(temporary);
{
let mut file = File::create(&temporary)?;
let owner = self.owner.start_token();
for worker in keep {
let start = worker.start_token();
let exe = worker.exe().map_or_else(
|| "-".to_owned(),
|exe| hex(exe.to_string_lossy().as_bytes()),
);
writeln!(
file,
"{MAGIC} S {} {} {} {} {} {} {exe}",
worker.pid(),
start.major,
start.minor,
self.owner.pid(),
owner.major,
owner.minor,
)?;
}
file.sync_all()?;
}
std::fs::rename(&temporary, &self.path)?;
self.file = OpenOptions::new().append(true).open(&self.path)?;
Ok(())
}
fn append(&mut self, line: &str) -> io::Result<()> {
self.file.write_all(line.as_bytes())?;
self.file.sync_data()
}
}
impl OwnedCommand {
pub fn spawn_journaled(self, journal: &mut WorkerJournal) -> io::Result<OwnedChild> {
let mut child = self.spawn_suspended()?;
let released = journal
.record_child(&child)
.map(|_| ())
.and_then(|()| child.resume());
match released {
Ok(()) => Ok(child),
Err(error) => {
let _ = child.terminate_tree_bounded(Duration::from_secs(2));
Err(error)
}
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct JournalRecovery {
pub orphans: Vec<AdoptedIdentity>,
pub held: Vec<AdoptedIdentity>,
pub gone: usize,
pub corrupt_lines: usize,
pub torn_tail: bool,
}
impl JournalRecovery {
pub fn terminate_orphans(&self, grace: Duration) -> io::Result<SweepReport> {
crate::sweep(&self.orphans, grace)
}
}
pub fn recover(path: impl AsRef<Path>) -> io::Result<JournalRecovery> {
let bytes = match std::fs::read(path.as_ref()) {
Ok(bytes) => bytes,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Ok(JournalRecovery::default())
}
Err(error) => return Err(error),
};
let mut recovery = JournalRecovery::default();
let text = String::from_utf8_lossy(&bytes);
let complete = match text.rfind('\n') {
Some(end) => {
recovery.torn_tail = end + 1 != text.len();
&text[..=end]
}
None => {
recovery.torn_tail = !text.is_empty();
""
}
};
let mut active: Vec<Active> = Vec::new();
for line in complete.lines() {
match parse(line) {
Some(Record::Start(entry)) => {
active.retain(|known| (known.pid, known.start) != (entry.pid, entry.start));
active.push(entry);
}
Some(Record::End { pid, start }) => {
active.retain(|known| (known.pid, known.start) != (pid, start));
}
None if line.trim().is_empty() => {}
None => recovery.corrupt_lines += 1,
}
}
for entry in active {
let worker = AdoptedIdentity::from_recorded(entry.pid, entry.start, entry.exe);
if worker.verify()? != IdentityCheck::Same || !worker.is_running()? {
recovery.gone += 1;
continue;
}
let owner = AdoptedIdentity::from_recorded(entry.owner_pid, entry.owner_start, None);
if owner.verify()? == IdentityCheck::Same && owner.is_running()? {
recovery.held.push(worker);
} else {
recovery.orphans.push(worker);
}
}
Ok(recovery)
}
#[derive(Debug)]
struct Active {
pid: NonZeroU32,
start: StartToken,
owner_pid: NonZeroU32,
owner_start: StartToken,
exe: Option<PathBuf>,
}
enum Record {
Start(Active),
End { pid: NonZeroU32, start: StartToken },
}
fn parse(line: &str) -> Option<Record> {
let mut fields = line.split(' ');
if fields.next()? != MAGIC {
return None;
}
let kind = fields.next()?;
let pid = NonZeroU32::new(fields.next()?.parse().ok()?)?;
let start = StartToken {
major: fields.next()?.parse().ok()?,
minor: fields.next()?.parse().ok()?,
};
match kind {
"E" => fields
.next()
.is_none()
.then_some(Record::End { pid, start }),
"S" => {
let owner_pid = NonZeroU32::new(fields.next()?.parse().ok()?)?;
let owner_start = StartToken {
major: fields.next()?.parse().ok()?,
minor: fields.next()?.parse().ok()?,
};
let exe = match fields.next()? {
"-" => None,
encoded => Some(PathBuf::from(unhex(encoded)?)),
};
fields.next().is_none().then_some(Record::Start(Active {
pid,
start,
owner_pid,
owner_start,
exe,
}))
}
_ => None,
}
}
fn own_identity() -> io::Result<AdoptedIdentity> {
let pid = NonZeroU32::new(std::process::id())
.ok_or_else(|| io::Error::other("this process has pid 0"))?;
AdoptedIdentity::capture(pid)?
.ok_or_else(|| io::Error::other("this process could not be described"))
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
fn unhex(text: &str) -> Option<String> {
if !text.len().is_multiple_of(2) {
return None;
}
let bytes: Option<Vec<u8>> = (0..text.len())
.step_by(2)
.map(|at| u8::from_str_radix(text.get(at..at + 2)?, 16).ok())
.collect();
String::from_utf8(bytes?).ok()
}