use crate::unit::service; use crate::{condition, ecs}; use bevy_ecs::prelude::*; use calloop::generic; use rustix::pipe; use std::os::fd; use std::os::fd::FromRawFd; use std::os::linux::net::SocketAddrExt; use std::os::unix::net; use std::os::unix::process::CommandExt; use std::{io, mem, process}; pub trait Notify { fn configure_command( &mut self, loop_handle: &ecs::LoopHandle, entity: Entity, cmd: &mut process::Command, ) -> anyhow::Result>; } #[derive(Resource)] pub struct Systemd { pid: u32, next_id: u32, } impl Default for Systemd { fn default() -> Self { Self { pid: process::id(), next_id: 1, } } } impl Notify for Systemd { fn configure_command( &mut self, loop_handle: &ecs::LoopHandle, entity: Entity, cmd: &mut process::Command, ) -> anyhow::Result> { let name = format!("run/apogee/notify/{}/{}", self.pid, self.next_id); self.next_id += 1; let address = net::SocketAddr::from_abstract_name(name.as_bytes())?; let socket = net::UnixDatagram::bind_addr(&address)?; socket.set_nonblocking(true)?; cmd.env("NOTIFY_SOCKET", format!("@{name}")); let fd_wrapper = unsafe { generic::FdWrapper::new(socket) }; let source = generic::Generic::new( fd_wrapper, calloop::Interest { readable: true, writable: false, }, calloop::Mode::Level, ); let token = loop_handle .0 .insert_source(source, move |readiness, socket, state| { if !readiness.readable { return Ok(calloop::PostAction::Continue); } let mut buffer = [0_u8; 8192]; loop { match socket.recv(&mut buffer) { Ok(length) => { let ready = buffer[..length] .split(|byte| *byte == b'\n') .any(|field| field == b"READY=1"); if !ready { continue; } let Some(ready) = state.world.get::(entity) else { return Ok(calloop::PostAction::Remove); }; state.world.entity_mut(ready.0).insert(condition::Fulfilled); return Ok(calloop::PostAction::Remove); } Err(error) if error.kind() == io::ErrorKind::WouldBlock => { return Ok(calloop::PostAction::Continue); } Err(error) => return Err(error), } } })?; Ok(Some(token)) } } pub struct Dummy; impl Notify for Dummy { fn configure_command( &mut self, loop_handle: &ecs::LoopHandle, entity: Entity, cmd: &mut process::Command, ) -> anyhow::Result> { _ = loop_handle; _ = entity; _ = cmd; Ok(None) } } #[derive(Resource)] pub struct S6; impl Notify for S6 { fn configure_command( &mut self, loop_handle: &ecs::LoopHandle, entity: Entity, cmd: &mut process::Command, ) -> anyhow::Result> { let (notify_read, notify_write) = pipe::pipe_with(pipe::PipeFlags::CLOEXEC)?; unsafe { cmd.pre_exec(move || { let mut target = fd::OwnedFd::from_raw_fd(3); rustix::io::dup2(¬ify_write, &mut target).map_err(io::Error::from)?; mem::forget(target); Ok(()) }); } let fd_wrapper = unsafe { generic::FdWrapper::new(notify_read) }; let source = generic::Generic::new( fd_wrapper, calloop::Interest { readable: true, writable: false, }, calloop::Mode::Level, ); let token = loop_handle.0.insert_source(source, move |_, fd, state| { let mut buf = [0u8; 64]; match rustix::io::read(fd, &mut buf) { Ok(0) => Ok(calloop::PostAction::Remove), Ok(n) => { if buf[..n].contains(&b'\n') { let Some(ready) = state.world.get::(entity) else { return Ok(calloop::PostAction::Remove); }; state.world.entity_mut(ready.0).insert(condition::Fulfilled); Ok(calloop::PostAction::Remove) } else { Ok(calloop::PostAction::Continue) } } Err(rustix::io::Errno::AGAIN) => Ok(calloop::PostAction::Continue), Err(err) => { log::error!("failed to read notify fd: {err}"); Ok(calloop::PostAction::Remove) } } })?; Ok(Some(token)) } }