tenseleyflow/hyprkvm / 98dc04d

Browse files

feat(cli): implement status, peers, and ping commands

- Add IPC client to CLI with Unix socket communication
- Implement `status` command with uptime, state, connected peers
- Implement `peers` command with colored status indicators
- Implement `ping` command with latency measurement
- Add JSON output support (--json flag) for scripting
- Extend protocol with uptime_secs, machine_name, PeerInfo fields
- Add PingPeer IPC request with timeout handling in daemon
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
98dc04dbeef186ab2467986338b667a36b3c42e3
Parents
df4590f
Tree
6ba05e5

3 changed files

StatusFile+-
M hyprkvm-cli/src/main.rs 270 15
M hyprkvm-common/src/protocol/mod.rs 18 0
M hyprkvm-daemon/src/main.rs 113 5
hyprkvm-cli/src/main.rsmodified
@@ -2,7 +2,13 @@
2
 //!
2
 //!
3
 //! Separate CLI for querying daemon status and managing configuration.
3
 //! Separate CLI for querying daemon status and managing configuration.
4
 
4
 
5
+use std::path::PathBuf;
6
+
5
 use clap::{Parser, Subcommand};
7
 use clap::{Parser, Subcommand};
8
+use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
9
+use tokio::net::UnixStream;
10
+
11
+use hyprkvm_common::protocol::{IpcRequest, IpcResponse};
6
 
12
 
7
 #[derive(Parser)]
13
 #[derive(Parser)]
8
 #[command(name = "hyprkvm-ctl")]
14
 #[command(name = "hyprkvm-ctl")]
@@ -23,35 +29,284 @@ enum Commands {
23
     },
29
     },
24
 
30
 
25
     /// List connected peers
31
     /// List connected peers
26
-    Peers,
32
+    Peers {
33
+        /// Output as JSON
34
+        #[arg(long)]
35
+        json: bool,
36
+    },
27
 
37
 
28
     /// Ping a peer
38
     /// Ping a peer
29
     Ping {
39
     Ping {
30
-        /// Peer name or direction
40
+        /// Peer name to ping
31
         peer: String,
41
         peer: String,
32
     },
42
     },
33
 }
43
 }
34
 
44
 
