diff options
27 files changed, 1799 insertions, 6 deletions
diff --git a/Cargo.lock b/Cargo.lock index a7baeb33a8e..0252ff2c591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,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)", @@ -106,6 +108,21 @@ dependencies = [ ] [[package]] +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" version = "0.1.0" source = "git+https://github.com/servo/rust-egl#48b85e30d557ab2ee536730a73dd86a8160d618b" @@ -364,6 +381,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 +401,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/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<LTF, STF> { pub compositor_chan: CompositorChan, pub resource_task: ResourceTask, pub image_cache_task: ImageCacheTask, + devtools_chan: Option<DevtoolsControlChan>, pipelines: HashMap<PipelineId, Rc<Pipeline>>, font_cache_task: FontCacheTask, navigation_context: NavigationContext, @@ -244,7 +246,8 @@ impl<LTF: LayoutTaskFactory, STF: ScriptTaskFactory> Constellation<LTF, STF> { resource_task: ResourceTask, image_cache_task: ImageCacheTask, font_cache_task: FontCacheTask, - time_profiler_chan: TimeProfilerChan) + time_profiler_chan: TimeProfilerChan, + devtools_chan: Option<DevtoolsControlChan>) -> ConstellationChan { let (constellation_port, constellation_chan) = ConstellationChan::new(); let constellation_chan_clone = constellation_chan.clone(); @@ -254,6 +257,7 @@ impl<LTF: LayoutTaskFactory, STF: ScriptTaskFactory> Constellation<LTF, STF> { 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<LTF: LayoutTaskFactory, STF: ScriptTaskFactory> Constellation<LTF, STF> { 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<SubpageId>, constellation_chan: ConstellationChan, compositor_chan: CompositorChan, + devtools_chan: Option<DevtoolsControlChan>, 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..2984f9daf61 --- /dev/null +++ b/components/devtools/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "devtools" +version = "0.0.1" +authors = ["The Servo Project Developers"] + +[lib] +name = "devtools" +path = "lib.rs" + +[dependencies.devtools_traits] +path = "../devtools_traits" + +[dependencies.msg] +path = "../msg" diff --git a/components/devtools/actor.rs b/components/devtools/actor.rs new file mode 100644 index 00000000000..fae6c8864cf --- /dev/null +++ b/components/devtools/actor.rs @@ -0,0 +1,171 @@ +/* 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::cell::{Cell, RefCell}; +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<T: 'static>(self) -> Option<&'a mut T> { + if self.is::<T>() { + 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<T: 'static>(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::<T>(); + let boxed = self.get_type_id(); + t == boxed*/ + true + } + + fn downcast_ref<T: 'static>(self) -> Option<&'a T> { + if self.is::<T>() { + 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<String, Box<Actor+Send+Sized>>, + new_actors: RefCell<Vec<Box<Actor+Send+Sized>>>, + script_actors: RefCell<HashMap<String, String>>, + next: Cell<u32>, +} + +impl ActorRegistry { + /// Create an empty registry. + pub fn new() -> 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(); + 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<Actor+Send+Sized>) { + self.actors.insert(actor.name().to_string(), actor); + } + + pub fn register_later(&self, actor: Box<Actor+Send+Sized>) { + 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 + // fails for unknown reasons. + /*let actor: &Actor+Send+Sized = *self.actors.find(&name.to_string()).unwrap(); + (actor as &Any).downcast_ref::<T>().unwrap()*/ + self.actors.find(&name.to_string()).unwrap().as_ref::<T>().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::<T>().unwrap()*/ + self.actors.find_mut(&name.to_string()).unwrap().downcast_mut::<T>().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(&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), + 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); + } + } + } + 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/console.rs b/components/devtools/actors/console.rs new file mode 100644 index 00000000000..c58d7f6373f --- /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<String>, + 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<json::Object>, +} + +#[deriving(Encodable)] +struct StopListenersReply { + from: String, + stoppedListeners: Vec<String>, +} + +#[deriving(Encodable)] +struct AutocompleteReply { + from: String, + matches: Vec<String>, + 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<DevtoolScriptControlMsg>, +} + +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/inspector.rs b/components/devtools/actors/inspector.rs new file mode 100644 index 00000000000..5d401e4ea7a --- /dev/null +++ b/components/devtools/actors/inspector.rs @@ -0,0 +1,516 @@ +/* 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 devtools_traits::{GetRootNode, GetDocumentElement, GetChildren, DevtoolScriptControlMsg}; +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; + +pub struct InspectorActor { + pub name: String, + pub walker: RefCell<Option<String>>, + pub pageStyle: RefCell<Option<String>>, + pub highlighter: RefCell<Option<String>>, + pub script_chan: Sender<DevtoolScriptControlMsg>, + pub pipeline: PipelineId, +} + +#[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<AttrMsg>, + + pseudoClassLocks: Vec<String>, + + isDisplayed: bool, + + hasEventListeners: bool, + + isDocumentElement: bool, + + shortValue: String, + 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<DevtoolScriptControlMsg>, + pipeline: PipelineId, +} + +#[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<NodeActorMsg>, + 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 (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: node, + }; + stream.write_json_packet(&msg); + true + } + + "clearPseudoClassLocks" => { + let msg = ClearPseudoclassesReply { + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + "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: children.move_iter().map(|child| { + child.encode(registry, true) + }).collect(), + from: self.name(), + }; + stream.write_json_packet(&msg); + true + } + + _ => false, + } + } +} + +#[deriving(Encodable)] +struct GetPageStyleReply { + from: String, + pageStyle: PageStyleMsg, +} + +#[deriving(Encodable)] +struct PageStyleMsg { + actor: String, +} + +struct PageStyleActor { + name: String, + script_chan: Sender<DevtoolScriptControlMsg>, + pipeline: PipelineId, +} + +#[deriving(Encodable)] +struct GetAppliedReply { + entries: Vec<AppliedEntry>, + rules: Vec<AppliedRule>, + sheets: Vec<AppliedSheet>, + from: String, +} + +#[deriving(Encodable)] +struct GetComputedReply { + computed: Vec<uint>, //XXX all css props + from: String, +} + +#[deriving(Encodable)] +struct AppliedEntry { + rule: String, + pseudoElement: json::Json, + isSystem: bool, + matchedSelectors: Vec<String>, +} + +#[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, +} + +#[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, + 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" => { + 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, + } + } +} + +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"), + 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 (tx, rx) = channel(); + self.script_chan.send(GetRootNode(self.pipeline, tx)); + let root_info = rx.recv(); + + let node = root_info.encode(registry, 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"), + script_chan: self.script_chan.clone(), + pipeline: self.pipeline, + }; + 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 new file mode 100644 index 00000000000..8ebd79e4016 --- /dev/null +++ b/components/devtools/actors/root.rs @@ -0,0 +1,99 @@ +/* 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, + highlightable: bool, + customHighlighters: Vec<String>, +} + +#[deriving(Encodable)] +struct ErrorReply { + from: String, + error: String, + message: String, +} + +#[deriving(Encodable)] +struct ListTabsReply { + from: String, + selected: uint, + tabs: Vec<TabActorMsg>, +} + +#[deriving(Encodable)] +struct RootActorMsg { + from: String, + applicationType: String, + traits: ActorTraits, +} + +pub struct RootActor { + pub tabs: Vec<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 + } + + //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::<TabActor>(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, + highlightable: true, + customHighlighters: vec!("BoxModelHighlighter".to_string()), + }, + } + } +} diff --git a/components/devtools/actors/tab.rs b/components/devtools/actors/tab.rs new file mode 100644 index 00000000000..c9cc3481264 --- /dev/null +++ b/components/devtools/actors/tab.rs @@ -0,0 +1,136 @@ +/* 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)] +struct ListFramesReply { + from: String, + frames: Vec<FrameMsg>, +} + +#[deriving(Encodable)] +struct FrameMsg { + id: uint, + url: String, + title: String, + parentID: uint, +} + +#[deriving(Encodable)] +pub struct TabActorMsg { + actor: String, + title: String, + 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 { + 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 + } + + "listFrames" => { + let msg = ListFramesReply { + from: self.name(), + frames: vec!(), + }; + 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: self.console.clone(), + inspectorActor: self.inspector.clone(), + } + } +} diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs new file mode 100644 index 00000000000..71fd82f5369 --- /dev/null +++ b/components/devtools/lib.rs @@ -0,0 +1,203 @@ +/* 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; + +/// 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; +extern crate debug; +extern crate std; +extern crate serialize; +extern crate sync; +extern crate servo_msg = "msg"; + +use actor::{Actor, ActorRegistry}; +use actors::console::ConsoleActor; +use actors::inspector::InspectorActor; +use actors::root::RootActor; +use actors::tab::TabActor; +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}; +use std::io::{Acceptor, Listener, EndOfFile, TimedOut}; +use std::num; +use std::task::TaskBuilder; +use serialize::json; +use sync::{Arc, Mutex}; + +mod actor; +/// Corresponds to http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/ +mod actors { + pub mod console; + pub mod inspector; + pub mod root; + pub mod tab; +} +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<DevtoolsControlMsg> { + 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<DevtoolsControlMsg>) { + 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 root = box RootActor { + tabs: vec!(), + }; + + registry.register(root); + registry.find::<RootActor>("root"); + + let actors = Arc::new(Mutex::new(registry)); + + /// Process the input from a single devtools client until EOF. + fn handle_client(actors: Arc<Mutex<ActorRegistry>>, mut stream: TcpStream) { + println!("connection established to {:?}", stream.peer_name().unwrap()); + + { + let mut actors = actors.lock(); + let msg = actors.find::<RootActor>("root").encodable(); + 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 { + 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; + } + } + } + } + } + + // 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<Mutex<ActorRegistry>>, + pipeline: PipelineId, + sender: Sender<DevtoolScriptControlMsg>) { + let mut actors = actors.lock(); + + //TODO: move all this actor creation into a constructor method on TabActor + let (tab, console, inspector) = { + let console = ConsoleActor { + name: actors.new_name("console"), + script_chan: sender.clone(), + pipeline: pipeline, + }; + let inspector = InspectorActor { + name: actors.new_name("inspector"), + 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 { + name: actors.new_name("tab"), + title: "".to_string(), + url: "about:blank".to_string(), + console: console.name(), + inspector: inspector.name(), + }; + + let root = actors.find_mut::<RootActor>("root"); + root.tabs.push(tab.name.clone()); + (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, + // 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 { + Err(ref e) if e.kind == TimedOut => { + match port.try_recv() { + Ok(ServerExitMsg) | Err(Disconnected) => break, + Ok(NewGlobal(id, sender)) => handle_new_global(actors.clone(), id, sender), + 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/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<json::Encoder<'a>,IoError>>(&mut self, obj: &T); +} + +impl JsonPacketSender for TcpStream { + fn write_json_packet<'a, T: Encodable<json::Encoder<'a>,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/Cargo.toml b/components/devtools_traits/Cargo.toml new file mode 100644 index 00000000000..c3ea63b06fa --- /dev/null +++ b/components/devtools_traits/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "devtools_traits" +version = "0.0.1" +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 new file mode 100644 index 00000000000..6bba96d81b3 --- /dev/null +++ b/components/devtools_traits/lib.rs @@ -0,0 +1,81 @@ +/* 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"] + +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<DevtoolsControlMsg>; +pub type DevtoolsControlPort = Receiver<DevtoolScriptControlMsg>; + +/// 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<DevtoolScriptControlMsg>), + ServerExitMsg +} + +/// Serialized JS return values +/// TODO: generalize this beyond the EvaluateJS message? +pub enum EvaluateJSReply { + VoidValue, + NullValue, + BooleanValue(bool), + NumberValue(f64), + StringValue(String), + 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<AttrInfo>, + + 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<EvaluateJSReply>), + GetRootNode(PipelineId, Sender<NodeInfo>), + GetDocumentElement(PipelineId, Sender<NodeInfo>), + GetChildren(PipelineId, String, Sender<Vec<NodeInfo>>), + GetLayout(PipelineId, String, Sender<(f32, f32)>), +} + +/// 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), +} 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/dom/attr.rs b/components/script/dom/attr.rs index be419eb2a61..61f520821a1 100644 --- a/components/script/dom/attr.rs +++ b/components/script/dom/attr.rs @@ -13,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; @@ -149,6 +151,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 +187,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.namespace.to_str().to_string(), + name: self.Name(), + value: self.Value(), + } + } } pub trait AttrHelpersForLayout { 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<AttrInfo>; } 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<AttrInfo> { + 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 e72e95052a6..608fdcad118 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -9,7 +9,9 @@ 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 +38,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}; @@ -45,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}; @@ -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<String>, } impl<S: Encoder<E>, E> Encodable<S, E> for LayoutDataRef { @@ -419,6 +425,9 @@ pub trait NodeHelpers<'m, 'n> { fn query_selector_all(&self, selectors: DOMString) -> Fallible<Temporary<NodeList>>; 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,48 @@ 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().is_empty() { + 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.get_unique_id()).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 elem: &JSRef<Element> = ElementCast::to_ref(self).unwrap(); + elem.summarize() + } 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 +1042,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 9f3effaa368..4318f4b3f21 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; @@ -39,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 a1f2cd27b22..1dd63abeadd 100644 --- a/components/script/script_task.rs +++ b/components/script/script_task.rs @@ -5,7 +5,11 @@ //! The script task is the task that owns the DOM in memory, runs JavaScript, and spawns parsing //! and layout tasks. -use dom::bindings::codegen::InheritTypes::{EventTargetCast, NodeCast, EventCast}; +use dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; +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; use dom::bindings::js::{JS, JSRef, RootCollection, Temporary, OptionalSettable}; @@ -31,6 +35,10 @@ use layout_interface::ContentChangedDocumentDamage; use layout_interface; 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, GetLayout}; 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 +165,12 @@ pub struct ScriptTask { /// A handle to the compositor for communicating ready state messages. compositor: Box<ScriptListener>, + /// For providing instructions to an optional devtools server. + devtools_chan: Option<DevtoolsControlChan>, + /// 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 +254,7 @@ impl ScriptTaskFactory for ScriptTask { failure_msg: Failure, resource_task: ResourceTask, image_cache_task: ImageCacheTask, + devtools_chan: Option<DevtoolsControlChan>, window_size: WindowSizeData) { let ConstellationChan(const_chan) = constellation_chan.clone(); let (script_chan, script_port) = channel(); @@ -255,6 +270,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 +293,7 @@ impl ScriptTask { constellation_chan: ConstellationChan, resource_task: ResourceTask, img_cache_task: ImageCacheTask, + devtools_chan: Option<DevtoolsControlChan>, window_size: WindowSizeData) -> Rc<ScriptTask> { let (js_runtime, js_context) = ScriptTask::new_rt_and_cx(); @@ -299,6 +316,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(id, devtools_sender.clone())); + }); + Rc::new(ScriptTask { page: RefCell::new(Rc::new(page)), @@ -311,6 +336,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 +419,7 @@ impl ScriptTask { enum MixedMessage { FromConstellation(ConstellationControlMsg), FromScript(ScriptMsg), + FromDevtools(DevtoolScriptControlMsg), } // Store new resizes, and gather all other events. @@ -402,20 +430,27 @@ 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(); + if self.devtools_chan.is_some() { + 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 +469,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,12 +504,87 @@ 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(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), + FromDevtools(GetLayout(id, node_id, reply)) => self.handle_get_layout(id, node_id, reply), } } true } + fn handle_evaluate_js(&self, pipeline: PipelineId, eval: String, reply: Sender<EvaluateJSReply>) { + 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_get_root_node(&self, pipeline: PipelineId, reply: Sender<NodeInfo>) { + let page = get_page(&*self.page.borrow(), pipeline); + let frame = page.frame(); + let document = frame.get_ref().document.root(); + + let node: &JSRef<Node> = NodeCast::from_ref(&*document); + reply.send(node.summarize()); + } + + fn handle_get_document_element(&self, pipeline: PipelineId, reply: Sender<NodeInfo>) { + 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<Node> = NodeCast::from_ref(&*document_element); + reply.send(node.summarize()); + } + + fn find_node_by_unique_id(&self, pipeline: PipelineId, node_id: String) -> Temporary<Node> { + let page = get_page(&*self.page.borrow(), pipeline); + let frame = page.frame(); + let document = frame.get_ref().document.root(); + let node: &JSRef<Node> = NodeCast::from_ref(&*document); + + for candidate in node.traverse_preorder() { + if candidate.get_unique_id().as_slice() == node_id.as_slice() { + return Temporary::from_rooted(&candidate); + } + } + + fail!("couldn't find node with unique id {:s}", node_id) + } + + fn handle_get_children(&self, pipeline: PipelineId, node_id: String, reply: Sender<Vec<NodeInfo>>) { + 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<Element> = 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 { 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<DevtoolsControlChan>, window_size: WindowSizeData); fn create_layout_channel(_phantom: Option<&mut Self>) -> OpaqueScriptLayoutChannel; fn clone_layout_channel(_phantom: Option<&mut Self>, pair: &OpaqueScriptLayoutChannel) -> Box<Any+Send>; 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<Opts> { 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<Opts> { 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(); |