use std::path::{Path, PathBuf};
use alba_core::{BeamId, Project, SourceMap};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use tokio_util::sync::CancellationToken;
use crate::EngineError;
use crate::affected::{Selection, select};
use crate::event::RunEvent;
use crate::scheduler::{Executors, RunOptions, run};
use crate::watch::set::{Relevance, WatchSet};
use crate::watch::{SessionError, WatchBatch, WatchExit, Watcher, beamfile_dir, display_paths};
#[derive(Debug)]
pub enum SessionCommand {
RunBeam { id: BeamId, force: bool },
CancelRun,
SetWatch(bool),
Shutdown,
}
#[allow(clippy::too_many_arguments)]
pub async fn session(
beamfile: &Path,
mut project: Project,
mut sources: SourceMap,
mut selection: Selection,
options: RunOptions,
executors: Executors,
events: UnboundedSender<RunEvent>,
mut commands: Option<UnboundedReceiver<SessionCommand>>,
mut watch_enabled: bool,
cancel: CancellationToken,
mut watcher: Box<dyn Watcher>,
render_error: &mut (dyn FnMut(&SessionError) -> String + Send),
) -> WatchExit {
let root = beamfile_dir(beamfile);
let mut force_next = options.cache.as_ref().is_some_and(|cache| cache.force);
loop {
let mut set = match WatchSet::new(&project, selection.watched_target(), &sources) {
Ok(set) => set,
Err(error) => {
let session_error = SessionError::Run {
error: EngineError::Core(error),
sources: sources.clone(),
};
let _ = events.send(RunEvent::ProjectBroken {
diagnostic: render_error(&session_error),
});
match park_until_the_project_changes(
beamfile,
sources.clone(),
&events,
&mut *watcher,
&cancel,
&mut commands,
&mut watch_enabled,
render_error,
)
.await
{
Reloaded::Project {
project: fresh_project,
sources: fresh_sources,
commanded,
} => {
(project, sources) = (fresh_project, fresh_sources);
match commanded {
Some((id, force)) => {
(selection, force_next) = (Selection::Beam(id), force);
}
None => announce_recovery(&events),
}
continue;
}
Reloaded::Exit(exit) => return exit,
}
}
};
let targets = match select(&project, &sources, &root, &selection) {
Ok(targets) => targets,
Err(error) => {
let session_error = SessionError::Run {
error,
sources: sources.clone(),
};
let _ = events.send(RunEvent::ProjectBroken {
diagnostic: render_error(&session_error),
});
match park_until_the_project_changes(
beamfile,
sources.clone(),
&events,
&mut *watcher,
&cancel,
&mut commands,
&mut watch_enabled,
render_error,
)
.await
{
Reloaded::Project {
project: fresh_project,
sources: fresh_sources,
commanded,
} => {
(project, sources) = (fresh_project, fresh_sources);
match commanded {
Some((id, force)) => {
(selection, force_next) = (Selection::Beam(id), force);
}
None => announce_recovery(&events),
}
continue;
}
Reloaded::Exit(exit) => return exit,
}
}
};
let run_cancel = cancel.child_token();
let mut run_options = options.clone();
if let Some(cache) = run_options.cache.as_mut() {
cache.force = force_next;
}
force_next = false;
let mut pending: Option<Trigger> = None;
let mut commanded: Option<(BeamId, bool)> = None;
let result = {
let run_future = run(
&project,
&targets,
run_options,
executors.clone(),
events.clone(),
run_cancel.clone(),
);
tokio::pin!(run_future);
loop {
tokio::select! {
result = &mut run_future => break result,
command = next_command(&mut commands) => match command {
SessionCommand::RunBeam { id, force } => {
commanded = Some((id, force));
run_cancel.cancel();
}
SessionCommand::CancelRun => run_cancel.cancel(),
SessionCommand::SetWatch(enabled) => watch_enabled = enabled,
SessionCommand::Shutdown => {
run_cancel.cancel();
let _ = (&mut run_future).await;
return WatchExit::Interrupted;
}
},
batch = watcher.next_batch() => match batch {
Some(batch) => {
if watch_enabled && let Some(trigger) = relevant(&mut set, batch) {
merge(&mut pending, trigger);
run_cancel.cancel();
}
}
None => {
run_cancel.cancel();
let _ = (&mut run_future).await;
return WatchExit::WatcherClosed;
}
},
}
}
};
if let Err(error) = result {
let session_error = SessionError::Run {
error,
sources: sources.clone(),
};
let _ = events.send(RunEvent::ProjectBroken {
diagnostic: render_error(&session_error),
});
}
if cancel.is_cancelled() {
return WatchExit::Interrupted;
}
let commanded_run = commanded.is_some();
if let Some((id, force)) = commanded {
(selection, force_next) = (Selection::Beam(id), force);
}
let trigger = match pending.take() {
Some(trigger) => Some(trigger),
None if commanded_run => None,
None => {
if watch_enabled {
let _ = events.send(RunEvent::WatchWaiting {
files: set.file_count(),
});
}
loop {
tokio::select! {
() = cancel.cancelled() => return WatchExit::Interrupted,
command = next_command(&mut commands) => match command {
SessionCommand::RunBeam { id, force } => {
(selection, force_next) = (Selection::Beam(id), force);
break None;
}
SessionCommand::CancelRun => {}
SessionCommand::SetWatch(enabled) => {
watch_enabled = enabled;
if enabled {
let _ = events.send(RunEvent::WatchWaiting {
files: set.file_count(),
});
}
}
SessionCommand::Shutdown => return WatchExit::Interrupted,
},
batch = watcher.next_batch() => match batch {
Some(batch) => {
if watch_enabled && let Some(trigger) = relevant(&mut set, batch) {
break Some(trigger);
}
}
None => return WatchExit::WatcherClosed,
},
}
}
}
};
let Some(trigger) = trigger else { continue };
let _ = events.send(RunEvent::WatchTriggered {
paths: display_paths(&root, &trigger.paths),
});
if trigger.beamfile {
match alba_core::load_project(beamfile) {
Ok((fresh_project, fresh_sources)) => {
(project, sources) = (fresh_project, fresh_sources);
}
Err(error) => {
let rejected = error.sources.clone();
let session_error = SessionError::Load(error);
let _ = events.send(RunEvent::ProjectBroken {
diagnostic: render_error(&session_error),
});
match park_until_the_project_changes(
beamfile,
rejected,
&events,
&mut *watcher,
&cancel,
&mut commands,
&mut watch_enabled,
render_error,
)
.await
{
Reloaded::Project {
project: fresh_project,
sources: fresh_sources,
commanded,
} => {
(project, sources) = (fresh_project, fresh_sources);
match commanded {
Some((id, force)) => {
(selection, force_next) = (Selection::Beam(id), force);
}
None => announce_recovery(&events),
}
}
Reloaded::Exit(exit) => return exit,
}
}
}
}
}
}
async fn next_command(commands: &mut Option<UnboundedReceiver<SessionCommand>>) -> SessionCommand {
match commands {
Some(receiver) => receiver.recv().await.unwrap_or(SessionCommand::Shutdown),
None => std::future::pending().await,
}
}
struct Trigger {
paths: Vec<PathBuf>,
beamfile: bool,
}
fn relevant(set: &mut WatchSet, batch: WatchBatch) -> Option<Trigger> {
match batch {
WatchBatch::Rescan => Some(Trigger {
paths: Vec::new(),
beamfile: true,
}),
WatchBatch::Paths(paths) => {
let mut trigger = Trigger {
paths: Vec::new(),
beamfile: false,
};
for path in paths {
match set.classify(&path) {
Relevance::Beamfile => {
trigger.beamfile = true;
trigger.paths.push(path);
}
Relevance::Input => trigger.paths.push(path),
Relevance::Irrelevant => {}
}
}
(!trigger.paths.is_empty()).then_some(trigger)
}
}
}
fn merge(pending: &mut Option<Trigger>, fresh: Trigger) {
match pending {
Some(existing) => {
existing.beamfile |= fresh.beamfile;
for path in fresh.paths {
if !existing.paths.contains(&path) {
existing.paths.push(path);
}
}
}
None => *pending = Some(fresh),
}
}
fn announce_recovery(events: &UnboundedSender<RunEvent>) {
let _ = events.send(RunEvent::WatchTriggered { paths: Vec::new() });
}
enum Reloaded {
Project {
project: Project,
sources: SourceMap,
commanded: Option<(BeamId, bool)>,
},
Exit(WatchExit),
}
fn retry_load(
beamfile: &Path,
rejected: &mut SourceMap,
events: &UnboundedSender<RunEvent>,
render_error: &mut (dyn FnMut(&SessionError) -> String + Send),
) -> Option<(Project, SourceMap)> {
match alba_core::load_project(beamfile) {
Ok(loaded) => Some(loaded),
Err(error) => {
if error.sources != *rejected {
*rejected = error.sources.clone();
let session_error = SessionError::Load(error);
let _ = events.send(RunEvent::ProjectBroken {
diagnostic: render_error(&session_error),
});
}
None
}
}
}
#[allow(clippy::too_many_arguments)]
async fn park_until_the_project_changes(
beamfile: &Path,
mut rejected: SourceMap,
events: &UnboundedSender<RunEvent>,
watcher: &mut dyn Watcher,
cancel: &CancellationToken,
commands: &mut Option<UnboundedReceiver<SessionCommand>>,
watch_enabled: &mut bool,
render_error: &mut (dyn FnMut(&SessionError) -> String + Send),
) -> Reloaded {
let _ = events.send(RunEvent::WatchWaiting { files: 0 });
loop {
tokio::select! {
() = cancel.cancelled() => return Reloaded::Exit(WatchExit::Interrupted),
command = next_command(commands) => match command {
SessionCommand::RunBeam { id, force } => {
if let Some((project, sources)) =
retry_load(beamfile, &mut rejected, events, render_error)
{
return Reloaded::Project {
project,
sources,
commanded: Some((id, force)),
};
}
}
SessionCommand::CancelRun => {}
SessionCommand::SetWatch(enabled) => *watch_enabled = enabled,
SessionCommand::Shutdown => return Reloaded::Exit(WatchExit::Interrupted),
},
batch = watcher.next_batch() => match batch {
Some(_) => {
if let Some((project, sources)) =
retry_load(beamfile, &mut rejected, events, render_error)
&& sources != rejected
{
return Reloaded::Project {
project,
sources,
commanded: None,
};
}
}
None => return Reloaded::Exit(WatchExit::WatcherClosed),
},
}
}
}