/* 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] //! (http://mxr.mozilla.org/mozilla-central/source/toolkit/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. use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::actors::emulation::EmulationActor; use crate::actors::inspector::InspectorActor; use crate::actors::performance::PerformanceActor; use crate::actors::profiler::ProfilerActor; use crate::actors::stylesheets::StyleSheetsActor; use crate::actors::tab::TabDescriptorActor; use crate::actors::thread::ThreadActor; use crate::actors::timeline::TimelineActor; use crate::protocol::JsonPacketStream; use crate::StreamId; use devtools_traits::DevtoolScriptControlMsg::{self, WantsLiveNotifications}; use devtools_traits::DevtoolsPageInfo; use devtools_traits::NavigationState; use ipc_channel::ipc::IpcSender; use msg::constellation_msg::{BrowsingContextId, PipelineId}; use serde_json::{Map, Value}; use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::net::TcpStream; #[derive(Serialize)] struct BrowsingContextTraits { isBrowsingContext: bool, } #[derive(Serialize)] struct AttachedTraits { reconfigure: bool, frames: bool, logInPage: bool, canRewind: bool, watchpoints: bool, } #[derive(Serialize)] struct BrowsingContextAttachedReply { from: String, #[serde(rename = "type")] type_: String, threadActor: String, cacheDisabled: bool, javascriptEnabled: bool, traits: AttachedTraits, } #[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 { id: u32, url: String, title: String, parentID: u32, } #[derive(Serialize)] struct ListWorkersReply { from: String, workers: Vec, } #[derive(Serialize)] struct WorkerMsg { id: u32, } #[derive(Serialize)] 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,*/ 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,*/ } pub(crate) struct BrowsingContextActor { pub name: String, pub title: RefCell, pub url: RefCell, pub console: String, pub _emulation: String, pub _inspector: String, pub _timeline: String, pub _profiler: String, pub _performance: String, pub _styleSheets: String, pub thread: String, pub _tab: String, pub streams: RefCell>, pub browsing_context_id: BrowsingContextId, pub active_pipeline: Cell, pub script_chan: IpcSender, } impl Actor for BrowsingContextActor { fn name(&self) -> String { self.name.clone() } fn handle_message( &self, _registry: &ActorRegistry, msg_type: &str, 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, }) } fn cleanup(&self, id: StreamId) { self.streams.borrow_mut().remove(&id); if self.streams.borrow().is_empty() { self.script_chan .send(WantsLiveNotifications(self.active_pipeline.get(), false)) .unwrap(); } } } impl BrowsingContextActor { pub(crate) fn new( console: String, id: BrowsingContextId, page_info: DevtoolsPageInfo, pipeline: PipelineId, script_sender: IpcSender, actors: &mut ActorRegistry, ) -> BrowsingContextActor { let emulation = EmulationActor::new(actors.new_name("emulation")); let name = actors.new_name("target"); let inspector = InspectorActor { name: actors.new_name("inspector"), walker: RefCell::new(None), pageStyle: RefCell::new(None), highlighter: RefCell::new(None), script_chan: script_sender.clone(), browsing_context: name.clone(), }; let timeline = TimelineActor::new(actors.new_name("timeline"), pipeline, script_sender.clone()); 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 tabdesc = TabDescriptorActor::new(actors, name.clone()); let target = BrowsingContextActor { name: name, script_chan: script_sender, title: RefCell::new(String::from(title)), url: RefCell::new(url.into_string()), console: 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(), streams: RefCell::new(HashMap::new()), browsing_context_id: id, active_pipeline: Cell::new(pipeline), }; 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(tabdesc)); target } pub fn encodable(&self) -> BrowsingContextActorMsg { BrowsingContextActorMsg { actor: self.name(), traits: BrowsingContextTraits { isBrowsingContext: 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(), //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(),*/ } } pub(crate) fn navigate(&self, state: NavigationState) { let (pipeline, title, url, state) = match state { NavigationState::Start(url) => (None, None, url, "start"), NavigationState::Stop(pipeline, info) => { (Some(pipeline), Some(info.title), info.url, "stop") }, }; if let Some(p) = pipeline { self.active_pipeline.set(p); } *self.url.borrow_mut() = url.as_str().to_owned(); if let Some(ref t) = title { *self.title.borrow_mut() = t.clone(); } let msg = TabNavigated { from: self.name(), type_: "tabNavigated".to_owned(), url: url.as_str().to_owned(), title: title, nativeConsoleAPI: true, state: state.to_owned(), isFrameSwitching: false, }; for stream in self.streams.borrow_mut().values_mut() { let _ = stream.write_json_packet(&msg); } } pub(crate) fn title_changed(&self, pipeline: PipelineId, title: String) { if pipeline != self.active_pipeline.get() { return; } *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, }