35
-#[tokio::main]
45
+// ============================================================================
36
-async fn main() -> anyhow::Result<()> {
46
+// IPC Client
37
-    let cli = Cli::parse();
47
+// ============================================================================
38
 
48
 
39
-    match cli.command {
49
+/// Get the IPC socket path
40
-        Commands::Status { json } => {
50
+fn socket_path() -> PathBuf {
41
-            // TODO: Connect to daemon and get status
51
+    let runtime_dir =
42
-            if json {
52
+        std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
43
-                println!("{{\"status\": \"not_implemented\"}}");
53
+    PathBuf::from(runtime_dir).join("hyprkvm.sock")
54
+}
55
+
56
+/// IPC client for sending commands to daemon
57
+struct IpcClient {
58
+    stream: UnixStream,
59
+}
60
+
61
+impl IpcClient {
62
+    /// Connect to the daemon
63
+    async fn connect() -> std::io::Result<Self> {
64
+        let path = socket_path();
65
+        let stream = UnixStream::connect(&path).await?;
66
+        Ok(Self { stream })
67
+    }
68
+
69
+    /// Send a request and get response
70
+    async fn request(&mut self, req: &IpcRequest) -> std::io::Result<IpcResponse> {
71
+        // Send request
72
+        let json = serde_json::to_string(req)?;
73
+        self.stream.write_all(json.as_bytes()).await?;
74
+        self.stream.write_all(b"\n").await?;
75
+        self.stream.flush().await?;
76
+
77
+        // Read response
78
+        let mut reader = BufReader::new(&mut self.stream);
79
+        let mut line = String::new();
80
+        reader.read_line(&mut line).await?;
81
+
82
+        serde_json::from_str(&line)
83
+            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
84
+    }
85
+}
86
+
87
+// ============================================================================
88
+// Helpers
89
+// ============================================================================
90
+
91
+/// Format uptime in human-readable form
92
+fn format_uptime(secs: u64) -> String {
93
+    let days = secs / 86400;
94
+    let hours = (secs % 86400) / 3600;
95
+    let mins = (secs % 3600) / 60;
96
+    let secs = secs % 60;
97
+
98
+    if days > 0 {
99
+        format!("{}d {}h {}m {}s", days, hours, mins, secs)
100
+    } else if hours > 0 {
101
+        format!("{}h {}m {}s", hours, mins, secs)
102
+    } else if mins > 0 {
103
+        format!("{}m {}s", mins, secs)
104
+    } else {
105
+        format!("{}s", secs)
106
+    }
107
+}
108
+
109
+/// Get colored status indicator
110
+fn status_indicator(status: &str) -> &'static str {
111
+    match status {
112
+        "connected" => "\x1b[32m●\x1b[0m",    // Green dot
113
+        "connecting" => "\x1b[33m●\x1b[0m",   // Yellow dot
114
+        "disconnected" => "\x1b[31m●\x1b[0m", // Red dot
115
+        _ => "○",                              // Empty dot
116
+    }
117
+}
118
+
119
+// ============================================================================
120
+// Command Handlers
121
+// ============================================================================
122
+
123
+async fn handle_status(json_output: bool) -> anyhow::Result<()> {
124
+    let mut client = match IpcClient::connect().await {
125
+        Ok(c) => c,
126
+        Err(e) => {
127
+            if json_output {
128
+                println!(
129
+                    "{}",
130
+                    serde_json::json!({
131
+                        "error": "daemon not running",
132
+                        "details": e.to_string()
133
+                    })
134
+                );
44
             } else {
135
             } else {
45
-                println!("HyprKVM Status: not implemented yet");
136
+                eprintln!("Error: daemon not running ({})", e);
46
             }
137
             }
138
+            std::process::exit(1);
47
         }
139
         }
48
-        Commands::Peers => {
140
+    };
49
-            println!("Peer listing not implemented yet");
141
+
142
+    let response = client.request(&IpcRequest::Status).await?;
143
+
144
+    match response {
145
+        IpcResponse::Status {
146
+            state,
147
+            connected_peers,
148
+            uptime_secs,
149
+            machine_name,
150
+        } => {
151
+            if json_output {
152
+                println!(
153
+                    "{}",
154
+                    serde_json::json!({
155
+                        "machine_name": machine_name,
156
+                        "state": state,
157
+                        "connected_peers": connected_peers,
158
+                        "uptime_secs": uptime_secs,
159
+                    })
160
+                );
161
+            } else {
162
+                println!("HyprKVM Status");
163
+                println!("──────────────────────────────");
164
+                println!("Machine:  {}", machine_name);
165
+                println!("State:    {}", state);
166
+                println!("Uptime:   {}", format_uptime(uptime_secs));
167
+                println!("Peers:    {} connected", connected_peers.len());
168
+                if !connected_peers.is_empty() {
169
+                    println!("          {}", connected_peers.join(", "));
170
+                }
171
+            }
172
+        }
173
+        IpcResponse::Error { message } => {
174
+            if json_output {
175
+                println!("{}", serde_json::json!({ "error": message }));
176
+            } else {
177
+                eprintln!("Error: {}", message);
178
+            }
179
+            std::process::exit(1);
50
         }
180
         }
51
-        Commands::Ping { peer } => {
181
+        _ => {
52
-            println!("Ping {} not implemented yet", peer);
182
+            eprintln!("Unexpected response from daemon");
183
+            std::process::exit(1);
53
         }
184
         }
54
     }
185
     }
55
 
186
 
56
     Ok(())
187
     Ok(())
57
 }
188
 }
