gardesk/garlock / 5989d6d

Browse files

Add daemon mode and CLI subcommands

New subcommands:
- daemon: Listen for IPC lock commands
- lock: Send to daemon or lock directly
- status: Query lock state from daemon
- shutdown: Gracefully stop daemon

Default (no args) locks directly for backwards compatibility.
Daemon handles lock/query-state/shutdown commands via IPC.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5989d6d687c11f3e8cf7b604157b7ac263cec272
Parents
3ffcab8
Tree
c2fd26a

1 changed file

StatusFile+-
M garlock/src/main.rs 131 19
garlock/src/main.rsmodified
@@ -4,7 +4,7 @@
44
 //! blur effects, and PAM authentication.
55
 
66
 use anyhow::Result;
7
-use clap::Parser;
7
+use clap::{Parser, Subcommand};
88
 use std::path::PathBuf;
99
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
1010
 use x11rb::connection::Connection;
@@ -13,6 +13,7 @@ mod auth;
1313
 mod background;
1414
 mod config;
1515
 mod error;
16
+mod ipc;
1617
 mod keyboard;
1718
 mod overlay;
1819
 mod password;
@@ -36,21 +37,31 @@ use x11::LockerWindow;
3637
 #[derive(Parser, Debug)]
3738
 #[command(name = "garlock", version, about, long_about = None)]
3839
 struct Args {
39
-    /// Run in daemon mode (listen for IPC lock commands)
40
-    #[arg(long)]
41
-    daemon: bool,
42
-
4340
     /// Config file path (default: ~/.config/garlock/config.toml)
44
-    #[arg(long, short)]
41
+    #[arg(long, short, global = true)]
4542
     config: Option<PathBuf>,
4643
 
4744
     /// Enable debug logging
48
-    #[arg(long, short)]
45
+    #[arg(long, short, global = true)]
4946
     debug: bool,
5047
 
51
-    /// Lock immediately without daemon mode
52
-    #[arg(long)]
53
-    lock: bool,
48
+    #[command(subcommand)]
49
+    command: Option<Commands>,
50
+}
51
+
52
+#[derive(Subcommand, Debug)]
53
+enum Commands {
54
+    /// Run in daemon mode (listen for IPC lock commands)
55
+    Daemon,
56
+
57
+    /// Lock the screen (sends command to daemon if running, otherwise locks directly)
58
+    Lock,
59
+
60
+    /// Query the current lock state (requires daemon)
61
+    Status,
62
+
63
+    /// Shutdown the daemon
64
+    Shutdown,
5465
 }
5566
 
