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