mirror of
https://github.com/servo/servo.git
synced 2025-08-03 12:40:06 +01:00
DevTools: Implement support for showing source_content
in Debugger > Source
panel (#36774)
This patch adds support for showing source_content in `Debugger > Source` panel. This works by handling the clients `source` messages in the source actor. These source actors are already advertised as resource via the watcher, populating the source list. We also update the `sources` handler in thread actor for future work in thread debugging. Note: while this PR also adds support for showing worker script source_content, worker has been broken (See https://github.com/servo/servo/issues/37012). I was able to confirm the `content_type` and `source_content` for worker script in logs.  Fixes: part of https://github.com/servo/servo/issues/36027 --------- Signed-off-by: atbrakhi <atbrakhi@igalia.com>
This commit is contained in:
parent
5159529888
commit
7a801f0ef5
8 changed files with 204 additions and 42 deletions
|
@ -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<SourceData>,
|
||||
}
|
||||
|
||||
pub(crate) struct Source {
|
||||
actor_name: String,
|
||||
source_urls: RefCell<BTreeSet<SourceData>>,
|
||||
pub(crate) struct SourceManager {
|
||||
pub source_urls: RefCell<BTreeSet<SourceData>>,
|
||||
}
|
||||
|
||||
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<String, Value>,
|
||||
stream: &mut TcpStream,
|
||||
_id: StreamId,
|
||||
) -> Result<ActorMessageStatus, ()> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#loading-script-sources>
|
||||
"sources" => {
|
||||
let sources: Vec<SourceData> = 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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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::<WorkerActor>(worker_actor_name).thread.clone();
|
||||
|
||||
let thread_actor = actors.find_mut::<ThreadActor>(&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::<WorkerActor>(worker_actor_name);
|
||||
|
@ -549,20 +558,24 @@ impl DevtoolsInstance {
|
|||
return;
|
||||
};
|
||||
|
||||
let thread_actor_name = actors
|
||||
.find::<BrowsingContextActor>(actor_name)
|
||||
.thread
|
||||
.clone();
|
||||
let thread_actor_name = {
|
||||
let browsing_context = actors.find::<BrowsingContextActor>(actor_name);
|
||||
browsing_context.thread.clone()
|
||||
};
|
||||
|
||||
let thread_actor = actors.find_mut::<ThreadActor>(&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
|
||||
|
|
|
@ -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::<WorkerGlobalScope>().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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<crate::DomTypeHolder> 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -563,4 +563,6 @@ pub struct SourceInfo {
|
|||
pub url: ServoUrl,
|
||||
pub external: bool,
|
||||
pub worker_id: Option<WorkerId>,
|
||||
pub content: String,
|
||||
pub content_type: Option<String>,
|
||||
}
|
||||
|
|
|
@ -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,<script type=module>;</script>")
|
||||
self.assert_sources_list(1, set([tuple(["data:text/html,<script type=module>;</script>"])]))
|
||||
|
||||
def test_source_content_inline_script(self):
|
||||
script_content = "console.log('Hello, world!');"
|
||||
self.run_servoshell(url=f"data:text/html,<script>{script_content}</script>")
|
||||
self.assert_source_content("data:text/html,<script>console.log('Hello, world!');</script>", 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,<script src="{self.base_url}/classic.js"></script>')
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue