From f0f7e98dfaf73e8a759a1c56979fff1c418e2ead Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Wed, 27 Aug 2014 15:06:27 -0400 Subject: [PATCH 1/7] Dump initial prototype of devtools server into the build. Expect lies if you try to use it for anything real. --- Cargo.lock | 16 + Cargo.toml | 3 + components/compositing/Cargo.toml | 6 + components/compositing/constellation.rs | 7 +- components/compositing/lib.rs | 1 + components/compositing/pipeline.rs | 3 + components/devtools/Cargo.toml | 12 + components/devtools/lib.rs | 519 ++++++++++++++++++++++++ components/devtools_traits/Cargo.toml | 8 + components/devtools_traits/lib.rs | 33 ++ components/script/Cargo.toml | 3 + components/script/lib.rs | 1 + components/script/script_task.rs | 36 +- components/script_traits/Cargo.toml | 3 + components/script_traits/lib.rs | 5 +- components/util/opts.rs | 5 + ports/cef/Cargo.toml | 3 + ports/cef/core.rs | 1 + src/lib.rs | 9 +- 19 files changed, 670 insertions(+), 4 deletions(-) create mode 100644 components/devtools/Cargo.toml create mode 100644 components/devtools/lib.rs create mode 100644 components/devtools_traits/Cargo.toml create mode 100644 components/devtools_traits/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a7baeb33a8e..43c64bcd7e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,6 +3,7 @@ name = "servo" version = "0.0.1" dependencies = [ "compositing 0.0.1", + "devtools 0.0.1", "gfx 0.0.1", "layout 0.0.1", "msg 0.0.1", @@ -60,6 +61,8 @@ dependencies = [ "azure 0.1.0 (git+https://github.com/servo/rust-azure#9c5567b79d8b87e8ef3b48c5842f453978035d21)", "core_graphics 0.1.0 (git+https://github.com/servo/rust-core-graphics#04bd18a4eb83a645a1a32326a33149ba2d0e81be)", "core_text 0.1.0 (git+https://github.com/servo/rust-core-text#e2280222889c030df27ded9a378c14a0e31ab463)", + "devtools 0.0.1", + "devtools_traits 0.0.1", "geom 0.1.0 (git+https://github.com/servo/rust-geom#2982b770db6e5e3270305e0fd6b8068f6f80a489)", "gfx 0.0.1", "glfw 0.0.1 (git+https://github.com/servo/glfw-rs?ref=servo#dd1a111c827994886d2cdebf91a1838603256390)", @@ -105,6 +108,17 @@ dependencies = [ "encoding 0.1.0 (git+https://github.com/lifthrasiir/rust-encoding#12b6610adff6eddc060691888c36017cd3ad57f7)", ] +[[package]] +name = "devtools" +version = "0.0.1" +dependencies = [ + "devtools_traits 0.0.1", +] + +[[package]] +name = "devtools_traits" +version = "0.0.1" + [[package]] name = "egl" version = "0.1.0" @@ -364,6 +378,7 @@ version = "0.0.1" dependencies = [ "canvas 0.0.1", "cssparser 0.1.0 (git+https://github.com/servo/rust-cssparser#42346400a6629b17a48d06f0a9b28ae498947c6f)", + "devtools_traits 0.0.1", "encoding 0.1.0 (git+https://github.com/lifthrasiir/rust-encoding#12b6610adff6eddc060691888c36017cd3ad57f7)", "geom 0.1.0 (git+https://github.com/servo/rust-geom#2982b770db6e5e3270305e0fd6b8068f6f80a489)", "gfx 0.0.1", @@ -383,6 +398,7 @@ dependencies = [ name = "script_traits" version = "0.0.1" dependencies = [ + "devtools_traits 0.0.1", "geom 0.1.0 (git+https://github.com/servo/rust-geom#2982b770db6e5e3270305e0fd6b8068f6f80a489)", "msg 0.0.1", "net 0.0.1", diff --git a/Cargo.toml b/Cargo.toml index 2472f99be99..0561ad480f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,5 +46,8 @@ path = "components/layout" [dependencies.gfx] path = "components/gfx" +[dependencies.devtools] +path = "components/devtools" + [dependencies.url] git = "https://github.com/servo/rust-url" diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml index d9b6adc57d2..e306ae8fd94 100644 --- a/components/compositing/Cargo.toml +++ b/components/compositing/Cargo.toml @@ -25,6 +25,12 @@ path = "../net" [dependencies.util] path = "../util" +[dependencies.devtools] +path = "../devtools" + +[dependencies.devtools_traits] +path = "../devtools_traits" + [dependencies.alert] git = "https://github.com/servo/rust-alert" diff --git a/components/compositing/constellation.rs b/components/compositing/constellation.rs index add70aaf39c..70b7e387773 100644 --- a/components/compositing/constellation.rs +++ b/components/compositing/constellation.rs @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use compositor_task::{CompositorChan, LoadComplete, ShutdownComplete, SetLayerOrigin, SetIds}; +use devtools_traits::DevtoolsControlChan; use std::collections::hashmap::{HashMap, HashSet}; use geom::rect::{Rect, TypedRect}; use geom::scale_factor::ScaleFactor; @@ -41,6 +42,7 @@ pub struct Constellation { pub compositor_chan: CompositorChan, pub resource_task: ResourceTask, pub image_cache_task: ImageCacheTask, + devtools_chan: Option, pipelines: HashMap>, font_cache_task: FontCacheTask, navigation_context: NavigationContext, @@ -244,7 +246,8 @@ impl Constellation { resource_task: ResourceTask, image_cache_task: ImageCacheTask, font_cache_task: FontCacheTask, - time_profiler_chan: TimeProfilerChan) + time_profiler_chan: TimeProfilerChan, + devtools_chan: Option) -> ConstellationChan { let (constellation_port, constellation_chan) = ConstellationChan::new(); let constellation_chan_clone = constellation_chan.clone(); @@ -254,6 +257,7 @@ impl Constellation { chan: constellation_chan_clone, request_port: constellation_port, compositor_chan: compositor_chan, + devtools_chan: devtools_chan, resource_task: resource_task, image_cache_task: image_cache_task, font_cache_task: font_cache_task, @@ -295,6 +299,7 @@ impl Constellation { subpage_id, self.chan.clone(), self.compositor_chan.clone(), + self.devtools_chan.clone(), self.image_cache_task.clone(), self.font_cache_task.clone(), self.resource_task.clone(), diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index f7644e8fc7f..83f9a9b61d2 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -16,6 +16,7 @@ extern crate debug; extern crate alert; extern crate azure; +extern crate devtools_traits; extern crate geom; extern crate gfx; #[cfg(not(target_os="android"))] diff --git a/components/compositing/pipeline.rs b/components/compositing/pipeline.rs index 8dd8c1f0de7..41ae22b13f7 100644 --- a/components/compositing/pipeline.rs +++ b/components/compositing/pipeline.rs @@ -7,6 +7,7 @@ use layout_traits::{LayoutTaskFactory, LayoutControlChan}; use script_traits::{ScriptControlChan, ScriptTaskFactory}; use script_traits::{AttachLayoutMsg, LoadMsg, NewLayoutInfo, ExitPipelineMsg}; +use devtools_traits::DevtoolsControlChan; use gfx::render_task::{PaintPermissionGranted, PaintPermissionRevoked}; use gfx::render_task::{RenderChan, RenderTask}; use servo_msg::constellation_msg::{ConstellationChan, Failure, PipelineId, SubpageId}; @@ -49,6 +50,7 @@ impl Pipeline { subpage_id: Option, constellation_chan: ConstellationChan, compositor_chan: CompositorChan, + devtools_chan: Option, image_cache_task: ImageCacheTask, font_cache_task: FontCacheTask, resource_task: ResourceTask, @@ -82,6 +84,7 @@ impl Pipeline { failure.clone(), resource_task.clone(), image_cache_task.clone(), + devtools_chan, window_size); ScriptControlChan(script_chan) } diff --git a/components/devtools/Cargo.toml b/components/devtools/Cargo.toml new file mode 100644 index 00000000000..d27e6bfed5a --- /dev/null +++ b/components/devtools/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "devtools" +version = "0.0.1" +authors = ["The Servo Project Developers"] + +[lib] +name = "devtools" +path = "lib.rs" +crate-type = ["dylib"] + +[dependencies.devtools_traits] +path = "../devtools_traits" diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs new file mode 100644 index 00000000000..991ddeb5376 --- /dev/null +++ b/components/devtools/lib.rs @@ -0,0 +1,519 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#![crate_name = "devtools"] +#![crate_type = "rlib"] + +#![comment = "The Servo Parallel Browser Project"] +#![license = "MPL"] + +#![feature(phase)] + +#![feature(phase)] +#[phase(plugin, link)] +extern crate log; + +extern crate collections; +extern crate core; +extern crate devtools_traits; +extern crate debug; +extern crate std; +extern crate serialize; +extern crate sync; + +use devtools_traits::{ServerExitMsg, DevtoolsControlMsg, NewGlobal}; + +use collections::TreeMap; +use std::any::{Any, AnyRefExt}; +use std::collections::hashmap::HashMap; +use std::comm; +use std::comm::{Disconnected, Empty}; +use std::io::{TcpListener, TcpStream}; +use std::io::{Acceptor, Listener, EndOfFile, IoError, TimedOut}; +use std::num; +use std::task::TaskBuilder; +use serialize::{json, Encodable}; +use sync::{Arc, Mutex}; + +#[deriving(Encodable)] +struct ActorTraits { + sources: bool +} + +#[deriving(Encodable)] +struct RootActorMsg { + from: String, + applicationType: String, + traits: ActorTraits, +} + +struct RootActor { + tabs: Vec, +} + +#[deriving(Encodable)] +struct ErrorReply { + from: String, + error: String, + message: String, +} + +#[deriving(Encodable)] +struct TabActorMsg { + actor: String, + title: String, + url: String, + outerWindowID: uint, + consoleActor: String, +} + +struct TabActor { + name: String, + title: String, + url: String, +} + +#[deriving(Encodable)] +struct ListTabsReply { + from: String, + selected: uint, + tabs: Vec, +} + +#[deriving(Encodable)] +struct TabTraits { + reconfigure: bool, +} + +#[deriving(Encodable)] +struct TabAttachedReply { + from: String, + __type__: String, + threadActor: String, + cacheEnabled: bool, + javascriptEnabled: bool, + traits: TabTraits, +} + +#[deriving(Encodable)] +struct TabDetachedReply { + from: String, + __type__: String, +} + +#[deriving(Encodable)] +struct StartedListenersReply { + from: String, + nativeConsoleAPI: bool, + startedListeners: Vec, +} + +#[deriving(Encodable)] +struct ConsoleAPIMessage { + _type: String, +} + +#[deriving(Encodable)] +struct PageErrorMessage { + _type: String, + errorMessage: String, + sourceName: String, + lineText: String, + lineNumber: uint, + columnNumber: uint, + category: String, + timeStamp: uint, + warning: bool, + error: bool, + exception: bool, + strict: bool, + private: bool, +} + +#[deriving(Encodable)] +struct LogMessage { + _type: String, + timeStamp: uint, + message: String, +} + +#[deriving(Encodable)] +enum ConsoleMessageType { + ConsoleAPIType(ConsoleAPIMessage), + PageErrorType(PageErrorMessage), + LogMessageType(LogMessage), +} + +#[deriving(Encodable)] +struct GetCachedMessagesReply { + from: String, + messages: Vec, +} + +#[deriving(Encodable)] +struct StopListenersReply { + from: String, + stoppedListeners: Vec, +} + +#[deriving(Encodable)] +struct AutocompleteReply { + from: String, + matches: Vec, + matchProp: String, +} + +#[deriving(Encodable)] +struct EvaluateJSReply { + from: String, + input: String, + result: json::Json, + timestamp: uint, + exception: json::Json, + exceptionMessage: String, + helperResult: json::Json, +} + +struct ActorRegistry { + actors: HashMap>, +} + +impl ActorRegistry { + fn new() -> ActorRegistry { + ActorRegistry { + actors: HashMap::new(), + } + } + + fn register(&mut self, actor: Box) { + self.actors.insert(actor.name().to_string(), actor); + } + + fn find<'a, T: 'static>(&'a self, name: &str) -> &'a T { + let actor: &Actor+Send+Sized = *self.actors.find(&name.to_string()).unwrap(); + (actor as &Any).downcast_ref::().unwrap() + } + + fn handle_message(&self, msg: &json::Object, stream: &mut TcpStream) { + let to = msg.find(&"to".to_string()).unwrap().as_string().unwrap(); + match self.actors.find(&to.to_string()) { + None => println!("message received for unknown actor \"{:s}\"", to), + Some(actor) => { + let msg_type = msg.find(&"type".to_string()).unwrap() + .as_string().unwrap(); + if !actor.handle_message(self, &msg_type.to_string(), msg, stream) { + println!("unexpected message type \"{:s}\" found for actor \"{:s}\"", + msg_type, to); + } + } + } + } +} + +trait Actor: Any { + fn handle_message(&self, + registry: &ActorRegistry, + msg_type: &String, + msg: &json::Object, + stream: &mut TcpStream) -> bool; + fn name(&self) -> String; +} + +impl Actor for RootActor { + fn name(&self) -> String { + "root".to_string() + } + + fn handle_message(&self, + registry: &ActorRegistry, + msg_type: &String, + _msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "listAddons" => { + let actor = ErrorReply { + from: "root".to_string(), + error: "noAddons".to_string(), + message: "This root actor has no browser addons.".to_string(), + }; + stream.write_json_packet(&actor); + true + } + "listTabs" => { + let actor = ListTabsReply { + from: "root".to_string(), + selected: 0, + tabs: self.tabs.iter().map(|tab| { + registry.find::(tab.as_slice()).encodable() + }).collect() + }; + stream.write_json_packet(&actor); + true + } + _ => false + } + } +} + +impl RootActor { + fn encodable(&self) -> RootActorMsg { + RootActorMsg { + from: "root".to_string(), + applicationType: "browser".to_string(), + traits: ActorTraits { + sources: true, + }, + } + } +} + +impl Actor for TabActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + _registry: &ActorRegistry, + msg_type: &String, + msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "attach" => { + let msg = TabAttachedReply { + from: self.name(), + __type__: "tabAttached".to_string(), + threadActor: self.name(), + cacheEnabled: false, + javascriptEnabled: true, + traits: TabTraits { + reconfigure: true, + }, + }; + stream.write_json_packet(&msg); + true + } + "detach" => { + let msg = TabDetachedReply { + from: self.name(), + __type__: "detached".to_string(), + }; + stream.write_json_packet(&msg); + true + } + "startListeners" => { + let msg = StartedListenersReply { + from: self.name(), + nativeConsoleAPI: true, + startedListeners: + vec!("PageError".to_string(), "ConsoleAPI".to_string()), + }; + stream.write_json_packet(&msg); + true + } + "getCachedMessages" => { + let types = msg.find(&"messageTypes".to_string()).unwrap().as_list().unwrap(); + let mut messages = vec!(); + for msg_type in types.iter() { + let msg_type = msg_type.as_string().unwrap(); + match msg_type.as_slice() { + "ConsoleAPI" => { + //XXX need more info about consoleapi properties + } + "PageError" => { + let message = PageErrorMessage { + _type: msg_type.to_string(), + sourceName: "".to_string(), + lineText: "".to_string(), + lineNumber: 0, + columnNumber: 0, + category: "".to_string(), + warning: false, + error: true, + exception: false, + strict: false, + private: false, + timeStamp: 0, + errorMessage: "page error test".to_string(), + }; + messages.push(json::from_str(json::encode(&message).as_slice()).unwrap().as_object().unwrap().clone()); + } + "LogMessage" => { + let message = LogMessage { + _type: msg_type.to_string(), + timeStamp: 0, + message: "log message test".to_string(), + }; + messages.push(json::from_str(json::encode(&message).as_slice()).unwrap().as_object().unwrap().clone()); + } + s => println!("unrecognized message type requested: \"{:s}\"", s), + } + } + let msg = GetCachedMessagesReply { + from: self.name(), + messages: messages, + }; + stream.write_json_packet(&msg); + true + } + "stopListeners" => { + let msg = StopListenersReply { + from: self.name(), + stoppedListeners: msg.find(&"listeners".to_string()) + .unwrap() + .as_list() + .unwrap_or(&vec!()) + .iter() + .map(|listener| listener.as_string().unwrap().to_string()) + .collect(), + }; + stream.write_json_packet(&msg); + true + } + "autocomplete" => { + let msg = AutocompleteReply { + from: self.name(), + matches: vec!(), + matchProp: "".to_string(), + }; + stream.write_json_packet(&msg); + true + } + "evaluateJS" => { + let msg = EvaluateJSReply { + from: self.name(), + input: msg.find(&"text".to_string()).unwrap().as_string().unwrap().to_string(), + result: json::Object(TreeMap::new()), + timestamp: 0, + exception: json::Object(TreeMap::new()), + exceptionMessage: "".to_string(), + helperResult: json::Object(TreeMap::new()), + }; + stream.write_json_packet(&msg); + true + } + _ => false + } + } +} + +impl TabActor { + fn encodable(&self) -> TabActorMsg { + TabActorMsg { + actor: self.name(), + title: self.title.clone(), + url: self.url.clone(), + outerWindowID: 0, + consoleActor: self.name(), + } + } +} + +trait JsonPacketSender { + fn write_json_packet<'a, T: Encodable,IoError>>(&mut self, obj: &T); +} + +impl JsonPacketSender for TcpStream { + fn write_json_packet<'a, T: Encodable,IoError>>(&mut self, obj: &T) { + let s = json::encode(obj).replace("__type__", "type"); + println!("<- {:s}", s); + self.write_str(s.len().to_string().as_slice()).unwrap(); + self.write_u8(':' as u8).unwrap(); + self.write_str(s.as_slice()).unwrap(); + } +} + +pub fn start_server() -> Sender { + let (chan, port) = comm::channel(); + TaskBuilder::new().named("devtools").spawn(proc() { + run_server(port) + }); + chan +} + +static POLL_TIMEOUT: u64 = 300; + +fn run_server(port: Receiver) { + let listener = TcpListener::bind("127.0.0.1", 6000); + + // bind the listener to the specified address + let mut acceptor = listener.listen().unwrap(); + acceptor.set_timeout(Some(POLL_TIMEOUT)); + + let mut registry = ActorRegistry::new(); + + let tab = box TabActor { + name: "tab1".to_string(), + title: "Performing Layout".to_string(), + url: "about-mozilla.html".to_string(), + }; + + let root = box RootActor { + tabs: vec!(tab.name().to_string()), + }; + + registry.register(tab); + registry.register(root); + + let actors = Arc::new(Mutex::new(registry)); + + fn handle_client(actors: Arc>, mut stream: TcpStream) { + println!("connection established to {:?}", stream.peer_name().unwrap()); + + { + let mut actors = actors.lock(); + let msg = actors.find::("root").encodable(); + stream.write_json_packet(&msg); + } + + 'outer: loop { + let mut buffer = vec!(); + loop { + let colon = ':' as u8; + match stream.read_byte() { + Ok(c) if c != colon => buffer.push(c as u8), + Ok(_) => { + let packet_len_str = String::from_utf8(buffer).unwrap(); + let packet_len = num::from_str_radix(packet_len_str.as_slice(), 10).unwrap(); + let packet_buf = stream.read_exact(packet_len).unwrap(); + let packet = String::from_utf8(packet_buf).unwrap(); + println!("{:s}", packet); + let json_packet = json::from_str(packet.as_slice()).unwrap(); + actors.lock().handle_message(json_packet.as_object().unwrap(), + &mut stream); + break; + } + Err(ref e) if e.kind == EndOfFile => { + println!("\nEOF"); + break 'outer; + }, + _ => { + println!("\nconnection error"); + break 'outer; + } + } + } + } + } + + // accept connections and process them, spawning a new tasks for each one + for stream in acceptor.incoming() { + match stream { + Err(ref e) if e.kind == TimedOut => { + match port.try_recv() { + Ok(ServerExitMsg) | Err(Disconnected) => break, + Ok(NewGlobal(_)) => { /*TODO*/ }, + Err(Empty) => acceptor.set_timeout(Some(POLL_TIMEOUT)), + } + } + Err(_e) => { /* connection failed */ } + Ok(stream) => { + let actors = actors.clone(); + spawn(proc() { + // connection succeeded + handle_client(actors, stream.clone()) + }) + } + } + } +} diff --git a/components/devtools_traits/Cargo.toml b/components/devtools_traits/Cargo.toml new file mode 100644 index 00000000000..ee897936dbb --- /dev/null +++ b/components/devtools_traits/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "devtools_traits" +version = "0.0.1" +authors = ["The Servo Project Developers"] + +[lib] +name = "devtools_traits" +path = "lib.rs" diff --git a/components/devtools_traits/lib.rs b/components/devtools_traits/lib.rs new file mode 100644 index 00000000000..823acd0ab8f --- /dev/null +++ b/components/devtools_traits/lib.rs @@ -0,0 +1,33 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#![crate_name = "devtools_traits"] +#![crate_type = "rlib"] + +#![comment = "The Servo Parallel Browser Project"] +#![license = "MPL"] + +pub type DevtoolsControlChan = Sender; +pub type DevtoolsControlPort = Receiver; + +pub enum DevtoolsControlMsg { + NewGlobal(Sender), + ServerExitMsg +} + +pub enum EvaluateJSReply { + VoidValue, + NullValue, + NumberValue(f64), + StringValue(String), + ActorValue(String), +} + +pub enum DevtoolScriptControlMsg { + EvaluateJS(String, Sender), +} + +pub enum ScriptDevtoolControlMsg { + ReportConsoleMsg(String), +} diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index 1748a57956d..8bf75b57b20 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -24,6 +24,9 @@ path = "../net" [dependencies.script_traits] path = "../script_traits" +[dependencies.devtools_traits] +path = "../devtools_traits" + [dependencies.style] path = "../style" diff --git a/components/script/lib.rs b/components/script/lib.rs index 9f3effaa368..d7c13cc06a1 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -16,6 +16,7 @@ extern crate log; extern crate debug; +extern crate devtools_traits; extern crate cssparser; extern crate collections; extern crate geom; diff --git a/components/script/script_task.rs b/components/script/script_task.rs index a1f2cd27b22..20f5eb3ded5 100644 --- a/components/script/script_task.rs +++ b/components/script/script_task.rs @@ -31,6 +31,8 @@ use layout_interface::ContentChangedDocumentDamage; use layout_interface; use page::{Page, IterablePage, Frame}; +use devtools_traits::{DevtoolsControlChan, DevtoolsControlPort, NewGlobal}; +use devtools_traits::{DevtoolScriptControlMsg, EvaluateJS}; use script_traits::{CompositorEvent, ResizeEvent, ReflowEvent, ClickEvent, MouseDownEvent}; use script_traits::{MouseMoveEvent, MouseUpEvent, ConstellationControlMsg, ScriptTaskFactory}; use script_traits::{ResizeMsg, AttachLayoutMsg, LoadMsg, SendEventMsg, ResizeInactiveMsg}; @@ -157,6 +159,12 @@ pub struct ScriptTask { /// A handle to the compositor for communicating ready state messages. compositor: Box, + /// For providing instructions to an optional devtools server. + _devtools_chan: Option, + /// For receiving commands from an optional devtools server. Will be ignored if + /// no such server exists. + devtools_port: DevtoolsControlPort, + /// The JavaScript runtime. js_runtime: js::rust::rt, /// The JSContext. @@ -240,6 +248,7 @@ impl ScriptTaskFactory for ScriptTask { failure_msg: Failure, resource_task: ResourceTask, image_cache_task: ImageCacheTask, + devtools_chan: Option, window_size: WindowSizeData) { let ConstellationChan(const_chan) = constellation_chan.clone(); let (script_chan, script_port) = channel(); @@ -255,6 +264,7 @@ impl ScriptTaskFactory for ScriptTask { constellation_chan, resource_task, image_cache_task, + devtools_chan, window_size); let mut failsafe = ScriptMemoryFailsafe::new(&*script_task); script_task.start(); @@ -277,6 +287,7 @@ impl ScriptTask { constellation_chan: ConstellationChan, resource_task: ResourceTask, img_cache_task: ImageCacheTask, + devtools_chan: Option, window_size: WindowSizeData) -> Rc { let (js_runtime, js_context) = ScriptTask::new_rt_and_cx(); @@ -299,6 +310,14 @@ impl ScriptTask { resource_task.clone(), constellation_chan.clone(), js_context.clone()); + + // Notify devtools that a new script global exists. + //FIXME: Move this into handle_load after we create a window instead. + let (devtools_sender, devtools_receiver) = channel(); + devtools_chan.as_ref().map(|chan| { + chan.send(NewGlobal(devtools_sender.clone())); + }); + Rc::new(ScriptTask { page: RefCell::new(Rc::new(page)), @@ -311,6 +330,8 @@ impl ScriptTask { control_port: control_port, constellation_chan: constellation_chan, compositor: compositor, + _devtools_chan: devtools_chan, + devtools_port: devtools_receiver, js_runtime: js_runtime, js_context: RefCell::new(Some(js_context)), @@ -392,6 +413,7 @@ impl ScriptTask { enum MixedMessage { FromConstellation(ConstellationControlMsg), FromScript(ScriptMsg), + FromDevtools(DevtoolScriptControlMsg), } // Store new resizes, and gather all other events. @@ -402,20 +424,25 @@ impl ScriptTask { let sel = Select::new(); let mut port1 = sel.handle(&self.port); let mut port2 = sel.handle(&self.control_port); + let mut port3 = sel.handle(&self.devtools_port); unsafe { port1.add(); port2.add(); + port3.add(); } let ret = sel.wait(); if ret == port1.id() { FromScript(self.port.recv()) } else if ret == port2.id() { FromConstellation(self.control_port.recv()) + } else if ret == port3.id() { + FromDevtools(self.devtools_port.recv()) } else { fail!("unexpected select result") } }; + // Squash any pending resize events in the queue. loop { match event { // This has to be handled before the ResizeMsg below, @@ -434,9 +461,15 @@ impl ScriptTask { } } + // If any of our input sources has an event pending, we'll perform another iteration + // and check for more resize events. If there are no events pending, we'll move + // on and execute the sequential non-resize events we've seen. match self.control_port.try_recv() { Err(_) => match self.port.try_recv() { - Err(_) => break, + Err(_) => match self.devtools_port.try_recv() { + Err(_) => break, + Ok(ev) => event = FromDevtools(ev), + }, Ok(ev) => event = FromScript(ev), }, Ok(ev) => event = FromConstellation(ev), @@ -463,6 +496,7 @@ impl ScriptTask { FromScript(DOMMessage(..)) => fail!("unexpected message"), FromScript(WorkerPostMessage(addr, data, nbytes)) => Worker::handle_message(addr, data, nbytes), FromScript(WorkerRelease(addr)) => Worker::handle_release(addr), + FromDevtools(EvaluateJS(_s, _reply)) => {/*TODO*/} } } diff --git a/components/script_traits/Cargo.toml b/components/script_traits/Cargo.toml index 3bc1beda99f..4183b3909de 100644 --- a/components/script_traits/Cargo.toml +++ b/components/script_traits/Cargo.toml @@ -13,6 +13,9 @@ path = "../msg" [dependencies.net] path = "../net" +[dependencies.devtools_traits] +path = "../devtools_traits" + [dependencies.geom] git = "https://github.com/servo/rust-geom" diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs index 4cead079ff1..fa85bb7c1bd 100644 --- a/components/script_traits/lib.rs +++ b/components/script_traits/lib.rs @@ -7,6 +7,7 @@ #![deny(unused_imports, unused_variable)] +extern crate devtools_traits; extern crate geom; extern crate servo_msg = "msg"; extern crate servo_net = "net"; @@ -16,9 +17,10 @@ extern crate serialize; // This module contains traits in script used generically // in the rest of Servo. -// The traits are here instead of in layout so +// The traits are here instead of in script so // that these modules won't have to depend on script. +use devtools_traits::DevtoolsControlChan; use servo_msg::constellation_msg::{ConstellationChan, PipelineId, Failure, WindowSizeData}; use servo_msg::constellation_msg::SubpageId; use servo_msg::compositor_msg::ScriptListener; @@ -91,6 +93,7 @@ pub trait ScriptTaskFactory { failure_msg: Failure, resource_task: ResourceTask, image_cache_task: ImageCacheTask, + devtools_chan: Option, window_size: WindowSizeData); fn create_layout_channel(_phantom: Option<&mut Self>) -> OpaqueScriptLayoutChannel; fn clone_layout_channel(_phantom: Option<&mut Self>, pair: &OpaqueScriptLayoutChannel) -> Box; diff --git a/components/util/opts.rs b/components/util/opts.rs index 40da68632d8..2145e83af81 100644 --- a/components/util/opts.rs +++ b/components/util/opts.rs @@ -83,6 +83,9 @@ pub struct Opts { /// for debugging purposes. Settings this implies sequential layout /// and render. pub trace_layout: bool, + + /// True if we should start a server to listen to remote Firefox devtools connections. + pub devtools_server: bool, } fn print_usage(app: &str, opts: &[getopts::OptGroup]) { @@ -117,6 +120,7 @@ pub fn from_cmdline_args(args: &[String]) -> Option { getopts::optflag("", "show-debug-borders", "Show debugging borders on layers and tiles."), getopts::optflag("", "disable-text-aa", "Disable antialiasing for text rendering."), getopts::optflag("", "trace-layout", "Write layout trace to external file for debugging."), + getopts::optflag("", "devtools", "Start remote devtools server"), getopts::optflag("h", "help", "Print this message") ); @@ -217,6 +221,7 @@ pub fn from_cmdline_args(args: &[String]) -> Option { show_debug_borders: opt_match.opt_present("show-debug-borders"), enable_text_antialiasing: !opt_match.opt_present("disable-text-aa"), trace_layout: trace_layout, + devtools_server: opt_match.opt_present("devtools"), }) } diff --git a/ports/cef/Cargo.toml b/ports/cef/Cargo.toml index d88ceb24405..5063307fe3f 100644 --- a/ports/cef/Cargo.toml +++ b/ports/cef/Cargo.toml @@ -32,6 +32,9 @@ path = "../../components/util" [dependencies.style] path = "../../components/style" +[dependencies.devtools] +path = "../../components/devtools" + [dependencies.azure] git = "https://github.com/servo/rust-azure" diff --git a/ports/cef/core.rs b/ports/cef/core.rs index 9ff237f2d82..b6415d27f12 100644 --- a/ports/cef/core.rs +++ b/ports/cef/core.rs @@ -67,6 +67,7 @@ pub extern "C" fn cef_run_message_loop() { show_debug_borders: false, enable_text_antialiasing: true, trace_layout: false, + devtools_server: false, }; native::start(0, 0 as *const *const u8, proc() { servo::run(opts); diff --git a/src/lib.rs b/src/lib.rs index a00d935ade3..8a8fb3217e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ extern crate log; extern crate debug; extern crate compositing; +extern crate devtools; extern crate rustuv; extern crate servo_net = "net"; extern crate servo_msg = "msg"; @@ -94,6 +95,11 @@ pub fn run(opts: opts::Opts) { let (compositor_port, compositor_chan) = CompositorChan::new(); let time_profiler_chan = TimeProfiler::create(opts.time_profiler_period); let memory_profiler_chan = MemoryProfiler::create(opts.memory_profiler_period); + let devtools_chan = if opts.devtools_server { + Some(devtools::start_server()) + } else { + None + }; let opts_clone = opts.clone(); let time_profiler_chan_clone = time_profiler_chan.clone(); @@ -121,7 +127,8 @@ pub fn run(opts: opts::Opts) { resource_task, image_cache_task, font_cache_task, - time_profiler_chan_clone); + time_profiler_chan_clone, + devtools_chan); // Send the URL command to the constellation. let cwd = os::getcwd(); From cdb4037ca22409af50847321ce86653073df39e6 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Fri, 29 Aug 2014 16:17:14 -0400 Subject: [PATCH 2/7] Enable executing JS snippets in the context of the main Servo window and viewing the responses from the Firefox remote console. --- Cargo.lock | 5 +- Cargo.toml | 3 - components/devtools/Cargo.toml | 4 +- components/devtools/lib.rs | 265 +++++++++++++++++++++----- components/devtools_traits/Cargo.toml | 3 + components/devtools_traits/lib.rs | 9 +- components/script/script_task.rs | 31 ++- 7 files changed, 262 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43c64bcd7e2..0252ff2c591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,7 +3,6 @@ name = "servo" version = "0.0.1" dependencies = [ "compositing 0.0.1", - "devtools 0.0.1", "gfx 0.0.1", "layout 0.0.1", "msg 0.0.1", @@ -113,11 +112,15 @@ name = "devtools" version = "0.0.1" dependencies = [ "devtools_traits 0.0.1", + "msg 0.0.1", ] [[package]] name = "devtools_traits" version = "0.0.1" +dependencies = [ + "msg 0.0.1", +] [[package]] name = "egl" diff --git a/Cargo.toml b/Cargo.toml index 0561ad480f0..2472f99be99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,8 +46,5 @@ path = "components/layout" [dependencies.gfx] path = "components/gfx" -[dependencies.devtools] -path = "components/devtools" - [dependencies.url] git = "https://github.com/servo/rust-url" diff --git a/components/devtools/Cargo.toml b/components/devtools/Cargo.toml index d27e6bfed5a..2984f9daf61 100644 --- a/components/devtools/Cargo.toml +++ b/components/devtools/Cargo.toml @@ -6,7 +6,9 @@ authors = ["The Servo Project Developers"] [lib] name = "devtools" path = "lib.rs" -crate-type = ["dylib"] [dependencies.devtools_traits] path = "../devtools_traits" + +[dependencies.msg] +path = "../msg" diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index 991ddeb5376..d00544026e9 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -21,19 +21,26 @@ extern crate debug; extern crate std; extern crate serialize; extern crate sync; +extern crate servo_msg = "msg"; -use devtools_traits::{ServerExitMsg, DevtoolsControlMsg, NewGlobal}; +use devtools_traits::{ServerExitMsg, DevtoolsControlMsg, NewGlobal, DevtoolScriptControlMsg}; +use devtools_traits::{EvaluateJS, NullValue, VoidValue, NumberValue, StringValue, BooleanValue}; +use devtools_traits::ActorValue; +use servo_msg::constellation_msg::PipelineId; use collections::TreeMap; -use std::any::{Any, AnyRefExt}; +use std::any::{Any, AnyRefExt, AnyMutRefExt}; use std::collections::hashmap::HashMap; use std::comm; use std::comm::{Disconnected, Empty}; use std::io::{TcpListener, TcpStream}; use std::io::{Acceptor, Listener, EndOfFile, IoError, TimedOut}; +use std::mem::{transmute, transmute_copy}; use std::num; +use std::raw::TraitObject; use std::task::TaskBuilder; use serialize::{json, Encodable}; +use serialize::json::ToJson; use sync::{Arc, Mutex}; #[deriving(Encodable)] @@ -49,6 +56,7 @@ struct RootActorMsg { } struct RootActor { + next: u32, tabs: Vec, } @@ -74,6 +82,12 @@ struct TabActor { url: String, } +struct ConsoleActor { + name: String, + pipeline: PipelineId, + script_chan: Sender, +} + #[deriving(Encodable)] struct ListTabsReply { from: String, @@ -82,16 +96,14 @@ struct ListTabsReply { } #[deriving(Encodable)] -struct TabTraits { - reconfigure: bool, -} +struct TabTraits; #[deriving(Encodable)] struct TabAttachedReply { from: String, __type__: String, threadActor: String, - cacheEnabled: bool, + cacheDisabled: bool, javascriptEnabled: bool, traits: TabTraits, } @@ -102,11 +114,18 @@ struct TabDetachedReply { __type__: String, } + +#[deriving(Encodable)] +struct StartedListenersTraits { + customNetworkRequest: bool, +} + #[deriving(Encodable)] struct StartedListenersReply { from: String, nativeConsoleAPI: bool, startedListeners: Vec, + traits: StartedListenersTraits, } #[deriving(Encodable)] @@ -186,13 +205,24 @@ impl ActorRegistry { } } - fn register(&mut self, actor: Box) { + fn register(&mut self, actor: Box) { + /*{ + let actor2: &Actor+Send+Sized = actor; + assert!((actor2 as &Any).is::()); + };*/ self.actors.insert(actor.name().to_string(), actor); } fn find<'a, T: 'static>(&'a self, name: &str) -> &'a T { - let actor: &Actor+Send+Sized = *self.actors.find(&name.to_string()).unwrap(); - (actor as &Any).downcast_ref::().unwrap() + /*let actor: &Actor+Send+Sized = *self.actors.find(&name.to_string()).unwrap(); + (actor as &Any).downcast_ref::().unwrap()*/ + self.actors.find(&name.to_string()).unwrap().as_ref::().unwrap() + } + + fn find_mut<'a, T: 'static>(&'a mut self, name: &str) -> &'a mut T { + /*let actor: &mut Actor+Send+Sized = *self.actors.find_mut(&name.to_string()).unwrap(); + (actor as &mut Any).downcast_mut::().unwrap()*/ + self.actors.find_mut(&name.to_string()).unwrap().downcast_mut::().unwrap() } fn handle_message(&self, msg: &json::Object, stream: &mut TcpStream) { @@ -220,6 +250,45 @@ trait Actor: Any { fn name(&self) -> String; } +impl<'a> AnyMutRefExt<'a> for &'a mut Actor { + fn downcast_mut(self) -> Option<&'a mut T> { + if self.is::() { + unsafe { + // Get the raw representation of the trait object + let to: TraitObject = transmute_copy(&self); + + // Extract the data pointer + Some(transmute(to.data)) + } + } else { + None + } + } +} + +impl<'a> AnyRefExt<'a> for &'a Actor { + fn is(self) -> bool { + /*let t = TypeId::of::(); + let boxed = self.get_type_id(); + t == boxed*/ + true + } + + fn downcast_ref(self) -> Option<&'a T> { + if self.is::() { + unsafe { + // Get the raw representation of the trait object + let to: TraitObject = transmute_copy(&self); + + // Extract the data pointer + Some(transmute(to.data)) + } + } else { + None + } + } +} + impl Actor for RootActor { fn name(&self) -> String { "root".to_string() @@ -268,6 +337,11 @@ impl RootActor { } } +#[deriving(Encodable)] +struct ReconfigureReply { + from: String +} + impl Actor for TabActor { fn name(&self) -> String { self.name.clone() @@ -276,19 +350,21 @@ impl Actor for TabActor { fn handle_message(&self, _registry: &ActorRegistry, msg_type: &String, - msg: &json::Object, + _msg: &json::Object, stream: &mut TcpStream) -> bool { match msg_type.as_slice() { + "reconfigure" => { + stream.write_json_packet(&ReconfigureReply { from: self.name() }); + true + } "attach" => { let msg = TabAttachedReply { from: self.name(), __type__: "tabAttached".to_string(), threadActor: self.name(), - cacheEnabled: false, + cacheDisabled: false, javascriptEnabled: true, - traits: TabTraits { - reconfigure: true, - }, + traits: TabTraits, }; stream.write_json_packet(&msg); true @@ -301,16 +377,34 @@ impl Actor for TabActor { stream.write_json_packet(&msg); true } - "startListeners" => { - let msg = StartedListenersReply { - from: self.name(), - nativeConsoleAPI: true, - startedListeners: - vec!("PageError".to_string(), "ConsoleAPI".to_string()), - }; - stream.write_json_packet(&msg); - true - } + _ => false + } + } +} + +impl TabActor { + fn encodable(&self) -> TabActorMsg { + TabActorMsg { + actor: self.name(), + title: self.title.clone(), + url: self.url.clone(), + outerWindowID: 0, + consoleActor: "console0".to_string(), + } + } +} + +impl Actor for ConsoleActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + _registry: &ActorRegistry, + msg_type: &String, + msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { "getCachedMessages" => { let types = msg.find(&"messageTypes".to_string()).unwrap().as_list().unwrap(); let mut messages = vec!(); @@ -356,6 +450,20 @@ impl Actor for TabActor { stream.write_json_packet(&msg); true } + "startListeners" => { + let msg = StartedListenersReply { + from: self.name(), + nativeConsoleAPI: true, + startedListeners: + vec!("PageError".to_string(), "ConsoleAPI".to_string(), + "NetworkActivity".to_string(), "FileActivity".to_string()), + traits: StartedListenersTraits { + customNetworkRequest: true, + } + }; + stream.write_json_packet(&msg); + true + } "stopListeners" => { let msg = StopListenersReply { from: self.name(), @@ -380,10 +488,60 @@ impl Actor for TabActor { true } "evaluateJS" => { + let input = msg.find(&"text".to_string()).unwrap().as_string().unwrap().to_string(); + let (chan, port) = channel(); + self.script_chan.send(EvaluateJS(self.pipeline, input.clone(), chan)); + + let result = match port.recv() { + VoidValue => { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "undefined".to_string().to_json()); + json::Object(m) + } + NullValue => { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "null".to_string().to_json()); + json::Object(m) + } + BooleanValue(val) => val.to_json(), + NumberValue(val) => { + if val.is_nan() { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "NaN".to_string().to_json()); + json::Object(m) + } else if val.is_infinite() { + let mut m = TreeMap::new(); + if val < 0. { + m.insert("type".to_string(), "Infinity".to_string().to_json()); + } else { + m.insert("type".to_string(), "-Infinity".to_string().to_json()); + } + json::Object(m) + } else if val == Float::neg_zero() { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "-0".to_string().to_json()); + json::Object(m) + } else { + val.to_json() + } + } + StringValue(s) => s.to_json(), + ActorValue(s) => { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "object".to_string().to_json()); + m.insert("class".to_string(), "???".to_string().to_json()); + m.insert("actor".to_string(), s.to_json()); + m.insert("extensible".to_string(), true.to_json()); + m.insert("frozen".to_string(), false.to_json()); + m.insert("sealed".to_string(), false.to_json()); + json::Object(m) + } + }; + let msg = EvaluateJSReply { from: self.name(), - input: msg.find(&"text".to_string()).unwrap().as_string().unwrap().to_string(), - result: json::Object(TreeMap::new()), + input: input, + result: result, timestamp: 0, exception: json::Object(TreeMap::new()), exceptionMessage: "".to_string(), @@ -397,18 +555,6 @@ impl Actor for TabActor { } } -impl TabActor { - fn encodable(&self) -> TabActorMsg { - TabActorMsg { - actor: self.name(), - title: self.title.clone(), - url: self.url.clone(), - outerWindowID: 0, - consoleActor: self.name(), - } - } -} - trait JsonPacketSender { fn write_json_packet<'a, T: Encodable,IoError>>(&mut self, obj: &T); } @@ -442,18 +588,13 @@ fn run_server(port: Receiver) { let mut registry = ActorRegistry::new(); - let tab = box TabActor { - name: "tab1".to_string(), - title: "Performing Layout".to_string(), - url: "about-mozilla.html".to_string(), - }; - let root = box RootActor { - tabs: vec!(tab.name().to_string()), + next: 0, + tabs: vec!(), }; - registry.register(tab); - registry.register(root); + registry.register::(root); + registry.find::("root"); let actors = Arc::new(Mutex::new(registry)); @@ -496,13 +637,41 @@ fn run_server(port: Receiver) { } } + fn handle_new_global(actors: Arc>, + pipeline: PipelineId, + sender: Sender) { + { + let mut actors = actors.lock(); + + let (tab, console) = { + let root = actors.find_mut::("root"); + + let tab = TabActor { + name: format!("tab{}", root.next), + title: "".to_string(), + url: "about:blank".to_string(), + }; + let console = ConsoleActor { + name: format!("console{}", root.next), + script_chan: sender, + pipeline: pipeline, + }; + root.next += 1; + root.tabs.push(tab.name.clone()); + (tab, console) + }; + actors.register::(box tab); + actors.register::(box console); + } + } + // accept connections and process them, spawning a new tasks for each one for stream in acceptor.incoming() { match stream { Err(ref e) if e.kind == TimedOut => { match port.try_recv() { Ok(ServerExitMsg) | Err(Disconnected) => break, - Ok(NewGlobal(_)) => { /*TODO*/ }, + Ok(NewGlobal(id, sender)) => handle_new_global(actors.clone(), id, sender), Err(Empty) => acceptor.set_timeout(Some(POLL_TIMEOUT)), } } diff --git a/components/devtools_traits/Cargo.toml b/components/devtools_traits/Cargo.toml index ee897936dbb..c3ea63b06fa 100644 --- a/components/devtools_traits/Cargo.toml +++ b/components/devtools_traits/Cargo.toml @@ -6,3 +6,6 @@ authors = ["The Servo Project Developers"] [lib] name = "devtools_traits" path = "lib.rs" + +[dependencies.msg] +path = "../msg" diff --git a/components/devtools_traits/lib.rs b/components/devtools_traits/lib.rs index 823acd0ab8f..08a061708ee 100644 --- a/components/devtools_traits/lib.rs +++ b/components/devtools_traits/lib.rs @@ -8,24 +8,29 @@ #![comment = "The Servo Parallel Browser Project"] #![license = "MPL"] +extern crate servo_msg = "msg"; + +use servo_msg::constellation_msg::PipelineId; + pub type DevtoolsControlChan = Sender; pub type DevtoolsControlPort = Receiver; pub enum DevtoolsControlMsg { - NewGlobal(Sender), + NewGlobal(PipelineId, Sender), ServerExitMsg } pub enum EvaluateJSReply { VoidValue, NullValue, + BooleanValue(bool), NumberValue(f64), StringValue(String), ActorValue(String), } pub enum DevtoolScriptControlMsg { - EvaluateJS(String, Sender), + EvaluateJS(PipelineId, String, Sender), } pub enum ScriptDevtoolControlMsg { diff --git a/components/script/script_task.rs b/components/script/script_task.rs index 20f5eb3ded5..02fa3d10921 100644 --- a/components/script/script_task.rs +++ b/components/script/script_task.rs @@ -6,6 +6,7 @@ //! and layout tasks. use dom::bindings::codegen::InheritTypes::{EventTargetCast, NodeCast, EventCast}; +use dom::bindings::conversions; use dom::bindings::conversions::{FromJSValConvertible, Empty}; use dom::bindings::global::Window; use dom::bindings::js::{JS, JSRef, RootCollection, Temporary, OptionalSettable}; @@ -31,8 +32,9 @@ use layout_interface::ContentChangedDocumentDamage; use layout_interface; use page::{Page, IterablePage, Frame}; +use devtools_traits; use devtools_traits::{DevtoolsControlChan, DevtoolsControlPort, NewGlobal}; -use devtools_traits::{DevtoolScriptControlMsg, EvaluateJS}; +use devtools_traits::{DevtoolScriptControlMsg, EvaluateJS, EvaluateJSReply}; use script_traits::{CompositorEvent, ResizeEvent, ReflowEvent, ClickEvent, MouseDownEvent}; use script_traits::{MouseMoveEvent, MouseUpEvent, ConstellationControlMsg, ScriptTaskFactory}; use script_traits::{ResizeMsg, AttachLayoutMsg, LoadMsg, SendEventMsg, ResizeInactiveMsg}; @@ -315,7 +317,7 @@ impl ScriptTask { //FIXME: Move this into handle_load after we create a window instead. let (devtools_sender, devtools_receiver) = channel(); devtools_chan.as_ref().map(|chan| { - chan.send(NewGlobal(devtools_sender.clone())); + chan.send(NewGlobal(id, devtools_sender.clone())); }); Rc::new(ScriptTask { @@ -496,13 +498,36 @@ impl ScriptTask { FromScript(DOMMessage(..)) => fail!("unexpected message"), FromScript(WorkerPostMessage(addr, data, nbytes)) => Worker::handle_message(addr, data, nbytes), FromScript(WorkerRelease(addr)) => Worker::handle_release(addr), - FromDevtools(EvaluateJS(_s, _reply)) => {/*TODO*/} + FromDevtools(EvaluateJS(id, s, reply)) => self.handle_evaluate_js(id, s, reply), } } true } + fn handle_evaluate_js(&self, pipeline: PipelineId, eval: String, reply: Sender) { + let page = get_page(&*self.page.borrow(), pipeline); + let frame = page.frame(); + let window = frame.get_ref().window.root(); + let cx = window.get_cx(); + let rval = window.evaluate_js_with_result(eval.as_slice()); + + reply.send(if rval.is_undefined() { + devtools_traits::VoidValue + } else if rval.is_boolean() { + devtools_traits::BooleanValue(rval.to_boolean()) + } else if rval.is_double() { + devtools_traits::NumberValue(FromJSValConvertible::from_jsval(cx, rval, ()).unwrap()) + } else if rval.is_string() { + //FIXME: use jsstring_to_str when jsval grows to_jsstring + devtools_traits::StringValue(FromJSValConvertible::from_jsval(cx, rval, conversions::Default).unwrap()) + } else { + //FIXME: jsvals don't have an is_int32/is_number yet + assert!(rval.is_object_or_null()); + fail!("object values unimplemented") + }); + } + fn handle_new_layout(&self, new_layout_info: NewLayoutInfo) { debug!("Script: new layout: {:?}", new_layout_info); let NewLayoutInfo { From bb9955c28132a0c6b2fa85187f6d4e1566d7db5e Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Mon, 1 Sep 2014 10:08:10 -0400 Subject: [PATCH 3/7] Split devtools implementation into sensible modules. --- components/devtools/actor.rs | 120 +++++ components/devtools/actors/console.rs | 284 ++++++++++++ components/devtools/actors/root.rs | 96 ++++ components/devtools/actors/tab.rs | 108 +++++ components/devtools/lib.rs | 612 +++----------------------- components/devtools/protocol.rs | 22 + components/devtools_traits/lib.rs | 12 + 7 files changed, 699 insertions(+), 555 deletions(-) create mode 100644 components/devtools/actor.rs create mode 100644 components/devtools/actors/console.rs create mode 100644 components/devtools/actors/root.rs create mode 100644 components/devtools/actors/tab.rs create mode 100644 components/devtools/protocol.rs diff --git a/components/devtools/actor.rs b/components/devtools/actor.rs new file mode 100644 index 00000000000..9dd2cb2b401 --- /dev/null +++ b/components/devtools/actor.rs @@ -0,0 +1,120 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/// General actor system infrastructure. + +use std::any::{Any, AnyRefExt, AnyMutRefExt}; +use std::collections::hashmap::HashMap; +use std::io::TcpStream; +use std::mem::{transmute, transmute_copy}; +use std::raw::TraitObject; +use serialize::json; + +/// A common trait for all devtools actors that encompasses an immutable name +/// and the ability to process messages that are directed to particular actors. +/// TODO: ensure the name is immutable +pub trait Actor: Any { + fn handle_message(&self, + registry: &ActorRegistry, + msg_type: &String, + msg: &json::Object, + stream: &mut TcpStream) -> bool; + fn name(&self) -> String; +} + +impl<'a> AnyMutRefExt<'a> for &'a mut Actor { + fn downcast_mut(self) -> Option<&'a mut T> { + if self.is::() { + unsafe { + // Get the raw representation of the trait object + let to: TraitObject = transmute_copy(&self); + + // Extract the data pointer + Some(transmute(to.data)) + } + } else { + None + } + } +} + +impl<'a> AnyRefExt<'a> for &'a Actor { + fn is(self) -> bool { + //FIXME: This implementation is bogus since get_type_id is private now. + // However, this implementation is only needed so long as there's a Rust bug + // that prevents downcast_ref from giving realistic return values, and this is + // ok since we're careful with the types we pull out of the hashmap. + /*let t = TypeId::of::(); + let boxed = self.get_type_id(); + t == boxed*/ + true + } + + fn downcast_ref(self) -> Option<&'a T> { + if self.is::() { + unsafe { + // Get the raw representation of the trait object + let to: TraitObject = transmute_copy(&self); + + // Extract the data pointer + Some(transmute(to.data)) + } + } else { + None + } + } +} + +/// A list of known, owned actors. +pub struct ActorRegistry { + actors: HashMap>, +} + +impl ActorRegistry { + /// Create an empty registry. + pub fn new() -> ActorRegistry { + ActorRegistry { + actors: HashMap::new(), + } + } + + /// Add an actor to the registry of known actors that can receive messages. + pub fn register(&mut self, actor: Box) { + self.actors.insert(actor.name().to_string(), actor); + } + + /// Find an actor by registered name + pub fn find<'a, T: 'static>(&'a self, name: &str) -> &'a T { + //FIXME: Rust bug forces us to implement bogus Any for Actor since downcast_ref currently + // fails for unknown reasons. + /*let actor: &Actor+Send+Sized = *self.actors.find(&name.to_string()).unwrap(); + (actor as &Any).downcast_ref::().unwrap()*/ + self.actors.find(&name.to_string()).unwrap().as_ref::().unwrap() + } + + /// Find an actor by registered name + pub fn find_mut<'a, T: 'static>(&'a mut self, name: &str) -> &'a mut T { + //FIXME: Rust bug forces us to implement bogus Any for Actor since downcast_ref currently + // fails for unknown reasons. + /*let actor: &mut Actor+Send+Sized = *self.actors.find_mut(&name.to_string()).unwrap(); + (actor as &mut Any).downcast_mut::().unwrap()*/ + self.actors.find_mut(&name.to_string()).unwrap().downcast_mut::().unwrap() + } + + /// Attempt to process a message as directed by its `to` property. If the actor is not + /// found or does not indicate that it knew how to process the message, ignore the failure. + pub fn handle_message(&self, msg: &json::Object, stream: &mut TcpStream) { + let to = msg.find(&"to".to_string()).unwrap().as_string().unwrap(); + match self.actors.find(&to.to_string()) { + None => println!("message received for unknown actor \"{:s}\"", to), + Some(actor) => { + let msg_type = msg.find(&"type".to_string()).unwrap().as_string().unwrap(); + if !actor.handle_message(self, &msg_type.to_string(), msg, stream) { + println!("unexpected message type \"{:s}\" found for actor \"{:s}\"", + msg_type, to); + } + } + } + } +} diff --git a/components/devtools/actors/console.rs b/components/devtools/actors/console.rs new file mode 100644 index 00000000000..7ae4cf89676 --- /dev/null +++ b/components/devtools/actors/console.rs @@ -0,0 +1,284 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/// Liberally derived from the [Firefox JS implementation](http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/webconsole.js). +/// Mediates interaction between the remote web console and equivalent functionality (object +/// inspection, JS evaluation, autocompletion) in Servo. + +use actor::{Actor, ActorRegistry}; +use protocol::JsonPacketSender; + +use devtools_traits::{EvaluateJS, NullValue, VoidValue, NumberValue, StringValue, BooleanValue}; +use devtools_traits::{ActorValue, DevtoolScriptControlMsg}; +use servo_msg::constellation_msg::PipelineId; + +use collections::TreeMap; +use serialize::json; +use serialize::json::ToJson; +use std::io::TcpStream; + +#[deriving(Encodable)] +struct StartedListenersTraits { + customNetworkRequest: bool, +} + +#[deriving(Encodable)] +struct StartedListenersReply { + from: String, + nativeConsoleAPI: bool, + startedListeners: Vec, + traits: StartedListenersTraits, +} + +#[deriving(Encodable)] +struct ConsoleAPIMessage { + _type: String, //FIXME: should this be __type__ instead? +} + +#[deriving(Encodable)] +struct PageErrorMessage { + _type: String, //FIXME: should this be __type__ instead? + errorMessage: String, + sourceName: String, + lineText: String, + lineNumber: uint, + columnNumber: uint, + category: String, + timeStamp: uint, + warning: bool, + error: bool, + exception: bool, + strict: bool, + private: bool, +} + +#[deriving(Encodable)] +struct LogMessage { + _type: String, //FIXME: should this be __type__ instead? + timeStamp: uint, + message: String, +} + +#[deriving(Encodable)] +enum ConsoleMessageType { + ConsoleAPIType(ConsoleAPIMessage), + PageErrorType(PageErrorMessage), + LogMessageType(LogMessage), +} + +#[deriving(Encodable)] +struct GetCachedMessagesReply { + from: String, + messages: Vec, +} + +#[deriving(Encodable)] +struct StopListenersReply { + from: String, + stoppedListeners: Vec, +} + +#[deriving(Encodable)] +struct AutocompleteReply { + from: String, + matches: Vec, + matchProp: String, +} + +#[deriving(Encodable)] +struct EvaluateJSReply { + from: String, + input: String, + result: json::Json, + timestamp: uint, + exception: json::Json, + exceptionMessage: String, + helperResult: json::Json, +} + +pub struct ConsoleActor { + pub name: String, + pub pipeline: PipelineId, + pub script_chan: Sender, +} + +impl Actor for ConsoleActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + _registry: &ActorRegistry, + msg_type: &String, + msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "getCachedMessages" => { + let types = msg.find(&"messageTypes".to_string()).unwrap().as_list().unwrap(); + let /*mut*/ messages = vec!(); + for msg_type in types.iter() { + let msg_type = msg_type.as_string().unwrap(); + match msg_type.as_slice() { + "ConsoleAPI" => { + //TODO: figure out all consoleapi properties from FFOX source + } + + "PageError" => { + //TODO: make script error reporter pass all reported errors + // to devtools and cache them for returning here. + + /*let message = PageErrorMessage { + _type: msg_type.to_string(), + sourceName: "".to_string(), + lineText: "".to_string(), + lineNumber: 0, + columnNumber: 0, + category: "".to_string(), + warning: false, + error: true, + exception: false, + strict: false, + private: false, + timeStamp: 0, + errorMessage: "page error test".to_string(), + }; + messages.push(json::from_str(json::encode(&message).as_slice()).unwrap().as_object().unwrap().clone());*/ + } + + "LogMessage" => { + //TODO: figure out when LogMessage is necessary + /*let message = LogMessage { + _type: msg_type.to_string(), + timeStamp: 0, + message: "log message test".to_string(), + }; + messages.push(json::from_str(json::encode(&message).as_slice()).unwrap().as_object().unwrap().clone());*/ + } + + s => println!("unrecognized message type requested: \"{:s}\"", s), + } + } + + let msg = GetCachedMessagesReply { + from: self.name(), + messages: messages, + }; + stream.write_json_packet(&msg); + true + } + + "startListeners" => { + //TODO: actually implement listener filters that support starting/stopping + let msg = StartedListenersReply { + from: self.name(), + nativeConsoleAPI: true, + startedListeners: + vec!("PageError".to_string(), "ConsoleAPI".to_string()), + traits: StartedListenersTraits { + customNetworkRequest: true, + } + }; + stream.write_json_packet(&msg); + true + } + + "stopListeners" => { + //TODO: actually implement listener filters that support starting/stopping + let msg = StopListenersReply { + from: self.name(), + stoppedListeners: msg.find(&"listeners".to_string()) + .unwrap() + .as_list() + .unwrap_or(&vec!()) + .iter() + .map(|listener| listener.as_string().unwrap().to_string()) + .collect(), + }; + stream.write_json_packet(&msg); + true + } + + //TODO: implement autocompletion like onAutocomplete in + // http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/webconsole.js + "autocomplete" => { + let msg = AutocompleteReply { + from: self.name(), + matches: vec!(), + matchProp: "".to_string(), + }; + stream.write_json_packet(&msg); + true + } + + "evaluateJS" => { + let input = msg.find(&"text".to_string()).unwrap().as_string().unwrap().to_string(); + let (chan, port) = channel(); + self.script_chan.send(EvaluateJS(self.pipeline, input.clone(), chan)); + + //TODO: extract conversion into protocol module or some other useful place + let result = match port.recv() { + VoidValue => { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "undefined".to_string().to_json()); + json::Object(m) + } + NullValue => { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "null".to_string().to_json()); + json::Object(m) + } + BooleanValue(val) => val.to_json(), + NumberValue(val) => { + if val.is_nan() { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "NaN".to_string().to_json()); + json::Object(m) + } else if val.is_infinite() { + let mut m = TreeMap::new(); + if val < 0. { + m.insert("type".to_string(), "Infinity".to_string().to_json()); + } else { + m.insert("type".to_string(), "-Infinity".to_string().to_json()); + } + json::Object(m) + } else if val == Float::neg_zero() { + let mut m = TreeMap::new(); + m.insert("type".to_string(), "-0".to_string().to_json()); + json::Object(m) + } else { + val.to_json() + } + } + StringValue(s) => s.to_json(), + ActorValue(s) => { + //TODO: make initial ActorValue message include these properties. + let mut m = TreeMap::new(); + m.insert("type".to_string(), "object".to_string().to_json()); + m.insert("class".to_string(), "???".to_string().to_json()); + m.insert("actor".to_string(), s.to_json()); + m.insert("extensible".to_string(), true.to_json()); + m.insert("frozen".to_string(), false.to_json()); + m.insert("sealed".to_string(), false.to_json()); + json::Object(m) + } + }; + + //TODO: catch and return exception values from JS evaluation + let msg = EvaluateJSReply { + from: self.name(), + input: input, + result: result, + timestamp: 0, + exception: json::Object(TreeMap::new()), + exceptionMessage: "".to_string(), + helperResult: json::Object(TreeMap::new()), + }; + stream.write_json_packet(&msg); + true + } + + _ => false + } + } +} diff --git a/components/devtools/actors/root.rs b/components/devtools/actors/root.rs new file mode 100644 index 00000000000..2e74528ae1f --- /dev/null +++ b/components/devtools/actors/root.rs @@ -0,0 +1,96 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/// 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 (tabs, addons, browser chrome, etc.) + +use actor::{Actor, ActorRegistry}; +use actors::tab::{TabActor, TabActorMsg}; +use protocol::JsonPacketSender; + +use serialize::json; +use std::io::TcpStream; + +#[deriving(Encodable)] +struct ActorTraits { + sources: bool +} + +#[deriving(Encodable)] +struct ErrorReply { + from: String, + error: String, + message: String, +} + +#[deriving(Encodable)] +struct ListTabsReply { + from: String, + selected: uint, + tabs: Vec, +} + +#[deriving(Encodable)] +struct RootActorMsg { + from: String, + applicationType: String, + traits: ActorTraits, +} + +pub struct RootActor { + pub next: u32, + pub tabs: Vec, +} + +impl Actor for RootActor { + fn name(&self) -> String { + "root".to_string() + } + + fn handle_message(&self, + registry: &ActorRegistry, + msg_type: &String, + _msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "listAddons" => { + let actor = ErrorReply { + from: "root".to_string(), + error: "noAddons".to_string(), + message: "This root actor has no browser addons.".to_string(), + }; + stream.write_json_packet(&actor); + true + } + + //https://wiki.mozilla.org/Remote_Debugging_Protocol#Listing_Browser_Tabs + "listTabs" => { + let actor = ListTabsReply { + from: "root".to_string(), + selected: 0, + tabs: self.tabs.iter().map(|tab| { + registry.find::(tab.as_slice()).encodable() + }).collect() + }; + stream.write_json_packet(&actor); + true + } + + _ => false + } + } +} + +impl RootActor { + pub fn encodable(&self) -> RootActorMsg { + RootActorMsg { + from: "root".to_string(), + applicationType: "browser".to_string(), + traits: ActorTraits { + sources: true, + }, + } + } +} diff --git a/components/devtools/actors/tab.rs b/components/devtools/actors/tab.rs new file mode 100644 index 00000000000..407bd32fa37 --- /dev/null +++ b/components/devtools/actors/tab.rs @@ -0,0 +1,108 @@ +/* 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 http://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 tab's contents. +/// Supports dynamic attaching and detaching which control notifications of navigation, etc. + +use actor::{Actor, ActorRegistry}; +use protocol::JsonPacketSender; + +use serialize::json; +use std::io::TcpStream; + +#[deriving(Encodable)] +struct TabTraits; + +#[deriving(Encodable)] +struct TabAttachedReply { + from: String, + __type__: String, + threadActor: String, + cacheDisabled: bool, + javascriptEnabled: bool, + traits: TabTraits, +} + +#[deriving(Encodable)] +struct TabDetachedReply { + from: String, + __type__: String, +} + +#[deriving(Encodable)] +struct ReconfigureReply { + from: String +} + +#[deriving(Encodable)] +pub struct TabActorMsg { + actor: String, + title: String, + url: String, + outerWindowID: uint, + consoleActor: String, +} + +pub struct TabActor { + pub name: String, + pub title: String, + pub url: String, +} + +impl Actor for TabActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + _registry: &ActorRegistry, + msg_type: &String, + _msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "reconfigure" => { + stream.write_json_packet(&ReconfigureReply { from: self.name() }); + true + } + + // https://wiki.mozilla.org/Remote_Debugging_Protocol#Listing_Browser_Tabs + // (see "To attach to a _tabActor_") + "attach" => { + let msg = TabAttachedReply { + from: self.name(), + __type__: "tabAttached".to_string(), + threadActor: self.name(), + cacheDisabled: false, + javascriptEnabled: true, + traits: TabTraits, + }; + stream.write_json_packet(&msg); + true + } + + "detach" => { + let msg = TabDetachedReply { + from: self.name(), + __type__: "detached".to_string(), + }; + stream.write_json_packet(&msg); + true + } + _ => false + } + } +} + +impl TabActor { + pub fn encodable(&self) -> TabActorMsg { + TabActorMsg { + actor: self.name(), + title: self.title.clone(), + url: self.url.clone(), + outerWindowID: 0, //FIXME: this should probably be the pipeline id + consoleActor: "console0".to_string(), //FIXME: this should be the actual actor name + } + } +} diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index d00544026e9..1249060118d 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -14,6 +14,10 @@ #[phase(plugin, link)] extern crate log; +/// 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/). + extern crate collections; extern crate core; extern crate devtools_traits; @@ -23,552 +27,35 @@ extern crate serialize; extern crate sync; extern crate servo_msg = "msg"; +use actor::ActorRegistry; +use actors::console::ConsoleActor; +use actors::root::RootActor; +use actors::tab::TabActor; +use protocol::JsonPacketSender; + use devtools_traits::{ServerExitMsg, DevtoolsControlMsg, NewGlobal, DevtoolScriptControlMsg}; -use devtools_traits::{EvaluateJS, NullValue, VoidValue, NumberValue, StringValue, BooleanValue}; -use devtools_traits::ActorValue; use servo_msg::constellation_msg::PipelineId; -use collections::TreeMap; -use std::any::{Any, AnyRefExt, AnyMutRefExt}; -use std::collections::hashmap::HashMap; use std::comm; use std::comm::{Disconnected, Empty}; use std::io::{TcpListener, TcpStream}; -use std::io::{Acceptor, Listener, EndOfFile, IoError, TimedOut}; -use std::mem::{transmute, transmute_copy}; +use std::io::{Acceptor, Listener, EndOfFile, TimedOut}; use std::num; -use std::raw::TraitObject; use std::task::TaskBuilder; -use serialize::{json, Encodable}; -use serialize::json::ToJson; +use serialize::json; use sync::{Arc, Mutex}; -#[deriving(Encodable)] -struct ActorTraits { - sources: bool -} - -#[deriving(Encodable)] -struct RootActorMsg { - from: String, - applicationType: String, - traits: ActorTraits, -} - -struct RootActor { - next: u32, - tabs: Vec, -} - -#[deriving(Encodable)] -struct ErrorReply { - from: String, - error: String, - message: String, -} - -#[deriving(Encodable)] -struct TabActorMsg { - actor: String, - title: String, - url: String, - outerWindowID: uint, - consoleActor: String, -} - -struct TabActor { - name: String, - title: String, - url: String, -} - -struct ConsoleActor { - name: String, - pipeline: PipelineId, - script_chan: Sender, -} - -#[deriving(Encodable)] -struct ListTabsReply { - from: String, - selected: uint, - tabs: Vec, -} - -#[deriving(Encodable)] -struct TabTraits; - -#[deriving(Encodable)] -struct TabAttachedReply { - from: String, - __type__: String, - threadActor: String, - cacheDisabled: bool, - javascriptEnabled: bool, - traits: TabTraits, -} - -#[deriving(Encodable)] -struct TabDetachedReply { - from: String, - __type__: String, -} - - -#[deriving(Encodable)] -struct StartedListenersTraits { - customNetworkRequest: bool, -} - -#[deriving(Encodable)] -struct StartedListenersReply { - from: String, - nativeConsoleAPI: bool, - startedListeners: Vec, - traits: StartedListenersTraits, -} - -#[deriving(Encodable)] -struct ConsoleAPIMessage { - _type: String, -} - -#[deriving(Encodable)] -struct PageErrorMessage { - _type: String, - errorMessage: String, - sourceName: String, - lineText: String, - lineNumber: uint, - columnNumber: uint, - category: String, - timeStamp: uint, - warning: bool, - error: bool, - exception: bool, - strict: bool, - private: bool, -} - -#[deriving(Encodable)] -struct LogMessage { - _type: String, - timeStamp: uint, - message: String, -} - -#[deriving(Encodable)] -enum ConsoleMessageType { - ConsoleAPIType(ConsoleAPIMessage), - PageErrorType(PageErrorMessage), - LogMessageType(LogMessage), -} - -#[deriving(Encodable)] -struct GetCachedMessagesReply { - from: String, - messages: Vec, -} - -#[deriving(Encodable)] -struct StopListenersReply { - from: String, - stoppedListeners: Vec, -} - -#[deriving(Encodable)] -struct AutocompleteReply { - from: String, - matches: Vec, - matchProp: String, -} - -#[deriving(Encodable)] -struct EvaluateJSReply { - from: String, - input: String, - result: json::Json, - timestamp: uint, - exception: json::Json, - exceptionMessage: String, - helperResult: json::Json, -} - -struct ActorRegistry { - actors: HashMap>, -} - -impl ActorRegistry { - fn new() -> ActorRegistry { - ActorRegistry { - actors: HashMap::new(), - } - } - - fn register(&mut self, actor: Box) { - /*{ - let actor2: &Actor+Send+Sized = actor; - assert!((actor2 as &Any).is::()); - };*/ - self.actors.insert(actor.name().to_string(), actor); - } - - fn find<'a, T: 'static>(&'a self, name: &str) -> &'a T { - /*let actor: &Actor+Send+Sized = *self.actors.find(&name.to_string()).unwrap(); - (actor as &Any).downcast_ref::().unwrap()*/ - self.actors.find(&name.to_string()).unwrap().as_ref::().unwrap() - } - - fn find_mut<'a, T: 'static>(&'a mut self, name: &str) -> &'a mut T { - /*let actor: &mut Actor+Send+Sized = *self.actors.find_mut(&name.to_string()).unwrap(); - (actor as &mut Any).downcast_mut::().unwrap()*/ - self.actors.find_mut(&name.to_string()).unwrap().downcast_mut::().unwrap() - } - - fn handle_message(&self, msg: &json::Object, stream: &mut TcpStream) { - let to = msg.find(&"to".to_string()).unwrap().as_string().unwrap(); - match self.actors.find(&to.to_string()) { - None => println!("message received for unknown actor \"{:s}\"", to), - Some(actor) => { - let msg_type = msg.find(&"type".to_string()).unwrap() - .as_string().unwrap(); - if !actor.handle_message(self, &msg_type.to_string(), msg, stream) { - println!("unexpected message type \"{:s}\" found for actor \"{:s}\"", - msg_type, to); - } - } - } - } -} - -trait Actor: Any { - fn handle_message(&self, - registry: &ActorRegistry, - msg_type: &String, - msg: &json::Object, - stream: &mut TcpStream) -> bool; - fn name(&self) -> String; -} - -impl<'a> AnyMutRefExt<'a> for &'a mut Actor { - fn downcast_mut(self) -> Option<&'a mut T> { - if self.is::() { - unsafe { - // Get the raw representation of the trait object - let to: TraitObject = transmute_copy(&self); - - // Extract the data pointer - Some(transmute(to.data)) - } - } else { - None - } - } -} - -impl<'a> AnyRefExt<'a> for &'a Actor { - fn is(self) -> bool { - /*let t = TypeId::of::(); - let boxed = self.get_type_id(); - t == boxed*/ - true - } - - fn downcast_ref(self) -> Option<&'a T> { - if self.is::() { - unsafe { - // Get the raw representation of the trait object - let to: TraitObject = transmute_copy(&self); - - // Extract the data pointer - Some(transmute(to.data)) - } - } else { - None - } - } -} - -impl Actor for RootActor { - fn name(&self) -> String { - "root".to_string() - } - - fn handle_message(&self, - registry: &ActorRegistry, - msg_type: &String, - _msg: &json::Object, - stream: &mut TcpStream) -> bool { - match msg_type.as_slice() { - "listAddons" => { - let actor = ErrorReply { - from: "root".to_string(), - error: "noAddons".to_string(), - message: "This root actor has no browser addons.".to_string(), - }; - stream.write_json_packet(&actor); - true - } - "listTabs" => { - let actor = ListTabsReply { - from: "root".to_string(), - selected: 0, - tabs: self.tabs.iter().map(|tab| { - registry.find::(tab.as_slice()).encodable() - }).collect() - }; - stream.write_json_packet(&actor); - true - } - _ => false - } - } -} - -impl RootActor { - fn encodable(&self) -> RootActorMsg { - RootActorMsg { - from: "root".to_string(), - applicationType: "browser".to_string(), - traits: ActorTraits { - sources: true, - }, - } - } -} - -#[deriving(Encodable)] -struct ReconfigureReply { - from: String -} - -impl Actor for TabActor { - fn name(&self) -> String { - self.name.clone() - } - - fn handle_message(&self, - _registry: &ActorRegistry, - msg_type: &String, - _msg: &json::Object, - stream: &mut TcpStream) -> bool { - match msg_type.as_slice() { - "reconfigure" => { - stream.write_json_packet(&ReconfigureReply { from: self.name() }); - true - } - "attach" => { - let msg = TabAttachedReply { - from: self.name(), - __type__: "tabAttached".to_string(), - threadActor: self.name(), - cacheDisabled: false, - javascriptEnabled: true, - traits: TabTraits, - }; - stream.write_json_packet(&msg); - true - } - "detach" => { - let msg = TabDetachedReply { - from: self.name(), - __type__: "detached".to_string(), - }; - stream.write_json_packet(&msg); - true - } - _ => false - } - } -} - -impl TabActor { - fn encodable(&self) -> TabActorMsg { - TabActorMsg { - actor: self.name(), - title: self.title.clone(), - url: self.url.clone(), - outerWindowID: 0, - consoleActor: "console0".to_string(), - } - } -} - -impl Actor for ConsoleActor { - fn name(&self) -> String { - self.name.clone() - } - - fn handle_message(&self, - _registry: &ActorRegistry, - msg_type: &String, - msg: &json::Object, - stream: &mut TcpStream) -> bool { - match msg_type.as_slice() { - "getCachedMessages" => { - let types = msg.find(&"messageTypes".to_string()).unwrap().as_list().unwrap(); - let mut messages = vec!(); - for msg_type in types.iter() { - let msg_type = msg_type.as_string().unwrap(); - match msg_type.as_slice() { - "ConsoleAPI" => { - //XXX need more info about consoleapi properties - } - "PageError" => { - let message = PageErrorMessage { - _type: msg_type.to_string(), - sourceName: "".to_string(), - lineText: "".to_string(), - lineNumber: 0, - columnNumber: 0, - category: "".to_string(), - warning: false, - error: true, - exception: false, - strict: false, - private: false, - timeStamp: 0, - errorMessage: "page error test".to_string(), - }; - messages.push(json::from_str(json::encode(&message).as_slice()).unwrap().as_object().unwrap().clone()); - } - "LogMessage" => { - let message = LogMessage { - _type: msg_type.to_string(), - timeStamp: 0, - message: "log message test".to_string(), - }; - messages.push(json::from_str(json::encode(&message).as_slice()).unwrap().as_object().unwrap().clone()); - } - s => println!("unrecognized message type requested: \"{:s}\"", s), - } - } - let msg = GetCachedMessagesReply { - from: self.name(), - messages: messages, - }; - stream.write_json_packet(&msg); - true - } - "startListeners" => { - let msg = StartedListenersReply { - from: self.name(), - nativeConsoleAPI: true, - startedListeners: - vec!("PageError".to_string(), "ConsoleAPI".to_string(), - "NetworkActivity".to_string(), "FileActivity".to_string()), - traits: StartedListenersTraits { - customNetworkRequest: true, - } - }; - stream.write_json_packet(&msg); - true - } - "stopListeners" => { - let msg = StopListenersReply { - from: self.name(), - stoppedListeners: msg.find(&"listeners".to_string()) - .unwrap() - .as_list() - .unwrap_or(&vec!()) - .iter() - .map(|listener| listener.as_string().unwrap().to_string()) - .collect(), - }; - stream.write_json_packet(&msg); - true - } - "autocomplete" => { - let msg = AutocompleteReply { - from: self.name(), - matches: vec!(), - matchProp: "".to_string(), - }; - stream.write_json_packet(&msg); - true - } - "evaluateJS" => { - let input = msg.find(&"text".to_string()).unwrap().as_string().unwrap().to_string(); - let (chan, port) = channel(); - self.script_chan.send(EvaluateJS(self.pipeline, input.clone(), chan)); - - let result = match port.recv() { - VoidValue => { - let mut m = TreeMap::new(); - m.insert("type".to_string(), "undefined".to_string().to_json()); - json::Object(m) - } - NullValue => { - let mut m = TreeMap::new(); - m.insert("type".to_string(), "null".to_string().to_json()); - json::Object(m) - } - BooleanValue(val) => val.to_json(), - NumberValue(val) => { - if val.is_nan() { - let mut m = TreeMap::new(); - m.insert("type".to_string(), "NaN".to_string().to_json()); - json::Object(m) - } else if val.is_infinite() { - let mut m = TreeMap::new(); - if val < 0. { - m.insert("type".to_string(), "Infinity".to_string().to_json()); - } else { - m.insert("type".to_string(), "-Infinity".to_string().to_json()); - } - json::Object(m) - } else if val == Float::neg_zero() { - let mut m = TreeMap::new(); - m.insert("type".to_string(), "-0".to_string().to_json()); - json::Object(m) - } else { - val.to_json() - } - } - StringValue(s) => s.to_json(), - ActorValue(s) => { - let mut m = TreeMap::new(); - m.insert("type".to_string(), "object".to_string().to_json()); - m.insert("class".to_string(), "???".to_string().to_json()); - m.insert("actor".to_string(), s.to_json()); - m.insert("extensible".to_string(), true.to_json()); - m.insert("frozen".to_string(), false.to_json()); - m.insert("sealed".to_string(), false.to_json()); - json::Object(m) - } - }; - - let msg = EvaluateJSReply { - from: self.name(), - input: input, - result: result, - timestamp: 0, - exception: json::Object(TreeMap::new()), - exceptionMessage: "".to_string(), - helperResult: json::Object(TreeMap::new()), - }; - stream.write_json_packet(&msg); - true - } - _ => false - } - } -} - -trait JsonPacketSender { - fn write_json_packet<'a, T: Encodable,IoError>>(&mut self, obj: &T); -} - -impl JsonPacketSender for TcpStream { - fn write_json_packet<'a, T: Encodable,IoError>>(&mut self, obj: &T) { - let s = json::encode(obj).replace("__type__", "type"); - println!("<- {:s}", s); - self.write_str(s.len().to_string().as_slice()).unwrap(); - self.write_u8(':' as u8).unwrap(); - self.write_str(s.as_slice()).unwrap(); - } +mod actor; +/// Corresponds to http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/ +mod actors { + pub mod console; + pub mod tab; + pub mod root; } +mod protocol; +/// Spin up a devtools server that listens for connections. Defaults to port 6000. +/// TODO: allow specifying a port pub fn start_server() -> Sender { let (chan, port) = comm::channel(); TaskBuilder::new().named("devtools").spawn(proc() { @@ -593,11 +80,12 @@ fn run_server(port: Receiver) { tabs: vec!(), }; - registry.register::(root); + registry.register(root); registry.find::("root"); let actors = Arc::new(Mutex::new(registry)); + /// Process the input from a single devtools client until EOF. fn handle_client(actors: Arc>, mut stream: TcpStream) { println!("connection established to {:?}", stream.peer_name().unwrap()); @@ -607,6 +95,9 @@ fn run_server(port: Receiver) { stream.write_json_packet(&msg); } + // https://wiki.mozilla.org/Remote_Debugging_Protocol_Stream_Transport + // In short, each JSON packet is [ascii length]:[JSON data of given length] + // TODO: this really belongs in the protocol module. 'outer: loop { let mut buffer = vec!(); loop { @@ -637,34 +128,45 @@ fn run_server(port: Receiver) { } } + // We need separate actor representations for each script global that exists; + // clients can theoretically connect to multiple globals simultaneously. + // TODO: move this into the root or tab modules? fn handle_new_global(actors: Arc>, pipeline: PipelineId, sender: Sender) { - { - let mut actors = actors.lock(); + let mut actors = actors.lock(); - let (tab, console) = { - let root = actors.find_mut::("root"); + let (tab, console) = { + let root = actors.find_mut::("root"); - let tab = TabActor { - name: format!("tab{}", root.next), - title: "".to_string(), - url: "about:blank".to_string(), - }; - let console = ConsoleActor { - name: format!("console{}", root.next), - script_chan: sender, - pipeline: pipeline, - }; - root.next += 1; - root.tabs.push(tab.name.clone()); - (tab, console) + let tab = TabActor { + name: format!("tab{}", root.next), + title: "".to_string(), + url: "about:blank".to_string(), }; - actors.register::(box tab); - actors.register::(box console); - } + let console = ConsoleActor { + name: format!("console{}", root.next), + script_chan: sender, + pipeline: pipeline, + }; + + root.next += 1; + root.tabs.push(tab.name.clone()); + (tab, console) + }; + + actors.register(box tab); + actors.register(box console); } + //TODO: figure out some system that allows us to watch for new connections, + // shut down existing ones at arbitrary times, and also watch for messages + // from multiple script tasks simultaneously. Polling for new connections + // for 300ms and then checking the receiver is not a good compromise + // (and makes Servo hang on exit if there's an open connection, no less). + + //TODO: make constellation send ServerExitMsg on shutdown. + // accept connections and process them, spawning a new tasks for each one for stream in acceptor.incoming() { match stream { diff --git a/components/devtools/protocol.rs b/components/devtools/protocol.rs new file mode 100644 index 00000000000..728e593040e --- /dev/null +++ b/components/devtools/protocol.rs @@ -0,0 +1,22 @@ +/* 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 http://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). + +use serialize::{json, Encodable}; +use std::io::{IoError, TcpStream}; + +pub trait JsonPacketSender { + fn write_json_packet<'a, T: Encodable,IoError>>(&mut self, obj: &T); +} + +impl JsonPacketSender for TcpStream { + fn write_json_packet<'a, T: Encodable,IoError>>(&mut self, obj: &T) { + let s = json::encode(obj).replace("__type__", "type"); + println!("<- {:s}", s); + self.write_str(s.len().to_string().as_slice()).unwrap(); + self.write_u8(':' as u8).unwrap(); + self.write_str(s.as_slice()).unwrap(); + } +} diff --git a/components/devtools_traits/lib.rs b/components/devtools_traits/lib.rs index 08a061708ee..904f4994fe5 100644 --- a/components/devtools_traits/lib.rs +++ b/components/devtools_traits/lib.rs @@ -10,16 +10,24 @@ extern crate servo_msg = "msg"; +/// This module contains shared types and messages for use by devtools/script. +/// The traits are here instead of in script so that the devtools crate can be +/// modified independently of the rest of Servo. + use servo_msg::constellation_msg::PipelineId; pub type DevtoolsControlChan = Sender; pub type DevtoolsControlPort = Receiver; +/// Messages to the instruct the devtools server to update its known actors/state +/// according to changes in the browser. pub enum DevtoolsControlMsg { NewGlobal(PipelineId, Sender), ServerExitMsg } +/// Serialized JS return values +/// TODO: generalize this beyond the EvaluateJS message? pub enum EvaluateJSReply { VoidValue, NullValue, @@ -29,10 +37,14 @@ pub enum EvaluateJSReply { ActorValue(String), } +/// Messages to process in a particular script task, as instructed by a devtools client. pub enum DevtoolScriptControlMsg { EvaluateJS(PipelineId, String, Sender), } +/// Messages to instruct devtools server to update its state relating to a particular +/// tab. pub enum ScriptDevtoolControlMsg { + /// Report a new JS error message ReportConsoleMsg(String), } From c31e2f928d2b8b2ab870c652a03efec779dff268 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Tue, 2 Sep 2014 22:10:26 -0400 Subject: [PATCH 4/7] Avoid selecting on the devtools port if no devtools server exists. --- components/script/script_task.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/script/script_task.rs b/components/script/script_task.rs index 02fa3d10921..2feb088201c 100644 --- a/components/script/script_task.rs +++ b/components/script/script_task.rs @@ -162,7 +162,7 @@ pub struct ScriptTask { compositor: Box, /// For providing instructions to an optional devtools server. - _devtools_chan: Option, + devtools_chan: Option, /// For receiving commands from an optional devtools server. Will be ignored if /// no such server exists. devtools_port: DevtoolsControlPort, @@ -332,7 +332,7 @@ impl ScriptTask { control_port: control_port, constellation_chan: constellation_chan, compositor: compositor, - _devtools_chan: devtools_chan, + devtools_chan: devtools_chan, devtools_port: devtools_receiver, js_runtime: js_runtime, @@ -430,7 +430,9 @@ impl ScriptTask { unsafe { port1.add(); port2.add(); - port3.add(); + if self.devtools_chan.is_some() { + port3.add(); + } } let ret = sel.wait(); if ret == port1.id() { From e9c4aa534da8bcd4457576b8c10719dd956ed062 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Wed, 3 Sep 2014 01:41:06 -0400 Subject: [PATCH 5/7] Implement wire protocol support for DOM inspector. --- components/devtools/actor.rs | 24 +- components/devtools/actors/inspector.rs | 472 ++++++++++++++++++++++++ components/devtools/actors/root.rs | 7 +- components/devtools/actors/tab.rs | 30 +- components/devtools/lib.rs | 39 +- 5 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 components/devtools/actors/inspector.rs diff --git a/components/devtools/actor.rs b/components/devtools/actor.rs index 9dd2cb2b401..f489e7facf4 100644 --- a/components/devtools/actor.rs +++ b/components/devtools/actor.rs @@ -6,6 +6,7 @@ use std::any::{Any, AnyRefExt, AnyMutRefExt}; use std::collections::hashmap::HashMap; +use std::cell::{Cell, RefCell}; use std::io::TcpStream; use std::mem::{transmute, transmute_copy}; use std::raw::TraitObject; @@ -69,6 +70,8 @@ impl<'a> AnyRefExt<'a> for &'a Actor { /// A list of known, owned actors. pub struct ActorRegistry { actors: HashMap>, + new_actors: RefCell>>, + next: Cell, } impl ActorRegistry { @@ -76,14 +79,28 @@ impl ActorRegistry { pub fn new() -> ActorRegistry { ActorRegistry { actors: HashMap::new(), + new_actors: RefCell::new(vec!()), + next: Cell::new(0), } } + /// Create a unique name based on a monotonically increasing suffix + pub fn new_name(&self, prefix: &str) -> String { + let suffix = self.next.get(); + self.next.set(suffix + 1); + format!("{:s}{:u}", prefix, suffix) + } + /// Add an actor to the registry of known actors that can receive messages. pub fn register(&mut self, actor: Box) { self.actors.insert(actor.name().to_string(), actor); } + pub fn register_later(&self, actor: Box) { + let mut actors = self.new_actors.borrow_mut(); + actors.push(actor); + } + /// Find an actor by registered name pub fn find<'a, T: 'static>(&'a self, name: &str) -> &'a T { //FIXME: Rust bug forces us to implement bogus Any for Actor since downcast_ref currently @@ -104,7 +121,7 @@ impl ActorRegistry { /// Attempt to process a message as directed by its `to` property. If the actor is not /// found or does not indicate that it knew how to process the message, ignore the failure. - pub fn handle_message(&self, msg: &json::Object, stream: &mut TcpStream) { + pub fn handle_message(&mut self, msg: &json::Object, stream: &mut TcpStream) { let to = msg.find(&"to".to_string()).unwrap().as_string().unwrap(); match self.actors.find(&to.to_string()) { None => println!("message received for unknown actor \"{:s}\"", to), @@ -116,5 +133,10 @@ impl ActorRegistry { } } } + let mut new_actors = self.new_actors.borrow_mut(); + for &actor in new_actors.iter() { + self.actors.insert(actor.name().to_string(), actor); + } + new_actors.clear(); } } diff --git a/components/devtools/actors/inspector.rs b/components/devtools/actors/inspector.rs new file mode 100644 index 00000000000..2bf0b0671d3 --- /dev/null +++ b/components/devtools/actors/inspector.rs @@ -0,0 +1,472 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/// Liberally derived from the [Firefox JS implementation](http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/inspector.js). + +use actor::{Actor, ActorRegistry}; +use protocol::JsonPacketSender; + +use serialize::json; +use std::cell::RefCell; +use std::io::TcpStream; + +pub struct InspectorActor { + pub name: String, + pub walker: RefCell>, + pub pageStyle: RefCell>, + pub highlighter: RefCell>, +} + +#[deriving(Encodable)] +struct GetHighlighterReply { + highligter: HighlighterMsg, // sic. + from: String, +} + +#[deriving(Encodable)] +struct HighlighterMsg { + actor: String, +} + +struct HighlighterActor { + name: String, +} + +#[deriving(Encodable)] +struct ShowBoxModelReply { + from: String, +} + +#[deriving(Encodable)] +struct HideBoxModelReply { + from: String, +} + +impl Actor for HighlighterActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + _registry: &ActorRegistry, + msg_type: &String, + _msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "showBoxModel" => { + let msg = ShowBoxModelReply { + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + "hideBoxModel" => { + let msg = HideBoxModelReply { + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + _ => false, + } + } +} + +#[deriving(Encodable)] +struct GetWalkerReply { + from: String, + walker: WalkerMsg, +} + +#[deriving(Encodable)] +struct WalkerMsg { + actor: String, + root: NodeActorMsg, +} + +#[deriving(Encodable)] +struct AttrMsg { + namespace: String, + name: String, + value: String, +} + +#[deriving(Encodable)] +struct NodeActorMsg { + actor: String, + baseURI: String, + parent: String, + nodeType: uint, + namespaceURI: String, + nodeName: String, + numChildren: uint, + + name: String, + publicId: String, + systemId: String, + + attrs: Vec, + + pseudoClassLocks: Vec, + + isDisplayed: bool, + + hasEventListeners: bool, + + isDocumentElement: bool, + + shortValue: String, + incompleteValue: bool, +} + +struct WalkerActor { + name: String, +} + +#[deriving(Encodable)] +struct QuerySelectorReply { + from: String, +} + +#[deriving(Encodable)] +struct DocumentElementReply { + from: String, + node: NodeActorMsg, +} + +#[deriving(Encodable)] +struct ClearPseudoclassesReply { + from: String, +} + +#[deriving(Encodable)] +struct ChildrenReply { + hasFirst: bool, + hasLast: bool, + nodes: Vec, + from: String, +} + +impl Actor for WalkerActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + _registry: &ActorRegistry, + msg_type: &String, + _msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "querySelector" => { + let msg = QuerySelectorReply { + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + "documentElement" => { + let msg = DocumentElementReply { + from: self.name(), + node: NodeActorMsg { + actor: "node0".to_string(), + baseURI: "".to_string(), + parent: "".to_string(), + nodeType: 1, //ELEMENT_NODE + namespaceURI: "".to_string(), + nodeName: "html".to_string(), + numChildren: 0, + + name: "".to_string(), + publicId: "".to_string(), + systemId: "".to_string(), + + attrs: vec!(AttrMsg { + namespace: "".to_string(), + name: "manifest".to_string(), + value: "foo.manifest".to_string(), + }), + + pseudoClassLocks: vec!(), + + isDisplayed: true, + + hasEventListeners: false, + + isDocumentElement: true, + + shortValue: "".to_string(), + incompleteValue: false, + } + }; + stream.write_json_packet(&msg); + true + } + + "clearPseudoClassLocks" => { + let msg = ClearPseudoclassesReply { + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + "children" => { + let msg = ChildrenReply { + hasFirst: true, + hasLast: true, + nodes: vec!(), + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + _ => false, + } + } +} + +struct NodeActor { + name: String, +} + +impl Actor for NodeActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + _registry: &ActorRegistry, + msg_type: &String, + _msg: &json::Object, + _stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + _ => false, + } + } +} + +#[deriving(Encodable)] +struct GetPageStyleReply { + from: String, + pageStyle: PageStyleMsg, +} + +#[deriving(Encodable)] +struct PageStyleMsg { + actor: String, +} + +struct PageStyleActor { + name: String, +} + +#[deriving(Encodable)] +struct GetAppliedReply { + entries: Vec, + rules: Vec, + sheets: Vec, + from: String, +} + +#[deriving(Encodable)] +struct GetComputedReply { + computed: Vec, //XXX all css props + from: String, +} + +#[deriving(Encodable)] +struct AppliedEntry { + rule: String, + pseudoElement: json::Json, + isSystem: bool, + matchedSelectors: Vec, +} + +#[deriving(Encodable)] +struct AppliedRule { + actor: String, + __type__: uint, + href: String, + cssText: String, + line: uint, + column: uint, + parentStyleSheet: String, +} + +#[deriving(Encodable)] +struct AppliedSheet { + actor: String, + href: String, + nodeHref: String, + disabled: bool, + title: String, + system: bool, + styleSheetIndex: int, + ruleCount: uint, +} + +impl Actor for PageStyleActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + _registry: &ActorRegistry, + msg_type: &String, + _msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "getApplied" => { + //TODO: query script for relevant applied styles to node (msg.node) + let msg = GetAppliedReply { + entries: vec!(), + rules: vec!(), + sheets: vec!(), + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + "getComputed" => { + //TODO: query script for relevant computed styles on node (msg.node) + let msg = GetComputedReply { + computed: vec!(), + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + //TODO: query script for box layout properties of node (msg.node) + //"getLayout" => {} + + _ => false, + } + } +} + +impl Actor for InspectorActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message(&self, + registry: &ActorRegistry, + msg_type: &String, + _msg: &json::Object, + stream: &mut TcpStream) -> bool { + match msg_type.as_slice() { + "getWalker" => { + if self.walker.borrow().is_none() { + let walker = WalkerActor { + name: registry.new_name("walker"), + }; + let mut walker_name = self.walker.borrow_mut(); + *walker_name = Some(walker.name()); + registry.register_later(box walker); + } + + let node = NodeActor { + name: registry.new_name("node"), + }; + let node_actor_name = node.name(); + registry.register_later(box node); + + //TODO: query script for actual root node + //TODO: extra node actor creation + let node = NodeActorMsg { + actor: node_actor_name, + baseURI: "".to_string(), + parent: "".to_string(), + nodeType: 1, //ELEMENT_NODE + namespaceURI: "".to_string(), + nodeName: "html".to_string(), + numChildren: 1, + + name: "".to_string(), + publicId: "".to_string(), + systemId: "".to_string(), + + attrs: vec!(AttrMsg { + namespace: "".to_string(), + name: "manifest".to_string(), + value: "foo.manifest".to_string(), + }), + + pseudoClassLocks: vec!(), + + isDisplayed: true, + + hasEventListeners: false, + + isDocumentElement: true, + + shortValue: "".to_string(), + incompleteValue: false, + }; + + let msg = GetWalkerReply { + from: self.name(), + walker: WalkerMsg { + actor: self.walker.borrow().clone().unwrap(), + root: node, + } + }; + stream.write_json_packet(&msg); + true + } + + "getPageStyle" => { + if self.pageStyle.borrow().is_none() { + let style = PageStyleActor { + name: registry.new_name("pageStyle"), + }; + let mut pageStyle = self.pageStyle.borrow_mut(); + *pageStyle = Some(style.name()); + registry.register_later(box style); + } + + let msg = GetPageStyleReply { + from: self.name(), + pageStyle: PageStyleMsg { + actor: self.pageStyle.borrow().clone().unwrap(), + }, + }; + stream.write_json_packet(&msg); + true + } + + //TODO: this is an old message; try adding highlightable to the root traits instead + // and support getHighlighter instead + //"highlight" => {} + "getHighlighter" => { + if self.highlighter.borrow().is_none() { + let highlighter_actor = HighlighterActor { + name: registry.new_name("highlighter"), + }; + let mut highlighter = self.highlighter.borrow_mut(); + *highlighter = Some(highlighter_actor.name()); + registry.register_later(box highlighter_actor); + } + + let msg = GetHighlighterReply { + from: self.name(), + highligter: HighlighterMsg { + actor: self.highlighter.borrow().clone().unwrap(), + }, + }; + stream.write_json_packet(&msg); + true + } + + _ => false, + } + } +} diff --git a/components/devtools/actors/root.rs b/components/devtools/actors/root.rs index 2e74528ae1f..8ebd79e4016 100644 --- a/components/devtools/actors/root.rs +++ b/components/devtools/actors/root.rs @@ -15,7 +15,9 @@ use std::io::TcpStream; #[deriving(Encodable)] struct ActorTraits { - sources: bool + sources: bool, + highlightable: bool, + customHighlighters: Vec, } #[deriving(Encodable)] @@ -40,7 +42,6 @@ struct RootActorMsg { } pub struct RootActor { - pub next: u32, pub tabs: Vec, } @@ -90,6 +91,8 @@ impl RootActor { applicationType: "browser".to_string(), traits: ActorTraits { sources: true, + highlightable: true, + customHighlighters: vec!("BoxModelHighlighter".to_string()), }, } } diff --git a/components/devtools/actors/tab.rs b/components/devtools/actors/tab.rs index 407bd32fa37..c9cc3481264 100644 --- a/components/devtools/actors/tab.rs +++ b/components/devtools/actors/tab.rs @@ -36,6 +36,20 @@ struct ReconfigureReply { from: String } +#[deriving(Encodable)] +struct ListFramesReply { + from: String, + frames: Vec, +} + +#[deriving(Encodable)] +struct FrameMsg { + id: uint, + url: String, + title: String, + parentID: uint, +} + #[deriving(Encodable)] pub struct TabActorMsg { actor: String, @@ -43,12 +57,15 @@ pub struct TabActorMsg { url: String, outerWindowID: uint, consoleActor: String, + inspectorActor: String, } pub struct TabActor { pub name: String, pub title: String, pub url: String, + pub console: String, + pub inspector: String, } impl Actor for TabActor { @@ -90,6 +107,16 @@ impl Actor for TabActor { stream.write_json_packet(&msg); true } + + "listFrames" => { + let msg = ListFramesReply { + from: self.name(), + frames: vec!(), + }; + stream.write_json_packet(&msg); + true + } + _ => false } } @@ -102,7 +129,8 @@ impl TabActor { title: self.title.clone(), url: self.url.clone(), outerWindowID: 0, //FIXME: this should probably be the pipeline id - consoleActor: "console0".to_string(), //FIXME: this should be the actual actor name + consoleActor: self.console.clone(), + inspectorActor: self.inspector.clone(), } } } diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index 1249060118d..5dbce103546 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -27,8 +27,9 @@ extern crate serialize; extern crate sync; extern crate servo_msg = "msg"; -use actor::ActorRegistry; +use actor::{Actor, ActorRegistry}; use actors::console::ConsoleActor; +use actors::inspector::InspectorActor; use actors::root::RootActor; use actors::tab::TabActor; use protocol::JsonPacketSender; @@ -36,6 +37,7 @@ use protocol::JsonPacketSender; use devtools_traits::{ServerExitMsg, DevtoolsControlMsg, NewGlobal, DevtoolScriptControlMsg}; use servo_msg::constellation_msg::PipelineId; +use std::cell::RefCell; use std::comm; use std::comm::{Disconnected, Empty}; use std::io::{TcpListener, TcpStream}; @@ -49,8 +51,9 @@ mod actor; /// Corresponds to http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/ mod actors { pub mod console; - pub mod tab; + pub mod inspector; pub mod root; + pub mod tab; } mod protocol; @@ -76,7 +79,6 @@ fn run_server(port: Receiver) { let mut registry = ActorRegistry::new(); let root = box RootActor { - next: 0, tabs: vec!(), }; @@ -136,27 +138,36 @@ fn run_server(port: Receiver) { sender: Sender) { let mut actors = actors.lock(); - let (tab, console) = { - let root = actors.find_mut::("root"); - - let tab = TabActor { - name: format!("tab{}", root.next), - title: "".to_string(), - url: "about:blank".to_string(), - }; + //TODO: move all this actor creation into a constructor method on TabActor + let (tab, console, inspector) = { let console = ConsoleActor { - name: format!("console{}", root.next), + name: actors.new_name("console"), script_chan: sender, pipeline: pipeline, }; + let inspector = InspectorActor { + name: actors.new_name("inspector"), + walker: RefCell::new(None), + pageStyle: RefCell::new(None), + highlighter: RefCell::new(None), + }; + //TODO: send along the current page title and URL + let tab = TabActor { + name: actors.new_name("tab"), + title: "".to_string(), + url: "about:blank".to_string(), + console: console.name(), + inspector: inspector.name(), + }; - root.next += 1; + let root = actors.find_mut::("root"); root.tabs.push(tab.name.clone()); - (tab, console) + (tab, console, inspector) }; actors.register(box tab); actors.register(box console); + actors.register(box inspector); } //TODO: figure out some system that allows us to watch for new connections, From fa57fe890b20200d1b69ef0a8baf6b313183b237 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Thu, 4 Sep 2014 12:23:48 -0400 Subject: [PATCH 6/7] Enable real DOM inspector support. --- components/devtools/actor.rs | 29 ++++ components/devtools/actors/inspector.rs | 172 +++++++++++------------- components/devtools/lib.rs | 4 +- components/devtools_traits/lib.rs | 30 +++++ components/script/dom/attr.rs | 10 ++ components/script/dom/node.rs | 63 ++++++++- components/script/lib.rs | 1 + components/script/script_task.rs | 50 ++++++- 8 files changed, 265 insertions(+), 94 deletions(-) diff --git a/components/devtools/actor.rs b/components/devtools/actor.rs index f489e7facf4..fae6c8864cf 100644 --- a/components/devtools/actor.rs +++ b/components/devtools/actor.rs @@ -71,6 +71,7 @@ impl<'a> AnyRefExt<'a> for &'a Actor { pub struct ActorRegistry { actors: HashMap>, new_actors: RefCell>>, + script_actors: RefCell>, next: Cell, } @@ -80,10 +81,38 @@ impl ActorRegistry { ActorRegistry { actors: HashMap::new(), new_actors: RefCell::new(vec!()), + script_actors: RefCell::new(HashMap::new()), next: Cell::new(0), } } + pub fn register_script_actor(&self, script_id: String, actor: String) { + println!("registering {:s} ({:s})", actor.as_slice(), script_id.as_slice()); + let mut script_actors = self.script_actors.borrow_mut(); + script_actors.insert(script_id, actor); + } + + pub fn script_to_actor(&self, script_id: String) -> String { + if script_id.as_slice() == "" { + return "".to_string(); + } + self.script_actors.borrow().find(&script_id).unwrap().to_string() + } + + pub fn script_actor_registered(&self, script_id: String) -> bool { + self.script_actors.borrow().contains_key(&script_id) + } + + pub fn actor_to_script(&self, actor: String) -> String { + for (key, value) in self.script_actors.borrow().iter() { + println!("checking {:s}", value.as_slice()); + if value.as_slice() == actor.as_slice() { + return key.to_string(); + } + } + fail!("couldn't find actor named {:s}", actor) + } + /// Create a unique name based on a monotonically increasing suffix pub fn new_name(&self, prefix: &str) -> String { let suffix = self.next.get(); diff --git a/components/devtools/actors/inspector.rs b/components/devtools/actors/inspector.rs index 2bf0b0671d3..ac5608ed63a 100644 --- a/components/devtools/actors/inspector.rs +++ b/components/devtools/actors/inspector.rs @@ -4,9 +4,13 @@ /// Liberally derived from the [Firefox JS implementation](http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/inspector.js). +use devtools_traits::{GetRootNode, GetDocumentElement, GetChildren, DevtoolScriptControlMsg}; +use devtools_traits::NodeInfo; + use actor::{Actor, ActorRegistry}; use protocol::JsonPacketSender; +use servo_msg::constellation_msg::PipelineId; use serialize::json; use std::cell::RefCell; use std::io::TcpStream; @@ -16,6 +20,8 @@ pub struct InspectorActor { pub walker: RefCell>, pub pageStyle: RefCell>, pub highlighter: RefCell>, + pub script_chan: Sender, + pub pipeline: PipelineId, } #[deriving(Encodable)] @@ -122,8 +128,59 @@ struct NodeActorMsg { incompleteValue: bool, } +trait NodeInfoToProtocol { + fn encode(self, actors: &ActorRegistry, display: bool) -> NodeActorMsg; +} + +impl NodeInfoToProtocol for NodeInfo { + fn encode(self, actors: &ActorRegistry, display: bool) -> NodeActorMsg { + let actor_name = if !actors.script_actor_registered(self.uniqueId.clone()) { + let name = actors.new_name("node"); + actors.register_script_actor(self.uniqueId, name.clone()); + name + } else { + actors.script_to_actor(self.uniqueId) + }; + + NodeActorMsg { + actor: actor_name, + baseURI: self.baseURI, + parent: actors.script_to_actor(self.parent.clone()), + nodeType: self.nodeType, + namespaceURI: self.namespaceURI, + nodeName: self.nodeName, + numChildren: self.numChildren, + + name: self.name, + publicId: self.publicId, + systemId: self.systemId, + + attrs: self.attrs.move_iter().map(|attr| { + AttrMsg { + namespace: attr.namespace, + name: attr.name, + value: attr.value, + } + }).collect(), + + pseudoClassLocks: vec!(), //TODO get this data from script + + isDisplayed: display, + + hasEventListeners: false, //TODO get this data from script + + isDocumentElement: self.isDocumentElement, + + shortValue: self.shortValue, + incompleteValue: self.incompleteValue, + } + } +} + struct WalkerActor { name: String, + script_chan: Sender, + pipeline: PipelineId, } #[deriving(Encodable)] @@ -156,9 +213,9 @@ impl Actor for WalkerActor { } fn handle_message(&self, - _registry: &ActorRegistry, + registry: &ActorRegistry, msg_type: &String, - _msg: &json::Object, + msg: &json::Object, stream: &mut TcpStream) -> bool { match msg_type.as_slice() { "querySelector" => { @@ -170,38 +227,15 @@ impl Actor for WalkerActor { } "documentElement" => { + let (tx, rx) = channel(); + self.script_chan.send(GetDocumentElement(self.pipeline, tx)); + let doc_elem_info = rx.recv(); + + let node = doc_elem_info.encode(registry, true); + let msg = DocumentElementReply { from: self.name(), - node: NodeActorMsg { - actor: "node0".to_string(), - baseURI: "".to_string(), - parent: "".to_string(), - nodeType: 1, //ELEMENT_NODE - namespaceURI: "".to_string(), - nodeName: "html".to_string(), - numChildren: 0, - - name: "".to_string(), - publicId: "".to_string(), - systemId: "".to_string(), - - attrs: vec!(AttrMsg { - namespace: "".to_string(), - name: "manifest".to_string(), - value: "foo.manifest".to_string(), - }), - - pseudoClassLocks: vec!(), - - isDisplayed: true, - - hasEventListeners: false, - - isDocumentElement: true, - - shortValue: "".to_string(), - incompleteValue: false, - } + node: node, }; stream.write_json_packet(&msg); true @@ -216,10 +250,19 @@ impl Actor for WalkerActor { } "children" => { + let target = msg.find(&"node".to_string()).unwrap().as_string().unwrap(); + let (tx, rx) = channel(); + self.script_chan.send(GetChildren(self.pipeline, + registry.actor_to_script(target.to_string()), + tx)); + let children = rx.recv(); + let msg = ChildrenReply { hasFirst: true, hasLast: true, - nodes: vec!(), + nodes: children.move_iter().map(|child| { + child.encode(registry, true) + }).collect(), from: self.name(), }; stream.write_json_packet(&msg); @@ -231,26 +274,6 @@ impl Actor for WalkerActor { } } -struct NodeActor { - name: String, -} - -impl Actor for NodeActor { - fn name(&self) -> String { - self.name.clone() - } - - fn handle_message(&self, - _registry: &ActorRegistry, - msg_type: &String, - _msg: &json::Object, - _stream: &mut TcpStream) -> bool { - match msg_type.as_slice() { - _ => false, - } - } -} - #[deriving(Encodable)] struct GetPageStyleReply { from: String, @@ -367,50 +390,19 @@ impl Actor for InspectorActor { if self.walker.borrow().is_none() { let walker = WalkerActor { name: registry.new_name("walker"), + script_chan: self.script_chan.clone(), + pipeline: self.pipeline, }; let mut walker_name = self.walker.borrow_mut(); *walker_name = Some(walker.name()); registry.register_later(box walker); } - let node = NodeActor { - name: registry.new_name("node"), - }; - let node_actor_name = node.name(); - registry.register_later(box node); + let (tx, rx) = channel(); + self.script_chan.send(GetRootNode(self.pipeline, tx)); + let root_info = rx.recv(); - //TODO: query script for actual root node - //TODO: extra node actor creation - let node = NodeActorMsg { - actor: node_actor_name, - baseURI: "".to_string(), - parent: "".to_string(), - nodeType: 1, //ELEMENT_NODE - namespaceURI: "".to_string(), - nodeName: "html".to_string(), - numChildren: 1, - - name: "".to_string(), - publicId: "".to_string(), - systemId: "".to_string(), - - attrs: vec!(AttrMsg { - namespace: "".to_string(), - name: "manifest".to_string(), - value: "foo.manifest".to_string(), - }), - - pseudoClassLocks: vec!(), - - isDisplayed: true, - - hasEventListeners: false, - - isDocumentElement: true, - - shortValue: "".to_string(), - incompleteValue: false, - }; + let node = root_info.encode(registry, false); let msg = GetWalkerReply { from: self.name(), diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index 5dbce103546..71fd82f5369 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -142,7 +142,7 @@ fn run_server(port: Receiver) { let (tab, console, inspector) = { let console = ConsoleActor { name: actors.new_name("console"), - script_chan: sender, + script_chan: sender.clone(), pipeline: pipeline, }; let inspector = InspectorActor { @@ -150,6 +150,8 @@ fn run_server(port: Receiver) { walker: RefCell::new(None), pageStyle: RefCell::new(None), highlighter: RefCell::new(None), + script_chan: sender, + pipeline: pipeline, }; //TODO: send along the current page title and URL let tab = TabActor { diff --git a/components/devtools_traits/lib.rs b/components/devtools_traits/lib.rs index 904f4994fe5..933b9c9f283 100644 --- a/components/devtools_traits/lib.rs +++ b/components/devtools_traits/lib.rs @@ -37,9 +37,39 @@ pub enum EvaluateJSReply { ActorValue(String), } +pub struct AttrInfo { + pub namespace: String, + pub name: String, + pub value: String, +} + +pub struct NodeInfo { + pub uniqueId: String, + pub baseURI: String, + pub parent: String, + pub nodeType: uint, + pub namespaceURI: String, + pub nodeName: String, + pub numChildren: uint, + + pub name: String, + pub publicId: String, + pub systemId: String, + + pub attrs: Vec, + + pub isDocumentElement: bool, + + pub shortValue: String, + pub incompleteValue: bool, +} + /// Messages to process in a particular script task, as instructed by a devtools client. pub enum DevtoolScriptControlMsg { EvaluateJS(PipelineId, String, Sender), + GetRootNode(PipelineId, Sender), + GetDocumentElement(PipelineId, Sender), + GetChildren(PipelineId, String, Sender>), } /// Messages to instruct devtools server to update its state relating to a particular diff --git a/components/script/dom/attr.rs b/components/script/dom/attr.rs index be419eb2a61..ebad173edda 100644 --- a/components/script/dom/attr.rs +++ b/components/script/dom/attr.rs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use devtools_traits::AttrInfo; use dom::bindings::codegen::Bindings::AttrBinding; use dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use dom::bindings::codegen::InheritTypes::NodeCast; @@ -149,6 +150,7 @@ pub trait AttrHelpers { fn set_value(&self, set_type: AttrSettingType, value: AttrValue); fn value<'a>(&'a self) -> Ref<'a, AttrValue>; fn local_name<'a>(&'a self) -> &'a Atom; + fn summarize(&self) -> AttrInfo; } impl<'a> AttrHelpers for JSRef<'a, Attr> { @@ -184,6 +186,14 @@ impl<'a> AttrHelpers for JSRef<'a, Attr> { fn local_name<'a>(&'a self) -> &'a Atom { &self.local_name } + + fn summarize(&self) -> AttrInfo { + AttrInfo { + namespace: self.GetNamespaceURI().unwrap_or("".to_string()), + name: self.Name(), + value: self.Value(), + } + } } pub trait AttrHelpersForLayout { diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 50af3e637ae..e58f0e888a6 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -4,12 +4,15 @@ //! The core DOM types. Defines the basic DOM hierarchy as well as all the HTML elements. +use devtools_traits::NodeInfo; use dom::attr::{Attr, AttrHelpers}; use dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods; use dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; +use dom::bindings::codegen::Bindings::NamedNodeMapBinding::NamedNodeMapMethods; use dom::bindings::codegen::Bindings::NodeBinding::{NodeConstants, NodeMethods}; +use dom::bindings::codegen::Bindings::NodeListBinding::NodeListMethods; use dom::bindings::codegen::Bindings::ProcessingInstructionBinding::ProcessingInstructionMethods; use dom::bindings::codegen::InheritTypes::{CommentCast, DocumentCast, DocumentTypeCast}; use dom::bindings::codegen::InheritTypes::{ElementCast, TextCast, NodeCast, ElementDerived}; @@ -36,7 +39,7 @@ use dom::element::{HTMLInputElementTypeId, HTMLSelectElementTypeId}; use dom::element::{HTMLTextAreaElementTypeId, HTMLOptGroupElementTypeId}; use dom::element::{HTMLOptionElementTypeId, HTMLFieldSetElementTypeId}; use dom::eventtarget::{EventTarget, NodeTargetTypeId}; -use dom::nodelist::{NodeList}; +use dom::nodelist::NodeList; use dom::processinginstruction::ProcessingInstruction; use dom::text::Text; use dom::virtualmethods::{VirtualMethods, vtable_for}; @@ -59,6 +62,7 @@ use std::mem; use style; use style::ComputedValues; use sync::Arc; +use uuid; use serialize::{Encoder, Encodable}; @@ -105,6 +109,8 @@ pub struct Node { /// Must be sent back to the layout task to be destroyed when this /// node is finalized. pub layout_data: LayoutDataRef, + + unique_id: RefCell, } impl, E> Encodable for LayoutDataRef { @@ -419,6 +425,9 @@ pub trait NodeHelpers<'m, 'n> { fn query_selector_all(&self, selectors: DOMString) -> Fallible>; fn remove_self(&self); + + fn get_unique_id(&self) -> String; + fn summarize(&self) -> NodeInfo; } impl<'m, 'n> NodeHelpers<'m, 'n> for JSRef<'n, Node> { @@ -687,6 +696,56 @@ impl<'m, 'n> NodeHelpers<'m, 'n> for JSRef<'n, Node> { None => () } } + + fn get_unique_id(&self) -> String { + self.unique_id.borrow().clone() + } + + fn summarize(&self) -> NodeInfo { + if self.unique_id.borrow().as_slice() == "" { + let mut unique_id = self.unique_id.borrow_mut(); + *unique_id = uuid::Uuid::new_v4().to_simple_str(); + } + + NodeInfo { + uniqueId: self.unique_id.borrow().clone(), + baseURI: self.GetBaseURI().unwrap_or("".to_string()), + parent: self.GetParentNode().root().map(|node| node.unique_id.borrow().clone()).unwrap_or("".to_string()), + nodeType: self.NodeType() as uint, + namespaceURI: "".to_string(), //FIXME + nodeName: self.NodeName(), + numChildren: self.ChildNodes().root().Length() as uint, + + //FIXME doctype nodes only + name: "".to_string(), + publicId: "".to_string(), + systemId: "".to_string(), + + attrs: if self.is_element() { + let mut summarized = vec!(); + let elem: &JSRef = ElementCast::to_ref(self).unwrap(); + let attrs = elem.Attributes().root(); + let mut i = 0; + while i < attrs.Length() { + let attr = attrs.Item(i).unwrap().root(); + summarized.push(attr.summarize()); + i += 1; + } + summarized + } else { + vec!() + }, + + isDocumentElement: + self.owner_doc().root() + .GetDocumentElement() + .map(|elem| NodeCast::from_ref(&*elem.root()) == self) + .unwrap_or(false), + + shortValue: self.GetNodeValue().unwrap_or("".to_string()), //FIXME: truncate + incompleteValue: false, //FIXME: reflect truncation + } + } } /// If the given untrusted node address represents a valid DOM node in the given runtime, @@ -991,6 +1050,8 @@ impl Node { flags: Traceable::new(RefCell::new(NodeFlags::new(type_id))), layout_data: LayoutDataRef::new(), + + unique_id: RefCell::new("".to_string()), } } diff --git a/components/script/lib.rs b/components/script/lib.rs index d7c13cc06a1..4318f4b3f21 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -40,6 +40,7 @@ extern crate style; extern crate sync; extern crate servo_msg = "msg"; extern crate url; +extern crate uuid; pub mod cors; diff --git a/components/script/script_task.rs b/components/script/script_task.rs index 2feb088201c..e49350a1942 100644 --- a/components/script/script_task.rs +++ b/components/script/script_task.rs @@ -5,6 +5,7 @@ //! The script task is the task that owns the DOM in memory, runs JavaScript, and spawns parsing //! and layout tasks. +use dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use dom::bindings::codegen::InheritTypes::{EventTargetCast, NodeCast, EventCast}; use dom::bindings::conversions; use dom::bindings::conversions::{FromJSValConvertible, Empty}; @@ -33,8 +34,9 @@ use layout_interface; use page::{Page, IterablePage, Frame}; use devtools_traits; -use devtools_traits::{DevtoolsControlChan, DevtoolsControlPort, NewGlobal}; -use devtools_traits::{DevtoolScriptControlMsg, EvaluateJS, EvaluateJSReply}; +use devtools_traits::{DevtoolsControlChan, DevtoolsControlPort, NewGlobal, NodeInfo, GetRootNode}; +use devtools_traits::{DevtoolScriptControlMsg, EvaluateJS, EvaluateJSReply, GetDocumentElement}; +use devtools_traits::{GetChildren}; use script_traits::{CompositorEvent, ResizeEvent, ReflowEvent, ClickEvent, MouseDownEvent}; use script_traits::{MouseMoveEvent, MouseUpEvent, ConstellationControlMsg, ScriptTaskFactory}; use script_traits::{ResizeMsg, AttachLayoutMsg, LoadMsg, SendEventMsg, ResizeInactiveMsg}; @@ -501,6 +503,9 @@ impl ScriptTask { FromScript(WorkerPostMessage(addr, data, nbytes)) => Worker::handle_message(addr, data, nbytes), FromScript(WorkerRelease(addr)) => Worker::handle_release(addr), FromDevtools(EvaluateJS(id, s, reply)) => self.handle_evaluate_js(id, s, reply), + FromDevtools(GetRootNode(id, reply)) => self.handle_get_root_node(id, reply), + FromDevtools(GetDocumentElement(id, reply)) => self.handle_get_document_element(id, reply), + FromDevtools(GetChildren(id, node_id, reply)) => self.handle_get_children(id, node_id, reply), } } @@ -530,6 +535,47 @@ impl ScriptTask { }); } + fn handle_get_root_node(&self, pipeline: PipelineId, reply: Sender) { + let page = get_page(&*self.page.borrow(), pipeline); + let frame = page.frame(); + let document = frame.get_ref().document.root(); + + let node: &JSRef = NodeCast::from_ref(&*document); + reply.send(node.summarize()); + } + + fn handle_get_document_element(&self, pipeline: PipelineId, reply: Sender) { + let page = get_page(&*self.page.borrow(), pipeline); + let frame = page.frame(); + let document = frame.get_ref().document.root(); + let document_element = document.GetDocumentElement().root().unwrap(); + + let node: &JSRef = NodeCast::from_ref(&*document_element); + reply.send(node.summarize()); + } + + fn handle_get_children(&self, pipeline: PipelineId, node_id: String, reply: Sender>) { + let page = get_page(&*self.page.borrow(), pipeline); + let frame = page.frame(); + let document = frame.get_ref().document.root(); + let node: &JSRef = NodeCast::from_ref(&*document); + + let mut children = vec!(); + let mut found_parent = false; + for candidate in node.traverse_preorder() { + if candidate.get_unique_id().as_slice() == node_id.as_slice() { + found_parent = true; + for kid in candidate.children() { + children.push(kid.summarize()); + } + break; + } + } + + assert!(found_parent); + reply.send(children); + } + fn handle_new_layout(&self, new_layout_info: NewLayoutInfo) { debug!("Script: new layout: {:?}", new_layout_info); let NewLayoutInfo { From fae7ce3c1dbcbf90460c3fba683c162b6c742cc7 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Thu, 4 Sep 2014 16:21:50 -0400 Subject: [PATCH 7/7] Retrieve some basic layout properties for nodes to make the box model somewhat useful. --- components/devtools/actors/console.rs | 4 +- components/devtools/actors/inspector.rs | 60 +++++++++++++++++++++++-- components/devtools_traits/lib.rs | 1 + components/script/dom/attr.rs | 5 ++- components/script/dom/element.rs | 15 +++++++ components/script/dom/node.rs | 16 ++----- components/script/script_task.rs | 31 ++++++++----- 7 files changed, 101 insertions(+), 31 deletions(-) diff --git a/components/devtools/actors/console.rs b/components/devtools/actors/console.rs index 7ae4cf89676..c58d7f6373f 100644 --- a/components/devtools/actors/console.rs +++ b/components/devtools/actors/console.rs @@ -237,9 +237,9 @@ impl Actor for ConsoleActor { } else if val.is_infinite() { let mut m = TreeMap::new(); if val < 0. { - m.insert("type".to_string(), "Infinity".to_string().to_json()); - } else { m.insert("type".to_string(), "-Infinity".to_string().to_json()); + } else { + m.insert("type".to_string(), "Infinity".to_string().to_json()); } json::Object(m) } else if val == Float::neg_zero() { diff --git a/components/devtools/actors/inspector.rs b/components/devtools/actors/inspector.rs index ac5608ed63a..5d401e4ea7a 100644 --- a/components/devtools/actors/inspector.rs +++ b/components/devtools/actors/inspector.rs @@ -5,13 +5,15 @@ /// Liberally derived from the [Firefox JS implementation](http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/inspector.js). use devtools_traits::{GetRootNode, GetDocumentElement, GetChildren, DevtoolScriptControlMsg}; -use devtools_traits::NodeInfo; +use devtools_traits::{GetLayout, NodeInfo}; use actor::{Actor, ActorRegistry}; use protocol::JsonPacketSender; +use collections::TreeMap; use servo_msg::constellation_msg::PipelineId; use serialize::json; +use serialize::json::ToJson; use std::cell::RefCell; use std::io::TcpStream; @@ -287,6 +289,8 @@ struct PageStyleMsg { struct PageStyleActor { name: String, + script_chan: Sender, + pipeline: PipelineId, } #[deriving(Encodable)] @@ -334,15 +338,31 @@ struct AppliedSheet { ruleCount: uint, } +#[deriving(Encodable)] +struct GetLayoutReply { + width: int, + height: int, + autoMargins: json::Json, + from: String, +} + +#[deriving(Encodable)] +struct AutoMargins { + top: String, + bottom: String, + left: String, + right: String, +} + impl Actor for PageStyleActor { fn name(&self) -> String { self.name.clone() } fn handle_message(&self, - _registry: &ActorRegistry, + registry: &ActorRegistry, msg_type: &String, - _msg: &json::Object, + msg: &json::Object, stream: &mut TcpStream) -> bool { match msg_type.as_slice() { "getApplied" => { @@ -368,7 +388,37 @@ impl Actor for PageStyleActor { } //TODO: query script for box layout properties of node (msg.node) - //"getLayout" => {} + "getLayout" => { + let target = msg.find(&"node".to_string()).unwrap().as_string().unwrap(); + let (tx, rx) = channel(); + self.script_chan.send(GetLayout(self.pipeline, + registry.actor_to_script(target.to_string()), + tx)); + let (width, height) = rx.recv(); + + let auto_margins = msg.find(&"autoMargins".to_string()).unwrap().as_boolean().unwrap(); + + //TODO: the remaining layout properties (margin, border, padding, position) + // as specified in getLayout in http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/styles.js + let msg = GetLayoutReply { + width: width.round() as int, + height: height.round() as int, + autoMargins: if auto_margins { + //TODO: real values like processMargins in http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/styles.js + let mut m = TreeMap::new(); + m.insert("top".to_string(), "auto".to_string().to_json()); + m.insert("bottom".to_string(), "auto".to_string().to_json()); + m.insert("left".to_string(), "auto".to_string().to_json()); + m.insert("right".to_string(), "auto".to_string().to_json()); + json::Object(m) + } else { + json::Null + }, + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } _ => false, } @@ -419,6 +469,8 @@ impl Actor for InspectorActor { if self.pageStyle.borrow().is_none() { let style = PageStyleActor { name: registry.new_name("pageStyle"), + script_chan: self.script_chan.clone(), + pipeline: self.pipeline, }; let mut pageStyle = self.pageStyle.borrow_mut(); *pageStyle = Some(style.name()); diff --git a/components/devtools_traits/lib.rs b/components/devtools_traits/lib.rs index 933b9c9f283..6bba96d81b3 100644 --- a/components/devtools_traits/lib.rs +++ b/components/devtools_traits/lib.rs @@ -70,6 +70,7 @@ pub enum DevtoolScriptControlMsg { GetRootNode(PipelineId, Sender), GetDocumentElement(PipelineId, Sender), GetChildren(PipelineId, String, Sender>), + GetLayout(PipelineId, String, Sender<(f32, f32)>), } /// Messages to instruct devtools server to update its state relating to a particular diff --git a/components/script/dom/attr.rs b/components/script/dom/attr.rs index ebad173edda..61f520821a1 100644 --- a/components/script/dom/attr.rs +++ b/components/script/dom/attr.rs @@ -2,7 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use devtools_traits::AttrInfo; use dom::bindings::codegen::Bindings::AttrBinding; use dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use dom::bindings::codegen::InheritTypes::NodeCast; @@ -14,6 +13,8 @@ use dom::element::{Element, AttributeHandlers}; use dom::node::Node; use dom::window::Window; use dom::virtualmethods::vtable_for; + +use devtools_traits::AttrInfo; use servo_util::atom::Atom; use servo_util::namespace; use servo_util::namespace::Namespace; @@ -189,7 +190,7 @@ impl<'a> AttrHelpers for JSRef<'a, Attr> { fn summarize(&self) -> AttrInfo { AttrInfo { - namespace: self.GetNamespaceURI().unwrap_or("".to_string()), + namespace: self.namespace.to_str().to_string(), name: self.Name(), value: self.Value(), } diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 953036788f4..ab61fc47136 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -10,6 +10,7 @@ use dom::namednodemap::NamedNodeMap; use dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use dom::bindings::codegen::Bindings::ElementBinding; use dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; +use dom::bindings::codegen::Bindings::NamedNodeMapBinding::NamedNodeMapMethods; use dom::bindings::codegen::InheritTypes::{ElementDerived, NodeCast}; use dom::bindings::js::{JS, JSRef, Temporary, TemporaryPushable}; use dom::bindings::js::{OptionalSettable, OptionalRootable, Root}; @@ -30,6 +31,7 @@ use dom::nodelist::NodeList; use dom::virtualmethods::{VirtualMethods, vtable_for}; use layout_interface::ContentChangedDocumentDamage; use layout_interface::MatchSelectorsDocumentDamage; +use devtools_traits::AttrInfo; use style::{matches, parse_selector_list_from_str}; use style; use servo_util::atom::Atom; @@ -239,6 +241,7 @@ pub trait ElementHelpers { fn html_element_in_html_document(&self) -> bool; fn get_local_name<'a>(&'a self) -> &'a Atom; fn get_namespace<'a>(&'a self) -> &'a Namespace; + fn summarize(&self) -> Vec; } impl<'a> ElementHelpers for JSRef<'a, Element> { @@ -254,6 +257,18 @@ impl<'a> ElementHelpers for JSRef<'a, Element> { fn get_namespace<'a>(&'a self) -> &'a Namespace { &self.deref().namespace } + + fn summarize(&self) -> Vec { + let attrs = self.Attributes().root(); + let mut i = 0; + let mut summarized = vec!(); + while i < attrs.Length() { + let attr = attrs.Item(i).unwrap().root(); + summarized.push(attr.summarize()); + i += 1; + } + summarized + } } pub trait AttributeHandlers { diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index e58f0e888a6..04d2686df18 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -4,7 +4,6 @@ //! The core DOM types. Defines the basic DOM hierarchy as well as all the HTML elements. -use devtools_traits::NodeInfo; use dom::attr::{Attr, AttrHelpers}; use dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods; @@ -48,6 +47,7 @@ use geom::rect::Rect; use html::hubbub_html_parser::build_element_from_tag; use layout_interface::{ContentBoxResponse, ContentBoxesResponse, LayoutRPC, LayoutChan, ReapLayoutDataMsg, TrustedNodeAddress, UntrustedNodeAddress}; +use devtools_traits::NodeInfo; use servo_util::geometry::Au; use servo_util::str::{DOMString, null_str_as_empty}; use style::{parse_selector_list_from_str, matches}; @@ -702,7 +702,7 @@ impl<'m, 'n> NodeHelpers<'m, 'n> for JSRef<'n, Node> { } fn summarize(&self) -> NodeInfo { - if self.unique_id.borrow().as_slice() == "" { + if self.unique_id.borrow().is_empty() { let mut unique_id = self.unique_id.borrow_mut(); *unique_id = uuid::Uuid::new_v4().to_simple_str(); } @@ -710,7 +710,7 @@ impl<'m, 'n> NodeHelpers<'m, 'n> for JSRef<'n, Node> { NodeInfo { uniqueId: self.unique_id.borrow().clone(), baseURI: self.GetBaseURI().unwrap_or("".to_string()), - parent: self.GetParentNode().root().map(|node| node.unique_id.borrow().clone()).unwrap_or("".to_string()), + parent: self.GetParentNode().root().map(|node| node.get_unique_id()).unwrap_or("".to_string()), nodeType: self.NodeType() as uint, namespaceURI: "".to_string(), //FIXME nodeName: self.NodeName(), @@ -722,16 +722,8 @@ impl<'m, 'n> NodeHelpers<'m, 'n> for JSRef<'n, Node> { systemId: "".to_string(), attrs: if self.is_element() { - let mut summarized = vec!(); let elem: &JSRef = ElementCast::to_ref(self).unwrap(); - let attrs = elem.Attributes().root(); - let mut i = 0; - while i < attrs.Length() { - let attr = attrs.Item(i).unwrap().root(); - summarized.push(attr.summarize()); - i += 1; - } - summarized + elem.summarize() } else { vec!() }, diff --git a/components/script/script_task.rs b/components/script/script_task.rs index e49350a1942..1dd63abeadd 100644 --- a/components/script/script_task.rs +++ b/components/script/script_task.rs @@ -6,7 +6,9 @@ //! and layout tasks. use dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; -use dom::bindings::codegen::InheritTypes::{EventTargetCast, NodeCast, EventCast}; +use dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods; +use dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; +use dom::bindings::codegen::InheritTypes::{EventTargetCast, NodeCast, EventCast, ElementCast}; use dom::bindings::conversions; use dom::bindings::conversions::{FromJSValConvertible, Empty}; use dom::bindings::global::Window; @@ -36,7 +38,7 @@ use page::{Page, IterablePage, Frame}; use devtools_traits; use devtools_traits::{DevtoolsControlChan, DevtoolsControlPort, NewGlobal, NodeInfo, GetRootNode}; use devtools_traits::{DevtoolScriptControlMsg, EvaluateJS, EvaluateJSReply, GetDocumentElement}; -use devtools_traits::{GetChildren}; +use devtools_traits::{GetChildren, GetLayout}; use script_traits::{CompositorEvent, ResizeEvent, ReflowEvent, ClickEvent, MouseDownEvent}; use script_traits::{MouseMoveEvent, MouseUpEvent, ConstellationControlMsg, ScriptTaskFactory}; use script_traits::{ResizeMsg, AttachLayoutMsg, LoadMsg, SendEventMsg, ResizeInactiveMsg}; @@ -506,6 +508,7 @@ impl ScriptTask { FromDevtools(GetRootNode(id, reply)) => self.handle_get_root_node(id, reply), FromDevtools(GetDocumentElement(id, reply)) => self.handle_get_document_element(id, reply), FromDevtools(GetChildren(id, node_id, reply)) => self.handle_get_children(id, node_id, reply), + FromDevtools(GetLayout(id, node_id, reply)) => self.handle_get_layout(id, node_id, reply), } } @@ -554,28 +557,34 @@ impl ScriptTask { reply.send(node.summarize()); } - fn handle_get_children(&self, pipeline: PipelineId, node_id: String, reply: Sender>) { + fn find_node_by_unique_id(&self, pipeline: PipelineId, node_id: String) -> Temporary { let page = get_page(&*self.page.borrow(), pipeline); let frame = page.frame(); let document = frame.get_ref().document.root(); let node: &JSRef = NodeCast::from_ref(&*document); - let mut children = vec!(); - let mut found_parent = false; for candidate in node.traverse_preorder() { if candidate.get_unique_id().as_slice() == node_id.as_slice() { - found_parent = true; - for kid in candidate.children() { - children.push(kid.summarize()); - } - break; + return Temporary::from_rooted(&candidate); } } - assert!(found_parent); + fail!("couldn't find node with unique id {:s}", node_id) + } + + fn handle_get_children(&self, pipeline: PipelineId, node_id: String, reply: Sender>) { + let parent = self.find_node_by_unique_id(pipeline, node_id).root(); + let children = parent.children().map(|child| child.summarize()).collect(); reply.send(children); } + fn handle_get_layout(&self, pipeline: PipelineId, node_id: String, reply: Sender<(f32, f32)>) { + let node = self.find_node_by_unique_id(pipeline, node_id).root(); + let elem: &JSRef = ElementCast::to_ref(&*node).expect("should be getting layout of element"); + let rect = elem.GetBoundingClientRect().root(); + reply.send((rect.Width(), rect.Height())); + } + fn handle_new_layout(&self, new_layout_info: NewLayoutInfo) { debug!("Script: new layout: {:?}", new_layout_info); let NewLayoutInfo {