189
+
190
+async fn handle_peers(json_output: bool) -> anyhow::Result<()> {
191
+    let mut client = match IpcClient::connect().await {
192
+        Ok(c) => c,
193
+        Err(e) => {
194
+            if json_output {
195
+                println!(
196
+                    "{}",
197
+                    serde_json::json!({
198
+                        "error": "daemon not running",
199
+                        "details": e.to_string()
200
+                    })
201
+                );
202
+            } else {
203
+                eprintln!("Error: daemon not running ({})", e);
204
+            }
205
+            std::process::exit(1);
206
+        }
207
+    };
208
+
209
+    let response = client.request(&IpcRequest::ListPeers).await?;
210
+
211
+    match response {
212
+        IpcResponse::Peers { peers } => {
213
+            if json_output {
214
+                println!("{}", serde_json::to_string_pretty(&peers)?);
215
+            } else {
216
+                if peers.is_empty() {
217
+                    println!("No peers configured");
218
+                } else {
219
+                    println!("Configured Peers");
220
+                    println!("──────────────────────────────────────────────────");
221
+                    for peer in &peers {
222
+                        let indicator = status_indicator(&peer.status);
223
+                        println!(
224
+                            "{} {} ({:?}) - {}",
225
+                            indicator, peer.name, peer.direction, peer.address
226
+                        );
227
+                    }
228
+                    println!("──────────────────────────────────────────────────");
229
+                    let connected = peers.iter().filter(|p| p.connected).count();
230
+                    println!("{}/{} peers connected", connected, peers.len());
231
+                }
232
+            }
233
+        }
234
+        IpcResponse::Error { message } => {
235
+            if json_output {
236
+                println!("{}", serde_json::json!({ "error": message }));
237
+            } else {
238
+                eprintln!("Error: {}", message);
239
+            }
240
+            std::process::exit(1);
241
+        }
242
+        _ => {
243
+            eprintln!("Unexpected response from daemon");
244
+            std::process::exit(1);
245
+        }
246
+    }
247
+
248
+    Ok(())
249
+}
250
+
251
+async fn handle_ping(peer_name: String) -> anyhow::Result<()> {
252
+    let mut client = match IpcClient::connect().await {
253
+        Ok(c) => c,
254
+        Err(e) => {
255
+            eprintln!("Error: daemon not running ({})", e);
256
+            std::process::exit(1);
257
+        }
258
+    };
259
+
260
+    println!("Pinging {}...", peer_name);
261
+
262
+    let response = client
263
+        .request(&IpcRequest::PingPeer {
264
+            peer_name: peer_name.clone(),
265
+        })
266
+        .await?;
267
+
268
+    match response {
269
+        IpcResponse::PingResult {
270
+            peer_name,
271
+            latency_ms,
272
+            error,
273
+        } => {
274
+            if let Some(err) = error {
275
+                eprintln!("Ping failed: {}", err);
276
+                std::process::exit(1);
277
+            } else if let Some(ms) = latency_ms {
278
+                println!("Reply from {}: time={}ms", peer_name, ms);
279
+            } else {
280
+                eprintln!("Ping failed: no response");
281
+                std::process::exit(1);
282
+            }
283
+        }
284
+        IpcResponse::Error { message } => {
285
+            eprintln!("Error: {}", message);
286
+            std::process::exit(1);
287
+        }
288
+        _ => {
289
+            eprintln!("Unexpected response from daemon");
290
+            std::process::exit(1);
291
+        }
292
+    }
293
+
294
+    Ok(())
295
+}
296
+
297
+// ============================================================================
298
+// Main
299
+// ============================================================================
300
+
301
+#[tokio::main]
302
+async fn main() -> anyhow::Result<()> {
303
+    let cli = Cli::parse();
304
+
305
+    match cli.command {
306
+        Commands::Status { json } => handle_status(json).await?,
307
+        Commands::Peers { json } => handle_peers(json).await?,
308
+        Commands::Ping { peer } => handle_ping(peer).await?,
309
+    }
310
+
311
+    Ok(())
312
+}
hyprkvm-common/src/protocol/mod.rsmodified
@@ -217,6 +217,8 @@ pub enum IpcRequest {
217
     Status,
217
     Status,
218
     /// List connected peers
218
     /// List connected peers
219
     ListPeers,
219
     ListPeers,
220
+    /// Ping a specific peer by name
221
+    PingPeer { peer_name: String },
220
 }
222
 }
221
 
223
 
222
 /// IPC response from daemon to CLI
224
 /// IPC response from daemon to CLI
