Files
servo/components/devtools/actors/thread.rs
eri 92259af702 devtools: Unify pause handling for breakpoints and interrupt (#42599)
The pause debugger screen should be shown for both pausing manually
(interrupt) and hitting a breakpoint.

Reuse the logic for pausing breakpoints to pause the debugger when the
user manually clicks the pause button.

Rename the pause event to interrupt to match the language of the
DevTools client and to avoid confusion with paused frames, which can
happen on interrupt or on a breakpoint.


https://github.com/user-attachments/assets/ceb0007d-0e57-44d6-a159-55980ff8b517

Testing: New DevTools test and manual testing.
Part of: #36027

cc @atbrakhi

---------

Signed-off-by: eri <eri@igalia.com>
Co-authored-by: atbrakhi <atbrakhi@igalia.com>
2026-02-23 09:29:59 +00:00

191 lines
5.6 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::collections::HashSet;
use atomic_refcell::AtomicRefCell;
use base::generic_channel::GenericSender;
use devtools_traits::DevtoolScriptControlMsg;
use malloc_size_of_derive::MallocSizeOf;
use serde::Serialize;
use serde_json::{Map, Value};
use super::source::{SourceManager, SourcesReply};
use crate::actor::{Actor, ActorError, ActorRegistry};
use crate::actors::frame::{FrameActor, FrameActorMsg};
use crate::actors::pause::PauseActor;
use crate::protocol::{ClientRequest, JsonPacketStream};
use crate::{EmptyReplyMsg, StreamId};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ThreadAttached {
from: String,
#[serde(rename = "type")]
type_: String,
actor: String,
frame: u32,
error: u32,
recording_endpoint: u32,
execution_point: u32,
popped_frames: Vec<PoppedFrameMsg>,
why: WhyMsg,
}
#[derive(Serialize)]
enum PoppedFrameMsg {}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WhyMsg {
#[serde(rename = "type")]
pub type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_next: Option<bool>,
}
#[derive(Serialize)]
struct ThreadResumedReply {
from: String,
#[serde(rename = "type")]
type_: String,
}
#[derive(Serialize)]
pub(crate) struct ThreadInterruptedReply {
pub from: String,
#[serde(rename = "type")]
pub type_: String,
pub actor: String,
pub frame: FrameActorMsg,
pub why: WhyMsg,
}
#[derive(Serialize)]
struct GetAvailableEventBreakpointsReply {
from: String,
value: Vec<()>,
}
#[derive(Serialize)]
struct FramesReply {
from: String,
frames: Vec<FrameActorMsg>,
}
#[derive(MallocSizeOf)]
pub(crate) struct ThreadActor {
name: String,
pub source_manager: SourceManager,
script_sender: GenericSender<DevtoolScriptControlMsg>,
pub frames: AtomicRefCell<HashSet<String>>,
}
impl ThreadActor {
pub fn new(name: String, script_sender: GenericSender<DevtoolScriptControlMsg>) -> ThreadActor {
ThreadActor {
name: name.clone(),
source_manager: SourceManager::new(),
script_sender,
frames: Default::default(),
}
}
}
impl Actor for ThreadActor {
fn name(&self) -> String {
self.name.clone()
}
fn handle_message(
&self,
mut request: ClientRequest,
registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
_id: StreamId,
) -> Result<(), ActorError> {
match msg_type {
"attach" => {
let pause = registry.new_name::<PauseActor>();
registry.register(PauseActor {
name: pause.clone(),
});
let msg = ThreadAttached {
from: self.name(),
type_: "paused".to_owned(),
actor: pause,
frame: 0,
error: 0,
recording_endpoint: 0,
execution_point: 0,
popped_frames: vec![],
why: WhyMsg {
type_: "attached".to_owned(),
on_next: None,
},
};
request.write_json_packet(&msg)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"resume" => {
let _ = self.script_sender.send(DevtoolScriptControlMsg::Resume);
let msg = ThreadResumedReply {
from: self.name(),
type_: "resumed".to_owned(),
};
request.write_json_packet(&msg)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"interrupt" => {
self.script_sender
.send(DevtoolScriptControlMsg::Interrupt)
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"reconfigure" => request.reply_final(&EmptyReplyMsg { from: self.name() })?,
"getAvailableEventBreakpoints" => {
// TODO: Send list of available event breakpoints (animation, clipboard, load...)
let msg = GetAvailableEventBreakpointsReply {
from: self.name(),
value: vec![],
};
request.reply_final(&msg)?
},
// Client has attached to the thread and wants to load script sources.
// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#loading-script-sources>
"sources" => {
let msg = SourcesReply {
from: self.name(),
sources: self.source_manager.source_forms(registry),
};
request.reply_final(&msg)?
},
"frames" => {
let msg = FramesReply {
from: self.name(),
frames: self
.frames
.borrow()
.iter()
.map(|frame| registry.encode::<FrameActor, _>(frame))
.collect(),
};
request.reply_final(&msg)?
},
_ => return Err(ActorError::UnrecognizedPacketType),
};
Ok(())
}
}