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