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