5667
 fn main() -> Result<()> {
@@ -73,19 +84,120 @@ fn main() -> Result<()> {
7384
     let config = Config::load(args.config.as_deref())?;
7485
     tracing::debug!(?config, "Configuration loaded");
7586
 
76
-    if args.daemon {
77
-        tracing::info!("Running in daemon mode");
78
-        run_daemon(config)
79
-    } else {
80
-        tracing::info!("Locking screen");
81
-        run_lock(config)
87
+    match args.command {
88
+        Some(Commands::Daemon) => {
89
+            tracing::info!("Running in daemon mode");
90
+            run_daemon(config)
91
+        }
92
+        Some(Commands::Lock) => {
93
+            // Try to send to daemon first, fall back to direct lock
94
+            if let Ok(mut client) = ipc::IpcClient::connect() {
95
+                tracing::info!("Sending lock command to daemon");
96
+                match client.lock() {
97
+                    Ok(response) => {
98
+                        tracing::info!(?response, "Daemon response");
99
+                        Ok(())
100
+                    }
101
+                    Err(e) => {
102
+                        tracing::warn!("Failed to send lock command: {}", e);
103
+                        tracing::info!("Falling back to direct lock");
104
+                        run_lock(config)
105
+                    }
106
+                }
107
+            } else {
108
+                tracing::info!("No daemon running, locking directly");
109
+                run_lock(config)
110
+            }
111
+        }
112
+        Some(Commands::Status) => {
113
+            let mut client = ipc::IpcClient::connect()?;
114
+            let response = client.query_state()?;
115
+            println!("{}", serde_json::to_string_pretty(&response)?);
116
+            Ok(())
117
+        }
118
+        Some(Commands::Shutdown) => {
119
+            let mut client = ipc::IpcClient::connect()?;
120
+            let response = client.shutdown()?;
121
+            println!("{}", serde_json::to_string_pretty(&response)?);
122
+            Ok(())
123
+        }
124
+        None => {
125
+            // Default: lock directly (backwards compatible)
126
+            tracing::info!("Locking screen");
127
+            run_lock(config)
128
+        }
82129
     }
83130
 }
84131
 
85132
 /// Run in daemon mode, listening for IPC lock commands
86
-fn run_daemon(_config: Config) -> Result<()> {
87
-    // TODO: Sprint 7 - IPC server implementation
88
-    tracing::warn!("Daemon mode not yet implemented");
133
+fn run_daemon(config: Config) -> Result<()> {
134
+    use std::sync::atomic::{AtomicBool, Ordering};
135
+    use std::sync::Arc;
136
+
137
+    let server = ipc::IpcServer::new()?;
138
+    let running = Arc::new(AtomicBool::new(true));
139
+    let is_locked = Arc::new(AtomicBool::new(false));
140
+
141
+    // Handle SIGTERM/SIGINT for graceful shutdown
142
+    let running_clone = running.clone();
143
+    ctrlc::set_handler(move || {
144
+        tracing::info!("Received shutdown signal");
145
+        running_clone.store(false, Ordering::SeqCst);
146
+    })
147
+    .ok();
148
+
149
+    tracing::info!(socket = ?server.socket_path(), "Daemon ready, waiting for commands");
150
+
151
+    while running.load(Ordering::SeqCst) {
152
+        if let Some((cmd, mut client)) = server.poll() {
153
+            let response = match cmd {
154
+                ipc::Command::Lock => {
155
+                    if is_locked.load(Ordering::SeqCst) {
156
+                        ipc::Response::error("Screen is already locked")
157
+                    } else {
158
+                        tracing::info!("Lock command received");
159
+                        is_locked.store(true, Ordering::SeqCst);
160
+
161
+                        // Send response before blocking on lock
162
+                        if let Err(e) = client.respond(ipc::Response::ok_with_message("Locking screen")) {
163
+                            tracing::warn!("Failed to send response: {}", e);
164
+                        }
165
+
166
+                        // Run the lock screen (this blocks until unlocked)
167
+                        match run_lock(config.clone()) {
168
+                            Ok(()) => {
169
+                                tracing::info!("Screen unlocked");
170
+                            }
171
+                            Err(e) => {
172
+                                tracing::error!("Lock failed: {}", e);
173
+                            }
174
+                        }
175
+
176
+                        is_locked.store(false, Ordering::SeqCst);
177
+                        continue; // Response already sent
178
+                    }
179
+                }
180
+                ipc::Command::QueryState => {
181
+                    let locked = is_locked.load(Ordering::SeqCst);
182
+                    ipc::Response::state(locked, None, None)
183
+                }
184
+                ipc::Command::Shutdown => {
185
+                    tracing::info!("Shutdown command received");
186
+                    running.store(false, Ordering::SeqCst);
187
+                    ipc::Response::ok_with_message("Shutting down")
188
+                }
189
+            };
190
+
191
+            if let Err(e) = client.respond(response) {
192
+                tracing::warn!("Failed to send response: {}", e);
193
+            }
194
+        }
195
+
196
+        std::thread::sleep(std::time::Duration::from_millis(10));
197
+    }
198
+
199
+    tracing::info!("Daemon shutting down");
200
+    server.cleanup();
89201
     Ok(())
90202
 }
91203