DevTools: Implement watcher actor (#32509)

* feat: base for watcher

* feat: some more watcher tests

* feat: implement getWatcher

* refactor: clean up getWatcher

* feat: implement watchTargets

* feat: implement watchResources

* feat: very messy watchTargets fix

* refactor: clean browsing context

* feat: target configuration

* refactor: start cleanup

* refactor: more doc coments

* refactor: clean browsing context
This commit is contained in:
eri 2024-06-21 18:06:55 +02:00 committed by GitHub
parent 26c585a0c5
commit 5eb8813448
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 680 additions and 244 deletions

View file

@ -2,7 +2,7 @@
* 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/. */
//! Liberally derived from the [Firefox JS implementation](http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/webbrowser.js).
//! Liberally derived from the [Firefox JS implementation](https://searchfox.org/mozilla-central/source/devtools/server/actors/webbrowser.js).
//! Connection point for remote devtools that wish to investigate a particular Browsing Context's contents.
//! Supports dynamic attaching and detaching which control notifications of navigation, etc.
@ -18,6 +18,7 @@ use serde::Serialize;
use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::configuration::{TargetConfigurationActor, ThreadConfigurationActor};
use crate::actors::emulation::EmulationActor;
use crate::actors::inspector::InspectorActor;
use crate::actors::performance::PerformanceActor;
@ -26,118 +27,126 @@ use crate::actors::stylesheets::StyleSheetsActor;
use crate::actors::tab::TabDescriptorActor;
use crate::actors::thread::ThreadActor;
use crate::actors::timeline::TimelineActor;
use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor};
use crate::protocol::JsonPacketStream;
use crate::StreamId;
#[derive(Serialize)]
struct BrowsingContextTraits {
isBrowsingContext: bool,
}
#[derive(Serialize)]
struct AttachedTraits {
reconfigure: bool,
frames: bool,
logInPage: bool,
canRewind: bool,
watchpoints: bool,
}
#[derive(Serialize)]
struct BrowsingContextAttachedReply {
struct FrameUpdateReply {
from: String,
#[serde(rename = "type")]
type_: String,
threadActor: String,
cacheDisabled: bool,
javascriptEnabled: bool,
traits: AttachedTraits,
frames: Vec<FrameUpdateMsg>,
}
#[derive(Serialize)]
struct BrowsingContextDetachedReply {
from: String,
#[serde(rename = "type")]
type_: String,
}
#[derive(Serialize)]
struct ReconfigureReply {
from: String,
}
#[derive(Serialize)]
struct ListFramesReply {
from: String,
frames: Vec<FrameMsg>,
}
#[derive(Serialize)]
struct FrameMsg {
#[serde(rename_all = "camelCase")]
struct FrameUpdateMsg {
id: u32,
is_top_level: bool,
url: String,
title: String,
parentID: u32,
}
#[derive(Serialize)]
struct ListWorkersReply {
struct ResourceAvailableReply {
from: String,
workers: Vec<WorkerMsg>,
#[serde(rename = "type")]
type_: String,
resources: Vec<ResourceAvailableMsg>,
}
#[derive(Serialize)]
struct WorkerMsg {
id: u32,
#[serde(rename_all = "camelCase")]
struct ResourceAvailableMsg {
#[serde(rename = "hasNativeConsoleAPI")]
has_native_console_api: Option<bool>,
name: String,
#[serde(rename = "newURI")]
new_uri: Option<String>,
resource_type: String,
time: u64,
title: Option<String>,
url: Option<String>,
}
#[derive(Serialize)]
struct TabNavigated {
from: String,
#[serde(rename = "type")]
type_: String,
url: String,
title: Option<String>,
#[serde(rename = "nativeConsoleAPI")]
native_console_api: bool,
state: String,
is_frame_switching: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct BrowsingContextTraits {
is_browsing_context: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowsingContextActorMsg {
actor: String,
title: String,
url: String,
outerWindowID: u32,
browsingContextId: u32,
consoleActor: String,
/*emulationActor: String,
inspectorActor: String,
timelineActor: String,
profilerActor: String,
performanceActor: String,
styleSheetsActor: String,*/
#[serde(rename = "outerWindowID")]
outer_window_id: u32,
#[serde(rename = "browsingContextID")]
browsing_context_id: u32,
is_top_level_target: bool,
console_actor: String,
thread_actor: String,
traits: BrowsingContextTraits,
// Part of the official protocol, but not yet implemented.
/*storageActor: String,
memoryActor: String,
framerateActor: String,
reflowActor: String,
cssPropertiesActor: String,
animationsActor: String,
webExtensionInspectedWindowActor: String,
accessibilityActor: String,
screenshotActor: String,
changesActor: String,
webSocketActor: String,
manifestActor: String,*/
// emulation_actor: String,
// inspector_actor: String,
// timeline_actor: String,
// profiler_actor: String,
// performance_actor: String,
// style_sheets_actor: String,
// storage_actor: String,
// memory_actor: String,
// framerate_actor: String,
// reflow_actor: String,
// css_properties_actor: String,
// animations_actor: String,
// web_extension_inspected_window_actor: String,
// accessibility_actor: String,
// screenshot_actor: String,
// changes_actor: String,
// web_socket_actor: String,
// manifest_actor: String,
}
/// The browsing context actor encompasses all of the other supporting actors when debugging a web
/// view. To this extent, it contains a watcher actor that helps when communicating with the host,
/// as well as resource actors that each perform one debugging function.
pub(crate) struct BrowsingContextActor {
pub name: String,
pub title: RefCell<String>,
pub url: RefCell<String>,
pub active_pipeline: Cell<PipelineId>,
pub browsing_context_id: BrowsingContextId,
pub console: String,
pub _emulation: String,
pub _inspector: String,
pub _timeline: String,
pub _profiler: String,
pub _performance: String,
pub _styleSheets: String,
pub _profiler: String,
pub _style_sheets: String,
pub target_configuration: String,
pub thread_configuration: String,
pub thread: String,
pub _timeline: String,
pub _tab: String,
pub streams: RefCell<HashMap<StreamId, TcpStream>>,
pub browsing_context_id: BrowsingContextId,
pub active_pipeline: Cell<PipelineId>,
pub script_chan: IpcSender<DevtoolScriptControlMsg>,
pub streams: RefCell<HashMap<StreamId, TcpStream>>,
pub watcher: String,
}
impl Actor for BrowsingContextActor {
@ -149,89 +158,11 @@ impl Actor for BrowsingContextActor {
&self,
_registry: &ActorRegistry,
msg_type: &str,
msg: &Map<String, Value>,
stream: &mut TcpStream,
id: StreamId,
_msg: &Map<String, Value>,
_stream: &mut TcpStream,
_id: StreamId,
) -> Result<ActorMessageStatus, ()> {
Ok(match msg_type {
"reconfigure" => {
if let Some(options) = msg.get("options").and_then(|o| o.as_object()) {
if let Some(val) = options.get("performReload") {
if val.as_bool().unwrap_or(false) {
let _ = self
.script_chan
.send(DevtoolScriptControlMsg::Reload(self.active_pipeline.get()));
}
}
}
let _ = stream.write_json_packet(&ReconfigureReply { from: self.name() });
ActorMessageStatus::Processed
},
// https://docs.firefox-dev.tools/backend/protocol.html#listing-browser-tabs
// (see "To attach to a _targetActor_")
"attach" => {
let msg = BrowsingContextAttachedReply {
from: self.name(),
type_: "tabAttached".to_owned(),
threadActor: self.thread.clone(),
cacheDisabled: false,
javascriptEnabled: true,
traits: AttachedTraits {
reconfigure: false,
frames: true,
logInPage: false,
canRewind: false,
watchpoints: false,
},
};
if stream.write_json_packet(&msg).is_err() {
return Ok(ActorMessageStatus::Processed);
}
self.streams
.borrow_mut()
.insert(id, stream.try_clone().unwrap());
self.script_chan
.send(WantsLiveNotifications(self.active_pipeline.get(), true))
.unwrap();
ActorMessageStatus::Processed
},
"detach" => {
let msg = BrowsingContextDetachedReply {
from: self.name(),
type_: "detached".to_owned(),
};
let _ = stream.write_json_packet(&msg);
self.cleanup(id);
ActorMessageStatus::Processed
},
"listFrames" => {
let msg = ListFramesReply {
from: self.name(),
frames: vec![FrameMsg {
//FIXME: shouldn't ignore pipeline namespace field
id: self.active_pipeline.get().index.0.get(),
parentID: 0,
url: self.url.borrow().clone(),
title: self.title.borrow().clone(),
}],
};
let _ = stream.write_json_packet(&msg);
ActorMessageStatus::Processed
},
"listWorkers" => {
let msg = ListWorkersReply {
from: self.name(),
workers: vec![],
};
let _ = stream.write_json_packet(&msg);
ActorMessageStatus::Processed
},
_ => ActorMessageStatus::Ignored,
})
}
@ -255,9 +186,10 @@ impl BrowsingContextActor {
script_sender: IpcSender<DevtoolScriptControlMsg>,
actors: &mut ActorRegistry,
) -> BrowsingContextActor {
let emulation = EmulationActor::new(actors.new_name("emulation"));
let name = actors.new_name("target");
let DevtoolsPageInfo { title, url } = page_info;
let emulation = EmulationActor::new(actors.new_name("emulation"));
let inspector = InspectorActor {
name: actors.new_name("inspector"),
@ -268,48 +200,66 @@ impl BrowsingContextActor {
browsing_context: name.clone(),
};
let timeline =
TimelineActor::new(actors.new_name("timeline"), pipeline, script_sender.clone());
let performance = PerformanceActor::new(actors.new_name("performance"));
let profiler = ProfilerActor::new(actors.new_name("profiler"));
let performance = PerformanceActor::new(actors.new_name("performance"));
// the strange switch between styleSheets and stylesheets is due
// to an inconsistency in devtools. See Bug #1498893 in bugzilla
let styleSheets = StyleSheetsActor::new(actors.new_name("stylesheets"));
let thread = ThreadActor::new(actors.new_name("context"));
let DevtoolsPageInfo { title, url } = page_info;
let style_sheets = StyleSheetsActor::new(actors.new_name("stylesheets"));
let tabdesc = TabDescriptorActor::new(actors, name.clone());
let target_configuration =
TargetConfigurationActor::new(actors.new_name("target-configuration"));
let thread_configuration =
ThreadConfigurationActor::new(actors.new_name("thread-configuration"));
let thread = ThreadActor::new(actors.new_name("context"));
let timeline =
TimelineActor::new(actors.new_name("timeline"), pipeline, script_sender.clone());
let watcher = WatcherActor::new(
actors.new_name("watcher"),
name.clone(),
SessionContext::new(SessionContextType::BrowserElement),
);
let target = BrowsingContextActor {
name,
script_chan: script_sender,
title: RefCell::new(title),
url: RefCell::new(url.into_string()),
active_pipeline: Cell::new(pipeline),
browsing_context_id: id,
console,
_emulation: emulation.name(),
_inspector: inspector.name(),
_timeline: timeline.name(),
_profiler: profiler.name(),
_performance: performance.name(),
_styleSheets: styleSheets.name(),
_tab: tabdesc.name(),
thread: thread.name(),
_profiler: profiler.name(),
streams: RefCell::new(HashMap::new()),
browsing_context_id: id,
active_pipeline: Cell::new(pipeline),
_style_sheets: style_sheets.name(),
_tab: tabdesc.name(),
target_configuration: target_configuration.name(),
thread_configuration: thread_configuration.name(),
thread: thread.name(),
_timeline: timeline.name(),
watcher: watcher.name(),
};
actors.register(Box::new(emulation));
actors.register(Box::new(inspector));
actors.register(Box::new(timeline));
actors.register(Box::new(profiler));
actors.register(Box::new(performance));
actors.register(Box::new(styleSheets));
actors.register(Box::new(thread));
actors.register(Box::new(profiler));
actors.register(Box::new(style_sheets));
actors.register(Box::new(tabdesc));
actors.register(Box::new(target_configuration));
actors.register(Box::new(thread_configuration));
actors.register(Box::new(thread));
actors.register(Box::new(timeline));
actors.register(Box::new(watcher));
target
}
@ -318,21 +268,23 @@ impl BrowsingContextActor {
BrowsingContextActorMsg {
actor: self.name(),
traits: BrowsingContextTraits {
isBrowsingContext: true,
is_browsing_context: true,
},
title: self.title.borrow().clone(),
url: self.url.borrow().clone(),
//FIXME: shouldn't ignore pipeline namespace field
browsingContextId: self.browsing_context_id.index.0.get(),
browsing_context_id: self.browsing_context_id.index.0.get(),
//FIXME: shouldn't ignore pipeline namespace field
outerWindowID: self.active_pipeline.get().index.0.get(),
consoleActor: self.console.clone(),
/*emulationActor: self.emulation.clone(),
inspectorActor: self.inspector.clone(),
timelineActor: self.timeline.clone(),
profilerActor: self.profiler.clone(),
performanceActor: self.performance.clone(),
styleSheetsActor: self.styleSheets.clone(),*/
outer_window_id: self.active_pipeline.get().index.0.get(),
is_top_level_target: true,
console_actor: self.console.clone(),
thread_actor: self.thread.clone(),
// emulation_actor: self.emulation.clone(),
// inspector_actor: self.inspector.clone(),
// performance_actor: self.performance.clone(),
// profiler_actor: self.profiler.clone(),
// style_sheets_actor: self.style_sheets.clone(),
// timeline_actor: self.timeline.clone(),
}
}
@ -356,10 +308,11 @@ impl BrowsingContextActor {
type_: "tabNavigated".to_owned(),
url: url.as_str().to_owned(),
title,
nativeConsoleAPI: true,
native_console_api: true,
state: state.to_owned(),
isFrameSwitching: false,
is_frame_switching: false,
};
for stream in self.streams.borrow_mut().values_mut() {
let _ = stream.write_json_packet(&msg);
}
@ -371,16 +324,40 @@ impl BrowsingContextActor {
}
*self.title.borrow_mut() = title;
}
}
#[derive(Serialize)]
struct TabNavigated {
from: String,
#[serde(rename = "type")]
type_: String,
url: String,
title: Option<String>,
nativeConsoleAPI: bool,
state: String,
isFrameSwitching: bool,
pub(crate) fn frame_update(&self, stream: &mut TcpStream) {
let _ = stream.write_json_packet(&FrameUpdateReply {
from: self.name(),
type_: "frameUpdate".into(),
frames: vec![FrameUpdateMsg {
id: self.browsing_context_id.index.0.get(),
is_top_level: true,
title: self.title.borrow().clone(),
url: self.url.borrow().clone(),
}],
});
}
pub(crate) fn document_event(&self, stream: &mut TcpStream) {
// TODO: This is a hacky way of sending the 3 messages
// Figure out if there needs work to be done here, ensure the page is loaded
for (i, &name) in ["dom-loading", "dom-interactive", "dom-complete"]
.iter()
.enumerate()
{
let _ = stream.write_json_packet(&ResourceAvailableReply {
from: self.name(),
type_: "resource-available-form".into(),
resources: vec![ResourceAvailableMsg {
has_native_console_api: None,
name: name.into(),
new_uri: None,
resource_type: "document-event".into(),
time: i as u64,
title: Some(self.title.borrow().clone()),
url: Some(self.url.borrow().clone()),
}],
});
}
}
}

View file

@ -0,0 +1,153 @@
/* 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/. */
//! Liberally derived from <https://searchfox.org/mozilla-central/source/devtools/server/actors/target-configuration.js>
//! and <https://searchfox.org/mozilla-central/source/devtools/server/actors/thread-configuration.js>
//! These actors manage the configuration flags that the devtools host can apply to the targets and threads.
use std::collections::HashMap;
use std::net::TcpStream;
use serde::Serialize;
use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::protocol::JsonPacketStream;
use crate::{EmptyReplyMsg, StreamId};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TargetConfigurationTraits {
supported_options: HashMap<&'static str, bool>,
}
#[derive(Serialize)]
pub struct TargetConfigurationActorMsg {
actor: String,
configuration: HashMap<&'static str, bool>,
traits: TargetConfigurationTraits,
}
pub struct TargetConfigurationActor {
name: String,
configuration: HashMap<&'static str, bool>,
supported_options: HashMap<&'static str, bool>,
}
#[derive(Serialize)]
pub struct ThreadConfigurationActorMsg {
actor: String,
}
pub struct ThreadConfigurationActor {
name: String,
_configuration: HashMap<&'static str, bool>,
}
impl Actor for TargetConfigurationActor {
fn name(&self) -> String {
self.name.clone()
}
/// The target configuration actor can handle the following messages:
///
/// - `updateConfiguration`: Receives new configuration flags from the devtools host.
fn handle_message(
&self,
_registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
stream: &mut TcpStream,
_id: StreamId,
) -> Result<ActorMessageStatus, ()> {
Ok(match msg_type {
"updateConfiguration" => {
// TODO: Actually update configuration
let _ = stream.write_json_packet(&EmptyReplyMsg { from: self.name() });
ActorMessageStatus::Processed
},
_ => ActorMessageStatus::Ignored,
})
}
}
impl TargetConfigurationActor {
pub fn new(name: String) -> Self {
Self {
name,
configuration: HashMap::new(),
supported_options: HashMap::from([
("cacheDisabled", false),
("colorSchemeSimulation", false),
("customFormatters", false),
("customUserAgent", false),
("javascriptEnabled", false),
("overrideDPPX", false),
("printSimulationEnabled", false),
("rdmPaneMaxTouchPoints", false),
("rdmPaneOrientation", false),
("recordAllocations", false),
("reloadOnTouchSimulationToggle", false),
("restoreFocus", false),
("serviceWorkersTestingEnabled", false),
("setTabOffline", false),
("touchEventsOverride", false),
("tracerOptions", false),
("useSimpleHighlightersForReducedMotion", false),
]),
}
}
pub fn encodable(&self) -> TargetConfigurationActorMsg {
TargetConfigurationActorMsg {
actor: self.name(),
configuration: self.configuration.clone(),
traits: TargetConfigurationTraits {
supported_options: self.supported_options.clone(),
},
}
}
}
impl Actor for ThreadConfigurationActor {
fn name(&self) -> String {
self.name.clone()
}
/// The thread configuration actor can handle the following messages:
///
/// - `updateConfiguration`: Receives new configuration flags from the devtools host.
fn handle_message(
&self,
_registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
stream: &mut TcpStream,
_id: StreamId,
) -> Result<ActorMessageStatus, ()> {
Ok(match msg_type {
"updateConfiguration" => {
// TODO: Actually update configuration
let _ = stream.write_json_packet(&EmptyReplyMsg { from: self.name() });
ActorMessageStatus::Processed
},
_ => ActorMessageStatus::Ignored,
})
}
}
impl ThreadConfigurationActor {
pub fn new(name: String) -> Self {
Self {
name,
_configuration: HashMap::new(),
}
}
pub fn encodable(&self) -> ThreadConfigurationActorMsg {
ThreadConfigurationActorMsg { actor: self.name() }
}
}

View file

@ -59,7 +59,7 @@ impl Actor for DeviceActor {
apptype: "servo".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
appbuildid: BUILD_ID.to_string(),
platformversion: "124.0".to_string(),
platformversion: "125.0".to_string(),
brandName: "Servo".to_string(),
},
};

View file

@ -2,12 +2,16 @@
* 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/. */
//! Liberally derived from the [Firefox JS implementation]
//! (https://searchfox.org/mozilla-central/source/devtools/server/actors/descriptors/process.js)
use std::net::TcpStream;
use serde::Serialize;
use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::root::DescriptorTraits;
use crate::protocol::JsonPacketStream;
use crate::StreamId;
@ -17,14 +21,18 @@ struct ListWorkersReply {
workers: Vec<u32>, // TODO: use proper JSON structure.
}
pub struct ProcessActor {
name: String,
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessActorMsg {
actor: String,
id: u32,
is_parent: bool,
is_windowless_parent: bool,
traits: DescriptorTraits,
}
impl ProcessActor {
pub fn new(name: String) -> Self {
Self { name }
}
pub struct ProcessActor {
name: String,
}
impl Actor for ProcessActor {
@ -32,6 +40,9 @@ impl Actor for ProcessActor {
self.name.clone()
}
/// The process actor can handle the following messages:
///
/// - `listWorkers`: Returns a list of web workers, not supported yet.
fn handle_message(
&self,
_registry: &ActorRegistry,
@ -54,3 +65,19 @@ impl Actor for ProcessActor {
})
}
}
impl ProcessActor {
pub fn new(name: String) -> Self {
Self { name }
}
pub fn encodable(&self) -> ProcessActorMsg {
ProcessActorMsg {
actor: self.name(),
id: 0,
is_parent: true,
is_windowless_parent: false,
traits: Default::default(),
}
}
}

View file

@ -2,18 +2,20 @@
* 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/. */
//! Liberally derived from the [Firefox JS implementation]
//! (https://searchfox.org/mozilla-central/source/devtools/server/actors/root.js).
//! Connection point for all new remote devtools interactions, providing lists of know actors
//! that perform more specific actions (targets, addons, browser chrome, etc.)
use std::net::TcpStream;
use serde::Serialize;
use serde_json::{Map, Value};
/// Liberally derived from the [Firefox JS implementation]
/// (http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/root.js).
/// Connection point for all new remote devtools interactions, providing lists of know actors
/// that perform more specific actions (targets, addons, browser chrome, etc.)
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::device::DeviceActor;
use crate::actors::performance::PerformanceActor;
use crate::actors::process::{ProcessActor, ProcessActorMsg};
use crate::actors::tab::{TabDescriptorActor, TabDescriptorActorMsg};
use crate::actors::worker::{WorkerActor, WorkerMsg};
use crate::protocol::{ActorDescription, JsonPacketStream};
@ -95,7 +97,7 @@ pub struct Types {
#[derive(Serialize)]
struct ListProcessesResponse {
from: String,
processes: Vec<ProcessForm>,
processes: Vec<ProcessActorMsg>,
}
#[derive(Default, Serialize)]
@ -105,21 +107,11 @@ pub struct DescriptorTraits {
pub(crate) supports_reload_descriptor: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ProcessForm {
actor: String,
id: u32,
is_parent: bool,
is_windowless_parent: bool,
traits: DescriptorTraits,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetProcessResponse {
from: String,
process_descriptor: ProcessForm,
process_descriptor: ProcessActorMsg,
}
pub struct RootActor {
@ -155,30 +147,21 @@ impl Actor for RootActor {
},
"listProcesses" => {
let process = registry.find::<ProcessActor>(&self.process).encodable();
let reply = ListProcessesResponse {
from: self.name(),
processes: vec![ProcessForm {
actor: self.process.clone(),
id: 0,
is_parent: true,
is_windowless_parent: false,
traits: Default::default(),
}],
processes: vec![process],
};
let _ = stream.write_json_packet(&reply);
ActorMessageStatus::Processed
},
// TODO: Unexpected message getTarget for process (when inspecting)
"getProcess" => {
let process = registry.find::<ProcessActor>(&self.process).encodable();
let reply = GetProcessResponse {
from: self.name(),
process_descriptor: ProcessForm {
actor: self.process.clone(),
id: 0,
is_parent: true,
is_windowless_parent: false,
traits: Default::default(),
},
process_descriptor: process,
};
let _ = stream.write_json_packet(&reply);
ActorMessageStatus::Processed
@ -196,7 +179,6 @@ impl Actor for RootActor {
ActorMessageStatus::Processed
},
// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#listing-browser-tabs
"listTabs" => {
let actor = ListTabsReply {
from: "root".to_owned(),

View file

@ -2,6 +2,11 @@
* 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/. */
//! Liberally derived from the [Firefox JS implementation]
//! (https://searchfox.org/mozilla-central/source/devtools/server/actors/descriptors/tab.js)
//! Descriptor actor that represents a web view. It can link a tab to the corresponding watcher
//! actor to enable inspection.
use std::net::TcpStream;
use serde::Serialize;
@ -10,17 +15,19 @@ use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg};
use crate::actors::root::{DescriptorTraits, RootActor};
use crate::actors::watcher::{WatcherActor, WatcherActorMsg};
use crate::protocol::JsonPacketStream;
use crate::StreamId;
// https://searchfox.org/mozilla-central/source/devtools/server/actors/descriptors/tab.js
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TabDescriptorActorMsg {
actor: String,
browser_id: u32,
#[serde(rename = "browsingContextID")]
browsing_context_id: u32,
is_zombie_tab: bool,
#[serde(rename = "outerWindowID")]
outer_window_id: u32,
selected: bool,
title: String,
@ -46,6 +53,13 @@ struct GetFaviconReply {
favicon: String,
}
#[derive(Serialize)]
struct GetWatcherReply {
from: String,
#[serde(flatten)]
watcher: WatcherActorMsg,
}
pub struct TabDescriptorActor {
name: String,
browsing_context_actor: String,
@ -56,6 +70,14 @@ impl Actor for TabDescriptorActor {
self.name.clone()
}
/// The tab actor can handle the following messages:
///
/// - `getTarget`: Returns the surrounding `BrowsingContextActor`.
///
/// - `getFavicon`: Should return the tab favicon, but it is not yet supported.
///
/// - `getWatcher`: Returns a `WatcherActor` linked to the tab's `BrowsingContext`. It is used
/// to describe the debugging capabilities of this tab.
fn handle_message(
&self,
registry: &ActorRegistry,
@ -83,7 +105,15 @@ impl Actor for TabDescriptorActor {
});
ActorMessageStatus::Processed
},
// TODO: Unexpected message getWatcher when inspecting tab (create watcher actor)
"getWatcher" => {
let ctx_actor = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let watcher = registry.find::<WatcherActor>(&ctx_actor.watcher);
let _ = stream.write_json_packet(&GetWatcherReply {
from: self.name(),
watcher: watcher.encodable(),
});
ActorMessageStatus::Processed
},
_ => ActorMessageStatus::Ignored,
})
}
@ -105,21 +135,22 @@ impl TabDescriptorActor {
pub fn encodable(&self, registry: &ActorRegistry, selected: bool) -> TabDescriptorActorMsg {
let ctx_actor = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let browser_id = ctx_actor.active_pipeline.get().index.0.get();
let browsing_context_id = ctx_actor.browsing_context_id.index.0.get();
let title = ctx_actor.title.borrow().clone();
let url = ctx_actor.url.borrow().clone();
TabDescriptorActorMsg {
actor: self.name(),
browsing_context_id: ctx_actor.browsing_context_id.index.0.get(),
browser_id: ctx_actor.active_pipeline.get().index.0.get(),
browsing_context_id,
browser_id,
is_zombie_tab: false,
outer_window_id: ctx_actor.active_pipeline.get().index.0.get(),
outer_window_id: browser_id,
selected,
title,
traits: DescriptorTraits {
watcher: true,
supports_reload_descriptor: false,
supports_reload_descriptor: true,
},
url,
}

View file

@ -0,0 +1,259 @@
/* 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/. */
//! Liberally derived from the [Firefox JS implementation]
//! (https://searchfox.org/mozilla-central/source/devtools/server/actors/watcher.js).
//! The watcher is the main entry point when debugging an element. Right now only web views are supported.
//! It talks to the devtools remote and lists the capabilities of the inspected target, and it serves
//! as a bridge for messages between actors.
use std::collections::HashMap;
use std::net::TcpStream;
use serde::Serialize;
use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg};
use crate::actors::configuration::{
TargetConfigurationActor, TargetConfigurationActorMsg, ThreadConfigurationActor,
ThreadConfigurationActorMsg,
};
use crate::protocol::JsonPacketStream;
use crate::{EmptyReplyMsg, StreamId};
/// Describes the debugged context. It informs the server of which objects can be debugged.
/// <https://searchfox.org/mozilla-central/source/devtools/server/actors/watcher/session-context.js>
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionContext {
is_server_target_switching_enabled: bool,
supported_targets: HashMap<&'static str, bool>,
supported_resources: HashMap<&'static str, bool>,
context_type: SessionContextType,
}
impl SessionContext {
pub fn new(context_type: SessionContextType) -> Self {
Self {
is_server_target_switching_enabled: false,
// Right now we only support debugging web views (frames)
supported_targets: HashMap::from([
("frame", true),
("process", false),
("worker", false),
("service_worker", false),
("shared_worker", false),
]),
// At the moment we are blocking most resources to avoid errors
// Support for them will be enabled gradually once the corresponding actors start
// working propperly
supported_resources: HashMap::from([
("console-message", true),
("css-change", false),
("css-message", false),
("css-registered-properties", false),
("document-event", true),
("Cache", false),
("cookies", false),
("error-message", true),
("extension-storage", false),
("indexed-db", false),
("local-storage", false),
("session-storage", false),
("platform-message", false),
("network-event", false),
("network-event-stacktrace", false),
("reflow", false),
("stylesheet", false),
("source", false),
("thread-state", false),
("server-sent-event", false),
("websocket", false),
("jstracer-trace", false),
("jstracer-state", false),
("last-private-context-exit", false),
]),
context_type,
}
}
}
#[derive(Serialize)]
pub enum SessionContextType {
BrowserElement,
_ContextProcess,
_WebExtension,
_Worker,
_All,
}
#[derive(Serialize)]
struct WatchTargetsReply {
from: String,
#[serde(rename = "type")]
type_: String,
target: BrowsingContextActorMsg,
}
#[derive(Serialize)]
struct GetTargetConfigurationActorReply {
from: String,
configuration: TargetConfigurationActorMsg,
}
#[derive(Serialize)]
struct GetThreadConfigurationActorReply {
from: String,
configuration: ThreadConfigurationActorMsg,
}
#[derive(Serialize)]
struct WatcherTraits {
resources: HashMap<&'static str, bool>,
#[serde(flatten)]
targets: HashMap<&'static str, bool>,
}
#[derive(Serialize)]
pub struct WatcherActorMsg {
actor: String,
traits: WatcherTraits,
}
pub struct WatcherActor {
name: String,
browsing_context_actor: String,
session_context: SessionContext,
}
impl Actor for WatcherActor {
fn name(&self) -> String {
self.name.clone()
}
/// The watcher actor can handle the following messages:
///
/// - `watchTargets`: Returns a list of objects to debug. Since we only support web views, it
/// returns the associated `BrowsingContextActor`. Every target sent creates a
/// `target-available-form` event.
///
/// - `watchResources`: Start watching certain resource types. This sends
/// `resource-available-form` events.
///
/// - `getTargetConfigurationActor`: Returns the configuration actor for a specific target, so
/// that the server can update its settings.
///
/// - `getThreadConfigurationActor`: The same but with the configuration actor for the thread
fn handle_message(
&self,
registry: &ActorRegistry,
msg_type: &str,
msg: &Map<String, Value>,
stream: &mut TcpStream,
_id: StreamId,
) -> Result<ActorMessageStatus, ()> {
Ok(match msg_type {
"watchTargets" => {
let target = registry
.find::<BrowsingContextActor>(&self.browsing_context_actor)
.encodable();
let _ = stream.write_json_packet(&WatchTargetsReply {
from: self.name(),
type_: "target-available-form".into(),
target,
});
let target = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
target.frame_update(stream);
// Messages that contain a `type` field are used to send event callbacks, but they
// don't count as a reply. Since every message needs to be responded, we send an
// extra empty packet to the devtools host to inform that we successfully received
// and processed the message so that it can continue
let _ = stream.write_json_packet(&EmptyReplyMsg { from: self.name() });
ActorMessageStatus::Processed
},
"watchResources" => {
let Some(resource_types) = msg.get("resourceTypes") else {
return Ok(ActorMessageStatus::Ignored);
};
let Some(resource_types) = resource_types.as_array() else {
return Ok(ActorMessageStatus::Ignored);
};
for resource in resource_types {
let Some(resource) = resource.as_str() else {
continue;
};
let target =
registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
match resource {
"document-event" => {
target.document_event(stream);
},
_ => {},
}
let _ = stream.write_json_packet(&EmptyReplyMsg { from: self.name() });
}
ActorMessageStatus::Processed
},
"getTargetConfigurationActor" => {
let target = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let target_configuration =
registry.find::<TargetConfigurationActor>(&target.target_configuration);
let _ = stream.write_json_packet(&GetTargetConfigurationActorReply {
from: self.name(),
configuration: target_configuration.encodable(),
});
ActorMessageStatus::Processed
},
"getThreadConfigurationActor" => {
let target = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let thread_configuration =
registry.find::<ThreadConfigurationActor>(&target.thread_configuration);
let _ = stream.write_json_packet(&GetThreadConfigurationActorReply {
from: self.name(),
configuration: thread_configuration.encodable(),
});
ActorMessageStatus::Processed
},
_ => ActorMessageStatus::Ignored,
})
}
}
impl WatcherActor {
pub fn new(
name: String,
browsing_context_actor: String,
session_context: SessionContext,
) -> Self {
Self {
name,
browsing_context_actor,
session_context,
}
}
pub fn encodable(&self) -> WatcherActorMsg {
WatcherActorMsg {
actor: self.name(),
traits: WatcherTraits {
resources: self.session_context.supported_resources.clone(),
targets: self.session_context.supported_targets.clone(),
},
}
}
}

View file

@ -5,7 +5,7 @@
//! An actor-based remote devtools server implementation. Only tested with
//! nightly Firefox versions at time of writing. Largely based on
//! reverse-engineering of Firefox chrome devtool logs and reading of
//! [code](http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/).
//! [code](https://searchfox.org/mozilla-central/source/devtools/server).
#![crate_name = "devtools"]
#![crate_type = "rlib"]
@ -48,9 +48,10 @@ use crate::actors::worker::{WorkerActor, WorkerType};
use crate::protocol::JsonPacketStream;
mod actor;
/// Corresponds to <http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/>
/// <https://searchfox.org/mozilla-central/source/devtools/server/actors>
mod actors {
pub mod browsing_context;
pub mod configuration;
pub mod console;
pub mod device;
pub mod emulation;
@ -68,6 +69,7 @@ mod actors {
pub mod tab;
pub mod thread;
pub mod timeline;
pub mod watcher;
pub mod worker;
}
mod protocol;
@ -113,6 +115,11 @@ struct ResponseStartUpdateMsg {
response: ResponseStartMsg,
}
#[derive(Serialize)]
pub struct EmptyReplyMsg {
pub from: String,
}
/// Spin up a devtools server that listens for connections on the specified port.
pub fn start_server(port: u16, embedder: EmbedderProxy) -> Sender<DevtoolsControlMsg> {
let (sender, receiver) = unbounded();

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Low-level wire protocol implementation. Currently only supports
//! [JSON packets](https://wiki.mozilla.org/Remote_Debugging_Protocol_Stream_Transport#JSON_Packets).
//! [JSON packets](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#json-packets).
use std::error::Error;
use std::io::{Read, Write};
@ -63,7 +63,7 @@ impl JsonPacketStream for TcpStream {
}
fn read_json_packet(&mut self) -> Result<Option<Value>, String> {
// https://wiki.mozilla.org/Remote_Debugging_Protocol_Stream_Transport
// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#stream-transport
// In short, each JSON packet is [ascii length]:[JSON data of given length]
let mut buffer = vec![];
loop {