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