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