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