| /* 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 https://mozilla.org/MPL/2.0/. */ |
| |
| //! The `Constellation`, Servo's Grand Central Station |
| //! |
| //! The constellation tracks all information kept globally by the |
| //! browser engine, which includes: |
| //! |
| //! * The set of all `EventLoop` objects. Each event loop is |
| //! the constellation's view of a script thread. The constellation |
| //! interacts with a script thread by message-passing. |
| //! |
| //! * The set of all `Pipeline` objects. Each pipeline gives the |
| //! constellation's view of a `Window`, with its script thread and |
| //! layout. Pipelines may share script threads. |
| //! |
| //! * The set of all `BrowsingContext` objects. Each browsing context |
| //! gives the constellation's view of a `WindowProxy`. |
| //! Each browsing context stores an independent |
| //! session history, created by navigation. The session |
| //! history can be traversed, for example by the back and forwards UI, |
| //! so each session history maintains a list of past and future pipelines, |
| //! as well as the current active pipeline. |
| //! |
| //! There are two kinds of browsing context: top-level ones (for |
| //! example tabs in a browser UI), and nested ones (typically caused |
| //! by `iframe` elements). Browsing contexts have a hierarchy |
| //! (typically caused by `iframe`s containing `iframe`s), giving rise |
| //! to a forest whose roots are top-level browsing context. The logical |
| //! relationship between these types is: |
| //! |
| //! ```text |
| //! +------------+ +------------+ +---------+ |
| //! | Browsing | ------parent?------> | Pipeline | --event_loop--> | Event | |
| //! | Context | ------current------> | | | Loop | |
| //! | | ------prev*--------> | | <---pipeline*-- | | |
| //! | | ------next*--------> | | +---------+ |
| //! | | | | |
| //! | | <-top_level--------- | | |
| //! | | <-browsing_context-- | | |
| //! +------------+ +------------+ |
| //! ``` |
| // |
| //! The constellation also maintains channels to threads, including: |
| //! |
| //! * The script thread. |
| //! * The graphics compositor. |
| //! * The font cache, image cache, and resource manager, which load |
| //! and cache shared fonts, images, or other resources. |
| //! * The service worker manager. |
| //! * The devtools and webdriver servers. |
| //! |
| //! The constellation passes messages between the threads, and updates its state |
| //! to track the evolving state of the browsing context tree. |
| //! |
| //! The constellation acts as a logger, tracking any `warn!` messages from threads, |
| //! and converting any `error!` or `panic!` into a crash report. |
| //! |
| //! Since there is only one constellation, and its responsibilities include crash reporting, |
| //! it is very important that it does not panic. |
| //! |
| //! It's also important that the constellation not deadlock. In particular, we need |
| //! to be careful that we don't introduce any cycles in the can-block-on relation. |
| //! Blocking is typically introduced by `receiver.recv()`, which blocks waiting for the |
| //! sender to send some data. Servo tries to achieve deadlock-freedom by using the following |
| //! can-block-on relation: |
| //! |
| //! * Constellation can block on compositor |
| //! * Constellation can block on embedder |
| //! * Script can block on anything (other than script) |
| //! * Blocking is transitive (if T1 can block on T2 and T2 can block on T3 then T1 can block on T3) |
| //! * Nothing can block on itself! |
| //! |
| //! There is a complexity intoduced by IPC channels, since they do not support |
| //! non-blocking send. This means that as well as `receiver.recv()` blocking, |
| //! `sender.send(data)` can also block when the IPC buffer is full. For this reason it is |
| //! very important that all IPC receivers where we depend on non-blocking send |
| //! use a router to route IPC messages to an mpsc channel. The reason why that solves |
| //! the problem is that under the hood, the router uses a dedicated thread to forward |
| //! messages, and: |
| //! |
| //! * Anything (other than a routing thread) can block on a routing thread |
| //! |
| //! See <https://github.com/servo/servo/issues/14704> |
| |
| use std::borrow::ToOwned; |
| use std::cell::OnceCell; |
| use std::collections::hash_map::Entry; |
| use std::collections::{HashMap, HashSet, VecDeque}; |
| use std::marker::PhantomData; |
| use std::mem::replace; |
| use std::rc::{Rc, Weak}; |
| use std::sync::{Arc, Mutex}; |
| use std::thread::JoinHandle; |
| use std::{process, thread}; |
| |
| use background_hang_monitor::HangMonitorRegister; |
| use background_hang_monitor_api::{ |
| BackgroundHangMonitorControlMsg, BackgroundHangMonitorRegister, HangMonitorAlert, |
| }; |
| use base::generic_channel::{GenericSender, RoutedReceiver}; |
| use base::id::{ |
| BrowsingContextGroupId, BrowsingContextId, HistoryStateId, MessagePortId, MessagePortRouterId, |
| PipelineId, PipelineNamespace, PipelineNamespaceId, PipelineNamespaceRequest, WebViewId, |
| }; |
| use base::{Epoch, generic_channel}; |
| #[cfg(feature = "bluetooth")] |
| use bluetooth_traits::BluetoothRequest; |
| use canvas::canvas_paint_thread::CanvasPaintThread; |
| use canvas_traits::ConstellationCanvasMsg; |
| use canvas_traits::canvas::{CanvasId, CanvasMsg}; |
| use canvas_traits::webgl::WebGLThreads; |
| use compositing_traits::{ |
| CompositorMsg, CompositorProxy, PipelineExitSource, SendableFrameTree, |
| WebrenderExternalImageRegistry, |
| }; |
| use constellation_traits::{ |
| AuxiliaryWebViewCreationRequest, AuxiliaryWebViewCreationResponse, DocumentState, |
| EmbedderToConstellationMessage, IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState, |
| IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry, MessagePortMsg, NavigationHistoryBehavior, |
| PaintMetricEvent, PortMessageTask, PortTransferInfo, SWManagerMsg, SWManagerSenders, |
| ScriptToConstellationChan, ScriptToConstellationMessage, ServiceWorkerManagerFactory, |
| ServiceWorkerMsg, StructuredSerializedData, TraversalDirection, WindowSizeType, |
| }; |
| use crossbeam_channel::{Receiver, Select, Sender, unbounded}; |
| use devtools_traits::{ |
| ChromeToDevtoolsControlMsg, DevtoolsControlMsg, DevtoolsPageInfo, NavigationState, |
| ScriptToDevtoolsControlMsg, |
| }; |
| use embedder_traits::resources::{self, Resource}; |
| use embedder_traits::user_content_manager::UserContentManager; |
| use embedder_traits::{ |
| AnimationState, CompositorHitTestResult, EmbedderMsg, EmbedderProxy, FocusId, |
| FocusSequenceNumber, InputEvent, JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId, |
| KeyboardEvent, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState, |
| MouseButton, MouseButtonAction, MouseButtonEvent, ScriptToEmbedderChan, Theme, ViewportDetails, |
| WebDriverCommandMsg, WebDriverCommandResponse, WebDriverLoadStatus, WebDriverScriptCommand, |
| }; |
| use euclid::Size2D; |
| use euclid::default::Size2D as UntypedSize2D; |
| use fnv::{FnvHashMap, FnvHashSet}; |
| use fonts::SystemFontServiceProxy; |
| use ipc_channel::Error as IpcError; |
| use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; |
| use ipc_channel::router::ROUTER; |
| use keyboard_types::{Key, KeyState, Modifiers, NamedKey}; |
| use layout_api::{LayoutFactory, ScriptThreadFactory}; |
| use log::{debug, error, info, trace, warn}; |
| use media::WindowGLContext; |
| use net_traits::pub_domains::reg_host; |
| use net_traits::request::Referrer; |
| use net_traits::storage_thread::{StorageThreadMsg, StorageType}; |
| use net_traits::{ |
| self, AsyncRuntime, IpcSend, ReferrerPolicy, ResourceThreads, exit_fetch_thread, |
| start_fetch_thread, |
| }; |
| use profile_traits::mem::ProfilerMsg; |
| use profile_traits::{mem, time}; |
| use script_traits::{ |
| ConstellationInputEvent, DiscardBrowsingContext, DocumentActivity, ProgressiveWebMetricType, |
| ScriptThreadMessage, UpdatePipelineIdReason, |
| }; |
| use serde::{Deserialize, Serialize}; |
| use servo_config::prefs::{self, PrefValue}; |
| use servo_config::{opts, pref}; |
| use servo_rand::{Rng, ServoRng, SliceRandom, random}; |
| use servo_url::{Host, ImmutableOrigin, ServoUrl}; |
| use style::global_style_data::StyleThreadPool; |
| #[cfg(feature = "webgpu")] |
| use webgpu::swapchain::WGPUImageMap; |
| #[cfg(feature = "webgpu")] |
| use webgpu_traits::{WebGPU, WebGPURequest}; |
| use webrender::RenderApiSender; |
| use webrender_api::units::LayoutVector2D; |
| use webrender_api::{DocumentId, ExternalScrollId, ImageKey}; |
| |
| use crate::broadcastchannel::BroadcastChannels; |
| use crate::browsingcontext::{ |
| AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator, |
| NewBrowsingContextInfo, |
| }; |
| use crate::constellation_webview::ConstellationWebView; |
| use crate::event_loop::EventLoop; |
| use crate::pipeline::{InitialPipelineState, Pipeline}; |
| use crate::process_manager::ProcessManager; |
| use crate::serviceworker::ServiceWorkerUnprivilegedContent; |
| use crate::session_history::{ |
| JointSessionHistory, NeedsToReload, SessionHistoryChange, SessionHistoryDiff, |
| }; |
| use crate::webview_manager::WebViewManager; |
| |
| type PendingApprovalNavigations = HashMap<PipelineId, (LoadData, NavigationHistoryBehavior)>; |
| |
| #[derive(Debug)] |
| /// The state used by MessagePortInfo to represent the various states the port can be in. |
| enum TransferState { |
| /// The port is currently managed by a given global, |
| /// identified by its router id. |
| Managed(MessagePortRouterId), |
| /// The port is currently in-transfer, |
| /// and incoming tasks should be buffered until it becomes managed again. |
| TransferInProgress(VecDeque<PortMessageTask>), |
| /// A global has requested the transfer to be completed, |
| /// it's pending a confirmation of either failure or success to complete the transfer. |
| CompletionInProgress(MessagePortRouterId), |
| /// While a completion of a transfer was in progress, the port was shipped, |
| /// hence the transfer failed to complete. |
| /// We start buffering incoming messages, |
| /// while awaiting the return of the previous buffer from the global |
| /// that failed to complete the transfer. |
| CompletionFailed(VecDeque<PortMessageTask>), |
| /// While a completion failed, another global requested to complete the transfer. |
| /// We are still buffering messages, and awaiting the return of the buffer from the global who failed. |
| CompletionRequested(MessagePortRouterId, VecDeque<PortMessageTask>), |
| } |
| |
| #[derive(Debug)] |
| /// Info related to a message-port tracked by the constellation. |
| struct MessagePortInfo { |
| /// The current state of the messageport. |
| state: TransferState, |
| |
| /// The id of the entangled port, if any. |
| entangled_with: Option<MessagePortId>, |
| } |
| |
| #[cfg(feature = "webgpu")] |
| /// Webrender related objects required by WebGPU threads |
| struct WebrenderWGPU { |
| /// List of Webrender external images |
| webrender_external_images: Arc<Mutex<WebrenderExternalImageRegistry>>, |
| |
| /// WebGPU data that supplied to Webrender for rendering |
| wgpu_image_map: WGPUImageMap, |
| } |
| |
| /// A browsing context group. |
| /// |
| /// <https://html.spec.whatwg.org/multipage/#browsing-context-group> |
| #[derive(Clone, Default)] |
| struct BrowsingContextGroup { |
| /// A browsing context group holds a set of top-level browsing contexts. |
| top_level_browsing_context_set: FnvHashSet<WebViewId>, |
| |
| /// The set of all event loops in this BrowsingContextGroup. |
| /// We store the event loops in a map |
| /// indexed by registered domain name (as a `Host`) to event loops. |
| /// It is important that scripts with the same eTLD+1, |
| /// who are part of the same browsing-context group |
| /// share an event loop, since they can use `document.domain` |
| /// to become same-origin, at which point they can share DOM objects. |
| event_loops: HashMap<Host, Weak<EventLoop>>, |
| |
| /// The set of all WebGPU channels in this BrowsingContextGroup. |
| #[cfg(feature = "webgpu")] |
| webgpus: HashMap<Host, WebGPU>, |
| } |
| |
| struct PreferenceForwarder(Sender<EmbedderToConstellationMessage>); |
| |
| impl prefs::Observer for PreferenceForwarder { |
| fn prefs_changed(&self, changes: &[(&'static str, PrefValue)]) { |
| let _ = self |
| .0 |
| .send(EmbedderToConstellationMessage::PreferencesUpdated( |
| changes.to_owned(), |
| )); |
| } |
| } |
| |
| /// The `Constellation` itself. In the servo browser, there is one |
| /// constellation, which maintains all of the browser global data. |
| /// In embedded applications, there may be more than one constellation, |
| /// which are independent of each other. |
| /// |
| /// The constellation may be in a different process from the pipelines, |
| /// and communicates using IPC. |
| /// |
| /// It is parameterized over a `LayoutThreadFactory` and a |
| /// `ScriptThreadFactory` (which in practice are implemented by |
| /// `LayoutThread` in the `layout` crate, and `ScriptThread` in |
| /// the `script` crate). Script and layout communicate using a `Message` |
| /// type. |
| pub struct Constellation<STF, SWF> { |
| /// An ipc-sender/threaded-receiver pair |
| /// to facilitate installing pipeline namespaces in threads |
| /// via a per-process installer. |
| namespace_receiver: RoutedReceiver<PipelineNamespaceRequest>, |
| namespace_ipc_sender: GenericSender<PipelineNamespaceRequest>, |
| |
| /// An IPC channel for script threads to send messages to the constellation. |
| /// This is the script threads' view of `script_receiver`. |
| script_sender: GenericSender<(PipelineId, ScriptToConstellationMessage)>, |
| |
| /// A channel for the constellation to receive messages from script threads. |
| /// This is the constellation's view of `script_sender`. |
| script_receiver: Receiver<Result<(PipelineId, ScriptToConstellationMessage), IpcError>>, |
| |
| /// A handle to register components for hang monitoring. |
| /// None when in multiprocess mode. |
| background_monitor_register: Option<Box<dyn BackgroundHangMonitorRegister>>, |
| |
| /// In single process mode, a join handle on the BHM worker thread. |
| background_monitor_register_join_handle: Option<JoinHandle<()>>, |
| |
| /// Channels to control all background-hang monitors. |
| /// TODO: store them on the relevant BrowsingContextGroup, |
| /// so that they could be controlled on a "per-tab/event-loop" basis. |
| background_monitor_control_senders: Vec<IpcSender<BackgroundHangMonitorControlMsg>>, |
| |
| /// A channel for the background hang monitor to send messages |
| /// to the constellation. |
| background_hang_monitor_sender: IpcSender<HangMonitorAlert>, |
| |
| /// A channel for the constellation to receiver messages |
| /// from the background hang monitor. |
| background_hang_monitor_receiver: Receiver<Result<HangMonitorAlert, IpcError>>, |
| |
| /// A factory for creating layouts. This allows customizing the kind |
| /// of layout created for a [`Constellation`] and prevents a circular crate |
| /// dependency between script and layout. |
| layout_factory: Arc<dyn LayoutFactory>, |
| |
| /// A channel for the constellation to receive messages from the compositor thread. |
| compositor_receiver: Receiver<EmbedderToConstellationMessage>, |
| |
| /// A channel through which messages can be sent to the embedder. |
| embedder_proxy: EmbedderProxy, |
| |
| /// A channel (the implementation of which is port-specific) for the |
| /// constellation to send messages to the compositor thread. |
| compositor_proxy: CompositorProxy, |
| |
| /// Bookkeeping data for all webviews in the constellation. |
| webviews: WebViewManager<ConstellationWebView>, |
| |
| /// Channels for the constellation to send messages to the public |
| /// resource-related threads. There are two groups of resource threads: one |
| /// for public browsing, and one for private browsing. |
| public_resource_threads: ResourceThreads, |
| |
| /// Channels for the constellation to send messages to the private |
| /// resource-related threads. There are two groups of resource |
| /// threads: one for public browsing, and one for private |
| /// browsing. |
| private_resource_threads: ResourceThreads, |
| |
| /// A channel for the constellation to send messages to the font |
| /// cache thread. |
| system_font_service: Arc<SystemFontServiceProxy>, |
| |
| /// A channel for the constellation to send messages to the |
| /// devtools thread. |
| devtools_sender: Option<Sender<DevtoolsControlMsg>>, |
| |
| /// An IPC channel for the constellation to send messages to the |
| /// bluetooth thread. |
| #[cfg(feature = "bluetooth")] |
| bluetooth_ipc_sender: IpcSender<BluetoothRequest>, |
| |
| /// A map of origin to sender to a Service worker manager. |
| sw_managers: HashMap<ImmutableOrigin, GenericSender<ServiceWorkerMsg>>, |
| |
| /// An IPC channel for Service Worker Manager threads to send |
| /// messages to the constellation. This is the SW Manager thread's |
| /// view of `swmanager_receiver`. |
| swmanager_ipc_sender: GenericSender<SWManagerMsg>, |
| |
| /// A channel for the constellation to receive messages from the |
| /// Service Worker Manager thread. This is the constellation's view of |
| /// `swmanager_sender`. |
| swmanager_receiver: RoutedReceiver<SWManagerMsg>, |
| |
| /// A channel for the constellation to send messages to the |
| /// time profiler thread. |
| time_profiler_chan: time::ProfilerChan, |
| |
| /// A channel for the constellation to send messages to the |
| /// memory profiler thread. |
| mem_profiler_chan: mem::ProfilerChan, |
| |
| /// Webrender related objects required by WebGPU threads |
| #[cfg(feature = "webgpu")] |
| webrender_wgpu: WebrenderWGPU, |
| |
| /// A map of message-port Id to info. |
| message_ports: FnvHashMap<MessagePortId, MessagePortInfo>, |
| |
| /// A map of router-id to ipc-sender, to route messages to ports. |
| message_port_routers: FnvHashMap<MessagePortRouterId, IpcSender<MessagePortMsg>>, |
| |
| /// Bookkeeping for BroadcastChannel functionnality. |
| broadcast_channels: BroadcastChannels, |
| |
| /// The set of all the pipelines in the browser. (See the `pipeline` module |
| /// for more details.) |
| pipelines: FnvHashMap<PipelineId, Pipeline>, |
| |
| /// The set of all the browsing contexts in the browser. |
| browsing_contexts: FnvHashMap<BrowsingContextId, BrowsingContext>, |
| |
| /// A user agent holds a a set of browsing context groups. |
| /// |
| /// <https://html.spec.whatwg.org/multipage/#browsing-context-group-set> |
| browsing_context_group_set: FnvHashMap<BrowsingContextGroupId, BrowsingContextGroup>, |
| |
| /// The Id counter for BrowsingContextGroup. |
| browsing_context_group_next_id: u32, |
| |
| /// When a navigation is performed, we do not immediately update |
| /// the session history, instead we ask the event loop to begin loading |
| /// the new document, and do not update the browsing context until the |
| /// document is active. Between starting the load and it activating, |
| /// we store a `SessionHistoryChange` object for the navigation in progress. |
| pending_changes: Vec<SessionHistoryChange>, |
| |
| /// Pipeline IDs are namespaced in order to avoid name collisions, |
| /// and the namespaces are allocated by the constellation. |
| next_pipeline_namespace_id: PipelineNamespaceId, |
| |
| /// An [`IpcSender`] to notify navigation events to webdriver. |
| webdriver_load_status_sender: Option<(GenericSender<WebDriverLoadStatus>, PipelineId)>, |
| |
| /// An [`IpcSender`] to forward responses from the `ScriptThread` to the WebDriver server. |
| webdriver_input_command_reponse_sender: Option<IpcSender<WebDriverCommandResponse>>, |
| |
| /// Document states for loaded pipelines (used only when writing screenshots). |
| document_states: FnvHashMap<PipelineId, DocumentState>, |
| |
| /// Are we shutting down? |
| shutting_down: bool, |
| |
| /// Have we seen any warnings? Hopefully always empty! |
| /// The buffer contains `(thread_name, reason)` entries. |
| handled_warnings: VecDeque<(Option<String>, String)>, |
| |
| /// The random number generator and probability for closing pipelines. |
| /// This is for testing the hardening of the constellation. |
| random_pipeline_closure: Option<(ServoRng, f32)>, |
| |
| /// Phantom data that keeps the Rust type system happy. |
| phantom: PhantomData<(STF, SWF)>, |
| |
| /// Entry point to create and get channels to a WebGLThread. |
| webgl_threads: Option<WebGLThreads>, |
| |
| /// The XR device registry |
| webxr_registry: Option<webxr_api::Registry>, |
| |
| /// Lazily initialized channels for canvas paint thread. |
| canvas: OnceCell<(Sender<ConstellationCanvasMsg>, IpcSender<CanvasMsg>)>, |
| |
| /// Navigation requests from script awaiting approval from the embedder. |
| pending_approval_navigations: PendingApprovalNavigations, |
| |
| /// Bitmask which indicates which combination of mouse buttons are |
| /// currently being pressed. |
| pressed_mouse_buttons: u16, |
| |
| /// The currently activated keyboard modifiers. |
| active_keyboard_modifiers: Modifiers, |
| |
| /// If True, exits on thread failure instead of displaying about:failure |
| hard_fail: bool, |
| |
| /// Pipeline ID of the active media session. |
| active_media_session: Option<PipelineId>, |
| |
| /// The image bytes associated with the RippyPNG embedder resource. |
| /// Read during startup and provided to image caches that are created |
| /// on an as-needed basis, rather than retrieving it every time. |
| rippy_data: Vec<u8>, |
| |
| /// User content manager |
| user_content_manager: UserContentManager, |
| |
| /// The process manager. |
| process_manager: ProcessManager, |
| |
| /// The async runtime. |
| async_runtime: Box<dyn AsyncRuntime>, |
| |
| /// When in single-process mode, join handles for script-threads. |
| script_join_handles: FnvHashMap<WebViewId, JoinHandle<()>>, |
| |
| /// A list of URLs that can access privileged internal APIs. |
| privileged_urls: Vec<ServoUrl>, |
| } |
| |
| /// State needed to construct a constellation. |
| pub struct InitialConstellationState { |
| /// A channel through which messages can be sent to the embedder. |
| pub embedder_proxy: EmbedderProxy, |
| |
| /// A channel through which messages can be sent to the compositor in-process. |
| pub compositor_proxy: CompositorProxy, |
| |
| /// A channel to the developer tools, if applicable. |
| pub devtools_sender: Option<Sender<DevtoolsControlMsg>>, |
| |
| /// A channel to the bluetooth thread. |
| #[cfg(feature = "bluetooth")] |
| pub bluetooth_thread: IpcSender<BluetoothRequest>, |
| |
| /// A proxy to the `SystemFontService` which manages the list of system fonts. |
| pub system_font_service: Arc<SystemFontServiceProxy>, |
| |
| /// A channel to the resource thread. |
| pub public_resource_threads: ResourceThreads, |
| |
| /// A channel to the resource thread. |
| pub private_resource_threads: ResourceThreads, |
| |
| /// A channel to the time profiler thread. |
| pub time_profiler_chan: time::ProfilerChan, |
| |
| /// A channel to the memory profiler thread. |
| pub mem_profiler_chan: mem::ProfilerChan, |
| |
| /// Webrender document ID. |
| pub webrender_document: DocumentId, |
| |
| /// Webrender API. |
| pub webrender_api_sender: RenderApiSender, |
| |
| /// Webrender external images |
| pub webrender_external_images: Arc<Mutex<WebrenderExternalImageRegistry>>, |
| |
| /// Entry point to create and get channels to a WebGLThread. |
| pub webgl_threads: Option<WebGLThreads>, |
| |
| /// The XR device registry |
| pub webxr_registry: Option<webxr_api::Registry>, |
| |
| #[cfg(feature = "webgpu")] |
| pub wgpu_image_map: WGPUImageMap, |
| |
| /// User content manager |
| pub user_content_manager: UserContentManager, |
| |
| /// A list of URLs that can access privileged internal APIs. |
| pub privileged_urls: Vec<ServoUrl>, |
| |
| /// The async runtime. |
| pub async_runtime: Box<dyn AsyncRuntime>, |
| } |
| |
| /// When we are running reftests, we save an image to compare against a reference. |
| /// This enum gives the possible states of preparing such an image. |
| #[derive(Debug, PartialEq)] |
| enum ReadyToSave { |
| NoTopLevelBrowsingContext, |
| PendingChanges, |
| DocumentLoading, |
| EpochMismatch, |
| PipelineUnknown, |
| Ready, |
| } |
| |
| /// When we are exiting a pipeline, we can either force exiting or not. |
| /// A normal exit waits for the compositor to update its state before |
| /// exiting, and delegates layout exit to script. A forced exit does |
| /// not notify the compositor, and exits layout without involving script. |
| #[derive(Clone, Copy, Debug)] |
| enum ExitPipelineMode { |
| Normal, |
| Force, |
| } |
| |
| /// The number of warnings to include in each crash report. |
| const WARNINGS_BUFFER_SIZE: usize = 32; |
| |
| /// Route an ipc receiver to an crossbeam receiver, preserving any errors. |
| fn route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors<T>( |
| ipc_receiver: IpcReceiver<T>, |
| ) -> Receiver<Result<T, IpcError>> |
| where |
| T: for<'de> Deserialize<'de> + Serialize + Send + 'static, |
| { |
| let (crossbeam_sender, crossbeam_receiver) = unbounded(); |
| ROUTER.add_typed_route( |
| ipc_receiver, |
| Box::new(move |message| { |
| let _ = crossbeam_sender.send(message); |
| }), |
| ); |
| crossbeam_receiver |
| } |
| |
| impl<STF, SWF> Constellation<STF, SWF> |
| where |
| STF: ScriptThreadFactory, |
| SWF: ServiceWorkerManagerFactory, |
| { |
| /// Create a new constellation thread. |
| #[allow(clippy::too_many_arguments)] |
| #[servo_tracing::instrument(skip(state, layout_factory))] |
| pub fn start( |
| state: InitialConstellationState, |
| layout_factory: Arc<dyn LayoutFactory>, |
| random_pipeline_closure_probability: Option<f32>, |
| random_pipeline_closure_seed: Option<usize>, |
| hard_fail: bool, |
| ) -> Sender<EmbedderToConstellationMessage> { |
| let (compositor_sender, compositor_receiver) = unbounded(); |
| let compositor_sender_self = compositor_sender.clone(); |
| |
| // service worker manager to communicate with constellation |
| let (swmanager_ipc_sender, swmanager_ipc_receiver) = |
| generic_channel::channel().expect("ipc channel failure"); |
| |
| thread::Builder::new() |
| .name("Constellation".to_owned()) |
| .spawn(move || { |
| let (script_ipc_sender, script_ipc_receiver) = |
| generic_channel::channel().expect("ipc channel failure"); |
| let script_receiver = script_ipc_receiver.route_preserving_errors(); |
| |
| let (namespace_ipc_sender, namespace_ipc_receiver) = |
| generic_channel::channel().expect("ipc channel failure"); |
| let namespace_receiver = namespace_ipc_receiver.route_preserving_errors(); |
| |
| let (background_hang_monitor_ipc_sender, background_hang_monitor_ipc_receiver) = |
| ipc::channel().expect("ipc channel failure"); |
| let background_hang_monitor_receiver = |
| route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors( |
| background_hang_monitor_ipc_receiver, |
| ); |
| |
| // If we are in multiprocess mode, |
| // a dedicated per-process hang monitor will be initialized later inside the content process. |
| // See run_content_process in servo/lib.rs |
| let ( |
| background_monitor_register, |
| background_monitor_register_join_handle, |
| background_hang_monitor_control_ipc_senders, |
| ) = if opts::get().multiprocess { |
| (None, None, vec![]) |
| } else { |
| let ( |
| background_hang_monitor_control_ipc_sender, |
| background_hang_monitor_control_ipc_receiver, |
| ) = ipc::channel().expect("ipc channel failure"); |
| let (register, join_handle) = HangMonitorRegister::init( |
| background_hang_monitor_ipc_sender.clone(), |
| background_hang_monitor_control_ipc_receiver, |
| opts::get().background_hang_monitor, |
| ); |
| ( |
| Some(register), |
| Some(join_handle), |
| vec![background_hang_monitor_control_ipc_sender], |
| ) |
| }; |
| |
| let swmanager_receiver = swmanager_ipc_receiver.route_preserving_errors(); |
| |
| // Zero is reserved for the embedder. |
| PipelineNamespace::install(PipelineNamespaceId(1)); |
| |
| #[cfg(feature = "webgpu")] |
| let webrender_wgpu = WebrenderWGPU { |
| webrender_external_images: state.webrender_external_images, |
| wgpu_image_map: state.wgpu_image_map, |
| }; |
| |
| let rippy_data = resources::read_bytes(Resource::RippyPNG); |
| |
| if opts::get().multiprocess { |
| prefs::add_observer(Box::new(PreferenceForwarder(compositor_sender_self))); |
| } |
| |
| let mut constellation: Constellation<STF, SWF> = Constellation { |
| namespace_receiver, |
| namespace_ipc_sender, |
| script_sender: script_ipc_sender, |
| background_hang_monitor_sender: background_hang_monitor_ipc_sender, |
| background_hang_monitor_receiver, |
| background_monitor_register, |
| background_monitor_register_join_handle, |
| background_monitor_control_senders: background_hang_monitor_control_ipc_senders, |
| script_receiver, |
| compositor_receiver, |
| layout_factory, |
| embedder_proxy: state.embedder_proxy, |
| compositor_proxy: state.compositor_proxy, |
| webviews: WebViewManager::default(), |
| devtools_sender: state.devtools_sender, |
| #[cfg(feature = "bluetooth")] |
| bluetooth_ipc_sender: state.bluetooth_thread, |
| public_resource_threads: state.public_resource_threads, |
| private_resource_threads: state.private_resource_threads, |
| system_font_service: state.system_font_service, |
| sw_managers: Default::default(), |
| swmanager_receiver, |
| swmanager_ipc_sender, |
| browsing_context_group_set: Default::default(), |
| browsing_context_group_next_id: Default::default(), |
| message_ports: Default::default(), |
| message_port_routers: Default::default(), |
| broadcast_channels: Default::default(), |
| pipelines: Default::default(), |
| browsing_contexts: Default::default(), |
| pending_changes: vec![], |
| // We initialize the namespace at 2, since we reserved |
| // namespace 0 for the embedder, and 0 for the constellation |
| next_pipeline_namespace_id: PipelineNamespaceId(2), |
| time_profiler_chan: state.time_profiler_chan, |
| mem_profiler_chan: state.mem_profiler_chan.clone(), |
| phantom: PhantomData, |
| webdriver_load_status_sender: None, |
| webdriver_input_command_reponse_sender: None, |
| document_states: Default::default(), |
| #[cfg(feature = "webgpu")] |
| webrender_wgpu, |
| shutting_down: false, |
| handled_warnings: VecDeque::new(), |
| random_pipeline_closure: random_pipeline_closure_probability.map(|prob| { |
| let seed = random_pipeline_closure_seed.unwrap_or_else(random); |
| let rng = ServoRng::new_manually_reseeded(seed as u64); |
| warn!("Randomly closing pipelines."); |
| info!("Using seed {} for random pipeline closure.", seed); |
| (rng, prob) |
| }), |
| webgl_threads: state.webgl_threads, |
| webxr_registry: state.webxr_registry, |
| canvas: OnceCell::new(), |
| pending_approval_navigations: Default::default(), |
| pressed_mouse_buttons: 0, |
| active_keyboard_modifiers: Modifiers::empty(), |
| hard_fail, |
| active_media_session: None, |
| rippy_data, |
| user_content_manager: state.user_content_manager, |
| process_manager: ProcessManager::new(state.mem_profiler_chan), |
| async_runtime: state.async_runtime, |
| script_join_handles: Default::default(), |
| privileged_urls: state.privileged_urls, |
| }; |
| |
| constellation.run(); |
| }) |
| .expect("Thread spawning failed"); |
| |
| compositor_sender |
| } |
| |
| /// The main event loop for the constellation. |
| fn run(&mut self) { |
| // Start a fetch thread. |
| // In single-process mode this will be the global fetch thread; |
| // in multi-process mode this will be used only by the canvas paint thread. |
| let join_handle = start_fetch_thread(); |
| |
| while !self.shutting_down || !self.pipelines.is_empty() { |
| // Randomly close a pipeline if --random-pipeline-closure-probability is set |
| // This is for testing the hardening of the constellation. |
| self.maybe_close_random_pipeline(); |
| self.handle_request(); |
| } |
| self.handle_shutdown(); |
| |
| if !opts::get().multiprocess { |
| StyleThreadPool::shutdown(); |
| } |
| |
| // Shut down the fetch thread started above. |
| exit_fetch_thread(); |
| join_handle |
| .join() |
| .expect("Failed to join on the fetch thread in the constellation"); |
| |
| // Note: the last thing the constellation does, is asking the embedder to |
| // shut down. This helps ensure we've shut down all our internal threads before |
| // de-initializing Servo (see the `thread_count` warning on MacOS). |
| debug!("Asking embedding layer to complete shutdown."); |
| self.embedder_proxy.send(EmbedderMsg::ShutdownComplete); |
| } |
| |
| /// Generate a new pipeline id namespace. |
| fn next_pipeline_namespace_id(&mut self) -> PipelineNamespaceId { |
| let namespace_id = self.next_pipeline_namespace_id; |
| let PipelineNamespaceId(ref mut i) = self.next_pipeline_namespace_id; |
| *i += 1; |
| namespace_id |
| } |
| |
| fn next_browsing_context_group_id(&mut self) -> BrowsingContextGroupId { |
| let id = self.browsing_context_group_next_id; |
| self.browsing_context_group_next_id += 1; |
| BrowsingContextGroupId(id) |
| } |
| |
| fn get_event_loop( |
| &mut self, |
| host: &Host, |
| webview_id: &WebViewId, |
| opener: &Option<BrowsingContextId>, |
| ) -> Result<Weak<EventLoop>, &'static str> { |
| let bc_group = match opener { |
| Some(browsing_context_id) => { |
| let opener = self |
| .browsing_contexts |
| .get(browsing_context_id) |
| .ok_or("Opener was closed before the openee started")?; |
| self.browsing_context_group_set |
| .get(&opener.bc_group_id) |
| .ok_or("Opener belongs to an unknown browsing context group")? |
| }, |
| None => self |
| .browsing_context_group_set |
| .iter() |
| .filter_map(|(_, bc_group)| { |
| if bc_group |
| .top_level_browsing_context_set |
| .contains(webview_id) |
| { |
| Some(bc_group) |
| } else { |
| None |
| } |
| }) |
| .last() |
| .ok_or( |
| "Trying to get an event-loop for a top-level belonging to an unknown browsing context group", |
| )?, |
| }; |
| bc_group |
| .event_loops |
| .get(host) |
| .ok_or("Trying to get an event-loop from an unknown browsing context group") |
| .cloned() |
| } |
| |
| fn set_event_loop( |
| &mut self, |
| event_loop: Weak<EventLoop>, |
| host: Host, |
| webview_id: WebViewId, |
| opener: Option<BrowsingContextId>, |
| ) { |
| let relevant_top_level = if let Some(opener) = opener { |
| match self.browsing_contexts.get(&opener) { |
| Some(opener) => opener.top_level_id, |
| None => { |
| warn!("Setting event-loop for an unknown auxiliary"); |
| return; |
| }, |
| } |
| } else { |
| webview_id |
| }; |
| let maybe_bc_group_id = self |
| .browsing_context_group_set |
| .iter() |
| .filter_map(|(id, bc_group)| { |
| if bc_group |
| .top_level_browsing_context_set |
| .contains(&webview_id) |
| { |
| Some(*id) |
| } else { |
| None |
| } |
| }) |
| .last(); |
| let bc_group_id = match maybe_bc_group_id { |
| Some(id) => id, |
| None => { |
| warn!("Trying to add an event-loop to an unknown browsing context group"); |
| return; |
| }, |
| }; |
| if let Some(bc_group) = self.browsing_context_group_set.get_mut(&bc_group_id) { |
| if bc_group |
| .event_loops |
| .insert(host.clone(), event_loop) |
| .is_some() |
| { |
| warn!( |
| "Double-setting an event-loop for {:?} at {:?}", |
| host, relevant_top_level |
| ); |
| } |
| } |
| } |
| |
| /// Helper function for creating a pipeline |
| #[allow(clippy::too_many_arguments)] |
| fn new_pipeline( |
| &mut self, |
| pipeline_id: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| webview_id: WebViewId, |
| parent_pipeline_id: Option<PipelineId>, |
| opener: Option<BrowsingContextId>, |
| initial_viewport_details: ViewportDetails, |
| // TODO: we have to provide ownership of the LoadData |
| // here, because it will be send on an ipc channel, |
| // and ipc channels take onership of their data. |
| // https://github.com/servo/ipc-channel/issues/138 |
| load_data: LoadData, |
| sandbox: IFrameSandboxState, |
| is_private: bool, |
| throttled: bool, |
| ) { |
| if self.shutting_down { |
| return; |
| } |
| |
| let Some(theme) = self |
| .webviews |
| .get(webview_id) |
| .map(ConstellationWebView::theme) |
| else { |
| warn!("Tried to create Pipeline for uknown WebViewId: {webview_id:?}"); |
| return; |
| }; |
| |
| debug!( |
| "{}: Creating new pipeline in {}", |
| pipeline_id, browsing_context_id |
| ); |
| |
| let (event_loop, host) = match sandbox { |
| IFrameSandboxState::IFrameSandboxed => (None, None), |
| IFrameSandboxState::IFrameUnsandboxed => { |
| // If this is an about:blank or about:srcdoc load, it must share the creator's |
| // event loop. This must match the logic in the script thread when determining |
| // the proper origin. |
| if load_data.url.as_str() != "about:blank" && |
| load_data.url.as_str() != "about:srcdoc" |
| { |
| match reg_host(&load_data.url) { |
| None => (None, None), |
| Some(host) => match self.get_event_loop(&host, &webview_id, &opener) { |
| Err(err) => { |
| warn!("{}", err); |
| (None, Some(host)) |
| }, |
| Ok(event_loop) => { |
| if let Some(event_loop) = event_loop.upgrade() { |
| (Some(event_loop), None) |
| } else { |
| (None, Some(host)) |
| } |
| }, |
| }, |
| } |
| } else if let Some(parent) = |
| parent_pipeline_id.and_then(|pipeline_id| self.pipelines.get(&pipeline_id)) |
| { |
| (Some(parent.event_loop.clone()), None) |
| } else if let Some(creator) = load_data |
| .creator_pipeline_id |
| .and_then(|pipeline_id| self.pipelines.get(&pipeline_id)) |
| { |
| (Some(creator.event_loop.clone()), None) |
| } else { |
| (None, None) |
| } |
| }, |
| }; |
| |
| let resource_threads = if is_private { |
| self.private_resource_threads.clone() |
| } else { |
| self.public_resource_threads.clone() |
| }; |
| |
| let embedder_chan = self.embedder_proxy.sender.clone(); |
| let eventloop_waker = self.embedder_proxy.event_loop_waker.clone(); |
| let script_to_embedder_chan = ScriptToEmbedderChan::new(embedder_chan, eventloop_waker); |
| |
| let result = Pipeline::spawn::<STF>(InitialPipelineState { |
| id: pipeline_id, |
| browsing_context_id, |
| webview_id, |
| parent_pipeline_id, |
| opener, |
| script_to_constellation_chan: ScriptToConstellationChan { |
| sender: self.script_sender.clone(), |
| pipeline_id, |
| }, |
| script_to_embedder_chan, |
| namespace_request_sender: self.namespace_ipc_sender.clone(), |
| pipeline_namespace_id: self.next_pipeline_namespace_id(), |
| background_monitor_register: self.background_monitor_register.clone(), |
| background_hang_monitor_to_constellation_chan: self |
| .background_hang_monitor_sender |
| .clone(), |
| layout_factory: self.layout_factory.clone(), |
| compositor_proxy: self.compositor_proxy.clone(), |
| devtools_sender: self.devtools_sender.clone(), |
| #[cfg(feature = "bluetooth")] |
| bluetooth_thread: self.bluetooth_ipc_sender.clone(), |
| swmanager_thread: self.swmanager_ipc_sender.clone(), |
| system_font_service: self.system_font_service.clone(), |
| resource_threads, |
| time_profiler_chan: self.time_profiler_chan.clone(), |
| mem_profiler_chan: self.mem_profiler_chan.clone(), |
| viewport_details: initial_viewport_details, |
| theme, |
| event_loop, |
| load_data, |
| prev_throttled: throttled, |
| webgl_chan: self |
| .webgl_threads |
| .as_ref() |
| .map(|threads| threads.pipeline()), |
| webxr_registry: self.webxr_registry.clone(), |
| player_context: WindowGLContext::get(), |
| rippy_data: self.rippy_data.clone(), |
| user_content_manager: self.user_content_manager.clone(), |
| privileged_urls: self.privileged_urls.clone(), |
| }); |
| |
| let pipeline = match result { |
| Ok(result) => result, |
| Err(e) => return self.handle_send_error(pipeline_id, e), |
| }; |
| |
| if let Some(chan) = pipeline.bhm_control_chan { |
| self.background_monitor_control_senders.push(chan); |
| } |
| |
| if let Some(join_handle) = pipeline.join_handle { |
| self.script_join_handles.insert(webview_id, join_handle); |
| } |
| |
| if let Some(host) = host { |
| debug!("{}: Adding new host entry {}", webview_id, host,); |
| self.set_event_loop( |
| Rc::downgrade(&pipeline.pipeline.event_loop), |
| host, |
| webview_id, |
| opener, |
| ); |
| } |
| |
| if let Some((lifeline_receiver, process)) = pipeline.lifeline { |
| let crossbeam_receiver = |
| route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors(lifeline_receiver); |
| self.process_manager.add(crossbeam_receiver, process); |
| } |
| |
| assert!(!self.pipelines.contains_key(&pipeline_id)); |
| self.pipelines.insert(pipeline_id, pipeline.pipeline); |
| } |
| |
| /// Get an iterator for the fully active browsing contexts in a subtree. |
| fn fully_active_descendant_browsing_contexts_iter( |
| &self, |
| browsing_context_id: BrowsingContextId, |
| ) -> FullyActiveBrowsingContextsIterator<'_> { |
| FullyActiveBrowsingContextsIterator { |
| stack: vec![browsing_context_id], |
| pipelines: &self.pipelines, |
| browsing_contexts: &self.browsing_contexts, |
| } |
| } |
| |
| /// Get an iterator for the fully active browsing contexts in a tree. |
| fn fully_active_browsing_contexts_iter( |
| &self, |
| webview_id: WebViewId, |
| ) -> FullyActiveBrowsingContextsIterator<'_> { |
| self.fully_active_descendant_browsing_contexts_iter(BrowsingContextId::from(webview_id)) |
| } |
| |
| /// Get an iterator for the browsing contexts in a subtree. |
| fn all_descendant_browsing_contexts_iter( |
| &self, |
| browsing_context_id: BrowsingContextId, |
| ) -> AllBrowsingContextsIterator<'_> { |
| AllBrowsingContextsIterator { |
| stack: vec![browsing_context_id], |
| pipelines: &self.pipelines, |
| browsing_contexts: &self.browsing_contexts, |
| } |
| } |
| |
| /// Enumerate the specified browsing context's ancestor pipelines up to |
| /// the top-level pipeline. |
| fn ancestor_pipelines_of_browsing_context_iter( |
| &self, |
| browsing_context_id: BrowsingContextId, |
| ) -> impl Iterator<Item = &Pipeline> + '_ { |
| let mut state: Option<PipelineId> = self |
| .browsing_contexts |
| .get(&browsing_context_id) |
| .and_then(|browsing_context| browsing_context.parent_pipeline_id); |
| std::iter::from_fn(move || { |
| if let Some(pipeline_id) = state { |
| let pipeline = self.pipelines.get(&pipeline_id)?; |
| let browsing_context = self.browsing_contexts.get(&pipeline.browsing_context_id)?; |
| state = browsing_context.parent_pipeline_id; |
| Some(pipeline) |
| } else { |
| None |
| } |
| }) |
| } |
| |
| /// Enumerate the specified browsing context's ancestor-or-self pipelines up |
| /// to the top-level pipeline. |
| fn ancestor_or_self_pipelines_of_browsing_context_iter( |
| &self, |
| browsing_context_id: BrowsingContextId, |
| ) -> impl Iterator<Item = &Pipeline> + '_ { |
| let this_pipeline = self |
| .browsing_contexts |
| .get(&browsing_context_id) |
| .map(|browsing_context| browsing_context.pipeline_id) |
| .and_then(|pipeline_id| self.pipelines.get(&pipeline_id)); |
| this_pipeline |
| .into_iter() |
| .chain(self.ancestor_pipelines_of_browsing_context_iter(browsing_context_id)) |
| } |
| |
| /// Create a new browsing context and update the internal bookkeeping. |
| #[allow(clippy::too_many_arguments)] |
| fn new_browsing_context( |
| &mut self, |
| browsing_context_id: BrowsingContextId, |
| top_level_id: WebViewId, |
| pipeline_id: PipelineId, |
| parent_pipeline_id: Option<PipelineId>, |
| viewport_details: ViewportDetails, |
| is_private: bool, |
| inherited_secure_context: Option<bool>, |
| throttled: bool, |
| ) { |
| debug!("{}: Creating new browsing context", browsing_context_id); |
| let bc_group_id = match self |
| .browsing_context_group_set |
| .iter_mut() |
| .filter_map(|(id, bc_group)| { |
| if bc_group |
| .top_level_browsing_context_set |
| .contains(&top_level_id) |
| { |
| Some(id) |
| } else { |
| None |
| } |
| }) |
| .last() |
| { |
| Some(id) => *id, |
| None => { |
| warn!("Top-level was unexpectedly removed from its top_level_browsing_context_set"); |
| return; |
| }, |
| }; |
| let browsing_context = BrowsingContext::new( |
| bc_group_id, |
| browsing_context_id, |
| top_level_id, |
| pipeline_id, |
| parent_pipeline_id, |
| viewport_details, |
| is_private, |
| inherited_secure_context, |
| throttled, |
| ); |
| self.browsing_contexts |
| .insert(browsing_context_id, browsing_context); |
| |
| // If this context is a nested container, attach it to parent pipeline. |
| if let Some(parent_pipeline_id) = parent_pipeline_id { |
| if let Some(parent) = self.pipelines.get_mut(&parent_pipeline_id) { |
| parent.add_child(browsing_context_id); |
| } |
| } |
| } |
| |
| fn add_pending_change(&mut self, change: SessionHistoryChange) { |
| debug!( |
| "adding pending session history change with {}", |
| if change.replace.is_some() { |
| "replacement" |
| } else { |
| "no replacement" |
| }, |
| ); |
| self.pending_changes.push(change); |
| } |
| |
| /// Handles loading pages, navigation, and granting access to the compositor |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_request(&mut self) { |
| #[allow(clippy::large_enum_variant)] |
| #[derive(Debug)] |
| enum Request { |
| PipelineNamespace(PipelineNamespaceRequest), |
| Script((PipelineId, ScriptToConstellationMessage)), |
| BackgroundHangMonitor(HangMonitorAlert), |
| Compositor(EmbedderToConstellationMessage), |
| FromSWManager(SWManagerMsg), |
| RemoveProcess(usize), |
| } |
| // Get one incoming request. |
| // This is one of the few places where the compositor is |
| // allowed to panic. If one of the receiver.recv() calls |
| // fails, it is because the matching sender has been |
| // reclaimed, but this can't happen in normal execution |
| // because the constellation keeps a pointer to the sender, |
| // so it should never be reclaimed. A possible scenario in |
| // which receiver.recv() fails is if some unsafe code |
| // produces undefined behaviour, resulting in the destructor |
| // being called. If this happens, there's not much we can do |
| // other than panic. |
| let mut sel = Select::new(); |
| sel.recv(&self.namespace_receiver); |
| sel.recv(&self.script_receiver); |
| sel.recv(&self.background_hang_monitor_receiver); |
| sel.recv(&self.compositor_receiver); |
| sel.recv(&self.swmanager_receiver); |
| |
| self.process_manager.register(&mut sel); |
| |
| let request = { |
| let oper = sel.select(); |
| let index = oper.index(); |
| |
| #[cfg(feature = "tracing")] |
| let _span = |
| tracing::trace_span!("handle_request::select", servo_profiling = true).entered(); |
| match index { |
| 0 => oper |
| .recv(&self.namespace_receiver) |
| .expect("Unexpected script channel panic in constellation") |
| .map(Request::PipelineNamespace), |
| 1 => oper |
| .recv(&self.script_receiver) |
| .expect("Unexpected script channel panic in constellation") |
| .map(Request::Script), |
| 2 => oper |
| .recv(&self.background_hang_monitor_receiver) |
| .expect("Unexpected BHM channel panic in constellation") |
| .map(Request::BackgroundHangMonitor), |
| 3 => Ok(Request::Compositor( |
| oper.recv(&self.compositor_receiver) |
| .expect("Unexpected compositor channel panic in constellation"), |
| )), |
| 4 => oper |
| .recv(&self.swmanager_receiver) |
| .expect("Unexpected SW channel panic in constellation") |
| .map(Request::FromSWManager), |
| _ => { |
| // This can only be a error reading on a closed lifeline receiver. |
| let process_index = index - 5; |
| let _ = oper.recv(self.process_manager.receiver_at(process_index)); |
| Ok(Request::RemoveProcess(process_index)) |
| }, |
| } |
| }; |
| |
| let request = match request { |
| Ok(request) => request, |
| Err(err) => return error!("Deserialization failed ({}).", err), |
| }; |
| |
| match request { |
| Request::PipelineNamespace(message) => { |
| self.handle_request_for_pipeline_namespace(message) |
| }, |
| Request::Compositor(message) => self.handle_request_from_compositor(message), |
| Request::Script(message) => { |
| self.handle_request_from_script(message); |
| }, |
| Request::BackgroundHangMonitor(message) => { |
| self.handle_request_from_background_hang_monitor(message); |
| }, |
| Request::FromSWManager(message) => { |
| self.handle_request_from_swmanager(message); |
| }, |
| Request::RemoveProcess(index) => self.process_manager.remove(index), |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_request_for_pipeline_namespace(&mut self, request: PipelineNamespaceRequest) { |
| let PipelineNamespaceRequest(sender) = request; |
| let _ = sender.send(self.next_pipeline_namespace_id()); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_request_from_background_hang_monitor(&self, message: HangMonitorAlert) { |
| match message { |
| HangMonitorAlert::Profile(bytes) => { |
| self.embedder_proxy.send(EmbedderMsg::ReportProfile(bytes)) |
| }, |
| HangMonitorAlert::Hang(hang) => { |
| // TODO: In case of a permanent hang being reported, add a "kill script" workflow, |
| // via the embedder? |
| warn!("Component hang alert: {:?}", hang); |
| }, |
| } |
| } |
| |
| fn handle_request_from_swmanager(&mut self, message: SWManagerMsg) { |
| match message { |
| SWManagerMsg::PostMessageToClient => { |
| // TODO: implement posting a message to a SW client. |
| // https://github.com/servo/servo/issues/24660 |
| }, |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_request_from_compositor(&mut self, message: EmbedderToConstellationMessage) { |
| trace_msg_from_compositor!(message, "{message:?}"); |
| match message { |
| EmbedderToConstellationMessage::Exit => { |
| self.handle_exit(); |
| }, |
| // Perform a navigation previously requested by script, if approved by the embedder. |
| // If there is already a pending page (self.pending_changes), it will not be overridden; |
| // However, if the id is not encompassed by another change, it will be. |
| EmbedderToConstellationMessage::AllowNavigationResponse(pipeline_id, allowed) => { |
| let pending = self.pending_approval_navigations.remove(&pipeline_id); |
| |
| let webview_id = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.webview_id, |
| None => return warn!("{}: Attempted to navigate after closure", pipeline_id), |
| }; |
| |
| match pending { |
| Some((load_data, history_handling)) => { |
| if allowed { |
| self.load_url(webview_id, pipeline_id, load_data, history_handling); |
| } else { |
| if let Some((sender, id)) = &self.webdriver_load_status_sender { |
| if pipeline_id == *id { |
| let _ = sender.send(WebDriverLoadStatus::NavigationStop); |
| } |
| } |
| |
| let pipeline_is_top_level_pipeline = self |
| .browsing_contexts |
| .get(&BrowsingContextId::from(webview_id)) |
| .map(|ctx| ctx.pipeline_id == pipeline_id) |
| .unwrap_or(false); |
| // If the navigation is refused, and this concerns an iframe, |
| // we need to take it out of it's "delaying-load-events-mode". |
| // https://html.spec.whatwg.org/multipage/#delaying-load-events-mode |
| if !pipeline_is_top_level_pipeline { |
| let msg = |
| ScriptThreadMessage::StopDelayingLoadEventsMode(pipeline_id); |
| let result = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.event_loop.send(msg), |
| None => { |
| return warn!( |
| "{}: Attempted to navigate after closure", |
| pipeline_id |
| ); |
| }, |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(pipeline_id, e); |
| } |
| } |
| } |
| }, |
| None => { |
| warn!( |
| "{}: AllowNavigationResponse for unknown request", |
| pipeline_id |
| ) |
| }, |
| } |
| }, |
| EmbedderToConstellationMessage::ClearCache => { |
| self.public_resource_threads.clear_cache(); |
| self.private_resource_threads.clear_cache(); |
| }, |
| // Load a new page from a typed url |
| // If there is already a pending page (self.pending_changes), it will not be overridden; |
| // However, if the id is not encompassed by another change, it will be. |
| EmbedderToConstellationMessage::LoadUrl(webview_id, url) => { |
| let load_data = LoadData::new( |
| LoadOrigin::Constellation, |
| url, |
| None, |
| Referrer::NoReferrer, |
| ReferrerPolicy::EmptyString, |
| None, |
| None, |
| false, |
| ); |
| let ctx_id = BrowsingContextId::from(webview_id); |
| let pipeline_id = match self.browsing_contexts.get(&ctx_id) { |
| Some(ctx) => ctx.pipeline_id, |
| None => { |
| return warn!("{}: LoadUrl for unknown browsing context", webview_id); |
| }, |
| }; |
| // Since this is a top-level load, initiated by the embedder, go straight to load_url, |
| // bypassing schedule_navigation. |
| self.load_url( |
| webview_id, |
| pipeline_id, |
| load_data, |
| NavigationHistoryBehavior::Push, |
| ); |
| }, |
| EmbedderToConstellationMessage::IsReadyToSaveImage(pipeline_states) => { |
| let is_ready = self.handle_is_ready_to_save_image(pipeline_states); |
| debug!("Ready to save image {:?}.", is_ready); |
| self.compositor_proxy |
| .send(CompositorMsg::IsReadyToSaveImageReply( |
| is_ready == ReadyToSave::Ready, |
| )); |
| }, |
| // Create a new top level browsing context. Will use response_chan to return |
| // the browsing context id. |
| EmbedderToConstellationMessage::NewWebView(url, webview_id, viewport_details) => { |
| self.handle_new_top_level_browsing_context(url, webview_id, viewport_details); |
| }, |
| // Close a top level browsing context. |
| EmbedderToConstellationMessage::CloseWebView(webview_id) => { |
| self.handle_close_top_level_browsing_context(webview_id); |
| }, |
| // Panic a top level browsing context. |
| EmbedderToConstellationMessage::SendError(webview_id, error) => { |
| debug!("constellation got SendError message"); |
| if webview_id.is_none() { |
| warn!("constellation got a SendError message without top level id"); |
| } |
| self.handle_panic(webview_id, error, None); |
| }, |
| EmbedderToConstellationMessage::FocusWebView(webview_id, focus_id) => { |
| self.handle_focus_web_view(webview_id, focus_id); |
| }, |
| EmbedderToConstellationMessage::BlurWebView => { |
| self.webviews.unfocus(); |
| self.embedder_proxy.send(EmbedderMsg::WebViewBlurred); |
| }, |
| // Handle a forward or back request |
| EmbedderToConstellationMessage::TraverseHistory( |
| webview_id, |
| direction, |
| traversal_id, |
| ) => { |
| self.handle_traverse_history_msg(webview_id, direction); |
| self.embedder_proxy |
| .send(EmbedderMsg::HistoryTraversalComplete( |
| webview_id, |
| traversal_id, |
| )); |
| }, |
| EmbedderToConstellationMessage::ChangeViewportDetails( |
| webview_id, |
| new_viewport_details, |
| size_type, |
| ) => { |
| self.handle_change_viewport_details_msg( |
| webview_id, |
| new_viewport_details, |
| size_type, |
| ); |
| }, |
| EmbedderToConstellationMessage::ThemeChange(webview_id, theme) => { |
| self.handle_theme_change(webview_id, theme); |
| }, |
| EmbedderToConstellationMessage::TickAnimation(webview_ids) => { |
| self.handle_tick_animation(webview_ids) |
| }, |
| EmbedderToConstellationMessage::NoLongerWaitingOnAsynchronousImageUpdates( |
| pipeline_ids, |
| ) => self.handle_no_longer_waiting_on_asynchronous_image_updates(pipeline_ids), |
| EmbedderToConstellationMessage::WebDriverCommand(command) => { |
| self.handle_webdriver_msg(command); |
| }, |
| EmbedderToConstellationMessage::Reload(webview_id) => { |
| self.handle_reload_msg(webview_id); |
| }, |
| EmbedderToConstellationMessage::LogEntry(webview_id, thread_name, entry) => { |
| self.handle_log_entry(webview_id, thread_name, entry); |
| }, |
| EmbedderToConstellationMessage::ForwardInputEvent(webview_id, event, hit_test) => { |
| self.forward_input_event(webview_id, event, hit_test); |
| }, |
| EmbedderToConstellationMessage::RefreshCursor(pipeline_id) => { |
| self.handle_refresh_cursor(pipeline_id) |
| }, |
| EmbedderToConstellationMessage::ToggleProfiler(rate, max_duration) => { |
| for background_monitor_control_sender in &self.background_monitor_control_senders { |
| if let Err(e) = background_monitor_control_sender.send( |
| BackgroundHangMonitorControlMsg::ToggleSampler(rate, max_duration), |
| ) { |
| warn!("error communicating with sampling profiler: {}", e); |
| } |
| } |
| }, |
| EmbedderToConstellationMessage::ExitFullScreen(webview_id) => { |
| self.handle_exit_fullscreen_msg(webview_id); |
| }, |
| EmbedderToConstellationMessage::MediaSessionAction(action) => { |
| self.handle_media_session_action_msg(action); |
| }, |
| EmbedderToConstellationMessage::SetWebViewThrottled(webview_id, throttled) => { |
| self.set_webview_throttled(webview_id, throttled); |
| }, |
| EmbedderToConstellationMessage::SetScrollStates(pipeline_id, scroll_states) => { |
| self.handle_set_scroll_states(pipeline_id, scroll_states) |
| }, |
| EmbedderToConstellationMessage::PaintMetric(pipeline_id, paint_metric_event) => { |
| self.handle_paint_metric(pipeline_id, paint_metric_event); |
| }, |
| EmbedderToConstellationMessage::EvaluateJavaScript( |
| webview_id, |
| evaluation_id, |
| script, |
| ) => { |
| self.handle_evaluate_javascript(webview_id, evaluation_id, script); |
| }, |
| EmbedderToConstellationMessage::CreateMemoryReport(sender) => { |
| self.mem_profiler_chan.send(ProfilerMsg::Report(sender)); |
| }, |
| EmbedderToConstellationMessage::SendImageKeysForPipeline(pipeline_id, image_keys) => { |
| if let Some(pipeline) = self.pipelines.get(&pipeline_id) { |
| if pipeline |
| .event_loop |
| .send(ScriptThreadMessage::SendImageKeysBatch( |
| pipeline_id, |
| image_keys, |
| )) |
| .is_err() |
| { |
| warn!("Could not send image keys to pipeline {:?}", pipeline_id); |
| } |
| } else { |
| warn!( |
| "Keys were generated for a pipeline ({:?}) that was |
| closed before the request could be fulfilled.", |
| pipeline_id |
| ) |
| } |
| }, |
| EmbedderToConstellationMessage::SetWebDriverResponseSender(sender) => { |
| self.webdriver_input_command_reponse_sender = Some(sender); |
| }, |
| EmbedderToConstellationMessage::PreferencesUpdated(updates) => { |
| let event_loops = self |
| .pipelines |
| .values() |
| .map(|pipeline| pipeline.event_loop.clone()); |
| for event_loop in event_loops { |
| let _ = event_loop.send(ScriptThreadMessage::PreferencesUpdated( |
| updates |
| .iter() |
| .map(|(name, value)| (String::from(*name), value.clone())) |
| .collect(), |
| )); |
| } |
| }, |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_evaluate_javascript( |
| &mut self, |
| webview_id: WebViewId, |
| evaluation_id: JavaScriptEvaluationId, |
| script: String, |
| ) { |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| let Some(pipeline) = self |
| .browsing_contexts |
| .get(&browsing_context_id) |
| .and_then(|browsing_context| self.pipelines.get(&browsing_context.pipeline_id)) |
| else { |
| self.handle_finish_javascript_evaluation( |
| evaluation_id, |
| Err(JavaScriptEvaluationError::InternalError), |
| ); |
| return; |
| }; |
| |
| if pipeline |
| .event_loop |
| .send(ScriptThreadMessage::EvaluateJavaScript( |
| pipeline.id, |
| evaluation_id, |
| script, |
| )) |
| .is_err() |
| { |
| self.handle_finish_javascript_evaluation( |
| evaluation_id, |
| Err(JavaScriptEvaluationError::InternalError), |
| ); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_request_from_script(&mut self, message: (PipelineId, ScriptToConstellationMessage)) { |
| let (source_pipeline_id, content) = message; |
| trace_script_msg!(content, "{source_pipeline_id}: {content:?}"); |
| |
| let webview_id = match self |
| .pipelines |
| .get(&source_pipeline_id) |
| .map(|pipeline| pipeline.webview_id) |
| { |
| None => return warn!("{}: ScriptMsg from closed pipeline", source_pipeline_id), |
| Some(ctx) => ctx, |
| }; |
| |
| match content { |
| ScriptToConstellationMessage::CompleteMessagePortTransfer(router_id, ports) => { |
| self.handle_complete_message_port_transfer(router_id, ports); |
| }, |
| ScriptToConstellationMessage::MessagePortTransferResult( |
| router_id, |
| succeeded, |
| failed, |
| ) => { |
| self.handle_message_port_transfer_completed(router_id, succeeded); |
| self.handle_message_port_transfer_failed(failed); |
| }, |
| ScriptToConstellationMessage::RerouteMessagePort(port_id, task) => { |
| self.handle_reroute_messageport(port_id, task); |
| }, |
| ScriptToConstellationMessage::MessagePortShipped(port_id) => { |
| self.handle_messageport_shipped(port_id); |
| }, |
| ScriptToConstellationMessage::NewMessagePortRouter(router_id, ipc_sender) => { |
| self.handle_new_messageport_router(router_id, ipc_sender); |
| }, |
| ScriptToConstellationMessage::RemoveMessagePortRouter(router_id) => { |
| self.handle_remove_messageport_router(router_id); |
| }, |
| ScriptToConstellationMessage::NewMessagePort(router_id, port_id) => { |
| self.handle_new_messageport(router_id, port_id); |
| }, |
| ScriptToConstellationMessage::EntanglePorts(port1, port2) => { |
| self.handle_entangle_messageports(port1, port2); |
| }, |
| ScriptToConstellationMessage::DisentanglePorts(port1, port2) => { |
| self.handle_disentangle_messageports(port1, port2); |
| }, |
| ScriptToConstellationMessage::NewBroadcastChannelRouter( |
| router_id, |
| response_sender, |
| origin, |
| ) => { |
| if self |
| .check_origin_against_pipeline(&source_pipeline_id, &origin) |
| .is_err() |
| { |
| return warn!("Attempt to add broadcast router from an unexpected origin."); |
| } |
| self.broadcast_channels |
| .new_broadcast_channel_router(router_id, response_sender); |
| }, |
| ScriptToConstellationMessage::NewBroadcastChannelNameInRouter( |
| router_id, |
| channel_name, |
| origin, |
| ) => { |
| if self |
| .check_origin_against_pipeline(&source_pipeline_id, &origin) |
| .is_err() |
| { |
| return warn!("Attempt to add channel name from an unexpected origin."); |
| } |
| self.broadcast_channels |
| .new_broadcast_channel_name_in_router(router_id, channel_name, origin); |
| }, |
| ScriptToConstellationMessage::RemoveBroadcastChannelNameInRouter( |
| router_id, |
| channel_name, |
| origin, |
| ) => { |
| if self |
| .check_origin_against_pipeline(&source_pipeline_id, &origin) |
| .is_err() |
| { |
| return warn!("Attempt to remove channel name from an unexpected origin."); |
| } |
| self.broadcast_channels |
| .remove_broadcast_channel_name_in_router(router_id, channel_name, origin); |
| }, |
| ScriptToConstellationMessage::RemoveBroadcastChannelRouter(router_id, origin) => { |
| if self |
| .check_origin_against_pipeline(&source_pipeline_id, &origin) |
| .is_err() |
| { |
| return warn!("Attempt to remove broadcast router from an unexpected origin."); |
| } |
| self.broadcast_channels |
| .remove_broadcast_channel_router(router_id); |
| }, |
| ScriptToConstellationMessage::ScheduleBroadcast(router_id, message) => { |
| if self |
| .check_origin_against_pipeline(&source_pipeline_id, &message.origin) |
| .is_err() |
| { |
| return warn!( |
| "Attempt to schedule broadcast from an origin not matching the origin of the msg." |
| ); |
| } |
| self.broadcast_channels |
| .schedule_broadcast(router_id, message); |
| }, |
| ScriptToConstellationMessage::PipelineExited => { |
| self.handle_pipeline_exited(source_pipeline_id); |
| }, |
| ScriptToConstellationMessage::DiscardDocument => { |
| self.handle_discard_document(webview_id, source_pipeline_id); |
| }, |
| ScriptToConstellationMessage::DiscardTopLevelBrowsingContext => { |
| self.handle_close_top_level_browsing_context(webview_id); |
| }, |
| ScriptToConstellationMessage::ScriptLoadedURLInIFrame(load_info) => { |
| self.handle_script_loaded_url_in_iframe_msg(load_info); |
| }, |
| ScriptToConstellationMessage::ScriptNewIFrame(load_info) => { |
| self.handle_script_new_iframe(load_info); |
| }, |
| ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info) => { |
| self.handle_script_new_auxiliary(load_info); |
| }, |
| ScriptToConstellationMessage::ChangeRunningAnimationsState(animation_state) => { |
| self.handle_change_running_animations_state(source_pipeline_id, animation_state) |
| }, |
| // Ask the embedder for permission to load a new page. |
| ScriptToConstellationMessage::LoadUrl(load_data, history_handling) => { |
| self.schedule_navigation( |
| webview_id, |
| source_pipeline_id, |
| load_data, |
| history_handling, |
| ); |
| }, |
| ScriptToConstellationMessage::AbortLoadUrl => { |
| self.handle_abort_load_url_msg(source_pipeline_id); |
| }, |
| // A page loaded has completed all parsing, script, and reflow messages have been sent. |
| ScriptToConstellationMessage::LoadComplete => { |
| self.handle_load_complete_msg(webview_id, source_pipeline_id) |
| }, |
| // Handle navigating to a fragment |
| ScriptToConstellationMessage::NavigatedToFragment(new_url, replacement_enabled) => { |
| self.handle_navigated_to_fragment(source_pipeline_id, new_url, replacement_enabled); |
| }, |
| // Handle a forward or back request |
| ScriptToConstellationMessage::TraverseHistory(direction) => { |
| self.handle_traverse_history_msg(webview_id, direction); |
| }, |
| // Handle a push history state request. |
| ScriptToConstellationMessage::PushHistoryState(history_state_id, url) => { |
| self.handle_push_history_state_msg(source_pipeline_id, history_state_id, url); |
| }, |
| ScriptToConstellationMessage::ReplaceHistoryState(history_state_id, url) => { |
| self.handle_replace_history_state_msg(source_pipeline_id, history_state_id, url); |
| }, |
| // Handle a joint session history length request. |
| ScriptToConstellationMessage::JointSessionHistoryLength(response_sender) => { |
| self.handle_joint_session_history_length(webview_id, response_sender); |
| }, |
| // Notification that the new document is ready to become active |
| ScriptToConstellationMessage::ActivateDocument => { |
| self.handle_activate_document_msg(source_pipeline_id); |
| }, |
| // Update pipeline url after redirections |
| ScriptToConstellationMessage::SetFinalUrl(final_url) => { |
| // The script may have finished loading after we already started shutting down. |
| if let Some(ref mut pipeline) = self.pipelines.get_mut(&source_pipeline_id) { |
| pipeline.url = final_url; |
| } else { |
| warn!("constellation got set final url message for dead pipeline"); |
| } |
| }, |
| ScriptToConstellationMessage::PostMessage { |
| target: browsing_context_id, |
| source: source_pipeline_id, |
| target_origin: origin, |
| source_origin, |
| data, |
| } => { |
| self.handle_post_message_msg( |
| browsing_context_id, |
| source_pipeline_id, |
| origin, |
| source_origin, |
| data, |
| ); |
| }, |
| ScriptToConstellationMessage::Focus(focused_child_browsing_context_id, sequence) => { |
| self.handle_focus_msg( |
| source_pipeline_id, |
| focused_child_browsing_context_id, |
| sequence, |
| ); |
| }, |
| ScriptToConstellationMessage::FocusRemoteDocument(focused_browsing_context_id) => { |
| self.handle_focus_remote_document_msg(focused_browsing_context_id); |
| }, |
| ScriptToConstellationMessage::SetThrottledComplete(throttled) => { |
| self.handle_set_throttled_complete(source_pipeline_id, throttled); |
| }, |
| ScriptToConstellationMessage::RemoveIFrame(browsing_context_id, response_sender) => { |
| let removed_pipeline_ids = self.handle_remove_iframe_msg(browsing_context_id); |
| if let Err(e) = response_sender.send(removed_pipeline_ids) { |
| warn!("Error replying to remove iframe ({})", e); |
| } |
| }, |
| ScriptToConstellationMessage::CreateCanvasPaintThread(size, response_sender) => { |
| self.handle_create_canvas_paint_thread_msg(size, response_sender) |
| }, |
| ScriptToConstellationMessage::SetDocumentState(state) => { |
| self.document_states.insert(source_pipeline_id, state); |
| }, |
| ScriptToConstellationMessage::SetLayoutEpoch(epoch, response_sender) => { |
| if let Some(pipeline) = self.pipelines.get_mut(&source_pipeline_id) { |
| pipeline.layout_epoch = epoch; |
| } |
| |
| response_sender.send(true).unwrap_or_default(); |
| }, |
| ScriptToConstellationMessage::LogEntry(thread_name, entry) => { |
| self.handle_log_entry(Some(webview_id), thread_name, entry); |
| }, |
| ScriptToConstellationMessage::TouchEventProcessed(result) => self |
| .compositor_proxy |
| .send(CompositorMsg::TouchEventProcessed(webview_id, result)), |
| ScriptToConstellationMessage::GetBrowsingContextInfo(pipeline_id, response_sender) => { |
| let result = self |
| .pipelines |
| .get(&pipeline_id) |
| .and_then(|pipeline| self.browsing_contexts.get(&pipeline.browsing_context_id)) |
| .map(|ctx| (ctx.id, ctx.parent_pipeline_id)); |
| if let Err(e) = response_sender.send(result) { |
| warn!( |
| "Sending reply to get browsing context info failed ({:?}).", |
| e |
| ); |
| } |
| }, |
| ScriptToConstellationMessage::GetTopForBrowsingContext( |
| browsing_context_id, |
| response_sender, |
| ) => { |
| let result = self |
| .browsing_contexts |
| .get(&browsing_context_id) |
| .map(|bc| bc.top_level_id); |
| if let Err(e) = response_sender.send(result) { |
| warn!( |
| "Sending reply to get top for browsing context info failed ({:?}).", |
| e |
| ); |
| } |
| }, |
| ScriptToConstellationMessage::GetChildBrowsingContextId( |
| browsing_context_id, |
| index, |
| response_sender, |
| ) => { |
| let result = self |
| .browsing_contexts |
| .get(&browsing_context_id) |
| .and_then(|bc| self.pipelines.get(&bc.pipeline_id)) |
| .and_then(|pipeline| pipeline.children.get(index)) |
| .copied(); |
| if let Err(e) = response_sender.send(result) { |
| warn!( |
| "Sending reply to get child browsing context ID failed ({:?}).", |
| e |
| ); |
| } |
| }, |
| ScriptToConstellationMessage::ScheduleJob(job) => { |
| self.handle_schedule_serviceworker_job(source_pipeline_id, job); |
| }, |
| ScriptToConstellationMessage::ForwardDOMMessage(msg_vec, scope_url) => { |
| if let Some(mgr) = self.sw_managers.get(&scope_url.origin()) { |
| let _ = mgr.send(ServiceWorkerMsg::ForwardDOMMessage(msg_vec, scope_url)); |
| } else { |
| warn!("Unable to forward DOMMessage for postMessage call"); |
| } |
| }, |
| ScriptToConstellationMessage::BroadcastStorageEvent( |
| storage, |
| url, |
| key, |
| old_value, |
| new_value, |
| ) => { |
| self.handle_broadcast_storage_event( |
| source_pipeline_id, |
| storage, |
| url, |
| key, |
| old_value, |
| new_value, |
| ); |
| }, |
| ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => { |
| // Unlikely at this point, but we may receive events coming from |
| // different media sessions, so we set the active media session based |
| // on Playing events. |
| // The last media session claiming to be in playing state is set to |
| // the active media session. |
| // Events coming from inactive media sessions are discarded. |
| if self.active_media_session.is_some() { |
| if let MediaSessionEvent::PlaybackStateChange(ref state) = event { |
| if !matches!( |
| state, |
| MediaSessionPlaybackState::Playing | MediaSessionPlaybackState::Paused |
| ) { |
| return; |
| } |
| }; |
| } |
| self.active_media_session = Some(pipeline_id); |
| self.embedder_proxy |
| .send(EmbedderMsg::MediaSessionEvent(webview_id, event)); |
| }, |
| #[cfg(feature = "webgpu")] |
| ScriptToConstellationMessage::RequestAdapter(response_sender, options, ids) => self |
| .handle_wgpu_request( |
| source_pipeline_id, |
| BrowsingContextId::from(webview_id), |
| ScriptToConstellationMessage::RequestAdapter(response_sender, options, ids), |
| ), |
| #[cfg(feature = "webgpu")] |
| ScriptToConstellationMessage::GetWebGPUChan(response_sender) => self |
| .handle_wgpu_request( |
| source_pipeline_id, |
| BrowsingContextId::from(webview_id), |
| ScriptToConstellationMessage::GetWebGPUChan(response_sender), |
| ), |
| ScriptToConstellationMessage::TitleChanged(pipeline, title) => { |
| if let Some(pipeline) = self.pipelines.get_mut(&pipeline) { |
| pipeline.title = title; |
| } |
| }, |
| ScriptToConstellationMessage::IFrameSizes(iframe_sizes) => { |
| self.handle_iframe_size_msg(iframe_sizes) |
| }, |
| ScriptToConstellationMessage::ReportMemory(sender) => { |
| // get memory report and send it back. |
| self.mem_profiler_chan |
| .send(mem::ProfilerMsg::Report(sender)); |
| }, |
| ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result) => { |
| self.handle_finish_javascript_evaluation(evaluation_id, result) |
| }, |
| ScriptToConstellationMessage::WebDriverInputComplete(msg_id) => { |
| if let Some(ref reply_sender) = self.webdriver_input_command_reponse_sender { |
| reply_sender |
| .send(WebDriverCommandResponse { id: msg_id }) |
| .unwrap_or_else(|_| { |
| warn!("Failed to send WebDriverInputComplete {:?}", msg_id); |
| }); |
| } else { |
| warn!("No webdriver_input_command_reponse_sender"); |
| } |
| }, |
| } |
| } |
| |
| /// Check the origin of a message against that of the pipeline it came from. |
| /// Note: this is still limited as a security check, |
| /// see <https://github.com/servo/servo/issues/11722> |
| fn check_origin_against_pipeline( |
| &self, |
| pipeline_id: &PipelineId, |
| origin: &ImmutableOrigin, |
| ) -> Result<(), ()> { |
| let pipeline_origin = match self.pipelines.get(pipeline_id) { |
| Some(pipeline) => pipeline.load_data.url.origin(), |
| None => { |
| warn!("Received message from closed or unknown pipeline."); |
| return Err(()); |
| }, |
| }; |
| if &pipeline_origin == origin { |
| return Ok(()); |
| } |
| Err(()) |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| #[cfg(feature = "webgpu")] |
| fn handle_wgpu_request( |
| &mut self, |
| source_pipeline_id: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| request: ScriptToConstellationMessage, |
| ) { |
| use webgpu::start_webgpu_thread; |
| |
| let browsing_context_group_id = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(bc) => &bc.bc_group_id, |
| None => return warn!("Browsing context not found"), |
| }; |
| let source_pipeline = match self.pipelines.get(&source_pipeline_id) { |
| Some(pipeline) => pipeline, |
| None => return warn!("{}: ScriptMsg from closed pipeline", source_pipeline_id), |
| }; |
| let host = match reg_host(&source_pipeline.url) { |
| Some(host) => host, |
| None => return warn!("Invalid host url"), |
| }; |
| let browsing_context_group = if let Some(bcg) = self |
| .browsing_context_group_set |
| .get_mut(browsing_context_group_id) |
| { |
| bcg |
| } else { |
| return warn!("Browsing context group not found"); |
| }; |
| let webgpu_chan = match browsing_context_group.webgpus.entry(host) { |
| Entry::Vacant(v) => start_webgpu_thread( |
| self.compositor_proxy.cross_process_compositor_api.clone(), |
| self.webrender_wgpu.webrender_external_images.clone(), |
| self.webrender_wgpu.wgpu_image_map.clone(), |
| ) |
| .map(|webgpu| { |
| let msg = ScriptThreadMessage::SetWebGPUPort(webgpu.1); |
| if let Err(e) = source_pipeline.event_loop.send(msg) { |
| warn!( |
| "{}: Failed to send SetWebGPUPort to pipeline ({:?})", |
| source_pipeline_id, e |
| ); |
| } |
| v.insert(webgpu.0).clone() |
| }), |
| Entry::Occupied(o) => Some(o.get().clone()), |
| }; |
| match request { |
| ScriptToConstellationMessage::RequestAdapter(response_sender, options, adapter_id) => { |
| match webgpu_chan { |
| None => { |
| if let Err(e) = response_sender.send(None) { |
| warn!("Failed to send request adapter message: {}", e) |
| } |
| }, |
| Some(webgpu_chan) => { |
| let adapter_request = WebGPURequest::RequestAdapter { |
| sender: response_sender, |
| options, |
| adapter_id, |
| }; |
| if webgpu_chan.0.send(adapter_request).is_err() { |
| warn!("Failed to send request adapter message on WebGPU channel"); |
| } |
| }, |
| } |
| }, |
| ScriptToConstellationMessage::GetWebGPUChan(response_sender) => { |
| if response_sender.send(webgpu_chan).is_err() { |
| warn!( |
| "{}: Failed to send WebGPU channel to pipeline", |
| source_pipeline_id |
| ) |
| } |
| }, |
| _ => warn!("Wrong message type in handle_wgpu_request"), |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_message_port_transfer_completed( |
| &mut self, |
| router_id: Option<MessagePortRouterId>, |
| ports: Vec<MessagePortId>, |
| ) { |
| let router_id = match router_id { |
| Some(router_id) => router_id, |
| None => { |
| if !ports.is_empty() { |
| warn!( |
| "Constellation unable to process port transfer successes, since no router id was received" |
| ); |
| } |
| return; |
| }, |
| }; |
| for port_id in ports.into_iter() { |
| let mut entry = match self.message_ports.entry(port_id) { |
| Entry::Vacant(_) => { |
| warn!( |
| "Constellation received a port transfer completed msg for unknown messageport {:?}", |
| port_id |
| ); |
| continue; |
| }, |
| Entry::Occupied(entry) => entry, |
| }; |
| match entry.get().state { |
| TransferState::CompletionInProgress(expected_router_id) => { |
| // Here, the transfer was normally completed. |
| |
| if expected_router_id != router_id { |
| return warn!( |
| "Transfer completed by an unexpected router: {:?}", |
| router_id |
| ); |
| } |
| // Update the state to managed. |
| let new_info = MessagePortInfo { |
| state: TransferState::Managed(router_id), |
| entangled_with: entry.get().entangled_with, |
| }; |
| entry.insert(new_info); |
| }, |
| _ => warn!("Constellation received unexpected port transfer completed message"), |
| } |
| } |
| } |
| |
| fn handle_message_port_transfer_failed( |
| &mut self, |
| ports: FnvHashMap<MessagePortId, PortTransferInfo>, |
| ) { |
| for (port_id, mut transfer_info) in ports.into_iter() { |
| let entry = match self.message_ports.remove(&port_id) { |
| None => { |
| warn!( |
| "Constellation received a port transfer completed msg for unknown messageport {:?}", |
| port_id |
| ); |
| continue; |
| }, |
| Some(entry) => entry, |
| }; |
| let new_info = match entry.state { |
| TransferState::CompletionFailed(mut current_buffer) => { |
| // The transfer failed, |
| // and now the global has returned us the buffer we previously sent. |
| // So the next update is back to a "normal" transfer in progress. |
| |
| // Tasks in the previous buffer are older, |
| // hence need to be added to the front of the current one. |
| while let Some(task) = transfer_info.port_message_queue.pop_back() { |
| current_buffer.push_front(task); |
| } |
| // Update the state to transfer-in-progress. |
| MessagePortInfo { |
| state: TransferState::TransferInProgress(current_buffer), |
| entangled_with: entry.entangled_with, |
| } |
| }, |
| TransferState::CompletionRequested(target_router_id, mut current_buffer) => { |
| // Here, before the global who failed the last transfer could return us the buffer, |
| // another global already sent us a request to complete a new transfer. |
| // So we use the returned buffer to update |
| // the current-buffer(of new incoming messages), |
| // and we send everything to the global |
| // who is waiting for completion of the current transfer. |
| |
| // Tasks in the previous buffer are older, |
| // hence need to be added to the front of the current one. |
| while let Some(task) = transfer_info.port_message_queue.pop_back() { |
| current_buffer.push_front(task); |
| } |
| // Forward the buffered message-queue to complete the current transfer. |
| if let Some(ipc_sender) = self.message_port_routers.get(&target_router_id) { |
| if ipc_sender |
| .send(MessagePortMsg::CompletePendingTransfer( |
| port_id, |
| PortTransferInfo { |
| port_message_queue: current_buffer, |
| disentangled: entry.entangled_with.is_none(), |
| }, |
| )) |
| .is_err() |
| { |
| warn!("Constellation failed to send complete port transfer response."); |
| } |
| } else { |
| warn!("No message-port sender for {:?}", target_router_id); |
| } |
| |
| // Update the state to completion-in-progress. |
| MessagePortInfo { |
| state: TransferState::CompletionInProgress(target_router_id), |
| entangled_with: entry.entangled_with, |
| } |
| }, |
| _ => { |
| warn!("Unexpected port transfer failed message received"); |
| continue; |
| }, |
| }; |
| self.message_ports.insert(port_id, new_info); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_complete_message_port_transfer( |
| &mut self, |
| router_id: MessagePortRouterId, |
| ports: Vec<MessagePortId>, |
| ) { |
| let mut response = FnvHashMap::default(); |
| for port_id in ports.into_iter() { |
| let entry = match self.message_ports.remove(&port_id) { |
| None => { |
| warn!( |
| "Constellation asked to complete transfer for unknown messageport {:?}", |
| port_id |
| ); |
| continue; |
| }, |
| Some(entry) => entry, |
| }; |
| let new_info = match entry.state { |
| TransferState::TransferInProgress(buffer) => { |
| response.insert( |
| port_id, |
| PortTransferInfo { |
| port_message_queue: buffer, |
| disentangled: entry.entangled_with.is_none(), |
| }, |
| ); |
| |
| // If the port was in transfer, and a global is requesting completion, |
| // we note the start of the completion. |
| MessagePortInfo { |
| state: TransferState::CompletionInProgress(router_id), |
| entangled_with: entry.entangled_with, |
| } |
| }, |
| TransferState::CompletionFailed(buffer) | |
| TransferState::CompletionRequested(_, buffer) => { |
| // If the completion had already failed, |
| // this is a request coming from a global to complete a new transfer, |
| // but we're still awaiting the return of the buffer |
| // from the first global who failed. |
| // |
| // So we note the request from the new global, |
| // and continue to buffer incoming messages |
| // and wait for the buffer used in the previous transfer to be returned. |
| // |
| // If another global requests completion in the CompletionRequested state, |
| // we simply swap the target router-id for the new one, |
| // keeping the buffer. |
| MessagePortInfo { |
| state: TransferState::CompletionRequested(router_id, buffer), |
| entangled_with: entry.entangled_with, |
| } |
| }, |
| _ => { |
| warn!("Unexpected complete port transfer message received"); |
| continue; |
| }, |
| }; |
| self.message_ports.insert(port_id, new_info); |
| } |
| |
| if !response.is_empty() { |
| // Forward the buffered message-queue. |
| if let Some(ipc_sender) = self.message_port_routers.get(&router_id) { |
| if ipc_sender |
| .send(MessagePortMsg::CompleteTransfer(response)) |
| .is_err() |
| { |
| warn!("Constellation failed to send complete port transfer response."); |
| } |
| } else { |
| warn!("No message-port sender for {:?}", router_id); |
| } |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_reroute_messageport(&mut self, port_id: MessagePortId, task: PortMessageTask) { |
| let info = match self.message_ports.get_mut(&port_id) { |
| Some(info) => info, |
| None => { |
| return warn!( |
| "Constellation asked to re-route msg to unknown messageport {:?}", |
| port_id |
| ); |
| }, |
| }; |
| match &mut info.state { |
| TransferState::Managed(router_id) | TransferState::CompletionInProgress(router_id) => { |
| // In both the managed and completion of a transfer case, we forward the message. |
| // Note that in both cases, if the port is transferred before the message is handled, |
| // it will be sent back here and buffered while the transfer is ongoing. |
| if let Some(ipc_sender) = self.message_port_routers.get(router_id) { |
| let _ = ipc_sender.send(MessagePortMsg::NewTask(port_id, task)); |
| } else { |
| warn!("No message-port sender for {:?}", router_id); |
| } |
| }, |
| TransferState::TransferInProgress(queue) => queue.push_back(task), |
| TransferState::CompletionFailed(queue) => queue.push_back(task), |
| TransferState::CompletionRequested(_, queue) => queue.push_back(task), |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_messageport_shipped(&mut self, port_id: MessagePortId) { |
| if let Some(info) = self.message_ports.get_mut(&port_id) { |
| match info.state { |
| TransferState::Managed(_) => { |
| // If shipped while managed, note the start of a transfer. |
| info.state = TransferState::TransferInProgress(VecDeque::new()); |
| }, |
| TransferState::CompletionInProgress(_) => { |
| // If shipped while completion of a transfer was in progress, |
| // the completion failed. |
| // This will be followed by a MessagePortTransferFailed message, |
| // containing the buffer we previously sent. |
| info.state = TransferState::CompletionFailed(VecDeque::new()); |
| }, |
| _ => warn!("Unexpected messageport shipped received"), |
| } |
| } else { |
| warn!( |
| "Constellation asked to mark unknown messageport as shipped {:?}", |
| port_id |
| ); |
| } |
| } |
| |
| fn handle_new_messageport_router( |
| &mut self, |
| router_id: MessagePortRouterId, |
| message_port_ipc_sender: IpcSender<MessagePortMsg>, |
| ) { |
| self.message_port_routers |
| .insert(router_id, message_port_ipc_sender); |
| } |
| |
| fn handle_remove_messageport_router(&mut self, router_id: MessagePortRouterId) { |
| self.message_port_routers.remove(&router_id); |
| } |
| |
| fn handle_new_messageport(&mut self, router_id: MessagePortRouterId, port_id: MessagePortId) { |
| match self.message_ports.entry(port_id) { |
| // If it's a new port, we should not know about it. |
| Entry::Occupied(_) => warn!( |
| "Constellation asked to start tracking an existing messageport {:?}", |
| port_id |
| ), |
| Entry::Vacant(entry) => { |
| let info = MessagePortInfo { |
| state: TransferState::Managed(router_id), |
| entangled_with: None, |
| }; |
| entry.insert(info); |
| }, |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_entangle_messageports(&mut self, port1: MessagePortId, port2: MessagePortId) { |
| if let Some(info) = self.message_ports.get_mut(&port1) { |
| info.entangled_with = Some(port2); |
| } else { |
| warn!( |
| "Constellation asked to entangle unknown messageport: {:?}", |
| port1 |
| ); |
| } |
| if let Some(info) = self.message_ports.get_mut(&port2) { |
| info.entangled_with = Some(port1); |
| } else { |
| warn!( |
| "Constellation asked to entangle unknown messageport: {:?}", |
| port2 |
| ); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| /// <https://html.spec.whatwg.org/multipage/#disentangle> |
| fn handle_disentangle_messageports( |
| &mut self, |
| port1: MessagePortId, |
| port2: Option<MessagePortId>, |
| ) { |
| // Disentangle initiatorPort and otherPort, |
| // so that they are no longer entangled or associated with each other. |
| // Note: If `port2` is some, then this is the first message |
| // and `port1` is the initiatorPort, `port2` is the otherPort. |
| // We can immediately remove the initiator. |
| let _ = self.message_ports.remove(&port1); |
| |
| // Note: the none case is when otherPort sent this message |
| // in response to completing its own local disentanglement. |
| let Some(port2) = port2 else { |
| return; |
| }; |
| |
| // Start disentanglement of the other port. |
| if let Some(info) = self.message_ports.get_mut(&port2) { |
| info.entangled_with = None; |
| match &mut info.state { |
| TransferState::Managed(router_id) | |
| TransferState::CompletionInProgress(router_id) => { |
| // We try to disentangle the other port now, |
| // and if it has been transfered out by the time the message is received, |
| // it will be ignored, |
| // and disentanglement will be completed as part of the transfer. |
| if let Some(ipc_sender) = self.message_port_routers.get(router_id) { |
| let _ = ipc_sender.send(MessagePortMsg::CompleteDisentanglement(port2)); |
| } else { |
| warn!("No message-port sender for {:?}", router_id); |
| } |
| }, |
| _ => { |
| // Note: the port is in transfer, disentanglement will complete along with it. |
| }, |
| } |
| } else { |
| warn!( |
| "Constellation asked to disentangle unknown messageport: {:?}", |
| port2 |
| ); |
| } |
| } |
| |
| /// <https://w3c.github.io/ServiceWorker/#schedule-job-algorithm> |
| /// and |
| /// <https://w3c.github.io/ServiceWorker/#dfn-job-queue> |
| /// |
| /// The Job Queue is essentially the channel to a SW manager, |
| /// which are scoped per origin. |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_schedule_serviceworker_job(&mut self, pipeline_id: PipelineId, job: Job) { |
| let origin = job.scope_url.origin(); |
| |
| if self |
| .check_origin_against_pipeline(&pipeline_id, &origin) |
| .is_err() |
| { |
| return warn!( |
| "Attempt to schedule a serviceworker job from an origin not matching the origin of the job." |
| ); |
| } |
| |
| // This match is equivalent to Entry.or_insert_with but allows for early return. |
| let sw_manager = match self.sw_managers.entry(origin.clone()) { |
| Entry::Occupied(entry) => entry.into_mut(), |
| Entry::Vacant(entry) => { |
| let (own_sender, receiver) = |
| generic_channel::channel().expect("Failed to create IPC channel!"); |
| |
| let sw_senders = SWManagerSenders { |
| swmanager_sender: self.swmanager_ipc_sender.clone(), |
| resource_threads: self.public_resource_threads.clone(), |
| own_sender: own_sender.clone(), |
| receiver, |
| compositor_api: self.compositor_proxy.cross_process_compositor_api.clone(), |
| system_font_service_sender: self.system_font_service.to_sender(), |
| }; |
| |
| if opts::get().multiprocess { |
| let (sender, receiver) = |
| ipc::channel().expect("Failed to create lifeline channel for sw"); |
| let content = |
| ServiceWorkerUnprivilegedContent::new(sw_senders, origin, Some(sender)); |
| |
| if let Ok(process) = content.spawn_multiprocess() { |
| let crossbeam_receiver = |
| route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors( |
| receiver, |
| ); |
| self.process_manager.add(crossbeam_receiver, process); |
| } else { |
| return warn!("Failed to spawn process for SW manager."); |
| } |
| } else { |
| let content = ServiceWorkerUnprivilegedContent::new(sw_senders, origin, None); |
| content.start::<SWF>(); |
| } |
| entry.insert(own_sender) |
| }, |
| }; |
| let _ = sw_manager.send(ServiceWorkerMsg::ScheduleJob(job)); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_broadcast_storage_event( |
| &self, |
| pipeline_id: PipelineId, |
| storage: StorageType, |
| url: ServoUrl, |
| key: Option<String>, |
| old_value: Option<String>, |
| new_value: Option<String>, |
| ) { |
| let origin = url.origin(); |
| for pipeline in self.pipelines.values() { |
| if (pipeline.id != pipeline_id) && (pipeline.url.origin() == origin) { |
| let msg = ScriptThreadMessage::DispatchStorageEvent( |
| pipeline.id, |
| storage, |
| url.clone(), |
| key.clone(), |
| old_value.clone(), |
| new_value.clone(), |
| ); |
| if let Err(err) = pipeline.event_loop.send(msg) { |
| warn!( |
| "{}: Failed to broadcast storage event to pipeline ({:?}).", |
| pipeline.id, err |
| ); |
| } |
| } |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_exit(&mut self) { |
| debug!("Handling exit."); |
| |
| // TODO: add a timer, which forces shutdown if threads aren't responsive. |
| if self.shutting_down { |
| return; |
| } |
| self.shutting_down = true; |
| |
| self.mem_profiler_chan.send(mem::ProfilerMsg::Exit); |
| |
| // Tell all BHMs to exit, |
| // and to ensure their monitored components exit |
| // even when currently hanging(on JS or sync XHR). |
| // This must be done before starting the process of closing all pipelines. |
| for chan in &self.background_monitor_control_senders { |
| // Note: the bhm worker thread will continue to run |
| // until all monitored components have exited, |
| // at which point we can join on the thread(done in `handle_shutdown`). |
| if let Err(e) = chan.send(BackgroundHangMonitorControlMsg::Exit) { |
| warn!("error communicating with bhm: {}", e); |
| continue; |
| } |
| } |
| |
| // Close the top-level browsing contexts |
| let browsing_context_ids: Vec<BrowsingContextId> = self |
| .browsing_contexts |
| .values() |
| .filter(|browsing_context| browsing_context.is_top_level()) |
| .map(|browsing_context| browsing_context.id) |
| .collect(); |
| for browsing_context_id in browsing_context_ids { |
| debug!( |
| "{}: Removing top-level browsing context", |
| browsing_context_id |
| ); |
| self.close_browsing_context(browsing_context_id, ExitPipelineMode::Normal); |
| } |
| |
| // Close any pending changes and pipelines |
| while let Some(pending) = self.pending_changes.pop() { |
| debug!( |
| "{}: Removing pending browsing context", |
| pending.browsing_context_id |
| ); |
| self.close_browsing_context(pending.browsing_context_id, ExitPipelineMode::Normal); |
| debug!("{}: Removing pending pipeline", pending.new_pipeline_id); |
| self.close_pipeline( |
| pending.new_pipeline_id, |
| DiscardBrowsingContext::Yes, |
| ExitPipelineMode::Normal, |
| ); |
| } |
| |
| // In case there are browsing contexts which weren't attached, we close them. |
| let browsing_context_ids: Vec<BrowsingContextId> = |
| self.browsing_contexts.keys().cloned().collect(); |
| for browsing_context_id in browsing_context_ids { |
| debug!( |
| "{}: Removing detached browsing context", |
| browsing_context_id |
| ); |
| self.close_browsing_context(browsing_context_id, ExitPipelineMode::Normal); |
| } |
| |
| // In case there are pipelines which weren't attached to the pipeline tree, we close them. |
| let pipeline_ids: Vec<PipelineId> = self.pipelines.keys().cloned().collect(); |
| for pipeline_id in pipeline_ids { |
| debug!("{}: Removing detached pipeline", pipeline_id); |
| self.close_pipeline( |
| pipeline_id, |
| DiscardBrowsingContext::Yes, |
| ExitPipelineMode::Normal, |
| ); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_shutdown(&mut self) { |
| debug!("Handling shutdown."); |
| |
| // In single process mode, join on script-threads |
| // from webview which haven't been manually closed before. |
| for (_, join_handle) in self.script_join_handles.drain() { |
| if join_handle.join().is_err() { |
| error!("Failed to join on a script-thread."); |
| } |
| } |
| |
| // In single process mode, join on the background hang monitor worker thread. |
| drop(self.background_monitor_register.take()); |
| if let Some(join_handle) = self.background_monitor_register_join_handle.take() { |
| if join_handle.join().is_err() { |
| error!("Failed to join on the bhm background thread."); |
| } |
| } |
| |
| // At this point, there are no active pipelines, |
| // so we can safely block on other threads, without worrying about deadlock. |
| // Channels to receive signals when threads are done exiting. |
| let (core_ipc_sender, core_ipc_receiver) = |
| ipc::channel().expect("Failed to create IPC channel!"); |
| let (storage_ipc_sender, storage_ipc_receiver) = |
| generic_channel::channel().expect("Failed to create IPC channel!"); |
| let mut webgl_threads_receiver = None; |
| |
| debug!("Exiting core resource threads."); |
| if let Err(e) = self |
| .public_resource_threads |
| .send(net_traits::CoreResourceMsg::Exit(core_ipc_sender)) |
| { |
| warn!("Exit resource thread failed ({})", e); |
| } |
| |
| if let Some(ref chan) = self.devtools_sender { |
| debug!("Exiting devtools."); |
| let msg = DevtoolsControlMsg::FromChrome(ChromeToDevtoolsControlMsg::ServerExitMsg); |
| if let Err(e) = chan.send(msg) { |
| warn!("Exit devtools failed ({:?})", e); |
| } |
| } |
| |
| debug!("Exiting storage resource threads."); |
| if let Err(e) = generic_channel::GenericSend::send( |
| &self.public_resource_threads, |
| StorageThreadMsg::Exit(storage_ipc_sender), |
| ) { |
| warn!("Exit storage thread failed ({})", e); |
| } |
| |
| #[cfg(feature = "bluetooth")] |
| { |
| debug!("Exiting bluetooth thread."); |
| if let Err(e) = self.bluetooth_ipc_sender.send(BluetoothRequest::Exit) { |
| warn!("Exit bluetooth thread failed ({})", e); |
| } |
| } |
| |
| debug!("Exiting service worker manager thread."); |
| for (_, mgr) in self.sw_managers.drain() { |
| if let Err(e) = mgr.send(ServiceWorkerMsg::Exit) { |
| warn!("Exit service worker manager failed ({})", e); |
| } |
| } |
| |
| let canvas_exit_receiver = if let Some((canvas_sender, _)) = self.canvas.get() { |
| debug!("Exiting Canvas Paint thread."); |
| let (canvas_exit_sender, canvas_exit_receiver) = unbounded(); |
| if let Err(e) = canvas_sender.send(ConstellationCanvasMsg::Exit(canvas_exit_sender)) { |
| warn!("Exit Canvas Paint thread failed ({})", e); |
| } |
| Some(canvas_exit_receiver) |
| } else { |
| None |
| }; |
| |
| debug!("Exiting WebGPU threads."); |
| #[cfg(feature = "webgpu")] |
| let receivers = self |
| .browsing_context_group_set |
| .values() |
| .flat_map(|browsing_context_group| { |
| browsing_context_group.webgpus.values().map(|webgpu| { |
| let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel!"); |
| if let Err(e) = webgpu.exit(sender) { |
| warn!("Exit WebGPU Thread failed ({})", e); |
| None |
| } else { |
| Some(receiver) |
| } |
| }) |
| }) |
| .flatten(); |
| |
| #[cfg(feature = "webgpu")] |
| for receiver in receivers { |
| if let Err(e) = receiver.recv() { |
| warn!("Failed to receive exit response from WebGPU ({:?})", e); |
| } |
| } |
| |
| if let Some(webgl_threads) = self.webgl_threads.as_ref() { |
| let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel!"); |
| webgl_threads_receiver = Some(receiver); |
| debug!("Exiting WebGL thread."); |
| |
| if let Err(e) = webgl_threads.exit(sender) { |
| warn!("Exit WebGL Thread failed ({e})"); |
| } |
| } |
| |
| debug!("Exiting GLPlayer thread."); |
| WindowGLContext::get().exit(); |
| |
| // Wait for the canvas thread to exit before shutting down the font service, as |
| // canvas might still be using the system font service before shutting down. |
| if let Some(canvas_exit_receiver) = canvas_exit_receiver { |
| let _ = canvas_exit_receiver.recv(); |
| } |
| |
| debug!("Exiting the system font service thread."); |
| self.system_font_service.exit(); |
| |
| // Receive exit signals from threads. |
| if let Err(e) = core_ipc_receiver.recv() { |
| warn!("Exit resource thread failed ({:?})", e); |
| } |
| if let Err(e) = storage_ipc_receiver.recv() { |
| warn!("Exit storage thread failed ({:?})", e); |
| } |
| if self.webgl_threads.is_some() { |
| if let Err(e) = webgl_threads_receiver |
| .expect("webgl_threads_receiver to be Some") |
| .recv() |
| { |
| warn!("Exit WebGL thread failed ({:?})", e); |
| } |
| } |
| |
| debug!("Shutting-down IPC router thread in constellation."); |
| ROUTER.shutdown(); |
| |
| debug!("Shutting-down the async runtime in constellation."); |
| self.async_runtime.shutdown(); |
| } |
| |
| fn handle_pipeline_exited(&mut self, pipeline_id: PipelineId) { |
| debug!("{}: Exited", pipeline_id); |
| let Some(pipeline) = self.pipelines.remove(&pipeline_id) else { |
| return; |
| }; |
| |
| // Now that the Script and Constellation parts of Servo no longer have a reference to |
| // this pipeline, tell the compositor that it has shut down. This is delayed until the |
| // last moment. |
| self.compositor_proxy.send(CompositorMsg::PipelineExited( |
| pipeline.webview_id, |
| pipeline.id, |
| PipelineExitSource::Constellation, |
| )); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_send_error(&mut self, pipeline_id: PipelineId, err: IpcError) { |
| // Treat send error the same as receiving a panic message |
| error!("{}: Send error ({})", pipeline_id, err); |
| let webview_id = self |
| .pipelines |
| .get(&pipeline_id) |
| .map(|pipeline| pipeline.webview_id); |
| let reason = format!("Send failed ({})", err); |
| self.handle_panic(webview_id, reason, None); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_panic( |
| &mut self, |
| webview_id: Option<WebViewId>, |
| reason: String, |
| backtrace: Option<String>, |
| ) { |
| if self.hard_fail { |
| // It's quite difficult to make Servo exit cleanly if some threads have failed. |
| // Hard fail exists for test runners so we crash and that's good enough. |
| println!("Pipeline failed in hard-fail mode. Crashing!"); |
| process::exit(1); |
| } |
| |
| let webview_id = match webview_id { |
| Some(id) => id, |
| None => return, |
| }; |
| |
| debug!( |
| "{}: Panic handler for top-level browsing context: {}", |
| webview_id, reason |
| ); |
| |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| |
| self.embedder_proxy.send(EmbedderMsg::Panic( |
| webview_id, |
| reason.clone(), |
| backtrace.clone(), |
| )); |
| |
| let browsing_context = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(context) => context, |
| None => return warn!("failed browsing context is missing"), |
| }; |
| let viewport_details = browsing_context.viewport_details; |
| let pipeline_id = browsing_context.pipeline_id; |
| let throttled = browsing_context.throttled; |
| |
| let pipeline = match self.pipelines.get(&pipeline_id) { |
| Some(p) => p, |
| None => return warn!("failed pipeline is missing"), |
| }; |
| let opener = pipeline.opener; |
| |
| self.close_browsing_context_children( |
| browsing_context_id, |
| DiscardBrowsingContext::No, |
| ExitPipelineMode::Force, |
| ); |
| |
| let old_pipeline_id = pipeline_id; |
| let old_load_data = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.load_data.clone(), |
| None => return warn!("failed pipeline is missing"), |
| }; |
| if old_load_data.crash.is_some() { |
| return error!("crash page crashed"); |
| } |
| |
| warn!("creating replacement pipeline for crash page"); |
| |
| let new_pipeline_id = PipelineId::new(); |
| let new_load_data = LoadData { |
| crash: Some( |
| backtrace |
| .map(|b| format!("{}\n{}", reason, b)) |
| .unwrap_or(reason), |
| ), |
| ..old_load_data.clone() |
| }; |
| |
| let sandbox = IFrameSandboxState::IFrameSandboxed; |
| let is_private = false; |
| self.new_pipeline( |
| new_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| None, |
| opener, |
| viewport_details, |
| new_load_data, |
| sandbox, |
| is_private, |
| throttled, |
| ); |
| self.add_pending_change(SessionHistoryChange { |
| webview_id, |
| browsing_context_id, |
| new_pipeline_id, |
| // Pipeline already closed by close_browsing_context_children, so we can pass Yes here |
| // to avoid closing again in handle_activate_document_msg (though it would be harmless) |
| replace: Some(NeedsToReload::Yes(old_pipeline_id, old_load_data)), |
| new_browsing_context_info: None, |
| viewport_details, |
| }); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_focus_web_view(&mut self, webview_id: WebViewId, focus_id: FocusId) { |
| let focused = self.webviews.focus(webview_id).is_ok(); |
| if !focused { |
| warn!("{webview_id}: FocusWebView on unknown top-level browsing context"); |
| } |
| self.embedder_proxy |
| .send(EmbedderMsg::WebViewFocused(webview_id, focus_id, focused)); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_log_entry( |
| &mut self, |
| webview_id: Option<WebViewId>, |
| thread_name: Option<String>, |
| entry: LogEntry, |
| ) { |
| if let LogEntry::Panic(ref reason, ref backtrace) = entry { |
| self.handle_panic(webview_id, reason.clone(), Some(backtrace.clone())); |
| } |
| |
| match entry { |
| LogEntry::Panic(reason, _) | LogEntry::Error(reason) | LogEntry::Warn(reason) => { |
| // VecDeque::truncate is unstable |
| if WARNINGS_BUFFER_SIZE <= self.handled_warnings.len() { |
| self.handled_warnings.pop_front(); |
| } |
| self.handled_warnings.push_back((thread_name, reason)); |
| }, |
| } |
| } |
| |
| fn update_pressed_mouse_buttons(&mut self, event: &MouseButtonEvent) { |
| // This value is ultimately used for a DOM mouse event, and the specification says that |
| // the pressed buttons should be represented as a bitmask with values defined at |
| // <https://w3c.github.io/uievents/#dom-mouseevent-buttons>. |
| let button_as_bitmask = match event.button { |
| MouseButton::Left => 1, |
| MouseButton::Right => 2, |
| MouseButton::Middle => 4, |
| MouseButton::Back => 8, |
| MouseButton::Forward => 16, |
| MouseButton::Other(_) => return, |
| }; |
| |
| match event.action { |
| MouseButtonAction::Click | MouseButtonAction::Down => { |
| self.pressed_mouse_buttons |= button_as_bitmask; |
| }, |
| MouseButtonAction::Up => { |
| self.pressed_mouse_buttons &= !(button_as_bitmask); |
| }, |
| } |
| } |
| |
| #[allow(deprecated)] |
| fn update_active_keybord_modifiers(&mut self, event: &KeyboardEvent) { |
| self.active_keyboard_modifiers = event.event.modifiers; |
| |
| // `KeyboardEvent::modifiers` contains the pre-existing modifiers before this key was |
| // either pressed or released, but `active_keyboard_modifiers` should track the subsequent |
| // state. If this event will update that state, we need to ensure that we are tracking what |
| // the event changes. |
| let Key::Named(named_key) = event.event.key else { |
| return; |
| }; |
| |
| let modified_modifier = match named_key { |
| NamedKey::Alt => Modifiers::ALT, |
| NamedKey::AltGraph => Modifiers::ALT_GRAPH, |
| NamedKey::CapsLock => Modifiers::CAPS_LOCK, |
| NamedKey::Control => Modifiers::CONTROL, |
| NamedKey::Fn => Modifiers::FN, |
| NamedKey::FnLock => Modifiers::FN_LOCK, |
| NamedKey::Meta => Modifiers::META, |
| NamedKey::NumLock => Modifiers::NUM_LOCK, |
| NamedKey::ScrollLock => Modifiers::SCROLL_LOCK, |
| NamedKey::Shift => Modifiers::SHIFT, |
| NamedKey::Symbol => Modifiers::SYMBOL, |
| NamedKey::SymbolLock => Modifiers::SYMBOL_LOCK, |
| NamedKey::Hyper => Modifiers::HYPER, |
| // The web doesn't make a distinction between these keys (there is only |
| // "meta") so map "super" to "meta". |
| NamedKey::Super => Modifiers::META, |
| _ => return, |
| }; |
| match event.event.state { |
| KeyState::Down => self.active_keyboard_modifiers.insert(modified_modifier), |
| KeyState::Up => self.active_keyboard_modifiers.remove(modified_modifier), |
| } |
| } |
| |
| fn forward_input_event( |
| &mut self, |
| webview_id: WebViewId, |
| event: InputEvent, |
| hit_test_result: Option<CompositorHitTestResult>, |
| ) { |
| if let InputEvent::MouseButton(event) = &event { |
| self.update_pressed_mouse_buttons(event); |
| } |
| |
| if let InputEvent::Keyboard(event) = &event { |
| self.update_active_keybord_modifiers(event); |
| } |
| |
| // The constellation tracks the state of pressed mouse buttons and keyboard |
| // modifiers and updates the event here to reflect the current state. |
| let pressed_mouse_buttons = self.pressed_mouse_buttons; |
| let active_keyboard_modifiers = self.active_keyboard_modifiers; |
| |
| // TODO: Click should be handled internally in the `Document`. |
| if let InputEvent::MouseButton(event) = &event { |
| if event.action == MouseButtonAction::Click { |
| self.pressed_mouse_buttons = 0; |
| } |
| } |
| |
| let Some(webview) = self.webviews.get_mut(webview_id) else { |
| warn!("Got input event for unknown WebViewId: {webview_id:?}"); |
| return; |
| }; |
| |
| let event = ConstellationInputEvent { |
| hit_test_result, |
| pressed_mouse_buttons, |
| active_keyboard_modifiers, |
| event, |
| }; |
| webview.forward_input_event(event, &self.pipelines, &self.browsing_contexts); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_new_top_level_browsing_context( |
| &mut self, |
| url: ServoUrl, |
| webview_id: WebViewId, |
| viewport_details: ViewportDetails, |
| ) { |
| let pipeline_id = PipelineId::new(); |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| let load_data = LoadData::new( |
| LoadOrigin::Constellation, |
| url, |
| None, |
| Referrer::NoReferrer, |
| ReferrerPolicy::EmptyString, |
| None, |
| None, |
| false, |
| ); |
| let sandbox = IFrameSandboxState::IFrameUnsandboxed; |
| let is_private = false; |
| let throttled = false; |
| |
| // Register this new top-level browsing context id as a webview and set |
| // its focused browsing context to be itself. |
| self.webviews |
| .add(webview_id, ConstellationWebView::new(browsing_context_id)); |
| |
| // https://html.spec.whatwg.org/multipage/#creating-a-new-browsing-context-group |
| let mut new_bc_group: BrowsingContextGroup = Default::default(); |
| let new_bc_group_id = self.next_browsing_context_group_id(); |
| new_bc_group |
| .top_level_browsing_context_set |
| .insert(webview_id); |
| self.browsing_context_group_set |
| .insert(new_bc_group_id, new_bc_group); |
| |
| self.new_pipeline( |
| pipeline_id, |
| browsing_context_id, |
| webview_id, |
| None, |
| None, |
| viewport_details, |
| load_data, |
| sandbox, |
| is_private, |
| throttled, |
| ); |
| self.add_pending_change(SessionHistoryChange { |
| webview_id, |
| browsing_context_id, |
| new_pipeline_id: pipeline_id, |
| replace: None, |
| new_browsing_context_info: Some(NewBrowsingContextInfo { |
| parent_pipeline_id: None, |
| is_private, |
| inherited_secure_context: None, |
| throttled, |
| }), |
| viewport_details, |
| }); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { |
| debug!("{webview_id}: Closing"); |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| let browsing_context = |
| self.close_browsing_context(browsing_context_id, ExitPipelineMode::Normal); |
| if self.webviews.focused_webview().map(|(id, _)| id) == Some(webview_id) { |
| self.embedder_proxy.send(EmbedderMsg::WebViewBlurred); |
| } |
| self.webviews.remove(webview_id); |
| self.compositor_proxy |
| .send(CompositorMsg::RemoveWebView(webview_id)); |
| self.embedder_proxy |
| .send(EmbedderMsg::WebViewClosed(webview_id)); |
| |
| let Some(browsing_context) = browsing_context else { |
| return warn!( |
| "fn handle_close_top_level_browsing_context {}: Closing twice", |
| browsing_context_id |
| ); |
| }; |
| // https://html.spec.whatwg.org/multipage/#bcg-remove |
| let bc_group_id = browsing_context.bc_group_id; |
| let Some(bc_group) = self.browsing_context_group_set.get_mut(&bc_group_id) else { |
| warn!("{}: Browsing context group not found!", bc_group_id); |
| return; |
| }; |
| if !bc_group.top_level_browsing_context_set.remove(&webview_id) { |
| warn!("{webview_id}: Top-level browsing context not found in {bc_group_id}",); |
| } |
| if bc_group.top_level_browsing_context_set.is_empty() { |
| self.browsing_context_group_set |
| .remove(&browsing_context.bc_group_id); |
| } |
| |
| // Note: In single-process mode, |
| // if the webview is manually closed, we drop the join handle without joining on it. |
| // It is unlikely the thread will still run when the constellation shuts-down. |
| self.script_join_handles.remove(&webview_id); |
| |
| debug!("{webview_id}: Closed"); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_iframe_size_msg(&mut self, iframe_sizes: Vec<IFrameSizeMsg>) { |
| for IFrameSizeMsg { |
| browsing_context_id, |
| size, |
| type_, |
| } in iframe_sizes |
| { |
| self.resize_browsing_context(size, type_, browsing_context_id); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_finish_javascript_evaluation( |
| &mut self, |
| evaluation_id: JavaScriptEvaluationId, |
| result: Result<JSValue, JavaScriptEvaluationError>, |
| ) { |
| self.embedder_proxy |
| .send(EmbedderMsg::FinishJavaScriptEvaluation( |
| evaluation_id, |
| result, |
| )); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_subframe_loaded(&mut self, pipeline_id: PipelineId) { |
| let browsing_context_id = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.browsing_context_id, |
| None => return warn!("{}: Subframe loaded after closure", pipeline_id), |
| }; |
| let parent_pipeline_id = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(browsing_context) => browsing_context.parent_pipeline_id, |
| None => { |
| return warn!( |
| "{}: Subframe loaded in closed {}", |
| pipeline_id, browsing_context_id, |
| ); |
| }, |
| }; |
| let parent_pipeline_id = match parent_pipeline_id { |
| Some(parent_pipeline_id) => parent_pipeline_id, |
| None => return warn!("{}: Subframe has no parent", pipeline_id), |
| }; |
| // https://html.spec.whatwg.org/multipage/#the-iframe-element:completely-loaded |
| // When a Document in an iframe is marked as completely loaded, |
| // the user agent must run the iframe load event steps. |
| let msg = ScriptThreadMessage::DispatchIFrameLoadEvent { |
| target: browsing_context_id, |
| parent: parent_pipeline_id, |
| child: pipeline_id, |
| }; |
| let result = match self.pipelines.get(&parent_pipeline_id) { |
| Some(parent) => parent.event_loop.send(msg), |
| None => { |
| return warn!( |
| "{}: Parent pipeline browsing context loaded after closure", |
| parent_pipeline_id |
| ); |
| }, |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(parent_pipeline_id, e); |
| } |
| } |
| |
| // The script thread associated with pipeline_id has loaded a URL in an |
| // iframe via script. This will result in a new pipeline being spawned and |
| // a child being added to the parent browsing context. This message is never |
| // the result of a page navigation. |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_script_loaded_url_in_iframe_msg(&mut self, load_info: IFrameLoadInfoWithData) { |
| let IFrameLoadInfo { |
| parent_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| new_pipeline_id, |
| is_private, |
| mut history_handling, |
| .. |
| } = load_info.info; |
| |
| // If no url is specified, reload. |
| let old_pipeline = load_info |
| .old_pipeline_id |
| .and_then(|id| self.pipelines.get(&id)); |
| |
| // Replacement enabled also takes into account whether the document is "completely loaded", |
| // see https://html.spec.whatwg.org/multipage/#the-iframe-element:completely-loaded |
| if let Some(old_pipeline) = old_pipeline { |
| if !old_pipeline.completely_loaded { |
| history_handling = NavigationHistoryBehavior::Replace; |
| } |
| debug!( |
| "{:?}: Old pipeline is {}completely loaded", |
| load_info.old_pipeline_id, |
| if old_pipeline.completely_loaded { |
| "" |
| } else { |
| "not " |
| } |
| ); |
| } |
| |
| let is_parent_private = { |
| let parent_browsing_context_id = match self.pipelines.get(&parent_pipeline_id) { |
| Some(pipeline) => pipeline.browsing_context_id, |
| None => { |
| return warn!( |
| "{}: Script loaded url in iframe {} in closed parent pipeline", |
| parent_pipeline_id, browsing_context_id, |
| ); |
| }, |
| }; |
| |
| match self.browsing_contexts.get(&parent_browsing_context_id) { |
| Some(ctx) => ctx.is_private, |
| None => { |
| return warn!( |
| "{}: Script loaded url in iframe {} in closed parent browsing context", |
| parent_browsing_context_id, browsing_context_id, |
| ); |
| }, |
| } |
| }; |
| let is_private = is_private || is_parent_private; |
| |
| let browsing_context = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(ctx) => ctx, |
| None => { |
| return warn!( |
| "{}: Script loaded url in iframe with closed browsing context", |
| browsing_context_id, |
| ); |
| }, |
| }; |
| |
| let replace = if history_handling == NavigationHistoryBehavior::Replace { |
| Some(NeedsToReload::No(browsing_context.pipeline_id)) |
| } else { |
| None |
| }; |
| |
| let browsing_context_size = browsing_context.viewport_details; |
| let browsing_context_throttled = browsing_context.throttled; |
| // TODO(servo#30571) revert to debug_assert_eq!() once underlying bug is fixed |
| #[cfg(debug_assertions)] |
| if !(browsing_context_size == load_info.viewport_details) { |
| log::warn!( |
| "debug assertion failed! browsing_context_size == load_info.viewport_details.initial_viewport" |
| ); |
| } |
| |
| // Create the new pipeline, attached to the parent and push to pending changes |
| self.new_pipeline( |
| new_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| Some(parent_pipeline_id), |
| None, |
| browsing_context_size, |
| load_info.load_data, |
| load_info.sandbox, |
| is_private, |
| browsing_context_throttled, |
| ); |
| self.add_pending_change(SessionHistoryChange { |
| webview_id, |
| browsing_context_id, |
| new_pipeline_id, |
| replace, |
| // Browsing context for iframe already exists. |
| new_browsing_context_info: None, |
| viewport_details: load_info.viewport_details, |
| }); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_script_new_iframe(&mut self, load_info: IFrameLoadInfoWithData) { |
| let IFrameLoadInfo { |
| parent_pipeline_id, |
| new_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| is_private, |
| .. |
| } = load_info.info; |
| |
| let (script_sender, parent_browsing_context_id) = |
| match self.pipelines.get(&parent_pipeline_id) { |
| Some(pipeline) => (pipeline.event_loop.clone(), pipeline.browsing_context_id), |
| None => { |
| return warn!( |
| "{}: Script loaded url in closed iframe pipeline", |
| parent_pipeline_id |
| ); |
| }, |
| }; |
| let (is_parent_private, is_parent_throttled, is_parent_secure) = |
| match self.browsing_contexts.get(&parent_browsing_context_id) { |
| Some(ctx) => (ctx.is_private, ctx.throttled, ctx.inherited_secure_context), |
| None => { |
| return warn!( |
| "{}: New iframe {} loaded in closed parent browsing context", |
| parent_browsing_context_id, browsing_context_id, |
| ); |
| }, |
| }; |
| let is_private = is_private || is_parent_private; |
| let pipeline = Pipeline::new( |
| new_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| None, |
| script_sender, |
| self.compositor_proxy.clone(), |
| is_parent_throttled, |
| load_info.load_data, |
| ); |
| |
| assert!(!self.pipelines.contains_key(&new_pipeline_id)); |
| self.pipelines.insert(new_pipeline_id, pipeline); |
| self.add_pending_change(SessionHistoryChange { |
| webview_id, |
| browsing_context_id, |
| new_pipeline_id, |
| replace: None, |
| // Browsing context for iframe doesn't exist yet. |
| new_browsing_context_info: Some(NewBrowsingContextInfo { |
| parent_pipeline_id: Some(parent_pipeline_id), |
| is_private, |
| inherited_secure_context: is_parent_secure, |
| throttled: is_parent_throttled, |
| }), |
| viewport_details: load_info.viewport_details, |
| }); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_script_new_auxiliary(&mut self, load_info: AuxiliaryWebViewCreationRequest) { |
| let AuxiliaryWebViewCreationRequest { |
| load_data, |
| opener_webview_id, |
| opener_pipeline_id, |
| response_sender, |
| } = load_info; |
| |
| let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { |
| warn!("Failed to create channel"); |
| let _ = response_sender.send(None); |
| return; |
| }; |
| self.embedder_proxy.send(EmbedderMsg::AllowOpeningWebView( |
| opener_webview_id, |
| webview_id_sender, |
| )); |
| let (new_webview_id, viewport_details) = match webview_id_receiver.recv() { |
| Ok(Some((webview_id, viewport_details))) => (webview_id, viewport_details), |
| Ok(None) | Err(_) => { |
| let _ = response_sender.send(None); |
| return; |
| }, |
| }; |
| let new_browsing_context_id = BrowsingContextId::from(new_webview_id); |
| |
| let (script_sender, opener_browsing_context_id) = |
| match self.pipelines.get(&opener_pipeline_id) { |
| Some(pipeline) => (pipeline.event_loop.clone(), pipeline.browsing_context_id), |
| None => { |
| return warn!( |
| "{}: Auxiliary loaded url in closed iframe pipeline", |
| opener_pipeline_id |
| ); |
| }, |
| }; |
| let (is_opener_private, is_opener_throttled, is_opener_secure) = |
| match self.browsing_contexts.get(&opener_browsing_context_id) { |
| Some(ctx) => (ctx.is_private, ctx.throttled, ctx.inherited_secure_context), |
| None => { |
| return warn!( |
| "{}: New auxiliary {} loaded in closed opener browsing context", |
| opener_browsing_context_id, new_browsing_context_id, |
| ); |
| }, |
| }; |
| let new_pipeline_id = PipelineId::new(); |
| let pipeline = Pipeline::new( |
| new_pipeline_id, |
| new_browsing_context_id, |
| new_webview_id, |
| Some(opener_browsing_context_id), |
| script_sender, |
| self.compositor_proxy.clone(), |
| is_opener_throttled, |
| load_data, |
| ); |
| let _ = response_sender.send(Some(AuxiliaryWebViewCreationResponse { |
| new_webview_id, |
| new_pipeline_id, |
| })); |
| |
| assert!(!self.pipelines.contains_key(&new_pipeline_id)); |
| self.pipelines.insert(new_pipeline_id, pipeline); |
| self.webviews.add( |
| new_webview_id, |
| ConstellationWebView::new(new_browsing_context_id), |
| ); |
| |
| // https://html.spec.whatwg.org/multipage/#bcg-append |
| let opener = match self.browsing_contexts.get(&opener_browsing_context_id) { |
| Some(id) => id, |
| None => { |
| warn!("Trying to append an unknown auxiliary to a browsing context group"); |
| return; |
| }, |
| }; |
| let bc_group = match self.browsing_context_group_set.get_mut(&opener.bc_group_id) { |
| Some(bc_group) => bc_group, |
| None => { |
| warn!("Trying to add a top-level to an unknown group."); |
| return; |
| }, |
| }; |
| bc_group |
| .top_level_browsing_context_set |
| .insert(new_webview_id); |
| |
| self.add_pending_change(SessionHistoryChange { |
| webview_id: new_webview_id, |
| browsing_context_id: new_browsing_context_id, |
| new_pipeline_id, |
| replace: None, |
| new_browsing_context_info: Some(NewBrowsingContextInfo { |
| // Auxiliary browsing contexts are always top-level. |
| parent_pipeline_id: None, |
| is_private: is_opener_private, |
| inherited_secure_context: is_opener_secure, |
| throttled: is_opener_throttled, |
| }), |
| viewport_details, |
| }); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { |
| let Some(pipeline) = self.pipelines.get(&pipeline_id) else { |
| return; |
| }; |
| |
| if let Err(error) = pipeline |
| .event_loop |
| .send(ScriptThreadMessage::RefreshCursor(pipeline_id)) |
| { |
| warn!("Could not send RefreshCursor message to pipeline: {error:?}"); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_change_running_animations_state( |
| &mut self, |
| pipeline_id: PipelineId, |
| animation_state: AnimationState, |
| ) { |
| if let Some(pipeline) = self.pipelines.get_mut(&pipeline_id) { |
| if pipeline.animation_state != animation_state { |
| pipeline.animation_state = animation_state; |
| self.compositor_proxy |
| .send(CompositorMsg::ChangeRunningAnimationsState( |
| pipeline.webview_id, |
| pipeline_id, |
| animation_state, |
| )) |
| } |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_tick_animation(&mut self, webview_ids: Vec<WebViewId>) { |
| let mut animating_event_loops = HashSet::new(); |
| |
| for webview_id in webview_ids.iter() { |
| for browsing_context in self.fully_active_browsing_contexts_iter(*webview_id) { |
| let Some(pipeline) = self.pipelines.get(&browsing_context.pipeline_id) else { |
| continue; |
| }; |
| animating_event_loops.insert(pipeline.event_loop.clone()); |
| } |
| } |
| |
| for event_loop in animating_event_loops { |
| // No error handling here. It's unclear what to do when this fails as the error isn't associated |
| // with a particular pipeline. In addition, the danger of not progressing animations is pretty |
| // low, so it's probably safe to ignore this error and handle the crashed ScriptThread on |
| // some other message. |
| let _ = event_loop.send(ScriptThreadMessage::TickAllAnimations(webview_ids.clone())); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_no_longer_waiting_on_asynchronous_image_updates( |
| &mut self, |
| pipeline_ids: Vec<PipelineId>, |
| ) { |
| for pipeline_id in pipeline_ids.into_iter() { |
| if let Some(pipeline) = self.pipelines.get(&pipeline_id) { |
| let _ = pipeline.event_loop.send( |
| ScriptThreadMessage::NoLongerWaitingOnAsychronousImageUpdates(pipeline_id), |
| ); |
| } |
| } |
| } |
| |
| /// Schedule a navigation(via load_url). |
| /// 1: Ask the embedder for permission. |
| /// 2: Store the details of the navigation, pending approval from the embedder. |
| #[servo_tracing::instrument(skip_all)] |
| fn schedule_navigation( |
| &mut self, |
| webview_id: WebViewId, |
| source_id: PipelineId, |
| load_data: LoadData, |
| history_handling: NavigationHistoryBehavior, |
| ) { |
| match self.pending_approval_navigations.entry(source_id) { |
| Entry::Occupied(_) => { |
| return warn!( |
| "{}: Tried to schedule a navigation while one is already pending", |
| source_id |
| ); |
| }, |
| Entry::Vacant(entry) => { |
| let _ = entry.insert((load_data.clone(), history_handling)); |
| }, |
| }; |
| // Allow the embedder to handle the url itself |
| self.embedder_proxy |
| .send(EmbedderMsg::AllowNavigationRequest( |
| webview_id, |
| source_id, |
| load_data.url.clone(), |
| )); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn load_url( |
| &mut self, |
| webview_id: WebViewId, |
| source_id: PipelineId, |
| load_data: LoadData, |
| history_handling: NavigationHistoryBehavior, |
| ) -> Option<PipelineId> { |
| debug!( |
| "{}: Loading ({}replacing): {}", |
| source_id, |
| match history_handling { |
| NavigationHistoryBehavior::Push => "", |
| NavigationHistoryBehavior::Replace => "not ", |
| NavigationHistoryBehavior::Auto => "unsure if ", |
| }, |
| load_data.url, |
| ); |
| // If this load targets an iframe, its framing element may exist |
| // in a separate script thread than the framed document that initiated |
| // the new load. The framing element must be notified about the |
| // requested change so it can update its internal state. |
| // |
| // If replace is true, the current entry is replaced instead of a new entry being added. |
| let (browsing_context_id, opener) = match self.pipelines.get(&source_id) { |
| Some(pipeline) => (pipeline.browsing_context_id, pipeline.opener), |
| None => { |
| warn!("{}: Loaded after closure", source_id); |
| return None; |
| }, |
| }; |
| let (viewport_details, pipeline_id, parent_pipeline_id, is_private, is_throttled) = |
| match self.browsing_contexts.get(&browsing_context_id) { |
| Some(ctx) => ( |
| ctx.viewport_details, |
| ctx.pipeline_id, |
| ctx.parent_pipeline_id, |
| ctx.is_private, |
| ctx.throttled, |
| ), |
| None => { |
| // This should technically never happen (since `load_url` is |
| // only called on existing browsing contexts), but we prefer to |
| // avoid `expect`s or `unwrap`s in `Constellation` to ward |
| // against future changes that might break things. |
| warn!( |
| "{}: Loaded url in closed {}", |
| source_id, browsing_context_id, |
| ); |
| return None; |
| }, |
| }; |
| |
| if let Some(ref chan) = self.devtools_sender { |
| let state = NavigationState::Start(load_data.url.clone()); |
| let _ = chan.send(DevtoolsControlMsg::FromScript( |
| ScriptToDevtoolsControlMsg::Navigate(browsing_context_id, state), |
| )); |
| } |
| |
| match parent_pipeline_id { |
| Some(parent_pipeline_id) => { |
| // Find the script thread for the pipeline containing the iframe |
| // and issue an iframe load through there. |
| let msg = ScriptThreadMessage::NavigateIframe( |
| parent_pipeline_id, |
| browsing_context_id, |
| load_data, |
| history_handling, |
| ); |
| let result = match self.pipelines.get(&parent_pipeline_id) { |
| Some(parent_pipeline) => parent_pipeline.event_loop.send(msg), |
| None => { |
| warn!("{}: Child loaded after closure", parent_pipeline_id); |
| return None; |
| }, |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(parent_pipeline_id, e); |
| } else if let Some((sender, id)) = &self.webdriver_load_status_sender { |
| if source_id == *id { |
| let _ = sender.send(WebDriverLoadStatus::NavigationStop); |
| } |
| } |
| |
| None |
| }, |
| None => { |
| // Make sure no pending page would be overridden. |
| for change in &self.pending_changes { |
| if change.browsing_context_id == browsing_context_id { |
| // id that sent load msg is being changed already; abort |
| return None; |
| } |
| } |
| |
| if self.get_activity(source_id) == DocumentActivity::Inactive { |
| // Disregard this load if the navigating pipeline is not actually |
| // active. This could be caused by a delayed navigation (eg. from |
| // a timer) or a race between multiple navigations (such as an |
| // onclick handler on an anchor element). |
| return None; |
| } |
| |
| // Being here means either there are no pending changes, or none of the pending |
| // changes would be overridden by changing the subframe associated with source_id. |
| |
| // Create the new pipeline |
| |
| let replace = if history_handling == NavigationHistoryBehavior::Replace { |
| Some(NeedsToReload::No(pipeline_id)) |
| } else { |
| None |
| }; |
| |
| let new_pipeline_id = PipelineId::new(); |
| let sandbox = IFrameSandboxState::IFrameUnsandboxed; |
| self.new_pipeline( |
| new_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| None, |
| opener, |
| viewport_details, |
| load_data, |
| sandbox, |
| is_private, |
| is_throttled, |
| ); |
| self.add_pending_change(SessionHistoryChange { |
| webview_id, |
| browsing_context_id, |
| new_pipeline_id, |
| replace, |
| // `load_url` is always invoked on an existing browsing context. |
| new_browsing_context_info: None, |
| viewport_details, |
| }); |
| Some(new_pipeline_id) |
| }, |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_abort_load_url_msg(&mut self, new_pipeline_id: PipelineId) { |
| let pending_index = self |
| .pending_changes |
| .iter() |
| .rposition(|change| change.new_pipeline_id == new_pipeline_id); |
| |
| // If it is found, remove it from the pending changes. |
| if let Some(pending_index) = pending_index { |
| self.pending_changes.remove(pending_index); |
| self.close_pipeline( |
| new_pipeline_id, |
| DiscardBrowsingContext::No, |
| ExitPipelineMode::Normal, |
| ); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_load_complete_msg(&mut self, webview_id: WebViewId, pipeline_id: PipelineId) { |
| if let Some(pipeline) = self.pipelines.get_mut(&pipeline_id) { |
| debug!("{}: Marking as loaded", pipeline_id); |
| pipeline.completely_loaded = true; |
| } |
| |
| // Notify the embedder that the TopLevelBrowsingContext current document |
| // has finished loading. |
| // We need to make sure the pipeline that has finished loading is the current |
| // pipeline and that no pending pipeline will replace the current one. |
| let pipeline_is_top_level_pipeline = self |
| .browsing_contexts |
| .get(&BrowsingContextId::from(webview_id)) |
| .map(|ctx| ctx.pipeline_id == pipeline_id) |
| .unwrap_or(false); |
| if pipeline_is_top_level_pipeline { |
| // Is there any pending pipeline that will replace the current top level pipeline |
| let current_top_level_pipeline_will_be_replaced = self |
| .pending_changes |
| .iter() |
| .any(|change| change.browsing_context_id == webview_id); |
| |
| if !current_top_level_pipeline_will_be_replaced { |
| // Notify embedder and compositor top level document finished loading. |
| self.compositor_proxy |
| .send(CompositorMsg::LoadComplete(webview_id)); |
| } |
| } else { |
| self.handle_subframe_loaded(pipeline_id); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_navigated_to_fragment( |
| &mut self, |
| pipeline_id: PipelineId, |
| new_url: ServoUrl, |
| history_handling: NavigationHistoryBehavior, |
| ) { |
| let (webview_id, old_url) = match self.pipelines.get_mut(&pipeline_id) { |
| Some(pipeline) => { |
| let old_url = replace(&mut pipeline.url, new_url.clone()); |
| (pipeline.webview_id, old_url) |
| }, |
| None => { |
| return warn!("{}: Navigated to fragment after closure", pipeline_id); |
| }, |
| }; |
| |
| match history_handling { |
| NavigationHistoryBehavior::Replace => {}, |
| _ => { |
| let diff = SessionHistoryDiff::Hash { |
| pipeline_reloader: NeedsToReload::No(pipeline_id), |
| new_url, |
| old_url, |
| }; |
| |
| self.get_joint_session_history(webview_id).push_diff(diff); |
| |
| self.notify_history_changed(webview_id); |
| }, |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_traverse_history_msg( |
| &mut self, |
| webview_id: WebViewId, |
| direction: TraversalDirection, |
| ) { |
| let mut browsing_context_changes = |
| FnvHashMap::<BrowsingContextId, NeedsToReload>::default(); |
| let mut pipeline_changes = |
| FnvHashMap::<PipelineId, (Option<HistoryStateId>, ServoUrl)>::default(); |
| let mut url_to_load = FnvHashMap::<PipelineId, ServoUrl>::default(); |
| { |
| let session_history = self.get_joint_session_history(webview_id); |
| match direction { |
| TraversalDirection::Forward(forward) => { |
| let future_length = session_history.future.len(); |
| |
| if future_length < forward { |
| return warn!("Cannot traverse that far into the future."); |
| } |
| |
| for diff in session_history |
| .future |
| .drain(future_length - forward..) |
| .rev() |
| { |
| match diff { |
| SessionHistoryDiff::BrowsingContext { |
| browsing_context_id, |
| ref new_reloader, |
| .. |
| } => { |
| browsing_context_changes |
| .insert(browsing_context_id, new_reloader.clone()); |
| }, |
| SessionHistoryDiff::Pipeline { |
| ref pipeline_reloader, |
| new_history_state_id, |
| ref new_url, |
| .. |
| } => match *pipeline_reloader { |
| NeedsToReload::No(pipeline_id) => { |
| pipeline_changes.insert( |
| pipeline_id, |
| (Some(new_history_state_id), new_url.clone()), |
| ); |
| }, |
| NeedsToReload::Yes(pipeline_id, ..) => { |
| url_to_load.insert(pipeline_id, new_url.clone()); |
| }, |
| }, |
| SessionHistoryDiff::Hash { |
| ref pipeline_reloader, |
| ref new_url, |
| .. |
| } => match *pipeline_reloader { |
| NeedsToReload::No(pipeline_id) => { |
| let state = pipeline_changes |
| .get(&pipeline_id) |
| .and_then(|change| change.0); |
| pipeline_changes.insert(pipeline_id, (state, new_url.clone())); |
| }, |
| NeedsToReload::Yes(pipeline_id, ..) => { |
| url_to_load.insert(pipeline_id, new_url.clone()); |
| }, |
| }, |
| } |
| session_history.past.push(diff); |
| } |
| }, |
| TraversalDirection::Back(back) => { |
| let past_length = session_history.past.len(); |
| |
| if past_length < back { |
| return warn!("Cannot traverse that far into the past."); |
| } |
| |
| for diff in session_history.past.drain(past_length - back..).rev() { |
| match diff { |
| SessionHistoryDiff::BrowsingContext { |
| browsing_context_id, |
| ref old_reloader, |
| .. |
| } => { |
| browsing_context_changes |
| .insert(browsing_context_id, old_reloader.clone()); |
| }, |
| SessionHistoryDiff::Pipeline { |
| ref pipeline_reloader, |
| old_history_state_id, |
| ref old_url, |
| .. |
| } => match *pipeline_reloader { |
| NeedsToReload::No(pipeline_id) => { |
| pipeline_changes.insert( |
| pipeline_id, |
| (old_history_state_id, old_url.clone()), |
| ); |
| }, |
| NeedsToReload::Yes(pipeline_id, ..) => { |
| url_to_load.insert(pipeline_id, old_url.clone()); |
| }, |
| }, |
| SessionHistoryDiff::Hash { |
| ref pipeline_reloader, |
| ref old_url, |
| .. |
| } => match *pipeline_reloader { |
| NeedsToReload::No(pipeline_id) => { |
| let state = pipeline_changes |
| .get(&pipeline_id) |
| .and_then(|change| change.0); |
| pipeline_changes.insert(pipeline_id, (state, old_url.clone())); |
| }, |
| NeedsToReload::Yes(pipeline_id, ..) => { |
| url_to_load.insert(pipeline_id, old_url.clone()); |
| }, |
| }, |
| } |
| session_history.future.push(diff); |
| } |
| }, |
| } |
| } |
| |
| for (browsing_context_id, mut pipeline_reloader) in browsing_context_changes.drain() { |
| if let NeedsToReload::Yes(pipeline_id, ref mut load_data) = pipeline_reloader { |
| if let Some(url) = url_to_load.get(&pipeline_id) { |
| load_data.url = url.clone(); |
| } |
| } |
| self.update_browsing_context(browsing_context_id, pipeline_reloader); |
| } |
| |
| for (pipeline_id, (history_state_id, url)) in pipeline_changes.drain() { |
| self.update_pipeline(pipeline_id, history_state_id, url); |
| } |
| |
| self.notify_history_changed(webview_id); |
| |
| self.trim_history(webview_id); |
| self.update_webview_in_compositor(webview_id); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn update_browsing_context( |
| &mut self, |
| browsing_context_id: BrowsingContextId, |
| new_reloader: NeedsToReload, |
| ) { |
| let new_pipeline_id = match new_reloader { |
| NeedsToReload::No(pipeline_id) => pipeline_id, |
| NeedsToReload::Yes(pipeline_id, load_data) => { |
| debug!( |
| "{}: Reloading document {}", |
| browsing_context_id, pipeline_id, |
| ); |
| |
| // TODO: Save the sandbox state so it can be restored here. |
| let sandbox = IFrameSandboxState::IFrameUnsandboxed; |
| let ( |
| top_level_id, |
| old_pipeline_id, |
| parent_pipeline_id, |
| viewport_details, |
| is_private, |
| throttled, |
| ) = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(ctx) => ( |
| ctx.top_level_id, |
| ctx.pipeline_id, |
| ctx.parent_pipeline_id, |
| ctx.viewport_details, |
| ctx.is_private, |
| ctx.throttled, |
| ), |
| None => return warn!("No browsing context to traverse!"), |
| }; |
| let opener = match self.pipelines.get(&old_pipeline_id) { |
| Some(pipeline) => pipeline.opener, |
| None => None, |
| }; |
| let new_pipeline_id = PipelineId::new(); |
| self.new_pipeline( |
| new_pipeline_id, |
| browsing_context_id, |
| top_level_id, |
| parent_pipeline_id, |
| opener, |
| viewport_details, |
| load_data.clone(), |
| sandbox, |
| is_private, |
| throttled, |
| ); |
| self.add_pending_change(SessionHistoryChange { |
| webview_id: top_level_id, |
| browsing_context_id, |
| new_pipeline_id, |
| replace: Some(NeedsToReload::Yes(pipeline_id, load_data)), |
| // Browsing context must exist at this point. |
| new_browsing_context_info: None, |
| viewport_details, |
| }); |
| return; |
| }, |
| }; |
| |
| let (old_pipeline_id, parent_pipeline_id, top_level_id) = |
| match self.browsing_contexts.get_mut(&browsing_context_id) { |
| Some(browsing_context) => { |
| let old_pipeline_id = browsing_context.pipeline_id; |
| browsing_context.update_current_entry(new_pipeline_id); |
| ( |
| old_pipeline_id, |
| browsing_context.parent_pipeline_id, |
| browsing_context.top_level_id, |
| ) |
| }, |
| None => { |
| return warn!("{}: Closed during traversal", browsing_context_id); |
| }, |
| }; |
| |
| if let Some(old_pipeline) = self.pipelines.get(&old_pipeline_id) { |
| old_pipeline.set_throttled(true); |
| } |
| if let Some(new_pipeline) = self.pipelines.get(&new_pipeline_id) { |
| if let Some(ref chan) = self.devtools_sender { |
| let state = NavigationState::Start(new_pipeline.url.clone()); |
| let _ = chan.send(DevtoolsControlMsg::FromScript( |
| ScriptToDevtoolsControlMsg::Navigate(browsing_context_id, state), |
| )); |
| let page_info = DevtoolsPageInfo { |
| title: new_pipeline.title.clone(), |
| url: new_pipeline.url.clone(), |
| is_top_level_global: top_level_id == browsing_context_id, |
| }; |
| let state = NavigationState::Stop(new_pipeline.id, page_info); |
| let _ = chan.send(DevtoolsControlMsg::FromScript( |
| ScriptToDevtoolsControlMsg::Navigate(browsing_context_id, state), |
| )); |
| } |
| |
| new_pipeline.set_throttled(false); |
| self.notify_focus_state(new_pipeline_id); |
| } |
| |
| self.update_activity(old_pipeline_id); |
| self.update_activity(new_pipeline_id); |
| |
| if let Some(parent_pipeline_id) = parent_pipeline_id { |
| let msg = ScriptThreadMessage::UpdatePipelineId( |
| parent_pipeline_id, |
| browsing_context_id, |
| top_level_id, |
| new_pipeline_id, |
| UpdatePipelineIdReason::Traversal, |
| ); |
| let result = match self.pipelines.get(&parent_pipeline_id) { |
| None => { |
| return warn!("{}: Child traversed after closure", parent_pipeline_id); |
| }, |
| Some(pipeline) => pipeline.event_loop.send(msg), |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(parent_pipeline_id, e); |
| } |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn update_pipeline( |
| &mut self, |
| pipeline_id: PipelineId, |
| history_state_id: Option<HistoryStateId>, |
| url: ServoUrl, |
| ) { |
| let result = match self.pipelines.get_mut(&pipeline_id) { |
| None => { |
| return warn!("{}: History state updated after closure", pipeline_id); |
| }, |
| Some(pipeline) => { |
| let msg = ScriptThreadMessage::UpdateHistoryState( |
| pipeline_id, |
| history_state_id, |
| url.clone(), |
| ); |
| pipeline.history_state_id = history_state_id; |
| pipeline.url = url; |
| pipeline.event_loop.send(msg) |
| }, |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(pipeline_id, e); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_joint_session_history_length( |
| &self, |
| webview_id: WebViewId, |
| response_sender: IpcSender<u32>, |
| ) { |
| let length = self |
| .webviews |
| .get(webview_id) |
| .map(|webview| webview.session_history.history_length()) |
| .unwrap_or(1); |
| let _ = response_sender.send(length as u32); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_push_history_state_msg( |
| &mut self, |
| pipeline_id: PipelineId, |
| history_state_id: HistoryStateId, |
| url: ServoUrl, |
| ) { |
| let (webview_id, old_state_id, old_url) = match self.pipelines.get_mut(&pipeline_id) { |
| Some(pipeline) => { |
| let old_history_state_id = pipeline.history_state_id; |
| let old_url = replace(&mut pipeline.url, url.clone()); |
| pipeline.history_state_id = Some(history_state_id); |
| pipeline.history_states.insert(history_state_id); |
| (pipeline.webview_id, old_history_state_id, old_url) |
| }, |
| None => { |
| return warn!( |
| "{}: Push history state {} for closed pipeline", |
| pipeline_id, history_state_id, |
| ); |
| }, |
| }; |
| |
| let diff = SessionHistoryDiff::Pipeline { |
| pipeline_reloader: NeedsToReload::No(pipeline_id), |
| new_history_state_id: history_state_id, |
| new_url: url, |
| old_history_state_id: old_state_id, |
| old_url, |
| }; |
| self.get_joint_session_history(webview_id).push_diff(diff); |
| self.notify_history_changed(webview_id); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_replace_history_state_msg( |
| &mut self, |
| pipeline_id: PipelineId, |
| history_state_id: HistoryStateId, |
| url: ServoUrl, |
| ) { |
| let webview_id = match self.pipelines.get_mut(&pipeline_id) { |
| Some(pipeline) => { |
| pipeline.history_state_id = Some(history_state_id); |
| pipeline.url = url.clone(); |
| pipeline.webview_id |
| }, |
| None => { |
| return warn!( |
| "{}: Replace history state {} for closed pipeline", |
| history_state_id, pipeline_id |
| ); |
| }, |
| }; |
| |
| let session_history = self.get_joint_session_history(webview_id); |
| session_history.replace_history_state(pipeline_id, history_state_id, url); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_reload_msg(&mut self, webview_id: WebViewId) { |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(browsing_context) => browsing_context.pipeline_id, |
| None => { |
| return warn!("{}: Got reload event after closure", browsing_context_id); |
| }, |
| }; |
| let msg = ScriptThreadMessage::Reload(pipeline_id); |
| let result = match self.pipelines.get(&pipeline_id) { |
| None => return warn!("{}: Got reload event after closure", pipeline_id), |
| Some(pipeline) => pipeline.event_loop.send(msg), |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(pipeline_id, e); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_post_message_msg( |
| &mut self, |
| browsing_context_id: BrowsingContextId, |
| source_pipeline: PipelineId, |
| origin: Option<ImmutableOrigin>, |
| source_origin: ImmutableOrigin, |
| data: StructuredSerializedData, |
| ) { |
| let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) { |
| None => { |
| return warn!( |
| "{}: PostMessage to closed browsing context", |
| browsing_context_id |
| ); |
| }, |
| Some(browsing_context) => browsing_context.pipeline_id, |
| }; |
| let source_browsing_context = match self.pipelines.get(&source_pipeline) { |
| Some(pipeline) => pipeline.webview_id, |
| None => return warn!("{}: PostMessage from closed pipeline", source_pipeline), |
| }; |
| let msg = ScriptThreadMessage::PostMessage { |
| target: pipeline_id, |
| source: source_pipeline, |
| source_browsing_context, |
| target_origin: origin, |
| source_origin, |
| data: Box::new(data), |
| }; |
| let result = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.event_loop.send(msg), |
| None => return warn!("{}: PostMessage to closed pipeline", pipeline_id), |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(pipeline_id, e); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_focus_msg( |
| &mut self, |
| pipeline_id: PipelineId, |
| focused_child_browsing_context_id: Option<BrowsingContextId>, |
| sequence: FocusSequenceNumber, |
| ) { |
| let (browsing_context_id, webview_id) = match self.pipelines.get_mut(&pipeline_id) { |
| Some(pipeline) => { |
| pipeline.focus_sequence = sequence; |
| (pipeline.browsing_context_id, pipeline.webview_id) |
| }, |
| None => return warn!("{}: Focus parent after closure", pipeline_id), |
| }; |
| |
| // Ignore if the pipeline isn't fully active. |
| if self.get_activity(pipeline_id) != DocumentActivity::FullyActive { |
| debug!( |
| "Ignoring the focus request because pipeline {} is not \ |
| fully active", |
| pipeline_id |
| ); |
| return; |
| } |
| |
| // Focus the top-level browsing context. |
| let focused = self.webviews.focus(webview_id); |
| self.embedder_proxy.send(EmbedderMsg::WebViewFocused( |
| webview_id, |
| FocusId::new(), |
| focused.is_ok(), |
| )); |
| |
| // If a container with a non-null nested browsing context is focused, |
| // the nested browsing context's active document becomes the focused |
| // area of the top-level browsing context instead. |
| let focused_browsing_context_id = |
| focused_child_browsing_context_id.unwrap_or(browsing_context_id); |
| |
| // Send focus messages to the affected pipelines, except |
| // `pipeline_id`, which has already its local focus state |
| // updated. |
| self.focus_browsing_context(Some(pipeline_id), focused_browsing_context_id); |
| } |
| |
| fn handle_focus_remote_document_msg(&mut self, focused_browsing_context_id: BrowsingContextId) { |
| let pipeline_id = match self.browsing_contexts.get(&focused_browsing_context_id) { |
| Some(browsing_context) => browsing_context.pipeline_id, |
| None => return warn!("Browsing context {} not found", focused_browsing_context_id), |
| }; |
| |
| // Ignore if its active document isn't fully active. |
| if self.get_activity(pipeline_id) != DocumentActivity::FullyActive { |
| debug!( |
| "Ignoring the remote focus request because pipeline {} of \ |
| browsing context {} is not fully active", |
| pipeline_id, focused_browsing_context_id, |
| ); |
| return; |
| } |
| |
| self.focus_browsing_context(None, focused_browsing_context_id); |
| } |
| |
| /// Perform [the focusing steps][1] for the active document of |
| /// `focused_browsing_context_id`. |
| /// |
| /// If `initiator_pipeline_id` is specified, this method avoids sending |
| /// a message to `initiator_pipeline_id`, assuming its local focus state has |
| /// already been updated. This is necessary for performing the focusing |
| /// steps for an object that is not the document itself but something that |
| /// belongs to the document. |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#focusing-steps |
| #[servo_tracing::instrument(skip_all)] |
| fn focus_browsing_context( |
| &mut self, |
| initiator_pipeline_id: Option<PipelineId>, |
| focused_browsing_context_id: BrowsingContextId, |
| ) { |
| let webview_id = match self.browsing_contexts.get(&focused_browsing_context_id) { |
| Some(browsing_context) => browsing_context.top_level_id, |
| None => return warn!("Browsing context {} not found", focused_browsing_context_id), |
| }; |
| |
| // Update the webview’s focused browsing context. |
| let old_focused_browsing_context_id = match self.webviews.get_mut(webview_id) { |
| Some(browser) => replace( |
| &mut browser.focused_browsing_context_id, |
| focused_browsing_context_id, |
| ), |
| None => { |
| return warn!( |
| "{}: Browsing context for focus msg does not exist", |
| webview_id |
| ); |
| }, |
| }; |
| |
| // The following part is similar to [the focus update steps][1] except |
| // that only `Document`s in the given focus chains are considered. It's |
| // ultimately up to the script threads to fire focus events at the |
| // affected objects. |
| // |
| // [1]: https://html.spec.whatwg.org/multipage/#focus-update-steps |
| let mut old_focus_chain_pipelines: Vec<&Pipeline> = self |
| .ancestor_or_self_pipelines_of_browsing_context_iter(old_focused_browsing_context_id) |
| .collect(); |
| let mut new_focus_chain_pipelines: Vec<&Pipeline> = self |
| .ancestor_or_self_pipelines_of_browsing_context_iter(focused_browsing_context_id) |
| .collect(); |
| |
| debug!( |
| "old_focus_chain_pipelines = {:?}", |
| old_focus_chain_pipelines |
| .iter() |
| .map(|p| p.id.to_string()) |
| .collect::<Vec<_>>() |
| ); |
| debug!( |
| "new_focus_chain_pipelines = {:?}", |
| new_focus_chain_pipelines |
| .iter() |
| .map(|p| p.id.to_string()) |
| .collect::<Vec<_>>() |
| ); |
| |
| // At least the last entries should match. Otherwise something is wrong, |
| // and we don't want to proceed and crash the top-level pipeline by |
| // sending an impossible `Unfocus` message to it. |
| match ( |
| &old_focus_chain_pipelines[..], |
| &new_focus_chain_pipelines[..], |
| ) { |
| ([.., p1], [.., p2]) if p1.id == p2.id => {}, |
| _ => { |
| warn!("Aborting the focus operation - focus chain sanity check failed"); |
| return; |
| }, |
| } |
| |
| // > If the last entry in `old chain` and the last entry in `new chain` |
| // > are the same, pop the last entry from `old chain` and the last |
| // > entry from `new chain` and redo this step. |
| let mut first_common_pipeline_in_chain = None; |
| while let ([.., p1], [.., p2]) = ( |
| &old_focus_chain_pipelines[..], |
| &new_focus_chain_pipelines[..], |
| ) { |
| if p1.id != p2.id { |
| break; |
| } |
| old_focus_chain_pipelines.pop(); |
| first_common_pipeline_in_chain = new_focus_chain_pipelines.pop(); |
| } |
| |
| let mut send_errors = Vec::new(); |
| |
| // > For each entry `entry` in `old chain`, in order, run these |
| // > substeps: [...] |
| for &pipeline in old_focus_chain_pipelines.iter() { |
| if Some(pipeline.id) != initiator_pipeline_id { |
| let msg = ScriptThreadMessage::Unfocus(pipeline.id, pipeline.focus_sequence); |
| trace!("Sending {:?} to {}", msg, pipeline.id); |
| if let Err(e) = pipeline.event_loop.send(msg) { |
| send_errors.push((pipeline.id, e)); |
| } |
| } else { |
| trace!( |
| "Not notifying {} - it's the initiator of this focus operation", |
| pipeline.id |
| ); |
| } |
| } |
| |
| // > For each entry entry in `new chain`, in reverse order, run these |
| // > substeps: [...] |
| let mut child_browsing_context_id = None; |
| for &pipeline in new_focus_chain_pipelines.iter().rev() { |
| // Don't send a message to the browsing context that initiated this |
| // focus operation. It already knows that it has gotten focus. |
| if Some(pipeline.id) != initiator_pipeline_id { |
| let msg = if let Some(child_browsing_context_id) = child_browsing_context_id { |
| // Focus the container element of `child_browsing_context_id`. |
| ScriptThreadMessage::FocusIFrame( |
| pipeline.id, |
| child_browsing_context_id, |
| pipeline.focus_sequence, |
| ) |
| } else { |
| // Focus the document. |
| ScriptThreadMessage::FocusDocument(pipeline.id, pipeline.focus_sequence) |
| }; |
| trace!("Sending {:?} to {}", msg, pipeline.id); |
| if let Err(e) = pipeline.event_loop.send(msg) { |
| send_errors.push((pipeline.id, e)); |
| } |
| } else { |
| trace!( |
| "Not notifying {} - it's the initiator of this focus operation", |
| pipeline.id |
| ); |
| } |
| child_browsing_context_id = Some(pipeline.browsing_context_id); |
| } |
| |
| if let (Some(pipeline), Some(child_browsing_context_id)) = |
| (first_common_pipeline_in_chain, child_browsing_context_id) |
| { |
| if Some(pipeline.id) != initiator_pipeline_id { |
| // Focus the container element of `child_browsing_context_id`. |
| let msg = ScriptThreadMessage::FocusIFrame( |
| pipeline.id, |
| child_browsing_context_id, |
| pipeline.focus_sequence, |
| ); |
| trace!("Sending {:?} to {}", msg, pipeline.id); |
| if let Err(e) = pipeline.event_loop.send(msg) { |
| send_errors.push((pipeline.id, e)); |
| } |
| } |
| } |
| |
| for (pipeline_id, e) in send_errors { |
| self.handle_send_error(pipeline_id, e); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_remove_iframe_msg( |
| &mut self, |
| browsing_context_id: BrowsingContextId, |
| ) -> Vec<PipelineId> { |
| let result = self |
| .all_descendant_browsing_contexts_iter(browsing_context_id) |
| .flat_map(|browsing_context| browsing_context.pipelines.iter().cloned()) |
| .collect(); |
| self.close_browsing_context(browsing_context_id, ExitPipelineMode::Normal); |
| result |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_set_throttled_complete(&mut self, pipeline_id: PipelineId, throttled: bool) { |
| let browsing_context_id = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.browsing_context_id, |
| None => { |
| return warn!( |
| "{}: Visibility change for closed browsing context", |
| pipeline_id |
| ); |
| }, |
| }; |
| let parent_pipeline_id = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(ctx) => ctx.parent_pipeline_id, |
| None => { |
| return warn!("{}: Visibility change for closed pipeline", pipeline_id); |
| }, |
| }; |
| |
| if let Some(parent_pipeline_id) = parent_pipeline_id { |
| let msg = ScriptThreadMessage::SetThrottledInContainingIframe( |
| parent_pipeline_id, |
| browsing_context_id, |
| throttled, |
| ); |
| let result = match self.pipelines.get(&parent_pipeline_id) { |
| None => return warn!("{}: Parent pipeline closed", parent_pipeline_id), |
| Some(parent_pipeline) => parent_pipeline.event_loop.send(msg), |
| }; |
| |
| if let Err(e) = result { |
| self.handle_send_error(parent_pipeline_id, e); |
| } |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_create_canvas_paint_thread_msg( |
| &mut self, |
| size: UntypedSize2D<u64>, |
| response_sender: IpcSender<Option<(IpcSender<CanvasMsg>, CanvasId, ImageKey)>>, |
| ) { |
| let (canvas_data_sender, canvas_data_receiver) = unbounded(); |
| let (canvas_sender, canvas_ipc_sender) = self |
| .canvas |
| .get_or_init(|| self.create_canvas_paint_thread()); |
| |
| let response = if let Err(e) = canvas_sender.send(ConstellationCanvasMsg::Create { |
| sender: canvas_data_sender, |
| size, |
| }) { |
| warn!("Create canvas paint thread failed ({})", e); |
| None |
| } else { |
| match canvas_data_receiver.recv() { |
| Ok(Some((canvas_id, image_key))) => { |
| Some((canvas_ipc_sender.clone(), canvas_id, image_key)) |
| }, |
| Ok(None) => None, |
| Err(e) => { |
| warn!("Create canvas paint thread id response failed ({})", e); |
| None |
| }, |
| } |
| }; |
| if let Err(e) = response_sender.send(response) { |
| warn!("Create canvas paint thread response failed ({})", e); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_webdriver_msg(&mut self, msg: WebDriverCommandMsg) { |
| // Find the script channel for the given parent pipeline, |
| // and pass the event to that script thread. |
| match msg { |
| WebDriverCommandMsg::IsBrowsingContextOpen(browsing_context_id, response_sender) => { |
| let is_open = self.browsing_contexts.contains_key(&browsing_context_id); |
| let _ = response_sender.send(is_open); |
| }, |
| WebDriverCommandMsg::FocusBrowsingContext(browsing_context_id) => { |
| self.handle_focus_remote_document_msg(browsing_context_id); |
| }, |
| // TODO: This should use the ScriptThreadMessage::EvaluateJavaScript command |
| WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd) => { |
| let pipeline_id = if let Some(browsing_context) = |
| self.browsing_contexts.get(&browsing_context_id) |
| { |
| browsing_context.pipeline_id |
| } else { |
| return warn!("{}: Browsing context is not ready", browsing_context_id); |
| }; |
| |
| match &cmd { |
| WebDriverScriptCommand::AddLoadStatusSender(_, sender) => { |
| self.webdriver_load_status_sender = Some((sender.clone(), pipeline_id)); |
| }, |
| WebDriverScriptCommand::RemoveLoadStatusSender(_) => { |
| self.webdriver_load_status_sender = None; |
| }, |
| _ => {}, |
| }; |
| |
| let control_msg = ScriptThreadMessage::WebDriverScriptCommand(pipeline_id, cmd); |
| let result = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.event_loop.send(control_msg), |
| None => return warn!("{}: ScriptCommand after closure", pipeline_id), |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(pipeline_id, e); |
| } |
| }, |
| WebDriverCommandMsg::CloseWebView(..) | |
| WebDriverCommandMsg::NewWebView(..) | |
| WebDriverCommandMsg::FocusWebView(..) | |
| WebDriverCommandMsg::IsWebViewOpen(..) | |
| WebDriverCommandMsg::GetWindowRect(..) | |
| WebDriverCommandMsg::GetViewportSize(..) | |
| WebDriverCommandMsg::SetWindowRect(..) | |
| WebDriverCommandMsg::MaximizeWebView(..) | |
| WebDriverCommandMsg::LoadUrl(..) | |
| WebDriverCommandMsg::Refresh(..) | |
| WebDriverCommandMsg::DispatchComposition(..) | |
| WebDriverCommandMsg::KeyboardAction(..) | |
| WebDriverCommandMsg::MouseButtonAction(..) | |
| WebDriverCommandMsg::MouseMoveAction(..) | |
| WebDriverCommandMsg::WheelScrollAction(..) | |
| WebDriverCommandMsg::TakeScreenshot(..) => { |
| unreachable!("This command should be send directly to the embedder."); |
| }, |
| _ => { |
| warn!("Unhandled WebDriver command: {:?}", msg); |
| }, |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn set_webview_throttled(&mut self, webview_id: WebViewId, throttled: bool) { |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(browsing_context) => browsing_context.pipeline_id, |
| None => { |
| return warn!("{browsing_context_id}: Tried to SetWebViewThrottled after closure"); |
| }, |
| }; |
| match self.pipelines.get(&pipeline_id) { |
| None => warn!("{pipeline_id}: Tried to SetWebViewThrottled after closure"), |
| Some(pipeline) => pipeline.set_throttled(throttled), |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn notify_history_changed(&self, webview_id: WebViewId) { |
| // Send a flat projection of the history to embedder. |
| // The final vector is a concatenation of the URLs of the past |
| // entries, the current entry and the future entries. |
| // URLs of inner frames are ignored and replaced with the URL |
| // of the parent. |
| |
| let session_history = match self.webviews.get(webview_id) { |
| Some(webview) => &webview.session_history, |
| None => { |
| return warn!( |
| "{}: Session history does not exist for browsing context", |
| webview_id |
| ); |
| }, |
| }; |
| |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| let browsing_context = match self.browsing_contexts.get(&browsing_context_id) { |
| Some(browsing_context) => browsing_context, |
| None => { |
| return warn!( |
| "notify_history_changed error after top-level browsing context closed." |
| ); |
| }, |
| }; |
| |
| let current_url = match self.pipelines.get(&browsing_context.pipeline_id) { |
| Some(pipeline) => pipeline.url.clone(), |
| None => { |
| return warn!("{}: Refresh after closure", browsing_context.pipeline_id); |
| }, |
| }; |
| |
| // If URL was ignored, use the URL of the previous SessionHistoryEntry, which |
| // is the URL of the parent browsing context. |
| let resolve_url_future = |
| |previous_url: &mut ServoUrl, diff: &SessionHistoryDiff| match *diff { |
| SessionHistoryDiff::BrowsingContext { |
| browsing_context_id, |
| ref new_reloader, |
| .. |
| } => { |
| if browsing_context_id == webview_id { |
| let url = match *new_reloader { |
| NeedsToReload::No(pipeline_id) => { |
| match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.url.clone(), |
| None => previous_url.clone(), |
| } |
| }, |
| NeedsToReload::Yes(_, ref load_data) => load_data.url.clone(), |
| }; |
| *previous_url = url.clone(); |
| Some(url) |
| } else { |
| Some(previous_url.clone()) |
| } |
| }, |
| _ => Some(previous_url.clone()), |
| }; |
| |
| let resolve_url_past = |previous_url: &mut ServoUrl, diff: &SessionHistoryDiff| match *diff |
| { |
| SessionHistoryDiff::BrowsingContext { |
| browsing_context_id, |
| ref old_reloader, |
| .. |
| } => { |
| if browsing_context_id == webview_id { |
| let url = match *old_reloader { |
| NeedsToReload::No(pipeline_id) => match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.url.clone(), |
| None => previous_url.clone(), |
| }, |
| NeedsToReload::Yes(_, ref load_data) => load_data.url.clone(), |
| }; |
| *previous_url = url.clone(); |
| Some(url) |
| } else { |
| Some(previous_url.clone()) |
| } |
| }, |
| _ => Some(previous_url.clone()), |
| }; |
| |
| let mut entries: Vec<ServoUrl> = session_history |
| .past |
| .iter() |
| .rev() |
| .scan(current_url.clone(), &resolve_url_past) |
| .collect(); |
| |
| entries.reverse(); |
| |
| let current_index = entries.len(); |
| |
| entries.push(current_url.clone()); |
| |
| entries.extend( |
| session_history |
| .future |
| .iter() |
| .rev() |
| .scan(current_url, &resolve_url_future), |
| ); |
| self.embedder_proxy.send(EmbedderMsg::HistoryChanged( |
| webview_id, |
| entries, |
| current_index, |
| )); |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn change_session_history(&mut self, change: SessionHistoryChange) { |
| debug!( |
| "{}: Setting to {}", |
| change.browsing_context_id, change.new_pipeline_id |
| ); |
| |
| // If the currently focused browsing context is a child of the browsing |
| // context in which the page is being loaded, then update the focused |
| // browsing context to be the one where the page is being loaded. |
| if self.focused_browsing_context_is_descendant_of(change.browsing_context_id) { |
| if let Some(webview) = self.webviews.get_mut(change.webview_id) { |
| webview.focused_browsing_context_id = change.browsing_context_id; |
| } |
| } |
| |
| let (old_pipeline_id, top_level_id) = |
| match self.browsing_contexts.get_mut(&change.browsing_context_id) { |
| Some(browsing_context) => { |
| debug!("Adding pipeline to existing browsing context."); |
| let old_pipeline_id = browsing_context.pipeline_id; |
| browsing_context.pipelines.insert(change.new_pipeline_id); |
| browsing_context.update_current_entry(change.new_pipeline_id); |
| (Some(old_pipeline_id), Some(browsing_context.top_level_id)) |
| }, |
| None => { |
| debug!("Adding pipeline to new browsing context."); |
| (None, None) |
| }, |
| }; |
| |
| match old_pipeline_id { |
| None => { |
| let new_context_info = match change.new_browsing_context_info { |
| Some(info) => info, |
| None => { |
| return warn!( |
| "{}: No NewBrowsingContextInfo for browsing context", |
| change.browsing_context_id, |
| ); |
| }, |
| }; |
| self.new_browsing_context( |
| change.browsing_context_id, |
| change.webview_id, |
| change.new_pipeline_id, |
| new_context_info.parent_pipeline_id, |
| change.viewport_details, |
| new_context_info.is_private, |
| new_context_info.inherited_secure_context, |
| new_context_info.throttled, |
| ); |
| self.update_activity(change.new_pipeline_id); |
| }, |
| Some(old_pipeline_id) => { |
| if let Some(pipeline) = self.pipelines.get(&old_pipeline_id) { |
| pipeline.set_throttled(true); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#unload-a-document |
| self.unload_document(old_pipeline_id); |
| // Deactivate the old pipeline, and activate the new one. |
| let (pipelines_to_close, states_to_close) = if let Some(replace_reloader) = |
| change.replace |
| { |
| self.get_joint_session_history(change.webview_id) |
| .replace_reloader( |
| replace_reloader.clone(), |
| NeedsToReload::No(change.new_pipeline_id), |
| ); |
| |
| match replace_reloader { |
| NeedsToReload::No(pipeline_id) => (Some(vec![pipeline_id]), None), |
| NeedsToReload::Yes(..) => (None, None), |
| } |
| } else { |
| let diff = SessionHistoryDiff::BrowsingContext { |
| browsing_context_id: change.browsing_context_id, |
| new_reloader: NeedsToReload::No(change.new_pipeline_id), |
| old_reloader: NeedsToReload::No(old_pipeline_id), |
| }; |
| |
| let mut pipelines_to_close = vec![]; |
| let mut states_to_close = FnvHashMap::default(); |
| |
| let diffs_to_close = self |
| .get_joint_session_history(change.webview_id) |
| .push_diff(diff); |
| |
| for diff in diffs_to_close { |
| match diff { |
| SessionHistoryDiff::BrowsingContext { new_reloader, .. } => { |
| if let Some(pipeline_id) = new_reloader.alive_pipeline_id() { |
| pipelines_to_close.push(pipeline_id); |
| } |
| }, |
| SessionHistoryDiff::Pipeline { |
| pipeline_reloader, |
| new_history_state_id, |
| .. |
| } => { |
| if let Some(pipeline_id) = pipeline_reloader.alive_pipeline_id() { |
| let states = |
| states_to_close.entry(pipeline_id).or_insert(Vec::new()); |
| states.push(new_history_state_id); |
| } |
| }, |
| _ => {}, |
| } |
| } |
| |
| (Some(pipelines_to_close), Some(states_to_close)) |
| }; |
| |
| self.update_activity(old_pipeline_id); |
| self.update_activity(change.new_pipeline_id); |
| |
| if let Some(states_to_close) = states_to_close { |
| for (pipeline_id, states) in states_to_close { |
| let msg = ScriptThreadMessage::RemoveHistoryStates(pipeline_id, states); |
| let result = match self.pipelines.get(&pipeline_id) { |
| None => { |
| return warn!( |
| "{}: Removed history states after closure", |
| pipeline_id |
| ); |
| }, |
| Some(pipeline) => pipeline.event_loop.send(msg), |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(pipeline_id, e); |
| } |
| } |
| } |
| |
| if let Some(pipelines_to_close) = pipelines_to_close { |
| for pipeline_id in pipelines_to_close { |
| self.close_pipeline( |
| pipeline_id, |
| DiscardBrowsingContext::No, |
| ExitPipelineMode::Normal, |
| ); |
| } |
| } |
| }, |
| } |
| |
| if let Some(top_level_id) = top_level_id { |
| self.trim_history(top_level_id); |
| } |
| |
| self.notify_focus_state(change.new_pipeline_id); |
| |
| self.notify_history_changed(change.webview_id); |
| self.update_webview_in_compositor(change.webview_id); |
| } |
| |
| /// Update the focus state of the specified pipeline that recently became |
| /// active (thus doesn't have a focused container element) and may have |
| /// out-dated information. |
| fn notify_focus_state(&mut self, pipeline_id: PipelineId) { |
| let pipeline = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline, |
| None => return warn!("Pipeline {} is closed", pipeline_id), |
| }; |
| |
| let is_focused = match self.webviews.get(pipeline.webview_id) { |
| Some(webview) => webview.focused_browsing_context_id == pipeline.browsing_context_id, |
| None => { |
| return warn!( |
| "Pipeline {}'s top-level browsing context {} is closed", |
| pipeline_id, pipeline.webview_id |
| ); |
| }, |
| }; |
| |
| // If the browsing context is focused, focus the document |
| let msg = if is_focused { |
| ScriptThreadMessage::FocusDocument(pipeline_id, pipeline.focus_sequence) |
| } else { |
| ScriptThreadMessage::Unfocus(pipeline_id, pipeline.focus_sequence) |
| }; |
| if let Err(e) = pipeline.event_loop.send(msg) { |
| self.handle_send_error(pipeline_id, e); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn focused_browsing_context_is_descendant_of( |
| &self, |
| browsing_context_id: BrowsingContextId, |
| ) -> bool { |
| let focused_browsing_context_id = self |
| .webviews |
| .focused_webview() |
| .map(|(_, webview)| webview.focused_browsing_context_id); |
| focused_browsing_context_id.is_some_and(|focus_ctx_id| { |
| focus_ctx_id == browsing_context_id || |
| self.fully_active_descendant_browsing_contexts_iter(browsing_context_id) |
| .any(|nested_ctx| nested_ctx.id == focus_ctx_id) |
| }) |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn trim_history(&mut self, webview_id: WebViewId) { |
| let pipelines_to_evict = { |
| let session_history = self.get_joint_session_history(webview_id); |
| |
| let history_length = pref!(session_history_max_length) as usize; |
| |
| // The past is stored with older entries at the front. |
| // We reverse the iter so that newer entries are at the front and then |
| // skip _n_ entries and evict the remaining entries. |
| let mut pipelines_to_evict = session_history |
| .past |
| .iter() |
| .rev() |
| .map(|diff| diff.alive_old_pipeline()) |
| .skip(history_length) |
| .flatten() |
| .collect::<Vec<_>>(); |
| |
| // The future is stored with oldest entries front, so we must |
| // reverse the iterator like we do for the `past`. |
| pipelines_to_evict.extend( |
| session_history |
| .future |
| .iter() |
| .rev() |
| .map(|diff| diff.alive_new_pipeline()) |
| .skip(history_length) |
| .flatten(), |
| ); |
| |
| pipelines_to_evict |
| }; |
| |
| let mut dead_pipelines = vec![]; |
| for evicted_id in pipelines_to_evict { |
| let load_data = match self.pipelines.get(&evicted_id) { |
| Some(pipeline) => { |
| let mut load_data = pipeline.load_data.clone(); |
| load_data.url = pipeline.url.clone(); |
| load_data |
| }, |
| None => continue, |
| }; |
| |
| dead_pipelines.push((evicted_id, NeedsToReload::Yes(evicted_id, load_data))); |
| self.close_pipeline( |
| evicted_id, |
| DiscardBrowsingContext::No, |
| ExitPipelineMode::Normal, |
| ); |
| } |
| |
| let session_history = self.get_joint_session_history(webview_id); |
| |
| for (alive_id, dead) in dead_pipelines { |
| session_history.replace_reloader(NeedsToReload::No(alive_id), dead); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_activate_document_msg(&mut self, pipeline_id: PipelineId) { |
| debug!("{}: Document ready to activate", pipeline_id); |
| |
| // Find the pending change whose new pipeline id is pipeline_id. |
| let pending_index = self |
| .pending_changes |
| .iter() |
| .rposition(|change| change.new_pipeline_id == pipeline_id); |
| |
| // If it is found, remove it from the pending changes, and make it |
| // the active document of its frame. |
| if let Some(pending_index) = pending_index { |
| let change = self.pending_changes.swap_remove(pending_index); |
| // Notify the parent (if there is one). |
| let parent_pipeline_id = match change.new_browsing_context_info { |
| // This will be a new browsing context. |
| Some(ref info) => info.parent_pipeline_id, |
| // This is an existing browsing context. |
| None => match self.browsing_contexts.get(&change.browsing_context_id) { |
| Some(ctx) => ctx.parent_pipeline_id, |
| None => { |
| return warn!( |
| "{}: Activated document after closure of {}", |
| change.new_pipeline_id, change.browsing_context_id, |
| ); |
| }, |
| }, |
| }; |
| if let Some(parent_pipeline_id) = parent_pipeline_id { |
| if let Some(parent_pipeline) = self.pipelines.get(&parent_pipeline_id) { |
| let msg = ScriptThreadMessage::UpdatePipelineId( |
| parent_pipeline_id, |
| change.browsing_context_id, |
| change.webview_id, |
| pipeline_id, |
| UpdatePipelineIdReason::Navigation, |
| ); |
| let _ = parent_pipeline.event_loop.send(msg); |
| } |
| } |
| self.change_session_history(change); |
| } |
| } |
| |
| /// Called when the window is resized. |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_change_viewport_details_msg( |
| &mut self, |
| webview_id: WebViewId, |
| new_viewport_details: ViewportDetails, |
| size_type: WindowSizeType, |
| ) { |
| debug!( |
| "handle_change_viewport_details_msg: {:?}", |
| new_viewport_details.size.to_untyped() |
| ); |
| |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| self.resize_browsing_context(new_viewport_details, size_type, browsing_context_id); |
| } |
| |
| /// Called when the window exits from fullscreen mode |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_exit_fullscreen_msg(&mut self, webview_id: WebViewId) { |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| self.switch_fullscreen_mode(browsing_context_id); |
| } |
| |
| /// Checks the state of all script and layout pipelines to see if they are idle |
| /// and compares the current layout state to what the compositor has. This is used |
| /// to check if the output image is "stable" and can be written as a screenshot |
| /// for reftests. |
| /// Since this function is only used in reftests, we do not harden it against panic. |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_is_ready_to_save_image( |
| &mut self, |
| pipeline_states: FnvHashMap<PipelineId, Epoch>, |
| ) -> ReadyToSave { |
| // Note that this function can panic, due to ipc-channel creation |
| // failure. Avoiding this panic would require a mechanism for dealing |
| // with low-resource scenarios. |
| // |
| // If there is no focus browsing context yet, the initial page has |
| // not loaded, so there is nothing to save yet. |
| let Some(webview_id) = self.webviews.focused_webview().map(|(id, _)| id) else { |
| return ReadyToSave::NoTopLevelBrowsingContext; |
| }; |
| |
| // If there are pending loads, wait for those to complete. |
| if !self.pending_changes.is_empty() { |
| return ReadyToSave::PendingChanges; |
| } |
| |
| // Step through the fully active browsing contexts, checking that the script thread is idle, |
| // and that the current epoch of the layout matches what the compositor has painted. If all |
| // these conditions are met, then the output image should not change and a reftest |
| // screenshot can safely be written. |
| for browsing_context in self.fully_active_browsing_contexts_iter(webview_id) { |
| let pipeline_id = browsing_context.pipeline_id; |
| trace!( |
| "{}: Checking readiness of {}", |
| browsing_context.id, pipeline_id |
| ); |
| |
| let pipeline = match self.pipelines.get(&pipeline_id) { |
| None => { |
| warn!("{}: Screenshot while closing", pipeline_id); |
| continue; |
| }, |
| Some(pipeline) => pipeline, |
| }; |
| |
| // See if this pipeline has reached idle script state yet. |
| match self.document_states.get(&browsing_context.pipeline_id) { |
| Some(&DocumentState::Idle) => {}, |
| Some(&DocumentState::Pending) | None => { |
| return ReadyToSave::DocumentLoading; |
| }, |
| } |
| |
| // Check the visible rectangle for this pipeline. If the constellation has received a |
| // size for the pipeline, then its painting should be up to date. |
| // |
| // If the rectangle for this pipeline is zero sized, it will |
| // never be painted. In this case, don't query the layout |
| // thread as it won't contribute to the final output image. |
| if browsing_context.viewport_details.size == Size2D::zero() { |
| continue; |
| } |
| |
| // Get the epoch that the compositor has drawn for this pipeline and then check if the |
| // last laid out epoch matches what the compositor has drawn. If they match (and script |
| // is idle) then this pipeline won't change again and can be considered stable. |
| let compositor_epoch = pipeline_states.get(&browsing_context.pipeline_id); |
| match compositor_epoch { |
| Some(compositor_epoch) => { |
| if pipeline.layout_epoch != *compositor_epoch { |
| return ReadyToSave::EpochMismatch; |
| } |
| }, |
| None => { |
| // The compositor doesn't know about this pipeline yet. |
| // Assume it hasn't rendered yet. |
| return ReadyToSave::PipelineUnknown; |
| }, |
| } |
| } |
| |
| // All script threads are idle and layout epochs match compositor, so output image! |
| ReadyToSave::Ready |
| } |
| |
| /// Get the current activity of a pipeline. |
| #[servo_tracing::instrument(skip_all)] |
| fn get_activity(&self, pipeline_id: PipelineId) -> DocumentActivity { |
| let mut ancestor_id = pipeline_id; |
| loop { |
| if let Some(ancestor) = self.pipelines.get(&ancestor_id) { |
| if let Some(browsing_context) = |
| self.browsing_contexts.get(&ancestor.browsing_context_id) |
| { |
| if browsing_context.pipeline_id == ancestor_id { |
| if let Some(parent_pipeline_id) = browsing_context.parent_pipeline_id { |
| ancestor_id = parent_pipeline_id; |
| continue; |
| } else { |
| return DocumentActivity::FullyActive; |
| } |
| } |
| } |
| } |
| if pipeline_id == ancestor_id { |
| return DocumentActivity::Inactive; |
| } else { |
| return DocumentActivity::Active; |
| } |
| } |
| } |
| |
| /// Set the current activity of a pipeline. |
| #[servo_tracing::instrument(skip_all)] |
| fn set_activity(&self, pipeline_id: PipelineId, activity: DocumentActivity) { |
| debug!("{}: Setting activity to {:?}", pipeline_id, activity); |
| if let Some(pipeline) = self.pipelines.get(&pipeline_id) { |
| pipeline.set_activity(activity); |
| let child_activity = if activity == DocumentActivity::Inactive { |
| DocumentActivity::Active |
| } else { |
| activity |
| }; |
| for child_id in &pipeline.children { |
| if let Some(child) = self.browsing_contexts.get(child_id) { |
| self.set_activity(child.pipeline_id, child_activity); |
| } |
| } |
| } |
| } |
| |
| /// Update the current activity of a pipeline. |
| #[servo_tracing::instrument(skip_all)] |
| fn update_activity(&self, pipeline_id: PipelineId) { |
| self.set_activity(pipeline_id, self.get_activity(pipeline_id)); |
| } |
| |
| /// Handle updating the size of a browsing context. |
| /// This notifies every pipeline in the context of the new size. |
| #[servo_tracing::instrument(skip_all)] |
| fn resize_browsing_context( |
| &mut self, |
| new_viewport_details: ViewportDetails, |
| size_type: WindowSizeType, |
| browsing_context_id: BrowsingContextId, |
| ) { |
| if let Some(browsing_context) = self.browsing_contexts.get_mut(&browsing_context_id) { |
| browsing_context.viewport_details = new_viewport_details; |
| // Send Resize (or ResizeInactive) messages to each pipeline in the frame tree. |
| let pipeline_id = browsing_context.pipeline_id; |
| let pipeline = match self.pipelines.get(&pipeline_id) { |
| None => return warn!("{}: Resized after closing", pipeline_id), |
| Some(pipeline) => pipeline, |
| }; |
| let _ = pipeline.event_loop.send(ScriptThreadMessage::Resize( |
| pipeline.id, |
| new_viewport_details, |
| size_type, |
| )); |
| let pipeline_ids = browsing_context |
| .pipelines |
| .iter() |
| .filter(|pipeline_id| **pipeline_id != pipeline.id); |
| for id in pipeline_ids { |
| if let Some(pipeline) = self.pipelines.get(id) { |
| let _ = pipeline |
| .event_loop |
| .send(ScriptThreadMessage::ResizeInactive( |
| pipeline.id, |
| new_viewport_details, |
| )); |
| } |
| } |
| } |
| |
| // Send resize message to any pending pipelines that aren't loaded yet. |
| for change in &self.pending_changes { |
| let pipeline_id = change.new_pipeline_id; |
| let pipeline = match self.pipelines.get(&pipeline_id) { |
| None => { |
| warn!("{}: Pending pipeline is closed", pipeline_id); |
| continue; |
| }, |
| Some(pipeline) => pipeline, |
| }; |
| if pipeline.browsing_context_id == browsing_context_id { |
| let _ = pipeline.event_loop.send(ScriptThreadMessage::Resize( |
| pipeline.id, |
| new_viewport_details, |
| size_type, |
| )); |
| } |
| } |
| } |
| |
| /// Handle theme change events from the embedder and forward them to all appropriate `ScriptThread`s. |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_theme_change(&mut self, webview_id: WebViewId, theme: Theme) { |
| let Some(webview) = self.webviews.get_mut(webview_id) else { |
| warn!("Received theme change request for uknown WebViewId: {webview_id:?}"); |
| return; |
| }; |
| if !webview.set_theme(theme) { |
| return; |
| } |
| |
| for pipeline in self.pipelines.values() { |
| if pipeline.webview_id != webview_id { |
| continue; |
| } |
| if let Err(error) = pipeline |
| .event_loop |
| .send(ScriptThreadMessage::ThemeChange(pipeline.id, theme)) |
| { |
| warn!( |
| "{}: Failed to send theme change event to pipeline ({error:?}).", |
| pipeline.id, |
| ); |
| } |
| } |
| } |
| |
| // Handle switching from fullscreen mode |
| #[servo_tracing::instrument(skip_all)] |
| fn switch_fullscreen_mode(&mut self, browsing_context_id: BrowsingContextId) { |
| if let Some(browsing_context) = self.browsing_contexts.get(&browsing_context_id) { |
| let pipeline_id = browsing_context.pipeline_id; |
| let pipeline = match self.pipelines.get(&pipeline_id) { |
| None => { |
| return warn!( |
| "{}: Switched from fullscreen mode after closing", |
| pipeline_id |
| ); |
| }, |
| Some(pipeline) => pipeline, |
| }; |
| let _ = pipeline |
| .event_loop |
| .send(ScriptThreadMessage::ExitFullScreen(pipeline.id)); |
| } |
| } |
| |
| // Close and return the browsing context with the given id (and its children), if it exists. |
| #[servo_tracing::instrument(skip_all)] |
| fn close_browsing_context( |
| &mut self, |
| browsing_context_id: BrowsingContextId, |
| exit_mode: ExitPipelineMode, |
| ) -> Option<BrowsingContext> { |
| debug!("{}: Closing", browsing_context_id); |
| |
| self.close_browsing_context_children( |
| browsing_context_id, |
| DiscardBrowsingContext::Yes, |
| exit_mode, |
| ); |
| |
| let browsing_context = match self.browsing_contexts.remove(&browsing_context_id) { |
| Some(ctx) => ctx, |
| None => { |
| warn!("fn close_browsing_context: {browsing_context_id}: Closing twice"); |
| return None; |
| }, |
| }; |
| |
| { |
| let session_history = self.get_joint_session_history(browsing_context.top_level_id); |
| session_history.remove_entries_for_browsing_context(browsing_context_id); |
| } |
| |
| if let Some(parent_pipeline_id) = browsing_context.parent_pipeline_id { |
| match self.pipelines.get_mut(&parent_pipeline_id) { |
| None => { |
| warn!("{parent_pipeline_id}: Child closed after parent"); |
| }, |
| Some(parent_pipeline) => { |
| parent_pipeline.remove_child(browsing_context_id); |
| |
| // If `browsing_context_id` has focus, focus the parent |
| // browsing context |
| if let Some(webview) = self.webviews.get_mut(browsing_context.top_level_id) { |
| if webview.focused_browsing_context_id == browsing_context_id { |
| trace!( |
| "About-to-be-closed browsing context {} is currently focused, so \ |
| focusing its parent {}", |
| browsing_context_id, parent_pipeline.browsing_context_id |
| ); |
| webview.focused_browsing_context_id = |
| parent_pipeline.browsing_context_id; |
| } |
| } else { |
| warn!( |
| "Browsing context {} contains a reference to \ |
| a non-existent top-level browsing context {}", |
| browsing_context_id, browsing_context.top_level_id |
| ); |
| } |
| }, |
| }; |
| } |
| debug!("{}: Closed", browsing_context_id); |
| Some(browsing_context) |
| } |
| |
| // Close the children of a browsing context |
| #[servo_tracing::instrument(skip_all)] |
| fn close_browsing_context_children( |
| &mut self, |
| browsing_context_id: BrowsingContextId, |
| dbc: DiscardBrowsingContext, |
| exit_mode: ExitPipelineMode, |
| ) { |
| debug!("{}: Closing browsing context children", browsing_context_id); |
| // Store information about the pipelines to be closed. Then close the |
| // pipelines, before removing ourself from the browsing_contexts hash map. This |
| // ordering is vital - so that if close_pipeline() ends up closing |
| // any child browsing contexts, they can be removed from the parent browsing context correctly. |
| let mut pipelines_to_close: Vec<PipelineId> = self |
| .pending_changes |
| .iter() |
| .filter(|change| change.browsing_context_id == browsing_context_id) |
| .map(|change| change.new_pipeline_id) |
| .collect(); |
| |
| if let Some(browsing_context) = self.browsing_contexts.get(&browsing_context_id) { |
| pipelines_to_close.extend(&browsing_context.pipelines) |
| } |
| |
| for pipeline_id in pipelines_to_close { |
| self.close_pipeline(pipeline_id, dbc, exit_mode); |
| } |
| |
| debug!("{}: Closed browsing context children", browsing_context_id); |
| } |
| |
| // Discard the pipeline for a given document, udpdate the joint session history. |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_discard_document(&mut self, webview_id: WebViewId, pipeline_id: PipelineId) { |
| match self.webviews.get_mut(webview_id) { |
| Some(webview) => { |
| let load_data = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline.load_data.clone(), |
| None => return warn!("{}: Discarding closed pipeline", pipeline_id), |
| }; |
| webview.session_history.replace_reloader( |
| NeedsToReload::No(pipeline_id), |
| NeedsToReload::Yes(pipeline_id, load_data), |
| ); |
| }, |
| None => { |
| return warn!( |
| "{}: Discarding after closure of {}", |
| pipeline_id, webview_id, |
| ); |
| }, |
| }; |
| self.close_pipeline( |
| pipeline_id, |
| DiscardBrowsingContext::No, |
| ExitPipelineMode::Normal, |
| ); |
| } |
| |
| // Send a message to script requesting the document associated with this pipeline runs the 'unload' algorithm. |
| #[servo_tracing::instrument(skip_all)] |
| fn unload_document(&self, pipeline_id: PipelineId) { |
| if let Some(pipeline) = self.pipelines.get(&pipeline_id) { |
| let msg = ScriptThreadMessage::UnloadDocument(pipeline_id); |
| let _ = pipeline.event_loop.send(msg); |
| } |
| } |
| |
| // Close all pipelines at and beneath a given browsing context |
| #[servo_tracing::instrument(skip_all)] |
| fn close_pipeline( |
| &mut self, |
| pipeline_id: PipelineId, |
| dbc: DiscardBrowsingContext, |
| exit_mode: ExitPipelineMode, |
| ) { |
| debug!("{}: Closing", pipeline_id); |
| |
| // Sever connection to browsing context |
| let browsing_context_id = self |
| .pipelines |
| .get(&pipeline_id) |
| .map(|pipeline| pipeline.browsing_context_id); |
| if let Some(browsing_context) = browsing_context_id |
| .and_then(|browsing_context_id| self.browsing_contexts.get_mut(&browsing_context_id)) |
| { |
| browsing_context.pipelines.remove(&pipeline_id); |
| } |
| |
| // Store information about the browsing contexts to be closed. Then close the |
| // browsing contexts, before removing ourself from the pipelines hash map. This |
| // ordering is vital - so that if close_browsing_context() ends up closing |
| // any child pipelines, they can be removed from the parent pipeline correctly. |
| let browsing_contexts_to_close = { |
| let mut browsing_contexts_to_close = vec![]; |
| |
| if let Some(pipeline) = self.pipelines.get(&pipeline_id) { |
| browsing_contexts_to_close.extend_from_slice(&pipeline.children); |
| } |
| |
| browsing_contexts_to_close |
| }; |
| |
| // Remove any child browsing contexts |
| for child_browsing_context in &browsing_contexts_to_close { |
| self.close_browsing_context(*child_browsing_context, exit_mode); |
| } |
| |
| // Note, we don't remove the pipeline now, we wait for the message to come back from |
| // the pipeline. |
| let pipeline = match self.pipelines.get(&pipeline_id) { |
| Some(pipeline) => pipeline, |
| None => return warn!("fn close_pipeline: {pipeline_id}: Closing twice"), |
| }; |
| |
| // Remove this pipeline from pending changes if it hasn't loaded yet. |
| let pending_index = self |
| .pending_changes |
| .iter() |
| .position(|change| change.new_pipeline_id == pipeline_id); |
| if let Some(pending_index) = pending_index { |
| self.pending_changes.remove(pending_index); |
| } |
| |
| // Inform script, compositor that this pipeline has exited. |
| pipeline.send_exit_message_to_script(dbc); |
| |
| debug!("{}: Closed", pipeline_id); |
| } |
| |
| // Randomly close a pipeline -if --random-pipeline-closure-probability is set |
| #[servo_tracing::instrument(skip_all)] |
| fn maybe_close_random_pipeline(&mut self) { |
| match self.random_pipeline_closure { |
| Some((ref mut rng, probability)) => { |
| if probability <= rng.r#gen::<f32>() { |
| return; |
| } |
| }, |
| _ => return, |
| }; |
| // In order to get repeatability, we sort the pipeline ids. |
| let mut pipeline_ids: Vec<&PipelineId> = self.pipelines.keys().collect(); |
| pipeline_ids.sort_unstable(); |
| if let Some((ref mut rng, probability)) = self.random_pipeline_closure { |
| if let Some(pipeline_id) = pipeline_ids.choose(rng) { |
| if let Some(pipeline) = self.pipelines.get(pipeline_id) { |
| if self |
| .pending_changes |
| .iter() |
| .any(|change| change.new_pipeline_id == pipeline.id) && |
| probability <= rng.r#gen::<f32>() |
| { |
| // We tend not to close pending pipelines, as that almost always |
| // results in pipelines being closed early in their lifecycle, |
| // and not stressing the constellation as much. |
| // https://github.com/servo/servo/issues/18852 |
| info!("{}: Not closing pending pipeline", pipeline_id); |
| } else { |
| // Note that we deliberately do not do any of the tidying up |
| // associated with closing a pipeline. The constellation should cope! |
| warn!("{}: Randomly closing pipeline", pipeline_id); |
| pipeline.send_exit_message_to_script(DiscardBrowsingContext::No); |
| } |
| } |
| } |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn get_joint_session_history(&mut self, top_level_id: WebViewId) -> &mut JointSessionHistory { |
| self.webviews |
| .get_mut(top_level_id) |
| .map(|webview| &mut webview.session_history) |
| .expect("Unknown top-level browsing context") |
| } |
| |
| // Convert a browsing context to a sendable form to pass to the compositor |
| #[servo_tracing::instrument(skip_all)] |
| fn browsing_context_to_sendable( |
| &self, |
| browsing_context_id: BrowsingContextId, |
| ) -> Option<SendableFrameTree> { |
| self.browsing_contexts |
| .get(&browsing_context_id) |
| .and_then(|browsing_context| { |
| self.pipelines |
| .get(&browsing_context.pipeline_id) |
| .map(|pipeline| { |
| let mut frame_tree = SendableFrameTree { |
| pipeline: pipeline.to_sendable(), |
| children: vec![], |
| }; |
| |
| for child_browsing_context_id in &pipeline.children { |
| if let Some(child) = |
| self.browsing_context_to_sendable(*child_browsing_context_id) |
| { |
| frame_tree.children.push(child); |
| } |
| } |
| |
| frame_tree |
| }) |
| }) |
| } |
| |
| /// Send the frame tree for the given webview to the compositor. |
| #[servo_tracing::instrument(skip_all)] |
| fn update_webview_in_compositor(&mut self, webview_id: WebViewId) { |
| // Note that this function can panic, due to ipc-channel creation failure. |
| // avoiding this panic would require a mechanism for dealing |
| // with low-resource scenarios. |
| let browsing_context_id = BrowsingContextId::from(webview_id); |
| if let Some(frame_tree) = self.browsing_context_to_sendable(browsing_context_id) { |
| debug!("{}: Sending frame tree", browsing_context_id); |
| self.compositor_proxy |
| .send(CompositorMsg::CreateOrUpdateWebView(frame_tree)); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_media_session_action_msg(&mut self, action: MediaSessionActionType) { |
| if let Some(media_session_pipeline_id) = self.active_media_session { |
| let result = match self.pipelines.get(&media_session_pipeline_id) { |
| None => { |
| return warn!( |
| "{}: Got media session action request after closure", |
| media_session_pipeline_id, |
| ); |
| }, |
| Some(pipeline) => { |
| let msg = |
| ScriptThreadMessage::MediaSessionAction(media_session_pipeline_id, action); |
| pipeline.event_loop.send(msg) |
| }, |
| }; |
| if let Err(e) = result { |
| self.handle_send_error(media_session_pipeline_id, e); |
| } |
| } else { |
| error!("Got a media session action but no active media session is registered"); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_set_scroll_states( |
| &self, |
| pipeline_id: PipelineId, |
| scroll_states: FnvHashMap<ExternalScrollId, LayoutVector2D>, |
| ) { |
| let Some(pipeline) = self.pipelines.get(&pipeline_id) else { |
| warn!("Discarding scroll offset update for unknown pipeline"); |
| return; |
| }; |
| if let Err(error) = pipeline |
| .event_loop |
| .send(ScriptThreadMessage::SetScrollStates( |
| pipeline_id, |
| scroll_states, |
| )) |
| { |
| warn!("Could not send scroll offsets to pipeline: {pipeline_id:?}: {error:?}"); |
| } |
| } |
| |
| #[servo_tracing::instrument(skip_all)] |
| fn handle_paint_metric(&mut self, pipeline_id: PipelineId, event: PaintMetricEvent) { |
| let Some(pipeline) = self.pipelines.get(&pipeline_id) else { |
| warn!("Discarding paint metric event for unknown pipeline"); |
| return; |
| }; |
| let (metric_type, metric_value, first_reflow) = match event { |
| PaintMetricEvent::FirstPaint(metric_value, first_reflow) => ( |
| ProgressiveWebMetricType::FirstPaint, |
| metric_value, |
| first_reflow, |
| ), |
| PaintMetricEvent::FirstContentfulPaint(metric_value, first_reflow) => ( |
| ProgressiveWebMetricType::FirstContentfulPaint, |
| metric_value, |
| first_reflow, |
| ), |
| }; |
| if let Err(error) = pipeline.event_loop.send(ScriptThreadMessage::PaintMetric( |
| pipeline_id, |
| metric_type, |
| metric_value, |
| first_reflow, |
| )) { |
| warn!("Could not sent paint metric event to pipeline: {pipeline_id:?}: {error:?}"); |
| } |
| } |
| |
| fn create_canvas_paint_thread(&self) -> (Sender<ConstellationCanvasMsg>, IpcSender<CanvasMsg>) { |
| CanvasPaintThread::start(self.compositor_proxy.cross_process_compositor_api.clone()) |
| } |
| } |