huskies: merge 855
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
//! MCP shell command execution: tool_run_command + SSE streaming variant.
|
||||
//! MCP shell command execution: `tool_run_command`.
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
use poem::{Body, Response};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -89,169 +86,6 @@ pub(crate) async fn tool_run_command(args: &Value, ctx: &AppContext) -> Result<S
|
||||
}
|
||||
}
|
||||
|
||||
/// SSE streaming run_command: spawns the process and emits stdout/stderr lines
|
||||
pub(crate) fn handle_run_command_sse(
|
||||
id: Option<Value>,
|
||||
params: &Value,
|
||||
ctx: &AppContext,
|
||||
) -> Response {
|
||||
use super::super::{JsonRpcResponse, to_sse_response};
|
||||
|
||||
let args = params.get("arguments").cloned().unwrap_or(json!({}));
|
||||
|
||||
let command = match args.get("command").and_then(|v| v.as_str()) {
|
||||
Some(c) => c.to_string(),
|
||||
None => {
|
||||
return to_sse_response(JsonRpcResponse::error(
|
||||
id,
|
||||
-32602,
|
||||
"Missing required argument: command".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let working_dir = match args.get("working_dir").and_then(|v| v.as_str()) {
|
||||
Some(d) => d.to_string(),
|
||||
None => {
|
||||
return to_sse_response(JsonRpcResponse::error(
|
||||
id,
|
||||
-32602,
|
||||
"Missing required argument: working_dir".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let timeout_secs = args
|
||||
.get("timeout")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(DEFAULT_TIMEOUT_SECS)
|
||||
.min(MAX_TIMEOUT_SECS);
|
||||
|
||||
if let Some(reason) = is_dangerous(&command) {
|
||||
return to_sse_response(JsonRpcResponse::error(id, -32602, reason));
|
||||
}
|
||||
|
||||
let canonical_dir = match validate_working_dir(&working_dir, ctx) {
|
||||
Ok(d) => d,
|
||||
Err(e) => return to_sse_response(JsonRpcResponse::error(id, -32602, e)),
|
||||
};
|
||||
|
||||
let final_id = id;
|
||||
|
||||
let stream = async_stream::stream! {
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
|
||||
let mut child = match tokio::process::Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.current_dir(&canonical_dir)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let resp = JsonRpcResponse::success(
|
||||
final_id,
|
||||
json!({
|
||||
"content": [{"type": "text", "text": format!("Failed to spawn process: {e}")}],
|
||||
"isError": true
|
||||
}),
|
||||
);
|
||||
if let Ok(s) = serde_json::to_string(&resp) {
|
||||
yield Ok::<_, std::io::Error>(format!("data: {s}\n\n"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let stdout = child.stdout.take().expect("stdout piped");
|
||||
let stderr = child.stderr.take().expect("stderr piped");
|
||||
let mut stdout_lines = tokio::io::BufReader::new(stdout).lines();
|
||||
let mut stderr_lines = tokio::io::BufReader::new(stderr).lines();
|
||||
|
||||
let deadline = tokio::time::Instant::now()
|
||||
+ std::time::Duration::from_secs(timeout_secs);
|
||||
let mut stdout_done = false;
|
||||
let mut stderr_done = false;
|
||||
let mut timed_out = false;
|
||||
|
||||
loop {
|
||||
if stdout_done && stderr_done {
|
||||
break;
|
||||
}
|
||||
|
||||
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
||||
if remaining.is_zero() {
|
||||
timed_out = true;
|
||||
let _ = child.kill().await;
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
line = stdout_lines.next_line(), if !stdout_done => {
|
||||
match line {
|
||||
Ok(Some(l)) => {
|
||||
let notif = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/tools/progress",
|
||||
"params": { "stream": "stdout", "line": l }
|
||||
});
|
||||
if let Ok(s) = serde_json::to_string(¬if) {
|
||||
yield Ok::<_, std::io::Error>(format!("data: {s}\n\n"));
|
||||
}
|
||||
}
|
||||
_ => { stdout_done = true; }
|
||||
}
|
||||
}
|
||||
line = stderr_lines.next_line(), if !stderr_done => {
|
||||
match line {
|
||||
Ok(Some(l)) => {
|
||||
let notif = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/tools/progress",
|
||||
"params": { "stream": "stderr", "line": l }
|
||||
});
|
||||
if let Ok(s) = serde_json::to_string(¬if) {
|
||||
yield Ok::<_, std::io::Error>(format!("data: {s}\n\n"));
|
||||
}
|
||||
}
|
||||
_ => { stderr_done = true; }
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(remaining) => {
|
||||
timed_out = true;
|
||||
let _ = child.kill().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let exit_code = child.wait().await.ok().and_then(|s| s.code()).unwrap_or(-1);
|
||||
|
||||
let summary = json!({
|
||||
"exit_code": exit_code,
|
||||
"timed_out": timed_out,
|
||||
});
|
||||
|
||||
let final_resp = JsonRpcResponse::success(
|
||||
final_id,
|
||||
json!({
|
||||
"content": [{"type": "text", "text": summary.to_string()}]
|
||||
}),
|
||||
);
|
||||
if let Ok(s) = serde_json::to_string(&final_resp) {
|
||||
yield Ok::<_, std::io::Error>(format!("data: {s}\n\n"));
|
||||
}
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(poem::http::StatusCode::OK)
|
||||
.header("Content-Type", "text/event-stream")
|
||||
.header("Cache-Control", "no-cache")
|
||||
.body(Body::from_bytes_stream(stream.map(|r| r.map(Bytes::from))))
|
||||
}
|
||||
|
||||
/// Run the project's test suite (`script/test`) and block until complete.
|
||||
///
|
||||
/// Spawns the test process, then polls every second server-side until the
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
mod exec;
|
||||
mod script;
|
||||
|
||||
pub(crate) use exec::{handle_run_command_sse, tool_run_command};
|
||||
pub(crate) use exec::tool_run_command;
|
||||
pub(crate) use script::{
|
||||
tool_get_test_result, tool_run_build, tool_run_check, tool_run_lint, tool_run_tests,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user