Skip to content

Commit 6252848

Browse files
committed
Fix: Interrupted events should only occur after interrupt(), not on every epoch yield.
1 parent c39e049 commit 6252848

File tree

3 files changed

+77
-27
lines changed

3 files changed

+77
-27
lines changed

crates/debugger/src/host.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,13 @@ pub fn add_debuggee<T: Send + 'static>(
2020
debuggee: crate::Debuggee<T>,
2121
) -> Result<Resource<Debuggee>> {
2222
let engine = debuggee.engine().clone();
23+
let interrupt_pending = debuggee.interrupt_pending().clone();
2324
let inner: Option<Box<dyn OpaqueDebugger + Send + 'static>> = Some(Box::new(debuggee));
24-
Ok(table.push(Debuggee { inner, engine })?)
25+
Ok(table.push(Debuggee {
26+
inner,
27+
engine,
28+
interrupt_pending,
29+
})?)
2530
}
2631

2732
/// A provider of a [`ResourceTable`] for debugger host APIs.

crates/debugger/src/host/api.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use crate::host::opaque::OpaqueDebugger;
44
use crate::host::wit;
55
use crate::{DebugRunResult, host::bindings::wasm_type_to_val_type};
66
use std::pin::Pin;
7+
use std::sync::Arc;
8+
use std::sync::atomic::{AtomicBool, Ordering};
79
use wasmtime::{
810
Engine, ExnRef, FrameHandle, Func, Global, Instance, Memory, Module, OwnedRooted, Result,
911
Table, Tag, Val, component::Resource, component::ResourceTable,
@@ -23,6 +25,10 @@ pub struct Debuggee {
2325
/// epoch (hence interrupting a running debuggee) without taking
2426
/// the mutex.
2527
pub(crate) engine: Engine,
28+
29+
/// Shared flag: set to `true` by `interrupt()` so the inner
30+
/// handler treats the next epoch yield as an `Interrupted` event.
31+
pub(crate) interrupt_pending: Arc<AtomicBool>,
2632
}
2733

2834
impl Debuggee {
@@ -205,6 +211,7 @@ impl wit::HostDebuggee for ResourceTable {
205211

206212
async fn interrupt(&mut self, debuggee: Resource<Debuggee>) -> Result<()> {
207213
let d = self.get_mut(&debuggee)?;
214+
d.interrupt_pending.store(true, Ordering::SeqCst);
208215
d.engine.increment_epoch();
209216
Ok(())
210217
}

crates/debugger/src/lib.rs

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@
99
//! In the future, this crate will also provide a WIT-level API and
1010
//! world in which to run debugger components.
1111
12-
use std::{any::Any, future::Future, pin::Pin, sync::Arc};
12+
use std::{
13+
any::Any,
14+
future::Future,
15+
pin::Pin,
16+
sync::{
17+
Arc,
18+
atomic::{AtomicBool, Ordering},
19+
},
20+
};
1321
use tokio::{
1422
sync::{Mutex, mpsc},
1523
task::JoinHandle,
@@ -44,6 +52,13 @@ pub struct Debuggee<T: Send + 'static> {
4452
in_tx: mpsc::Sender<Command<T>>,
4553
out_rx: mpsc::Receiver<Response<T>>,
4654
handle: Option<JoinHandle<Result<()>>>,
55+
/// Flag shared with the inner handler: set to `true` by
56+
/// `interrupt()` so the next epoch yield is surfaced as an
57+
/// `Interrupted` event rather than eaten by the handler. Epoch
58+
/// yields serve two purposes, namely ensuring regular yields to
59+
/// the event loop and enacting an explicit interrupt, and this
60+
/// flag distinguishes those cases.
61+
interrupt_pending: Arc<AtomicBool>,
4762
}
4863

4964
/// State machine from the perspective of the outer logic.
@@ -145,6 +160,7 @@ enum Response<T: 'static> {
145160
struct HandlerInner<T: Send + 'static> {
146161
in_rx: Mutex<mpsc::Receiver<Command<T>>>,
147162
out_tx: mpsc::Sender<Response<T>>,
163+
interrupt_pending: Arc<AtomicBool>,
148164
}
149165

150166
struct Handler<T: Send + 'static>(Arc<HandlerInner<T>>);
@@ -168,7 +184,17 @@ impl<T: Send + 'static> DebugHandler for Handler<T> {
168184
}
169185
DebugEvent::Trap(trap) => DebugRunResult::Trap(trap),
170186
DebugEvent::Breakpoint => DebugRunResult::Breakpoint,
171-
DebugEvent::EpochYield => DebugRunResult::EpochYield,
187+
DebugEvent::EpochYield => {
188+
// Only pause on epoch yields that were requested via
189+
// interrupt(). Other epoch ticks simply yield to the
190+
// event loop (funcionality already implemented in
191+
// core Wasmtime; no need to do that yield here in the
192+
// debug handler).
193+
if !self.0.interrupt_pending.swap(false, Ordering::SeqCst) {
194+
return;
195+
}
196+
DebugRunResult::EpochYield
197+
}
172198
};
173199
if self.0.out_tx.send(Response::Paused(result)).await.is_err() {
174200
// Outer Debuggee has been dropped: just continue
@@ -226,30 +252,35 @@ impl<T: Send + 'static> Debuggee<T> {
226252
let engine = store.engine().clone();
227253
let (in_tx, in_rx) = mpsc::channel(1);
228254
let (out_tx, out_rx) = mpsc::channel(1);
229-
230-
let handle = tokio::spawn(async move {
231-
// Create the handler that's invoked from within the async
232-
// debug-event callback.
233-
let out_tx_clone = out_tx.clone();
234-
let handler = Handler(Arc::new(HandlerInner {
235-
in_rx: Mutex::new(in_rx),
236-
out_tx,
237-
}));
238-
239-
// Emulate a breakpoint at startup.
240-
log::trace!("inner debuggee task: first breakpoint");
241-
handler
242-
.handle(store.as_context_mut(), DebugEvent::Breakpoint)
243-
.await;
244-
log::trace!("inner debuggee task: first breakpoint resumed");
245-
246-
// Now invoke the actual inner body.
247-
store.set_debug_handler(handler);
248-
log::trace!("inner debuggee task: running `inner`");
249-
let result = inner(&mut store).await;
250-
log::trace!("inner debuggee task: done with `inner`");
251-
let _ = out_tx_clone.send(Response::Finished(store)).await;
252-
result
255+
let interrupt_pending = Arc::new(AtomicBool::new(false));
256+
257+
let handle = tokio::spawn({
258+
let interrupt_pending = interrupt_pending.clone();
259+
async move {
260+
// Create the handler that's invoked from within the async
261+
// debug-event callback.
262+
let out_tx_clone = out_tx.clone();
263+
let handler = Handler(Arc::new(HandlerInner {
264+
in_rx: Mutex::new(in_rx),
265+
out_tx,
266+
interrupt_pending,
267+
}));
268+
269+
// Emulate a breakpoint at startup.
270+
log::trace!("inner debuggee task: first breakpoint");
271+
handler
272+
.handle(store.as_context_mut(), DebugEvent::Breakpoint)
273+
.await;
274+
log::trace!("inner debuggee task: first breakpoint resumed");
275+
276+
// Now invoke the actual inner body.
277+
store.set_debug_handler(handler);
278+
log::trace!("inner debuggee task: running `inner`");
279+
let result = inner(&mut store).await;
280+
log::trace!("inner debuggee task: done with `inner`");
281+
let _ = out_tx_clone.send(Response::Finished(store)).await;
282+
result
283+
}
253284
});
254285

255286
Debuggee {
@@ -258,6 +289,7 @@ impl<T: Send + 'static> Debuggee<T> {
258289
store: None,
259290
in_tx,
260291
out_rx,
292+
interrupt_pending,
261293
handle: Some(handle),
262294
}
263295
}
@@ -275,6 +307,12 @@ impl<T: Send + 'static> Debuggee<T> {
275307
&self.engine
276308
}
277309

310+
/// Get the interrupt-pending flag. Setting this to `true` causes
311+
/// the next epoch yield to surface as an `Interrupted` event.
312+
pub fn interrupt_pending(&self) -> &Arc<AtomicBool> {
313+
&self.interrupt_pending
314+
}
315+
278316
async fn wait_for_initial(&mut self) -> Result<()> {
279317
if let DebuggeeState::Initial = &self.state {
280318
// Need to receive and discard first `Paused`.

0 commit comments

Comments
 (0)