aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/serviceworker_manager.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/serviceworker_manager.rs')
-rw-r--r--components/script/serviceworker_manager.rs573
1 files changed, 447 insertions, 126 deletions
diff --git a/components/script/serviceworker_manager.rs b/components/script/serviceworker_manager.rs
index 793d7ccf370..46cf22531c8 100644
--- a/components/script/serviceworker_manager.rs
+++ b/components/script/serviceworker_manager.rs
@@ -1,74 +1,248 @@
/* 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 http://mozilla.org/MPL/2.0/. */
+ * 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 devtools_traits::{DevtoolsPageInfo, ScriptToDevtoolsControlMsg};
-use dom::abstractworker::WorkerScriptMsg;
-use dom::bindings::structuredclone::StructuredCloneData;
-use dom::serviceworkerglobalscope::{ServiceWorkerGlobalScope, ServiceWorkerScriptMsg};
-use dom::serviceworkerregistration::longest_prefix_match;
+use crate::dom::abstractworker::WorkerScriptMsg;
+use crate::dom::serviceworkerglobalscope::{
+ ServiceWorkerControlMsg, ServiceWorkerGlobalScope, ServiceWorkerScriptMsg,
+};
+use crate::dom::serviceworkerregistration::longest_prefix_match;
+use crate::script_runtime::ContextForRequestInterrupt;
+use crossbeam_channel::{unbounded, Receiver, RecvError, Sender};
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
-use net_traits::{CustomResponseMediator, CoreResourceMsg};
-use script_traits::{ServiceWorkerMsg, ScopeThings, SWManagerMsg, SWManagerSenders, DOMMessage};
-use servo_config::prefs::PREFS;
+use msg::constellation_msg::PipelineNamespace;
+use msg::constellation_msg::{ServiceWorkerId, ServiceWorkerRegistrationId};
+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;
use servo_url::ServoUrl;
use std::collections::HashMap;
-use std::sync::mpsc::{channel, Sender, Receiver, RecvError};
-use std::thread;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+use std::thread::{self, JoinHandle};
enum Message {
FromResource(CustomResponseMediator),
- FromConstellation(ServiceWorkerMsg)
+ FromConstellation(ServiceWorkerMsg),
}
+/// <https://w3c.github.io/ServiceWorker/#dfn-service-worker>
+#[derive(Clone)]
+struct ServiceWorker {
+ /// A unique identifer.
+ pub id: ServiceWorkerId,
+ /// <https://w3c.github.io/ServiceWorker/#dfn-script-url>
+ pub script_url: ServoUrl,
+ /// A sender to the running service worker scope.
+ pub 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();
+
+ // 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<ContextForRequestInterrupt>,
+ /// The closing flag for the worker.
+ closing: Option<Arc<AtomicBool>>,
+}
+
+impl ServiceWorkerRegistration {
+ pub 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: ContextForRequestInterrupt,
+ 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 {
- // map of registered service worker descriptors
- registered_workers: HashMap<ServoUrl, ScopeThings>,
- // map of active service worker descriptors
- active_workers: HashMap<ServoUrl, Sender<ServiceWorkerScriptMsg>>,
+ /// 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>
+ resource_receiver: Receiver<CustomResponseMediator>,
}
impl ServiceWorkerManager {
- fn new(own_sender: IpcSender<ServiceWorkerMsg>,
- from_constellation_receiver: Receiver<ServiceWorkerMsg>,
- resource_port: Receiver<CustomResponseMediator>) -> 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 {
- registered_workers: HashMap::new(),
- active_workers: HashMap::new(),
+ registrations: HashMap::new(),
own_sender: own_sender,
own_port: from_constellation_receiver,
- resource_receiver: resource_port
+ resource_receiver: resource_port,
+ _constellation_sender: constellation_sender,
}
}
- pub fn spawn_manager(sw_senders: SWManagerSenders) {
- let (own_sender, from_constellation_receiver) = ipc::channel().unwrap();
- let (resource_chan, resource_port) = ipc::channel().unwrap();
- let from_constellation = ROUTER.route_ipc_receiver_to_new_mpsc_receiver(from_constellation_receiver);
- let resource_port = ROUTER.route_ipc_receiver_to_new_mpsc_receiver(resource_port);
- let _ = sw_senders.resource_sender.send(CoreResourceMsg::NetworkMediator(resource_chan));
- let _ = sw_senders.swmanager_sender.send(SWManagerMsg::OwnSender(own_sender.clone()));
- thread::Builder::new().name("ServiceWorkerManager".to_owned()).spawn(move || {
- ServiceWorkerManager::new(own_sender,
- from_constellation,
- resource_port).handle_message();
- }).expect("Thread spawning failed");
- }
-
pub fn get_matching_scope(&self, load_url: &ServoUrl) -> Option<ServoUrl> {
- for scope in self.registered_workers.keys() {
+ for scope in self.registrations.keys() {
if longest_prefix_match(&scope, load_url) {
return Some(scope.clone());
}
@@ -76,125 +250,272 @@ impl ServiceWorkerManager {
None
}
- pub fn wakeup_serviceworker(&mut self, scope_url: ServoUrl) -> Option<Sender<ServiceWorkerScriptMsg>> {
- let scope_things = self.registered_workers.get(&scope_url);
- if let Some(scope_things) = scope_things {
- let (sender, receiver) = channel();
- let (devtools_sender, devtools_receiver) = ipc::channel().unwrap();
- if let Some(ref chan) = scope_things.devtools_chan {
- let title = format!("ServiceWorker for {}", scope_things.script_url);
- let page_info = DevtoolsPageInfo {
- title: title,
- url: scope_things.script_url.clone(),
- };
- let _ = chan.send(ScriptToDevtoolsControlMsg::NewGlobal((scope_things.init.pipeline_id,
- Some(scope_things.worker_id)),
- devtools_sender,
- page_info));
- };
- ServiceWorkerGlobalScope::run_serviceworker_scope(scope_things.clone(),
- sender.clone(),
- receiver,
- devtools_receiver,
- self.own_sender.clone(),
- scope_url.clone());
- // We store the activated worker
- self.active_workers.insert(scope_url, sender.clone());
- return Some(sender);
- } else {
- warn!("Unable to activate service worker");
- 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)
- }
+ 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 forward_message(&self, msg: DOMMessage, sender: &Sender<ServiceWorkerScriptMsg>) {
- let DOMMessage(data) = msg;
- let data = StructuredCloneData::Vector(data);
- let _ = sender.send(ServiceWorkerScriptMsg::CommonWorker(WorkerScriptMsg::DOMMessage(data)));
+ 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(Message::FromConstellation),
+ recv(self.resource_receiver) -> msg => msg.map(Message::FromResource),
+ }
}
fn handle_message_from_constellation(&mut self, msg: ServiceWorkerMsg) -> bool {
match msg {
- ServiceWorkerMsg::RegisterServiceWorker(scope_things, scope) => {
- if self.registered_workers.contains_key(&scope) {
- warn!("ScopeThings for {:?} already stored in SW-Manager", scope);
- } else {
- self.registered_workers.insert(scope, scope_things);
- }
- true
- }
- ServiceWorkerMsg::Timeout(scope) => {
- if self.active_workers.contains_key(&scope) {
- let _ = self.active_workers.remove(&scope);
- } else {
- warn!("ServiceWorker for {:?} is not active", scope);
- }
- true
+ ServiceWorkerMsg::Timeout(_scope) => {
+ // TODO: https://w3c.github.io/ServiceWorker/#terminate-service-worker
},
ServiceWorkerMsg::ForwardDOMMessage(msg, scope_url) => {
- if self.active_workers.contains_key(&scope_url) {
- if let Some(ref sender) = self.active_workers.get(&scope_url) {
- self.forward_message(msg, &sender);
- }
- } else {
- if let Some(ref sender) = self.wakeup_serviceworker(scope_url) {
- self.forward_message(msg, &sender);
+ if let Some(registration) = self.registrations.get_mut(&scope_url) {
+ if let Some(ref worker) = registration.active_worker {
+ worker.forward_dom_message(msg);
}
}
- true
+ },
+ 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),
+ },
+ ));
+ return;
}
- ServiceWorkerMsg::Exit => false
+ } 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));
}
}
- 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 self.active_workers.contains_key(&scope) {
- if let Some(sender) = self.active_workers.get(&scope) {
- let _ = sender.send(ServiceWorkerScriptMsg::Response(mediator));
- }
- } else {
- if let Some(sender) = self.wakeup_serviceworker(scope) {
- let _ = sender.send(ServiceWorkerScriptMsg::Response(mediator));
- }
+ /// <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;
}
- } else {
- let _ = mediator.response_chan.send(None);
}
+
+ 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 {
- let _ = mediator.response_chan.send(None);
+ // Step 2
+ let _ = job
+ .client
+ .send(JobResult::RejectPromise(JobError::TypeError));
}
- true
}
+}
- #[allow(unsafe_code)]
- fn receive_message(&mut self) -> Result<Message, RecvError> {
- let msg_from_constellation = &self.own_port;
- let msg_from_resource = &self.resource_receiver;
- select! {
- msg = msg_from_constellation.recv() => msg.map(Message::FromConstellation),
- msg = msg_from_resource.recv() => msg.map(Message::FromResource)
+/// <https://w3c.github.io/ServiceWorker/#update-algorithm>
+fn update_serviceworker(
+ own_sender: IpcSender<ServiceWorkerMsg>,
+ scope_url: ServoUrl,
+ scope_things: ScopeThings,
+) -> (
+ ServiceWorker,
+ JoinHandle<()>,
+ Sender<ServiceWorkerControlMsg>,
+ ContextForRequestInterrupt,
+ 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));
+ if thread::Builder::new()
+ .name("ServiceWorkerManager".to_owned())
+ .spawn(move || {
+ ServiceWorkerManager::new(
+ own_sender,
+ from_constellation,
+ resource_port,
+ constellation_sender,
+ )
+ .handle_message();
+ })
+ .is_err()
+ {
+ warn!("ServiceWorkerManager thread spawning failed");
}
}
}
pub fn serviceworker_enabled() -> bool {
- PREFS.get("dom.serviceworker.enabled").as_boolean().unwrap_or(false)
+ pref!(dom.serviceworker.enabled)
}