1use 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
63pub(crate) enum ServiceWorkerScriptMsg {
65 CommonWorker(WorkerScriptMsg),
67 Response(CustomResponseMediator),
69 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 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 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
127pub(crate) enum ServiceWorkerControlMsg {
129 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 #[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 #[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, worker_url,
241 runtime,
242 from_devtools_receiver,
243 closing,
244 #[cfg(feature = "webgpu")]
245 Arc::new(IdentityHub::default()),
246 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 #[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 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 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 .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 JS_AddInterruptCallback(cx.raw_cx(), Some(interrupt_callback));
394 }
395
396 {
397 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 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 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 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 !worker.is_closing()
539}
540
541impl ServiceWorkerGlobalScopeMethods<crate::DomTypeHolder> for ServiceWorkerGlobalScope {
542 event_handler!(message, GetOnmessage, SetOnmessage);
544
545 event_handler!(messageerror, GetOnmessageerror, SetOnmessageerror);
547}