diff --git a/Cargo.lock b/Cargo.lock index 632a8c2b..f504262b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,16 @@ dependencies = [ "serde", ] +[[package]] +name = "server" +version = "0.1.0" +dependencies = [ + "anyhow", + "tracing", + "tracing-subscriber", + "transport", +] + [[package]] name = "sharded-slab" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 641f9ba7..01c6a095 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "transport", "debugger", + "server", ] [profile.release] diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 00000000..e438ee1a --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +tracing.workspace = true +transport = { path = "../transport" } + +[dev-dependencies] +tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] } diff --git a/server/src/debugpy.rs b/server/src/debugpy.rs new file mode 100644 index 00000000..0c2edae0 --- /dev/null +++ b/server/src/debugpy.rs @@ -0,0 +1,114 @@ +use std::{ + io::{BufRead, BufReader}, + process::{Child, Stdio}, + sync::mpsc, + thread, +}; + +use anyhow::Context; + +use crate::Server; + +pub struct DebugpyServer { + child: Child, +} + +impl Server for DebugpyServer { + fn on_port(port: impl Into) -> anyhow::Result { + let port = port.into(); + + tracing::debug!(port = ?port, "starting server process"); + let cwd = std::env::current_dir().unwrap(); + let mut child = std::process::Command::new("python") + .args([ + "-m", + "debugpy.adapter", + "--host", + "127.0.0.1", + "--port", + &format!("{port}"), + "--log-stderr", + ]) + .stderr(Stdio::piped()) + .current_dir(cwd.join("..").canonicalize().unwrap()) + .spawn() + .context("spawning background process")?; + + // wait until server is ready + tracing::debug!("waiting until server is ready"); + let stderr = child.stderr.take().unwrap(); + let reader = BufReader::new(stderr); + + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + let mut should_signal = true; + for line in reader.lines() { + let line = line.unwrap(); + if should_signal && line.contains("Listening for incoming Client connections") { + should_signal = false; + let _ = tx.send(()); + } + } + }); + let _ = rx.recv(); + + tracing::debug!("server ready"); + Ok(Self { child }) + } +} + +impl Drop for DebugpyServer { + fn drop(&mut self) { + tracing::debug!("terminating server"); + match self.child.kill() { + Ok(_) => { + tracing::debug!("server terminated"); + let _ = self.child.wait(); + } + Err(e) => tracing::warn!(error = %e, "could not terminate server process"), + } + } +} + +#[cfg(test)] +mod tests { + use std::{io::IsTerminal, net::TcpStream}; + + use anyhow::Context; + use tracing_subscriber::EnvFilter; + use transport::bindings::get_random_tcp_port; + + use crate::{for_implementation_on_port, Implementation}; + + fn init_test_logger() { + let in_ci = std::env::var("CI") + .map(|val| val == "true") + .unwrap_or(false); + + if std::io::stderr().is_terminal() || in_ci { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + } else { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .json() + .init(); + } + } + + #[test] + fn test_create() -> anyhow::Result<()> { + init_test_logger(); + + let port = get_random_tcp_port().context("reserving custom port")?; + let _server = + for_implementation_on_port(Implementation::Debugpy, port).context("creating server")?; + + // server should be running + tracing::info!("making connection"); + let _conn = + TcpStream::connect(&format!("127.0.0.1:{port}")).context("connecting to server")?; + Ok(()) + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 00000000..08f179bb --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,37 @@ +use anyhow::Context; +use transport::DEFAULT_DAP_PORT; + +pub mod debugpy; + +pub enum Implementation { + Debugpy, +} + +pub trait Server { + fn on_port(port: impl Into) -> anyhow::Result + where + Self: Sized; + + fn new() -> anyhow::Result + where + Self: Sized, + { + Self::on_port(DEFAULT_DAP_PORT) + } +} + +pub fn for_implementation(implementation: Implementation) -> anyhow::Result> { + for_implementation_on_port(implementation, DEFAULT_DAP_PORT) +} + +pub fn for_implementation_on_port( + implementation: Implementation, + port: impl Into, +) -> anyhow::Result> { + match implementation { + Implementation::Debugpy => { + let server = crate::debugpy::DebugpyServer::on_port(port).context("creating server")?; + Ok(Box::new(server)) + } + } +} diff --git a/transport/src/lib.rs b/transport/src/lib.rs index 37bf03a4..c874ca91 100644 --- a/transport/src/lib.rs +++ b/transport/src/lib.rs @@ -11,3 +11,6 @@ pub mod types; pub use client::Client; pub use client::Received; + +/// The default port the DAP protocol listens on +pub const DEFAULT_DAP_PORT: u16 = 5678;