mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Flatten and simplify Servo's preferences code. In addition, have both preferences and options passed in as arguments to `Servo::new()` and make sure not to use the globally set preferences in `servoshell` (as much as possible now). Instead of a complex procedural macro to generate preferences, just expose a very simple derive macro that adds string based getters and setters. - All command-line parsing is moved to servoshell. - There is no longer the concept of a missing preference. - Preferences no longer have to be part of the resources bundle because they now have reasonable default values. - servoshell specific preferences are no longer part of the preferences exposed by the Servo API. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
521 lines
18 KiB
Rust
521 lines
18 KiB
Rust
/* 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 service worker manager persists the descriptor of any registered service workers.
|
|
//! It also stores an active workers map, which holds descriptors of running service workers.
|
|
//! If an active service worker timeouts, then it removes the descriptor entry from its
|
|
//! active_workers map
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::Arc;
|
|
use std::thread::{self, JoinHandle};
|
|
|
|
use base::id::{PipelineNamespace, ServiceWorkerId, ServiceWorkerRegistrationId};
|
|
use crossbeam_channel::{select, unbounded, Receiver, RecvError, Sender};
|
|
use ipc_channel::ipc::{self, IpcSender};
|
|
use ipc_channel::router::ROUTER;
|
|
use net_traits::{CoreResourceMsg, CustomResponseMediator};
|
|
use script_traits::{
|
|
DOMMessage, Job, JobError, JobResult, JobResultValue, JobType, SWManagerMsg, SWManagerSenders,
|
|
ScopeThings, ServiceWorkerManagerFactory, ServiceWorkerMsg,
|
|
};
|
|
use servo_config::pref;
|
|
use servo_url::{ImmutableOrigin, ServoUrl};
|
|
|
|
use crate::dom::abstractworker::WorkerScriptMsg;
|
|
use crate::dom::serviceworkerglobalscope::{
|
|
ServiceWorkerControlMsg, ServiceWorkerGlobalScope, ServiceWorkerScriptMsg,
|
|
};
|
|
use crate::dom::serviceworkerregistration::longest_prefix_match;
|
|
use crate::script_runtime::ThreadSafeJSContext;
|
|
|
|
enum Message {
|
|
FromResource(CustomResponseMediator),
|
|
FromConstellation(Box<ServiceWorkerMsg>),
|
|
}
|
|
|
|
/// <https://w3c.github.io/ServiceWorker/#dfn-service-worker>
|
|
#[derive(Clone)]
|
|
pub(crate) struct ServiceWorker {
|
|
/// A unique identifer.
|
|
pub(crate) id: ServiceWorkerId,
|
|
/// <https://w3c.github.io/ServiceWorker/#dfn-script-url>
|
|
pub(crate) script_url: ServoUrl,
|
|
/// A sender to the running service worker scope.
|
|
pub(crate) sender: Sender<ServiceWorkerScriptMsg>,
|
|
}
|
|
|
|
impl ServiceWorker {
|
|
fn new(
|
|
script_url: ServoUrl,
|
|
sender: Sender<ServiceWorkerScriptMsg>,
|
|
id: ServiceWorkerId,
|
|
) -> ServiceWorker {
|
|
ServiceWorker {
|
|
id,
|
|
script_url,
|
|
sender,
|
|
}
|
|
}
|
|
|
|
/// Forward a DOM message to the running service worker scope.
|
|
fn forward_dom_message(&self, msg: DOMMessage) {
|
|
let DOMMessage { origin, data } = msg;
|
|
let _ = self.sender.send(ServiceWorkerScriptMsg::CommonWorker(
|
|
WorkerScriptMsg::DOMMessage { origin, data },
|
|
));
|
|
}
|
|
|
|
/// Send a message to the running service worker scope.
|
|
fn send_message(&self, msg: ServiceWorkerScriptMsg) {
|
|
let _ = self.sender.send(msg);
|
|
}
|
|
}
|
|
|
|
/// When updating a registration, which worker are we targetting?
|
|
#[allow(dead_code)]
|
|
enum RegistrationUpdateTarget {
|
|
Installing,
|
|
Waiting,
|
|
Active,
|
|
}
|
|
|
|
impl Drop for ServiceWorkerRegistration {
|
|
/// <https://html.spec.whatwg.org/multipage/#terminate-a-worker>
|
|
fn drop(&mut self) {
|
|
// Drop the channel to signal shutdown.
|
|
if self
|
|
.control_sender
|
|
.take()
|
|
.expect("No control sender to worker thread.")
|
|
.send(ServiceWorkerControlMsg::Exit)
|
|
.is_err()
|
|
{
|
|
warn!("Failed to send exit message to service worker scope.");
|
|
}
|
|
|
|
self.closing
|
|
.take()
|
|
.expect("No close flag for worker")
|
|
.store(true, Ordering::SeqCst);
|
|
self.context
|
|
.take()
|
|
.expect("No context to request interrupt.")
|
|
.request_interrupt_callback();
|
|
|
|
// TODO: Step 1, 2 and 3.
|
|
if self
|
|
.join_handle
|
|
.take()
|
|
.expect("No handle to join on worker.")
|
|
.join()
|
|
.is_err()
|
|
{
|
|
warn!("Failed to join on service worker thread.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <https://w3c.github.io/ServiceWorker/#service-worker-registration-concept>
|
|
struct ServiceWorkerRegistration {
|
|
/// A unique identifer.
|
|
id: ServiceWorkerRegistrationId,
|
|
/// <https://w3c.github.io/ServiceWorker/#dfn-active-worker>
|
|
active_worker: Option<ServiceWorker>,
|
|
/// <https://w3c.github.io/ServiceWorker/#dfn-waiting-worker>
|
|
waiting_worker: Option<ServiceWorker>,
|
|
/// <https://w3c.github.io/ServiceWorker/#dfn-installing-worker>
|
|
installing_worker: Option<ServiceWorker>,
|
|
/// A channel to send control message to the worker,
|
|
/// currently only used to signal shutdown.
|
|
control_sender: Option<Sender<ServiceWorkerControlMsg>>,
|
|
/// A handle to join on the worker thread.
|
|
join_handle: Option<JoinHandle<()>>,
|
|
/// A context to request an interrupt.
|
|
context: Option<ThreadSafeJSContext>,
|
|
/// The closing flag for the worker.
|
|
closing: Option<Arc<AtomicBool>>,
|
|
}
|
|
|
|
impl ServiceWorkerRegistration {
|
|
pub(crate) fn new() -> ServiceWorkerRegistration {
|
|
ServiceWorkerRegistration {
|
|
id: ServiceWorkerRegistrationId::new(),
|
|
active_worker: None,
|
|
waiting_worker: None,
|
|
installing_worker: None,
|
|
join_handle: None,
|
|
control_sender: None,
|
|
context: None,
|
|
closing: None,
|
|
}
|
|
}
|
|
|
|
fn note_worker_thread(
|
|
&mut self,
|
|
join_handle: JoinHandle<()>,
|
|
control_sender: Sender<ServiceWorkerControlMsg>,
|
|
context: ThreadSafeJSContext,
|
|
closing: Arc<AtomicBool>,
|
|
) {
|
|
assert!(self.join_handle.is_none());
|
|
self.join_handle = Some(join_handle);
|
|
|
|
assert!(self.control_sender.is_none());
|
|
self.control_sender = Some(control_sender);
|
|
|
|
assert!(self.context.is_none());
|
|
self.context = Some(context);
|
|
|
|
assert!(self.closing.is_none());
|
|
self.closing = Some(closing);
|
|
}
|
|
|
|
/// <https://w3c.github.io/ServiceWorker/#get-newest-worker>
|
|
fn get_newest_worker(&self) -> Option<ServiceWorker> {
|
|
if let Some(worker) = self.active_worker.as_ref() {
|
|
return Some(worker.clone());
|
|
}
|
|
if let Some(worker) = self.waiting_worker.as_ref() {
|
|
return Some(worker.clone());
|
|
}
|
|
if let Some(worker) = self.installing_worker.as_ref() {
|
|
return Some(worker.clone());
|
|
}
|
|
None
|
|
}
|
|
|
|
/// <https://w3c.github.io/ServiceWorker/#update-registration-state>
|
|
fn update_registration_state(
|
|
&mut self,
|
|
target: RegistrationUpdateTarget,
|
|
worker: ServiceWorker,
|
|
) {
|
|
match target {
|
|
RegistrationUpdateTarget::Active => {
|
|
self.active_worker = Some(worker);
|
|
},
|
|
RegistrationUpdateTarget::Waiting => {
|
|
self.waiting_worker = Some(worker);
|
|
},
|
|
RegistrationUpdateTarget::Installing => {
|
|
self.installing_worker = Some(worker);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A structure managing all registrations and workers for a given origin.
|
|
pub struct ServiceWorkerManager {
|
|
/// <https://w3c.github.io/ServiceWorker/#dfn-scope-to-registration-map>
|
|
registrations: HashMap<ServoUrl, ServiceWorkerRegistration>,
|
|
// Will be useful to implement posting a message to a client.
|
|
// See https://github.com/servo/servo/issues/24660
|
|
_constellation_sender: IpcSender<SWManagerMsg>,
|
|
// own sender to send messages here
|
|
own_sender: IpcSender<ServiceWorkerMsg>,
|
|
// receiver to receive messages from constellation
|
|
own_port: Receiver<ServiceWorkerMsg>,
|
|
// to receive resource messages
|
|
resource_receiver: Receiver<CustomResponseMediator>,
|
|
}
|
|
|
|
impl ServiceWorkerManager {
|
|
fn new(
|
|
own_sender: IpcSender<ServiceWorkerMsg>,
|
|
from_constellation_receiver: Receiver<ServiceWorkerMsg>,
|
|
resource_port: Receiver<CustomResponseMediator>,
|
|
constellation_sender: IpcSender<SWManagerMsg>,
|
|
) -> ServiceWorkerManager {
|
|
// Install a pipeline-namespace in the current thread.
|
|
PipelineNamespace::auto_install();
|
|
|
|
ServiceWorkerManager {
|
|
registrations: HashMap::new(),
|
|
own_sender,
|
|
own_port: from_constellation_receiver,
|
|
resource_receiver: resource_port,
|
|
_constellation_sender: constellation_sender,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn get_matching_scope(&self, load_url: &ServoUrl) -> Option<ServoUrl> {
|
|
for scope in self.registrations.keys() {
|
|
if longest_prefix_match(scope, load_url) {
|
|
return Some(scope.clone());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn handle_message(&mut self) {
|
|
while let Ok(message) = self.receive_message() {
|
|
let should_continue = match message {
|
|
Message::FromConstellation(msg) => self.handle_message_from_constellation(*msg),
|
|
Message::FromResource(msg) => self.handle_message_from_resource(msg),
|
|
};
|
|
if !should_continue {
|
|
for registration in self.registrations.drain() {
|
|
// Signal shut-down, and join on the thread.
|
|
drop(registration);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_message_from_resource(&mut self, mediator: CustomResponseMediator) -> bool {
|
|
if serviceworker_enabled() {
|
|
if let Some(scope) = self.get_matching_scope(&mediator.load_url) {
|
|
if let Some(registration) = self.registrations.get(&scope) {
|
|
if let Some(ref worker) = registration.active_worker {
|
|
worker.send_message(ServiceWorkerScriptMsg::Response(mediator));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let _ = mediator.response_chan.send(None);
|
|
true
|
|
}
|
|
|
|
fn receive_message(&mut self) -> Result<Message, RecvError> {
|
|
select! {
|
|
recv(self.own_port) -> msg => msg.map(|m| Message::FromConstellation(Box::new(m))),
|
|
recv(self.resource_receiver) -> msg => msg.map(Message::FromResource),
|
|
}
|
|
}
|
|
|
|
fn handle_message_from_constellation(&mut self, msg: ServiceWorkerMsg) -> bool {
|
|
match msg {
|
|
ServiceWorkerMsg::Timeout(_scope) => {
|
|
// TODO: https://w3c.github.io/ServiceWorker/#terminate-service-worker
|
|
},
|
|
ServiceWorkerMsg::ForwardDOMMessage(msg, scope_url) => {
|
|
if let Some(registration) = self.registrations.get_mut(&scope_url) {
|
|
if let Some(ref worker) = registration.active_worker {
|
|
worker.forward_dom_message(msg);
|
|
}
|
|
}
|
|
},
|
|
ServiceWorkerMsg::ScheduleJob(job) => match job.job_type {
|
|
JobType::Register => {
|
|
self.handle_register_job(job);
|
|
},
|
|
JobType::Update => {
|
|
self.handle_update_job(job);
|
|
},
|
|
JobType::Unregister => {
|
|
// TODO: https://w3c.github.io/ServiceWorker/#unregister-algorithm
|
|
},
|
|
},
|
|
ServiceWorkerMsg::Exit => return false,
|
|
}
|
|
true
|
|
}
|
|
|
|
/// <https://w3c.github.io/ServiceWorker/#register-algorithm>
|
|
fn handle_register_job(&mut self, mut job: Job) {
|
|
if !job.script_url.is_origin_trustworthy() {
|
|
// Step 1.1
|
|
let _ = job
|
|
.client
|
|
.send(JobResult::RejectPromise(JobError::SecurityError));
|
|
return;
|
|
}
|
|
|
|
if job.script_url.origin() != job.referrer.origin() ||
|
|
job.scope_url.origin() != job.referrer.origin()
|
|
{
|
|
// Step 2.1
|
|
let _ = job
|
|
.client
|
|
.send(JobResult::RejectPromise(JobError::SecurityError));
|
|
return;
|
|
}
|
|
|
|
// Step 4: Get registration.
|
|
if let Some(registration) = self.registrations.get(&job.scope_url) {
|
|
// Step 5, we have a registation.
|
|
|
|
// Step 5.1, get newest worker
|
|
let newest_worker = registration.get_newest_worker();
|
|
|
|
// step 5.2
|
|
if newest_worker.is_some() {
|
|
// TODO: the various checks of job versus worker.
|
|
|
|
// Step 2.1: Run resolve job.
|
|
let client = job.client.clone();
|
|
let _ = client.send(JobResult::ResolvePromise(
|
|
job,
|
|
JobResultValue::Registration {
|
|
id: registration.id,
|
|
installing_worker: registration
|
|
.installing_worker
|
|
.as_ref()
|
|
.map(|worker| worker.id),
|
|
waiting_worker: registration
|
|
.waiting_worker
|
|
.as_ref()
|
|
.map(|worker| worker.id),
|
|
active_worker: registration.active_worker.as_ref().map(|worker| worker.id),
|
|
},
|
|
));
|
|
}
|
|
} else {
|
|
// Step 6: we do not have a registration.
|
|
|
|
// Step 6.1: Run Set Registration.
|
|
let new_registration = ServiceWorkerRegistration::new();
|
|
self.registrations
|
|
.insert(job.scope_url.clone(), new_registration);
|
|
|
|
// Step 7: Schedule update
|
|
job.job_type = JobType::Update;
|
|
let _ = self.own_sender.send(ServiceWorkerMsg::ScheduleJob(job));
|
|
}
|
|
}
|
|
|
|
/// <https://w3c.github.io/ServiceWorker/#update>
|
|
fn handle_update_job(&mut self, job: Job) {
|
|
// Step 1: Get registation
|
|
if let Some(registration) = self.registrations.get_mut(&job.scope_url) {
|
|
// Step 3.
|
|
let newest_worker = registration.get_newest_worker();
|
|
|
|
// Step 4.
|
|
if let Some(worker) = newest_worker {
|
|
if worker.script_url != job.script_url {
|
|
let _ = job
|
|
.client
|
|
.send(JobResult::RejectPromise(JobError::TypeError));
|
|
return;
|
|
}
|
|
}
|
|
|
|
let scope_things = job
|
|
.scope_things
|
|
.clone()
|
|
.expect("Update job should have scope things.");
|
|
|
|
// Very roughly steps 5 to 18.
|
|
// TODO: implement all steps precisely.
|
|
let (new_worker, join_handle, control_sender, context, closing) =
|
|
update_serviceworker(self.own_sender.clone(), job.scope_url.clone(), scope_things);
|
|
|
|
// Since we've just started the worker thread, ensure we can shut it down later.
|
|
registration.note_worker_thread(join_handle, control_sender, context, closing);
|
|
|
|
// Step 19, run Install.
|
|
|
|
// Install: Step 4, run Update Registration State.
|
|
registration
|
|
.update_registration_state(RegistrationUpdateTarget::Installing, new_worker);
|
|
|
|
// Install: Step 7, run Resolve Job Promise.
|
|
let client = job.client.clone();
|
|
let _ = client.send(JobResult::ResolvePromise(
|
|
job,
|
|
JobResultValue::Registration {
|
|
id: registration.id,
|
|
installing_worker: registration
|
|
.installing_worker
|
|
.as_ref()
|
|
.map(|worker| worker.id),
|
|
waiting_worker: registration.waiting_worker.as_ref().map(|worker| worker.id),
|
|
active_worker: registration.active_worker.as_ref().map(|worker| worker.id),
|
|
},
|
|
));
|
|
} else {
|
|
// Step 2
|
|
let _ = job
|
|
.client
|
|
.send(JobResult::RejectPromise(JobError::TypeError));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <https://w3c.github.io/ServiceWorker/#update-algorithm>
|
|
fn update_serviceworker(
|
|
own_sender: IpcSender<ServiceWorkerMsg>,
|
|
scope_url: ServoUrl,
|
|
scope_things: ScopeThings,
|
|
) -> (
|
|
ServiceWorker,
|
|
JoinHandle<()>,
|
|
Sender<ServiceWorkerControlMsg>,
|
|
ThreadSafeJSContext,
|
|
Arc<AtomicBool>,
|
|
) {
|
|
let (sender, receiver) = unbounded();
|
|
let (_devtools_sender, devtools_receiver) = ipc::channel().unwrap();
|
|
let worker_id = ServiceWorkerId::new();
|
|
|
|
let (control_sender, control_receiver) = unbounded();
|
|
let (context_sender, context_receiver) = unbounded();
|
|
let closing = Arc::new(AtomicBool::new(false));
|
|
|
|
let join_handle = ServiceWorkerGlobalScope::run_serviceworker_scope(
|
|
scope_things.clone(),
|
|
sender.clone(),
|
|
receiver,
|
|
devtools_receiver,
|
|
own_sender,
|
|
scope_url.clone(),
|
|
control_receiver,
|
|
context_sender,
|
|
closing.clone(),
|
|
);
|
|
|
|
let context = context_receiver
|
|
.recv()
|
|
.expect("Couldn't receive a context for worker.");
|
|
|
|
(
|
|
ServiceWorker::new(scope_things.script_url, sender, worker_id),
|
|
join_handle,
|
|
control_sender,
|
|
context,
|
|
closing,
|
|
)
|
|
}
|
|
|
|
impl ServiceWorkerManagerFactory for ServiceWorkerManager {
|
|
fn create(sw_senders: SWManagerSenders, origin: ImmutableOrigin) {
|
|
let (resource_chan, resource_port) = ipc::channel().unwrap();
|
|
|
|
let SWManagerSenders {
|
|
resource_sender,
|
|
own_sender,
|
|
receiver,
|
|
swmanager_sender: constellation_sender,
|
|
} = sw_senders;
|
|
|
|
let from_constellation = ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(receiver);
|
|
let resource_port = ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(resource_port);
|
|
let _ = resource_sender.send(CoreResourceMsg::NetworkMediator(resource_chan, origin));
|
|
let swmanager_thread = move || {
|
|
ServiceWorkerManager::new(
|
|
own_sender,
|
|
from_constellation,
|
|
resource_port,
|
|
constellation_sender,
|
|
)
|
|
.handle_message()
|
|
};
|
|
if thread::Builder::new()
|
|
.name("SvcWorkerManager".to_owned())
|
|
.spawn(swmanager_thread)
|
|
.is_err()
|
|
{
|
|
warn!("ServiceWorkerManager thread spawning failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn serviceworker_enabled() -> bool {
|
|
pref!(dom_serviceworker_enabled)
|
|
}
|