use std::io::{self, Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::Poll;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_json::{Value, json};
use super::protocol::Version;
use super::storage::{Catalogue, Failure};
pub fn follow(
catalogue: &Catalogue,
branch: &str,
expires: Option<u64>,
clock: impl Fn() -> SystemTime,
send: impl FnMut(&Value) -> io::Result<()>,
) -> io::Result<Failure> {
follow_cancellable(
catalogue,
branch,
expires,
clock,
&AtomicBool::new(false),
send,
)
}
pub(in crate::serve) fn follow_cancellable(
catalogue: &Catalogue,
branch: &str,
expires: Option<u64>,
clock: impl Fn() -> SystemTime,
cancelled: &AtomicBool,
mut send: impl FnMut(&Value) -> io::Result<()>,
) -> io::Result<Failure> {
let mut seen = catalogue.changes();
let mut last = match catalogue.head(branch) {
Ok(Some(version)) => version,
Ok(None) => return Ok(unknown(branch)),
Err(failure) => return Ok(failure),
};
if let Err(failure) = remaining(expires, clock()) {
return Ok(failure);
}
send(&head(branch, last))?;
loop {
connected(cancelled)?;
let left = match remaining(expires, clock()) {
Ok(left) => left,
Err(failure) => return Ok(failure),
};
seen = catalogue.changed_past_or_closed(seen, left, cancelled);
connected(cancelled)?;
if let Err(failure) = remaining(expires, clock()) {
return Ok(failure);
}
match catalogue.head(branch) {
Ok(Some(version)) if version != last => {
send(&head(branch, version))?;
last = version;
}
Ok(Some(_)) => {}
Ok(None) => return Ok(unknown(branch)),
Err(failure) => return Ok(failure),
}
}
}
fn connected(cancelled: &AtomicBool) -> io::Result<()> {
if cancelled.load(Ordering::Acquire) {
Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"follow client closed",
))
} else {
Ok(())
}
}
pub(crate) struct Monitor {
stop: UnixStream,
worker: Option<std::thread::JoinHandle<()>>,
}
impl Monitor {
pub(crate) fn start(
peer: impl AsRawFd + Send + 'static,
closed: impl FnOnce() + Send + 'static,
) -> io::Result<Self> {
Self::start_observed(peer, closed, |_| Ok(()))
}
fn start_observed(
peer: impl AsRawFd + Send + 'static,
closed: impl FnOnce() + Send + 'static,
observed: impl Fn(bool) -> io::Result<()> + Send + 'static,
) -> io::Result<Self> {
use tokio::io::unix::AsyncFd;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let (stop, stopped) = UnixStream::pair()?;
stopped.set_nonblocking(true)?;
let (peer, stopped) = {
let _entered = runtime.enter();
(AsyncFd::new(peer)?, AsyncFd::new(stopped)?)
};
let worker = std::thread::Builder::new()
.name("haematite-follow-close".to_owned())
.spawn(move || {
let result: io::Result<bool> = runtime.block_on(std::future::poll_fn(|context| {
match stop_ready(&stopped, context) {
Poll::Ready(Ok(())) => return Poll::Ready(Ok(false)),
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
Poll::Pending => {}
}
loop {
match peer.poll_read_ready(context) {
Poll::Ready(Ok(mut ready)) => {
let hangup =
ready.ready().is_read_closed() || ready.ready().is_error();
observed(hangup)?;
if hangup {
return Poll::Ready(Ok(true));
}
ready.clear_ready();
}
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
Poll::Pending => return Poll::Pending,
}
}
}));
match result {
Ok(false) => {}
Ok(true) => closed(),
Err(error) => {
eprintln!("haematite follow_monitor_failed: {error}");
closed();
}
}
})?;
Ok(Self {
stop,
worker: Some(worker),
})
}
}
fn stop_ready(
signal: &tokio::io::unix::AsyncFd<UnixStream>,
context: &mut std::task::Context<'_>,
) -> Poll<io::Result<()>> {
loop {
let mut ready = std::task::ready!(signal.poll_read_ready(context))?;
let mut input = signal.get_ref();
match input.read(&mut [0]) {
Ok(_) => return Poll::Ready(Ok(())),
Err(error) if error.kind() == io::ErrorKind::WouldBlock => ready.clear_ready(),
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(error) => return Poll::Ready(Err(error)),
}
}
}
impl Drop for Monitor {
fn drop(&mut self) {
if let Err(error) = self.stop.write_all(&[1])
&& error.kind() != io::ErrorKind::BrokenPipe
{
eprintln!("haematite follow_monitor_stop_failed: {error}");
}
if let Some(worker) = self.worker.take()
&& worker.join().is_err()
{
eprintln!("haematite follow_monitor_panicked");
}
}
}
fn head(branch: &str, version: Version) -> Value {
json!({"branch": branch, "version": version})
}
fn unknown(branch: &str) -> Failure {
Failure::new(
"branch_unknown",
format!("branch {branch} is not published, so it has no head to follow"),
)
}
fn remaining(expires: Option<u64>, now: SystemTime) -> Result<Option<Duration>, Failure> {
let Some(expires) = expires else {
return Ok(None);
};
UNIX_EPOCH
.checked_add(Duration::from_secs(expires))
.and_then(|end| end.duration_since(now).ok())
.filter(|left| !left.is_zero())
.map(Some)
.ok_or_else(|| {
Failure::new(
"pass_expired",
"the Lys pass this connection presented has expired; follow again with a new one",
)
})
}
#[cfg(test)]
#[path = "follow_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "follow_hangup_tests.rs"]
mod hangup_tests;