pub mod service; pub mod task; use crate::apogee_core_v1; use crate::apogee_core_v1::apogee_service_logs_v1; use crate::{condition, ecs}; use bevy_ecs::prelude::*; use std::collections::{BTreeMap, HashSet, VecDeque}; use std::{process, sync, time}; #[derive(Message)] pub struct ServiceOutput { pub stream: apogee_core_v1::LogStream, pub process: ProcessId, pub line: sync::Arc, } pub fn store_service_output( mut reader: MessageReader, mut services: Query< (&mut Logs, &Process, &LogObjects), (With, Without), >, mut tasks: Query< (&mut Logs, &Process, &LogObjects), (With, Without), >, ) { for message in reader.read() { if let Some((mut logs, process, log_objects)) = services .iter_mut() .find(|(_, process, _)| process.id == message.process) .or_else(|| { tasks .iter_mut() .find(|(_, process, _)| process.id == message.process) }) { logs.push(process.id, sync::Arc::clone(&message.line), message.stream); for log_object in &log_objects.0 { log_object.send_entry(process.id.0, message.stream, &message.line); } } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ProcessId(pub u32); #[derive(Component, Debug, Default, PartialEq, Eq)] pub struct LogObjects(Vec); #[derive(Component, Default)] pub struct Timestamp(pub u64); pub fn unix_timestamp() -> u64 { time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(0, |duration| duration.as_secs()) } #[derive(Component)] pub struct Name(pub sync::Arc); #[derive(Component)] pub struct Description(pub String); #[derive(Component)] pub struct Command(pub String); #[derive(Component)] pub struct Args(pub Vec); #[derive(Component)] pub struct Conditions(pub Vec); #[derive(Component)] pub struct Running(pub Entity); #[derive(Component)] pub struct Environment(pub BTreeMap); pub struct LogEntry { process: ProcessId, message: sync::Arc, stream: apogee_core_v1::LogStream, } #[derive(Component, Default)] pub struct Logs(pub VecDeque); impl Logs { pub fn push( &mut self, process: ProcessId, line: sync::Arc, stream: apogee_core_v1::LogStream, ) { if self.0.len() == 10 { self.0.pop_front(); } self.0.push_back(LogEntry { process, message: line, stream, }); } } #[derive(Component)] pub struct Process { pub id: ProcessId, pub child: process::Child, pub token: calloop::RegistrationToken, pub notify_token: Option, } impl Process { pub fn remove_notify_source(&mut self, loop_handle: &ecs::LoopHandle) { if let Some(token) = self.notify_token.take() { loop_handle.0.remove(token); } } pub fn remove_sources(&mut self, loop_handle: &ecs::LoopHandle) { loop_handle.0.remove(self.token); self.remove_notify_source(loop_handle); } } fn wire_restart_delay(delay: u64) -> u32 { u32::try_from(delay).unwrap_or_else(|_| { log::warn!( "restart delay {delay} exceeds the Hyprwire uint range; advertising {}", u32::MAX ); u32::MAX }) } fn wire_timestamp(timestamp: u64) -> u32 { u32::try_from(timestamp).unwrap_or(u32::MAX) } #[allow(clippy::panic)] fn condition_name(entity: Entity, names: &Query<&condition::Name>) -> sync::Arc { sync::Arc::clone( &names .get(entity) .unwrap_or_else(|error| panic!("condition {entity:?} is missing its name: {error}")) .0, ) } fn condition_names(entities: &[Entity], names: &Query<&condition::Name>) -> Vec> { entities .iter() .map(|entity| condition_name(*entity, names)) .collect() } pub fn sync_objects( mut removed_processes: RemovedComponents, names: Query<&condition::Name>, mut services: Query< ( Entity, Ref, Ref, Ref, Ref, Ref, Ref, Ref, Ref, Ref, Ref, Ref, Ref, (Ref, Option>), &mut service::Objects, ), (With, Without), >, mut tasks: Query< ( Entity, Ref, Ref, Ref, Ref, Ref, Ref, Ref, Ref, Ref, (Ref, Option>), &mut task::Objects, ), (With, Without), >, ) { let removed_processes = removed_processes.read().collect::>(); for ( entity, name, description, command, args, conditions, ready, running, environment, restart_delay, restart_policy, restart_count, state, (timestamp, process), mut objects, ) in &mut services { objects .bypass_change_detection() .0 .retain(|object| object.try_client().is_some()); if objects.0.is_empty() { continue; } if objects.is_changed() { let condition_names = condition_names(&conditions.0, &names); let ready_name = condition_name(ready.0, &names); let running_name = condition_name(running.0, &names); let environment_names = environment.0.keys().map(String::as_str).collect::>(); let environment_values = environment .0 .values() .map(String::as_str) .collect::>(); for object in &objects.0 { object.send_name(&name.0); object.send_command(&command.0); object.send_user(rustix::process::getuid().to_string()); object.send_group(rustix::process::getgid().to_string()); object.send_args(&args.0); object.send_conditions(&condition_names); object.send_ready(&ready_name); object.send_running(&running_name); object.send_environment(&environment_names, &environment_values); object.send_restart_delay(wire_restart_delay(restart_delay.0)); object.send_restart_policy(*restart_policy); object.send_restart_count(restart_count.0); object.send_description(&description.0); object.send_state(*state); if let Some(process) = &process { object.send_pid(process.id.0); object.send_process_started(wire_timestamp(timestamp.0)); } else { object.send_process_stopped(); } object.send_done(); } continue; } let resolved_conditions = conditions .is_changed() .then(|| condition_names(&conditions.0, &names)); let ready_name = ready.is_changed().then(|| condition_name(ready.0, &names)); let running_name = running .is_changed() .then(|| condition_name(running.0, &names)); let environment_names = environment .is_changed() .then(|| environment.0.keys().map(String::as_str).collect::>()); let environment_values = environment.is_changed().then(|| { environment .0 .values() .map(String::as_str) .collect::>() }); let process_changed = removed_processes.contains(&entity) || process .as_ref() .is_some_and(bevy_ecs::change_detection::DetectChanges::is_changed) || timestamp.is_changed(); for object in &objects.0 { if name.is_changed() { object.send_name(&name.0); } if description.is_changed() { object.send_description(&description.0); } if command.is_changed() { object.send_command(&command.0); } if args.is_changed() { object.send_args(&args.0); } if let Some(conditions) = &resolved_conditions { object.send_conditions(conditions); } if let Some(ready_name) = &ready_name { object.send_ready(ready_name); } if let Some(running_name) = &running_name { object.send_running(running_name); } if let (Some(names), Some(values)) = (&environment_names, &environment_values) { object.send_environment(names, values); } if restart_delay.is_changed() { object.send_restart_delay(wire_restart_delay(restart_delay.0)); } if restart_policy.is_changed() { object.send_restart_policy(*restart_policy); } if restart_count.is_changed() { object.send_restart_count(restart_count.0); } if state.is_changed() { object.send_state(*state); } if process_changed { if let Some(process) = &process { object.send_pid(process.id.0); object.send_process_started(wire_timestamp(timestamp.0)); } else { object.send_process_stopped(); } } object.send_done(); } } for ( entity, name, description, command, args, conditions, successful, running, environment, state, (timestamp, process), mut objects, ) in &mut tasks { objects .bypass_change_detection() .0 .retain(|object| object.try_client().is_some()); if objects.0.is_empty() { continue; } if objects.is_changed() { let condition_names = condition_names(&conditions.0, &names); let ready_name = condition_name(successful.0, &names); let running_name = condition_name(running.0, &names); let environment_names = environment.0.keys().map(String::as_str).collect::>(); let environment_values = environment .0 .values() .map(String::as_str) .collect::>(); for object in &objects.0 { object.send_name(&name.0); object.send_command(&command.0); object.send_user(rustix::process::getuid().to_string()); object.send_group(rustix::process::getgid().to_string()); object.send_args(&args.0); object.send_conditions(&condition_names); object.send_successful(&ready_name); object.send_running(&running_name); object.send_environment(&environment_names, &environment_values); object.send_description(&description.0); object.send_state(*state); if let Some(process) = &process { object.send_pid(process.id.0); object.send_process_started(wire_timestamp(timestamp.0)); } else { object.send_process_stopped(); } object.send_done(); } continue; } let resolved_conditions = conditions .is_changed() .then(|| condition_names(&conditions.0, &names)); let ready_name = successful .is_changed() .then(|| condition_name(successful.0, &names)); let running_name = running .is_changed() .then(|| condition_name(running.0, &names)); let environment_names = environment .is_changed() .then(|| environment.0.keys().map(String::as_str).collect::>()); let environment_values = environment.is_changed().then(|| { environment .0 .values() .map(String::as_str) .collect::>() }); let process_changed = removed_processes.contains(&entity) || process .as_ref() .is_some_and(bevy_ecs::change_detection::DetectChanges::is_changed) || timestamp.is_changed(); for object in &objects.0 { if name.is_changed() { object.send_name(&name.0); } if description.is_changed() { object.send_description(&description.0); } if command.is_changed() { object.send_command(&command.0); } if args.is_changed() { object.send_args(&args.0); } if let Some(conditions) = &resolved_conditions { object.send_conditions(conditions); } if let Some(ready_name) = &ready_name { object.send_successful(ready_name); } if let Some(running_name) = &running_name { object.send_running(running_name); } if let (Some(names), Some(values)) = (&environment_names, &environment_values) { object.send_environment(names, values); } if state.is_changed() { object.send_state(*state); } if process_changed { if let Some(process) = &process { object.send_pid(process.id.0); object.send_process_started(wire_timestamp(timestamp.0)); } else { object.send_process_stopped(); } } object.send_done(); } } } #[derive(Event)] pub struct NewLogObject { entity: Entity, object: apogee_service_logs_v1::ApogeeServiceLogsV1, } pub fn handle_new_log_object( new_log_object: On, mut services: Query<(&Logs, &mut LogObjects), (With, Without)>, mut tasks: Query<(&Logs, &mut LogObjects), (With, Without)>, ) { let (logs, mut log_objects) = if let Ok(value) = services.get_mut(new_log_object.entity) { value } else if let Ok(value) = tasks.get_mut(new_log_object.entity) { value } else { return; }; for log in &logs.0 { new_log_object .object .send_entry(log.process.0, log.stream, &log.message); } log_objects.0.push(new_log_object.object.clone()); new_log_object.object.send_done(); } hyprwire::delegate_noop!(crate::Apogee: apogee_service_logs_v1::ApogeeServiceLogsV1); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StopReason { Exited, Restarted, Removed, }