@@ -231,9 +233,21 @@ pub enum IpcResponse {
231
     Status {
233
     Status {
232
         state: String,
234
         state: String,
233
         connected_peers: Vec<String>,
235
         connected_peers: Vec<String>,
236
+        /// Daemon uptime in seconds
237
+        uptime_secs: u64,
238
+        /// This machine's name
239
+        machine_name: String,
234
     },
240
     },
235
     /// Peer list
241
     /// Peer list
236
     Peers { peers: Vec<PeerInfo> },
242
     Peers { peers: Vec<PeerInfo> },
243
+    /// Ping result
244
+    PingResult {
245
+        peer_name: String,
246
+        /// Round-trip time in milliseconds (None if peer not connected)
247
+        latency_ms: Option<u64>,
248
+        /// Error message if ping failed
249
+        error: Option<String>,
250
+    },
237
     /// Error occurred
251
     /// Error occurred
238
     Error { message: String },
252
     Error { message: String },
239
 }
253
 }
@@ -244,4 +258,8 @@ pub struct PeerInfo {
244
     pub name: String,
258
     pub name: String,
245
     pub direction: Direction,
259
     pub direction: Direction,
246
     pub connected: bool,
260
     pub connected: bool,
261
+    /// Configured address for this peer
262
+    pub address: String,
263
+    /// Connection status: "connected", "disconnected", "connecting"
264
+    pub status: String,
247
 }
265
 }
hyprkvm-daemon/src/main.rsmodified
@@ -155,6 +155,9 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
155
     info!("Machine name: {}", config.machines.self_name);
155
     info!("Machine name: {}", config.machines.self_name);
156
     info!("Listening on port: {}", config.network.listen_port);
156
     info!("Listening on port: {}", config.network.listen_port);
157
 
157
 
158
+    // Track daemon start time for uptime reporting
159
+    let daemon_start_time = std::time::Instant::now();
160
+
158
     // Connect to Hyprland
161
     // Connect to Hyprland
159
     info!("Connecting to Hyprland...");
162
     info!("Connecting to Hyprland...");
160
     let hypr_client = hyprland::ipc::HyprlandClient::new().await?;
163
     let hypr_client = hyprland::ipc::HyprlandClient::new().await?;
@@ -1347,20 +1350,125 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1347
                                 .map(|n| n.name.clone())
1350
                                 .map(|n| n.name.clone())
1348
                                 .collect()
1351
                                 .collect()
1349
                         };
1352
                         };
1350
-                        IpcResponse::Status { state, connected_peers }
1353
+                        let uptime_secs = daemon_start_time.elapsed().as_secs();
1354
+                        IpcResponse::Status {
1355
+                            state,
1356
+                            connected_peers,
1357
+                            uptime_secs,
1358
+                            machine_name: config.machines.self_name.clone(),
1359
+                        }
1351
                     }
1360
                     }
1352
                     IpcRequest::ListPeers => {
1361
                     IpcRequest::ListPeers => {
1353
                         let peers_guard = peers.read().await;
1362
                         let peers_guard = peers.read().await;
1354
                         let peer_list: Vec<hyprkvm_common::protocol::PeerInfo> = config.machines.neighbors
1363
                         let peer_list: Vec<hyprkvm_common::protocol::PeerInfo> = config.machines.neighbors
1355
                             .iter()
1364
                             .iter()
1356
-                            .map(|n| hyprkvm_common::protocol::PeerInfo {
1365
+                            .map(|n| {
1357
-                                name: n.name.clone(),
1366
+                                let connected = peers_guard.contains_key(&n.direction);
1358
-                                direction: n.direction,
1367
+                                let status = if connected {
1359
-                                connected: peers_guard.contains_key(&n.direction),
1368
+                                    "connected".to_string()
1369
+                                } else {
1370
+                                    "disconnected".to_string()
1371
+                                };
1372
+                                hyprkvm_common::protocol::PeerInfo {
1373
+                                    name: n.name.clone(),
1374
+                                    direction: n.direction,
1375
+                                    connected,
1376
+                                    address: n.address.to_string(),
1377
+                                    status,
1378
+                                }
1360
                             })
1379
                             })
1361
                             .collect();
1380
                             .collect();
1362
                         IpcResponse::Peers { peers: peer_list }
1381
                         IpcResponse::Peers { peers: peer_list }
1363
                     }
1382
                     }
