#[cfg(target_os = "linux")]
mod linux_example {
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
use tracing_subscriber::prelude::*;
use wora::prelude::*;
#[derive(Debug, Default, serde::Deserialize)]
struct SystemdConfig {
message: String,
interval_ms: u64,
}
impl Config for SystemdConfig {
type ConfigT = Self;
fn parse_main_config_file(data: String) -> Result<Self::ConfigT, Box<dyn std::error::Error>> {
Ok(toml::from_str(&data)?)
}
fn parse_supplemental_config_file(_file_path: PathBuf, data: String) -> Result<Self::ConfigT, Box<dyn std::error::Error>> {
Ok(toml::from_str(&data)?)
}
}
#[derive(Clone, Debug)]
enum DaemonEvent {
Tick,
}
struct SystemdDaemon {
config: SystemdConfig,
}
#[async_trait]
impl App<DaemonEvent, ()> for SystemdDaemon {
type AppConfig = SystemdConfig;
type AppSecrets = NoSecrets;
type Setup = ();
fn name(&self) -> &'static str {
"systemd_daemon"
}
async fn reload_config(&mut self, reload: ConfigReload<SystemdConfig>) -> Result<(), Box<dyn std::error::Error>> {
if let Some(config) = reload.main {
self.config = config;
}
Ok(())
}
async fn setup(
&mut self,
wora: &Wora<DaemonEvent, ()>,
_exec: impl AsyncExecutor<DaemonEvent, ()>,
_fs: impl WFS + 'static,
_metrics: Sender<O11yEvent<()>>,
_is_first_boot: bool,
) -> Result<Self::Setup, Box<dyn std::error::Error>> {
let sender = wora.sender.clone();
let status = wora.status_handle();
let interval = Duration::from_millis(self.config.interval_ms.max(100));
tokio::spawn(async move {
status.report_readiness(ReadinessState::Ready);
let mut ticker = tokio::time::interval(interval);
loop {
ticker.tick().await;
if sender.send(Event::App(DaemonEvent::Tick)).await.is_err() {
break;
}
}
});
Ok(())
}
async fn main(
&mut self,
wora: &mut Wora<DaemonEvent, ()>,
_exec: impl AsyncExecutor<DaemonEvent, ()>,
fs: impl WFS + 'static,
_metrics: Sender<O11yEvent<()>>,
) -> MainRetryAction {
match wora
.run_event_loop(self, fs, |app, _wora, event| match event {
Event::App(DaemonEvent::Tick) => {
tracing::info!("tick message={}", app.config.message);
EventLoopAction::Continue
}
Event::Control(ControlEvent::ReloadConfiguration) => {
tracing::info!("systemd requested configuration reload");
EventLoopAction::Continue
}
Event::Control(ControlEvent::LogRotation) => {
tracing::info!("systemd requested log rotation");
EventLoopAction::Continue
}
Event::Control(ControlEvent::Shutdown(deadline)) => {
tracing::info!("systemd requested shutdown deadline={deadline:?}");
EventLoopAction::Exit(MainRetryAction::Success)
}
_ => EventLoopAction::Continue,
})
.await
{
Ok(action) => action,
Err(err) => {
tracing::error!("event loop failed: {}", err);
MainRetryAction::UseExitCode(78)
}
}
}
async fn end(
&mut self,
_wora: &Wora<DaemonEvent, ()>,
_exec: impl AsyncExecutor<DaemonEvent, ()>,
_fs: impl WFS + 'static,
_metrics: Sender<O11yEvent<()>>,
) {
}
}
#[tokio::main]
pub async fn main() -> Result<(), MainEarlyReturn> {
let pipeline = O11yPipeline::builder()
.capacity(64)
.sink("stdout", O11yStdoutSink)
.flush_interval(Duration::from_secs(30))
.status_interval(Duration::from_secs(30))
.host_stats_interval(Duration::from_secs(30))
.build()
.map_err(|err| MainEarlyReturn::WoraSetup(WoraSetupError::Str(err.to_string())))?;
tracing_subscriber::registry().with(pipeline.tracing_layer(tracing::Level::INFO)).init();
let app = SystemdDaemon {
config: SystemdConfig {
message: "hello from systemd".to_string(),
interval_ms: 1000,
},
};
let exec = SystemdExecutor::system(app.name()).await;
tracing::info!(
"starting under systemd; write config to {} and use systemctl reload/stop to drive control events",
<SystemdExecutor as AsyncExecutor<DaemonEvent, ()>>::dirs(&exec)
.metadata_root_dir
.join("systemd_daemon.toml")
.display()
);
exec_async_runner(exec, app, PhysicalVFS::new(), pipeline).await
}
}
#[cfg(target_os = "linux")]
fn main() -> Result<(), wora::errors::MainEarlyReturn> {
linux_example::main()
}
#[cfg(not(target_os = "linux"))]
fn main() {
eprintln!("examples/systemd_daemon.rs is a Linux-only runtime example. It is kept buildable on non-Linux targets for development.");
}