gardesk/garwarp / e3c451c

Browse files

add request inspect command

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
e3c451ce8ffdcc04ee6f43a9e420efa24368d9c7
Parents
c35eb64
Tree
3c7e46d

5 changed files

StatusFile+-
M README.md 1 0
M garwarp-ipc/src/lib.rs 85 2
M garwarp/src/daemon.rs 94 0
M garwarp/src/request.rs 42 0
M garwarpctl/src/main.rs 46 0
README.mdmodified
@@ -23,6 +23,7 @@ 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`
2627
 
2728
 ## Runtime Tuning
2829
 1. `GARWARP_REQUEST_TIMEOUT_MS`: timeout before in-flight requests are marked `expired`.
garwarp-ipc/src/lib.rsmodified
@@ -38,6 +38,9 @@ impl HealthStatus {
3838
 pub enum ControlRequest {
3939
     Status,
4040
     Stop,
41
+    InspectRequest {
42
+        id: String,
43
+    },
4144
     BeginRequest {
4245
         id: String,
4346
         sender: String,
@@ -88,6 +91,7 @@ impl ControlRequest {
8891
         match self {
8992
             Self::Status => "status".to_string(),
9093
             Self::Stop => "stop".to_string(),
94
+            Self::InspectRequest { id } => format!("inspect id={id}"),
9195
             Self::BeginRequest {
9296
                 id,
9397
                 sender,
@@ -139,6 +143,11 @@ impl ControlRequest {
139143
 
140144
         let mut parts = trimmed.split_whitespace();
141145
         match parts.next() {
146
+            Some("inspect") => {
147
+                let fields = parse_fields(parts)?;
148
+                let id = fields.get("id")?.clone();
149
+                Some(Self::InspectRequest { id })
150
+            }
142151
             Some("begin") => {
143152
                 let fields = parse_fields(parts)?;
144153
                 let id = fields.get("id")?.clone();
@@ -192,8 +201,21 @@ impl StatusResponse {
192201
 pub enum ControlResponse {
193202
     Status(StatusResponse),
194203
     AckStopping,
195
-    AckRequest { id: String, state: String },
196
-    Error { code: u32, reason: String },
204
+    AckRequest {
205
+        id: String,
206
+        state: String,
207
+    },
208
+    RequestSnapshot {
209
+        id: String,
210
+        state: String,
211
+        sender: String,
212
+        app_id: Option<String>,
213
+        parent_window: Option<String>,
214
+    },
215
+    Error {
216
+        code: u32,
217
+        reason: String,
218
+    },
197219
 }
198220
 
199221
 impl ControlResponse {
@@ -210,6 +232,20 @@ impl ControlResponse {
210232
             Self::AckRequest { id, state } => {
211233
                 format!("ack request id={} state={}\n", id, state)
212234
             }
235
+            Self::RequestSnapshot {
236
+                id,
237
+                state,
238
+                sender,
239
+                app_id,
240
+                parent_window,
241
+            } => {
242
+                let app_id = app_id.as_deref().unwrap_or("-");
243
+                let parent_window = parent_window.as_deref().unwrap_or("-");
244
+                format!(
245
+                    "snapshot id={} state={} sender={} app_id={} parent={}\n",
246
+                    id, state, sender, app_id, parent_window
247
+                )
248
+            }
213249
             Self::Error { code, reason } => format!("error code={} reason={}\n", code, reason),
214250
         }
215251
     }
@@ -285,6 +321,43 @@ impl ControlResponse {
285321
                 Some(other) => Err(ParseError::UnknownToken(other.to_string())),
286322
                 None => Err(ParseError::MissingField("ack")),
287323
             },
324
+            Some("snapshot") => {
325
+                let mut id = None;
326
+                let mut state = None;
327
+                let mut sender = None;
328
+                let mut app_id = None;
329
+                let mut parent_window = None;
330
+
331
+                for part in parts {
332
+                    let (key, value) = part
333
+                        .split_once('=')
334
+                        .ok_or(ParseError::InvalidField(part.to_string()))?;
335
+                    match key {
336
+                        "id" => id = Some(value.to_string()),
337
+                        "state" => state = Some(value.to_string()),
338
+                        "sender" => sender = Some(value.to_string()),
339
+                        "app_id" => {
340
+                            if value != "-" {
341
+                                app_id = Some(value.to_string());
342
+                            }
343
+                        }
344
+                        "parent" => {
345
+                            if value != "-" {
346
+                                parent_window = Some(value.to_string());
347
+                            }
348
+                        }
349
+                        _ => return Err(ParseError::InvalidField(part.to_string())),
350
+                    }
351
+                }
352
+
353
+                Ok(Self::RequestSnapshot {
354
+                    id: id.ok_or(ParseError::MissingField("id"))?,
355
+                    state: state.ok_or(ParseError::MissingField("state"))?,
356
+                    sender: sender.ok_or(ParseError::MissingField("sender"))?,
357
+                    app_id,
358
+                    parent_window,
359
+                })
360
+            }
288361
             Some("error") => match parts.next() {
289362
                 Some(first_field) => {
290363
                     let mut code = None;
@@ -365,6 +438,9 @@ mod tests {
365438
         for request in [
366439
             ControlRequest::Status,
367440
             ControlRequest::Stop,
441
+            ControlRequest::InspectRequest {
442
+                id: "req-1".to_string(),
443
+            },
368444
             ControlRequest::BeginRequest {
369445
                 id: "req-1".to_string(),
370446
                 sender: ":1.2".to_string(),
@@ -404,6 +480,13 @@ mod tests {
404480
                 id: "req-1".to_string(),
405481
                 state: "pending".to_string(),
406482
             },
483
+            ControlResponse::RequestSnapshot {
484
+                id: "req-1".to_string(),
485
+                state: "awaiting_user".to_string(),
486
+                sender: ":1.2".to_string(),
487
+                app_id: Some("org.test.App".to_string()),
488
+                parent_window: Some("x11:0x2a".to_string()),
489
+            },
407490
             ControlResponse::Error {
408491
                 code: 2,
409492
                 reason: "invalid_request".to_string(),
garwarp/src/daemon.rsmodified
@@ -123,6 +123,22 @@ fn handle_connection(stream: UnixStream, state: &mut DaemonState) -> io::Result<
123123
             state.running = false;
124124
             ControlResponse::AckStopping
125125
         }
126
+        Some(ControlRequest::InspectRequest { id }) => match state.requests.record(&id) {
127
+            Some(record) => ControlResponse::RequestSnapshot {
128
+                id: record.id,
129
+                state: record.state.as_str().to_string(),
130
+                sender: record.owner.sender,
131
+                app_id: record.owner.app_id,
132
+                parent_window: record.parent_window.map(|parent| parent.as_str()),
133
+            },
134
+            None => {
135
+                let mapping = map_portal_error(&PortalError::RequestNotFound);
136
+                ControlResponse::Error {
137
+                    code: mapping.code as u32,
138
+                    reason: mapping.reason.to_string(),
139
+                }
140
+            }
141
+        },
126142
         Some(ControlRequest::BeginRequest {
127143
             id,
128144
             sender,
@@ -589,6 +605,84 @@ mod tests {
589605
         assert_eq!(state.requests.state("req-1"), Some(RequestState::Cancelled));
590606
     }
591607
 
608
+    #[test]
609
+    fn inspect_returns_request_snapshot() {
610
+        let (mut client, server) = UnixStream::pair().expect("pair should be created");
611
+        client
612
+            .write_all(b"inspect id=req-1\n")
613
+            .expect("inspect request should be written");
614
+
615
+        let mut state = DaemonState {
616
+            health: HealthStatus::Healthy,
617
+            requests: RequestRegistry::new(Duration::from_secs(5)),
618
+            running: true,
619
+        };
620
+        state
621
+            .requests
622
+            .begin_at(
623
+                "req-1",
624
+                RequestOwner::new(":1.2", Some("org.test.App".to_string())),
625
+                Some(ParentWindowContext::X11 { window_id: 42 }),
626
+                Instant::now(),
627
+            )
628
+            .expect("request should be created");
629
+        state
630
+            .requests
631
+            .transition(
632
+                "req-1",
633
+                &RequestOwner::new(":1.2", Some("org.test.App".to_string())),
634
+                RequestState::AwaitingUser,
635
+            )
636
+            .expect("request should transition");
637
+        handle_connection(server, &mut state).expect("inspect should be handled");
638
+
639
+        let mut response_line = String::new();
640
+        let mut reader = BufReader::new(client);
641
+        reader
642
+            .read_line(&mut response_line)
643
+            .expect("response should be readable");
644
+        let response = ControlResponse::parse_line(&response_line).expect("response should parse");
645
+        assert_eq!(
646
+            response,
647
+            ControlResponse::RequestSnapshot {
648
+                id: "req-1".to_string(),
649
+                state: "awaiting_user".to_string(),
650
+                sender: ":1.2".to_string(),
651
+                app_id: Some("org.test.App".to_string()),
652
+                parent_window: Some("x11:0x2a".to_string()),
653
+            }
654
+        );
655
+    }
656
+
657
+    #[test]
658
+    fn inspect_missing_request_maps_to_not_found() {
659
+        let (mut client, server) = UnixStream::pair().expect("pair should be created");
660
+        client
661
+            .write_all(b"inspect id=req-missing\n")
662
+            .expect("inspect request should be written");
663
+
664
+        let mut state = DaemonState {
665
+            health: HealthStatus::Healthy,
666
+            requests: RequestRegistry::new(Duration::from_secs(5)),
667
+            running: true,
668
+        };
669
+        handle_connection(server, &mut state).expect("inspect should be handled");
670
+
671
+        let mut response_line = String::new();
672
+        let mut reader = BufReader::new(client);
673
+        reader
674
+            .read_line(&mut response_line)
675
+            .expect("response should be readable");
676
+        let response = ControlResponse::parse_line(&response_line).expect("response should parse");
677
+        assert_eq!(
678
+            response,
679
+            ControlResponse::Error {
680
+                code: 2,
681
+                reason: "request_not_found".to_string(),
682
+            }
683
+        );
684
+    }
685
+
592686
     #[test]
593687
     fn startup_recovery_expires_non_terminal_requests() {
594688
         let path = unique_temp_file();
garwarp/src/request.rsmodified
@@ -279,6 +279,16 @@ impl RequestRegistry {
279279
         self.entries.get(id).map(|entry| entry.owner.clone())
280280
     }
281281
 
282
+    #[must_use]
283
+    pub fn record(&self, id: &str) -> Option<RequestRecord> {
284
+        self.entries.get(id).map(|entry| RequestRecord {
285
+            id: entry.id.clone(),
286
+            owner: entry.owner.clone(),
287
+            parent_window: entry.parent_window,
288
+            state: entry.state,
289
+        })
290
+    }
291
+
282292
     #[must_use]
283293
     pub fn records(&self) -> Vec<RequestRecord> {
284294
         let mut records = self
@@ -573,4 +583,36 @@ mod tests {
573583
             .expect("duplicate cancel should be idempotent");
574584
         assert_eq!(registry.state("req-1"), Some(RequestState::Cancelled));
575585
     }
586
+
587
+    #[test]
588
+    fn record_returns_current_snapshot() {
589
+        let now = Instant::now();
590
+        let request_owner = owner(":1.2");
591
+        let mut registry = RequestRegistry::new(Duration::from_secs(5));
592
+        registry
593
+            .begin_at(
594
+                "req-1",
595
+                request_owner.clone(),
596
+                Some(ParentWindowContext::X11 { window_id: 42 }),
597
+                now,
598
+            )
599
+            .expect("request should be created");
600
+        registry
601
+            .transition_at(
602
+                "req-1",
603
+                &request_owner,
604
+                RequestState::AwaitingUser,
605
+                now + Duration::from_millis(1),
606
+            )
607
+            .expect("request should transition");
608
+
609
+        let snapshot = registry.record("req-1").expect("record should exist");
610
+        assert_eq!(snapshot.id, "req-1");
611
+        assert_eq!(snapshot.owner, request_owner);
612
+        assert_eq!(snapshot.state, RequestState::AwaitingUser);
613
+        assert_eq!(
614
+            snapshot.parent_window,
615
+            Some(ParentWindowContext::X11 { window_id: 42 })
616
+        );
617
+    }
576618
 }
garwarpctl/src/main.rsmodified
@@ -29,6 +29,9 @@ fn main() {
2929
 enum Command {
3030
     Status,
3131
     Stop,
32
+    Inspect {
33
+        id: String,
34
+    },
3235
     Version,
3336
     Help,
3437
     Begin {
@@ -50,6 +53,7 @@ fn parse_command(args: &[String]) -> Result<Command, String> {
5053
         [] => Ok(Command::Status),
5154
         [command] if command == "status" => Ok(Command::Status),
5255
         [command] if command == "stop" => Ok(Command::Stop),
56
+        [command, id] if command == "inspect" => Ok(Command::Inspect { id: id.clone() }),
5357
         [command] if command == "version" || command == "--version" || command == "-V" => {
5458
             Ok(Command::Version)
5559
         }
@@ -194,6 +198,35 @@ fn run(command: Command) -> io::Result<()> {
194198
                 )),
195199
             }
196200
         }
201
+        Command::Inspect { id } => {
202
+            let response = send_request(ControlRequest::InspectRequest { id })?;
203
+            match response {
204
+                ControlResponse::RequestSnapshot {
205
+                    id,
206
+                    state,
207
+                    sender,
208
+                    app_id,
209
+                    parent_window,
210
+                } => {
211
+                    println!("id={id}");
212
+                    println!("state={state}");
213
+                    println!("sender={sender}");
214
+                    println!("app_id={}", app_id.unwrap_or_else(|| "-".to_string()));
215
+                    println!(
216
+                        "parent_window={}",
217
+                        parent_window.unwrap_or_else(|| "-".to_string())
218
+                    );
219
+                    Ok(())
220
+                }
221
+                ControlResponse::Error { code, reason } => Err(io::Error::other(format!(
222
+                    "daemon error: code={code} reason={reason}"
223
+                ))),
224
+                other => Err(io::Error::new(
225
+                    io::ErrorKind::InvalidData,
226
+                    format!("unexpected response: {other:?}"),
227
+                )),
228
+            }
229
+        }
197230
         Command::Begin {
198231
             id,
199232
             sender,
@@ -290,6 +323,7 @@ fn print_help() {
290323
     println!("commands:");
291324
     println!("  status (default)");
292325
     println!("  stop");
326
+    println!("  inspect <id>");
293327
     println!("  begin <id> <sender> [app_id|-] [parent_window|-]");
294328
     println!("  transition <id> <sender> <awaiting_user|fulfilled|cancelled|failed> [app_id|-]");
295329
     println!("  await|fulfill|cancel|fail <id> <sender> [app_id|-]");
@@ -351,6 +385,18 @@ mod tests {
351385
         );
352386
     }
353387
 
388
+    #[test]
389
+    fn parse_inspect_command() {
390
+        let args = vec!["inspect".to_string(), "req-1".to_string()];
391
+        let command = parse_command(&args).expect("inspect command should parse");
392
+        assert_eq!(
393
+            command,
394
+            Command::Inspect {
395
+                id: "req-1".to_string()
396
+            }
397
+        );
398
+    }
399
+
354400
     #[test]
355401
     fn parse_transition_target_rejects_unknown_state() {
356402
         let parsed = parse_transition_target("bogus");