use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use serde_json::{json, Value};
use tracing::debug;
use crate::{
error::PortalError,
payload::{
JsonRpcError, JsonRpcRequest, JsonRpcResponse, SandboxCommandRunParams,
SandboxReplRunParams, JSONRPC_VERSION,
},
portal::command::create_command_executor,
state::SharedState,
};
#[cfg(any(feature = "python", feature = "nodejs"))]
use crate::portal::repl::{start_engines, Language};
pub async fn json_rpc_handler(
State(state): State<SharedState>,
req: Json<JsonRpcRequest>,
) -> Result<impl IntoResponse, PortalError> {
let request = req.0;
debug!(?request, "Received JSON-RPC request");
if request.jsonrpc != JSONRPC_VERSION {
let error = JsonRpcError {
code: -32600,
message: "Invalid or missing jsonrpc version field".to_string(),
data: None,
};
return Ok((
StatusCode::BAD_REQUEST,
Json(JsonRpcResponse::error(error, request.id.clone())),
));
}
let method = request.method.as_str();
let id = request.id.clone();
match method {
"sandbox.repl.run" => {
match sandbox_run_impl(state, request.params).await {
Ok(result) => {
Ok((StatusCode::OK, Json(JsonRpcResponse::success(result, id))))
}
Err(e) => {
Ok(create_error_response(e, id))
}
}
}
"sandbox.command.run" => {
match sandbox_command_run_impl(state, request.params).await {
Ok(result) => {
Ok((StatusCode::OK, Json(JsonRpcResponse::success(result, id))))
}
Err(e) => {
Ok(create_error_response(e, id))
}
}
}
_ => {
let error = PortalError::MethodNotFound(format!("Method not found: {}", method));
Ok(create_error_response(error, id))
}
}
}
async fn sandbox_run_impl(_state: SharedState, params: Value) -> Result<Value, PortalError> {
debug!(?params, "Sandbox run method called");
let params: SandboxReplRunParams = serde_json::from_value(params)
.map_err(|e| PortalError::JsonRpc(format!("Invalid parameters: {}", e)))?;
#[cfg(any(feature = "python", feature = "nodejs"))]
let language;
match params.language.to_lowercase().as_str() {
#[cfg(feature = "python")]
"python" => language = Language::Python,
#[cfg(feature = "nodejs")]
"node" | "nodejs" | "javascript" => language = Language::Node,
_ => {
let error_msg = match params.language.to_lowercase().as_str() {
"python" => {
"Python language support is not enabled. Recompile with --features python"
.to_string()
}
"node" | "nodejs" | "javascript" => {
"Node.js language support is not enabled. Recompile with --features nodejs"
.to_string()
}
_ => format!("Unsupported language: {}", params.language),
};
return Err(PortalError::JsonRpc(error_msg));
}
};
#[cfg(any(feature = "python", feature = "nodejs"))]
let engine_handle = {
let mut lock = _state.engine_handle.lock().await;
if let Some(ref handle) = *lock {
handle.clone()
} else {
let handle = start_engines()
.await
.map_err(|e| PortalError::Internal(format!("Failed to start engines: {}", e)))?;
*lock = Some(handle.clone());
handle
}
};
#[cfg(any(feature = "python", feature = "nodejs"))]
debug!("Language: {}", params.language);
#[cfg(any(feature = "python", feature = "nodejs"))]
let temp_id = uuid::Uuid::new_v4().to_string();
#[cfg(any(feature = "python", feature = "nodejs"))]
let lines = engine_handle
.eval(¶ms.code, language, &temp_id, params.timeout)
.await
.map_err(|e| PortalError::Internal(format!("REPL execution failed: {}", e)))?;
#[cfg(any(feature = "python", feature = "nodejs"))]
debug!("REPL execution produced {} output lines", lines.len());
#[cfg(any(feature = "python", feature = "nodejs"))]
let output_lines: Vec<Value> = lines
.iter()
.map(|line| {
json!({
"stream": match line.stream {
crate::portal::repl::Stream::Stdout => "stdout",
crate::portal::repl::Stream::Stderr => "stderr",
},
"text": line.text,
})
})
.collect();
#[cfg(any(feature = "python", feature = "nodejs"))]
let result = json!({
"status": "success".to_string(),
"language": params.language.to_string(),
"output": output_lines,
});
#[cfg(any(feature = "python", feature = "nodejs"))]
debug!("Returning result with output: {}", result);
#[cfg(any(feature = "python", feature = "nodejs"))]
Ok(result)
}
async fn sandbox_command_run_impl(state: SharedState, params: Value) -> Result<Value, PortalError> {
debug!(?params, "Sandbox command run method called");
let params: SandboxCommandRunParams = serde_json::from_value(params)
.map_err(|e| PortalError::JsonRpc(format!("Invalid parameters: {}", e)))?;
let cmd_handle = {
let mut lock = state.command_handle.lock().await;
if let Some(ref handle) = *lock {
handle.clone()
} else {
let handle = create_command_executor();
*lock = Some(handle.clone());
handle
}
};
let (exit_code, output_lines) = cmd_handle
.execute(¶ms.command, params.args.clone(), params.timeout)
.await
.map_err(|e| PortalError::Internal(format!("Command execution failed: {}", e)))?;
let formatted_lines = output_lines
.iter()
.map(|line| {
json!({
"stream": match line.stream {
crate::portal::repl::Stream::Stdout => "stdout",
crate::portal::repl::Stream::Stderr => "stderr",
},
"text": line.text,
})
})
.collect::<Vec<Value>>();
let result = json!({
"command": params.command,
"args": params.args,
"exit_code": exit_code,
"success": exit_code == 0,
"output": formatted_lines,
});
debug!("Returning command result with output: {}", result);
Ok(result)
}
fn create_error_response(
error: PortalError,
id: Option<Value>,
) -> (StatusCode, Json<JsonRpcResponse>) {
let code = match &error {
PortalError::JsonRpc(_) => -32600, PortalError::MethodNotFound(_) => -32601, PortalError::Parse(_) => -32700, PortalError::Internal(_) => -32603, };
let json_rpc_error = JsonRpcError {
code,
message: error.to_string(),
data: None,
};
(
StatusCode::BAD_REQUEST,
Json(JsonRpcResponse::error(json_rpc_error, id)),
)
}