use bugstalker::dap;
use bugstalker::dap::yadap;
use bugstalker::debugger::rust;
use bugstalker::log::LOGGER_SWITCHER;
use bugstalker::ui;
use bugstalker::ui::config::{Theme, UIConfig};
use bugstalker::ui::supervisor::{DebugeeSource, Interface};
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};
use std::fmt::Display;
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[clap(long)]
#[arg(default_value_t = false)]
tui: bool,
#[clap(long)]
#[arg(default_value_t = false, aliases=["dap"])]
dap_local: bool,
#[clap(long)]
dap_remote: Option<String>,
#[clap(long)]
dap_oneshot: bool,
#[clap(long)]
dap_log_file: Option<PathBuf>,
#[clap(long, short)]
pid: Option<i32>,
#[clap(long)]
cwd: Option<PathBuf>,
debugee: Option<String>,
#[clap(short, long)]
std_lib_path: Option<String>,
#[clap(short, long)]
oracle: Vec<String>,
#[arg(raw(true))]
args: Vec<String>,
#[clap(short, long)]
#[arg(default_value = "solarized_dark")]
theme: String,
#[clap(long, env)]
keymap_file: Option<String>,
#[clap(long, env)]
#[arg(default_value_t = false)]
save_history: bool,
}
fn print_fatal_and_exit(kind: ErrorKind, message: impl Display) -> ! {
let mut cmd = Args::command();
_ = cmd.error(kind, message).print();
exit(1);
}
trait FatalResult<T> {
fn unwrap_or_exit(self, kind: ErrorKind, message: impl Display) -> T;
}
impl<T, E: Display> FatalResult<T> for Result<T, E> {
fn unwrap_or_exit(self, kind: ErrorKind, message: impl Display) -> T {
match self {
Ok(ok) => ok,
Err(err) => print_fatal_and_exit(kind, format!("{message}: {err:#}")),
}
}
}
impl From<&Args> for UIConfig {
fn from(args: &Args) -> Self {
Self {
theme: Theme::from_str(&args.theme)
.unwrap_or_exit(ErrorKind::InvalidValue, "Not an available theme"),
tui_keymap: ui::tui::config::KeyMap::from_file(args.keymap_file.as_deref())
.unwrap_or_default(),
save_history: args.save_history,
}
}
}
fn main() {
let logger = env_logger::Logger::from_default_env();
let filter = logger.filter();
LOGGER_SWITCHER.switch(logger, filter);
let args = Args::parse();
ui::config::set(UIConfig::from(&args));
fn fun_name(p: &String) -> PathBuf {
PathBuf::from(p)
}
rust::Environment::init(args.std_lib_path.as_ref().map(fun_name));
let debugee_src = || {
if let Some(ref debugee) = args.debugee {
DebugeeSource::File {
path: debugee,
args: &args.args,
cwd: args.cwd.as_deref(),
}
} else if let Some(pid) = args.pid {
DebugeeSource::Process { pid }
} else {
print_fatal_and_exit(
ErrorKind::ArgumentConflict,
"Please provide a debugee name or use a \"-p\" option for attach to already running process",
);
}
};
let interface = if args.dap_local {
let tracer = args.dap_log_file.as_ref().map(|path| {
dap::tracer::FileTracer::new(path).unwrap_or_exit(ErrorKind::Io, "DAP server error")
});
Interface::DAP { tracer }
} else if let Some(listen_addr) = &args.dap_remote {
run_dap_tcp_server(&args, listen_addr)
.unwrap_or_exit(ErrorKind::Io, "DAP TCP server error");
return;
} else if args.tui {
Interface::TUI {
source: debugee_src(),
}
} else {
Interface::Default {
source: debugee_src(),
}
};
ui::supervisor::Supervisor::run(interface, &args.oracle)
.unwrap_or_exit(ErrorKind::InvalidSubcommand, "Application error")
}
fn run_dap_tcp_server(args: &Args, listen_addr: &str) -> anyhow::Result<()> {
use log::warn;
use std::net::{SocketAddr, TcpListener};
let addr: SocketAddr = listen_addr
.parse()
.map_err(|_| anyhow::anyhow!("Invalid listen address: {}", listen_addr))?;
let listener =
TcpListener::bind(addr).map_err(|e| anyhow::anyhow!("Failed to bind {}: {}", addr, e))?;
log::info!(target: "dap", "DAP TCP server listening on {addr}");
let tracer = match &args.dap_log_file {
Some(path) => Some(dap::tracer::FileTracer::new(path)?),
None => None,
};
loop {
let (stream, peer) = match listener.accept() {
Ok(v) => v,
Err(err) => {
warn!(target: "dap", "accept failed: {err:#}");
continue;
}
};
log::info!(target: "dap", "DAP client connected: {peer}");
if let Some(t) = &tracer {
t.line(&format!("client connected: {peer}"));
}
let io = match dap::transport::new_tcp_transport(stream, tracer.clone()) {
Ok(v) => v,
Err(err) => {
warn!(target: "dap", "failed to init DAP I/O: {err:#}");
continue;
}
};
let transport = Arc::new(Mutex::new(io));
let res = yadap::session::DebugSession::new(transport).run(args.oracle.clone());
if let Err(err) = res {
warn!(target: "dap", "session ended with error: {err:#}");
if let Some(t) = &tracer {
t.line(&format!("session error: {err:#}"));
}
} else if let Some(t) = &tracer {
t.line("session finished OK");
}
if args.dap_oneshot {
break;
}
}
Ok(())
}