devtools/
lib.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! An actor-based remote devtools server implementation. Only tested with
6//! nightly Firefox versions at time of writing. Largely based on
7//! reverse-engineering of Firefox chrome devtool logs and reading of
8//! [code](https://searchfox.org/mozilla-central/source/devtools/server).
9
10#![crate_name = "devtools"]
11#![crate_type = "rlib"]
12#![deny(unsafe_code)]
13
14use std::borrow::ToOwned;
15use std::collections::HashMap;
16use std::io::Read;
17use std::net::{Shutdown, TcpListener, TcpStream};
18use std::sync::{Arc, Mutex};
19use std::thread;
20
21use base::generic_channel;
22use base::id::{BrowsingContextId, PipelineId, WebViewId};
23use crossbeam_channel::{Receiver, Sender, unbounded};
24use devtools_traits::{
25    ChromeToDevtoolsControlMsg, ConsoleMessage, ConsoleMessageBuilder, DevtoolScriptControlMsg,
26    DevtoolsControlMsg, DevtoolsPageInfo, LogLevel, NavigationState, NetworkEvent, PageError,
27    ScriptToDevtoolsControlMsg, SourceInfo, WorkerId,
28};
29use embedder_traits::{AllowOrDeny, EmbedderMsg, EmbedderProxy};
30use ipc_channel::ipc::IpcSender;
31use log::{trace, warn};
32use rand::{RngCore, rng};
33use resource::{ResourceArrayType, ResourceAvailable};
34use rustc_hash::FxHashMap;
35use serde::Serialize;
36
37use crate::actor::{Actor, ActorRegistry};
38use crate::actors::browsing_context::BrowsingContextActor;
39use crate::actors::console::{ConsoleActor, Root};
40use crate::actors::device::DeviceActor;
41use crate::actors::framerate::FramerateActor;
42use crate::actors::network_event::NetworkEventActor;
43use crate::actors::performance::PerformanceActor;
44use crate::actors::preference::PreferenceActor;
45use crate::actors::process::ProcessActor;
46use crate::actors::root::RootActor;
47use crate::actors::source::SourceActor;
48use crate::actors::thread::ThreadActor;
49use crate::actors::watcher::WatcherActor;
50use crate::actors::worker::{WorkerActor, WorkerType};
51use crate::id::IdMap;
52use crate::network_handler::handle_network_event;
53use crate::protocol::JsonPacketStream;
54
55mod actor;
56/// <https://searchfox.org/mozilla-central/source/devtools/server/actors>
57mod actors {
58    pub mod breakpoint;
59    pub mod browsing_context;
60    pub mod console;
61    pub mod device;
62    pub mod environment;
63    pub mod frame;
64    pub mod framerate;
65    pub mod inspector;
66    pub mod long_string;
67    pub mod memory;
68    pub mod network_event;
69    pub mod object;
70    pub mod pause;
71    pub mod performance;
72    pub mod preference;
73    pub mod process;
74    pub mod reflow;
75    pub mod root;
76    pub mod source;
77    pub mod stylesheets;
78    pub mod tab;
79    pub mod thread;
80    pub mod timeline;
81    pub mod watcher;
82    pub mod worker;
83}
84mod id;
85mod network_handler;
86mod protocol;
87mod resource;
88
89#[derive(Clone, Debug, Eq, Hash, PartialEq)]
90enum UniqueId {
91    Pipeline(PipelineId),
92    Worker(WorkerId),
93}
94
95#[derive(Serialize)]
96pub struct EmptyReplyMsg {
97    pub from: String,
98}
99
100/// Spin up a devtools server that listens for connections on the specified port.
101pub fn start_server(port: u16, embedder: EmbedderProxy) -> Sender<DevtoolsControlMsg> {
102    let (sender, receiver) = unbounded();
103    {
104        let sender = sender.clone();
105        thread::Builder::new()
106            .name("Devtools".to_owned())
107            .spawn(move || {
108                if let Some(instance) = DevtoolsInstance::create(sender, receiver, port, embedder) {
109                    instance.run()
110                }
111            })
112            .expect("Thread spawning failed");
113    }
114    sender
115}
116
117#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
118pub(crate) struct StreamId(u32);
119
120struct DevtoolsInstance {
121    actors: Arc<Mutex<ActorRegistry>>,
122    id_map: Arc<Mutex<IdMap>>,
123    browsing_contexts: FxHashMap<BrowsingContextId, String>,
124    receiver: Receiver<DevtoolsControlMsg>,
125    pipelines: FxHashMap<PipelineId, BrowsingContextId>,
126    actor_workers: FxHashMap<WorkerId, String>,
127    actor_requests: HashMap<String, String>,
128    connections: FxHashMap<StreamId, TcpStream>,
129    next_resource_id: u64,
130}
131
132impl DevtoolsInstance {
133    fn create(
134        sender: Sender<DevtoolsControlMsg>,
135        receiver: Receiver<DevtoolsControlMsg>,
136        port: u16,
137        embedder: EmbedderProxy,
138    ) -> Option<Self> {
139        let bound = TcpListener::bind(("0.0.0.0", port)).ok().and_then(|l| {
140            l.local_addr()
141                .map(|addr| addr.port())
142                .ok()
143                .map(|port| (l, port))
144        });
145
146        // A token shared with the embedder to bypass permission prompt.
147        let port = if bound.is_some() { Ok(port) } else { Err(()) };
148        let token = format!("{:X}", rng().next_u32());
149        embedder.send(EmbedderMsg::OnDevtoolsStarted(port, token.clone()));
150
151        let listener = match bound {
152            Some((l, _)) => l,
153            None => {
154                return None;
155            },
156        };
157
158        // Create basic actors
159        let mut registry = ActorRegistry::new();
160        let performance = PerformanceActor::new(registry.new_name("performance"));
161        let device = DeviceActor::new(registry.new_name("device"));
162        let preference = PreferenceActor::new(registry.new_name("preference"));
163        let process = ProcessActor::new(registry.new_name("process"));
164        let root = Box::new(RootActor {
165            tabs: vec![],
166            workers: vec![],
167            device: device.name(),
168            performance: performance.name(),
169            preference: preference.name(),
170            process: process.name(),
171            active_tab: None.into(),
172        });
173
174        registry.register(root);
175        registry.register(Box::new(performance));
176        registry.register(Box::new(device));
177        registry.register(Box::new(preference));
178        registry.register(Box::new(process));
179        registry.find::<RootActor>("root");
180
181        let actors = registry.create_shareable();
182
183        let instance = Self {
184            actors,
185            id_map: Arc::new(Mutex::new(IdMap::default())),
186            browsing_contexts: FxHashMap::default(),
187            pipelines: FxHashMap::default(),
188            receiver,
189            actor_requests: HashMap::new(),
190            actor_workers: FxHashMap::default(),
191            connections: FxHashMap::default(),
192            next_resource_id: 1,
193        };
194
195        thread::Builder::new()
196            .name("DevtoolsCliAcceptor".to_owned())
197            .spawn(move || {
198                // accept connections and process them, spawning a new thread for each one
199                for stream in listener.incoming() {
200                    let mut stream = stream.expect("Can't retrieve stream");
201                    if !allow_devtools_client(&mut stream, &embedder, &token) {
202                        continue;
203                    };
204                    // connection succeeded and accepted
205                    sender
206                        .send(DevtoolsControlMsg::FromChrome(
207                            ChromeToDevtoolsControlMsg::AddClient(stream),
208                        ))
209                        .unwrap();
210                }
211            })
212            .expect("Thread spawning failed");
213
214        Some(instance)
215    }
216
217    fn run(mut self) {
218        let mut next_id = StreamId(0);
219        while let Ok(msg) = self.receiver.recv() {
220            trace!("{:?}", msg);
221            match msg {
222                DevtoolsControlMsg::FromChrome(ChromeToDevtoolsControlMsg::AddClient(stream)) => {
223                    let actors = self.actors.clone();
224                    let id = next_id;
225                    next_id = StreamId(id.0 + 1);
226                    self.connections.insert(id, stream.try_clone().unwrap());
227
228                    // Inform every browsing context of the new stream
229                    for name in self.browsing_contexts.values() {
230                        let actors = actors.lock().unwrap();
231                        let browsing_context = actors.find::<BrowsingContextActor>(name);
232                        let mut streams = browsing_context.streams.borrow_mut();
233                        streams.insert(id, stream.try_clone().unwrap());
234                    }
235
236                    thread::Builder::new()
237                        .name("DevtoolsClientHandler".to_owned())
238                        .spawn(move || handle_client(actors, stream.try_clone().unwrap(), id))
239                        .expect("Thread spawning failed");
240                },
241                DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::FramerateTick(
242                    actor_name,
243                    tick,
244                )) => self.handle_framerate_tick(actor_name, tick),
245                DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::TitleChanged(
246                    pipeline,
247                    title,
248                )) => self.handle_title_changed(pipeline, title),
249                DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::NewGlobal(
250                    ids,
251                    script_sender,
252                    pageinfo,
253                )) => self.handle_new_global(ids, script_sender, pageinfo),
254                DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::Navigate(
255                    browsing_context,
256                    state,
257                )) => self.handle_navigate(browsing_context, state),
258                DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::ConsoleAPI(
259                    pipeline_id,
260                    console_message,
261                    worker_id,
262                )) => self.handle_console_message(pipeline_id, worker_id, console_message),
263                DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::CreateSourceActor(
264                    script_sender,
265                    pipeline_id,
266                    source_info,
267                )) => self.handle_create_source_actor(script_sender, pipeline_id, source_info),
268                DevtoolsControlMsg::FromScript(
269                    ScriptToDevtoolsControlMsg::UpdateSourceContent(pipeline_id, source_content),
270                ) => self.handle_update_source_content(pipeline_id, source_content),
271                DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::ReportPageError(
272                    pipeline_id,
273                    page_error,
274                )) => self.handle_page_error(pipeline_id, None, page_error),
275                DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::ReportCSSError(
276                    pipeline_id,
277                    css_error,
278                )) => {
279                    let mut console_message = ConsoleMessageBuilder::new(
280                        LogLevel::Warn,
281                        css_error.filename,
282                        css_error.line,
283                        css_error.column,
284                    );
285                    console_message.add_argument(css_error.msg.into());
286
287                    self.handle_console_message(pipeline_id, None, console_message.finish())
288                },
289                DevtoolsControlMsg::FromChrome(ChromeToDevtoolsControlMsg::NetworkEvent(
290                    request_id,
291                    network_event,
292                )) => {
293                    // copy the connections vector
294                    let mut connections = Vec::<TcpStream>::new();
295                    for stream in self.connections.values() {
296                        connections.push(stream.try_clone().unwrap());
297                    }
298
299                    self.handle_network_event(connections, request_id, network_event);
300                },
301                DevtoolsControlMsg::FromChrome(ChromeToDevtoolsControlMsg::ServerExitMsg) => break,
302            }
303        }
304
305        // Shut down all active connections
306        for connection in self.connections.values_mut() {
307            let _ = connection.shutdown(Shutdown::Both);
308        }
309    }
310
311    fn handle_framerate_tick(&self, actor_name: String, tick: f64) {
312        let mut actors = self.actors.lock().unwrap();
313        let framerate_actor = actors.find_mut::<FramerateActor>(&actor_name);
314        framerate_actor.add_tick(tick);
315    }
316
317    fn handle_navigate(&self, browsing_context_id: BrowsingContextId, state: NavigationState) {
318        let actor_name = self.browsing_contexts.get(&browsing_context_id).unwrap();
319        let actors = self.actors.lock().unwrap();
320        let actor = actors.find::<BrowsingContextActor>(actor_name);
321        let mut id_map = self.id_map.lock().expect("Mutex poisoned");
322        if let NavigationState::Start(url) = &state {
323            let mut connections = Vec::<TcpStream>::new();
324            for stream in self.connections.values() {
325                connections.push(stream.try_clone().unwrap());
326            }
327            let watcher_actor = actors.find::<WatcherActor>(&actor.watcher);
328            watcher_actor.emit_will_navigate(
329                browsing_context_id,
330                url.clone(),
331                &mut connections,
332                &mut id_map,
333            );
334        };
335        actor.navigate(state, &mut id_map);
336    }
337
338    // We need separate actor representations for each script global that exists;
339    // clients can theoretically connect to multiple globals simultaneously.
340    // TODO: move this into the root or target modules?
341    fn handle_new_global(
342        &mut self,
343        ids: (BrowsingContextId, PipelineId, Option<WorkerId>, WebViewId),
344        script_sender: IpcSender<DevtoolScriptControlMsg>,
345        page_info: DevtoolsPageInfo,
346    ) {
347        let mut actors = self.actors.lock().unwrap();
348
349        let (browsing_context_id, pipeline_id, worker_id, webview_id) = ids;
350        let id_map = &mut self.id_map.lock().expect("Mutex poisoned");
351        let devtools_browser_id = id_map.browser_id(webview_id);
352        let devtools_browsing_context_id = id_map.browsing_context_id(browsing_context_id);
353        let devtools_outer_window_id = id_map.outer_window_id(pipeline_id);
354
355        let console_name = actors.new_name("console");
356
357        let parent_actor = if let Some(id) = worker_id {
358            assert!(self.pipelines.contains_key(&pipeline_id));
359            assert!(self.browsing_contexts.contains_key(&browsing_context_id));
360
361            let thread = ThreadActor::new(actors.new_name("thread"));
362            let thread_name = thread.name();
363            actors.register(Box::new(thread));
364
365            let worker_name = actors.new_name("worker");
366            let worker = WorkerActor {
367                name: worker_name.clone(),
368                console: console_name.clone(),
369                thread: thread_name,
370                worker_id: id,
371                url: page_info.url.clone(),
372                type_: WorkerType::Dedicated,
373                script_chan: script_sender,
374                streams: Default::default(),
375            };
376            let root = actors.find_mut::<RootActor>("root");
377            root.workers.push(worker.name.clone());
378
379            self.actor_workers.insert(id, worker_name.clone());
380            actors.register(Box::new(worker));
381
382            Root::DedicatedWorker(worker_name)
383        } else {
384            self.pipelines.insert(pipeline_id, browsing_context_id);
385            let name = self
386                .browsing_contexts
387                .entry(browsing_context_id)
388                .or_insert_with(|| {
389                    let browsing_context_actor = BrowsingContextActor::new(
390                        console_name.clone(),
391                        devtools_browser_id,
392                        devtools_browsing_context_id,
393                        page_info,
394                        pipeline_id,
395                        devtools_outer_window_id,
396                        script_sender,
397                        &mut actors,
398                    );
399                    let name = browsing_context_actor.name();
400                    actors.register(Box::new(browsing_context_actor));
401                    name
402                });
403
404            // Add existing streams to the new browsing context
405            let browsing_context = actors.find::<BrowsingContextActor>(name);
406            let mut streams = browsing_context.streams.borrow_mut();
407            for (id, stream) in &self.connections {
408                streams.insert(*id, stream.try_clone().unwrap());
409            }
410
411            Root::BrowsingContext(name.clone())
412        };
413
414        let console = ConsoleActor {
415            name: console_name,
416            cached_events: Default::default(),
417            root: parent_actor,
418        };
419
420        actors.register(Box::new(console));
421    }
422
423    fn handle_title_changed(&self, pipeline_id: PipelineId, title: String) {
424        let bc = match self.pipelines.get(&pipeline_id) {
425            Some(bc) => bc,
426            None => return,
427        };
428        let name = match self.browsing_contexts.get(bc) {
429            Some(name) => name,
430            None => return,
431        };
432        let actors = self.actors.lock().unwrap();
433        let browsing_context = actors.find::<BrowsingContextActor>(name);
434        browsing_context.title_changed(pipeline_id, title);
435    }
436
437    fn handle_page_error(
438        &mut self,
439        pipeline_id: PipelineId,
440        worker_id: Option<WorkerId>,
441        page_error: PageError,
442    ) {
443        let console_actor_name = match self.find_console_actor(pipeline_id, worker_id) {
444            Some(name) => name,
445            None => return,
446        };
447        let actors = self.actors.lock().unwrap();
448        let console_actor = actors.find::<ConsoleActor>(&console_actor_name);
449        let id = worker_id.map_or(UniqueId::Pipeline(pipeline_id), UniqueId::Worker);
450        for stream in self.connections.values_mut() {
451            console_actor.handle_page_error(page_error.clone(), id.clone(), &actors, stream);
452        }
453    }
454
455    fn handle_console_message(
456        &mut self,
457        pipeline_id: PipelineId,
458        worker_id: Option<WorkerId>,
459        console_message: ConsoleMessage,
460    ) {
461        let console_actor_name = match self.find_console_actor(pipeline_id, worker_id) {
462            Some(name) => name,
463            None => return,
464        };
465        let actors = self.actors.lock().unwrap();
466        let console_actor = actors.find::<ConsoleActor>(&console_actor_name);
467        let id = worker_id.map_or(UniqueId::Pipeline(pipeline_id), UniqueId::Worker);
468        for stream in self.connections.values_mut() {
469            console_actor.handle_console_api(console_message.clone(), id.clone(), &actors, stream);
470        }
471    }
472
473    fn find_console_actor(
474        &self,
475        pipeline_id: PipelineId,
476        worker_id: Option<WorkerId>,
477    ) -> Option<String> {
478        let actors = self.actors.lock().unwrap();
479        if let Some(worker_id) = worker_id {
480            let actor_name = self.actor_workers.get(&worker_id)?;
481            Some(actors.find::<WorkerActor>(actor_name).console.clone())
482        } else {
483            let id = self.pipelines.get(&pipeline_id)?;
484            let actor_name = self.browsing_contexts.get(id)?;
485            Some(
486                actors
487                    .find::<BrowsingContextActor>(actor_name)
488                    .console
489                    .clone(),
490            )
491        }
492    }
493
494    fn handle_network_event(
495        &mut self,
496        connections: Vec<TcpStream>,
497        request_id: String,
498        network_event: NetworkEvent,
499    ) {
500        let browsing_context_id = match &network_event {
501            NetworkEvent::HttpRequest(req) => req.browsing_context_id,
502            NetworkEvent::HttpRequestUpdate(req) => req.browsing_context_id,
503            NetworkEvent::HttpResponse(resp) => resp.browsing_context_id,
504        };
505
506        let Some(browsing_context_actor_name) = self.browsing_contexts.get(&browsing_context_id)
507        else {
508            return;
509        };
510        let watcher_name = self
511            .actors
512            .lock()
513            .unwrap()
514            .find::<BrowsingContextActor>(browsing_context_actor_name)
515            .watcher
516            .clone();
517
518        let netevent_actor_name = match self.actor_requests.get(&request_id) {
519            Some(name) => name.clone(),
520            None => self.create_network_event_actor(request_id, watcher_name),
521        };
522
523        handle_network_event(
524            Arc::clone(&self.actors),
525            netevent_actor_name,
526            connections,
527            network_event,
528        )
529    }
530
531    /// Create a new NetworkEventActor for a given request ID and watcher name.
532    fn create_network_event_actor(&mut self, request_id: String, watcher_name: String) -> String {
533        let mut actors = self.actors.lock().unwrap();
534        let resource_id = self.next_resource_id;
535        self.next_resource_id += 1;
536
537        let actor_name = actors.new_name("netevent");
538        let actor = NetworkEventActor::new(actor_name.clone(), resource_id, watcher_name);
539
540        self.actor_requests.insert(request_id, actor_name.clone());
541        actors.register(Box::new(actor));
542
543        actor_name
544    }
545
546    fn handle_create_source_actor(
547        &mut self,
548        script_sender: IpcSender<DevtoolScriptControlMsg>,
549        pipeline_id: PipelineId,
550        source_info: SourceInfo,
551    ) {
552        let mut actors = self.actors.lock().unwrap();
553
554        let source_content = source_info
555            .content
556            .or_else(|| actors.inline_source_content(pipeline_id));
557        let source_actor = SourceActor::new_registered(
558            &mut actors,
559            pipeline_id,
560            source_info.url,
561            source_content,
562            source_info.content_type,
563            source_info.spidermonkey_id,
564            source_info.introduction_type,
565            script_sender,
566        );
567        let source_actor_name = source_actor.name.clone();
568        let source_form = source_actor.source_form();
569
570        if let Some(worker_id) = source_info.worker_id {
571            let Some(worker_actor_name) = self.actor_workers.get(&worker_id) else {
572                return;
573            };
574
575            let thread_actor_name = actors.find::<WorkerActor>(worker_actor_name).thread.clone();
576            let thread_actor = actors.find_mut::<ThreadActor>(&thread_actor_name);
577
578            thread_actor.source_manager.add_source(&source_actor_name);
579
580            let worker_actor = actors.find::<WorkerActor>(worker_actor_name);
581
582            for stream in self.connections.values_mut() {
583                worker_actor.resource_array(
584                    &source_form,
585                    "source".into(),
586                    ResourceArrayType::Available,
587                    stream,
588                );
589            }
590        } else {
591            let Some(browsing_context_id) = self.pipelines.get(&pipeline_id) else {
592                return;
593            };
594            let Some(actor_name) = self.browsing_contexts.get(browsing_context_id) else {
595                return;
596            };
597
598            let thread_actor_name = {
599                let browsing_context = actors.find::<BrowsingContextActor>(actor_name);
600                browsing_context.thread.clone()
601            };
602
603            let thread_actor = actors.find_mut::<ThreadActor>(&thread_actor_name);
604
605            thread_actor.source_manager.add_source(&source_actor_name);
606
607            // Notify browsing context about the new source
608            let browsing_context = actors.find::<BrowsingContextActor>(actor_name);
609
610            for stream in self.connections.values_mut() {
611                browsing_context.resource_array(
612                    &source_form,
613                    "source".into(),
614                    ResourceArrayType::Available,
615                    stream,
616                );
617            }
618        }
619    }
620
621    fn handle_update_source_content(&mut self, pipeline_id: PipelineId, source_content: String) {
622        let mut actors = self.actors.lock().unwrap();
623
624        for actor_name in actors.source_actor_names_for_pipeline(pipeline_id) {
625            let source_actor: &mut SourceActor = actors.find_mut(&actor_name);
626            if source_actor.content.is_none() {
627                source_actor.content = Some(source_content.clone());
628            }
629        }
630
631        // Store the source content separately for any future source actors that get created *after* we finish parsing
632        // the HTML. For example, adding an `import` to an inline module script can delay it until after parsing.
633        actors.set_inline_source_content(pipeline_id, source_content);
634    }
635}
636
637fn allow_devtools_client(stream: &mut TcpStream, embedder: &EmbedderProxy, token: &str) -> bool {
638    // By-pass prompt if we receive a valid token.
639    let token = format!("25:{{\"auth_token\":\"{}\"}}", token);
640    let mut buf = [0; 28];
641    let timeout = std::time::Duration::from_millis(500);
642    // This will read but not consume the bytes from the stream.
643    stream.set_read_timeout(Some(timeout)).unwrap();
644    let peek = stream.peek(&mut buf);
645    stream.set_read_timeout(None).unwrap();
646    if let Ok(len) = peek {
647        if len == buf.len() {
648            if let Ok(s) = std::str::from_utf8(&buf) {
649                if s == token {
650                    // Consume the message as it was relevant to us.
651                    let _ = stream.read_exact(&mut buf);
652                    return true;
653                }
654            }
655        }
656    };
657
658    // No token found. Prompt user
659    let (request_sender, request_receiver) =
660        generic_channel::channel().expect("Failed to create IPC channel!");
661    embedder.send(EmbedderMsg::RequestDevtoolsConnection(request_sender));
662    request_receiver.recv().unwrap() == AllowOrDeny::Allow
663}
664
665/// Process the input from a single devtools client until EOF.
666fn handle_client(actors: Arc<Mutex<ActorRegistry>>, mut stream: TcpStream, stream_id: StreamId) {
667    log::info!("Connection established to {}", stream.peer_addr().unwrap());
668    let msg = actors.lock().unwrap().find::<RootActor>("root").encodable();
669    if let Err(error) = stream.write_json_packet(&msg) {
670        warn!("Failed to send initial packet from root actor: {error:?}");
671        return;
672    }
673
674    loop {
675        match stream.read_json_packet() {
676            Ok(Some(json_packet)) => {
677                if let Err(()) = actors.lock().unwrap().handle_message(
678                    json_packet.as_object().unwrap(),
679                    &mut stream,
680                    stream_id,
681                ) {
682                    log::error!("Devtools actor stopped responding");
683                    let _ = stream.shutdown(Shutdown::Both);
684                    break;
685                }
686            },
687            Ok(None) => {
688                log::info!("Devtools connection closed");
689                break;
690            },
691            Err(err_msg) => {
692                log::error!("Failed to read message from devtools client: {}", err_msg);
693                break;
694            },
695        }
696    }
697
698    actors.lock().unwrap().cleanup(stream_id);
699}