add request list command
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
265e97fd22c9db1b7fc4f143e153a930a97b4433- Parents
-
8bc8337 - Tree
a629a35
265e97f
265e97fd22c9db1b7fc4f143e153a930a97b44338bc8337
a629a35| Status | File | + | - |
|---|---|---|---|
| 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"); |