Skip to main content

systemd_daemon/
systemd_daemon.rs

1#[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}