use std::fmt::Write as _;
use std::io::{ErrorKind, IsTerminal};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_log::{error, warn};
use rig_harness_protocol::{Home, Invocation, RunDir, env};
use super::cli;
use super::exit::{Exit, ExitReason, reason_from_exit};
use super::session::leave_session;
use crate::describe::{Describing, describing};
use crate::secrets::Secrets;
use crate::settings::Settings;
use rig_core::conversation::Author;
use rig_ecs::agent::Notice;
use rig_ecs::tasks::Wake;
use crate::main_agent::{MainAgentQuery, main_agent};
use rig_ecs::inbox::{Deliver, DeliveryMode};
use rig_ecs::session_log::SessionRestored;
pub const BUILD_ORIGIN: &str = "build";
#[derive(Resource, Clone, Debug, Default)]
pub struct Invoked {
pub args: Invocation,
pub terminal: bool,
}
impl Invoked {
pub fn from_env() -> Self {
let args = Invocation::from_env().unwrap_or_else(|failure| {
eprintln!("rig: {failure}; starting as usual");
Invocation::default()
});
Self {
args,
terminal: std::io::stdin().is_terminal(),
}
}
pub fn interactive(&self) -> bool {
!self.args.is_headless() && self.terminal
}
}
#[derive(Resource, Clone, Debug)]
pub struct LauncherRun(pub RunDir);
pub struct LauncherPlugin;
impl Plugin for LauncherPlugin {
fn build(&self, app: &mut App) {
if !app.world().contains_resource::<Invoked>() {
app.insert_resource(Invoked::from_env());
}
if !app.world().contains_resource::<LauncherRun>()
&& let Some(run) = RunDir::from_env()
{
app.insert_resource(LauncherRun(run));
watch_launcher(app);
}
if !app.world().contains_resource::<Secrets>() {
app.insert_resource(Secrets::from_env());
}
if describing().is_some() {
app.insert_resource(Describing);
}
app.add_systems(Startup, launcher_notice)
.add_systems(
Update,
deliver_build_failure
.run_if(resource_exists::<SessionRestored>)
.run_if(resource_exists::<StartBuildFailure>),
)
.add_systems(Last, signal_ready.before(reason_from_exit));
}
fn cleanup(&self, app: &mut App) {
if let Some(refusal) = cli::refusal(app) {
refuse(app, refusal);
}
}
}
pub(crate) fn watch_launcher(app: &mut App) {
app.add_systems(PostStartup, start_watching)
.add_systems(First, exit_when_launcher_gone);
}
#[derive(Resource)]
struct LauncherGone(Arc<AtomicBool>);
fn start_watching(run: Option<Res<LauncherRun>>, wake: Option<Res<Wake>>, mut commands: Commands) {
let Some(run) = run else {
return;
};
let gone = Arc::new(AtomicBool::new(false));
let (lock, set, wake) = (run.0.lock(), gone.clone(), wake.map(|wake| wake.clone()));
let watching = std::thread::Builder::new()
.name("rig-launcher-watch".to_owned())
.spawn(move || {
if launcher_left(&lock) {
set.store(true, Ordering::Release);
if let Some(wake) = wake {
wake.wake();
}
}
});
match watching {
Ok(_) => commands.insert_resource(LauncherGone(gone)),
Err(failure) => warn!("the agent will not exit with its launcher: {failure}"),
}
}
fn launcher_left(lock: &Path) -> bool {
let file = match rig_harness_protocol::lock_file(lock, false) {
Ok(file) => file,
Err(failure) if failure.kind() == ErrorKind::NotFound => return true,
Err(failure) => {
warn!("the agent will not exit with its launcher: {failure}");
return false;
}
};
file.lock()
.inspect_err(|failure| warn!("the agent will not exit with its launcher: {failure}"))
.is_ok()
}
fn exit_when_launcher_gone(
gone: Option<Res<LauncherGone>>,
mut asked: Local<bool>,
mut commands: Commands,
) {
if !*asked && gone.is_some_and(|gone| gone.0.load(Ordering::Acquire)) {
*asked = true;
commands.trigger(Exit(ExitReason::Signal));
}
}
#[derive(Resource)]
struct Refused;
pub(crate) fn refuse(app: &mut App, why: String) {
if app.world().contains_resource::<Refused>() {
return;
}
leave_session(app.world_mut());
let stop = if why.ends_with('.') { "" } else { "." };
let said = format!("rig: {why}{stop}\n");
eprint!("{said}");
if let Some(run) = app.world().get_resource::<LauncherRun>()
&& let Err(failure) = std::fs::write(run.0.refusal(), said)
{
error!("could not tell the launcher why the run is refused: {failure}");
}
app.insert_resource(Refused).add_systems(
Last,
exit_refused
.after(signal_ready)
.run_if(resource_exists::<Refused>),
);
}
fn exit_refused(mut commands: Commands) {
commands.remove_resource::<Refused>();
commands.trigger(Exit(ExitReason::Failed(2)));
}
#[derive(Resource)]
struct StartBuildFailure(String);
pub fn build_failure_message(what: &str, summary: &str) -> String {
let home = Home::from_env();
let project = home.project();
let mut message = format!(
"Building the agent ({what}) failed; the previous build keeps running.\n\n{}\n\n\
Whole build output: {}\n\
Plugin list: {}\n\
Generated project: {} and {}",
summary.trim(),
home.build_log().display(),
home.plugins_toml().display(),
project.join("Cargo.toml").display(),
project.join("src/main.rs").display(),
);
if let Some(source) = std::env::var_os(env::SOURCE).filter(|source| !source.is_empty()) {
let _ = write!(
message,
"\nrig-harness source (RIG_SOURCE): {}",
std::path::Path::new(&source).display()
);
}
message
}
pub fn inform_build_failure(commands: &mut Commands, agent: Entity, text: String) {
let inform = Deliver::new(agent, text, DeliveryMode::Inform);
commands.trigger(inform.with_author(Author::plugin(BUILD_ORIGIN)));
}
fn launcher_notice(
settings: Option<Res<Settings>>,
mut notices: MessageWriter<Notice>,
mut commands: Commands,
) {
if let Ok(notice) = std::env::var(env::NOTICE)
&& !notice.is_empty()
{
notices.write(Notice::info(None, notice));
}
for problem in settings.iter().flat_map(|settings| settings.problems()) {
notices.write(Notice::error(None, problem.clone()));
}
if let Ok(failure) = std::env::var(env::BUILD_FAILURE)
&& !failure.trim().is_empty()
{
commands.insert_resource(StartBuildFailure(failure));
}
}
fn deliver_build_failure(
failure: Res<StartBuildFailure>,
agents: MainAgentQuery,
mut commands: Commands,
) {
let Some(agent) = main_agent(&agents) else {
return;
};
inform_build_failure(
&mut commands,
agent,
build_failure_message("before this start", &failure.0),
);
commands.remove_resource::<StartBuildFailure>();
}
fn signal_ready(
mut signalled: Local<bool>,
exits: MessageReader<AppExit>,
reason: Option<Res<ExitReason>>,
run: Option<Res<LauncherRun>>,
) {
if *signalled || (!exits.is_empty() && reason.is_none()) {
return;
}
*signalled = true;
let Some(run) = run else {
return;
};
if let Err(failure) = std::fs::write(run.0.ready(), b"") {
error!("could not write the ready file: {failure}");
}
}
#[cfg(test)]
mod tests;