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:
23
 4. Verify D-Bus activation: `./scripts/test-dbus-activation.sh`
23
 4. Verify D-Bus activation: `./scripts/test-dbus-activation.sh`
24
 5. Create mock request: `cargo run -p garwarpctl -- begin req-1 :1.2 - x11:0x2a`
24
 5. Create mock request: `cargo run -p garwarpctl -- begin req-1 :1.2 - x11:0x2a`
25
 6. Transition mock request: `cargo run -p garwarpctl -- transition req-1 :1.2 awaiting_user`
25
 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`
27
 
28
 
28
 ## Runtime Tuning
29
 ## Runtime Tuning
29
 1. `GARWARP_REQUEST_TIMEOUT_MS`: timeout before in-flight requests are marked `expired`.
30
 1. `GARWARP_REQUEST_TIMEOUT_MS`: timeout before in-flight requests are marked `expired`.
garwarp-ipc/src/lib.rsmodified
@@ -38,6 +38,7 @@ impl HealthStatus {
38
 pub enum ControlRequest {
38
 pub enum ControlRequest {
39
     Status,
39
     Status,
40
     Stop,
40
     Stop,
41
+    ListRequests,
41
     InspectRequest {
42
     InspectRequest {
42
         id: String,
43
         id: String,
43
     },
44
     },
@@ -91,6 +92,7 @@ impl ControlRequest {
91
         match self {
92
         match self {
92
             Self::Status => "status".to_string(),
93
             Self::Status => "status".to_string(),
93
             Self::Stop => "stop".to_string(),
94
             Self::Stop => "stop".to_string(),
95
+            Self::ListRequests => "list".to_string(),
94
             Self::InspectRequest { id } => format!("inspect id={id}"),
96
             Self::InspectRequest { id } => format!("inspect id={id}"),
95
             Self::BeginRequest {
97
             Self::BeginRequest {
96
                 id,
98
                 id,
@@ -140,6 +142,9 @@ impl ControlRequest {
140
         if trimmed == "stop" {
142
         if trimmed == "stop" {
141
             return Some(Self::Stop);
143
             return Some(Self::Stop);
142
         }
144
         }
145
+        if trimmed == "list" {
146
+            return Some(Self::ListRequests);
147
+        }
143
 
148
 
144
         let mut parts = trimmed.split_whitespace();
149
         let mut parts = trimmed.split_whitespace();
145
         match parts.next() {
150
         match parts.next() {
@@ -201,6 +206,9 @@ impl StatusResponse {
201
 pub enum ControlResponse {
206
 pub enum ControlResponse {
202
     Status(StatusResponse),
207
     Status(StatusResponse),
203
     AckStopping,
208
     AckStopping,
209
+    RequestList {
210
+        ids: Vec<String>,
211
+    },
204
     AckRequest {
212
     AckRequest {
205
         id: String,
213
         id: String,
206
         state: String,
214
         state: String,
@@ -229,6 +237,14 @@ impl ControlResponse {
229
                 status.in_flight_requests
237
                 status.in_flight_requests
230
             ),
238
             ),
231
             Self::AckStopping => "ack stopping\n".to_string(),
239
             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
+            }
232
             Self::AckRequest { id, state } => {
248
             Self::AckRequest { id, state } => {
233
                 format!("ack request id={} state={}\n", id, state)
249
                 format!("ack request id={} state={}\n", id, state)
234
             }
250
             }
@@ -321,6 +337,32 @@ impl ControlResponse {
321
                 Some(other) => Err(ParseError::UnknownToken(other.to_string())),
337
                 Some(other) => Err(ParseError::UnknownToken(other.to_string())),
322
                 None => Err(ParseError::MissingField("ack")),
338
                 None => Err(ParseError::MissingField("ack")),
323
             },
339
             },
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
+            }
324
             Some("snapshot") => {
366
             Some("snapshot") => {
325
                 let mut id = None;
367
                 let mut id = None;
326
                 let mut state = None;
368
                 let mut state = None;
@@ -438,6 +480,7 @@ mod tests {
438
         for request in [
480
         for request in [
439
             ControlRequest::Status,
481
             ControlRequest::Status,
440
             ControlRequest::Stop,
482
             ControlRequest::Stop,
483
+            ControlRequest::ListRequests,
441
             ControlRequest::InspectRequest {
484
             ControlRequest::InspectRequest {
442
                 id: "req-1".to_string(),
485
                 id: "req-1".to_string(),
443
             },
486
             },
@@ -476,6 +519,10 @@ mod tests {
476
     fn response_ack_roundtrip() {
519
     fn response_ack_roundtrip() {
477
         for response in [
520
         for response in [
478
             ControlResponse::AckStopping,
521
             ControlResponse::AckStopping,
522
+            ControlResponse::RequestList {
523
+                ids: vec!["req-1".to_string(), "req-2".to_string()],
524
+            },
525
+            ControlResponse::RequestList { ids: Vec::new() },
479
             ControlResponse::AckRequest {
526
             ControlResponse::AckRequest {
480
                 id: "req-1".to_string(),
527
                 id: "req-1".to_string(),
481
                 state: "pending".to_string(),
528
                 state: "pending".to_string(),
garwarp/src/daemon.rsmodified
@@ -123,6 +123,15 @@ fn handle_connection(stream: UnixStream, state: &mut DaemonState) -> io::Result<
123
             state.running = false;
123
             state.running = false;
124
             ControlResponse::AckStopping
124
             ControlResponse::AckStopping
125
         }
125
         }
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
+        }
126
         Some(ControlRequest::InspectRequest { id }) => {
135
         Some(ControlRequest::InspectRequest { id }) => {
127
             let validation = validate_request_id(&id);
136
             let validation = validate_request_id(&id);
128
             if let Err(error) = validation {
137
             if let Err(error) = validation {
@@ -410,6 +419,53 @@ mod tests {
410
         assert!(!state.running);
419
         assert!(!state.running);
411
     }
420
     }
412
 
421
 
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
+
413
     #[test]
469
     #[test]
414
     fn invalid_request_uses_stable_error_reason() {
470
     fn invalid_request_uses_stable_error_reason() {
415
         let (mut client, server) = UnixStream::pair().expect("pair should be created");
471
         let (mut client, server) = UnixStream::pair().expect("pair should be created");
garwarpctl/src/main.rsmodified
@@ -29,6 +29,7 @@ fn main() {
29
 enum Command {
29
 enum Command {
30
     Status,
30
     Status,
31
     Stop,
31
     Stop,
32
+    List,
32
     Inspect {
33
     Inspect {
33
         id: String,
34
         id: String,
34
     },
35
     },
@@ -53,6 +54,7 @@ fn parse_command(args: &[String]) -> Result<Command, String> {
53
         [] => Ok(Command::Status),
54
         [] => Ok(Command::Status),
54
         [command] if command == "status" => Ok(Command::Status),
55
         [command] if command == "status" => Ok(Command::Status),
55
         [command] if command == "stop" => Ok(Command::Stop),
56
         [command] if command == "stop" => Ok(Command::Stop),
57
+        [command] if command == "list" => Ok(Command::List),
56
         [command, id] if command == "inspect" => Ok(Command::Inspect { id: id.clone() }),
58
         [command, id] if command == "inspect" => Ok(Command::Inspect { id: id.clone() }),
57
         [command] if command == "version" || command == "--version" || command == "-V" => {
59
         [command] if command == "version" || command == "--version" || command == "-V" => {
58
             Ok(Command::Version)
60
             Ok(Command::Version)
@@ -198,6 +200,28 @@ fn run(command: Command) -> io::Result<()> {
198
                 )),
200
                 )),
199
             }
201
             }
200
         }
202
         }
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
+        }
201
         Command::Inspect { id } => {
225
         Command::Inspect { id } => {
202
             let response = send_request(ControlRequest::InspectRequest { id })?;
226
             let response = send_request(ControlRequest::InspectRequest { id })?;
203
             match response {
227
             match response {
@@ -323,6 +347,7 @@ fn print_help() {
323
     println!("commands:");
347
     println!("commands:");
324
     println!("  status (default)");
348
     println!("  status (default)");
325
     println!("  stop");
349
     println!("  stop");
350
+    println!("  list");
326
     println!("  inspect <id>");
351
     println!("  inspect <id>");
327
     println!("  begin <id> <sender> [app_id|-] [parent_window|-]");
352
     println!("  begin <id> <sender> [app_id|-] [parent_window|-]");
328
     println!("  transition <id> <sender> <awaiting_user|fulfilled|cancelled|failed> [app_id|-]");
353
     println!("  transition <id> <sender> <awaiting_user|fulfilled|cancelled|failed> [app_id|-]");
@@ -397,6 +422,13 @@ mod tests {
397
         );
422
         );
398
     }
423
     }
399
 
424
 
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
+
400
     #[test]
432
     #[test]
401
     fn parse_transition_target_rejects_unknown_state() {
433
     fn parse_transition_target_rejects_unknown_state() {
402
         let parsed = parse_transition_target("bogus");
434
         let parsed = parse_transition_target("bogus");