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