script/
serviceworker_manager.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The service worker manager persists the descriptor of any registered service workers.
6//! It also stores an active workers map, which holds descriptors of running service workers.
7//! If an active service worker timeouts, then it removes the descriptor entry from its
8//! active_workers map
9
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::thread::{self, JoinHandle};
14
15use base::generic_channel::{self, GenericSender, ReceiveError, RoutedReceiver};
16use base::id::{PipelineNamespace, ServiceWorkerId, ServiceWorkerRegistrationId};
17use constellation_traits::{
18    DOMMessage, Job, JobError, JobResult, JobResultValue, JobType, SWManagerMsg, SWManagerSenders,
19    ScopeThings, ServiceWorkerManagerFactory, ServiceWorkerMsg,
20};
21use crossbeam_channel::{Receiver, Sender, select, unbounded};
22use fonts::FontContext;
23use ipc_channel::ipc;
24use ipc_channel::router::ROUTER;
25use net_traits::{CoreResourceMsg, CustomResponseMediator};
26use servo_config::pref;
27use servo_url::{ImmutableOrigin, ServoUrl};
28
29use crate::dom::abstractworker::WorkerScriptMsg;
30use crate::dom::serviceworkerglobalscope::{
31    ServiceWorkerControlMsg, ServiceWorkerGlobalScope, ServiceWorkerScriptMsg,
32};
33use crate::dom::serviceworkerregistration::longest_prefix_match;
34use crate::script_runtime::ThreadSafeJSContext;
35
36enum Message {
37    FromResource(CustomResponseMediator),
38    FromConstellation(Box<ServiceWorkerMsg>),
39}
40
41/// <https://w3c.github.io/ServiceWorker/#dfn-service-worker>
42#[derive(Clone)]
43pub(crate) struct ServiceWorker {
44    /// A unique identifer.
45    pub(crate) id: ServiceWorkerId,
46    /// <https://w3c.github.io/ServiceWorker/#dfn-script-url>
47    pub(crate) script_url: ServoUrl,
48    /// A sender to the running service worker scope.
49    pub(crate) sender: Sender<ServiceWorkerScriptMsg>,
50}
51
52impl ServiceWorker {
53    fn new(
54        script_url: ServoUrl,
55        sender: Sender<ServiceWorkerScriptMsg>,
56        id: ServiceWorkerId,
57    ) -> ServiceWorker {
58        ServiceWorker {
59            id,
60            script_url,
61            sender,
62        }
63    }
64
65    /// Forward a DOM message to the running service worker scope.
66    fn forward_dom_message(&self, msg: DOMMessage) {
67        let DOMMessage { origin, data } = msg;
68        let _ = self.sender.send(ServiceWorkerScriptMsg::CommonWorker(
69            WorkerScriptMsg::DOMMessage {
70                origin,
71                data: Box::new(data),
72            },
73        ));
74    }
75
76    /// Send a message to the running service worker scope.
77    fn send_message(&self, msg: ServiceWorkerScriptMsg) {
78        let _ = self.sender.send(msg);
79    }
80}
81
82/// When updating a registration, which worker are we targetting?
83#[allow(dead_code)]
84enum RegistrationUpdateTarget {
85    Installing,
86    Waiting,
87    Active,
88}
89
90impl Drop for ServiceWorkerRegistration {
91    /// <https://html.spec.whatwg.org/multipage/#terminate-a-worker>
92    fn drop(&mut self) {
93        // Drop the channel to signal shutdown.
94        if self
95            .control_sender
96            .take()
97            .expect("No control sender to worker thread.")
98            .send(ServiceWorkerControlMsg::Exit)
99            .is_err()
100        {
101            warn!("Failed to send exit message to service worker scope.");
102        }
103
104        self.closing
105            .take()
106            .expect("No close flag for worker")
107            .store(true, Ordering::SeqCst);
108        self.context
109            .take()
110            .expect("No context to request interrupt.")
111            .request_interrupt_callback();
112
113        // TODO: Step 1, 2 and 3.
114        if self
115            .join_handle
116            .take()
117            .expect("No handle to join on worker.")
118            .join()
119            .is_err()
120        {
121            warn!("Failed to join on service worker thread.");
122        }
123    }
124}
125
126/// <https://w3c.github.io/ServiceWorker/#service-worker-registration-concept>
127struct ServiceWorkerRegistration {
128    /// A unique identifer.
129    id: ServiceWorkerRegistrationId,
130    /// <https://w3c.github.io/ServiceWorker/#dfn-active-worker>
131    active_worker: Option<ServiceWorker>,
132    /// <https://w3c.github.io/ServiceWorker/#dfn-waiting-worker>
133    waiting_worker: Option<ServiceWorker>,
134    /// <https://w3c.github.io/ServiceWorker/#dfn-installing-worker>
135    installing_worker: Option<ServiceWorker>,
136    /// A channel to send control message to the worker,
137    /// currently only used to signal shutdown.
138    control_sender: Option<Sender<ServiceWorkerControlMsg>>,
139    /// A handle to join on the worker thread.
140    join_handle: Option<JoinHandle<()>>,
141    /// A context to request an interrupt.
142    context: Option<ThreadSafeJSContext>,
143    /// The closing flag for the worker.
144    closing: Option<Arc<AtomicBool>>,
145}
146
147impl ServiceWorkerRegistration {
148    pub(crate) fn new() -> ServiceWorkerRegistration {
149        ServiceWorkerRegistration {
150            id: ServiceWorkerRegistrationId::new(),
151            active_worker: None,
152            waiting_worker: None,
153            installing_worker: None,
154            join_handle: None,
155            control_sender: None,
156            context: None,
157            closing: None,
158        }
159    }
160
161    fn note_worker_thread(
162        &mut self,
163        join_handle: JoinHandle<()>,
164        control_sender: Sender<ServiceWorkerControlMsg>,
165        context: ThreadSafeJSContext,
166        closing: Arc<AtomicBool>,
167    ) {
168        assert!(self.join_handle.is_none());
169        self.join_handle = Some(join_handle);
170
171        assert!(self.control_sender.is_none());
172        self.control_sender = Some(control_sender);
173
174        assert!(self.context.is_none());
175        self.context = Some(context);
176
177        assert!(self.closing.is_none());
178        self.closing = Some(closing);
179    }
180
181    /// <https://w3c.github.io/ServiceWorker/#get-newest-worker>
182    fn get_newest_worker(&self) -> Option<ServiceWorker> {
183        if let Some(worker) = self.active_worker.as_ref() {
184            return Some(worker.clone());
185        }
186        if let Some(worker) = self.waiting_worker.as_ref() {
187            return Some(worker.clone());
188        }
189        if let Some(worker) = self.installing_worker.as_ref() {
190            return Some(worker.clone());
191        }
192        None
193    }
194
195    /// <https://w3c.github.io/ServiceWorker/#update-registration-state>
196    fn update_registration_state(
197        &mut self,
198        target: RegistrationUpdateTarget,
199        worker: ServiceWorker,
200    ) {
201        match target {
202            RegistrationUpdateTarget::Active => {
203                self.active_worker = Some(worker);
204            },
205            RegistrationUpdateTarget::Waiting => {
206                self.waiting_worker = Some(worker);
207            },
208            RegistrationUpdateTarget::Installing => {
209                self.installing_worker = Some(worker);
210            },
211        }
212    }
213}
214
215/// A structure managing all registrations and workers for a given origin.
216pub struct ServiceWorkerManager {
217    /// <https://w3c.github.io/ServiceWorker/#dfn-scope-to-registration-map>
218    registrations: HashMap<ServoUrl, ServiceWorkerRegistration>,
219    // Will be useful to implement posting a message to a client.
220    // See https://github.com/servo/servo/issues/24660
221    _constellation_sender: GenericSender<SWManagerMsg>,
222    // own sender to send messages here
223    own_sender: GenericSender<ServiceWorkerMsg>,
224    // receiver to receive messages from constellation
225    own_port: RoutedReceiver<ServiceWorkerMsg>,
226    // to receive resource messages
227    resource_receiver: Receiver<CustomResponseMediator>,
228    /// A shared [`FontContext`] to use for all service workers spawned by this [`ServiceWorkerManager`].
229    font_context: Arc<FontContext>,
230}
231
232impl ServiceWorkerManager {
233    fn new(
234        own_sender: GenericSender<ServiceWorkerMsg>,
235        from_constellation_receiver: RoutedReceiver<ServiceWorkerMsg>,
236        resource_port: Receiver<CustomResponseMediator>,
237        constellation_sender: GenericSender<SWManagerMsg>,
238        font_context: Arc<FontContext>,
239    ) -> ServiceWorkerManager {
240        // Install a pipeline-namespace in the current thread.
241        PipelineNamespace::auto_install();
242
243        ServiceWorkerManager {
244            registrations: HashMap::new(),
245            own_sender,
246            own_port: from_constellation_receiver,
247            resource_receiver: resource_port,
248            _constellation_sender: constellation_sender,
249            font_context,
250        }
251    }
252
253    pub(crate) fn get_matching_scope(&self, load_url: &ServoUrl) -> Option<ServoUrl> {
254        for scope in self.registrations.keys() {
255            if longest_prefix_match(scope, load_url) {
256                return Some(scope.clone());
257            }
258        }
259        None
260    }
261
262    fn handle_message(&mut self) {
263        while let Ok(message) = self.receive_message() {
264            let should_continue = match message {
265                Message::FromConstellation(msg) => self.handle_message_from_constellation(*msg),
266                Message::FromResource(msg) => self.handle_message_from_resource(msg),
267            };
268            if !should_continue {
269                for registration in self.registrations.drain() {
270                    // Signal shut-down, and join on the thread.
271                    drop(registration);
272                }
273                break;
274            }
275        }
276    }
277
278    fn handle_message_from_resource(&mut self, mediator: CustomResponseMediator) -> bool {
279        if serviceworker_enabled() {
280            if let Some(scope) = self.get_matching_scope(&mediator.load_url) {
281                if let Some(registration) = self.registrations.get(&scope) {
282                    if let Some(ref worker) = registration.active_worker {
283                        worker.send_message(ServiceWorkerScriptMsg::Response(mediator));
284                        return true;
285                    }
286                }
287            }
288        }
289        let _ = mediator.response_chan.send(None);
290        true
291    }
292
293    fn receive_message(&mut self) -> generic_channel::ReceiveResult<Message> {
294        select! {
295            recv(self.own_port) -> result_msg => generic_channel::to_receive_result::<ServiceWorkerMsg>(result_msg).map(|msg| Message::FromConstellation(Box::new(msg))),
296            recv(self.resource_receiver) -> msg => msg.map(Message::FromResource).map_err(|_e| ReceiveError::Disconnected),
297        }
298    }
299
300    fn handle_message_from_constellation(&mut self, msg: ServiceWorkerMsg) -> bool {
301        match msg {
302            ServiceWorkerMsg::Timeout(_scope) => {
303                // TODO: https://w3c.github.io/ServiceWorker/#terminate-service-worker
304            },
305            ServiceWorkerMsg::ForwardDOMMessage(msg, scope_url) => {
306                if let Some(registration) = self.registrations.get_mut(&scope_url) {
307                    if let Some(ref worker) = registration.active_worker {
308                        worker.forward_dom_message(msg);
309                    }
310                }
311            },
312            ServiceWorkerMsg::ScheduleJob(job) => match job.job_type {
313                JobType::Register => {
314                    self.handle_register_job(job);
315                },
316                JobType::Update => {
317                    self.handle_update_job(job);
318                },
319                JobType::Unregister => {
320                    // TODO: https://w3c.github.io/ServiceWorker/#unregister-algorithm
321                },
322            },
323            ServiceWorkerMsg::Exit => return false,
324        }
325        true
326    }
327
328    /// <https://w3c.github.io/ServiceWorker/#register-algorithm>
329    fn handle_register_job(&mut self, mut job: Job) {
330        if !job.script_url.origin().is_potentially_trustworthy() {
331            // Step 1.1
332            let _ = job
333                .client
334                .send(JobResult::RejectPromise(JobError::SecurityError));
335            return;
336        }
337
338        if job.script_url.origin() != job.referrer.origin() ||
339            job.scope_url.origin() != job.referrer.origin()
340        {
341            // Step 2.1
342            let _ = job
343                .client
344                .send(JobResult::RejectPromise(JobError::SecurityError));
345            return;
346        }
347
348        // Step 4: Get registration.
349        if let Some(registration) = self.registrations.get(&job.scope_url) {
350            // Step 5, we have a registation.
351
352            // Step 5.1, get newest worker
353            let newest_worker = registration.get_newest_worker();
354
355            // step 5.2
356            if newest_worker.is_some() {
357                // TODO: the various checks of job versus worker.
358
359                // Step 2.1: Run resolve job.
360                let client = job.client.clone();
361                let _ = client.send(JobResult::ResolvePromise(
362                    job,
363                    JobResultValue::Registration {
364                        id: registration.id,
365                        installing_worker: registration
366                            .installing_worker
367                            .as_ref()
368                            .map(|worker| worker.id),
369                        waiting_worker: registration
370                            .waiting_worker
371                            .as_ref()
372                            .map(|worker| worker.id),
373                        active_worker: registration.active_worker.as_ref().map(|worker| worker.id),
374                    },
375                ));
376            }
377        } else {
378            // Step 6: we do not have a registration.
379
380            // Step 6.1: Run Set Registration.
381            let new_registration = ServiceWorkerRegistration::new();
382            self.registrations
383                .insert(job.scope_url.clone(), new_registration);
384
385            // Step 7: Schedule update
386            job.job_type = JobType::Update;
387            let _ = self.own_sender.send(ServiceWorkerMsg::ScheduleJob(job));
388        }
389    }
390
391    /// <https://w3c.github.io/ServiceWorker/#update>
392    fn handle_update_job(&mut self, job: Job) {
393        // Step 1: Get registation
394        if let Some(registration) = self.registrations.get_mut(&job.scope_url) {
395            // Step 3.
396            let newest_worker = registration.get_newest_worker();
397
398            // Step 4.
399            if let Some(worker) = newest_worker {
400                if worker.script_url != job.script_url {
401                    let _ = job
402                        .client
403                        .send(JobResult::RejectPromise(JobError::TypeError));
404                    return;
405                }
406            }
407
408            let scope_things = job
409                .scope_things
410                .clone()
411                .expect("Update job should have scope things.");
412
413            // Very roughly steps 5 to 18.
414            // TODO: implement all steps precisely.
415            let (new_worker, join_handle, control_sender, context, closing) = update_serviceworker(
416                self.own_sender.clone(),
417                job.scope_url.clone(),
418                scope_things,
419                self.font_context.clone(),
420            );
421
422            // Since we've just started the worker thread, ensure we can shut it down later.
423            registration.note_worker_thread(join_handle, control_sender, context, closing);
424
425            // Step 19, run Install.
426
427            // Install: Step 4, run Update Registration State.
428            registration
429                .update_registration_state(RegistrationUpdateTarget::Installing, new_worker);
430
431            // Install: Step 7, run Resolve Job Promise.
432            let client = job.client.clone();
433            let _ = client.send(JobResult::ResolvePromise(
434                job,
435                JobResultValue::Registration {
436                    id: registration.id,
437                    installing_worker: registration
438                        .installing_worker
439                        .as_ref()
440                        .map(|worker| worker.id),
441                    waiting_worker: registration.waiting_worker.as_ref().map(|worker| worker.id),
442                    active_worker: registration.active_worker.as_ref().map(|worker| worker.id),
443                },
444            ));
445        } else {
446            // Step 2
447            let _ = job
448                .client
449                .send(JobResult::RejectPromise(JobError::TypeError));
450        }
451    }
452}
453
454/// <https://w3c.github.io/ServiceWorker/#update-algorithm>
455fn update_serviceworker(
456    own_sender: GenericSender<ServiceWorkerMsg>,
457    scope_url: ServoUrl,
458    scope_things: ScopeThings,
459    font_context: Arc<FontContext>,
460) -> (
461    ServiceWorker,
462    JoinHandle<()>,
463    Sender<ServiceWorkerControlMsg>,
464    ThreadSafeJSContext,
465    Arc<AtomicBool>,
466) {
467    let (sender, receiver) = unbounded();
468    let (_devtools_sender, devtools_receiver) = ipc::channel().unwrap();
469    let worker_id = ServiceWorkerId::new();
470
471    let (control_sender, control_receiver) = unbounded();
472    let (context_sender, context_receiver) = unbounded();
473    let closing = Arc::new(AtomicBool::new(false));
474
475    let join_handle = ServiceWorkerGlobalScope::run_serviceworker_scope(
476        scope_things.clone(),
477        sender.clone(),
478        receiver,
479        devtools_receiver,
480        own_sender,
481        scope_url.clone(),
482        control_receiver,
483        context_sender,
484        closing.clone(),
485        font_context,
486    );
487
488    let context = context_receiver
489        .recv()
490        .expect("Couldn't receive a context for worker.");
491
492    (
493        ServiceWorker::new(scope_things.script_url, sender, worker_id),
494        join_handle,
495        control_sender,
496        context,
497        closing,
498    )
499}
500
501impl ServiceWorkerManagerFactory for ServiceWorkerManager {
502    fn create(sw_senders: SWManagerSenders, origin: ImmutableOrigin) {
503        let (resource_chan, resource_port) = ipc::channel().unwrap();
504
505        let SWManagerSenders {
506            resource_threads,
507            own_sender,
508            receiver,
509            swmanager_sender: constellation_sender,
510            system_font_service_sender,
511            compositor_api,
512        } = sw_senders;
513
514        let from_constellation = receiver.route_preserving_errors();
515        let resource_port = ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(resource_port);
516        let _ = resource_threads
517            .core_thread
518            .send(CoreResourceMsg::NetworkMediator(resource_chan, origin));
519
520        let font_context = Arc::new(FontContext::new(
521            Arc::new(system_font_service_sender.to_proxy()),
522            compositor_api,
523            resource_threads,
524        ));
525
526        let swmanager_thread = move || {
527            ServiceWorkerManager::new(
528                own_sender,
529                from_constellation,
530                resource_port,
531                constellation_sender,
532                font_context,
533            )
534            .handle_message()
535        };
536        if thread::Builder::new()
537            .name("SvcWorkerManager".to_owned())
538            .spawn(swmanager_thread)
539            .is_err()
540        {
541            warn!("ServiceWorkerManager thread spawning failed");
542        }
543    }
544}
545
546pub(crate) fn serviceworker_enabled() -> bool {
547    pref!(dom_serviceworker_enabled)
548}