use std::fs;
use std::path::Path;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::watch;
use tokio::task::JoinSet;
use super::attend::{Attendance, attend};
use super::watch::Watch;
use super::wire::{self, Announced, Control, Pipe};
use super::{Attended, Kept, Layout, Rig, Shell};
use crate::failure::{Doing, Failure};
pub(super) struct Session<'r, R: Rig> {
rig: &'r R,
pub(super) layout: Layout,
control: Control,
attending: JoinSet<Result<Attendance<Kept<R>>, Failure>>,
closing: watch::Sender<bool>,
joined: usize,
done: Vec<Attended<Kept<R>>>,
_lock: Lock,
_temporary: Option<TempDir>,
}
impl<'r, R: Rig> Session<'r, R> {
pub(super) fn open(rig: &'r R, at: Option<&Path>) -> Result<Self, Failure> {
let (dir, temporary) = match at {
Some(at) => {
let dir = fs::canonicalize(at).doing(|| {
format!(
"opening the prescribed workspace {}",
at.display()
)
})?;
(dir, None)
}
None => {
let temp = tempfile::tempdir().doing(|| "opening a workspace for the run".into())?;
let dir = fs::canonicalize(temp.path()).doing(|| "opening a workspace for the run".into())?;
(dir, Some(temp))
}
};
raise_descriptor_limit()?;
let layout = Layout::new(dir)?;
let lock = Lock::hold(&layout)?;
sweep(&layout)?;
let control = Control::open(layout.clone())?;
wire::lay(&layout, &rig.bash(&layout))?;
let (closing, _) = watch::channel(false);
Ok(Self {
rig,
layout,
control,
attending: JoinSet::new(),
closing,
joined: 0,
done: Vec::new(),
_lock: lock,
_temporary: temporary,
})
}
pub(super) async fn serve(&mut self, watch: &Watch) -> Result<(), Failure> {
loop {
tokio::select! {
biased;
announced = self.control.next() => self.announced(announced?).await?,
Some(done) = self.attending.join_next() => {
let Attendance { attended, cut } = finished(done)?;
self.done.push(attended);
if let Some(why) = cut {
return Err(why);
}
}
fired = watch.fired() => return fired,
}
}
}
async fn announced(&mut self, Announced { token, account }: Announced) -> Result<(), Failure> {
let (up, rep) = (
self.layout.up(&token),
self.layout.rep(&token),
);
wire::mkfifo(Path::new(&rep))?;
let pipe = Pipe::open(up.into(), rep.into())?;
let shell = Arc::new(Shell::of(self.joined, account)?);
self.joined += 1;
let reaction = self.rig.joined(&self.layout, Arc::clone(&shell)).await?;
self.attending.spawn_local(attend(
shell,
pipe,
reaction,
self.closing.subscribe(),
));
Ok(())
}
pub(super) async fn close(mut self) -> (Vec<Attended<Kept<R>>>, Option<Failure>) {
let mut failed = self.control.close().err();
let _ = self.closing.send(true);
while let Some(done) = self.attending.join_next().await {
match finished(done) {
Ok(Attendance { attended, cut }) => {
self.done.push(attended);
failed = failed.or(cut);
}
Err(why) => failed = failed.or(Some(why)),
}
}
self.done.sort_by_key(|at| at.shell.nth);
(self.done, failed)
}
}
fn finished<K>(done: Result<Result<Attendance<K>, Failure>, tokio::task::JoinError>) -> Result<Attendance<K>, Failure> {
match done {
Ok(outcome) => outcome,
Err(join) => std::panic::resume_unwind(join.into_panic()),
}
}
struct Lock {
_file: fs::File,
}
impl Lock {
fn hold(at: &Layout) -> Result<Self, Failure> {
use std::os::fd::AsRawFd;
let holding = || format!("holding the workspace {}", at.text());
let file = fs::File::create(at.lock()).doing(holding)?;
if unsafe {
libc::flock(
file.as_raw_fd(),
libc::LOCK_EX | libc::LOCK_NB,
)
} < 0
{
let error = std::io::Error::last_os_error();
return match error.kind() {
std::io::ErrorKind::WouldBlock => Err(Failure::new(
holding(),
"it is already held by a live session",
)),
_ => Err(error).doing(holding),
};
}
Ok(Self { _file: file })
}
}
fn sweep(at: &Layout) -> Result<(), Failure> {
use std::os::unix::fs::FileTypeExt;
let sweeping = || format!("sweeping the workspace {}", at.text());
for entry in fs::read_dir(at.path()).doing(sweeping)? {
let entry = entry.doing(sweeping)?;
if entry.file_type().doing(sweeping)?.is_fifo() {
fs::remove_file(entry.path()).doing(|| {
format!(
"removing the stale fifo {}",
entry.path().display()
)
})?;
}
}
Ok(())
}
fn raise_descriptor_limit() -> Result<(), Failure> {
let raising = || "raising the descriptor limit".to_string();
let mut limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) } < 0 {
return Err(std::io::Error::last_os_error()).doing(raising);
}
limit.rlim_cur = limit.rlim_max;
if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limit) } < 0 {
return Err(std::io::Error::last_os_error()).doing(raising);
}
Ok(())
}