1383
+                    IpcRequest::PingPeer { peer_name } => {
1384
+                        // Find the peer by name
1385
+                        let neighbor = config.machines.neighbors
1386
+                            .iter()
1387
+                            .find(|n| n.name == peer_name);
1388
+
1389
+                        match neighbor {
1390
+                            Some(n) => {
1391
+                                let direction = n.direction;
1392
+                                let mut peers_guard = peers.write().await;
1393
+
1394
+                                if let Some(peer_conn) = peers_guard.get_mut(&direction) {
1395
+                                    // Send Ping with current timestamp
1396
+                                    let timestamp = std::time::SystemTime::now()
1397
+                                        .duration_since(std::time::UNIX_EPOCH)
1398
+                                        .unwrap()
1399
+                                        .as_millis() as u64;
1400
+
1401
+                                    if let Err(e) = peer_conn.send(&Message::Ping { timestamp }).await {
1402
+                                        IpcResponse::PingResult {
1403
+                                            peer_name,
1404
+                                            latency_ms: None,
1405
+                                            error: Some(format!("Send failed: {}", e)),
1406
+                                        }
1407
+                                    } else {
1408
+                                        // Wait for Pong with timeout
1409
+                                        match tokio::time::timeout(
1410
+                                            std::time::Duration::from_secs(5),
1411
+                                            peer_conn.recv()
1412
+                                        ).await {
1413
+                                            Ok(Ok(Some(Message::Pong { timestamp: pong_ts }))) => {
1414
+                                                let now = std::time::SystemTime::now()
1415
+                                                    .duration_since(std::time::UNIX_EPOCH)
1416
+                                                    .unwrap()
1417
+                                                    .as_millis() as u64;
1418
+                                                let latency = now.saturating_sub(pong_ts);
1419
+                                                IpcResponse::PingResult {
1420
+                                                    peer_name,
1421
+                                                    latency_ms: Some(latency),
1422
+                                                    error: None,
1423
+                                                }
1424
+                                            }
1425
+                                            Ok(Ok(Some(_))) => {
1426
+                                                IpcResponse::PingResult {
1427
+                                                    peer_name,
1428
+                                                    latency_ms: None,
1429
+                                                    error: Some("Unexpected response".to_string()),
1430
+                                                }
1431
+                                            }
1432
+                                            Ok(Ok(None)) => {
1433
+                                                // Connection closed
1434
+                                                peers_guard.remove(&direction);
1435
+                                                IpcResponse::PingResult {
1436
+                                                    peer_name,
1437
+                                                    latency_ms: None,
1438
+                                                    error: Some("Connection closed".to_string()),
1439
+                                                }
1440
+                                            }
1441
+                                            Ok(Err(e)) => {
1442
+                                                IpcResponse::PingResult {
1443
+                                                    peer_name,
1444
+                                                    latency_ms: None,
1445
+                                                    error: Some(format!("Receive error: {}", e)),
1446
+                                                }
1447
+                                            }
1448
+                                            Err(_) => {
1449
+                                                IpcResponse::PingResult {
1450
+                                                    peer_name,
1451
+                                                    latency_ms: None,
1452
+                                                    error: Some("Timeout".to_string()),
1453
+                                                }
1454
+                                            }
1455
+                                        }
1456
+                                    }
1457
+                                } else {
1458
+                                    IpcResponse::PingResult {
1459
+                                        peer_name,
1460
+                                        latency_ms: None,
1461
+                                        error: Some("Peer not connected".to_string()),
1462
+                                    }
1463
+                                }
1464
+                            }
1465
+                            None => {
1466
+                                IpcResponse::Error {
1467
+                                    message: format!("Unknown peer: {}", peer_name),
1468
+                                }
1469
+                            }
1470
+                        }
1471
+                    }
1364
                 };
1472
                 };
1365
 
1473
 
1366
                 let _ = response_tx.send(response);
1474
                 let _ = response_tx.send(response);