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