diff --git a/crates/wasmtime/src/runtime/debug.rs b/crates/wasmtime/src/runtime/debug.rs index 2f40b92a0de0..c91d369cf697 100644 --- a/crates/wasmtime/src/runtime/debug.rs +++ b/crates/wasmtime/src/runtime/debug.rs @@ -1102,7 +1102,6 @@ impl<'a> BreakpointEdit<'a> { *refcount += 1; if *refcount == 1 { // First reference: actually patch the code. - log::trace!("patching in breakpoint {actual_key:?}"); let mem = Self::get_code_memory(self.state, self.registry, &mut self.dirty_modules, module)?; let patches = frame_table.lookup_breakpoint_patches_by_pc(actual_pc); diff --git a/crates/wasmtime/src/runtime/vm/libcalls.rs b/crates/wasmtime/src/runtime/vm/libcalls.rs index c547f95a7518..186d1c02acd8 100644 --- a/crates/wasmtime/src/runtime/vm/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/libcalls.rs @@ -1755,7 +1755,6 @@ fn throw_ref( fn breakpoint(store: &mut dyn VMStore, _instance: InstanceId) -> Result<()> { #[cfg(feature = "debug")] { - log::trace!("hit breakpoint"); store.block_on_debug_handler(crate::DebugEvent::Breakpoint)?; } // Avoid unused-argument warning in no-debugger builds. diff --git a/src/commands/run.rs b/src/commands/run.rs index 0df297096ae6..2d69b41d819e 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -396,7 +396,10 @@ impl RunCommand { } #[cfg(feature = "debug")] - fn add_debugger_api(&mut self, linker: &mut wasmtime::component::Linker) -> Result<()> { + pub(crate) fn add_debugger_api( + &mut self, + linker: &mut wasmtime::component::Linker, + ) -> Result<()> { wasmtime_debugger::add_to_linker(linker, |x| x.ctx().table)?; Ok(()) } @@ -510,7 +513,7 @@ impl RunCommand { Ok(instance) } - fn compute_argv(&self) -> Result> { + pub(crate) fn compute_argv(&self) -> Result> { let mut result = Vec::new(); for (i, arg) in self.module_and_args.iter().enumerate() { @@ -898,10 +901,15 @@ impl RunCommand { } } + /// Invoke a debugger component with a debuggee. + /// + /// The debugger runs in `store` (using run's `Host`), while the + /// debuggee wraps an arbitrary store type `T` and body closure. #[cfg(feature = "debug")] - async fn invoke_debugger< + pub(crate) async fn invoke_debugger< + T: Send + 'static, F: for<'a> FnOnce( - &'a mut Store, + &'a mut Store, ) -> Pin> + Send + 'a>> + Send + 'static, @@ -910,7 +918,7 @@ impl RunCommand { store: &mut Store, component: &wasmtime::component::Component, linker: &mut wasmtime::component::Linker, - debuggee_host: Store, + debuggee_host: Store, body: F, ) -> Result<()> { let instance = linker.instantiate_async(&mut *store, component).await?; diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 390bf270138e..b49be312eceb 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -7,6 +7,7 @@ use http_body_util::BodyExt as _; use http_body_util::combinators::UnsyncBoxBody; use hyper::body::{Body, Frame, SizeHint}; use std::convert::Infallible; +use std::ffi::OsString; use std::net::SocketAddr; use std::pin::Pin; use std::task::{Context, Poll}; @@ -33,6 +34,9 @@ use wasmtime_wasi_http::handler::{HandlerState, Proxy, ProxyHandler, ProxyPre, S use wasmtime_wasi_http::io::TokioIo; use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; +#[cfg(feature = "debug")] +use crate::commands::run::RunCommand; + #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; #[cfg(feature = "wasi-keyvalue")] @@ -194,6 +198,155 @@ impl ServeCommand { Ok(()) } + /// Set up the debugger component side-car, mirroring + /// [`RunCommand::debugger_run`]. + #[cfg(feature = "debug")] + fn debugger_setup(&mut self) -> Result> { + fn set_implicit_option( + place: &str, + name: &str, + setting: &mut Option, + value: bool, + ) -> Result<()> { + if *setting == Some(!value) { + bail!( + "Explicitly-set option on {place} {name}={} is not compatible \ + with debugging-implied setting {value}", + setting.unwrap() + ); + } + *setting = Some(value); + Ok(()) + } + + #[cfg(feature = "gdbstub")] + let override_bytes = if let Some(addr) = self.run.gdbstub.as_deref() { + if self.run.common.debug.debugger.is_some() { + bail!("-g/--gdb cannot be combined with -Ddebugger="); + } + let addr = if addr.parse::().is_ok() { + format!("127.0.0.1:{addr}") + } else { + use std::net::SocketAddr as SA; + addr.parse::() + .with_context(|| format!("invalid gdbstub address: `{addr}`"))?; + addr.to_string() + }; + self.run.common.debug.debugger = Some("".into()); + self.run.common.debug.arg.push(addr); + Some(gdbstub_component_artifact::GDBSTUB_COMPONENT) + } else { + None + }; + #[cfg(not(feature = "gdbstub"))] + let override_bytes = None; + + if let Some(debugger_component_path) = self.run.common.debug.debugger.as_ref() { + set_implicit_option( + "debuggee", + "guest_debug", + &mut self.run.common.debug.guest_debug, + true, + )?; + set_implicit_option( + "debuggee", + "epoch_interruption", + &mut self.run.common.wasm.epoch_interruption, + true, + )?; + + let mut debugger_run = RunCommand::try_parse_from( + ["run".into(), debugger_component_path.into()] + .into_iter() + .chain(self.run.common.debug.arg.iter().map(OsString::from)), + )?; + debugger_run.module_bytes = override_bytes; + + debugger_run.run.common.wasi.tcp.get_or_insert(true); + debugger_run + .run + .common + .wasi + .inherit_network + .get_or_insert(true); + + set_implicit_option( + "debugger", + "inherit_stdin", + &mut debugger_run.run.common.wasi.inherit_stdin, + self.run.common.debug.inherit_stdin.unwrap_or(false), + )?; + set_implicit_option( + "debugger", + "inherit_stdout", + &mut debugger_run.run.common.wasi.inherit_stdout, + self.run.common.debug.inherit_stdout.unwrap_or(false), + )?; + set_implicit_option( + "debugger", + "inherit_stderr", + &mut debugger_run.run.common.wasi.inherit_stderr, + self.run.common.debug.inherit_stderr.unwrap_or(false), + )?; + Ok(Some(debugger_run)) + } else { + Ok(None) + } + } + + /// Run the HTTP server under a debugger component. + /// + /// Uses a single store and instance to handle all requests + /// sequentially, so the debugger can pause and inspect state. + #[cfg(feature = "debug")] + async fn serve_under_debugger( + &self, + mut debug_run: RunCommand, + engine: &Engine, + linker: &Linker, + component: &Component, + ) -> Result<()> { + let instance_pre = linker.instantiate_pre(component)?; + let proxy_pre = wasmtime_wasi_http::p2::bindings::ProxyPre::new(instance_pre)?; + + let mut debuggee_store = self.new_store(engine, None)?; + + // Pre-register component modules so the debugger can see + // them and set breakpoints at the initial stop. + debuggee_store.debug_register_component(component)?; + + let debug_engine = debug_run.new_engine()?; + let debug_main = debug_run.run.load_module( + &debug_engine, + debug_run.module_and_args[0].as_ref(), + debug_run.module_bytes.as_ref().map(|v| &v[..]), + )?; + let (mut debug_store, debug_linker) = + debug_run.new_store_and_linker(&debug_engine, &debug_main)?; + let debug_component = match debug_main { + RunTarget::Core(_) => { + bail!("Debugger component is a core module; only components are supported") + } + RunTarget::Component(c) => c, + }; + let mut debug_linker = match debug_linker { + crate::commands::run::CliLinker::Core(_) => unreachable!(), + crate::commands::run::CliLinker::Component(l) => l, + }; + debug_run.add_debugger_api(&mut debug_linker)?; + + let addr = self.addr; + debug_run + .invoke_debugger( + &mut debug_store, + &debug_component, + &mut debug_linker, + debuggee_store, + move |store| Box::pin(debug_serve_body(store, proxy_pre, addr)), + ) + .await + } + fn new_store(&self, engine: &Engine, req_id: Option) -> Result> { let mut builder = WasiCtxBuilder::new(); self.run.configure_wasip2(&mut builder)?; @@ -389,6 +542,9 @@ impl ServeCommand { async fn serve(mut self) -> Result<()> { use hyper::server::conn::http1; + #[cfg(feature = "debug")] + let debug_run = self.debugger_setup()?; + let mut config = self .run .common @@ -419,6 +575,13 @@ impl ServeCommand { RunTarget::Component(c) => c, }; + #[cfg(feature = "debug")] + if let Some(debug_run) = debug_run { + return self + .serve_under_debugger(debug_run, &engine, &linker, &component) + .await; + } + let instance = linker.instantiate_pre(&component)?; #[cfg(feature = "component-model-async")] let instance = match wasmtime_wasi_http::p3::bindings::ServicePre::new(instance.clone()) { @@ -813,6 +976,148 @@ fn setup_guest_profiler( Ok(write_profile) } +/// Build a minimal error response with an empty body. +fn error_response(status: StatusCode) -> hyper::Response> { + Response::builder() + .status(status) + .body( + http_body_util::Empty::new() + .map_err(|_| unreachable!()) + .boxed_unsync(), + ) + .unwrap() +} + +/// Debuggee body for `wasmtime serve -g`: instantiate the HTTP component +/// once, then handle requests sequentially on a single store. +#[cfg(feature = "debug")] +async fn debug_serve_body( + store: &mut Store, + proxy_pre: wasmtime_wasi_http::p2::bindings::ProxyPre, + addr: SocketAddr, +) -> Result<()> { + use hyper::server::conn::http1; + use wasmtime_wasi_http::p2::bindings::http::types::Scheme; + use wasmtime_wasi_http::p2::body::HyperOutgoingBody; + + type P2Response = std::result::Result< + hyper::Response, + wasmtime_wasi_http::p2::bindings::http::types::ErrorCode, + >; + + let engine_clone = store.engine().clone(); + let _epoch_thread = std::thread::spawn(move || { + loop { + std::thread::sleep(Duration::from_millis(1)); + engine_clone.increment_epoch(); + } + }); + + store.epoch_deadline_async_yield_and_update(1); + + // Instantiate the HTTP component once. + let proxy = proxy_pre.instantiate_async(&mut *store).await?; + + // Bind the TCP listener. + let socket = match addr { + SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?, + SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?, + }; + socket.set_reuseaddr(!cfg!(windows))?; + socket.bind(addr)?; + let listener = socket.listen(100)?; + eprintln!("Serving HTTP on http://{}/", listener.local_addr()?); + + // Accept loop: handle one connection at a time, requests sequentially. + loop { + let (stream, _) = listener.accept().await?; + stream.set_nodelay(true)?; + let stream = TokioIo::new(stream); + + // Channel to bridge hyper's service_fn with our sequential + // request processing on the single store. + type RespBody = hyper::Response>; + let (req_tx, mut req_rx) = tokio::sync::mpsc::channel::<( + hyper::Request, + tokio::sync::oneshot::Sender>, + )>(1); + + let serve_conn = http1::Builder::new().keep_alive(true).serve_connection( + stream, + hyper::service::service_fn(move |req| { + let req_tx = req_tx.clone(); + async move { + let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); + if req_tx.send((req, resp_tx)).await.is_err() { + return Ok::<_, Infallible>(error_response( + StatusCode::SERVICE_UNAVAILABLE, + )); + } + resp_rx + .await + .unwrap_or(Ok(error_response(StatusCode::SERVICE_UNAVAILABLE))) + } + }), + ); + + tokio::pin!(serve_conn); + + loop { + tokio::select! { + result = &mut serve_conn => { + if let Err(e) = result { + eprintln!("connection error: {e:?}"); + } + break; + } + msg = req_rx.recv() => { + let Some((req, resp_tx)) = msg else { break }; + + let (p2_tx, p2_rx) = tokio::sync::oneshot::channel::(); + let wasi_req = store + .data_mut() + .http() + .new_incoming_request(Scheme::Http, req); + let wasi_out = wasi_req.and_then(|_req| { + let out = store.data_mut().http().new_response_outparam(p2_tx); + out.map(|out| (_req, out)) + }); + let (wasi_req, wasi_out) = match wasi_out { + Ok(pair) => pair, + Err(e) => { + eprintln!("error creating WASI request: {e:?}"); + let _ = resp_tx.send(Ok(error_response( + StatusCode::INTERNAL_SERVER_ERROR, + ))); + continue; + } + }; + + if let Err(e) = proxy + .wasi_http_incoming_handler() + .call_handle(&mut *store, wasi_req, wasi_out) + .await + { + eprintln!("handler error: {e:?}"); + } + + let resp = match p2_rx.await { + Ok(Ok(resp)) => resp.map(|body| { + body.map_err(|e| e.into()).boxed_unsync() + }), + Ok(Err(e)) => { + eprintln!("component error: {e:?}"); + error_response(StatusCode::INTERNAL_SERVER_ERROR) + } + Err(_) => error_response(StatusCode::INTERNAL_SERVER_ERROR), + }; + let _ = resp_tx.send(Ok(resp)); + } + } + } + } +} + type Request = hyper::Request; async fn handle_request( diff --git a/tests/all/guest_debug/mod.rs b/tests/all/guest_debug/mod.rs index a51e42073b85..2a1342e597ad 100644 --- a/tests/all/guest_debug/mod.rs +++ b/tests/all/guest_debug/mod.rs @@ -10,8 +10,8 @@ //! - Built with `--features gdbstub` use filecheck::{CheckerBuilder, NO_VARIABLES}; -use std::io::{BufRead, BufReader}; -use std::net::TcpListener; +use std::io::{BufRead, BufReader, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; use std::process::{Child, Command, Stdio}; use std::time::Duration; use test_programs_artifacts::*; @@ -51,7 +51,7 @@ const GDBSTUB_READY_MARKER: &str = "Debugger listening on"; struct WasmtimeWithGdbstub { child: Child, /// Keeps the stderr pipe alive to avoid SIGPIPE on the child. - #[allow(dead_code)] + /// Also used by serve tests to read the HTTP address. stderr_reader: BufReader, } @@ -99,6 +99,26 @@ impl WasmtimeWithGdbstub { } } } + + /// Read stderr lines until one contains `marker`, returning that line. + fn wait_for_stderr(&mut self, marker: &str, timeout: Duration) -> Result { + let deadline = std::time::Instant::now() + timeout; + let mut line = String::new(); + loop { + if std::time::Instant::now() > deadline { + bail!("timed out waiting for '{marker}' on stderr"); + } + line.clear(); + self.stderr_reader.read_line(&mut line)?; + eprintln!("wasmtime stderr: {}", line.trim_end()); + if line.contains(marker) { + return Ok(line); + } + if line.is_empty() { + bail!("wasmtime stderr closed before finding '{marker}'"); + } + } + } } /// Run an LLDB debug script against a gdbstub endpoint. @@ -219,3 +239,160 @@ check: fib )?; Ok(()) } + +/// Helper: send an HTTP/1.0 request and return the full response. +fn http_request(addr: SocketAddr, path: &str) -> Result { + let mut tcp = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?; + tcp.set_read_timeout(Some(Duration::from_secs(5)))?; + write!(tcp, "GET {path} HTTP/1.0\r\nHost: localhost\r\n\r\n")?; + let mut response = String::new(); + let _ = std::io::Read::read_to_string(&mut tcp, &mut response); + Ok(response) +} + +/// Parse an HTTP serve address from a "Serving HTTP on http://addr/" line. +fn parse_http_addr(line: &str) -> Result { + line.find("127.0.0.1") + .and_then(|start| { + let addr = &line[start..]; + let end = addr.find('/')?; + addr[..end].parse().ok() + }) + .ok_or_else(|| format_err!("failed to parse HTTP address from: {line}")) +} + +/// Start serve under debugger, continue, and send multiple HTTP requests +/// to verify instance reuse works correctly under the debugger. +#[test] +#[ignore] +fn guest_debug_serve_requests() -> Result<()> { + let gdb_port = free_port(); + + let mut wt = WasmtimeWithGdbstub::spawn( + "serve", + gdb_port, + &[ + "-Ccache=n", + "--addr=127.0.0.1:0", + "-Scli", + P2_CLI_SERVE_HELLO_WORLD_COMPONENT, + ], + Duration::from_secs(30), + )?; + + // Connect LLDB in background: just continue to start the HTTP server. + let lldb_handle = std::thread::spawn(move || lldb_with_gdbstub_script(gdb_port, "c\n")); + + // Wait for the HTTP server to start. + let line = wt.wait_for_stderr("Serving HTTP", Duration::from_secs(15))?; + let http_addr = parse_http_addr(&line)?; + eprintln!("HTTP address: {http_addr}"); + + // Send 3 requests to the same instance, verifying instance reuse. + for i in 1..=3 { + let resp = http_request(http_addr, "/")?; + eprintln!("Response {i}: {}", resp.lines().last().unwrap_or("")); + assert!( + resp.contains("Hello, WASI!"), + "request {i}: expected 'Hello, WASI!' in response, got:\n{resp}" + ); + } + + // Kill wasmtime to unblock LLDB (which is waiting for the process). + wt.child.kill().ok(); + wt.child.wait()?; + + // Collect LLDB output (it exits once the process is killed). + let lldb_output = lldb_handle.join().unwrap()?; + + // Verify LLDB connected and the process was running. + check_output( + &lldb_output, + r#" +check: stop reason +check: resuming +"#, + )?; + + Ok(()) +} + +/// Start serve under debugger, set a breakpoint on the HTTP handler, +/// send requests, verify breakpoints fire and responses are correct. +/// Tests instance reuse across multiple requests. +#[test] +#[ignore] +fn guest_debug_serve_breakpoint() -> Result<()> { + let gdb_port = free_port(); + + let mut wt = WasmtimeWithGdbstub::spawn( + "serve", + gdb_port, + &[ + "-Ccache=n", + "--addr=127.0.0.1:0", + "-Scli", + P2_CLI_SERVE_HELLO_WORLD_COMPONENT, + ], + Duration::from_secs(30), + )?; + + // LLDB script: set a breakpoint on the incoming-handler Guest::handle, + // continue to start the server, then for each request: print backtrace + // at breakpoint and continue. We do this for 3 requests. + let lldb_handle = std::thread::spawn(move || { + lldb_with_gdbstub_script( + gdb_port, + r#" +rbreak Guest.*handle +c +bt +c +bt +c +bt +c +"#, + ) + }); + + // Wait for the HTTP server to start. + let line = wt.wait_for_stderr("Serving HTTP", Duration::from_secs(15))?; + let http_addr = parse_http_addr(&line)?; + eprintln!("HTTP address: {http_addr}"); + + // Send 3 requests. Each one will hit the breakpoint, LLDB prints + // the backtrace, then continues to let the response through. + for i in 1..=3 { + let resp = http_request(http_addr, "/")?; + eprintln!("Response {i}: {}", resp.lines().last().unwrap_or("")); + assert!( + resp.contains("Hello, WASI!"), + "request {i}: expected 'Hello, WASI!' in response, got:\n{resp}" + ); + } + + // Kill wasmtime to unblock LLDB. + wt.child.kill().ok(); + wt.child.wait()?; + + let lldb_output = lldb_handle.join().unwrap()?; + + // Verify LLDB stopped at the breakpoint with the correct function + // in the backtrace, and that it happened multiple times. + check_output( + &lldb_output, + r#" +check: Guest +check: handle +check: stop reason +check: Guest +check: handle +check: stop reason +check: Guest +check: handle +"#, + )?; + + Ok(()) +}