diff --git a/components/devtools/actors/source.rs b/components/devtools/actors/source.rs index 9d29bc1d3ef..e03173b5fce 100644 --- a/components/devtools/actors/source.rs +++ b/components/devtools/actors/source.rs @@ -4,10 +4,16 @@ use std::cell::{Ref, RefCell}; use std::collections::BTreeSet; +use std::net::TcpStream; use serde::Serialize; +use serde_json::{Map, Value}; use servo_url::ServoUrl; +use crate::StreamId; +use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; +use crate::protocol::JsonPacketStream; + #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SourceData { @@ -15,6 +21,7 @@ pub(crate) struct SourceData { /// URL of the script, or URL of the page for inline scripts. pub url: String, pub is_black_boxed: bool, + pub source_content: String, } #[derive(Serialize)] @@ -23,24 +30,38 @@ pub(crate) struct SourcesReply { pub sources: Vec, } -pub(crate) struct Source { - actor_name: String, - source_urls: RefCell>, +pub(crate) struct SourceManager { + pub source_urls: RefCell>, } -impl Source { - pub fn new(actor_name: String) -> Self { +#[derive(Clone, Debug)] +pub struct SourceActor { + pub name: String, + pub content: String, + pub content_type: String, +} + +#[derive(Serialize)] +struct SourceContentReply { + from: String, + #[serde(rename = "contentType")] + content_type: String, + source: String, +} + +impl SourceManager { + pub fn new() -> Self { Self { - actor_name, source_urls: RefCell::new(BTreeSet::default()), } } - pub fn add_source(&self, url: ServoUrl) { + pub fn add_source(&self, url: ServoUrl, source_content: String, actor_name: String) { self.source_urls.borrow_mut().insert(SourceData { - actor: self.actor_name.clone(), + actor: actor_name, url: url.to_string(), is_black_boxed: false, + source_content, }); } @@ -48,3 +69,51 @@ impl Source { self.source_urls.borrow() } } + +impl SourceActor { + pub fn new(name: String, content: String, content_type: String) -> SourceActor { + SourceActor { + name, + content, + content_type, + } + } + + pub fn new_source(actors: &mut ActorRegistry, content: String, content_type: String) -> String { + let source_actor_name = actors.new_name("source"); + + let source_actor = SourceActor::new(source_actor_name.clone(), content, content_type); + actors.register(Box::new(source_actor)); + + source_actor_name + } +} + +impl Actor for SourceActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message( + &self, + _registry: &ActorRegistry, + msg_type: &str, + _msg: &Map, + stream: &mut TcpStream, + _id: StreamId, + ) -> Result { + Ok(match msg_type { + // Client has requested contents of the source. + "source" => { + let reply = SourceContentReply { + from: self.name(), + content_type: self.content_type.clone(), + source: self.content.clone(), + }; + let _ = stream.write_json_packet(&reply); + ActorMessageStatus::Processed + }, + _ => ActorMessageStatus::Ignored, + }) + } +} diff --git a/components/devtools/actors/thread.rs b/components/devtools/actors/thread.rs index 7ff11dff675..c7ddb19eb64 100644 --- a/components/devtools/actors/thread.rs +++ b/components/devtools/actors/thread.rs @@ -7,7 +7,7 @@ use std::net::TcpStream; use serde::Serialize; use serde_json::{Map, Value}; -use super::source::{Source, SourcesReply}; +use super::source::{SourceData, SourceManager, SourcesReply}; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::protocol::JsonPacketStream; use crate::{EmptyReplyMsg, StreamId}; @@ -52,14 +52,14 @@ struct ThreadInterruptedReply { pub struct ThreadActor { pub name: String, - pub source_manager: Source, + pub source_manager: SourceManager, } impl ThreadActor { pub fn new(name: String) -> ThreadActor { ThreadActor { name: name.clone(), - source_manager: Source::new(name), + source_manager: SourceManager::new(), } } } @@ -124,14 +124,21 @@ impl Actor for ThreadActor { // Client has attached to the thread and wants to load script sources. // "sources" => { + let sources: Vec = self + .source_manager + .source_urls + .borrow() + .iter() + .cloned() + .collect(); + let msg = SourcesReply { from: self.name(), - sources: vec![], // TODO: Add sources for the debugger here + sources, }; let _ = stream.write_json_packet(&msg); ActorMessageStatus::Processed }, - _ => ActorMessageStatus::Ignored, }) } diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index a623a892323..d0b7e843faf 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -19,7 +19,6 @@ use std::net::{Shutdown, TcpListener, TcpStream}; use std::sync::{Arc, Mutex}; use std::thread; -use actors::source::SourceData; use base::id::{BrowsingContextId, PipelineId, WebViewId}; use crossbeam_channel::{Receiver, Sender, unbounded}; use devtools_traits::{ @@ -44,6 +43,7 @@ use crate::actors::performance::PerformanceActor; use crate::actors::preference::PreferenceActor; use crate::actors::process::ProcessActor; use crate::actors::root::RootActor; +use crate::actors::source::{SourceActor, SourceData}; use crate::actors::thread::ThreadActor; use crate::actors::worker::{WorkerActor, WorkerType}; use crate::id::IdMap; @@ -518,22 +518,31 @@ impl DevtoolsInstance { fn handle_script_source_info(&mut self, pipeline_id: PipelineId, source_info: SourceInfo) { let mut actors = self.actors.lock().unwrap(); + let source_actor_name = SourceActor::new_source( + &mut actors, + source_info.content.clone(), + source_info.content_type.unwrap(), + ); + if let Some(worker_id) = source_info.worker_id { let Some(worker_actor_name) = self.actor_workers.get(&worker_id) else { return; }; let thread_actor_name = actors.find::(worker_actor_name).thread.clone(); - let thread_actor = actors.find_mut::(&thread_actor_name); - thread_actor - .source_manager - .add_source(source_info.url.clone()); + + thread_actor.source_manager.add_source( + source_info.url.clone(), + source_info.content.clone(), + source_actor_name.clone(), + ); let source = SourceData { - actor: thread_actor_name.clone(), + actor: source_actor_name, url: source_info.url.to_string(), is_black_boxed: false, + source_content: source_info.content, }; let worker_actor = actors.find::(worker_actor_name); @@ -549,20 +558,24 @@ impl DevtoolsInstance { return; }; - let thread_actor_name = actors - .find::(actor_name) - .thread - .clone(); + let thread_actor_name = { + let browsing_context = actors.find::(actor_name); + browsing_context.thread.clone() + }; let thread_actor = actors.find_mut::(&thread_actor_name); - thread_actor - .source_manager - .add_source(source_info.url.clone()); + + thread_actor.source_manager.add_source( + source_info.url.clone(), + source_info.content.clone(), + source_actor_name.clone(), + ); let source = SourceData { - actor: thread_actor_name.clone(), + actor: source_actor_name, url: source_info.url.to_string(), is_black_boxed: false, + source_content: source_info.content, }; // Notify browsing context about the new source diff --git a/components/script/dom/dedicatedworkerglobalscope.rs b/components/script/dom/dedicatedworkerglobalscope.rs index 972a9d90948..00a00067e34 100644 --- a/components/script/dom/dedicatedworkerglobalscope.rs +++ b/components/script/dom/dedicatedworkerglobalscope.rs @@ -9,7 +9,7 @@ use std::thread::{self, JoinHandle}; use base::id::{BrowsingContextId, PipelineId, WebViewId}; use constellation_traits::{WorkerGlobalScopeInit, WorkerScriptLoadOrigin}; use crossbeam_channel::{Receiver, Sender, unbounded}; -use devtools_traits::DevtoolScriptControlMsg; +use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, SourceInfo}; use dom_struct::dom_struct; use ipc_channel::ipc::IpcReceiver; use ipc_channel::router::ROUTER; @@ -478,10 +478,24 @@ impl DedicatedWorkerGlobalScope { }, Ok((metadata, bytes)) => (metadata, bytes), }; - scope.set_url(metadata.final_url); + scope.set_url(metadata.final_url.clone()); scope.set_csp_list(GlobalScope::parse_csp_list_from_metadata(&metadata.headers)); global_scope.set_https_state(metadata.https_state); let source = String::from_utf8_lossy(&bytes); + if let Some(chan) = global_scope.devtools_chan() { + let pipeline_id = global_scope.pipeline_id(); + let source_info = SourceInfo { + url: metadata.final_url, + external: true, // Worker scripts are always external. + worker_id: Some(global.upcast::().get_worker_id()), + content: source.to_string(), + content_type: metadata.content_type.map(|c_type| c_type.0.to_string()), + }; + let _ = chan.send(ScriptToDevtoolsControlMsg::ScriptSourceLoaded( + pipeline_id, + source_info, + )); + } unsafe { // Handle interrupt requests diff --git a/components/script/dom/htmlscriptelement.rs b/components/script/dom/htmlscriptelement.rs index 2874cb07731..d2390d775e1 100644 --- a/components/script/dom/htmlscriptelement.rs +++ b/components/script/dom/htmlscriptelement.rs @@ -1020,7 +1020,6 @@ impl HTMLScriptElement { } // TODO: Step 3. Unblock rendering on el. - let mut script = match result { // Step 4. If el's result is null, then fire an event named error at el, and return. Err(e) => { @@ -1034,10 +1033,22 @@ impl HTMLScriptElement { if let Some(chan) = self.global().devtools_chan() { let pipeline_id = self.global().pipeline_id(); + + // TODO: https://github.com/servo/servo/issues/36874 + let content = match &script.code { + SourceCode::Text(text) => text.to_string(), + SourceCode::Compiled(compiled) => compiled.original_text.to_string(), + }; + + // https://html.spec.whatwg.org/multipage/#scriptingLanguages + let content_type = Some("text/javascript".to_string()); + let source_info = SourceInfo { url: script.url.clone(), external: script.external, worker_id: None, + content, + content_type, }; let _ = chan.send(ScriptToDevtoolsControlMsg::ScriptSourceLoaded( pipeline_id, diff --git a/components/script/dom/worker.rs b/components/script/dom/worker.rs index 7a6b226cf07..9f555283a82 100644 --- a/components/script/dom/worker.rs +++ b/components/script/dom/worker.rs @@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use constellation_traits::{StructuredSerializedData, WorkerScriptLoadOrigin}; use crossbeam_channel::{Sender, unbounded}; -use devtools_traits::{DevtoolsPageInfo, ScriptToDevtoolsControlMsg, SourceInfo, WorkerId}; +use devtools_traits::{DevtoolsPageInfo, ScriptToDevtoolsControlMsg, WorkerId}; use dom_struct::dom_struct; use ipc_channel::ipc; use js::jsapi::{Heap, JSObject}; @@ -224,16 +224,6 @@ impl WorkerMethods for Worker { devtools_sender.clone(), page_info, )); - - let source_info = SourceInfo { - url: worker_url.clone(), - external: true, // Worker scripts are always external. - worker_id: Some(worker_id), - }; - let _ = chan.send(ScriptToDevtoolsControlMsg::ScriptSourceLoaded( - pipeline_id, - source_info, - )); } } diff --git a/components/shared/devtools/lib.rs b/components/shared/devtools/lib.rs index 628d76bd25d..815ad087dd7 100644 --- a/components/shared/devtools/lib.rs +++ b/components/shared/devtools/lib.rs @@ -563,4 +563,6 @@ pub struct SourceInfo { pub url: ServoUrl, pub external: bool, pub worker_id: Option, + pub content: String, + pub content_type: Option, } diff --git a/python/servo/devtools_tests.py b/python/servo/devtools_tests.py index 31127481707..f505639a534 100644 --- a/python/servo/devtools_tests.py +++ b/python/servo/devtools_tests.py @@ -60,6 +60,7 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): f"{self.base_url}/classic.js", f"{self.base_url}/test.html", "https://servo.org/js/load-table.js", + f"{self.base_url}/test.html", ] ), tuple([f"{self.base_url}/worker.js"]), @@ -92,6 +93,17 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): self.run_servoshell(url="data:text/html,") self.assert_sources_list(1, set([tuple(["data:text/html,"])])) + def test_source_content_inline_script(self): + script_content = "console.log('Hello, world!');" + self.run_servoshell(url=f"data:text/html,") + self.assert_source_content("data:text/html,", script_content) + + def test_source_content_external_script(self): + self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) + self.run_servoshell(url=f'data:text/html,') + expected_content = 'console.log("external classic");\n' + self.assert_source_content(f"{self.base_url}/classic.js", expected_content) + # Sets `base_url` and `web_server` and `web_server_thread`. def start_web_server(self, *, test_dir=None): if test_dir is None: @@ -145,7 +157,7 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): if self.base_url is not None: self.base_url = None - def assert_sources_list(self, expected_targets: int, expected_urls_by_target: set[tuple[str]]): + def _setup_devtools_client(self, expected_targets=1): client = RDPClient() client.connect("127.0.0.1", 6080) root = RootActor(client) @@ -179,6 +191,11 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): result: Optional[Exception] = done.result(1) if result: raise result + + return client, watcher, targets + + def assert_sources_list(self, expected_targets: int, expected_urls_by_target: set[tuple[str]]): + client, watcher, targets = self._setup_devtools_client(expected_targets) done = Future() # NOTE: breaks if two targets have the same list of source urls. # This should really be a multiset, but Python does not have multisets. @@ -212,6 +229,45 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(actual_urls_by_target, expected_urls_by_target) client.disconnect() + def assert_source_content(self, source_url: str, expected_content: str): + client, watcher, targets = self._setup_devtools_client() + + done = Future() + source_actors = {} + + def on_source_resource(data): + for [resource_type, sources] in data["array"]: + try: + self.assertEqual(resource_type, "source") + for source in sources: + if source["url"] == source_url: + source_actors[source_url] = source["actor"] + done.set_result(None) + except Exception as e: + done.set_result(e) + + for target in targets: + client.add_event_listener( + target["actor"], + Events.Watcher.RESOURCES_AVAILABLE_ARRAY, + on_source_resource, + ) + watcher.watch_resources([Resources.SOURCE]) + + result: Optional[Exception] = done.result(1) + if result: + raise result + + # We found at least one source with the given url. + self.assertIn(source_url, source_actors) + source_actor = source_actors[source_url] + + response = client.send_receive({"to": source_actor, "type": "source"}) + + self.assertEqual(response["source"], expected_content) + + client.disconnect() + def run_tests(script_path): DevtoolsTests.script_path = script_path