1#![cfg_attr(crown, allow(crown::jscontext_first_arg))]
6
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Condvar, LazyLock, Mutex};
9
10use crossbeam_channel::{Sender, unbounded};
11#[cfg(feature = "devtools")]
12use devtools_traits::{DevtoolsPageInfo, ScriptToDevtoolsControlMsg};
13use dom_struct::dom_struct;
14use js::context::JSContext;
15use js::rust::HandleObject;
16use malloc_size_of_derive::MallocSizeOf;
17use net_traits::pub_domains::reg_suffix;
18use net_traits::request::{CredentialsMode, Referrer};
19use script_bindings::reflector::reflect_dom_object_with_proto;
20use servo_base::generic_channel;
21use servo_base::id::WorkerId;
22use servo_constellation_traits::{MessagePortImpl, WorkerScriptLoadOrigin};
23use servo_url::{Host, ImmutableOrigin, ServoUrl};
24use uuid::Uuid;
25
26use crate::conversions::Convert;
27use crate::dom::abstractworker::SimpleWorkerErrorHandler;
28use crate::dom::bindings::codegen::Bindings::SharedWorkerBinding::{
29 SharedWorkerMethods, SharedWorkerOptions,
30};
31use crate::dom::bindings::codegen::Bindings::WorkerBinding::WorkerType;
32use crate::dom::bindings::codegen::UnionTypes::{
33 StringOrSharedWorkerOptions, TrustedScriptURLOrUSVString,
34};
35use crate::dom::bindings::error::{Error, Fallible};
36use crate::dom::bindings::inheritance::Castable;
37use crate::dom::bindings::refcounted::Trusted;
38use crate::dom::bindings::reflector::DomGlobal;
39use crate::dom::bindings::root::{Dom, DomRoot};
40use crate::dom::bindings::trace::CustomTraceable;
41use crate::dom::bindings::transferable::Transferable;
42use crate::dom::eventtarget::EventTarget;
43use crate::dom::globalscope::GlobalScope;
44use crate::dom::messageport::MessagePort;
45use crate::dom::sharedworkerglobalscope::{
46 SharedWorkerControlMsg, SharedWorkerGlobalScope, SharedWorkerScriptMsg,
47};
48use crate::dom::trustedtypes::trustedscripturl::TrustedScriptURL;
49use crate::dom::window::Window;
50use crate::dom::workerglobalscope::prepare_workerscope_init;
51use crate::tasks::task::TaskOnce;
52use crate::url::ensure_blob_referenced_by_url_is_kept_alive;
53
54#[dom_struct]
56pub(crate) struct SharedWorker {
57 eventtarget: EventTarget,
58 port: Dom<MessagePort>,
59 _control_sender: Sender<SharedWorkerControlMsg>,
60}
61
62pub(crate) type TrustedSharedWorkerAddress = Trusted<SharedWorker>;
63
64#[derive(Clone)]
66struct SharedWorkerKey {
67 storage_key: SharedWorkerStorageKey,
68 constructor_origin: ImmutableOrigin,
69 constructor_url: ServoUrl,
70 name: String,
71}
72
73impl SharedWorkerKey {
74 fn matches(
75 &self,
76 storage_key: &SharedWorkerStorageKey,
77 constructor_origin: &ImmutableOrigin,
78 constructor_url: &ServoUrl,
79 name: &str,
80 ) -> bool {
81 self.storage_key == *storage_key &&
82 self.constructor_origin == *constructor_origin &&
83 self.constructor_url == *constructor_url &&
84 self.name == name
85 }
86}
87
88#[derive(Clone, Eq, MallocSizeOf, PartialEq)]
89enum SharedWorkerSite {
90 Opaque(ImmutableOrigin),
91 Tuple { scheme: String, host: Host },
92}
93
94impl SharedWorkerSite {
95 fn from_origin(origin: ImmutableOrigin) -> Self {
97 match origin {
98 ImmutableOrigin::Opaque(_) => SharedWorkerSite::Opaque(origin),
100 ImmutableOrigin::Tuple(scheme, Host::Domain(domain), _) => SharedWorkerSite::Tuple {
102 scheme,
103 host: Host::Domain(reg_suffix(&domain).to_owned()),
104 },
105 ImmutableOrigin::Tuple(scheme, host, _) => SharedWorkerSite::Tuple { scheme, host },
108 }
109 }
110}
111
112#[derive(Clone, Eq, MallocSizeOf, PartialEq)]
113pub(crate) struct SharedWorkerStorageKey {
114 origin: ImmutableOrigin,
115 top_level_site: Option<SharedWorkerSite>,
116}
117
118impl SharedWorkerStorageKey {
119 fn for_window(global: &GlobalScope, window: &Window) -> Self {
121 let origin = global.obtain_storage_key_for_non_storage_purposes();
124
125 let top_level_site = window
126 .top_level_document_if_local()
127 .map(|document| SharedWorkerSite::from_origin(document.origin().immutable().clone()));
128
129 SharedWorkerStorageKey {
130 origin,
131 top_level_site,
132 }
133 }
134}
135
136enum SharedWorkerRegistryState {
137 Creating { waiters: usize },
138 Created(SharedWorkerRegistration),
139 Failed { waiters: usize },
140}
141
142struct SharedWorkerRegistryEntry {
143 key: SharedWorkerKey,
144 state: SharedWorkerRegistryState,
145}
146
147#[derive(Clone)]
149struct SharedWorkerRegistration {
150 id: Uuid,
151 worker_type: WorkerType,
152 credentials: CredentialsMode,
153 extended_lifetime: bool,
154 worker_is_secure_context: bool,
155 closing: Arc<AtomicBool>,
156 sender: Sender<SharedWorkerScriptMsg>,
157 _control_sender: Sender<SharedWorkerControlMsg>,
158}
159
160static SHARED_WORKERS: LazyLock<(Mutex<Vec<SharedWorkerRegistryEntry>>, Condvar)> =
166 LazyLock::new(|| (Mutex::new(Vec::new()), Condvar::new()));
167
168enum SharedWorkerClaimResult {
172 Created(SharedWorkerRegistration),
173 Claimed,
174 Failed,
175}
176
177fn prune_closed_shared_workers(workers: &mut Vec<SharedWorkerRegistryEntry>) {
178 workers.retain(|entry| match &entry.state {
179 SharedWorkerRegistryState::Creating { .. } => true,
180 SharedWorkerRegistryState::Created(worker) => !worker.closing.load(Ordering::SeqCst),
181 SharedWorkerRegistryState::Failed { waiters } => *waiters > 0,
182 });
183}
184
185fn find_matching_shared_worker(
186 workers: &[SharedWorkerRegistryEntry],
187 key: &SharedWorkerKey,
188) -> Option<usize> {
189 workers.iter().position(|entry| {
190 entry.key.matches(
191 &key.storage_key,
192 &key.constructor_origin,
193 &key.constructor_url,
194 &key.name,
195 )
196 })
197}
198
199fn find_or_claim_shared_worker(key: SharedWorkerKey) -> SharedWorkerClaimResult {
202 let (workers, ready) = &*SHARED_WORKERS;
203 let mut workers = workers.lock().expect("SharedWorker registry poisoned");
204
205 prune_closed_shared_workers(&mut workers);
206
207 let Some(index) = find_matching_shared_worker(&workers, &key) else {
208 workers.push(SharedWorkerRegistryEntry {
209 key,
210 state: SharedWorkerRegistryState::Creating { waiters: 0 },
211 });
212 return SharedWorkerClaimResult::Claimed;
213 };
214
215 match &mut workers[index].state {
216 SharedWorkerRegistryState::Creating { waiters } => *waiters += 1,
217 SharedWorkerRegistryState::Created(registration) => {
218 return SharedWorkerClaimResult::Created(registration.clone());
219 },
220 SharedWorkerRegistryState::Failed { waiters } => *waiters += 1,
221 };
222
223 loop {
224 workers = ready.wait(workers).expect("SharedWorker registry poisoned");
225
226 let Some(index) = find_matching_shared_worker(&workers, &key) else {
227 return SharedWorkerClaimResult::Failed;
228 };
229
230 match &mut workers[index].state {
231 SharedWorkerRegistryState::Creating { .. } => {},
232 SharedWorkerRegistryState::Created(registration) => {
233 return SharedWorkerClaimResult::Created(registration.clone());
234 },
235 SharedWorkerRegistryState::Failed { waiters } => {
236 debug_assert!(*waiters > 0);
237 if *waiters > 0 {
238 *waiters -= 1;
239 }
240 if *waiters == 0 {
241 workers.remove(index);
242 }
245 return SharedWorkerClaimResult::Failed;
246 },
247 }
248 }
249}
250
251fn transition_creating_to_created(
252 key: &SharedWorkerKey,
253 registration: SharedWorkerRegistration,
254) -> bool {
255 let (workers, ready) = &*SHARED_WORKERS;
256 let mut workers = workers.lock().expect("SharedWorker registry poisoned");
257 let index = find_matching_shared_worker(&workers, key);
258 debug_assert!(index.is_some(), "claimed SharedWorker entry should exist");
259 let Some(index) = index else {
260 ready.notify_all();
261 return false;
262 };
263
264 let entry_is_creating = matches!(
265 &workers[index].state,
266 SharedWorkerRegistryState::Creating { .. }
267 );
268 debug_assert!(
269 entry_is_creating,
270 "claimed SharedWorker entry should still be creating"
271 );
272 if !entry_is_creating {
273 ready.notify_all();
274 return false;
275 }
276
277 workers[index].state = SharedWorkerRegistryState::Created(registration);
278 ready.notify_all();
279 true
280}
281
282fn remove_creating_shared_worker(key: &SharedWorkerKey) {
283 let (workers, ready) = &*SHARED_WORKERS;
284 let mut workers = workers.lock().expect("SharedWorker registry poisoned");
285 let index = find_matching_shared_worker(&workers, key);
286 debug_assert!(index.is_some(), "claimed SharedWorker entry should exist");
287 let Some(index) = index else {
288 return;
289 };
290
291 let waiters = match &workers[index].state {
292 SharedWorkerRegistryState::Creating { waiters } => *waiters,
293 state => {
294 debug_assert!(
295 matches!(state, SharedWorkerRegistryState::Creating { .. }),
296 "claimed SharedWorker entry should still be creating"
297 );
298 return;
299 },
300 };
301
302 if waiters == 0 {
303 workers.remove(index);
304 } else {
305 workers[index].state = SharedWorkerRegistryState::Failed { waiters };
306 };
307 ready.notify_all();
308}
309
310fn send_connect_to_created_worker(
311 registration: &SharedWorkerRegistration,
312 inside_port: MessagePortImpl,
313) -> bool {
314 registration
315 .sender
316 .send(SharedWorkerScriptMsg::Connect(inside_port))
317 .is_err()
318}
319
320impl SharedWorker {
321 pub(crate) fn unregister_shared_worker(id: Uuid) {
322 let (workers, ready) = &*SHARED_WORKERS;
323 let mut workers = workers.lock().expect("SharedWorker registry poisoned");
324 let old_len = workers.len();
325 workers.retain(|entry| {
326 !matches!(&entry.state, SharedWorkerRegistryState::Created(worker) if worker.id == id)
327 });
328 if workers.len() != old_len {
329 ready.notify_all();
330 }
331 }
332
333 fn new_inherited(
334 port: &MessagePort,
335 control_sender: Sender<SharedWorkerControlMsg>,
336 ) -> SharedWorker {
337 SharedWorker {
338 eventtarget: EventTarget::new_inherited(),
339 port: Dom::from_ref(port),
340 _control_sender: control_sender,
341 }
342 }
343
344 fn new(
345 global: &GlobalScope,
346 proto: Option<HandleObject>,
347 port: &MessagePort,
348 control_sender: Sender<SharedWorkerControlMsg>,
349 cx: &mut js::context::JSContext,
350 ) -> DomRoot<SharedWorker> {
351 reflect_dom_object_with_proto(
352 cx,
353 Box::new(SharedWorker::new_inherited(port, control_sender)),
354 global,
355 proto,
356 )
357 }
358
359 pub(crate) fn dispatch_simple_error(cx: &mut JSContext, address: TrustedSharedWorkerAddress) {
360 let worker = address.root();
361 worker.upcast().fire_event(cx, atom!("error"));
362 }
363
364 fn queue_simple_error(global: &GlobalScope, address: TrustedSharedWorkerAddress) {
365 global.task_manager().dom_manipulation_task_source().queue(
366 task!(sharedworker_constructor_error: move |cx| {
367 SharedWorker::dispatch_simple_error(cx, address);
368 }),
369 );
370 }
371
372 fn create_entangled_inside_port(
373 cx: &mut JSContext,
374 global: &GlobalScope,
375 outside_port: &MessagePort,
376 ) -> Fallible<MessagePortImpl> {
377 let inside_port = MessagePort::new(cx, global);
378 global.track_message_port(&inside_port, None);
379 global.entangle_ports(
380 *outside_port.message_port_id(),
381 *inside_port.message_port_id(),
382 );
383 let (_, inside_port_impl) = inside_port.transfer(cx)?;
384 Ok(inside_port_impl)
385 }
386
387 pub(crate) fn enable_outside_port_message_queue(
389 address: TrustedSharedWorkerAddress,
390 cx: &mut JSContext,
391 ) {
392 let worker = address.root();
393 let global = worker.global();
394 global.start_message_port(cx, worker.port.message_port_id());
396 }
397
398 pub(crate) fn set_network_online_state(online: bool) {
400 let (workers, _ready) = &*SHARED_WORKERS;
401 let workers = workers.lock().expect("SharedWorker registry poisoned");
402 for entry in workers.iter() {
403 let SharedWorkerRegistryState::Created(registration) = &entry.state else {
404 continue;
405 };
406 if registration.closing.load(Ordering::SeqCst) {
407 continue;
408 }
409 let _ = registration
410 ._control_sender
411 .send(SharedWorkerControlMsg::SetNetworkOnlineState(online));
412 }
413 }
414}
415
416impl SharedWorkerMethods<crate::DomTypeHolder> for SharedWorker {
417 fn Constructor(
419 cx: &mut JSContext,
420 window: &Window,
421 proto: Option<HandleObject>,
422 script_url: TrustedScriptURLOrUSVString,
423 options: StringOrSharedWorkerOptions,
424 ) -> Fallible<DomRoot<SharedWorker>> {
425 let global = window.upcast::<GlobalScope>();
426
427 let compliant_script_url = TrustedScriptURL::get_trusted_type_compliant_string(
431 cx,
432 global,
433 script_url,
434 "SharedWorker constructor",
435 )?;
436
437 let worker_options = match options {
441 StringOrSharedWorkerOptions::String(name) => {
442 let mut options = SharedWorkerOptions::empty();
443 options.parent.name = name;
444 options
445 },
446 StringOrSharedWorkerOptions::SharedWorkerOptions(options) => options,
447 };
448 let worker_name = worker_options.parent.name.clone();
449 let worker_type = worker_options.parent.type_;
450 let credentials = worker_options.parent.credentials.convert();
451 let extended_lifetime = worker_options.extendedLifetime;
452
453 let Ok(worker_url) = global
459 .encoding_parse_a_url(&compliant_script_url.str())
460 .map(|url| ensure_blob_referenced_by_url_is_kept_alive(global, url))
461 else {
462 return Err(Error::Syntax(None));
463 };
464 let constructor_origin = global.origin().immutable().clone();
465 let constructor_url = worker_url.url();
466
467 let outside_port = MessagePort::new(cx, global);
469 global.track_message_port(&outside_port, None);
470
471 let caller_is_secure_context = global.is_secure_context();
475 let outside_storage_key = SharedWorkerStorageKey::for_window(global, window);
478
479 let worker_name_string = worker_name.to_string();
480 let (control_sender, control_receiver) = unbounded();
481
482 let worker = SharedWorker::new(global, proto, &outside_port, control_sender.clone(), cx);
484 let worker_addr = Trusted::new(&*worker);
485
486 let shared_worker_key = SharedWorkerKey {
489 storage_key: outside_storage_key.clone(),
490 constructor_origin: constructor_origin.clone(),
492 constructor_url: constructor_url.clone(),
493 name: worker_name_string,
494 };
495
496 let shared_worker = find_or_claim_shared_worker(shared_worker_key.clone());
507
508 match shared_worker {
509 SharedWorkerClaimResult::Created(registration) => {
510 if registration.worker_type != worker_type ||
518 registration.credentials != credentials ||
519 registration.extended_lifetime != extended_lifetime
520 {
521 SharedWorker::queue_simple_error(global, worker_addr);
523 return Ok(worker);
525 }
526
527 if registration.worker_is_secure_context != caller_is_secure_context {
532 SharedWorker::queue_simple_error(global, worker_addr);
534 return Ok(worker);
536 }
537
538 let inside_port_impl =
542 SharedWorker::create_entangled_inside_port(cx, global, &outside_port)?;
543 if send_connect_to_created_worker(®istration, inside_port_impl) {
545 SharedWorker::queue_simple_error(global, worker_addr);
546 }
547 return Ok(worker);
549 },
550 SharedWorkerClaimResult::Failed => {
551 SharedWorker::queue_simple_error(global, worker_addr);
552 return Ok(worker);
553 },
554 SharedWorkerClaimResult::Claimed => {},
555 }
556
557 let initial_inside_port_impl =
558 match SharedWorker::create_entangled_inside_port(cx, global, &outside_port) {
559 Ok(inside_port_impl) => inside_port_impl,
560 Err(error) => {
561 remove_creating_shared_worker(&shared_worker_key);
562 return Err(error);
563 },
564 };
565
566 let parent_event_loop_sender = global
567 .event_loop_sender()
568 .expect("Window global must have an event loop sender");
569
570 let (sender, receiver) = unbounded();
571 let closing = Arc::new(AtomicBool::new(false));
572 let registration_id = Uuid::new_v4();
573
574 let worker_load_origin = WorkerScriptLoadOrigin {
575 referrer_url: match global.get_referrer() {
576 Referrer::Client(url) => Some(url),
577 Referrer::ReferrerUrl(url) => Some(url),
578 _ => None,
579 },
580 referrer_policy: global.get_referrer_policy(),
581 pipeline_id: global.pipeline_id(),
582 };
583
584 #[cfg(feature = "devtools")]
585 let ((devtools_sender, devtools_receiver), worker_id) = {
586 let (devtools_sender, devtools_receiver) = generic_channel::channel().unwrap();
587 let worker_id = Some(WorkerId(Uuid::new_v4()));
588
589 if let Some(chan) = global.devtools_chan() {
590 let webview_id = global
591 .webview_id()
592 .expect("Window global must have a WebViewId");
593 let page_info = DevtoolsPageInfo {
594 title: format!("SharedWorker for {}", worker_url.url()),
595 url: worker_url.url(),
596 is_top_level_global: false,
597 is_service_worker: false,
598 };
599 let _ = chan.send(ScriptToDevtoolsControlMsg::NewGlobal(
600 (
601 window.window_proxy().browsing_context_id(),
602 global.pipeline_id(),
603 worker_id,
604 webview_id,
605 ),
606 devtools_sender.clone(),
607 page_info,
608 ));
609 }
610
611 ((devtools_sender, devtools_receiver), worker_id)
612 };
613 #[cfg(not(feature = "devtools"))]
614 let worker_id: Option<WorkerId> = None;
615
616 let init = prepare_workerscope_init(
617 global,
618 #[cfg(feature = "devtools")]
619 Some(devtools_sender),
620 worker_id,
621 #[cfg(feature = "webgl")]
622 window.webgl_chan(),
623 );
624
625 let (setup_sender, setup_receiver) = unbounded();
626 let (registered_sender, registered_receiver) = unbounded();
627
628 let _join_handle = match SharedWorkerGlobalScope::run_shared_worker_scope(
630 init,
631 window.webview_id(),
632 Some(window.window_proxy().browsing_context_id()),
633 worker_name,
634 worker_type,
635 worker_url,
636 worker_addr.clone(),
637 parent_event_loop_sender,
638 #[cfg(feature = "devtools")]
639 devtools_receiver,
640 sender.clone(),
641 receiver,
642 worker_load_origin,
643 closing.clone(),
644 global.image_cache(),
645 #[cfg(feature = "webgpu")]
646 global.wgpu_id_hub(),
647 control_receiver,
648 setup_sender,
649 registered_receiver,
650 registration_id,
651 credentials,
652 extended_lifetime,
653 constructor_origin,
654 constructor_url,
655 outside_storage_key,
656 global.insecure_requests_policy(),
657 global.policy_container(),
658 global.font_context(),
659 ) {
660 Ok(join_handle) => join_handle,
661 Err(error) => {
662 error!("Failed to spawn SharedWorker thread: {error}");
663 remove_creating_shared_worker(&shared_worker_key);
664 SharedWorker::queue_simple_error(global, worker_addr);
665 return Ok(worker);
666 },
667 };
668
669 let Ok(worker_is_secure_context) = setup_receiver.recv() else {
671 remove_creating_shared_worker(&shared_worker_key);
672 SharedWorker::queue_simple_error(global, worker_addr);
673 return Ok(worker);
674 };
675
676 let registration = SharedWorkerRegistration {
677 id: registration_id,
678 worker_type,
679 credentials,
680 extended_lifetime,
681 worker_is_secure_context,
682 closing,
683 sender,
684 _control_sender: control_sender,
685 };
686
687 if !transition_creating_to_created(&shared_worker_key, registration.clone()) {
688 SharedWorker::queue_simple_error(global, worker_addr);
689 return Ok(worker);
690 }
691
692 if registered_sender.send(()).is_err() {
693 SharedWorker::unregister_shared_worker(registration_id);
694 SharedWorker::queue_simple_error(global, worker_addr);
695 return Ok(worker);
696 }
697
698 if send_connect_to_created_worker(®istration, initial_inside_port_impl) {
705 SharedWorker::unregister_shared_worker(registration_id);
706 SharedWorker::queue_simple_error(global, worker_addr);
707 return Ok(worker);
708 }
709
710 Ok(worker)
711 }
712
713 fn Port(&self) -> DomRoot<MessagePort> {
715 DomRoot::from_ref(&*self.port)
717 }
718
719 event_handler!(error, GetOnerror, SetOnerror);
721}
722
723impl TaskOnce for SimpleWorkerErrorHandler<SharedWorker> {
724 #[cfg_attr(crown, expect(crown::unrooted_must_root))]
725 fn run_once(self, cx: &mut JSContext) {
726 SharedWorker::dispatch_simple_error(cx, self.addr);
727 }
728}