2513
2514
/// `command_output` tool: output since the last poll + running/exited status.
2515
fn exec_command_output(arguments: &str, owner: Option<&str>) -> String {
2516
- poll_command_output(arguments, owner).text
2516
+ poll_command_output(arguments, owner, false).text
2517
}
2518
2519
/// One `command_output` check, with the tool text and whether the task is
2534
}
2535
}
2536
2537
-fn poll_command_output(arguments: &str, owner: Option<&str>) -> CommandPoll {
2537
+/// `hold_while_running` is the long-poll's "not my last look" flag: a task
2538
+/// that is still running keeps its output buffered and the returned text is
2539
+/// empty, so the call that finally reports is the one that drains it.
2540
+fn poll_command_output(
2541
+ arguments: &str,
2542
+ owner: Option<&str>,
2543
+ hold_while_running: bool,
2544
+) -> CommandPoll {
2545
let task_id = match parse_task_id(arguments) {
2546
Ok(id) => id,
2547
Err(err) => return CommandPoll::done(err),
2557
2558
let was_running = task.exit_code.is_none();
2559
let exit_code = poll_exit_code(task);
2560
+ if hold_while_running && exit_code.is_none() {
2561
+ return CommandPoll {
2562
+ text: String::new(),
2563
+ running: true,
2564
+ };
2565
+ }
2566
if was_running && exit_code.is_some() {
2567
// The child just exited; give the reader threads a moment to flush
2568
// the final output through the pipes before draining the buffer.
2611
loop {
2612
let arguments = arguments.to_owned();
2613
let owner = owner.clone();
2601
- let poll =
2602
- tokio::task::spawn_blocking(move || poll_command_output(&arguments, owner.as_deref()))
2603
- .await
2604
- .unwrap_or_else(|err| {
2605
- CommandPoll::done(format!("Error: command_output task failed: {err}"))
2606
- });
2614
+ // Every poll drains the task's buffer, so only the last look may take
2615
+ // the output of a running task; an earlier one would throw it away.
2616
+ let last_look = wait_seconds == 0 || tokio::time::Instant::now() >= deadline;
2617
+ let poll = tokio::task::spawn_blocking(move || {
2618
+ poll_command_output(&arguments, owner.as_deref(), !last_look)
2619
+ })
2620
+ .await
2621
+ .unwrap_or_else(|err| {
2622
+ CommandPoll::done(format!("Error: command_output task failed: {err}"))
2623
+ });
2624
2608
- if wait_seconds == 0 || !poll.running || tokio::time::Instant::now() >= deadline {
2625
+ if last_look || !poll.running {
2626
return poll.text;
2627
}
2628
3808
}
3809
}
3810
3811
+ /// The output part of a `command_output` reply. The status line quotes
3812
+ /// the command, so matching the whole reply finds "echo done" whether or
3813
+ /// not `done` was ever printed.
3814
+ fn polled_output(poll: &str) -> &str {
3815
+ poll.split_once("New output since the last check:")
3816
+ .map_or("", |(_, output)| output)
3817
+ }
3818
+
3819
/// Wait (bounded) for a background task's child to exit.
3820
fn wait_for_background_exit(task_id: u64) {
3821
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
3897
let poll = exec_command_output_wait(&poll_args, None).await;
3898
3899
assert!(poll.contains("exited with code 0"), "got: {poll}");
3875
- assert!(poll.contains("done"), "got: {poll}");
3900
+ assert!(polled_output(&poll).contains("done"), "got: {poll}");
3901
+ }
3902
+
3903
+ #[tokio::test]
3904
+ async fn test_command_output_wait_keeps_output_printed_before_exit() {
3905
+ #[cfg(unix)]
3906
+ let command = "echo start; sleep 1; echo done";
3907
+ #[cfg(windows)]
3908
+ let command = "echo start&& ping -n 2 127.0.0.1 > nul&& echo done";
3909
+
3910
+ let args = serde_json::json!({
3911
+ "command": command,
3912
+ "cwd": std::env::temp_dir(),
3913
+ "run_in_background": true
3914
+ })
3915
+ .to_string();
3916
+ let task_id = background_task_id(&exec_run_command(&args, None));
3917
+
3918
+ let poll_args = serde_json::json!({ "task_id": task_id, "wait_seconds": 5 }).to_string();
3919
+ let poll = exec_command_output_wait(&poll_args, None).await;
3920
+
3921
+ // "start" is printed well before the exit the wait returns on; the
3922
+ // polls in between must not have drained it away.
3923
+ assert!(poll.contains("exited with code 0"), "got: {poll}");
3924
+ assert!(polled_output(&poll).contains("start"), "got: {poll}");
3925
+ assert!(polled_output(&poll).contains("done"), "got: {poll}");
3926
+ }
3927
+
3928
+ #[tokio::test]
3929
+ async fn test_command_output_wait_reports_output_of_a_running_task() {
3930
+ #[cfg(unix)]
3931
+ let command = "echo start; sleep 30";
3932
+ #[cfg(windows)]
3933
+ let command = "echo start&& ping -n 31 127.0.0.1 > nul";
3934
+
3935
+ let args = serde_json::json!({
3936
+ "command": command,
3937
+ "cwd": std::env::temp_dir(),
3938
+ "run_in_background": true
3939
+ })
3940
+ .to_string();
3941
+ let task_id = background_task_id(&exec_run_command(&args, None));
3942
+
3943
+ let poll_args = serde_json::json!({ "task_id": task_id, "wait_seconds": 1 }).to_string();
3944
+ let poll = exec_command_output_wait(&poll_args, None).await;
3945
+
3946
+ assert!(poll.contains("still running"), "got: {poll}");
3947
+ assert!(polled_output(&poll).contains("start"), "got: {poll}");
3948
+ exec_kill_command(&serde_json::json!({ "task_id": task_id }).to_string(), None);
3949
}
3950
3951
#[tokio::test]