diff --git a/components/devtools/actors/browsing_context.rs b/components/devtools/actors/browsing_context.rs index 1058e1fe712..453e373fb88 100644 --- a/components/devtools/actors/browsing_context.rs +++ b/components/devtools/actors/browsing_context.rs @@ -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, } #[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, -} - -#[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, + #[serde(rename = "type")] + type_: String, + resources: Vec, } #[derive(Serialize)] -struct WorkerMsg { - id: u32, +#[serde(rename_all = "camelCase")] +struct ResourceAvailableMsg { + #[serde(rename = "hasNativeConsoleAPI")] + has_native_console_api: Option, + name: String, + #[serde(rename = "newURI")] + new_uri: Option, + resource_type: String, + time: u64, + title: Option, + url: Option, } #[derive(Serialize)] +struct TabNavigated { + from: String, + #[serde(rename = "type")] + type_: String, + url: String, + title: Option, + #[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, pub url: RefCell, + pub active_pipeline: Cell, + 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>, - pub browsing_context_id: BrowsingContextId, - pub active_pipeline: Cell, pub script_chan: IpcSender, + pub streams: RefCell>, + pub watcher: String, } impl Actor for BrowsingContextActor { @@ -149,89 +158,11 @@ impl Actor for BrowsingContextActor { &self, _registry: &ActorRegistry, msg_type: &str, - msg: &Map, - stream: &mut TcpStream, - id: StreamId, + _msg: &Map, + _stream: &mut TcpStream, + _id: StreamId, ) -> Result { 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, 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, - 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()), + }], + }); + } + } } diff --git a/components/devtools/actors/configuration.rs b/components/devtools/actors/configuration.rs new file mode 100644 index 00000000000..853c984d39b --- /dev/null +++ b/components/devtools/actors/configuration.rs @@ -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 +//! and +//! 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, + stream: &mut TcpStream, + _id: StreamId, + ) -> Result { + 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, + stream: &mut TcpStream, + _id: StreamId, + ) -> Result { + 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() } + } +} diff --git a/components/devtools/actors/device.rs b/components/devtools/actors/device.rs index 3fb5eb9a0a7..32fa2d92a53 100644 --- a/components/devtools/actors/device.rs +++ b/components/devtools/actors/device.rs @@ -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(), }, }; diff --git a/components/devtools/actors/process.rs b/components/devtools/actors/process.rs index 115946d7a05..2e40e598003 100644 --- a/components/devtools/actors/process.rs +++ b/components/devtools/actors/process.rs @@ -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, // 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(), + } + } +} diff --git a/components/devtools/actors/root.rs b/components/devtools/actors/root.rs index 3275f9e434f..efe58c2de71 100644 --- a/components/devtools/actors/root.rs +++ b/components/devtools/actors/root.rs @@ -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, + processes: Vec, } #[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::(&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::(&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(), diff --git a/components/devtools/actors/tab.rs b/components/devtools/actors/tab.rs index 221e8803dda..6d54ef2cb6f 100644 --- a/components/devtools/actors/tab.rs +++ b/components/devtools/actors/tab.rs @@ -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::(&self.browsing_context_actor); + let watcher = registry.find::(&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::(&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, } diff --git a/components/devtools/actors/watcher.rs b/components/devtools/actors/watcher.rs new file mode 100644 index 00000000000..1c68e921493 --- /dev/null +++ b/components/devtools/actors/watcher.rs @@ -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. +/// +#[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, + stream: &mut TcpStream, + _id: StreamId, + ) -> Result { + Ok(match msg_type { + "watchTargets" => { + let target = registry + .find::(&self.browsing_context_actor) + .encodable(); + let _ = stream.write_json_packet(&WatchTargetsReply { + from: self.name(), + type_: "target-available-form".into(), + target, + }); + + let target = registry.find::(&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::(&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::(&self.browsing_context_actor); + let target_configuration = + registry.find::(&target.target_configuration); + + let _ = stream.write_json_packet(&GetTargetConfigurationActorReply { + from: self.name(), + configuration: target_configuration.encodable(), + }); + + ActorMessageStatus::Processed + }, + "getThreadConfigurationActor" => { + let target = registry.find::(&self.browsing_context_actor); + let thread_configuration = + registry.find::(&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(), + }, + } + } +} diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index 1dbc4cd1b76..3475e10c258 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -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 +/// 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 { let (sender, receiver) = unbounded(); diff --git a/components/devtools/protocol.rs b/components/devtools/protocol.rs index 5926e27701a..a151fde7286 100644 --- a/components/devtools/protocol.rs +++ b/components/devtools/protocol.rs @@ -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, 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 {