Skip to main content

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            let Some(scope) = self.get_matching_scope(&mediator.load_url) &&
282            let Some(registration) = self.registrations.get(&scope) &&
283            let Some(ref worker) = registration.active_worker
284        {
285            worker.send_message(ServiceWorkerScriptMsg::Response(mediator));
286            return true;
287        }
288        let _ = mediator.response_chan.send(None);
289        true
290    }
291
292    fn receive_message(&mut self) -> generic_channel::ReceiveResult<Message> {
293        select! {
294            recv(self.own_port) -> result_msg => generic_channel::to_receive_result::<ServiceWorkerMsg>(result_msg).map(|msg| Message::FromConstellation(Box::new(msg))),
295            recv(self.resource_receiver) -> msg => msg.map(Message::FromResource).map_err(|_e| ReceiveError::Disconnected),
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                    let Some(ref worker) = registration.active_worker
307                {
308                    worker.forward_dom_message(msg);
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                worker.script_url != job.script_url
400            {
401                let _ = job
402                    .client
403                    .send(JobResult::RejectPromise(JobError::TypeError));
404                return;
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: GenericSender<ServiceWorkerMsg>,
456    scope_url: ServoUrl,
457    mut 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) = generic_channel::channel().unwrap();
468    scope_things.init.from_devtools_sender = Some(devtools_sender);
469
470    if let Some(ref chan) = scope_things.devtools_chan &&
471        let Some(ref sender) = scope_things.init.from_devtools_sender
472    {
473        let page_info = DevtoolsPageInfo {
474            title: format!("Service Worker for {}", scope_things.script_url),
475            url: scope_things.script_url.clone(),
476            is_top_level_global: false,
477            is_service_worker: true,
478        };
479        let _ = chan.send(ScriptToDevtoolsControlMsg::NewGlobal(
480            (
481                scope_things.browsing_context_id,
482                scope_things.init.pipeline_id,
483                Some(scope_things.worker_id),
484                scope_things.webview_id,
485            ),
486            sender.clone(),
487            page_info,
488        ));
489    }
490
491    let worker_id = ServiceWorkerId::new();
492
493    let (control_sender, control_receiver) = unbounded();
494    let (context_sender, context_receiver) = unbounded();
495    let closing = Arc::new(AtomicBool::new(false));
496
497    let join_handle = ServiceWorkerGlobalScope::run_serviceworker_scope(
498        scope_things.clone(),
499        sender.clone(),
500        receiver,
501        devtools_receiver,
502        own_sender,
503        scope_url,
504        control_receiver,
505        context_sender,
506        closing.clone(),
507        font_context,
508    );
509
510    let context = context_receiver
511        .recv()
512        .expect("Couldn't receive a context for worker.");
513
514    (
515        ServiceWorker::new(scope_things.script_url, sender, worker_id),
516        join_handle,
517        control_sender,
518        context,
519        closing,
520    )
521}
522
523impl ServiceWorkerManagerFactory for ServiceWorkerManager {
524    fn create(sw_senders: SWManagerSenders, origin: ImmutableOrigin) {
525        let (resource_chan, resource_port) = ipc::channel().unwrap();
526
527        let SWManagerSenders {
528            resource_threads,
529            own_sender,
530            receiver,
531            swmanager_sender: constellation_sender,
532            system_font_service_sender,
533            paint_api,
534        } = sw_senders;
535
536        let from_constellation = receiver.route_preserving_errors();
537        let resource_port = ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(resource_port);
538        let _ = resource_threads
539            .core_thread
540            .send(CoreResourceMsg::NetworkMediator(resource_chan, origin));
541
542        let font_context = Arc::new(FontContext::new(
543            Arc::new(system_font_service_sender.to_proxy()),
544            paint_api,
545            resource_threads,
546        ));
547
548        let swmanager_thread = move || {
549            ServiceWorkerManager::new(
550                own_sender,
551                from_constellation,
552                resource_port,
553                constellation_sender,
554                font_context,
555            )
556            .handle_message()
557        };
558        if thread::Builder::new()
559            .name("SvcWorkerManager".to_owned())
560            .spawn(swmanager_thread)
561            .is_err()
562        {
563            warn!("ServiceWorkerManager thread spawning failed");
564        }
565    }
566}
567
568pub(crate) fn serviceworker_enabled() -> bool {
569    pref!(dom_serviceworker_enabled)
570}