From 2887e8694b66e666c80a0d2d4dca38bc75d91ae4 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sun, 8 Mar 2026 17:57:46 -0700 Subject: [PATCH] Fix desktop scroll, timeout cleanup, and OpenAPI codes --- docs/openapi.json | 18 +-- .../sandbox-agent/src/desktop_runtime.rs | 110 ++++++++++++++++-- server/packages/sandbox-agent/src/router.rs | 18 +-- 3 files changed, 121 insertions(+), 25 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index a252150..cc71dcc 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -745,7 +745,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or input failed", "content": { "application/json": { @@ -807,7 +807,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or input failed", "content": { "application/json": { @@ -869,7 +869,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or input failed", "content": { "application/json": { @@ -931,7 +931,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or input failed", "content": { "application/json": { @@ -993,7 +993,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or input failed", "content": { "application/json": { @@ -1035,7 +1035,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or input check failed", "content": { "application/json": { @@ -1097,7 +1097,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or input failed", "content": { "application/json": { @@ -1132,7 +1132,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or screenshot capture failed", "content": { "application/json": { @@ -1221,7 +1221,7 @@ } } }, - "503": { + "502": { "description": "Desktop runtime health or screenshot capture failed", "content": { "application/json": { diff --git a/server/packages/sandbox-agent/src/desktop_runtime.rs b/server/packages/sandbox-agent/src/desktop_runtime.rs index d0623ba..38608dd 100644 --- a/server/packages/sandbox-agent/src/desktop_runtime.rs +++ b/server/packages/sandbox-agent/src/desktop_runtime.rs @@ -386,8 +386,8 @@ impl DesktopRuntime { request.y.to_string(), ]; - append_scroll_clicks(&mut args, delta_y, 4, 5); - append_scroll_clicks(&mut args, delta_x, 6, 7); + append_scroll_clicks(&mut args, delta_y, 5, 4); + append_scroll_clicks(&mut args, delta_x, 7, 6); self.run_input_command_locked(&state, &ready, args).await?; self.mouse_position_locked(&state, &ready).await @@ -1192,6 +1192,8 @@ async fn run_command_output( environment: &HashMap, timeout: Duration, ) -> Result { + use tokio::io::AsyncReadExt; + let mut child = Command::new(command); child.args(args); child.envs(environment); @@ -1199,11 +1201,51 @@ async fn run_command_output( child.stdout(Stdio::piped()); child.stderr(Stdio::piped()); - let output = tokio::time::timeout(timeout, child.output()) + let mut child = child.spawn().map_err(|err| err.to_string())?; + let stdout = child + .stdout + .take() + .ok_or_else(|| "failed to capture child stdout".to_string())?; + let stderr = child + .stderr + .take() + .ok_or_else(|| "failed to capture child stderr".to_string())?; + + let stdout_task = tokio::spawn(async move { + let mut stdout = stdout; + let mut bytes = Vec::new(); + stdout.read_to_end(&mut bytes).await.map(|_| bytes) + }); + let stderr_task = tokio::spawn(async move { + let mut stderr = stderr; + let mut bytes = Vec::new(); + stderr.read_to_end(&mut bytes).await.map(|_| bytes) + }); + + let status = match tokio::time::timeout(timeout, child.wait()).await { + Ok(result) => result.map_err(|err| err.to_string())?, + Err(_) => { + terminate_child(&mut child).await?; + let _ = stdout_task.await; + let _ = stderr_task.await; + return Err(format!("command timed out after {}s", timeout.as_secs())); + } + }; + + let stdout = stdout_task .await - .map_err(|_| format!("command timed out after {}s", timeout.as_secs()))? + .map_err(|err| err.to_string())? .map_err(|err| err.to_string())?; - Ok(output) + let stderr = stderr_task + .await + .map_err(|err| err.to_string())? + .map_err(|err| err.to_string())?; + + Ok(Output { + status, + stdout, + stderr, + }) } async fn terminate_child(child: &mut Child) -> Result<(), String> { @@ -1349,11 +1391,20 @@ fn mouse_button_code(button: DesktopMouseButton) -> u8 { } } -fn append_scroll_clicks(args: &mut Vec, delta: i32, up_button: u8, down_button: u8) { +fn append_scroll_clicks( + args: &mut Vec, + delta: i32, + positive_button: u8, + negative_button: u8, +) { if delta == 0 { return; } - let button = if delta > 0 { up_button } else { down_button }; + let button = if delta > 0 { + positive_button + } else { + negative_button + }; let repeat = delta.unsigned_abs(); args.push("click".to_string()); if repeat > 1 { @@ -1402,4 +1453,49 @@ mod tests { let args = press_key_args("--help".to_string()); assert_eq!(args, vec!["key", "--", "--help"]); } + + #[test] + fn append_scroll_clicks_uses_positive_direction_buttons() { + let mut args = Vec::new(); + append_scroll_clicks(&mut args, 2, 5, 4); + append_scroll_clicks(&mut args, -3, 7, 6); + assert_eq!( + args, + vec!["click", "--repeat", "2", "5", "click", "--repeat", "3", "6"] + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn run_command_output_kills_child_on_timeout() { + let pid_file = std::env::temp_dir().join(format!( + "sandbox-agent-desktop-runtime-timeout-{}.pid", + std::process::id() + )); + let _ = std::fs::remove_file(&pid_file); + let command = format!("echo $$ > {}; exec sleep 30", pid_file.display()); + let args = vec!["-c".to_string(), command]; + + let error = run_command_output("sh", &args, &HashMap::new(), Duration::from_millis(200)) + .await + .expect_err("command should time out"); + assert!(error.contains("timed out")); + + let pid = std::fs::read_to_string(&pid_file) + .expect("pid file should exist") + .trim() + .parse::() + .expect("pid should parse"); + + for _ in 0..20 { + if !process_exists(pid) { + let _ = std::fs::remove_file(&pid_file); + return; + } + tokio::time::sleep(Duration::from_millis(50)).await; + } + + let _ = std::fs::remove_file(&pid_file); + panic!("timed out child process {pid} still exists after timeout cleanup"); + } } diff --git a/server/packages/sandbox-agent/src/router.rs b/server/packages/sandbox-agent/src/router.rs index 757b36b..9c1a690 100644 --- a/server/packages/sandbox-agent/src/router.rs +++ b/server/packages/sandbox-agent/src/router.rs @@ -624,7 +624,7 @@ async fn post_v1_desktop_stop( responses( (status = 200, description = "Desktop screenshot as PNG bytes"), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or screenshot capture failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or screenshot capture failed", body = ProblemDetails) ) )] async fn get_v1_desktop_screenshot( @@ -652,7 +652,7 @@ async fn get_v1_desktop_screenshot( (status = 200, description = "Desktop screenshot region as PNG bytes"), (status = 400, description = "Invalid screenshot region", body = ProblemDetails), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or screenshot capture failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or screenshot capture failed", body = ProblemDetails) ) )] async fn get_v1_desktop_screenshot_region( @@ -673,7 +673,7 @@ async fn get_v1_desktop_screenshot_region( responses( (status = 200, description = "Desktop mouse position", body = DesktopMousePositionResponse), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or input check failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or input check failed", body = ProblemDetails) ) )] async fn get_v1_desktop_mouse_position( @@ -696,7 +696,7 @@ async fn get_v1_desktop_mouse_position( (status = 200, description = "Desktop mouse position after move", body = DesktopMousePositionResponse), (status = 400, description = "Invalid mouse move request", body = ProblemDetails), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or input failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or input failed", body = ProblemDetails) ) )] async fn post_v1_desktop_mouse_move( @@ -720,7 +720,7 @@ async fn post_v1_desktop_mouse_move( (status = 200, description = "Desktop mouse position after click", body = DesktopMousePositionResponse), (status = 400, description = "Invalid mouse click request", body = ProblemDetails), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or input failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or input failed", body = ProblemDetails) ) )] async fn post_v1_desktop_mouse_click( @@ -744,7 +744,7 @@ async fn post_v1_desktop_mouse_click( (status = 200, description = "Desktop mouse position after drag", body = DesktopMousePositionResponse), (status = 400, description = "Invalid mouse drag request", body = ProblemDetails), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or input failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or input failed", body = ProblemDetails) ) )] async fn post_v1_desktop_mouse_drag( @@ -768,7 +768,7 @@ async fn post_v1_desktop_mouse_drag( (status = 200, description = "Desktop mouse position after scroll", body = DesktopMousePositionResponse), (status = 400, description = "Invalid mouse scroll request", body = ProblemDetails), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or input failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or input failed", body = ProblemDetails) ) )] async fn post_v1_desktop_mouse_scroll( @@ -792,7 +792,7 @@ async fn post_v1_desktop_mouse_scroll( (status = 200, description = "Desktop keyboard action result", body = DesktopActionResponse), (status = 400, description = "Invalid keyboard type request", body = ProblemDetails), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or input failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or input failed", body = ProblemDetails) ) )] async fn post_v1_desktop_keyboard_type( @@ -816,7 +816,7 @@ async fn post_v1_desktop_keyboard_type( (status = 200, description = "Desktop keyboard action result", body = DesktopActionResponse), (status = 400, description = "Invalid keyboard press request", body = ProblemDetails), (status = 409, description = "Desktop runtime is not ready", body = ProblemDetails), - (status = 503, description = "Desktop runtime health or input failed", body = ProblemDetails) + (status = 502, description = "Desktop runtime health or input failed", body = ProblemDetails) ) )] async fn post_v1_desktop_keyboard_press(