From bb9955c28132a0c6b2fa85187f6d4e1566d7db5e Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Mon, 1 Sep 2014 10:08:10 -0400 Subject: [PATCH] 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), }