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