pub(crate) mod actor;
pub(crate) mod handlers;
pub(crate) mod types;
use std::net::SocketAddr;
use axum::{routing::post, Router};
use log::{error, info};
use tokio::sync::oneshot;
use tower_http::cors::CorsLayer;
use crate::{ForkConfig, ForkError};
use self::handlers::{jsonrpc_handler, AppState};
#[derive(Debug, thiserror::Error)]
pub enum ServeError {
#[error("fork build failed: {0}")]
Fork(#[from] ForkError),
#[error("listener I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("worker thread aborted before reporting build status")]
WorkerAborted,
}
#[must_use = "call `serve()` to start the server"]
pub struct ServerBuilder {
config: ForkConfig,
listen: Option<SocketAddr>,
}
impl ServerBuilder {
pub fn listen(mut self, addr: SocketAddr) -> Self {
self.listen = Some(addr);
self
}
pub async fn start(self) -> std::result::Result<RunningServer, ServeError> {
let addr = self
.listen
.unwrap_or_else(|| "127.0.0.1:8000".parse().expect("hard-coded address parses"));
let (actor, ready) = actor::spawn(self.config);
match ready.await {
Ok(Ok(())) => {}
Ok(Err(e)) => {
error!("soroban-fork: fork build failed: {e}");
return Err(ServeError::Fork(e));
}
Err(_) => return Err(ServeError::WorkerAborted),
}
let state = AppState { actor };
let app = Router::new()
.route("/", post(jsonrpc_handler))
.layer(CorsLayer::permissive())
.with_state(state);
let listener = tokio::net::TcpListener::bind(addr).await?;
let local_addr = listener.local_addr()?;
info!("soroban-fork: serving JSON-RPC on http://{local_addr}");
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let server_task = tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async move {
let _ = shutdown_rx.await;
})
.await
});
Ok(RunningServer {
local_addr,
shutdown_tx: Some(shutdown_tx),
server_task,
})
}
pub async fn serve(self) -> std::result::Result<(), ServeError> {
let running = self.start().await?;
running.run_until_signal().await
}
}
#[must_use = "RunningServer must be awaited or shut down explicitly"]
pub struct RunningServer {
local_addr: SocketAddr,
shutdown_tx: Option<oneshot::Sender<()>>,
server_task: tokio::task::JoinHandle<std::io::Result<()>>,
}
impl RunningServer {
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
pub async fn run_until_signal(mut self) -> std::result::Result<(), ServeError> {
shutdown_signal().await;
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
self.server_task
.await
.map_err(|e| ServeError::Io(std::io::Error::other(e.to_string())))??;
Ok(())
}
pub async fn shutdown(mut self) -> std::result::Result<(), ServeError> {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
self.server_task
.await
.map_err(|e| ServeError::Io(std::io::Error::other(e.to_string())))??;
Ok(())
}
}
pub struct Server;
impl Server {
pub fn builder(config: ForkConfig) -> ServerBuilder {
ServerBuilder {
config,
listen: None,
}
}
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl-C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => info!("soroban-fork: SIGINT received, shutting down"),
_ = terminate => info!("soroban-fork: SIGTERM received, shutting down"),
}
}