Rust · 7003 bytes Raw Blame History
1 //! Daemon state machine and main event loop
2
3 use anyhow::{Context, Result};
4 use std::fs;
5 use std::io::Write;
6 use std::path::PathBuf;
7 use std::sync::mpsc::Receiver;
8 use tokio::signal::unix::{signal, SignalKind};
9 use tracing::{debug, info, warn};
10
11 use crate::config::{self, Config};
12 use crate::ipc::{Command, IpcServer};
13 use crate::tray::xembed::XEmbedManager;
14
15 /// Get the path to the PID file
16 fn pid_file_path() -> PathBuf {
17 dirs::runtime_dir()
18 .unwrap_or_else(|| PathBuf::from("/tmp"))
19 .join("gartray.pid")
20 }
21
22 /// Check if an existing daemon is running
23 fn check_existing_daemon() -> Result<()> {
24 let pid_path = pid_file_path();
25
26 if pid_path.exists() {
27 let pid_str = fs::read_to_string(&pid_path)?;
28 let pid: i32 = pid_str.trim().parse()?;
29
30 let proc_path = format!("/proc/{}", pid);
31 if std::path::Path::new(&proc_path).exists() {
32 anyhow::bail!(
33 "gartray daemon already running (PID {}). Remove {} if incorrect.",
34 pid,
35 pid_path.display()
36 );
37 } else {
38 warn!("Removing stale PID file for PID {}", pid);
39 fs::remove_file(&pid_path)?;
40 }
41 }
42
43 Ok(())
44 }
45
46 /// Write the current process PID to the PID file
47 fn write_pid_file() -> Result<()> {
48 let pid_path = pid_file_path();
49 let pid = std::process::id();
50
51 let mut file = fs::File::create(&pid_path)?;
52 writeln!(file, "{}", pid)?;
53
54 debug!("Wrote PID {} to {}", pid, pid_path.display());
55 Ok(())
56 }
57
58 /// Remove the PID file
59 fn remove_pid_file() {
60 let pid_path = pid_file_path();
61 if let Err(e) = fs::remove_file(&pid_path) {
62 warn!("Failed to remove PID file: {}", e);
63 } else {
64 debug!("Removed PID file");
65 }
66 }
67
68 /// PID file guard - removes on drop
69 struct PidGuard;
70
71 impl Drop for PidGuard {
72 fn drop(&mut self) {
73 remove_pid_file();
74 }
75 }
76
77 /// Daemon state
78 pub struct Daemon {
79 config: Config,
80 xembed: Option<XEmbedManager>,
81 ipc_server: IpcServer,
82 ipc_rx: Receiver<Command>,
83 running: bool,
84 panel_visible: bool,
85 }
86
87 impl Daemon {
88 /// Create a new daemon
89 pub fn new(config: Config) -> Result<Self> {
90 let (ipc_server, ipc_rx) = IpcServer::new();
91 Ok(Self {
92 config,
93 xembed: None,
94 ipc_server,
95 ipc_rx,
96 running: true,
97 panel_visible: false,
98 })
99 }
100
101 /// Initialize IPC server
102 pub fn init_ipc(&mut self) -> Result<()> {
103 self.ipc_server.start().context("Failed to start IPC server")?;
104 info!("IPC server started");
105 Ok(())
106 }
107
108 /// Initialize X11 and tray
109 pub fn init_x11(&mut self) -> Result<()> {
110 info!("Initializing X11 connection...");
111
112 let mut xembed = XEmbedManager::new(&self.config.tray)?;
113
114 if xembed.acquire_selection() {
115 info!("Acquired system tray selection");
116 self.xembed = Some(xembed);
117 } else {
118 warn!("Failed to acquire tray selection (another tray running?)");
119 }
120
121 Ok(())
122 }
123
124 /// Run the main event loop
125 pub async fn run(&mut self) -> Result<()> {
126 info!("Entering main event loop");
127
128 let mut sigterm = signal(SignalKind::terminate())?;
129 let mut sighup = signal(SignalKind::hangup())?;
130
131 while self.running {
132 // Check for IPC commands (non-blocking)
133 self.poll_ipc_commands();
134
135 tokio::select! {
136 _ = sigterm.recv() => {
137 info!("Received SIGTERM, shutting down");
138 self.running = false;
139 }
140 _ = sighup.recv() => {
141 info!("Received SIGHUP, reloading config");
142 self.handle_reload()?;
143 }
144 _ = tokio::signal::ctrl_c() => {
145 info!("Received Ctrl+C, shutting down");
146 self.running = false;
147 }
148 _ = self.poll_x11_events() => {
149 // Events processed
150 }
151 }
152 }
153
154 info!("Daemon shutdown complete");
155 Ok(())
156 }
157
158 /// Poll for IPC commands
159 fn poll_ipc_commands(&mut self) {
160 while let Ok(cmd) = self.ipc_rx.try_recv() {
161 self.handle_ipc_command(cmd);
162 }
163 }
164
165 /// Handle an IPC command
166 fn handle_ipc_command(&mut self, cmd: Command) {
167 debug!("Handling IPC command: {:?}", cmd);
168 match cmd {
169 Command::Show => {
170 info!("Showing panel");
171 self.panel_visible = true;
172 // TODO: Actually show the panel window
173 }
174 Command::Hide => {
175 info!("Hiding panel");
176 self.panel_visible = false;
177 // TODO: Actually hide the panel window
178 }
179 Command::Toggle => {
180 self.panel_visible = !self.panel_visible;
181 info!("Panel visibility toggled: {}", self.panel_visible);
182 // TODO: Actually toggle the panel window
183 }
184 Command::Reload => {
185 info!("Reloading config via IPC");
186 let _ = self.handle_reload();
187 }
188 Command::Status => {
189 info!(
190 "Status: running, {} icons, panel {}",
191 self.xembed.as_ref().map(|x| x.icon_count()).unwrap_or(0),
192 if self.panel_visible { "visible" } else { "hidden" }
193 );
194 }
195 Command::Quit => {
196 info!("Quit requested via IPC");
197 self.running = false;
198 }
199 }
200 }
201
202 /// Poll X11 events
203 async fn poll_x11_events(&mut self) -> Result<()> {
204 if let Some(ref mut xembed) = self.xembed {
205 xembed.process_events()?;
206 }
207
208 // Small delay to prevent busy loop
209 tokio::time::sleep(tokio::time::Duration::from_millis(16)).await;
210 Ok(())
211 }
212
213 /// Handle config reload
214 fn handle_reload(&mut self) -> Result<()> {
215 match config::load(None) {
216 Ok(new_config) => {
217 info!("Reloaded configuration");
218 self.config = new_config;
219 }
220 Err(e) => {
221 warn!("Failed to reload config: {}", e);
222 }
223 }
224 Ok(())
225 }
226 }
227
228 /// Run the tray daemon
229 pub async fn run(config_path: Option<String>, _foreground: bool) -> Result<()> {
230 check_existing_daemon()?;
231 write_pid_file()?;
232 let _pid_guard = PidGuard;
233
234 let config = config::load(config_path.as_deref())?;
235 info!("Loaded configuration");
236 info!("Tray position: {}", config.tray.position);
237 info!("Icon size: {}px", config.tray.icon_size);
238
239 let mut daemon = Daemon::new(config)?;
240 daemon.init_ipc().context("Failed to initialize IPC")?;
241 daemon.init_x11().context("Failed to initialize X11")?;
242 daemon.run().await
243 }
244