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 * 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/. */ * 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. //! 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. //! Supports dynamic attaching and detaching which control notifications of navigation, etc.
@ -18,6 +18,7 @@ use serde::Serialize;
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::configuration::{TargetConfigurationActor, ThreadConfigurationActor};
use crate::actors::emulation::EmulationActor; use crate::actors::emulation::EmulationActor;
use crate::actors::inspector::InspectorActor; use crate::actors::inspector::InspectorActor;
use crate::actors::performance::PerformanceActor; use crate::actors::performance::PerformanceActor;
@ -26,118 +27,126 @@ use crate::actors::stylesheets::StyleSheetsActor;
use crate::actors::tab::TabDescriptorActor; use crate::actors::tab::TabDescriptorActor;
use crate::actors::thread::ThreadActor; use crate::actors::thread::ThreadActor;
use crate::actors::timeline::TimelineActor; use crate::actors::timeline::TimelineActor;
use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor};
use crate::protocol::JsonPacketStream; use crate::protocol::JsonPacketStream;
use crate::StreamId; use crate::StreamId;
#[derive(Serialize)] #[derive(Serialize)]
struct BrowsingContextTraits { struct FrameUpdateReply {
isBrowsingContext: bool,
}
#[derive(Serialize)]
struct AttachedTraits {
reconfigure: bool,
frames: bool,
logInPage: bool,
canRewind: bool,
watchpoints: bool,
}
#[derive(Serialize)]
struct BrowsingContextAttachedReply {
from: String, from: String,
#[serde(rename = "type")] #[serde(rename = "type")]
type_: String, type_: String,
threadActor: String, frames: Vec<FrameUpdateMsg>,
cacheDisabled: bool,
javascriptEnabled: bool,
traits: AttachedTraits,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct BrowsingContextDetachedReply { #[serde(rename_all = "camelCase")]
from: String, struct FrameUpdateMsg {
#[serde(rename = "type")]
type_: String,
}
#[derive(Serialize)]
struct ReconfigureReply {
from: String,
}
#[derive(Serialize)]
struct ListFramesReply {
from: String,
frames: Vec<FrameMsg>,
}
#[derive(Serialize)]
struct FrameMsg {
id: u32, id: u32,
is_top_level: bool,
url: String, url: String,
title: String, title: String,
parentID: u32,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct ListWorkersReply { struct ResourceAvailableReply {
from: String, from: String,
workers: Vec<WorkerMsg>, #[serde(rename = "type")]
type_: String,
resources: Vec<ResourceAvailableMsg>,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct WorkerMsg { #[serde(rename_all = "camelCase")]
id: u32, 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)] #[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 { pub struct BrowsingContextActorMsg {
actor: String, actor: String,
title: String, title: String,
url: String, url: String,
outerWindowID: u32, #[serde(rename = "outerWindowID")]
browsingContextId: u32, outer_window_id: u32,
consoleActor: String, #[serde(rename = "browsingContextID")]
/*emulationActor: String, browsing_context_id: u32,
inspectorActor: String, is_top_level_target: bool,
timelineActor: String, console_actor: String,
profilerActor: String, thread_actor: String,
performanceActor: String,
styleSheetsActor: String,*/
traits: BrowsingContextTraits, traits: BrowsingContextTraits,
// Part of the official protocol, but not yet implemented. // Part of the official protocol, but not yet implemented.
/*storageActor: String, // emulation_actor: String,
memoryActor: String, // inspector_actor: String,
framerateActor: String, // timeline_actor: String,
reflowActor: String, // profiler_actor: String,
cssPropertiesActor: String, // performance_actor: String,
animationsActor: String, // style_sheets_actor: String,
webExtensionInspectedWindowActor: String, // storage_actor: String,
accessibilityActor: String, // memory_actor: String,
screenshotActor: String, // framerate_actor: String,
changesActor: String, // reflow_actor: String,
webSocketActor: String, // css_properties_actor: String,
manifestActor: 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(crate) struct BrowsingContextActor {
pub name: String, pub name: String,
pub title: RefCell<String>, pub title: RefCell<String>,
pub url: RefCell<String>, pub url: RefCell<String>,
pub active_pipeline: Cell<PipelineId>,
pub browsing_context_id: BrowsingContextId,
pub console: String, pub console: String,
pub _emulation: String, pub _emulation: String,
pub _inspector: String, pub _inspector: String,
pub _timeline: String,
pub _profiler: String,
pub _performance: 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 thread: String,
pub _timeline: String,
pub _tab: 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 script_chan: IpcSender<DevtoolScriptControlMsg>,
pub streams: RefCell<HashMap<StreamId, TcpStream>>,
pub watcher: String,
} }
impl Actor for BrowsingContextActor { impl Actor for BrowsingContextActor {
@ -149,89 +158,11 @@ impl Actor for BrowsingContextActor {
&self, &self,
_registry: &ActorRegistry, _registry: &ActorRegistry,
msg_type: &str, msg_type: &str,
msg: &Map<String, Value>, _msg: &Map<String, Value>,
stream: &mut TcpStream, _stream: &mut TcpStream,
id: StreamId, _id: StreamId,
) -> Result<ActorMessageStatus, ()> { ) -> Result<ActorMessageStatus, ()> {
Ok(match msg_type { 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, _ => ActorMessageStatus::Ignored,
}) })
} }
@ -255,9 +186,10 @@ impl BrowsingContextActor {
script_sender: IpcSender<DevtoolScriptControlMsg>, script_sender: IpcSender<DevtoolScriptControlMsg>,
actors: &mut ActorRegistry, actors: &mut ActorRegistry,
) -> BrowsingContextActor { ) -> BrowsingContextActor {
let emulation = EmulationActor::new(actors.new_name("emulation"));
let name = actors.new_name("target"); let name = actors.new_name("target");
let DevtoolsPageInfo { title, url } = page_info;
let emulation = EmulationActor::new(actors.new_name("emulation"));
let inspector = InspectorActor { let inspector = InspectorActor {
name: actors.new_name("inspector"), name: actors.new_name("inspector"),
@ -268,48 +200,66 @@ impl BrowsingContextActor {
browsing_context: name.clone(), browsing_context: name.clone(),
}; };
let timeline = let performance = PerformanceActor::new(actors.new_name("performance"));
TimelineActor::new(actors.new_name("timeline"), pipeline, script_sender.clone());
let profiler = ProfilerActor::new(actors.new_name("profiler")); 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 // the strange switch between styleSheets and stylesheets is due
// to an inconsistency in devtools. See Bug #1498893 in bugzilla // to an inconsistency in devtools. See Bug #1498893 in bugzilla
let styleSheets = StyleSheetsActor::new(actors.new_name("stylesheets")); let style_sheets = StyleSheetsActor::new(actors.new_name("stylesheets"));
let thread = ThreadActor::new(actors.new_name("context"));
let DevtoolsPageInfo { title, url } = page_info;
let tabdesc = TabDescriptorActor::new(actors, name.clone()); 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 { let target = BrowsingContextActor {
name, name,
script_chan: script_sender, script_chan: script_sender,
title: RefCell::new(title), title: RefCell::new(title),
url: RefCell::new(url.into_string()), url: RefCell::new(url.into_string()),
active_pipeline: Cell::new(pipeline),
browsing_context_id: id,
console, console,
_emulation: emulation.name(), _emulation: emulation.name(),
_inspector: inspector.name(), _inspector: inspector.name(),
_timeline: timeline.name(),
_profiler: profiler.name(),
_performance: performance.name(), _performance: performance.name(),
_styleSheets: styleSheets.name(), _profiler: profiler.name(),
_tab: tabdesc.name(),
thread: thread.name(),
streams: RefCell::new(HashMap::new()), streams: RefCell::new(HashMap::new()),
browsing_context_id: id, _style_sheets: style_sheets.name(),
active_pipeline: Cell::new(pipeline), _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(emulation));
actors.register(Box::new(inspector)); actors.register(Box::new(inspector));
actors.register(Box::new(timeline));
actors.register(Box::new(profiler));
actors.register(Box::new(performance)); actors.register(Box::new(performance));
actors.register(Box::new(styleSheets)); actors.register(Box::new(profiler));
actors.register(Box::new(thread)); actors.register(Box::new(style_sheets));
actors.register(Box::new(tabdesc)); 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 target
} }
@ -318,21 +268,23 @@ impl BrowsingContextActor {
BrowsingContextActorMsg { BrowsingContextActorMsg {
actor: self.name(), actor: self.name(),
traits: BrowsingContextTraits { traits: BrowsingContextTraits {
isBrowsingContext: true, is_browsing_context: true,
}, },
title: self.title.borrow().clone(), title: self.title.borrow().clone(),
url: self.url.borrow().clone(), url: self.url.borrow().clone(),
//FIXME: shouldn't ignore pipeline namespace field //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 //FIXME: shouldn't ignore pipeline namespace field
outerWindowID: self.active_pipeline.get().index.0.get(), outer_window_id: self.active_pipeline.get().index.0.get(),
consoleActor: self.console.clone(), is_top_level_target: true,
/*emulationActor: self.emulation.clone(), console_actor: self.console.clone(),
inspectorActor: self.inspector.clone(), thread_actor: self.thread.clone(),
timelineActor: self.timeline.clone(), // emulation_actor: self.emulation.clone(),
profilerActor: self.profiler.clone(), // inspector_actor: self.inspector.clone(),
performanceActor: self.performance.clone(), // performance_actor: self.performance.clone(),
styleSheetsActor: self.styleSheets.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(), type_: "tabNavigated".to_owned(),
url: url.as_str().to_owned(), url: url.as_str().to_owned(),
title, title,
nativeConsoleAPI: true, native_console_api: true,
state: state.to_owned(), state: state.to_owned(),
isFrameSwitching: false, is_frame_switching: false,
}; };
for stream in self.streams.borrow_mut().values_mut() { for stream in self.streams.borrow_mut().values_mut() {
let _ = stream.write_json_packet(&msg); let _ = stream.write_json_packet(&msg);
} }
@ -371,16 +324,40 @@ impl BrowsingContextActor {
} }
*self.title.borrow_mut() = title; *self.title.borrow_mut() = title;
} }
}
#[derive(Serialize)] pub(crate) fn frame_update(&self, stream: &mut TcpStream) {
struct TabNavigated { let _ = stream.write_json_packet(&FrameUpdateReply {
from: String, from: self.name(),
#[serde(rename = "type")] type_: "frameUpdate".into(),
type_: String, frames: vec![FrameUpdateMsg {
url: String, id: self.browsing_context_id.index.0.get(),
title: Option<String>, is_top_level: true,
nativeConsoleAPI: bool, title: self.title.borrow().clone(),
state: String, url: self.url.borrow().clone(),
isFrameSwitching: bool, }],
});
}
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(), apptype: "servo".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(), version: env!("CARGO_PKG_VERSION").to_string(),
appbuildid: BUILD_ID.to_string(), appbuildid: BUILD_ID.to_string(),
platformversion: "124.0".to_string(), platformversion: "125.0".to_string(),
brandName: "Servo".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 * 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/. */ * 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 std::net::TcpStream;
use serde::Serialize; use serde::Serialize;
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::root::DescriptorTraits;
use crate::protocol::JsonPacketStream; use crate::protocol::JsonPacketStream;
use crate::StreamId; use crate::StreamId;
@ -17,14 +21,18 @@ struct ListWorkersReply {
workers: Vec<u32>, // TODO: use proper JSON structure. workers: Vec<u32>, // TODO: use proper JSON structure.
} }
pub struct ProcessActor { #[derive(Serialize)]
name: String, #[serde(rename_all = "camelCase")]
pub struct ProcessActorMsg {
actor: String,
id: u32,
is_parent: bool,
is_windowless_parent: bool,
traits: DescriptorTraits,
} }
impl ProcessActor { pub struct ProcessActor {
pub fn new(name: String) -> Self { name: String,
Self { name }
}
} }
impl Actor for ProcessActor { impl Actor for ProcessActor {
@ -32,6 +40,9 @@ impl Actor for ProcessActor {
self.name.clone() self.name.clone()
} }
/// The process actor can handle the following messages:
///
/// - `listWorkers`: Returns a list of web workers, not supported yet.
fn handle_message( fn handle_message(
&self, &self,
_registry: &ActorRegistry, _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 * 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/. */ * 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 std::net::TcpStream;
use serde::Serialize; use serde::Serialize;
use serde_json::{Map, Value}; 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::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::device::DeviceActor; use crate::actors::device::DeviceActor;
use crate::actors::performance::PerformanceActor; use crate::actors::performance::PerformanceActor;
use crate::actors::process::{ProcessActor, ProcessActorMsg};
use crate::actors::tab::{TabDescriptorActor, TabDescriptorActorMsg}; use crate::actors::tab::{TabDescriptorActor, TabDescriptorActorMsg};
use crate::actors::worker::{WorkerActor, WorkerMsg}; use crate::actors::worker::{WorkerActor, WorkerMsg};
use crate::protocol::{ActorDescription, JsonPacketStream}; use crate::protocol::{ActorDescription, JsonPacketStream};
@ -95,7 +97,7 @@ pub struct Types {
#[derive(Serialize)] #[derive(Serialize)]
struct ListProcessesResponse { struct ListProcessesResponse {
from: String, from: String,
processes: Vec<ProcessForm>, processes: Vec<ProcessActorMsg>,
} }
#[derive(Default, Serialize)] #[derive(Default, Serialize)]
@ -105,21 +107,11 @@ pub struct DescriptorTraits {
pub(crate) supports_reload_descriptor: bool, 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)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct GetProcessResponse { struct GetProcessResponse {
from: String, from: String,
process_descriptor: ProcessForm, process_descriptor: ProcessActorMsg,
} }
pub struct RootActor { pub struct RootActor {
@ -155,30 +147,21 @@ impl Actor for RootActor {
}, },
"listProcesses" => { "listProcesses" => {
let process = registry.find::<ProcessActor>(&self.process).encodable();
let reply = ListProcessesResponse { let reply = ListProcessesResponse {
from: self.name(), from: self.name(),
processes: vec![ProcessForm { processes: vec![process],
actor: self.process.clone(),
id: 0,
is_parent: true,
is_windowless_parent: false,
traits: Default::default(),
}],
}; };
let _ = stream.write_json_packet(&reply); let _ = stream.write_json_packet(&reply);
ActorMessageStatus::Processed ActorMessageStatus::Processed
}, },
// TODO: Unexpected message getTarget for process (when inspecting)
"getProcess" => { "getProcess" => {
let process = registry.find::<ProcessActor>(&self.process).encodable();
let reply = GetProcessResponse { let reply = GetProcessResponse {
from: self.name(), from: self.name(),
process_descriptor: ProcessForm { process_descriptor: process,
actor: self.process.clone(),
id: 0,
is_parent: true,
is_windowless_parent: false,
traits: Default::default(),
},
}; };
let _ = stream.write_json_packet(&reply); let _ = stream.write_json_packet(&reply);
ActorMessageStatus::Processed ActorMessageStatus::Processed
@ -196,7 +179,6 @@ impl Actor for RootActor {
ActorMessageStatus::Processed ActorMessageStatus::Processed
}, },
// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#listing-browser-tabs
"listTabs" => { "listTabs" => {
let actor = ListTabsReply { let actor = ListTabsReply {
from: "root".to_owned(), 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 * 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/. */ * 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 std::net::TcpStream;
use serde::Serialize; use serde::Serialize;
@ -10,17 +15,19 @@ use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg}; use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg};
use crate::actors::root::{DescriptorTraits, RootActor}; use crate::actors::root::{DescriptorTraits, RootActor};
use crate::actors::watcher::{WatcherActor, WatcherActorMsg};
use crate::protocol::JsonPacketStream; use crate::protocol::JsonPacketStream;
use crate::StreamId; use crate::StreamId;
// https://searchfox.org/mozilla-central/source/devtools/server/actors/descriptors/tab.js
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TabDescriptorActorMsg { pub struct TabDescriptorActorMsg {
actor: String, actor: String,
browser_id: u32, browser_id: u32,
#[serde(rename = "browsingContextID")]
browsing_context_id: u32, browsing_context_id: u32,
is_zombie_tab: bool, is_zombie_tab: bool,
#[serde(rename = "outerWindowID")]
outer_window_id: u32, outer_window_id: u32,
selected: bool, selected: bool,
title: String, title: String,
@ -46,6 +53,13 @@ struct GetFaviconReply {
favicon: String, favicon: String,
} }
#[derive(Serialize)]
struct GetWatcherReply {
from: String,
#[serde(flatten)]
watcher: WatcherActorMsg,
}
pub struct TabDescriptorActor { pub struct TabDescriptorActor {
name: String, name: String,
browsing_context_actor: String, browsing_context_actor: String,
@ -56,6 +70,14 @@ impl Actor for TabDescriptorActor {
self.name.clone() 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( fn handle_message(
&self, &self,
registry: &ActorRegistry, registry: &ActorRegistry,
@ -83,7 +105,15 @@ impl Actor for TabDescriptorActor {
}); });
ActorMessageStatus::Processed 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, _ => ActorMessageStatus::Ignored,
}) })
} }
@ -105,21 +135,22 @@ impl TabDescriptorActor {
pub fn encodable(&self, registry: &ActorRegistry, selected: bool) -> TabDescriptorActorMsg { pub fn encodable(&self, registry: &ActorRegistry, selected: bool) -> TabDescriptorActorMsg {
let ctx_actor = registry.find::<BrowsingContextActor>(&self.browsing_context_actor); 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 title = ctx_actor.title.borrow().clone();
let url = ctx_actor.url.borrow().clone(); let url = ctx_actor.url.borrow().clone();
TabDescriptorActorMsg { TabDescriptorActorMsg {
actor: self.name(), actor: self.name(),
browsing_context_id: ctx_actor.browsing_context_id.index.0.get(), browsing_context_id,
browser_id: ctx_actor.active_pipeline.get().index.0.get(), browser_id,
is_zombie_tab: false, is_zombie_tab: false,
outer_window_id: ctx_actor.active_pipeline.get().index.0.get(), outer_window_id: browser_id,
selected, selected,
title, title,
traits: DescriptorTraits { traits: DescriptorTraits {
watcher: true, watcher: true,
supports_reload_descriptor: false, supports_reload_descriptor: true,
}, },
url, 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 //! An actor-based remote devtools server implementation. Only tested with
//! nightly Firefox versions at time of writing. Largely based on //! nightly Firefox versions at time of writing. Largely based on
//! reverse-engineering of Firefox chrome devtool logs and reading of //! 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_name = "devtools"]
#![crate_type = "rlib"] #![crate_type = "rlib"]
@ -48,9 +48,10 @@ use crate::actors::worker::{WorkerActor, WorkerType};
use crate::protocol::JsonPacketStream; use crate::protocol::JsonPacketStream;
mod actor; 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 { mod actors {
pub mod browsing_context; pub mod browsing_context;
pub mod configuration;
pub mod console; pub mod console;
pub mod device; pub mod device;
pub mod emulation; pub mod emulation;
@ -68,6 +69,7 @@ mod actors {
pub mod tab; pub mod tab;
pub mod thread; pub mod thread;
pub mod timeline; pub mod timeline;
pub mod watcher;
pub mod worker; pub mod worker;
} }
mod protocol; mod protocol;
@ -113,6 +115,11 @@ struct ResponseStartUpdateMsg {
response: ResponseStartMsg, response: ResponseStartMsg,
} }
#[derive(Serialize)]
pub struct EmptyReplyMsg {
pub from: String,
}
/// Spin up a devtools server that listens for connections on the specified port. /// Spin up a devtools server that listens for connections on the specified port.
pub fn start_server(port: u16, embedder: EmbedderProxy) -> Sender<DevtoolsControlMsg> { pub fn start_server(port: u16, embedder: EmbedderProxy) -> Sender<DevtoolsControlMsg> {
let (sender, receiver) = unbounded(); let (sender, receiver) = unbounded();

View file

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