1use std::collections::HashMap;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12
13use atomic_refcell::AtomicRefCell;
14use devtools_traits::{
15 ConsoleMessage, ConsoleMessageFields, DevtoolScriptControlMsg, GetEnvironmentRequest,
16 PageError, StackFrame, get_time_stamp,
17};
18use malloc_size_of_derive::MallocSizeOf;
19use serde::Serialize;
20use serde_json::{self, Map, Value};
21use servo_base::generic_channel::{self, GenericSender, channel};
22use servo_base::id::{PipelineId, TEST_PIPELINE_ID};
23use uuid::Uuid;
24
25use crate::actor::{Actor, ActorError, ActorRegistry};
26use crate::actors::browsing_context::BrowsingContextActor;
27use crate::actors::environment::EnvironmentActor;
28use crate::actors::worker::WorkerTargetActor;
29use crate::protocol::{ClientRequest, DevtoolsConnection, JsonPacketStream};
30use crate::resource::{ResourceArrayType, ResourceAvailable};
31use crate::{EmptyReplyMsg, StreamId, UniqueId, debugger_value_to_json};
32
33#[derive(Clone, Serialize, MallocSizeOf)]
34#[serde(rename_all = "camelCase")]
35pub(crate) struct DevtoolsConsoleMessage {
36 #[serde(flatten)]
37 fields: ConsoleMessageFields,
38 #[ignore_malloc_size_of = "Currently no way to have serde_json::Value"]
39 arguments: Vec<Value>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 stacktrace: Option<Vec<StackFrame>>,
42 }
46
47impl DevtoolsConsoleMessage {
48 pub(crate) fn new(message: ConsoleMessage, registry: &ActorRegistry) -> Self {
49 Self {
50 fields: message.fields,
51 arguments: message
52 .arguments
53 .into_iter()
54 .map(|argument| debugger_value_to_json(registry, argument))
55 .collect(),
56 stacktrace: message.stacktrace,
57 }
58 }
59}
60
61#[derive(Clone, Serialize, MallocSizeOf)]
62#[serde(rename_all = "camelCase")]
63struct DevtoolsPageError {
64 #[serde(flatten)]
65 page_error: PageError,
66 category: String,
67 error: bool,
68 warning: bool,
69 info: bool,
70 private: bool,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 stacktrace: Option<Vec<StackFrame>>,
73 }
79
80impl From<PageError> for DevtoolsPageError {
81 fn from(page_error: PageError) -> Self {
82 Self {
83 page_error,
84 category: "script".to_string(),
85 error: true,
86 warning: false,
87 info: false,
88 private: false,
89 stacktrace: None,
90 }
91 }
92}
93#[derive(Clone, Serialize, MallocSizeOf)]
94#[serde(rename_all = "camelCase")]
95pub(crate) struct PageErrorWrapper {
96 page_error: DevtoolsPageError,
97}
98
99impl From<PageError> for PageErrorWrapper {
100 fn from(page_error: PageError) -> Self {
101 Self {
102 page_error: page_error.into(),
103 }
104 }
105}
106
107#[derive(Clone, Serialize, MallocSizeOf)]
108#[serde(untagged)]
109pub(crate) enum ConsoleResource {
110 ConsoleMessage(DevtoolsConsoleMessage),
111 PageError(PageErrorWrapper),
112}
113
114impl ConsoleResource {
115 pub fn resource_type(&self) -> String {
116 match self {
117 ConsoleResource::ConsoleMessage(_) => "console-message".into(),
118 ConsoleResource::PageError(_) => "error-message".into(),
119 }
120 }
121}
122
123#[derive(Serialize)]
124pub struct ConsoleClearMessage {
125 pub level: String,
126}
127
128#[derive(Serialize)]
129#[serde(rename_all = "camelCase")]
130struct AutocompleteReply {
131 from: String,
132 matches: Vec<String>,
133 match_prop: String,
134 is_element_access: bool,
135}
136
137#[derive(Serialize)]
138#[serde(rename_all = "camelCase")]
139struct EvaluateJSReply {
140 from: String,
141 input: String,
142 result: Value,
143 timestamp: u64,
144 exception: Value,
145 exception_message: Option<String>,
146 has_exception: bool,
147 helper_result: Value,
148}
149
150#[derive(Serialize)]
151#[serde(rename_all = "camelCase")]
152struct EvaluateJSEvent {
153 from: String,
154 #[serde(rename = "type")]
155 type_: String,
156 input: String,
157 result: Value,
158 timestamp: u64,
159 #[serde(rename = "resultID")]
160 result_id: String,
161 exception: Value,
162 exception_message: Option<String>,
163 has_exception: bool,
164 helper_result: Value,
165}
166
167#[derive(Serialize)]
168struct EvaluateJSAsyncReply {
169 from: String,
170 #[serde(rename = "resultID")]
171 result_id: String,
172}
173
174#[derive(Serialize)]
175struct SetPreferencesReply {
176 from: String,
177 updated: Vec<String>,
178}
179
180#[derive(MallocSizeOf)]
181pub(crate) enum Root {
182 BrowsingContext(String),
183 DedicatedWorker(String),
184}
185
186#[derive(MallocSizeOf)]
187pub(crate) struct ConsoleActor {
188 name: String,
189 root: Root,
190 cached_events: AtomicRefCell<HashMap<UniqueId, Vec<ConsoleResource>>>,
191 client_ready_to_receive_messages: AtomicBool,
197}
198
199impl ConsoleActor {
200 pub fn register(registry: &ActorRegistry, name: String, root: Root) -> Arc<Self> {
201 let actor = Self {
202 name,
203 root,
204 cached_events: Default::default(),
205 client_ready_to_receive_messages: false.into(),
206 };
207 registry.register::<Self>(actor)
208 }
209
210 fn script_chan(&self, registry: &ActorRegistry) -> GenericSender<DevtoolScriptControlMsg> {
211 match &self.root {
212 Root::BrowsingContext(browsing_context_name) => registry
213 .find::<BrowsingContextActor>(browsing_context_name)
214 .script_chan(),
215 Root::DedicatedWorker(worker_name) => registry
216 .find::<WorkerTargetActor>(worker_name)
217 .script_sender
218 .clone(),
219 }
220 }
221
222 fn current_unique_id(&self, registry: &ActorRegistry) -> UniqueId {
223 match &self.root {
224 Root::BrowsingContext(browsing_context_name) => UniqueId::Pipeline(
225 registry
226 .find::<BrowsingContextActor>(browsing_context_name)
227 .pipeline_id(),
228 ),
229 Root::DedicatedWorker(worker_name) => {
230 UniqueId::Worker(registry.find::<WorkerTargetActor>(worker_name).worker_id)
231 },
232 }
233 }
234
235 fn pipeline_id(&self, registry: &ActorRegistry) -> PipelineId {
236 match self.current_unique_id(registry) {
238 UniqueId::Pipeline(p) => p,
239 UniqueId::Worker(_) => TEST_PIPELINE_ID,
240 }
241 }
242
243 fn evaluate_js(
244 &self,
245 registry: &ActorRegistry,
246 msg: &Map<String, Value>,
247 ) -> Result<EvaluateJSReply, ()> {
248 let input = msg.get("text").unwrap().as_str().unwrap().to_owned();
249 let eager = msg.get("eager").and_then(|v| v.as_bool()).unwrap_or(false);
250 let frame_actor_id = msg
251 .get("frameActor")
252 .and_then(|v| v.as_str())
253 .map(String::from);
254
255 let (chan, port) = generic_channel::channel().unwrap();
256 self.script_chan(registry)
257 .send(DevtoolScriptControlMsg::Eval(
258 input.clone(),
259 self.pipeline_id(registry),
260 frame_actor_id,
261 eager,
262 chan,
263 ))
264 .unwrap();
265
266 let eval_result = port.recv().map_err(|_| ())?;
267 let has_exception = eval_result.has_exception;
268 let (result, exception) = if has_exception {
269 (
270 Value::Null,
271 debugger_value_to_json(registry, eval_result.value),
272 )
273 } else {
274 (
275 debugger_value_to_json(registry, eval_result.value),
276 Value::Null,
277 )
278 };
279 let reply = EvaluateJSReply {
280 from: self.name().into(),
281 input,
282 result,
283 timestamp: get_time_stamp(),
284 exception,
285 exception_message: eval_result.exception_message,
286 has_exception,
287 helper_result: Value::Null,
288 };
289 Ok(reply)
290 }
291
292 pub(crate) fn handle_console_resource(
293 &self,
294 resource: ConsoleResource,
295 id: UniqueId,
296 registry: &ActorRegistry,
297 stream: &mut DevtoolsConnection,
298 ) {
299 self.cached_events
300 .borrow_mut()
301 .entry(id.clone())
302 .or_default()
303 .push(resource.clone());
304 if !self
305 .client_ready_to_receive_messages
306 .load(Ordering::Relaxed)
307 {
308 return;
309 }
310 let resource_type = resource.resource_type();
311 if id == self.current_unique_id(registry) &&
312 let Root::BrowsingContext(browsing_context_name) = &self.root
313 {
314 registry
315 .find::<BrowsingContextActor>(browsing_context_name)
316 .resource_array(
317 resource,
318 resource_type,
319 ResourceArrayType::Available,
320 stream,
321 )
322 };
323 }
324
325 pub(crate) fn send_clear_message(
326 &self,
327 id: UniqueId,
328 registry: &ActorRegistry,
329 stream: &mut DevtoolsConnection,
330 ) {
331 if id == self.current_unique_id(registry) &&
332 let Root::BrowsingContext(browsing_context_name) = &self.root
333 {
334 registry
335 .find::<BrowsingContextActor>(browsing_context_name)
336 .resource_array(
337 ConsoleClearMessage {
338 level: "clear".to_owned(),
339 },
340 "console-message".into(),
341 ResourceArrayType::Available,
342 stream,
343 )
344 };
345 }
346
347 pub(crate) fn get_cached_messages(
348 &self,
349 registry: &ActorRegistry,
350 resource: &str,
351 ) -> Vec<ConsoleResource> {
352 let id = self.current_unique_id(registry);
353 let cached_events = self.cached_events.borrow();
354 let Some(events) = cached_events.get(&id) else {
355 return vec![];
356 };
357 events
358 .iter()
359 .filter(|event| event.resource_type() == resource)
360 .cloned()
361 .collect()
362 }
363
364 pub(crate) fn received_first_message_from_client(&self) {
365 self.client_ready_to_receive_messages
366 .store(true, Ordering::Relaxed);
367 }
368
369 pub(crate) fn autocomplete_match(prefix: &str, identifier: &str) -> bool {
372 let is_insensitive = prefix.chars().next().is_some_and(|c| c.is_lowercase());
373 if is_insensitive {
374 identifier
375 .to_lowercase()
376 .starts_with(&prefix.to_lowercase())
377 } else {
378 identifier.starts_with(prefix)
379 }
380 }
381}
382
383impl Actor for ConsoleActor {
384 fn name(&self) -> &str {
385 &self.name
386 }
387
388 fn handle_message(
389 &self,
390 request: ClientRequest,
391 registry: &ActorRegistry,
392 msg_type: &str,
393 msg: &Map<String, Value>,
394 _id: StreamId,
395 ) -> Result<(), ActorError> {
396 match msg_type {
397 "clearMessagesCacheAsync" => {
398 self.cached_events
399 .borrow_mut()
400 .remove(&self.current_unique_id(registry));
401 let msg = EmptyReplyMsg {
402 from: self.name().into(),
403 };
404 request.reply_final(&msg)?
405 },
406
407 "autocomplete" => {
408 let Some((tx, rx)) = channel() else {
409 return Err(ActorError::Internal);
410 };
411
412 let env_request = if let Some(frame_actor) = msg
413 .get("frameActor")
414 .and_then(|v| v.as_str())
415 .map(String::from)
416 {
417 GetEnvironmentRequest::Frame(frame_actor)
418 } else {
419 GetEnvironmentRequest::Global(self.pipeline_id(registry))
420 };
421
422 self.script_chan(registry)
423 .send(DevtoolScriptControlMsg::GetEnvironment(env_request, tx))
424 .map_err(|_| ActorError::Internal)?;
425
426 let environment_name = rx.recv().map_err(|_| ActorError::Internal)?;
427 let environment_actor = registry.find::<EnvironmentActor>(&environment_name);
428
429 let prompt = msg
430 .get("text")
431 .and_then(|v| v.as_str())
432 .map(String::from)
433 .ok_or(ActorError::Internal)?;
434
435 let identifiers = environment_actor.search_identifiers_recursive(registry, &prompt);
436
437 let matches: Vec<String> = identifiers
438 .into_iter()
439 .filter(|name| ConsoleActor::autocomplete_match(&prompt, name))
440 .collect();
441
442 let msg = if matches.is_empty() {
443 AutocompleteReply {
444 from: self.name().into(),
445 matches: vec![],
446 match_prop: String::new(),
447 is_element_access: false,
448 }
449 } else {
450 AutocompleteReply {
451 from: self.name().into(),
452 matches,
453 match_prop: prompt,
454 is_element_access: false,
455 }
456 };
457 request.reply_final(&msg)?
458 },
459
460 "evaluateJS" => {
461 let msg = self.evaluate_js(registry, msg);
462 request.reply_final(&msg)?
463 },
464
465 "evaluateJSAsync" => {
466 let result_id = Uuid::new_v4().to_string();
467 let early_reply = EvaluateJSAsyncReply {
468 from: self.name().into(),
469 result_id: result_id.clone(),
470 };
471 let mut stream = request.reply(&early_reply)?;
474
475 let reply = self.evaluate_js(registry, msg).unwrap();
476 let msg = EvaluateJSEvent {
477 from: self.name().into(),
478 type_: "evaluationResult".to_owned(),
479 input: reply.input,
480 result: reply.result,
481 timestamp: reply.timestamp,
482 result_id,
483 exception: reply.exception,
484 exception_message: reply.exception_message,
485 has_exception: reply.has_exception,
486 helper_result: reply.helper_result,
487 };
488 stream.write_json_packet(&msg)?
490 },
491
492 "setPreferences" => {
493 let msg = SetPreferencesReply {
494 from: self.name().into(),
495 updated: vec![],
496 };
497 request.reply_final(&msg)?
498 },
499
500 _ => return Err(ActorError::UnrecognizedPacketType),
504 };
505 Ok(())
506 }
507}