Skip to main content

script/dom/serviceworker/
serviceworkerglobalscope.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
5use std::sync::Arc;
6use std::sync::atomic::AtomicBool;
7use std::thread::{self, JoinHandle};
8use std::time::{Duration, Instant};
9
10use crossbeam_channel::{Receiver, Sender, after};
11use devtools_traits::DevtoolScriptControlMsg;
12use dom_struct::dom_struct;
13use fonts::FontContext;
14use js::context::{JSContext, RawJSContext};
15use js::jsapi::JS_AddInterruptCallback;
16use js::jsval::UndefinedValue;
17use js::realm::CurrentRealm;
18use net_traits::CustomResponseMediator;
19use net_traits::blob_url_store::UrlWithBlobClaim;
20use net_traits::request::{
21    CredentialsMode, Destination, InsecureRequestsPolicy, ParserMetadata, Referrer, RequestBuilder,
22};
23use rand::random;
24use script_bindings::interfaces::HasOrigin;
25use servo_base::generic_channel::{GenericReceiver, GenericSend, GenericSender, RoutedReceiver};
26use servo_base::id::{PipelineId, ServiceWorkerId};
27use servo_config::pref;
28use servo_constellation_traits::{
29    ScopeThings, ServiceWorkerMsg, WorkerGlobalScopeInit, WorkerScriptLoadOrigin,
30};
31use servo_url::{MutableOrigin, ServoUrl};
32use style::thread_state::{self, ThreadState};
33
34use crate::dom::abstractworker::WorkerScriptMsg;
35use crate::dom::abstractworkerglobalscope::{WorkerEventLoopMethods, run_worker_event_loop};
36use crate::dom::bindings::codegen::Bindings::ClientBinding::FrameType;
37use crate::dom::bindings::codegen::Bindings::ServiceWorkerGlobalScopeBinding;
38use crate::dom::bindings::codegen::Bindings::ServiceWorkerGlobalScopeBinding::ServiceWorkerGlobalScopeMethods;
39use crate::dom::bindings::codegen::Bindings::WorkerBinding::WorkerType;
40use crate::dom::bindings::codegen::UnionTypes::ClientOrServiceWorkerOrMessagePort;
41use crate::dom::bindings::inheritance::Castable;
42use crate::dom::bindings::root::DomRoot;
43use crate::dom::bindings::str::DOMString;
44use crate::dom::bindings::structuredclone;
45use crate::dom::bindings::trace::CustomTraceable;
46use crate::dom::bindings::utils::define_all_exposed_interfaces;
47use crate::dom::client::Client;
48use crate::dom::csp::Violation;
49use crate::dom::debugger::debuggerglobalscope::DebuggerGlobalScope;
50use crate::dom::dedicatedworkerglobalscope::AutoWorkerReset;
51use crate::dom::event::Event;
52use crate::dom::eventtarget::EventTarget;
53use crate::dom::extendableevent::ExtendableEvent;
54use crate::dom::extendablemessageevent::ExtendableMessageEvent;
55use crate::dom::globalscope::GlobalScope;
56use crate::dom::globalscope::script_execution::RethrowErrors;
57use crate::dom::script_execution::ScriptOptions;
58#[cfg(feature = "webgpu")]
59use crate::dom::webgpu::identityhub::IdentityHub;
60use crate::dom::worker::TrustedWorkerAddress;
61use crate::dom::workerglobalscope::WorkerGlobalScope;
62use crate::fetch::fetch::{CspViolationsProcessor, load_whole_resource};
63use crate::messaging::{CommonScriptMsg, ScriptEventLoopSender};
64use crate::modules::script_module::ScriptFetchOptions;
65use crate::realms::enter_auto_realm;
66use crate::runtime::script_runtime::{IntroductionType, Runtime, ThreadSafeJSContext};
67use crate::tasks::task_queue::{QueuedTask, QueuedTaskConversion, TaskQueue};
68use crate::tasks::task_source::TaskSourceName;
69
70/// Messages used to control service worker event loop
71pub(crate) enum ServiceWorkerScriptMsg {
72    /// Message common to all workers
73    CommonWorker(WorkerScriptMsg),
74    /// Message to request a custom response by the service worker
75    Response(CustomResponseMediator),
76    /// Wake-up call from the task queue.
77    WakeUp,
78}
79
80impl QueuedTaskConversion for ServiceWorkerScriptMsg {
81    fn task_source_name(&self) -> Option<&TaskSourceName> {
82        let script_msg = match self {
83            ServiceWorkerScriptMsg::CommonWorker(WorkerScriptMsg::Common(script_msg)) => script_msg,
84            _ => return None,
85        };
86        match script_msg {
87            CommonScriptMsg::Task(_category, _boxed, _pipeline_id, task_source) => {
88                Some(task_source)
89            },
90            _ => None,
91        }
92    }
93
94    fn pipeline_id(&self) -> Option<PipelineId> {
95        // Workers always return None, since the pipeline_id is only used to check for document activity,
96        // and this check does not apply to worker event-loops.
97        None
98    }
99
100    fn into_queued_task(self) -> Option<QueuedTask> {
101        let script_msg = match self {
102            ServiceWorkerScriptMsg::CommonWorker(WorkerScriptMsg::Common(script_msg)) => script_msg,
103            _ => return None,
104        };
105        let (event_category, task, pipeline_id, task_source) = match script_msg {
106            CommonScriptMsg::Task(category, boxed, pipeline_id, task_source) => {
107                (category, boxed, pipeline_id, task_source)
108            },
109            _ => return None,
110        };
111        Some(QueuedTask {
112            worker: None,
113            event_category,
114            task,
115            pipeline_id,
116            task_source,
117        })
118    }
119
120    fn from_queued_task(queued_task: QueuedTask) -> Self {
121        let script_msg = CommonScriptMsg::Task(
122            queued_task.event_category,
123            queued_task.task,
124            queued_task.pipeline_id,
125            queued_task.task_source,
126        );
127        ServiceWorkerScriptMsg::CommonWorker(WorkerScriptMsg::Common(script_msg))
128    }
129
130    fn inactive_msg() -> Self {
131        // Inactive is only relevant in the context of a browsing-context event-loop.
132        panic!("Workers should never receive messages marked as inactive");
133    }
134
135    fn wake_up_msg() -> Self {
136        ServiceWorkerScriptMsg::WakeUp
137    }
138
139    fn is_wake_up(&self) -> bool {
140        matches!(self, ServiceWorkerScriptMsg::WakeUp)
141    }
142}
143
144/// Messages sent from the owning registration.
145pub(crate) enum ServiceWorkerControlMsg {
146    /// Shutdown.
147    Exit,
148}
149
150pub(crate) enum MixedMessage {
151    ServiceWorker(ServiceWorkerScriptMsg),
152    Devtools(DevtoolScriptControlMsg),
153    Control(ServiceWorkerControlMsg),
154    Timer,
155}
156
157struct ServiceWorkerCspProcessor {}
158
159impl CspViolationsProcessor for ServiceWorkerCspProcessor {
160    fn process_csp_violations(&self, _cx: &mut JSContext, _violations: Vec<Violation>) {}
161}
162
163#[dom_struct]
164pub(crate) struct ServiceWorkerGlobalScope {
165    workerglobalscope: WorkerGlobalScope,
166
167    #[ignore_malloc_size_of = "Defined in std"]
168    #[no_trace]
169    task_queue: TaskQueue<ServiceWorkerScriptMsg>,
170
171    own_sender: Sender<ServiceWorkerScriptMsg>,
172
173    /// A port on which a single "time-out" message can be received,
174    /// indicating the sw should stop running,
175    /// while still draining the task-queue
176    // and running all enqueued, and not cancelled, tasks.
177    #[no_trace]
178    time_out_port: Receiver<Instant>,
179
180    #[no_trace]
181    swmanager_sender: GenericSender<ServiceWorkerMsg>,
182
183    #[no_trace]
184    scope_url: ServoUrl,
185
186    /// A receiver of control messages,
187    /// currently only used to signal shutdown.
188    #[no_trace]
189    control_receiver: Receiver<ServiceWorkerControlMsg>,
190
191    #[no_trace]
192    worker_id: ServiceWorkerId,
193}
194
195impl WorkerEventLoopMethods for ServiceWorkerGlobalScope {
196    type WorkerMsg = ServiceWorkerScriptMsg;
197    type ControlMsg = ServiceWorkerControlMsg;
198    type Event = MixedMessage;
199
200    fn task_queue(&self) -> &TaskQueue<ServiceWorkerScriptMsg> {
201        &self.task_queue
202    }
203
204    fn handle_event(&self, event: MixedMessage, cx: &mut JSContext) -> bool {
205        self.handle_mixed_message(event, cx)
206    }
207
208    fn handle_worker_post_event(
209        &self,
210        _worker: &TrustedWorkerAddress,
211    ) -> Option<AutoWorkerReset<'_>> {
212        None
213    }
214
215    fn from_control_msg(msg: ServiceWorkerControlMsg) -> MixedMessage {
216        MixedMessage::Control(msg)
217    }
218
219    fn from_worker_msg(msg: ServiceWorkerScriptMsg) -> MixedMessage {
220        MixedMessage::ServiceWorker(msg)
221    }
222
223    fn from_devtools_msg(msg: DevtoolScriptControlMsg) -> MixedMessage {
224        MixedMessage::Devtools(msg)
225    }
226
227    fn from_timer_msg() -> MixedMessage {
228        MixedMessage::Timer
229    }
230
231    fn control_receiver(&self) -> &Receiver<ServiceWorkerControlMsg> {
232        &self.control_receiver
233    }
234}
235
236impl ServiceWorkerGlobalScope {
237    #[allow(clippy::too_many_arguments)]
238    fn new_inherited(
239        init: WorkerGlobalScopeInit,
240        worker_url: ServoUrl,
241        from_devtools_receiver: RoutedReceiver<DevtoolScriptControlMsg>,
242        runtime: Runtime,
243        own_sender: Sender<ServiceWorkerScriptMsg>,
244        receiver: Receiver<ServiceWorkerScriptMsg>,
245        time_out_port: Receiver<Instant>,
246        swmanager_sender: GenericSender<ServiceWorkerMsg>,
247        scope_url: ServoUrl,
248        control_receiver: Receiver<ServiceWorkerControlMsg>,
249        closing: Arc<AtomicBool>,
250        font_context: Arc<FontContext>,
251        worker_id: ServiceWorkerId,
252    ) -> ServiceWorkerGlobalScope {
253        ServiceWorkerGlobalScope {
254            workerglobalscope: WorkerGlobalScope::new_inherited(
255                init,
256                DOMString::new(),
257                WorkerType::Classic, // FIXME(cybai): Should be provided from `Run Service Worker`
258                worker_url,
259                runtime,
260                from_devtools_receiver,
261                closing,
262                #[cfg(feature = "webgpu")]
263                Arc::new(IdentityHub::default()),
264                // FIXME: investigate what environment this value comes from for service workers.
265                InsecureRequestsPolicy::DoNotUpgrade,
266                font_context,
267                Some(ScriptEventLoopSender::ServiceWorker(own_sender.clone())),
268            ),
269            task_queue: TaskQueue::new(receiver, own_sender.clone()),
270            own_sender,
271            time_out_port,
272            swmanager_sender,
273            scope_url,
274            control_receiver,
275            worker_id,
276        }
277    }
278
279    #[allow(clippy::too_many_arguments)]
280    pub(crate) fn new(
281        init: WorkerGlobalScopeInit,
282        worker_url: ServoUrl,
283        from_devtools_receiver: RoutedReceiver<DevtoolScriptControlMsg>,
284        runtime: Runtime,
285        own_sender: Sender<ServiceWorkerScriptMsg>,
286        receiver: Receiver<ServiceWorkerScriptMsg>,
287        time_out_port: Receiver<Instant>,
288        swmanager_sender: GenericSender<ServiceWorkerMsg>,
289        scope_url: ServoUrl,
290        control_receiver: Receiver<ServiceWorkerControlMsg>,
291        closing: Arc<AtomicBool>,
292        font_context: Arc<FontContext>,
293        debugger_global: &DebuggerGlobalScope,
294        worker_id: ServiceWorkerId,
295        cx: &mut JSContext,
296    ) -> DomRoot<ServiceWorkerGlobalScope> {
297        let scope = Box::new(ServiceWorkerGlobalScope::new_inherited(
298            init,
299            worker_url,
300            from_devtools_receiver,
301            runtime,
302            own_sender,
303            receiver,
304            time_out_port,
305            swmanager_sender,
306            scope_url,
307            control_receiver,
308            closing,
309            font_context,
310            worker_id,
311        ));
312        let scope = ServiceWorkerGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(
313            cx,
314            &scope.origin(),
315            scope,
316        );
317        scope
318            .upcast::<WorkerGlobalScope>()
319            .init_debugger_global(debugger_global, cx);
320
321        scope
322    }
323
324    /// <https://w3c.github.io/ServiceWorker/#run-service-worker-algorithm>
325    #[expect(unsafe_code)]
326    #[allow(clippy::too_many_arguments)]
327    pub(crate) fn run_serviceworker_scope(
328        scope_things: ScopeThings,
329        own_sender: Sender<ServiceWorkerScriptMsg>,
330        receiver: Receiver<ServiceWorkerScriptMsg>,
331        devtools_receiver: GenericReceiver<DevtoolScriptControlMsg>,
332        swmanager_sender: GenericSender<ServiceWorkerMsg>,
333        scope_url: ServoUrl,
334        control_receiver: Receiver<ServiceWorkerControlMsg>,
335        context_sender: Sender<ThreadSafeJSContext>,
336        closing: Arc<AtomicBool>,
337        font_context: Arc<FontContext>,
338        worker_id: ServiceWorkerId,
339    ) -> JoinHandle<()> {
340        let ScopeThings {
341            script_url,
342            init,
343            worker_load_origin,
344            ..
345        } = scope_things;
346
347        let serialized_worker_url = script_url.to_string();
348        let origin = scope_url.origin();
349        thread::Builder::new()
350            .name(format!("SW:{}", script_url.debug_compact()))
351            .spawn(move || {
352                thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
353                let runtime = Runtime::new(None);
354                // SAFETY: We are in a new thread, so this first cx.
355                // It is OK to have it separated of runtime here,
356                // because it will never outlive it (runtime destruction happens at the end of this function
357                let mut cx = unsafe { runtime.cx() };
358                let cx = &mut cx;
359                let context_for_interrupt = runtime.thread_safe_js_context();
360                let _ = context_sender.send(context_for_interrupt);
361
362                let WorkerScriptLoadOrigin {
363                    referrer_url,
364                    referrer_policy,
365                    pipeline_id,
366                } = worker_load_origin;
367
368                let debugger_global = DebuggerGlobalScope::new(
369                    pipeline_id,
370                    init.to_devtools_sender.clone(),
371                    init.from_devtools_sender
372                        .clone()
373                        .expect("Guaranteed by update_serviceworker"),
374                    init.mem_profiler_chan.clone(),
375                    init.time_profiler_chan.clone(),
376                    init.script_to_constellation_chan.clone(),
377                    init.script_to_embedder_chan.clone(),
378                    init.resource_threads.clone(),
379                    init.storage_threads.clone(),
380                    #[cfg(feature = "webgpu")]
381                    Arc::new(IdentityHub::default()),
382                    cx,
383                );
384                debugger_global.execute(cx);
385
386                // Service workers are time limited
387                // https://w3c.github.io/ServiceWorker/#service-worker-lifetime
388                let sw_lifetime_timeout = pref!(dom_serviceworker_timeout_seconds) as u64;
389                let time_out_port = after(Duration::new(sw_lifetime_timeout, 0));
390
391                let devtools_mpsc_port = devtools_receiver.route_preserving_errors();
392
393                let resource_threads_sender = init.resource_threads.sender();
394                let devtools_enabled = init.to_devtools_sender.is_some();
395                let global = ServiceWorkerGlobalScope::new(
396                    init,
397                    script_url.clone(),
398                    devtools_mpsc_port,
399                    runtime,
400                    own_sender,
401                    receiver,
402                    time_out_port,
403                    swmanager_sender,
404                    scope_url,
405                    control_receiver,
406                    closing,
407                    font_context,
408                    &debugger_global,
409                    worker_id,
410                    cx,
411                );
412
413                let worker_scope = global.upcast::<WorkerGlobalScope>();
414                let global_scope = global.upcast::<GlobalScope>();
415
416                if devtools_enabled {
417                    debugger_global.fire_add_debuggee(
418                        cx,
419                        global_scope,
420                        pipeline_id,
421                        Some(worker_scope.worker_id()),
422                    );
423                }
424
425                let referrer = referrer_url
426                    .map(Referrer::ReferrerUrl)
427                    .unwrap_or_else(|| global_scope.get_referrer());
428
429                let request = RequestBuilder::new(
430                    None,
431                    UrlWithBlobClaim::from_url_without_having_claimed_blob(script_url),
432                    referrer,
433                )
434                .destination(Destination::ServiceWorker)
435                .credentials_mode(CredentialsMode::Include)
436                .parser_metadata(ParserMetadata::NotParserInserted)
437                .use_url_credentials(true)
438                .pipeline_id(Some(pipeline_id))
439                .referrer_policy(referrer_policy)
440                // TODO: Use policy container from ScopeThings
441                .policy_container(global_scope.policy_container())
442                .origin(origin);
443
444                let (url, source) = match load_whole_resource(
445                    request,
446                    &resource_threads_sender,
447                    global.upcast(),
448                    &ServiceWorkerCspProcessor {},
449                    cx,
450                ) {
451                    Err(_) => {
452                        error!("error loading script {}", serialized_worker_url);
453                        worker_scope.clear_js_runtime();
454                        return;
455                    },
456                    Ok((metadata, bytes, _)) => (metadata.final_url, bytes),
457                };
458
459                unsafe {
460                    // Handle interrupt requests
461                    JS_AddInterruptCallback(cx.raw_cx(), Some(interrupt_callback));
462                }
463
464                {
465                    // TODO: use AutoWorkerReset as in dedicated worker?
466                    let mut realm = enter_auto_realm(cx, worker_scope);
467                    let mut realm = realm.current_realm();
468                    define_all_exposed_interfaces(&mut realm, global_scope);
469
470                    let script = global_scope.create_a_classic_script(
471                        &mut realm,
472                        String::from_utf8_lossy(&source),
473                        url,
474                        ScriptOptions::External,
475                        ScriptFetchOptions::default_classic_script(),
476                        Some(IntroductionType::WORKER),
477                        1,
478                    );
479                    _ = global_scope.run_a_classic_script(
480                        &mut realm,
481                        script,
482                        RethrowErrors::No,
483                        None, // return_value
484                    );
485                    global.dispatch_activate(&mut realm);
486                }
487
488                let reporter_name = format!("service-worker-reporter-{}", random::<u64>());
489                global_scope.mem_profiler_chan().run_with_memory_reporting(
490                    || {
491                        // Step 18, Run the responsible event loop specified
492                        // by inside settings until it is destroyed.
493                        // The worker processing model remains on this step
494                        // until the event loop is destroyed,
495                        // which happens after the closing flag is set to true,
496                        // or until the worker has run beyond its allocated time.
497                        while !worker_scope.is_closing() && !global.has_timed_out() {
498                            run_worker_event_loop(&*global, None, cx);
499                        }
500                    },
501                    reporter_name,
502                    global.event_loop_sender(),
503                    CommonScriptMsg::CollectReports,
504                );
505
506                worker_scope.clear_js_runtime();
507            })
508            .expect("Thread spawning failed")
509    }
510
511    fn handle_mixed_message(&self, msg: MixedMessage, cx: &mut JSContext) -> bool {
512        match msg {
513            MixedMessage::Devtools(msg) => self
514                .upcast::<WorkerGlobalScope>()
515                .handle_devtools_message(msg, cx),
516            MixedMessage::ServiceWorker(msg) => {
517                self.handle_script_event(msg, cx);
518            },
519            MixedMessage::Control(ServiceWorkerControlMsg::Exit) => {
520                return false;
521            },
522            MixedMessage::Timer => {},
523        }
524        true
525    }
526
527    fn has_timed_out(&self) -> bool {
528        // TODO: https://w3c.github.io/ServiceWorker/#service-worker-lifetime
529        false
530    }
531
532    fn handle_script_event(&self, msg: ServiceWorkerScriptMsg, cx: &mut JSContext) {
533        use self::ServiceWorkerScriptMsg::*;
534
535        match msg {
536            CommonWorker(WorkerScriptMsg::DOMMessage(msg)) => {
537                let scope = self.upcast::<WorkerGlobalScope>();
538                let target = self.upcast();
539
540                let mut realm = enter_auto_realm(cx, scope);
541                let cx = &mut realm.current_realm();
542
543                rooted!(&in(cx) let mut message = UndefinedValue());
544                let client = Client::new(
545                    cx,
546                    scope.upcast(),
547                    self.swmanager_sender.clone(),
548                    self.scope_url.clone(),
549                    FrameType::None,
550                    self.worker_id,
551                );
552                if let Ok(ports) =
553                    structuredclone::read(cx, scope.upcast(), *msg.data, message.handle_mut())
554                {
555                    ExtendableMessageEvent::dispatch_jsval(
556                        cx,
557                        target,
558                        scope.upcast(),
559                        message.handle(),
560                        Some(&ClientOrServiceWorkerOrMessagePort::Client(client)),
561                        ports,
562                    );
563                } else {
564                    ExtendableMessageEvent::dispatch_error(cx, target, scope.upcast());
565                }
566            },
567            CommonWorker(WorkerScriptMsg::Common(msg)) => {
568                self.upcast::<WorkerGlobalScope>().process_event(msg, cx);
569            },
570            Response(mediator) => {
571                // TODO XXXcreativcoder This will eventually use a FetchEvent interface to fire event
572                // when we have the Request and Response dom api's implemented
573                // https://w3c.github.io/ServiceWorker/#fetchevent-interface
574                self.upcast::<EventTarget>().fire_event(cx, atom!("fetch"));
575                let _ = mediator.response_chan.send(None);
576            },
577            WakeUp => {},
578        }
579    }
580
581    pub(crate) fn event_loop_sender(&self) -> ScriptEventLoopSender {
582        ScriptEventLoopSender::ServiceWorker(self.own_sender.clone())
583    }
584
585    fn dispatch_activate(&self, cx: &mut CurrentRealm) {
586        let event = ExtendableEvent::new(cx, self, atom!("activate"), false, false);
587        let event = (*event).upcast::<Event>();
588        event.dispatch(cx, self.upcast(), false);
589    }
590}
591
592#[expect(unsafe_code)]
593unsafe extern "C" fn interrupt_callback(cx: *mut RawJSContext) -> bool {
594    // SAFETY: it is safe to construct a JSContext from engine hook.
595    let mut cx = unsafe { JSContext::from_ptr(std::ptr::NonNull::new(cx).unwrap()) };
596    let mut realm = CurrentRealm::assert(&mut cx);
597
598    let global = GlobalScope::from_current_realm(&mut realm);
599    let worker =
600        DomRoot::downcast::<WorkerGlobalScope>(global).expect("global is not a worker scope");
601    assert!(worker.is::<ServiceWorkerGlobalScope>());
602
603    // A false response causes the script to terminate
604    !worker.is_closing()
605}
606
607impl ServiceWorkerGlobalScopeMethods<crate::DomTypeHolder> for ServiceWorkerGlobalScope {
608    // https://w3c.github.io/ServiceWorker/#dom-serviceworkerglobalscope-onmessage
609    event_handler!(message, GetOnmessage, SetOnmessage);
610
611    // https://w3c.github.io/ServiceWorker/#dom-serviceworkerglobalscope-onmessageerror
612    event_handler!(messageerror, GetOnmessageerror, SetOnmessageerror);
613}
614
615impl HasOrigin for ServiceWorkerGlobalScope {
616    fn origin(&self) -> MutableOrigin {
617        self.upcast::<WorkerGlobalScope>().origin()
618    }
619}