#![warn(clippy::pedantic, clippy::nursery)]
#![warn(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::format_push_string,
clippy::panic_in_result_fn,
clippy::print_stdout,
clippy::print_stderr
)]
#![cfg_attr(
test,
allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic_in_result_fn)
)]
use anyhow::Result;
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tracing::{debug, error, info};
mod args;
mod classfile;
mod generics;
mod handlers;
mod protocol;
mod session;
mod stop_point_set;
mod tools;
mod value_reads;
use handlers::RequestHandler;
use protocol::{
Alerter, JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ALERT_CAPACITY,
INVALID_REQUEST, PARSE_ERROR,
};
use tokio::sync::mpsc;
const DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
#[tokio::main]
async fn main() -> Result<()> {
let env_filter = tracing_subscriber::EnvFilter::from_default_env()
.add_directive("jdwp_mcp=info".parse()?)
.add_directive("jdwp_client=warn".parse()?);
tracing_subscriber::fmt().with_env_filter(env_filter).with_writer(std::io::stderr).init();
info!("Starting JDWP MCP Server...");
let (out_tx, mut out_rx) = mpsc::channel::<String>(ALERT_CAPACITY);
let writer = tokio::spawn(async move {
let mut stdout = tokio::io::stdout();
while let Some(line) = out_rx.recv().await {
if let Err(e) = write_message(&mut stdout, &line).await {
error!("Write error: {e}");
break;
}
}
});
let alerter = Alerter::new(out_tx.clone());
let handler = RequestHandler::new(alerter);
let mut reader = BufReader::new(tokio::io::stdin());
info!("JDWP MCP server ready, waiting for requests...");
let mut line_buf = String::new();
loop {
line_buf.clear();
match reader.read_line(&mut line_buf).await {
Ok(0) => {
info!("Client disconnected");
break;
}
Ok(_) => {
let line = line_buf.trim();
if line.is_empty() {
continue;
}
debug!("Received: {}", line);
process_line(&handler, &out_tx, line).await?;
}
Err(e) => {
error!("Read error: {}", e);
break;
}
}
}
drop(out_tx);
drop(handler);
if tokio::time::timeout(DRAIN_TIMEOUT, writer).await.is_err() {
error!("writer task did not finish draining within {DRAIN_TIMEOUT:?}");
}
info!("JDWP MCP server shutting down");
Ok(())
}
fn error_response(code: i32, message: &str) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: serde_json::Value::Null,
result: None,
error: Some(JsonRpcError { code, message: message.to_string(), data: None }),
}
}
const fn kind_of(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}
async fn write_message<W: AsyncWriteExt + Unpin>(stdout: &mut W, message: &str) -> Result<()> {
debug!("Sending: {}", message);
stdout.write_all(message.as_bytes()).await?;
stdout.write_all(b"\n").await?;
stdout.flush().await?;
Ok(())
}
async fn send_message(out: &mpsc::Sender<String>, message: String) -> Result<()> {
out.send(message).await.map_err(|_| anyhow::anyhow!("stdout writer task has gone away"))
}
async fn process_line(handler: &RequestHandler, out: &mpsc::Sender<String>, line: &str) -> Result<()> {
let value: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(e) => {
error!("Parse error: {}", e);
let response = serde_json::to_string(&error_response(PARSE_ERROR, "Parse error"))?;
return send_message(out, response).await;
}
};
if !value.is_object() {
error!("Not a JSON-RPC message: expected an object, got {}", kind_of(&value));
let response = serde_json::to_string(&error_response(
INVALID_REQUEST,
"Invalid request: a JSON-RPC message must be an object",
))?;
return send_message(out, response).await;
}
if value.get("id").is_some() {
match serde_json::from_value::<JsonRpcRequest>(value) {
Ok(request) => {
let response = handler.handle_request(request).await;
send_message(out, serde_json::to_string(&response)?).await?;
}
Err(e) => {
error!("Invalid request: {}", e);
let response = serde_json::to_string(&error_response(INVALID_REQUEST, "Invalid request"))?;
send_message(out, response).await?;
}
}
} else {
match serde_json::from_value::<JsonRpcNotification>(value) {
Ok(notification) => handler.handle_notification(¬ification),
Err(e) => error!("Invalid notification: {}", e),
}
}
Ok(())
}