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