systemd_daemon/
systemd_daemon.rs1#[cfg(target_os = "linux")]
2mod linux_example {
3 use std::path::PathBuf;
4 use std::time::Duration;
5
6 use tokio::sync::mpsc::Sender;
7 use tracing_subscriber::prelude::*;
8 use wora::prelude::*;
9
10 #[derive(Debug, Default, serde::Deserialize)]
11 struct SystemdConfig {
12 message: String,
13 interval_ms: u64,
14 }
15
16 impl Config for SystemdConfig {
17 type ConfigT = Self;
18
19 fn parse_main_config_file(data: String) -> Result<Self::ConfigT, Box<dyn std::error::Error>> {
20 Ok(toml::from_str(&data)?)
21 }
22
23 fn parse_supplemental_config_file(_file_path: PathBuf, data: String) -> Result<Self::ConfigT, Box<dyn std::error::Error>> {
24 Ok(toml::from_str(&data)?)
25 }
26 }
27
28 #[derive(Clone, Debug)]
29 enum DaemonEvent {
30 Tick,
31 }
32
33 struct SystemdDaemon {
34 config: SystemdConfig,
35 }
36
37 #[async_trait]
38 impl App<DaemonEvent, ()> for SystemdDaemon {
39 type AppConfig = SystemdConfig;
40 type AppSecrets = NoSecrets;
41 type Setup = ();
42
43 fn name(&self) -> &'static str {
44 "systemd_daemon"
45 }
46
47 async fn reload_config(&mut self, reload: ConfigReload<SystemdConfig>) -> Result<(), Box<dyn std::error::Error>> {
48 if let Some(config) = reload.main {
49 self.config = config;
50 }
51 Ok(())
52 }
53
54 async fn setup(
55 &mut self,
56 wora: &Wora<DaemonEvent, ()>,
57 _exec: impl AsyncExecutor<DaemonEvent, ()>,
58 _fs: impl WFS + 'static,
59 _metrics: Sender<O11yEvent<()>>,
60 _is_first_boot: bool,
61 ) -> Result<Self::Setup, Box<dyn std::error::Error>> {
62 let sender = wora.sender.clone();
63 let status = wora.status_handle();
64 let interval = Duration::from_millis(self.config.interval_ms.max(100));
65 tokio::spawn(async move {
66 status.report_readiness(ReadinessState::Ready);
67 let mut ticker = tokio::time::interval(interval);
68 loop {
69 ticker.tick().await;
70 if sender.send(Event::App(DaemonEvent::Tick)).await.is_err() {
71 break;
72 }
73 }
74 });
75 Ok(())
76 }
77
78 async fn main(
79 &mut self,
80 wora: &mut Wora<DaemonEvent, ()>,
81 _exec: impl AsyncExecutor<DaemonEvent, ()>,
82 fs: impl WFS + 'static,
83 _metrics: Sender<O11yEvent<()>>,
84 ) -> MainRetryAction {
85 match wora
86 .run_event_loop(self, fs, |app, _wora, event| match event {
87 Event::App(DaemonEvent::Tick) => {
88 tracing::info!("tick message={}", app.config.message);
89 EventLoopAction::Continue
90 }
91 Event::Control(ControlEvent::ReloadConfiguration) => {
92 tracing::info!("systemd requested configuration reload");
93 EventLoopAction::Continue
94 }
95 Event::Control(ControlEvent::LogRotation) => {
96 tracing::info!("systemd requested log rotation");
97 EventLoopAction::Continue
98 }
99 Event::Control(ControlEvent::Shutdown(deadline)) => {
100 tracing::info!("systemd requested shutdown deadline={deadline:?}");
101 EventLoopAction::Exit(MainRetryAction::Success)
102 }
103 _ => EventLoopAction::Continue,
104 })
105 .await
106 {
107 Ok(action) => action,
108 Err(err) => {
109 tracing::error!("event loop failed: {}", err);
110 MainRetryAction::UseExitCode(78)
111 }
112 }
113 }
114
115 async fn end(
116 &mut self,
117 _wora: &Wora<DaemonEvent, ()>,
118 _exec: impl AsyncExecutor<DaemonEvent, ()>,
119 _fs: impl WFS + 'static,
120 _metrics: Sender<O11yEvent<()>>,
121 ) {
122 }
123 }
124
125 #[tokio::main]
126 pub async fn main() -> Result<(), MainEarlyReturn> {
127 let (tx, _rx) = tokio::sync::mpsc::channel::<O11yEvent<()>>(64);
128 tracing_subscriber::registry()
129 .with(Observability {
130 tx: tx.clone(),
131 level: tracing::Level::INFO,
132 })
133 .init();
134
135 let o11y = O11yProcessorOptionsBuilder::default()
136 .sender(tx)
137 .flush_interval(Duration::from_secs(30))
138 .status_interval(Duration::from_secs(30))
139 .host_stats_interval(Duration::from_secs(30))
140 .build()
141 .map_err(|err| MainEarlyReturn::WoraSetup(WoraSetupError::Str(err.to_string())))?;
142
143 let app = SystemdDaemon {
144 config: SystemdConfig {
145 message: "hello from systemd".to_string(),
146 interval_ms: 1000,
147 },
148 };
149
150 let exec = SystemdExecutor::system(app.name()).await;
151
152 tracing::info!(
153 "starting under systemd; write config to {} and use systemctl reload/stop to drive control events",
154 <SystemdExecutor as AsyncExecutor<DaemonEvent, ()>>::dirs(&exec)
155 .metadata_root_dir
156 .join("systemd_daemon.toml")
157 .display()
158 );
159
160 exec_async_runner(exec, app, PhysicalVFS::new(), o11y).await
161 }
162}
163
164#[cfg(target_os = "linux")]
165fn main() -> Result<(), wora::errors::MainEarlyReturn> {
166 linux_example::main()
167}
168
169#[cfg(not(target_os = "linux"))]
170fn main() {
171 eprintln!("examples/systemd_daemon.rs is a Linux-only runtime example. It is kept buildable on non-Linux targets for development.");
172}