use std::process::ExitCode;
use std::str::FromStr;
use std::sync::Arc;
use clap::Parser;
use dapz::interceptors::Interceptor;
use dapz::interceptors::InterceptorChain;
use dapz::interceptors::capping::CappingInterceptor;
use dapz::interceptors::output::OutputCompressor;
use dapz::interceptors::stacktrace::StackTraceCompressor;
use dapz::interceptors::variables::VariablesCompressor;
use dapz::{CappingConfig, Config, OutputFormat, Proxy, StdioTransport, Transport};
use tokio::sync::RwLock;
use tracing_subscriber::EnvFilter;
#[derive(Parser, Debug)]
#[command(version, about)]
struct Cli {
#[arg(short, long, env = "DAPZ_BACKEND_CMD")]
backend: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
backend_args: Vec<String>,
#[arg(short, long, env = "DAPZ_LOG_LEVEL", default_value = "info")]
log_level: String,
#[arg(long, env = "DAPZ_MAX_FRAMES", default_value_t = 0)]
max_frames: usize,
#[arg(long, env = "DAPZ_MAX_VARIABLES", default_value_t = 0)]
max_variables: usize,
#[arg(long, env = "DAPZ_MAX_OUTPUT_LENGTH", default_value_t = 0)]
max_output_length: usize,
#[arg(
long = "compress-output",
env = "DAPZ_ENABLE_OUTPUT_COMPRESS",
default_value_t = true
)]
compress_output: bool,
#[arg(
long = "compress-variables",
env = "DAPZ_ENABLE_VARIABLES_COMPRESS",
default_value_t = true
)]
compress_variables: bool,
#[arg(
long = "compress-stacktrace",
env = "DAPZ_ENABLE_STACKTRACE_COMPRESS",
default_value_t = true
)]
compress_stacktrace: bool,
#[arg(short, long, env = "DAPZ_OUTPUT_FORMAT", default_value = "json")]
output: String,
}
#[tokio::main]
async fn main() -> ExitCode {
let args = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::builder().parse_lossy(&args.log_level))
.with_target(false)
.init();
let output_format = match OutputFormat::from_str(&args.output) {
Ok(f) => f,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
let config = match build_config(&args, output_format) {
Ok(c) => c,
Err(code) => return code,
};
let shared_config = Arc::new(RwLock::new(config));
let backend_cmd = shared_config.blocking_read().backend_cmd.clone();
let transport: Box<dyn Transport> =
match StdioTransport::spawn(&backend_cmd, &args.backend_args) {
Ok(t) => Box::new(t),
Err(e) => {
eprintln!("Failed to start backend server: {e}");
return ExitCode::FAILURE;
}
};
let interceptor_chain = build_interceptor_chain(&shared_config);
let mut proxy = Proxy::new(shared_config, transport, interceptor_chain);
if let Err(e) = proxy.start().await {
tracing::error!(error = %e, "Proxy exited with error");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
fn build_config(args: &Cli, output_format: OutputFormat) -> Result<Config, ExitCode> {
Config::builder()
.backend_cmd(&args.backend)
.capping(CappingConfig {
max_frames: args.max_frames,
max_variables: args.max_variables,
max_output_length: args.max_output_length,
})
.enable_output_compress(args.compress_output)
.enable_variables_compress(args.compress_variables)
.enable_stacktrace_compress(args.compress_stacktrace)
.output_format(output_format)
.log_level(&args.log_level)
.build()
.map_err(|e| {
eprintln!("Configuration error: {e}");
ExitCode::FAILURE
})
}
fn build_interceptor_chain(shared_config: &Arc<RwLock<Config>>) -> InterceptorChain {
let interceptors: Vec<Box<dyn Interceptor>> = vec![
Box::new(CappingInterceptor::new(
shared_config.blocking_read().capping.max_frames,
shared_config.blocking_read().capping.max_variables,
shared_config.blocking_read().capping.max_output_length,
)),
Box::new(OutputCompressor),
Box::new(VariablesCompressor),
Box::new(StackTraceCompressor),
];
tracing::info!("Interceptor chain built: capping, output, variables, stacktrace");
InterceptorChain::new(interceptors, shared_config.clone())
}