gardesk/garwarp / 265e97f

Browse files

add request list command

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
265e97fd22c9db1b7fc4f143e153a930a97b4433
Parents
8bc8337
Tree
a629a35

4 changed files

StatusFile+-
M README.md 2 1
M garwarp-ipc/src/lib.rs 47 0
M garwarp/src/daemon.rs 56 0
M garwarpctl/src/main.rs 32 0
README.mdmodified
@@ -23,7 +23,8 @@ Current scaffold includes:
2323
 4. Verify D-Bus activation: `./scripts/test-dbus-activation.sh`
2424
 5. Create mock request: `cargo run -p garwarpctl -- begin req-1 :1.2 - x11:0x2a`
2525
 6. Transition mock request: `cargo run -p garwarpctl -- transition req-1 :1.2 awaiting_user`
26
-7. Inspect request snapshot: `cargo run -p garwarpctl -- inspect req-1`
26
+7. List known requests: `cargo run -p garwarpctl -- list`
27
+8. Inspect request snapshot: `cargo run -p garwarpctl -- inspect req-1`
2728
 
2829
 ## Runtime Tuning
2930
 1. `GARWARP_REQUEST_TIMEOUT_MS`: timeout before in-flight requests are marked `expired`.
garwarp-ipc/src/lib.rsmodified
@@ -38,6 +38,7 @@ impl HealthStatus {
3838
 pub enum ControlRequest {
3939
     Status,
4040
     Stop,
41
+    ListRequests,
4142
     InspectRequest {
4243
         id: String,
4344
     },
@@ -91,6 +92,7 @@ impl ControlRequest {
9192
         match self {
9293
             Self::Status => "status".to_string(),
9394
             Self::Stop => "stop".to_string(),
95
+            Self::ListRequests => "list".to_string(),
9496
             Self::InspectRequest { id } => format!("inspect id={id}"),
9597
             Self::BeginRequest {
9698
                 id,
@@ -140,6 +142,9 @@ impl ControlRequest {
140142
         if trimmed == "stop" {
141143
             return Some(Self::Stop);
142144
         }
145
+        if trimmed == "list" {
146
+            return Some(Self::ListRequests);
147
+        }
143148
 
144149
         let mut parts = trimmed.split_whitespace();
145150
         match parts.next() {
@@ -201,6 +206,9 @@ impl StatusResponse {
201206
 pub enum ControlResponse {
202207
     Status(StatusResponse),
203208
     AckStopping,
209
+    RequestList {
210
+        ids: Vec<String>,
211
+    },
204212
     AckRequest {
205213
         id: String,
206214
         state: String,
@@ -229,6 +237,14 @@ impl ControlResponse {
229237
                 status.in_flight_requests
230238
             ),
231239
             Self::AckStopping => "ack stopping\n".to_string(),
240
+            Self::RequestList { ids } => {
241
+                let ids = if ids.is_empty() {
242
+                    "-".to_string()
243
+                } else {
244
+                    ids.join(",")
245
+                };
246
+                format!("list ids={ids}\n")
247
+            }
232248
             Self::AckRequest { id, state } => {
233249
                 format!("ack request id={} state={}\n", id, state)
234250
             }
@@ -321,6 +337,32 @@ impl ControlResponse {
321337
                 Some(other) => Err(ParseError::UnknownToken(other.to_string())),
322338
                 None => Err(ParseError::MissingField("ack")),
323339
             },
340
+            Some("list") => {
341
+                let mut ids = None;
342
+                for part in parts {
343
+                    let (key, value) = part
344
+                        .split_once('=')
345
+                        .ok_or(ParseError::InvalidField(part.to_string()))?;
346
+                    match key {
347
+                        "ids" => {
348
+                            if value == "-" {
349
+                                ids = Some(Vec::new());
350
+                            } else {
351
+                                let parsed =
352
+                                    value.split(',').map(str::to_string).collect::<Vec<_>>();
353
+                                if parsed.iter().any(|id| id.is_empty()) {
354
+                                    return Err(ParseError::InvalidField(part.to_string()));
355
+                                }
356
+                                ids = Some(parsed);
357
+                            }
358
+                        }
359
+                        _ => return Err(ParseError::InvalidField(part.to_string())),
360
+                    }
361
+                }
362
+                Ok(Self::RequestList {
363
+                    ids: ids.ok_or(ParseError::MissingField("ids"))?,
364
+                })
365
+            }
324366
             Some("snapshot") => {
325367
                 let mut id = None;
326368
                 let mut state = None;
@@ -438,6 +480,7 @@ mod tests {
438480
         for request in [
439481
             ControlRequest::Status,
440482
             ControlRequest::Stop,
483
+            ControlRequest::ListRequests,
441484
             ControlRequest::InspectRequest {
442485
                 id: "req-1".to_string(),
443486
             },
@@ -476,6 +519,10 @@ mod tests {
476519
     fn response_ack_roundtrip() {
477520
         for response in [
478521
             ControlResponse::AckStopping,
522
+            ControlResponse::RequestList {
523
+                ids: vec!["req-1".to_string(), "req-2".to_string()],
524
+            },
525
+            ControlResponse::RequestList { ids: Vec::new() },
479526
             ControlResponse::AckRequest {
480527
                 id: "req-1".to_string(),
481528
                 state: "pending".to_string(),
garwarp/src/daemon.rsmodified
@@ -123,6 +123,15 @@ fn handle_connection(stream: UnixStream, state: &mut DaemonState) -> io::Result<
123123
             state.running = false;
124124
             ControlResponse::AckStopping
125125
         }
126
+        Some(ControlRequest::ListRequests) => {
127
+            let ids = state
128
+                .requests
129
+                .records()
130
+                .into_iter()
131
+                .map(|record| record.id)
132
+                .collect::<Vec<_>>();
133
+            ControlResponse::RequestList { ids }
134
+        }
126135
         Some(ControlRequest::InspectRequest { id }) => {
127136
             let validation = validate_request_id(&id);
128137
             if let Err(error) = validation {
@@ -410,6 +419,53 @@ mod tests {
410419
         assert!(!state.running);
411420
     }
412421
 
422
+    #[test]
423
+    fn list_requests_returns_sorted_ids() {
424
+        let (mut client, server) = UnixStream::pair().expect("pair should be created");
425
+        client
426
+            .write_all(b"list\n")
427
+            .expect("list request should be written");
428
+
429
+        let mut state = DaemonState {
430
+            health: HealthStatus::Healthy,
431
+            requests: RequestRegistry::new(Duration::from_secs(5)),
432
+            running: true,
433
+        };
434
+        state
435
+            .requests
436
+            .begin_at(
437
+                "req-b",
438
+                RequestOwner::new(":1.2", None),
439
+                None,
440
+                Instant::now(),
441
+            )
442
+            .expect("request should be created");
443
+        state
444
+            .requests
445
+            .begin_at(
446
+                "req-a",
447
+                RequestOwner::new(":1.3", None),
448
+                None,
449
+                Instant::now(),
450
+            )
451
+            .expect("request should be created");
452
+
453
+        handle_connection(server, &mut state).expect("list should be handled");
454
+
455
+        let mut response_line = String::new();
456
+        let mut reader = BufReader::new(client);
457
+        reader
458
+            .read_line(&mut response_line)
459
+            .expect("response should be readable");
460
+        let response = ControlResponse::parse_line(&response_line).expect("response should parse");
461
+        assert_eq!(
462
+            response,
463
+            ControlResponse::RequestList {
464
+                ids: vec!["req-a".to_string(), "req-b".to_string()],
465
+            }
466
+        );
467
+    }
468
+
413469
     #[test]
414470
     fn invalid_request_uses_stable_error_reason() {
415471
         let (mut client, server) = UnixStream::pair().expect("pair should be created");
garwarpctl/src/main.rsmodified
@@ -29,6 +29,7 @@ fn main() {
2929
 enum Command {
3030
     Status,
3131
     Stop,
32
+    List,
3233
     Inspect {
3334
         id: String,
3435
     },
@@ -53,6 +54,7 @@ fn parse_command(args: &[String]) -> Result<Command, String> {
5354
         [] => Ok(Command::Status),
5455
         [command] if command == "status" => Ok(Command::Status),
5556
         [command] if command == "stop" => Ok(Command::Stop),
57
+        [command] if command == "list" => Ok(Command::List),
5658
         [command, id] if command == "inspect" => Ok(Command::Inspect { id: id.clone() }),
5759
         [command] if command == "version" || command == "--version" || command == "-V" => {
5860
             Ok(Command::Version)
@@ -198,6 +200,28 @@ fn run(command: Command) -> io::Result<()> {
198200
                 )),
199201
             }
200202
         }
203
+        Command::List => {
204
+            let response = send_request(ControlRequest::ListRequests)?;
205
+            match response {
206
+                ControlResponse::RequestList { ids } => {
207
+                    if ids.is_empty() {
208
+                        println!("ids=-");
209
+                    } else {
210
+                        for id in ids {
211
+                            println!("id={id}");
212
+                        }
213
+                    }
214
+                    Ok(())
215
+                }
216
+                ControlResponse::Error { code, reason } => Err(io::Error::other(format!(
217
+                    "daemon error: code={code} reason={reason}"
218
+                ))),
219
+                other => Err(io::Error::new(
220
+                    io::ErrorKind::InvalidData,
221
+                    format!("unexpected response: {other:?}"),
222
+                )),
223
+            }
224
+        }
201225
         Command::Inspect { id } => {
202226
             let response = send_request(ControlRequest::InspectRequest { id })?;
203227
             match response {
@@ -323,6 +347,7 @@ fn print_help() {
323347
     println!("commands:");
324348
     println!("  status (default)");
325349
     println!("  stop");
350
+    println!("  list");
326351
     println!("  inspect <id>");
327352
     println!("  begin <id> <sender> [app_id|-] [parent_window|-]");
328353
     println!("  transition <id> <sender> <awaiting_user|fulfilled|cancelled|failed> [app_id|-]");
@@ -397,6 +422,13 @@ mod tests {
397422
         );
398423
     }
399424
 
425
+    #[test]
426
+    fn parse_list_command() {
427
+        let args = vec!["list".to_string()];
428
+        let command = parse_command(&args).expect("list command should parse");
429
+        assert_eq!(command, Command::List);
430
+    }
431
+
400432
     #[test]
401433
     fn parse_transition_target_rejects_unknown_state() {
402434
         let parsed = parse_transition_target("bogus");