From 4d0db1a658ca6b8ab8fa522e40a9551ec2e4fc56 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 15 Sep 2026 22:44:52 +0800 Subject: [PATCH] fix(service-worker): implement registration update jobs --- .../window_runtime/service_worker.rs | 148 ++++++++- .../context_host/service_workers.rs | 71 ++++- .../src/network/request_client.rs | 18 +- .../service_worker_internal_promise_body.rs | 5 + .../src/script_vm/tests/browser_api/misc.rs | 296 +++++++++++++++++- .../src/service_worker_runtime.rs | 4 +- .../src/service_worker_runtime/errors.rs | 2 + .../src/service_worker_runtime/host.rs | 11 + .../src/service_worker_runtime/jobs.rs | 76 ++++- .../service_worker_runtime/script_loading.rs | 114 +++++-- .../src/service_worker_runtime/service.rs | 204 +++++++++++- .../service/devtools_commands.rs | 4 +- .../service/event_dispatch.rs | 4 - .../service/event_start.rs | 6 - .../service/registration_jobs.rs | 201 ++++++++++-- .../service/worker_completion.rs | 93 +++++- .../src/service_worker_runtime/snapshots.rs | 1 - .../start_completion.rs | 6 +- .../src/service_worker_runtime/state.rs | 1 - .../src/worker/global_scope/import_scripts.rs | 20 +- .../src/worker/global_scope/mod.rs | 239 ++++++++++++++ moli-renderer-v8/src/worker/handle.rs | 18 ++ moli-renderer-v8/src/worker/mod.rs | 3 + .../src/worker/thread/dispatch.rs | 1 + moli-renderer-v8/src/worker/thread/mod.rs | 49 +++ 25 files changed, 1475 insertions(+), 120 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs index 02186b0717..34465fabaa 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs @@ -15,6 +15,7 @@ use crate::worker::WorkerScriptKind; use moli_webapi_declare::{ObjectLiteralDeclaration, WebApiObject, WebApiObjectDeclaration}; const SERVICE_WORKER_REGISTRATION_SCOPE_SLOT: &str = "__moliServiceWorkerRegistrationScope"; +const SERVICE_WORKER_REGISTRATION_ID_SLOT: &str = "__moliServiceWorkerRegistrationId"; const SERVICE_WORKER_REGISTRATION_EVENTS_SLOT: &str = "__moliServiceWorkerRegistrationEvents"; const SERVICE_WORKER_REGISTRATION_WORKER_SLOT: &str = "__moliServiceWorkerRegistrationWorker"; const SERVICE_WORKER_REGISTRATION_WORKERS_SLOT: &str = "__moliServiceWorkerRegistrationWorkers"; @@ -64,6 +65,9 @@ struct ServiceWorkerRegistrationObjectDeclaration<'scope> { #[webapi(method, callback = navigator_service_worker_unregister_callback, length = 0)] unregister: (), + #[webapi(method, callback = service_worker_registration_update_callback, length = 0)] + update: (), + #[webapi( method = "showNotification", callback = service_worker_registration_show_notification_callback, @@ -513,6 +517,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<' scope, scope_url.clone(), resolver, + None, request_context.owner(), ); host.start_service_worker_runtime( @@ -532,6 +537,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<' pub(crate) fn settle_service_worker_register_completion<'s>( scope: &mut v8::PinScope<'s, '_>, resolver: v8::Local<'s, v8::PromiseResolver>, + update_registration: Option>, owner: OwnerDispatchScope, scope_url: &url::Url, result: std::result::Result< @@ -547,6 +553,16 @@ pub(crate) fn settle_service_worker_register_completion<'s>( .or_else(|| state.installing()) .map(|version| version.script_url().as_str()) .unwrap_or_else(|| state.scope_url().as_str()); + if let Some(registration) = update_registration { + update_service_worker_registration_object( + scope, + registration, + script_url, + ServiceWorkerRegistrationPhase::Snapshot(&state), + ); + let _ = resolver.resolve(scope, registration.into()); + return; + } let registration = if let Some(container) = service_worker_container_for_owner(scope, owner) { build_service_worker_registration_object_for_container( @@ -673,6 +689,11 @@ pub(crate) fn dispatch_service_worker_lifecycle_notification( notification.document_owner, ); for (owner, registration) in registrations { + if service_worker_registration_id_from_object(scope, registration) + != Some(notification.registration.registration_id()) + { + continue; + } let previous_owner_context = owner.enter(scope); update_service_worker_registration_object( scope, @@ -1010,12 +1031,20 @@ fn remember_service_worker_container_registration<'s>( return; }; let scope_url = scope_url.to_rust_string_lossy(scope); - if service_worker_container_cached_registration_for_scope(scope, container, &scope_url) - .is_some() - { - return; - } let cache = service_worker_container_registration_cache(scope, container); + if let Some(previous) = + service_worker_container_cached_registration_for_scope(scope, container, &scope_url) + { + for index in 0..cache.length() { + if cache + .get_index(scope, index) + .is_some_and(|value| value.strict_equals(previous.into())) + { + let _ = cache.set_index(scope, index, registration.into()); + return; + } + } + } array_push_value(scope, cache, registration.into()); } @@ -1310,6 +1339,95 @@ fn service_worker_container_owner_scope<'s>( service_worker_owner_scope_from_object(scope, container) } +fn service_worker_registration_id_from_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + registration: v8::Local<'s, v8::Object>, +) -> Option { + let value = get_private_value(scope, registration, SERVICE_WORKER_REGISTRATION_ID_SLOT)?; + let value = v8::Local::::try_from(value).ok()?; + let (value, lossless) = value.u64_value(); + lossless.then(|| { + crate::service_worker_runtime::ServiceWorkerRegistrationId::from_u64_for_binding(value) + }) +} + +fn service_worker_registration_update_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some(resolver) = v8::PromiseResolver::new(scope) else { + return; + }; + rv.set(resolver.get_promise(scope).into()); + let Some(registration_id) = service_worker_registration_id_from_object(scope, args.this()) + else { + reject_service_worker_promise_with_type_error( + scope, + resolver, + "Illegal invocation of ServiceWorkerRegistration.update", + ); + return; + }; + let Some(scope_url) = service_worker_registration_scope_from_this(scope, args.this()) else { + reject_service_worker_promise_with_dom_exception( + scope, + resolver, + "The registration is unavailable.", + "InvalidStateError", + ); + return; + }; + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + reject_service_worker_promise_with_dom_exception( + scope, + resolver, + "The registration owner is unavailable.", + "InvalidStateError", + ); + return; + }; + let host = unsafe { &mut *host_ptr }; + let owner = service_worker_owner_scope_from_object(scope, args.this()); + let Some(request_context) = host.service_worker_window_request_context(owner) else { + reject_service_worker_promise_with_dom_exception( + scope, + resolver, + "The registration Document is no longer active.", + "InvalidStateError", + ); + return; + }; + let Some(request_client) = host + .document_resource_loader_for_window_owner(request_context.owner().window_document_owner()) + .map(|loader| loader.request_client().clone()) + else { + reject_service_worker_promise_with_dom_exception( + scope, + resolver, + "The registration Document is no longer active.", + "InvalidStateError", + ); + return; + }; + let (request_id, document_owner, completion_tx) = host + .register_pending_service_worker_register( + scope, + scope_url, + resolver, + Some(args.this()), + request_context.owner(), + ); + host.start_service_worker_registration_update( + registration_id, + &request_context, + request_client, + request_id, + document_owner, + completion_tx, + ); +} + fn navigator_service_worker_unregister_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -2364,6 +2482,13 @@ fn build_service_worker_registration_object<'s>( navigation_preload, ); let registration = bind_declared_service_worker_object(scope, &declaration); + let id = v8::BigInt::new_from_u64(scope, resolved.registration_id.as_u64()); + set_private_value( + scope, + registration, + SERVICE_WORKER_REGISTRATION_ID_SLOT, + id.into(), + ); mark_service_worker_registration_event_target(scope, registration); if let Some(scope_value) = v8_string(scope, scope_url) { define_non_enumerable_value_property( @@ -2408,6 +2533,12 @@ fn build_service_worker_registration_object_for_container<'s>( let owner = service_worker_container_owner_scope(scope, container); if let Some(registration) = service_worker_container_cached_registration_for_scope(scope, container, scope_url) + && match &phase { + ServiceWorkerRegistrationPhase::Snapshot(snapshot) => { + service_worker_registration_id_from_object(scope, registration) + == Some(snapshot.registration_id()) + } + } { service_worker_worker_set_owner_scope(scope, registration, owner); update_service_worker_registration_object(scope, registration, script_url, phase); @@ -2441,6 +2572,7 @@ fn watch_service_worker_registration_object_lifecycle<'s>( } struct ResolvedServiceWorkerRegistrationPhase<'s> { + registration_id: crate::service_worker_runtime::ServiceWorkerRegistrationId, installing: v8::Local<'s, v8::Value>, waiting: v8::Local<'s, v8::Value>, active: v8::Local<'s, v8::Value>, @@ -2496,6 +2628,7 @@ fn resolve_service_worker_registration_phase<'s>( }) .unwrap_or_else(|| build_service_worker_object(scope, script_url, "redundant")); ResolvedServiceWorkerRegistrationPhase { + registration_id: snapshot.registration_id(), installing, waiting, active, @@ -2533,6 +2666,11 @@ fn update_service_worker_registration_object<'s>( ) { let resolved = resolve_service_worker_registration_phase(scope, Some(registration), script_url, phase); + if service_worker_registration_id_from_object(scope, registration) + != Some(resolved.registration_id) + { + return; + } set_service_worker_registration_worker_values( scope, registration, diff --git a/moli-renderer-v8/src/native_bridge/context_host/service_workers.rs b/moli-renderer-v8/src/native_bridge/context_host/service_workers.rs index 55e248c75e..1b5ffe24e1 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/service_workers.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/service_workers.rs @@ -70,6 +70,7 @@ pub(crate) struct PendingServiceWorkerRegister { pub(crate) scope_url: Url, pub(crate) context: v8::Global, pub(crate) resolver: v8::Global, + pub(crate) update_registration: Option>, } pub(crate) struct PendingServiceWorkerReady { @@ -444,6 +445,7 @@ impl JsContextHost { scope: &mut v8::PinScope<'_, '_>, scope_url: Url, resolver: v8::Local<'_, v8::PromiseResolver>, + update_registration: Option>, owner: ServiceWorkerWindowOwner, ) -> ( u64, @@ -458,6 +460,8 @@ impl JsContextHost { scope_url, context: v8::Global::new(scope, scope.get_current_context()), resolver: v8::Global::new(scope, resolver), + update_registration: update_registration + .map(|registration| v8::Global::new(scope, registration)), }, ); tracing::debug!( @@ -673,6 +677,54 @@ impl JsContextHost { self.watch_pending_service_worker_ready(); } + pub(crate) fn start_service_worker_registration_update( + &mut self, + registration_id: crate::service_worker_runtime::ServiceWorkerRegistrationId, + request_context: &ServiceWorkerWindowRequestContext, + request_client: crate::network::ResourceRequestClient, + request_id: u64, + document_owner: WindowDocumentOwner, + completion_tx: crate::page_task_queue::RendererPageServiceWorkerTaskSender, + ) { + self.ensure_service_worker_client_for_request_context(request_context); + self.browser_context_runtime + .service_worker_runtime() + .start_registration_update( + crate::service_worker_runtime::ServiceWorkerRegistrationUpdate { + registration_id, + caller_version_id: None, + storage_key: request_context.serialized_storage_key(), + document_url: request_context.document_url().clone(), + request_client, + network_policy: WorkerNetworkPolicy { + secure_context: moli_url::is_potentially_trustworthy_url( + request_context.document_url(), + ), + permission_overrides: self.permission_overrides().to_vec(), + extra_http_headers: self.extra_http_headers().to_vec(), + network_offline: self.network_offline(), + blocked_url_patterns: self.blocked_url_patterns().to_vec(), + network_partition_key: None, + fetch_subresource_interception_enabled: self + .fetch_subresource_interception_enabled(), + fetch_subresource_interception_resource_type: self + .fetch_subresource_interception_resource_type(), + }, + worker_context_runtime: self.browser_context_runtime.worker_context_runtime(), + broadcast_channel_top_level_site: Some( + request_context.storage_key_top_level_site(), + ), + indexed_db_manager: self.indexed_db_manager(), + storage_bucket_store: Some(self.storage_bucket_store()), + completion: crate::service_worker_runtime::ServiceWorkerRegisterJob::Page { + request_id, + document_owner, + completion_tx, + }, + }, + ); + } + pub(crate) fn watch_pending_service_worker_ready(&mut self) -> bool { let requests = self.pending_service_worker_ready_requests(); let mut attached_any = false; @@ -829,16 +881,17 @@ impl JsContextHost { let owner = request_context.owner(); let storage_key = request_context.serialized_storage_key(); self.compact_service_worker_registration_watchers(); - if self - .service_worker_registration_watchers - .iter() - .any(|watcher| { - watcher.owner == owner - && watcher.scope_url == scope_url - && watcher.storage_key == storage_key - && !watcher.registration.is_empty() - }) + if let Some(watcher) = + self.service_worker_registration_watchers + .iter_mut() + .find(|watcher| { + watcher.owner == owner + && watcher.scope_url == scope_url + && watcher.storage_key == storage_key + && !watcher.registration.is_empty() + }) { + watcher.registration = v8::Weak::new(scope, registration); return; } self.service_worker_registration_watchers diff --git a/moli-renderer-v8/src/network/request_client.rs b/moli-renderer-v8/src/network/request_client.rs index b4e423c027..0fc900edff 100644 --- a/moli-renderer-v8/src/network/request_client.rs +++ b/moli-renderer-v8/src/network/request_client.rs @@ -291,7 +291,10 @@ impl ResourceRequestClient { cancel_handle: FetchCancelHandle, ) -> Result> { let request = self.apply_network_policy(request)?; - if request.auth_requires_buffered_transport() || !request.follow_redirects { + if request.auth_requires_buffered_transport() + || (!request.follow_redirects + && request.redirect_mode != moli_fetch::RequestRedirectMode::Error) + { return self .resource_runtime .client() @@ -714,12 +717,16 @@ impl ResourceRequestClient { request: Request, cancel_handle: FetchCancelHandle, ) -> Result { - if request.auth_requires_buffered_transport() || !request.follow_redirects { + if request.auth_requires_buffered_transport() + || (!request.follow_redirects + && request.redirect_mode != moli_fetch::RequestRedirectMode::Error) + { // Challenge-response schemes still need libcurl's buffered auth // retry behavior until the streaming collector models // intermediate authentication challenges explicitly. Manual // redirect callers need the intermediate 3xx response before raw - // streaming starts. + // streaming starts. Error-mode script fetches can use the raw + // stream and its HTTP cache; their caller rejects any 3xx status. return self .resource_runtime .client() @@ -999,7 +1006,10 @@ impl ResourceRequestClient { cancel_handle: Option, ) -> Result { let request = self.apply_network_policy(request)?; - if request.auth_requires_buffered_transport() || !request.follow_redirects { + if request.auth_requires_buffered_transport() + || (!request.follow_redirects + && request.redirect_mode != moli_fetch::RequestRedirectMode::Error) + { // Digest auth retries are still completed inside libcurl on the // buffered path. Keep auth requests there until the streaming // collector can distinguish intermediate auth challenges from diff --git a/moli-renderer-v8/src/script_vm/service_worker_internal_promise_body.rs b/moli-renderer-v8/src/script_vm/service_worker_internal_promise_body.rs index 4767a48014..fb70fd98af 100644 --- a/moli-renderer-v8/src/script_vm/service_worker_internal_promise_body.rs +++ b/moli-renderer-v8/src/script_vm/service_worker_internal_promise_body.rs @@ -45,12 +45,17 @@ impl ScriptVm { let context = pending.context; let resolver = pending.resolver; let scope_url = pending.scope_url; + let update_registration = pending.update_registration; let context_ptr: *const v8::Global = &context; self.with_context_scope_by_ptr(context_ptr, move |scope, _host_ptr| { let resolver = v8::Local::new(scope, &resolver); + let update_registration = update_registration + .as_ref() + .map(|value| v8::Local::new(scope, value)); crate::context_bootstrap::settle_service_worker_register_completion( scope, resolver, + update_registration, owner.dispatch_scope(), &scope_url, completion.result, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index b3bc39b384..645cc590d9 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -15655,7 +15655,7 @@ async fn navigator_service_worker_intercepts_connected_stylesheet_link() { } #[tokio::test] -async fn navigator_service_worker_update_check_failure_rejects_register_with_type_error() { +async fn navigator_service_worker_update_check_preserves_mime_security_error() { let (base_url, server) = spawn_service_worker_response_server_with_headers(vec![ ( "/app/worker.js", @@ -15717,7 +15717,7 @@ async fn navigator_service_worker_update_check_failure_rejects_register_with_typ &browser_context_runtime, &loader, "String(globalThis.__serviceWorkerUpdateFailureProbe)", - r#"{"name":"TypeError","isTypeError":true,"isDomException":false,"messageIncludesNosniff":true}"#, + r#"{"name":"SecurityError","isTypeError":false,"isDomException":true,"messageIncludesNosniff":true}"#, ) .await; @@ -18888,6 +18888,263 @@ async fn navigator_service_worker_update_via_cache_option_reflects_registration( .expect("service worker updateViaCache script server should finish"); } +#[tokio::test] +async fn navigator_service_worker_update_preserves_identity_and_rejects_stale_receivers() { + let (base_url, server) = spawn_service_worker_response_server(vec![ + ("/app/workers/sw.js", "text/javascript", "// first"), + ("/app/workers/sw.js", "text/javascript", "// second"), + ("/app/workers/sw.js", "text/javascript", "// second"), + ( + "/app/workers/sw.js", + "text/javascript", + "// new registration", + ), + ("/app/workers/sw.js", "text/javascript", "// new update"), + ]) + .await; + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let (mut vm, runtime) = new_service_worker_page_test_vm_with_loader_and_browser_context_runtime( + &format!("{base_url}/app/page.html"), + &loader, + ); + vm.eval(r#" + globalThis.__updateIdentity = 'pending'; + (async () => { + const activated = async r => { + const w = r.installing || r.waiting || r.active; + if (w.state !== 'activated') await new Promise(resolve => w.addEventListener('statechange', () => { + if (w.state === 'activated') resolve(); + })); + }; + const errorName = async p => { try { await p; return 'success'; } catch(e) { return e.name; } }; + const register = () => navigator.serviceWorker.register('workers/sw.js', {scope:'workers/'}); + const r = await register(); + let found = 0; + r.addEventListener('updatefound', () => found++); + await activated(r); + const initial = found; + const receivers = []; + for (const fake of [{}, Object.create(r), new Proxy(r, {}), null]) { + const p = Reflect.apply(r.update, fake, []); + receivers.push(p instanceof Promise && await errorName(p) === 'TypeError'); + } + const before = r.active; + const order = []; + r.addEventListener('updatefound', () => order.push('event'), {once:true}); + const updated = await r.update(); order.push('promise'); + await activated(r); + const changed = before !== r.active; + const current = r.active; + const unchanged = await r.update({get unused(){throw new Error('argument read');}}); + const stable = unchanged === r && current === r.active && found === 2; + await r.unregister(); + const removed = await errorName(r.update()); + const replacement = await register(); + await activated(replacement); + const stale = await errorName(r.update()); + const again = await replacement.update(); + await activated(replacement); + const lookup = await navigator.serviceWorker.getRegistration('workers/client'); + globalThis.__updateIdentity = [r.update.name, r.update.length, initial, + receivers.every(Boolean), updated === r, changed, order.join(','), stable, + removed, stale, replacement !== r, again === replacement, lookup === replacement].join('|'); + await replacement.unregister(); + })().catch(e => globalThis.__updateIdentity = 'error:' + e); + "#).expect("update identity probe"); + drain_service_worker_test_until_eval_equals(&mut vm, &runtime, &loader, + "String(globalThis.__updateIdentity)", + "update|0|1|true|true|true|promise,event|true|InvalidStateError|InvalidStateError|true|true|true", + ).await; + server.await.expect("update identity server"); +} + +#[tokio::test] +async fn navigator_service_worker_update_reuses_failed_import_responses() { + const MAIN: &str = "importScripts('a.js', 'z.js');"; + let (base_url, server) = spawn_service_worker_response_server(vec![ + ("/app/workers/sw.js", "text/javascript", MAIN), + ("/app/workers/a.js", "text/javascript", "// a"), + ("/app/workers/z.js", "text/javascript", "// z1"), + ("/app/workers/sw.js", "text/javascript", MAIN), + ("/app/workers/a.js", "text/html", "missing import"), + ("/app/workers/z.js", "text/javascript", "// z2"), + ( + "/app/workers/sw.js", + "text/javascript", + "importScripts('z.js');", + ), + ("/app/workers/z.js", "text/javascript", "// z3"), + ]) + .await; + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let (mut vm, runtime) = new_service_worker_page_test_vm_with_loader_and_browser_context_runtime( + &format!("{base_url}/app/page.html"), + &loader, + ); + vm.eval(r#" + globalThis.__updateImports = 'pending'; + (async () => { + const activated = async r => { + const w = r.installing || r.waiting || r.active; + if (w.state !== 'activated') await new Promise(resolve => w.addEventListener('statechange', () => { + if (w.state === 'activated') resolve(); + })); + }; + const r = await navigator.serviceWorker.register('workers/sw.js', {scope:'workers/'}); + await activated(r); + const before = r.active; + let found = 0; + r.addEventListener('updatefound', () => found++); + let error = 'success'; + try { await r.update(); } catch(e) { error = e.name; } + const failed = error === 'TypeError' && found === 0 && r.active === before && r.installing === null; + const updated = await r.update(); + await activated(r); + globalThis.__updateImports = [failed, updated === r, r.active !== before, found].join('|'); + await r.unregister(); + })().catch(e => globalThis.__updateImports = 'error:' + e); + "#).expect("update import response probe"); + drain_service_worker_test_until_eval_equals( + &mut vm, + &runtime, + &loader, + "String(globalThis.__updateImports)", + "true|true|true|1", + ) + .await; + server + .await + .expect("update import server should not refetch failed responses"); +} + +#[tokio::test] +async fn navigator_service_worker_update_discards_prefetched_but_unused_imports_after_install() { + const MAIN: &str = r#" + importScripts('flag.js'); + if (!self.skipOld) importScripts('unused.js'); + onmessage = e => { + try { importScripts('unused.js'); e.ports[0].postMessage('success'); } + catch(error) { e.ports[0].postMessage(error.name); } + }; + "#; + let (base_url, server) = spawn_service_worker_response_server(vec![ + ("/app/workers/sw.js", "text/javascript", MAIN), + ( + "/app/workers/flag.js", + "text/javascript", + "self.skipOld = false;", + ), + ("/app/workers/unused.js", "text/javascript", "// unused"), + ("/app/workers/sw.js", "text/javascript", MAIN), + ( + "/app/workers/flag.js", + "text/javascript", + "self.skipOld = true;", + ), + ("/app/workers/unused.js", "text/javascript", "// unused"), + ]) + .await; + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let (mut vm, runtime) = new_service_worker_page_test_vm_with_loader_and_browser_context_runtime( + &format!("{base_url}/app/page.html"), + &loader, + ); + vm.eval(r#" + globalThis.__unusedUpdateImport = 'pending'; + (async () => { + const activated = async r => { + const w = r.installing || r.waiting || r.active; + if (w.state !== 'activated') await new Promise(resolve => w.addEventListener('statechange', () => { + if (w.state === 'activated') resolve(); + })); + }; + const r = await navigator.serviceWorker.register('workers/sw.js', {scope:'workers/'}); + await activated(r); + await r.update(); await activated(r); + const value = await new Promise(resolve => { + const c = new MessageChannel(); + c.port1.onmessage = e => { c.port1.close(); resolve(e.data); }; + r.active.postMessage('probe', [c.port2]); + }); + globalThis.__unusedUpdateImport = value; + await r.unregister(); + })().catch(e => globalThis.__unusedUpdateImport = 'error:' + e); + "#).expect("unused update import probe"); + drain_service_worker_test_until_eval_equals( + &mut vm, + &runtime, + &loader, + "String(globalThis.__unusedUpdateImport)", + "NetworkError", + ) + .await; + server + .await + .expect("prefetched imports should not be fetched again"); +} + +#[tokio::test] +async fn navigator_service_worker_update_in_worker_preserves_events_and_install_rejection() { + let (base_url, server) = spawn_service_worker_response_server(vec![ + ("/app/workers/sw.js", "text/javascript", r#" + const original = registration; + const seen = []; + let installError; + original.addEventListener('updatefound', () => seen.push('before')); + original.onupdatefound = function(e) { + seen.push([this === original, e.target === original, e.currentTarget === original, + e instanceof Event, e.isTrusted, e.bubbles, e.cancelable].join(':')); + }; + original.addEventListener('updatefound', () => seen.push('after')); + oninstall = e => e.waitUntil(original.update().then( + () => installError = 'success', e => installError = e.name)); + onactivate = () => seen.push('activate'); + onmessage = e => { + if (e.data === 'sample') e.ports[0].postMessage(seen.join(',') + '|' + installError); + else e.waitUntil(original.update().then(r => e.ports[0].postMessage(r === original))); + }; + Object.defineProperty(self, 'registration', {get(){throw new Error('public registration read');}}); + "#), + ("/app/workers/sw.js", "text/javascript", "// updated"), + ]).await; + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let (mut vm, runtime) = new_service_worker_page_test_vm_with_loader_and_browser_context_runtime( + &format!("{base_url}/app/page.html"), + &loader, + ); + vm.eval( + r#" + globalThis.__workerUpdate = 'pending'; + (async () => { + const r = await navigator.serviceWorker.register('workers/sw.js', {scope:'workers/'}); + const w = r.installing; + await new Promise(resolve => w.addEventListener('statechange', () => { + if (w.state === 'activated') resolve(); + })); + const message = data => new Promise(resolve => { + const c = new MessageChannel(); + c.port1.onmessage = e => { c.port1.close(); resolve(e.data); }; + w.postMessage(data, [c.port2]); + }); + const first = await message('sample'); + const same = await message('update'); + globalThis.__workerUpdate = first + '|' + same; + await r.unregister(); + })().catch(e => globalThis.__workerUpdate = 'error:' + e); + "#, + ) + .expect("worker update probe"); + drain_service_worker_test_until_eval_equals( + &mut vm, + &runtime, + &loader, + "String(globalThis.__workerUpdate)", + "before,true:true:true:true:true:false:false,after,activate|InvalidStateError|true", + ) + .await; + server.await.expect("worker update server"); +} + #[tokio::test] async fn navigator_service_worker_update_via_cache_all_uses_fresh_main_script_cache() { let cache_dir = service_worker_http_cache_test_root("update-via-cache-all"); @@ -18907,12 +19164,33 @@ async fn navigator_service_worker_update_via_cache_all_uses_fresh_main_script_ca .await; let mut fetch_config = moli_fetch::FetchConfig::default(); fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string())); - let loader = ResourceRequestClient::new(&fetch_config).expect("loader"); - let (mut vm, browser_context_runtime) = - new_service_worker_page_test_vm_with_loader_and_browser_context_runtime( - &format!("{base_url}/app/page.html"), - &loader, - ); + let browser_context_runtime = crate::runtime::RendererBrowserContextRuntime::new(); + let resource_runtime = browser_context_runtime + .replace_browser_resource_runtime(crate::network::BrowserResourceRuntimeOwner::new( + &fetch_config, + moli_cookie_jar::new_shared_browser_cookie_store(), + )) + .expect("cache-enabled browser resource runtime"); + let loader = ResourceRequestClient::from_browser_resource_runtime(resource_runtime); + let mut vm = crate::runtime::PageVmTaskExecutorTestHarness::new_with_browser_context_runtime( + url::Url::parse(&format!("{base_url}/app/page.html")).unwrap(), + &loader, + browser_context_runtime.handle(), + ); + // Execute register and update with the same Document transport, including + // its browser-site context used to partition the HTTP cache. + let loader = vm + ._context_host + .borrow() + .current_main_document_resource_loader() + .expect("current Document loader") + .request_client() + .clone(); + assert!( + loader + .browser_resource_runtime() + .matches_fetch_config(&fetch_config) + ); vm.eval( r#" @@ -18933,6 +19211,8 @@ async fn navigator_service_worker_update_via_cache_all_uses_fresh_main_script_ca scope: "./", updateViaCache: "all" }); + const updated = await registration.update(); + if (updated !== registration) throw new Error('update identity changed'); const all = await sw.getRegistrations(); globalThis.__serviceWorkerUpdateViaCacheAllProbe = [ updatefoundCount, diff --git a/moli-renderer-v8/src/service_worker_runtime.rs b/moli-renderer-v8/src/service_worker_runtime.rs index 3b9b964ac5..04b28fcd92 100644 --- a/moli-renderer-v8/src/service_worker_runtime.rs +++ b/moli-renderer-v8/src/service_worker_runtime.rs @@ -66,7 +66,9 @@ pub(crate) use ids::{ ServiceWorkerClientId, ServiceWorkerClientIdAllocator, ServiceWorkerEventId, ServiceWorkerRegistrationId, ServiceWorkerVersionId, }; -pub(crate) use jobs::ServiceWorkerUnregisterStart; +pub(crate) use jobs::{ + ServiceWorkerRegisterJob, ServiceWorkerRegistrationUpdate, ServiceWorkerUnregisterStart, +}; pub(crate) use owner_wake::{ ServiceWorkerRuntimeOwnerWake, ServiceWorkerRuntimeOwnerWakeSender, service_worker_owner_wake_channel, diff --git a/moli-renderer-v8/src/service_worker_runtime/errors.rs b/moli-renderer-v8/src/service_worker_runtime/errors.rs index b3a12b06d6..b538431695 100644 --- a/moli-renderer-v8/src/service_worker_runtime/errors.rs +++ b/moli-renderer-v8/src/service_worker_runtime/errors.rs @@ -2,6 +2,7 @@ pub(crate) enum ServiceWorkerRegistrationErrorKind { Abort, Install, + InvalidState, Network, NotFound, ScriptEvaluateFailed, @@ -15,6 +16,7 @@ impl ServiceWorkerRegistrationErrorKind { match self { Self::Abort | Self::Install | Self::ScriptEvaluateFailed => "AbortError", Self::Network => "NetworkError", + Self::InvalidState => "InvalidStateError", Self::NotFound => "NotFoundError", Self::Security => "SecurityError", Self::Type => unreachable!("type errors are not DOMExceptions"), diff --git a/moli-renderer-v8/src/service_worker_runtime/host.rs b/moli-renderer-v8/src/service_worker_runtime/host.rs index cb844506c0..4f811b0046 100644 --- a/moli-renderer-v8/src/service_worker_runtime/host.rs +++ b/moli-renderer-v8/src/service_worker_runtime/host.rs @@ -227,6 +227,16 @@ impl RendererServiceWorkerHost { true } + pub(super) fn dispatch_registration_update_found(&self) { + let state = self.state.lock(); + if let RendererServiceWorkerHostState::Running { + handle: Some(handle), + } = &*state + { + handle.dispatch_service_worker_registration_update_found(); + } + } + pub(super) fn dispatch_fetch_event(&self, event: ServiceWorkerFetchEvent) -> bool { let state = self.state.lock(); let RendererServiceWorkerHostState::Running { @@ -991,6 +1001,7 @@ fn spawn_service_worker( script_map.imported_scripts, script_map.can_import_new_scripts, ) + .with_service_worker_updated_script_resources(script.updated_imports) .with_global_kind(crate::worker::WorkerGlobalKind::Service { registration_id: params.registration_id, version_id: params.run_owner.version_id(), diff --git a/moli-renderer-v8/src/service_worker_runtime/jobs.rs b/moli-renderer-v8/src/service_worker_runtime/jobs.rs index 6c8855266f..9b312008f4 100644 --- a/moli-renderer-v8/src/service_worker_runtime/jobs.rs +++ b/moli-renderer-v8/src/service_worker_runtime/jobs.rs @@ -6,7 +6,7 @@ use url::Url; use crate::{ network::{BrowserResourceRuntimeBinding, ResourceRequestClient}, page_task_queue::RendererPageServiceWorkerTaskSender, - runtime::{RendererBrowserContextRuntime, RendererWorkerContextRuntime}, + runtime::RendererWorkerContextRuntime, types::{ServiceWorkerRegisterCompletion, ServiceWorkerUnregisterCompletion}, worker::{WorkerNetworkPolicy, WorkerScriptKind}, }; @@ -39,10 +39,16 @@ pub(crate) struct ServiceWorkerLaunchParams { } #[derive(Clone, Debug)] -pub(super) struct ServiceWorkerRegisterJob { - pub(super) request_id: u64, - pub(super) document_owner: crate::window_document_identity::WindowDocumentOwner, - pub(super) completion_tx: RendererPageServiceWorkerTaskSender, +pub(crate) enum ServiceWorkerRegisterJob { + Page { + request_id: u64, + document_owner: crate::window_document_identity::WindowDocumentOwner, + completion_tx: RendererPageServiceWorkerTaskSender, + }, + Worker { + request_id: u64, + completion_tx: tokio::sync::mpsc::UnboundedSender, + }, } impl ServiceWorkerRegisterJob { @@ -53,13 +59,30 @@ impl ServiceWorkerRegisterJob { ServiceWorkerRegistrationError, >, ) { - let _ = self - .completion_tx - .send_service_worker_register(ServiceWorkerRegisterCompletion { - request_id: self.request_id, - document_owner: self.document_owner, - result, - }); + match self { + Self::Page { + request_id, + document_owner, + completion_tx, + } => { + let _ = + completion_tx.send_service_worker_register(ServiceWorkerRegisterCompletion { + request_id, + document_owner, + result, + }); + } + Self::Worker { + request_id, + completion_tx, + } => { + let _ = + completion_tx.send(crate::worker::WorkerMessage::ServiceWorkerUpdateResult { + request_id, + result: Box::new(result), + }); + } + } } pub(super) fn send_all( @@ -75,6 +98,20 @@ impl ServiceWorkerRegisterJob { } } +pub(crate) struct ServiceWorkerRegistrationUpdate { + pub(crate) registration_id: ServiceWorkerRegistrationId, + pub(crate) caller_version_id: Option, + pub(crate) storage_key: String, + pub(crate) document_url: Url, + pub(crate) request_client: ResourceRequestClient, + pub(crate) network_policy: WorkerNetworkPolicy, + pub(crate) worker_context_runtime: RendererWorkerContextRuntime, + pub(crate) broadcast_channel_top_level_site: Option, + pub(crate) indexed_db_manager: Option, + pub(crate) storage_bucket_store: Option, + pub(crate) completion: ServiceWorkerRegisterJob, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum ServiceWorkerRegisterJobPhase { Initial, @@ -89,6 +126,7 @@ pub(super) enum ServiceWorkerRegisterJobPhase { #[derive(Clone, Debug)] pub(super) struct ServiceWorkerPendingRegisterJob { + pub(super) is_update: bool, phase: ServiceWorkerRegisterJobPhase, skip_waiting_after_install: bool, callbacks: Vec, @@ -109,6 +147,7 @@ impl ServiceWorkerPendingRegisterJob { ) -> Self { Self { phase: ServiceWorkerRegisterJobPhase::Initial, + is_update: false, skip_waiting_after_install, callbacks, resolved_result: None, @@ -250,7 +289,7 @@ mod tests { #[test] fn pending_register_job_resolves_when_install_starts() { let queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new(); - let mut job = ServiceWorkerPendingRegisterJob::new(vec![ServiceWorkerRegisterJob { + let mut job = ServiceWorkerPendingRegisterJob::new(vec![ServiceWorkerRegisterJob::Page { request_id: 1, document_owner: crate::window_document_identity::WindowDocumentOwner::for_test(1), completion_tx: queue.sender(), @@ -353,6 +392,9 @@ impl ServiceWorkerQueuedUnregisterJob { #[derive(Clone)] pub(super) struct ServiceWorkerQueuedRegisterJob { + /// Web API update jobs retain their registration identity and cannot + /// create a new registration or clear a pending unregistration. + pub(super) update_registration_id: Option, pub(super) script_url: Url, pub(super) scope_url: Url, pub(super) document_url: Url, @@ -365,7 +407,7 @@ pub(super) struct ServiceWorkerQueuedRegisterJob { pub(super) force_update_page_load_waiter_ids: Vec, pub(super) request_client: ResourceRequestClient, pub(super) network_policy: WorkerNetworkPolicy, - pub(super) browser_context_runtime: RendererBrowserContextRuntime, + pub(super) worker_context_runtime: RendererWorkerContextRuntime, pub(super) broadcast_channel_top_level_site: Option, pub(super) indexed_db_manager: Option, pub(super) storage_bucket_store: Option, @@ -381,7 +423,8 @@ impl ServiceWorkerQueuedRegisterJob { } pub(super) fn matches_registration_job(&self, other: &Self) -> bool { - self.scope_url == other.scope_url + self.update_registration_id == other.update_registration_id + && self.scope_url == other.scope_url && self.storage_key == other.storage_key && self.script_url == other.script_url && self.script_kind == other.script_kind @@ -461,6 +504,7 @@ pub(super) enum ServiceWorkerMainScriptUpdateCheckStart { impl std::fmt::Debug for ServiceWorkerQueuedRegisterJob { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ServiceWorkerQueuedRegisterJob") + .field("update_registration_id", &self.update_registration_id) .field("script_url", &self.script_url) .field("scope_url", &self.scope_url) .field("document_url", &self.document_url) @@ -718,7 +762,7 @@ impl ServiceWorkerVersionLaunchConfig { document_url: job.document_url.clone(), request_client: ServiceWorkerRequestClientSource::Captured(job.request_client.clone()), network_policy: job.network_policy.clone(), - worker_context_runtime: job.browser_context_runtime.worker_context_runtime(), + worker_context_runtime: job.worker_context_runtime.clone(), broadcast_channel_top_level_site: job.broadcast_channel_top_level_site.clone(), indexed_db_manager: job.indexed_db_manager.clone(), storage_bucket_store: job.storage_bucket_store.clone(), diff --git a/moli-renderer-v8/src/service_worker_runtime/script_loading.rs b/moli-renderer-v8/src/service_worker_runtime/script_loading.rs index cb9c8b538c..9550d125b4 100644 --- a/moli-renderer-v8/src/service_worker_runtime/script_loading.rs +++ b/moli-renderer-v8/src/service_worker_runtime/script_loading.rs @@ -2,8 +2,8 @@ use std::time::Instant; use moli_crypto::sha256_hex; use moli_fetch::{ - FetchCancelHandle, Request, RequestCacheMode, RequestCredentialsMode, ResponseHead, - ScriptFetchRequestMetadata, + FetchCancelHandle, Request, RequestCacheMode, RequestCredentialsMode, RequestMode, + RequestRedirectMode, ResponseHead, ScriptFetchRequestMetadata, }; use url::Url; @@ -12,6 +12,7 @@ use crate::network::ResourceRequestClient; use crate::worker::WorkerScriptResourceKind; use super::{ + errors::{ServiceWorkerRegistrationError, ServiceWorkerRegistrationErrorKind}, jobs::ServiceWorkerLaunchParams, path_restriction::{ service_worker_allowed_header_value, verify_service_worker_script_path_restriction, @@ -41,6 +42,7 @@ impl ServiceWorkerScriptLoadParams { #[derive(Clone)] pub(super) struct ServiceWorkerScriptUpdateCheckParams { + pub(super) script_kind: crate::worker::WorkerScriptKind, pub(super) main_script: ServiceWorkerScriptLoadParams, pub(super) newest_main_body_sha256: String, pub(super) imported_scripts: Vec, @@ -125,6 +127,7 @@ pub(super) struct ServiceWorkerScriptMapSnapshot { } pub(super) struct LoadedServiceWorkerScript { + pub(super) updated_imports: crate::worker::WorkerScriptUpdateResources, pub(super) resource: ServiceWorkerScriptResource, pub(super) source: String, pub(super) response_referrer_policy: Option, @@ -147,6 +150,7 @@ impl LoadedServiceWorkerScript { ); let response_content_security_reporting_endpoints = crate::content_security_policy::content_security_policy_reporting_endpoints_from_headers(&resource.headers, &resource.final_url); Some(Self { + updated_imports: Default::default(), resource, source, response_referrer_policy, @@ -173,6 +177,7 @@ pub(super) enum ServiceWorkerScriptUpdateCheckChange { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum ServiceWorkerScriptUpdateCheckFailureStatus { ScriptLoadFailed, + Security, Internal, Stale, } @@ -181,6 +186,7 @@ impl ServiceWorkerScriptUpdateCheckFailureStatus { pub(super) fn as_str(self) -> &'static str { match self { Self::ScriptLoadFailed => "script-load-failed", + Self::Security => "security", Self::Internal => "internal", Self::Stale => "stale", } @@ -229,18 +235,21 @@ pub(super) fn load_service_worker_script_source( load_service_worker_script_source_for_params( &ServiceWorkerScriptLoadParams::from_launch_params(params), ) + .map_err(|error| error.message) } pub(super) fn load_service_worker_script_source_for_params( params: &ServiceWorkerScriptLoadParams, -) -> Result { +) -> Result { let request_client = ¶ms.request_client; let mut request_url = params.script_url.clone(); request_url.set_fragment(None); let response_started_at = Instant::now(); let request = Request::new("GET", request_url.as_str(), None, vec![]) - .map_err(|error| error.to_string())? + .map_err(|error| ServiceWorkerRegistrationError::network(error.to_string()))? .with_credentials_mode(RequestCredentialsMode::SameOrigin) + .with_request_mode(RequestMode::SameOrigin) + .with_redirect_mode(RequestRedirectMode::Error) .with_cache_mode(params.cache_mode) .with_page_network_policy() .with_initiator_url(¶ms.document_url) @@ -251,7 +260,9 @@ pub(super) fn load_service_worker_script_source_for_params( let response = request_client .fetch_text_for_worker_blocking_boundary_with_cancel(request, FetchCancelHandle::new()) .map_err(|error| { - format!("Failed to load service worker script `{request_url}`: {error}") + ServiceWorkerRegistrationError::network(format!( + "Failed to load service worker script `{request_url}`: {error}" + )) })?; let response_time_ms = response_started_at .elapsed() @@ -263,21 +274,30 @@ pub(super) fn load_service_worker_script_source_for_params( &response.final_url, ) .map_err(|message| { - format!("Failed to load service worker script `{request_url}`: {message}") + ServiceWorkerRegistrationError::new( + ServiceWorkerRegistrationErrorKind::Security, + format!("Failed to load service worker script `{request_url}`: {message}"), + ) })?; moli_fetch::ensure_http_status_success(response.final_url.as_str(), response.status, false) - .map_err(|error| error.to_string())?; + .map_err(|error| ServiceWorkerRegistrationError::network(error.to_string()))?; crate::worker::ensure_worker_script_mime_acceptable( &response.final_url, &response.headers, response.body_bytes(), - )?; + ) + .map_err(|message| { + ServiceWorkerRegistrationError::new(ServiceWorkerRegistrationErrorKind::Security, message) + })?; let service_worker_allowed_header = service_worker_allowed_header_value(&response.headers); verify_service_worker_script_path_restriction( ¶ms.scope_url, &response.final_url, service_worker_allowed_header.as_deref(), - )?; + ) + .map_err(|message| { + ServiceWorkerRegistrationError::new(ServiceWorkerRegistrationErrorKind::Security, message) + })?; let response_referrer_policy = crate::referrer_policy::response_referrer_policy_from_headers(&response.headers); let response_content_security_policies = @@ -311,6 +331,7 @@ pub(super) fn load_service_worker_script_source_for_params( final_url.set_fragment(params.script_url.fragment()); resource.final_url = final_url; Ok(LoadedServiceWorkerScript { + updated_imports: Default::default(), resource, source: body, response_referrer_policy, @@ -323,8 +344,14 @@ pub(super) fn load_service_worker_script_source_for_params( pub(super) fn load_service_worker_script_update_check( params: &ServiceWorkerScriptUpdateCheckParams, ) -> ServiceWorkerScriptUpdateCheckCompletion { - let main_script = load_service_worker_script_source_for_params(¶ms.main_script) - .map_err(ServiceWorkerScriptUpdateCheckFailure::script_load)?; + let mut main_script = load_service_worker_script_source_for_params(¶ms.main_script) + .map_err(|error| { + let mut failure = ServiceWorkerScriptUpdateCheckFailure::script_load(error.message); + if error.kind == ServiceWorkerRegistrationErrorKind::Security { + failure.status = ServiceWorkerScriptUpdateCheckFailureStatus::Security; + } + failure + })?; if params.skip_script_comparison { return Ok(ServiceWorkerScriptUpdateCheckResult { main_script, @@ -338,22 +365,26 @@ pub(super) fn load_service_worker_script_update_check( }); } let request_client = ¶ms.main_script.request_client; + let mut change = ServiceWorkerScriptUpdateCheckChange::Identical; for imported_script in ¶ms.imported_scripts { + if !matches!(imported_script.request_url.scheme(), "http" | "https") { + continue; + } let result = load_imported_script_resource_for_update_check( request_client, &imported_script.request_url, &main_script.resource.final_url, params.imported_script_cache_mode, imported_script.kind, + params.script_kind == crate::worker::WorkerScriptKind::Classic, ); - match result { + match &result { Ok(updated_resource) if updated_resource.body_sha256 != imported_script.body_sha256 => { - return Ok(ServiceWorkerScriptUpdateCheckResult { - main_script, - change: ServiceWorkerScriptUpdateCheckChange::ImportedScriptDifferent { + if change == ServiceWorkerScriptUpdateCheckChange::Identical { + change = ServiceWorkerScriptUpdateCheckChange::ImportedScriptDifferent { script_url: imported_script.request_url.clone(), - }, - }); + }; + } } Ok(_) => {} Err(message) => { @@ -364,10 +395,16 @@ pub(super) fn load_service_worker_script_update_check( ); } } + // The candidate worker must consume these exact responses, including + // failures, rather than fetching a third version during evaluation. + main_script.updated_imports.insert( + imported_script.request_url.clone(), + result.map(|resource| resource.to_worker_script_resource()), + ); } Ok(ServiceWorkerScriptUpdateCheckResult { main_script, - change: ServiceWorkerScriptUpdateCheckChange::Identical, + change, }) } @@ -377,12 +414,18 @@ fn load_imported_script_resource_for_update_check( initiator_url: &Url, cache_mode: RequestCacheMode, kind: WorkerScriptResourceKind, + classic: bool, ) -> Result { let mut request_url_without_fragment = request_url.clone(); request_url_without_fragment.set_fragment(None); let request = Request::new("GET", request_url_without_fragment.as_str(), None, vec![]) .map_err(|error| error.to_string())? .with_credentials_mode(RequestCredentialsMode::SameOrigin) + .with_request_mode(if classic { + RequestMode::NoCors + } else { + RequestMode::Cors + }) .with_cache_mode(cache_mode) .with_page_network_policy() .with_initiator_url(initiator_url) @@ -402,7 +445,8 @@ fn load_imported_script_resource_for_update_check( .elapsed() .as_millis() .min(u64::MAX as u128) as u64; - crate::worker::ensure_worker_script_redirect_chain_same_origin( + if !classic { + crate::worker::ensure_worker_script_redirect_chain_same_origin( initiator_url, &response.redirect_chain, &response.final_url, @@ -412,19 +456,38 @@ fn load_imported_script_resource_for_update_check( "Failed to load service worker imported script `{request_url_without_fragment}`: {message}" ) })?; + } moli_fetch::ensure_http_status_success(response.final_url.as_str(), response.status, false) .map_err(|error| error.to_string())?; ensure_imported_script_resource_mime(kind, &response)?; - let (head, body_bytes) = response.into_byte_parts(); + let (head, body, body_bytes) = response.into_parts(); let mut resource = ServiceWorkerScriptResource::from_response_parts( - request_url_without_fragment, + request_url.clone(), &head, &body_bytes, response_time_ms, ); resource.kind = kind; + if classic { + resource.classic_script = Some(crate::worker::WorkerStoredClassicScript { + source: body.into(), + muted_errors: moli_url::WebOrigin::from_url(initiator_url) + != moli_url::WebOrigin::from_url(&head.final_url) + || head.redirect_chain.iter().any(|redirect| { + moli_url::WebOrigin::from_url(initiator_url) + != moli_url::WebOrigin::from_url(&redirect.to_url) + }), + redirect_urls: head + .redirect_chain + .iter() + .map(|redirect| redirect.to_url.clone()) + .collect(), + }); + } let mut final_url = head.final_url; - final_url.set_fragment(request_url.fragment()); + if !classic { + final_url.set_fragment(request_url.fragment()); + } resource.final_url = final_url; Ok(resource) } @@ -535,6 +598,7 @@ mod tests { let result = load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams { + script_kind: crate::worker::WorkerScriptKind::Classic, main_script: ServiceWorkerScriptLoadParams { script_url: script_url.clone(), scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(), @@ -576,6 +640,7 @@ mod tests { let result = load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams { + script_kind: crate::worker::WorkerScriptKind::Classic, main_script: ServiceWorkerScriptLoadParams { script_url: script_url.clone(), scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(), @@ -623,6 +688,7 @@ mod tests { let result = load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams { + script_kind: crate::worker::WorkerScriptKind::Module, main_script: ServiceWorkerScriptLoadParams { script_url, scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(), @@ -676,6 +742,7 @@ mod tests { let result = load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams { + script_kind: crate::worker::WorkerScriptKind::Module, main_script: ServiceWorkerScriptLoadParams { script_url, scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(), @@ -729,6 +796,7 @@ mod tests { let result = load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams { + script_kind: crate::worker::WorkerScriptKind::Module, main_script: ServiceWorkerScriptLoadParams { script_url, scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(), @@ -782,6 +850,7 @@ mod tests { let result = load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams { + script_kind: crate::worker::WorkerScriptKind::Module, main_script: ServiceWorkerScriptLoadParams { script_url, scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(), @@ -834,6 +903,7 @@ mod tests { let result = load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams { + script_kind: crate::worker::WorkerScriptKind::Classic, main_script: ServiceWorkerScriptLoadParams { script_url: Url::parse(&format!("{base_url}/app/sw.js")).unwrap(), scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(), diff --git a/moli-renderer-v8/src/service_worker_runtime/service.rs b/moli-renderer-v8/src/service_worker_runtime/service.rs index 1cff9ec78c..9e8c09d4cf 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service.rs @@ -770,7 +770,6 @@ mod tests { ServiceWorkerQueuedLaunch { params, host: new_loading_test_host(version_id, &run), - lifecycle_notifications: Vec::new(), preloaded_script: None, } } @@ -1349,6 +1348,7 @@ mod tests { fn test_loaded_script(script_url: &Url, source: &str) -> LoadedServiceWorkerScript { LoadedServiceWorkerScript { + updated_imports: Default::default(), resource: test_script_resource(script_url), source: source.to_owned(), response_referrer_policy: None, @@ -1381,6 +1381,7 @@ mod tests { scope_url: Url, ) -> ServiceWorkerQueuedRegisterJob { ServiceWorkerQueuedRegisterJob { + update_registration_id: None, script_url, document_url: scope_url.join("page.html").expect("document url"), storage_key: ServiceWorkerRegistrationKey::storage_key_for_scope_url(&scope_url), @@ -1393,11 +1394,11 @@ mod tests { force_update_page_load_waiter_ids: Vec::new(), request_client: test_request_client(service), network_policy: WorkerNetworkPolicy::default(), - browser_context_runtime: service.browser_context_runtime(), + worker_context_runtime: service.browser_context_runtime().worker_context_runtime(), broadcast_channel_top_level_site: None, indexed_db_manager: None, storage_bucket_store: None, - callbacks: vec![ServiceWorkerRegisterJob { + callbacks: vec![ServiceWorkerRegisterJob::Page { request_id: 1, document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1), completion_tx: test_completion_sender(), @@ -1417,6 +1418,7 @@ mod tests { let document_url = scope_url.join("page.html").expect("document url"); let browser_context_runtime = service.browser_context_runtime(); let queued_job = ServiceWorkerQueuedRegisterJob { + update_registration_id: None, script_url: script_url.clone(), scope_url: scope_url.clone(), document_url, @@ -1429,11 +1431,11 @@ mod tests { force_update_page_load_waiter_ids: Vec::new(), request_client: test_request_client(service), network_policy: WorkerNetworkPolicy::default(), - browser_context_runtime, + worker_context_runtime: browser_context_runtime.worker_context_runtime(), broadcast_channel_top_level_site: None, indexed_db_manager: None, storage_bucket_store: None, - callbacks: vec![ServiceWorkerRegisterJob { + callbacks: vec![ServiceWorkerRegisterJob::Page { request_id, document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1), completion_tx, @@ -1610,7 +1612,7 @@ mod tests { registration.scope_url = scope_url.clone(); registration.installing_version_id = Some(version_id); let mut pending_register_job = - ServiceWorkerPendingRegisterJob::new(vec![ServiceWorkerRegisterJob { + ServiceWorkerPendingRegisterJob::new(vec![ServiceWorkerRegisterJob::Page { request_id, document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1), completion_tx, @@ -1665,6 +1667,7 @@ mod tests { state.job_coordinator.enqueue_register( registration_key.clone(), ServiceWorkerQueuedRegisterJob { + update_registration_id: None, script_url, scope_url: scope_url.clone(), document_url: scope_url.join("page.html").expect("document url"), @@ -1677,11 +1680,11 @@ mod tests { force_update_page_load_waiter_ids: Vec::new(), request_client: test_request_client(service), network_policy: WorkerNetworkPolicy::default(), - browser_context_runtime: service.browser_context_runtime(), + worker_context_runtime: service.browser_context_runtime().worker_context_runtime(), broadcast_channel_top_level_site: None, indexed_db_manager: None, storage_bucket_store: None, - callbacks: vec![ServiceWorkerRegisterJob { + callbacks: vec![ServiceWorkerRegisterJob::Page { request_id, document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1), completion_tx, @@ -5297,6 +5300,191 @@ self.addEventListener("message", event => { ); } + #[test] + fn registration_update_rejects_wrong_identity_partition_and_installing_worker() { + use crate::service_worker_runtime::{ + ServiceWorkerRegistrationErrorKind, ServiceWorkerRegistrationUpdate, + }; + for invalid in ["identity", "partition", "installing"] { + let service = new_service_worker_runtime_service(); + let (registration_id, version_id) = insert_running_installing_version(&service); + let mut queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new(); + let scope_url = url("https://example.test/app/"); + let mut request = ServiceWorkerRegistrationUpdate { + registration_id, + caller_version_id: None, + storage_key: ServiceWorkerRegistrationKey::storage_key_for_scope_url(&scope_url), + document_url: scope_url.join("page.html").unwrap(), + request_client: test_request_client(&service), + network_policy: WorkerNetworkPolicy::default(), + worker_context_runtime: service.browser_context_runtime().worker_context_runtime(), + broadcast_channel_top_level_site: None, + indexed_db_manager: None, + storage_bucket_store: None, + completion: ServiceWorkerRegisterJob::Page { + request_id: 7, + document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1), + completion_tx: queue.sender(), + }, + }; + match invalid { + "identity" => request.registration_id = ServiceWorkerRegistrationId(999), + "partition" => request.storage_key = "another-partition".into(), + "installing" => request.caller_version_id = Some(version_id), + _ => unreachable!(), + } + service.start_registration_update(request); + let completion = pop_register_completion(&mut queue); + assert_eq!(completion.request_id, 7); + assert_eq!( + completion.result.err().unwrap().kind, + ServiceWorkerRegistrationErrorKind::InvalidState, + "{invalid}" + ); + let diagnostics = service.diagnostics_snapshot(); + assert_eq!(diagnostics.registration_count, 1); + assert_eq!(diagnostics.queued_register_job_count, 0); + } + } + + #[test] + fn queued_registration_update_revalidates_after_installation() { + for unregister in [false, true] { + let service = new_service_worker_runtime_service(); + let (registration_id, version_id) = insert_running_installing_version(&service); + let mut queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new(); + let mut job = test_queued_register_job( + &service, + url("https://example.test/app/sw.js"), + url("https://example.test/app/"), + ); + job.update_registration_id = Some(registration_id); + job.callbacks = vec![ServiceWorkerRegisterJob::Page { + request_id: 3, + document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1), + completion_tx: queue.sender(), + }]; + service.start_queued_register_job(job); + assert!(!queue.has_ready_task()); + assert_eq!(service.diagnostics_snapshot().queued_register_job_count, 1); + let progress = { + let mut state = service.inner.state.lock(); + let registration = state.registrations.get_mut(®istration_id).unwrap(); + registration.installing_version_id = None; + registration.active_version_id = Some(version_id); + registration.pending_unregistration = unregister; + let version = state.versions.get_mut(&version_id).unwrap(); + version.lifecycle_state = ServiceWorkerVersionLifecycleState::Activated; + if !unregister { + version.script_url = url("https://example.test/app/replaced.js"); + } + service.advance_registration_job_queue_locked(&mut state, registration_id) + }; + for action in progress { + service.run_lifecycle_progress(action); + } + let error = pop_register_completion(&mut queue).result.err().unwrap(); + assert_eq!( + error.kind, + crate::service_worker_runtime::ServiceWorkerRegistrationErrorKind::Type + ); + assert_eq!(service.diagnostics_snapshot().queued_register_job_count, 0); + assert_eq!(service.diagnostics_snapshot().version_count, 1); + } + } + + #[test] + fn register_queues_behind_an_installing_update_job() { + let service = new_service_worker_runtime_service(); + let script_url = url("https://example.test/app/sw.js"); + let scope_url = url("https://example.test/app/"); + let registration_id = ServiceWorkerRegistrationId(1); + let version_id = ServiceWorkerVersionId(1); + insert_starting_version_with_register_job( + &service, + registration_id, + version_id, + script_url.clone(), + scope_url.clone(), + 1, + test_completion_sender(), + ); + service + .inner + .state + .lock() + .registrations + .get_mut(®istration_id) + .unwrap() + .pending_register_jobs + .get_mut(&version_id) + .unwrap() + .is_update = true; + let mut queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new(); + let mut job = test_queued_register_job(&service, script_url, scope_url); + job.callbacks = vec![ServiceWorkerRegisterJob::Page { + request_id: 2, + document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1), + completion_tx: queue.sender(), + }]; + service.start_queued_register_job(job); + assert_eq!(service.diagnostics_snapshot().queued_register_job_count, 1); + assert!(!queue.has_ready_task()); + let progress = { + let mut state = service.inner.state.lock(); + let registration = state.registrations.get_mut(®istration_id).unwrap(); + registration.installing_version_id = None; + registration.active_version_id = Some(version_id); + service.advance_registration_job_queue_locked(&mut state, registration_id) + }; + for action in progress { + service.run_lifecycle_progress(action); + } + assert_eq!(pop_register_completion(&mut queue).request_id, 2); + assert_eq!(service.diagnostics_snapshot().queued_register_job_count, 0); + assert_eq!( + service + .diagnostics_snapshot() + .pending_main_script_update_check_count, + 0 + ); + service.terminate_all_for_context_shutdown(); + } + + #[test] + fn queued_forced_update_keeps_the_script_check() { + let service = new_service_worker_runtime_service(); + let registration_id = ServiceWorkerRegistrationId(42); + let version_id = ServiceWorkerVersionId(17); + let script_url = url("https://example.test/app/sw.js"); + let scope_url = url("https://example.test/app/"); + insert_registered_version( + &service, + registration_id, + version_id, + script_url.clone(), + scope_url.clone(), + [], + ); + make_version_persistable(&service, version_id); + let mut job = test_queued_register_job(&service, script_url, scope_url); + job.force_bypass_cache = true; + job.skip_script_comparison = true; + let mut state = service.inner.state.lock(); + state + .job_coordinator + .enqueue_register(job.registration_key(), job); + let progress = service.advance_registration_job_queue_locked(&mut state, registration_id); + let [LifecycleProgress::StartMainScriptUpdateCheck(check)] = progress.as_slice() else { + panic!("queued forced update must not take the register shortcut"); + }; + assert!(check.1.skip_script_comparison); + assert_eq!( + check.1.main_script.cache_mode, + moli_fetch::RequestCacheMode::Validate + ); + } + #[test] fn start_registration_queues_same_scope_job_while_installing() { let service = new_service_worker_runtime_service(); diff --git a/moli-renderer-v8/src/service_worker_runtime/service/devtools_commands.rs b/moli-renderer-v8/src/service_worker_runtime/service/devtools_commands.rs index 64300dc662..d1d107b0f4 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/devtools_commands.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/devtools_commands.rs @@ -334,7 +334,6 @@ impl ServiceWorkerRuntimeService { Some(ServiceWorkerQueuedLaunch { params, host, - lifecycle_notifications: Vec::new(), preloaded_script: None, }) } @@ -481,6 +480,7 @@ impl ServiceWorkerRuntimeService { }; let request_client = newest_version.launch_config.request_client(); ServiceWorkerQueuedRegisterJob { + update_registration_id: None, script_url: newest_version.script_url.clone(), scope_url: registration.scope_url.clone(), document_url: newest_version.launch_config.document_url.clone(), @@ -493,7 +493,7 @@ impl ServiceWorkerRuntimeService { force_update_page_load_waiter_ids, request_client, network_policy: newest_version.launch_config.network_policy.clone(), - browser_context_runtime, + worker_context_runtime: browser_context_runtime.worker_context_runtime(), broadcast_channel_top_level_site: newest_version .launch_config .broadcast_channel_top_level_site diff --git a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs index cb4b8df61b..5874c6602a 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs @@ -181,9 +181,6 @@ impl ServiceWorkerRuntimeService { &mut launch, debugger_release_consumed, ); - for notification in launch.lifecycle_notifications { - notification.send(); - } launch .host .start_loading(self.clone(), launch.params, launch.preloaded_script); @@ -420,7 +417,6 @@ impl ServiceWorkerRuntimeService { Some(ServiceWorkerQueuedLaunch { params, host, - lifecycle_notifications: Vec::new(), preloaded_script: None, }), None, diff --git a/moli-renderer-v8/src/service_worker_runtime/service/event_start.rs b/moli-renderer-v8/src/service_worker_runtime/service/event_start.rs index b85df0125b..cdabcc457b 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/event_start.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/event_start.rs @@ -363,7 +363,6 @@ impl ServiceWorkerRuntimeService { ServiceWorkerQueuedLaunch { params, host, - lifecycle_notifications: Vec::new(), preloaded_script: None, }, ))) @@ -430,7 +429,6 @@ impl ServiceWorkerRuntimeService { ServiceWorkerMessageStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, - lifecycle_notifications: Vec::new(), preloaded_script: None, })) } @@ -492,7 +490,6 @@ impl ServiceWorkerRuntimeService { ServiceWorkerNotificationStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, - lifecycle_notifications: Vec::new(), preloaded_script: None, })) } @@ -556,7 +553,6 @@ impl ServiceWorkerRuntimeService { ServiceWorkerPushStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, - lifecycle_notifications: Vec::new(), preloaded_script: None, })) } @@ -618,7 +614,6 @@ impl ServiceWorkerRuntimeService { ServiceWorkerSyncStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, - lifecycle_notifications: Vec::new(), preloaded_script: None, })) } @@ -680,7 +675,6 @@ impl ServiceWorkerRuntimeService { ServiceWorkerPeriodicSyncStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, - lifecycle_notifications: Vec::new(), preloaded_script: None, })) } diff --git a/moli-renderer-v8/src/service_worker_runtime/service/registration_jobs.rs b/moli-renderer-v8/src/service_worker_runtime/service/registration_jobs.rs index a66cb225c9..a1d678cd7d 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/registration_jobs.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/registration_jobs.rs @@ -1,6 +1,57 @@ use super::*; +use crate::service_worker_runtime::{ + ServiceWorkerRegistrationErrorKind, ServiceWorkerRegistrationUpdate, +}; use moli_fetch::RequestCacheMode; +fn newest_exposed_version_id( + state: &ServiceWorkerRuntimeState, + registration: &ServiceWorkerRegistration, +) -> Option { + let checking = state + .pending_main_script_update_checks + .get(®istration.id) + .map(|check| check.new_version_id); + registration + .installing_version_id + .filter(|version_id| Some(*version_id) != checking) + .or(registration.waiting_version_id) + .or(registration.active_version_id) +} + +fn validate_queued_update( + state: &ServiceWorkerRuntimeState, + job: &ServiceWorkerQueuedRegisterJob, +) -> Result<(), ServiceWorkerRegistrationError> { + let Some(registration_id) = job.update_registration_id else { + return Ok(()); + }; + let registration = state + .registrations + .get(®istration_id) + .filter(|registration| { + !registration.pending_unregistration && registration.key() == job.registration_key() + }) + .ok_or_else(|| { + ServiceWorkerRegistrationError::type_error( + "The ServiceWorker registration was unregistered.", + ) + })?; + let newest = newest_exposed_version_id(state, registration) + .and_then(|id| state.versions.get(&id)) + .ok_or_else(|| { + ServiceWorkerRegistrationError::type_error( + "The ServiceWorker registration has no worker.", + ) + })?; + if newest.script_url != job.script_url { + return Err(ServiceWorkerRegistrationError::type_error( + "The newest ServiceWorker script URL changed before update().", + )); + } + Ok(()) +} + fn same_registration_options_fast_path_snapshot( state: &ServiceWorkerRuntimeState, registration: &ServiceWorkerRegistration, @@ -28,6 +79,77 @@ fn same_registration_options_fast_path_snapshot( } impl ServiceWorkerRuntimeService { + pub(crate) fn start_registration_update(&self, request: ServiceWorkerRegistrationUpdate) { + let newest = { + let state = self.inner.state.lock(); + (|| { + let registration = state + .registrations + .get(&request.registration_id) + .filter(|registration| registration.storage_key == request.storage_key) + .ok_or_else(|| { + ServiceWorkerRegistrationError::new( + ServiceWorkerRegistrationErrorKind::InvalidState, + "The ServiceWorker registration has no worker.", + ) + })?; + let newest = newest_exposed_version_id(&state, registration) + .and_then(|id| state.versions.get(&id)) + .ok_or_else(|| { + ServiceWorkerRegistrationError::new( + ServiceWorkerRegistrationErrorKind::InvalidState, + "The ServiceWorker registration has no worker.", + ) + })?; + if request + .caller_version_id + .and_then(|id| state.versions.get(&id)) + .is_some_and(|caller| { + caller.lifecycle_state == ServiceWorkerVersionLifecycleState::Installing + }) + { + return Err(ServiceWorkerRegistrationError::new( + ServiceWorkerRegistrationErrorKind::InvalidState, + "An installing ServiceWorker cannot call update().", + )); + } + Ok(( + registration.scope_url.clone(), + newest.script_url.clone(), + newest.script_kind, + registration.update_via_cache, + )) + })() + }; + let (scope_url, script_url, script_kind, update_via_cache) = match newest { + Ok(newest) => newest, + Err(error) => { + request.completion.send(Err(error)); + return; + } + }; + self.start_queued_register_job(ServiceWorkerQueuedRegisterJob { + update_registration_id: Some(request.registration_id), + script_url, + scope_url, + document_url: request.document_url, + storage_key: request.storage_key, + script_kind, + update_via_cache, + force_bypass_cache: false, + skip_script_comparison: false, + skip_waiting_after_install: false, + force_update_page_load_waiter_ids: Vec::new(), + request_client: request.request_client, + network_policy: request.network_policy, + worker_context_runtime: request.worker_context_runtime, + broadcast_channel_top_level_site: request.broadcast_channel_top_level_site, + indexed_db_manager: request.indexed_db_manager, + storage_bucket_store: request.storage_bucket_store, + callbacks: vec![request.completion], + }); + } + fn record_force_update_page_load_devtools_message_locked( &self, state: &mut ServiceWorkerRuntimeState, @@ -109,6 +231,7 @@ impl ServiceWorkerRuntimeService { register_completion_tx: RendererPageServiceWorkerTaskSender, ) { let queued_job = ServiceWorkerQueuedRegisterJob { + update_registration_id: None, script_url, scope_url, document_url, @@ -121,11 +244,11 @@ impl ServiceWorkerRuntimeService { force_update_page_load_waiter_ids: Vec::new(), request_client, network_policy, - browser_context_runtime, + worker_context_runtime: browser_context_runtime.worker_context_runtime(), broadcast_channel_top_level_site, indexed_db_manager, storage_bucket_store, - callbacks: vec![ServiceWorkerRegisterJob { + callbacks: vec![ServiceWorkerRegisterJob::Page { request_id: register_request_id, document_owner: register_document_owner, completion_tx: register_completion_tx, @@ -138,6 +261,11 @@ impl ServiceWorkerRuntimeService { let (launch, update_check, completed_register_callbacks) = { let mut state = self.inner.state.lock(); let registration_key = queued_job.registration_key(); + if let Err(error) = validate_queued_update(&state, &queued_job) { + drop(state); + ServiceWorkerRegisterJob::send_all(queued_job.callbacks, Err(error)); + return; + } self.restore_stored_registration_for_queued_job_locked(&mut state, &queued_job); let registration_id = state .registrations @@ -190,14 +318,19 @@ impl ServiceWorkerRuntimeService { { registration.pending_unregistration = false; } - let can_coalesce_with_installing = installing_version_id - .and_then(|version_id| state.versions.get(&version_id)) - .is_some_and(|version| { - version.script_url == queued_job.script_url - && version.script_kind == queued_job.script_kind - && registration_scope_url == queued_job.scope_url - && registration_update_via_cache == queued_job.update_via_cache - }); + let can_coalesce_with_installing = queued_job.update_registration_id.is_none() + && installing_version_id + .and_then(|version_id| state.versions.get(&version_id)) + .is_some_and(|version| { + version.script_url == queued_job.script_url + && version.script_kind == queued_job.script_kind + && registration_scope_url == queued_job.scope_url + && registration_update_via_cache == queued_job.update_via_cache + && !state.registrations[®istration_id] + .pending_register_jobs + .get(&version.id) + .is_some_and(|job| job.is_update) + }); let pending_update_check_matches = state .pending_main_script_update_checks .get(®istration_id) @@ -514,6 +647,7 @@ impl ServiceWorkerRuntimeService { ), }; let update_check_params = ServiceWorkerScriptUpdateCheckParams { + script_kind: queued_job.script_kind, main_script: load_params, newest_main_body_sha256: newest_body_sha256.clone(), imported_scripts, @@ -616,16 +750,12 @@ impl ServiceWorkerRuntimeService { register_callbacks, queued_job.skip_waiting_after_install, ); + pending_register_job.is_update = queued_job.update_registration_id.is_some(); pending_register_job.start_current_moli_job(); let registration = state.registrations.get_mut(®istration_id)?; registration .pending_register_jobs .insert(version_id, pending_register_job); - let lifecycle_notifications = lifecycle_notifications_for_registration_locked( - state, - registration_id, - vec![ServiceWorkerLifecycleClientEvent::UpdateFound], - ); let request_client = launch_config.request_client(); Some(ServiceWorkerQueuedLaunch { params: ServiceWorkerLaunchParams { @@ -645,7 +775,6 @@ impl ServiceWorkerRuntimeService { pause_evaluation_until_debugger: false, }, host, - lifecycle_notifications, preloaded_script, }) } @@ -681,6 +810,7 @@ impl ServiceWorkerRuntimeService { register_callbacks, queued_job.skip_waiting_after_install, ); + pending_register_job.is_update = queued_job.update_registration_id.is_some(); pending_register_job.start_current_moli_job(); registration .pending_register_jobs @@ -691,11 +821,6 @@ impl ServiceWorkerRuntimeService { host.run_identity(), &queued_job, ); - let lifecycle_notifications = lifecycle_notifications_for_registration_locked( - state, - registration_id, - vec![ServiceWorkerLifecycleClientEvent::UpdateFound], - ); let request_client = launch_config.request_client(); Some(ServiceWorkerQueuedLaunch { params: ServiceWorkerLaunchParams { @@ -715,7 +840,6 @@ impl ServiceWorkerRuntimeService { pause_evaluation_until_debugger: false, }, host, - lifecycle_notifications, preloaded_script: Some(preloaded_script), }) } @@ -837,6 +961,39 @@ impl ServiceWorkerRuntimeService { match queued_job { ServiceWorkerQueuedJob::Register(queued_job) => { let queued_job = *queued_job; + if let Err(error) = validate_queued_update(state, &queued_job) { + progress.push(LifecycleProgress::RegisterFailed(( + queued_job.callbacks, + error, + ))); + continue; + } + // A register queued behind update must wait for that job, + // then reuse the matching registration without another fetch. + if queued_job.update_registration_id.is_none() + && !queued_job.force_bypass_cache + && !queued_job.skip_script_comparison + && let Some(snapshot) = + state + .registrations + .get(®istration_id) + .and_then(|registration| { + same_registration_options_fast_path_snapshot( + state, + registration, + &queued_job.script_url, + queued_job.script_kind, + queued_job.update_via_cache, + false, + ) + }) + { + progress.push(LifecycleProgress::RegisterCompleted(Box::new(( + queued_job.callbacks, + snapshot, + )))); + continue; + } if let Some(update_check) = self.start_main_script_update_check_locked( state, registration_id, diff --git a/moli-renderer-v8/src/service_worker_runtime/service/worker_completion.rs b/moli-renderer-v8/src/service_worker_runtime/service/worker_completion.rs index 1a81740042..0ebaa2668f 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/worker_completion.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/worker_completion.rs @@ -66,7 +66,7 @@ impl ServiceWorkerRuntimeService { return; }; let loaded_main_script_resource = script_resource.is_some(); - let (lifecycle_start, register_completion, pending_events) = { + let (lifecycle_start, register_completion, notifications, worker_hosts, pending_events) = { let mut state = self.inner.state.lock(); let (registration_id, lifecycle_state, pending_events, run) = { let Some(version) = state.versions.get_mut(&version_id) else { @@ -149,19 +149,90 @@ impl ServiceWorkerRuntimeService { } else { None }; + let (mut notifications, worker_hosts) = if lifecycle_start.is_some() { + let notifications = lifecycle_notifications_for_registration_locked( + &state, + registration_id, + vec![ServiceWorkerLifecycleClientEvent::UpdateFound], + ); + let hosts = state + .versions + .values() + .filter_map(|version| { + if version.registration_id != registration_id { + return None; + } + match &version.running_state { + ServiceWorkerVersionRunningState::Running { host } => { + Some(host.clone()) + } + _ => None, + } + }) + .collect::>(); + (notifications, hosts) + } else { + (Vec::new(), Vec::new()) + }; + // A first register() creates its JS registration when the completion is + // delivered. Queue updatefound for that document after the completion, + // even though it cannot have a registration watcher yet. + if let Some((callbacks, snapshot)) = ®ister_completion { + for callback in callbacks { + let ServiceWorkerRegisterJob::Page { + document_owner, + completion_tx, + .. + } = callback + else { + continue; + }; + if notifications + .iter() + .any(|notification| notification.watcher.document_owner == *document_owner) + { + continue; + } + notifications.push(ServiceWorkerLifecycleNotificationDelivery { + watcher: ServiceWorkerLifecycleWatcher { + scope_url: snapshot.scope_url().clone(), + storage_key: state.registrations[®istration_id].storage_key.clone(), + document_owner: *document_owner, + completion_tx: completion_tx.clone(), + }, + registration: snapshot.clone(), + events: vec![ServiceWorkerLifecycleClientEvent::UpdateFound], + }); + } + } state.record_target_started(version_id, run); - (lifecycle_start, register_completion, pending_events) + ( + lifecycle_start, + register_completion, + notifications, + worker_hosts, + pending_events, + ) }; - if let Some(start) = lifecycle_start - && let Some(progress) = Self::lifecycle_start_to_progress(start) - { - self.run_lifecycle_progress(progress); - } if let Some((callbacks, snapshot)) = register_completion && !callbacks.is_empty() { ServiceWorkerRegisterJob::send_all(callbacks, Ok(snapshot)); } + // Script evaluation must succeed before updatefound. Deliver it on each + // existing worker loop before install (and before page listeners can send + // another event to the active worker). + for host in worker_hosts { + host.dispatch_registration_update_found(); + } + for notification in notifications { + notification.send(); + } + if let Some(start) = lifecycle_start + && let Some(progress) = Self::lifecycle_start_to_progress(start) + { + self.run_lifecycle_progress(progress); + } for event in pending_events { match event { ServiceWorkerPendingStartEvent::Fetch(event) => { @@ -981,6 +1052,12 @@ fn registration_error_for_update_check_failure( failure: ServiceWorkerScriptUpdateCheckFailure, ) -> ServiceWorkerRegistrationError { match failure.status { + ServiceWorkerScriptUpdateCheckFailureStatus::Security => { + ServiceWorkerRegistrationError::new( + crate::service_worker_runtime::ServiceWorkerRegistrationErrorKind::Security, + failure.message, + ) + } ServiceWorkerScriptUpdateCheckFailureStatus::ScriptLoadFailed => { registration_error_for_script_load_failure(failure.message) } @@ -1002,6 +1079,8 @@ fn registration_error_for_script_load_failure(message: String) -> ServiceWorkerR ); } if normalized.contains("cross-origin") + || normalized.contains("script mime type") + || normalized.contains("blocked by x-content-type-options nosniff") || normalized.contains("service-worker-allowed") || normalized.contains("not under the max scope allowed") || normalized.contains("disallowed escape") diff --git a/moli-renderer-v8/src/service_worker_runtime/snapshots.rs b/moli-renderer-v8/src/service_worker_runtime/snapshots.rs index d515bc6934..d0f730b033 100644 --- a/moli-renderer-v8/src/service_worker_runtime/snapshots.rs +++ b/moli-renderer-v8/src/service_worker_runtime/snapshots.rs @@ -132,7 +132,6 @@ impl ServiceWorkerRegistrationSnapshot { ) } - #[cfg(test)] pub(crate) fn registration_id(&self) -> ServiceWorkerRegistrationId { self.registration_id } diff --git a/moli-renderer-v8/src/service_worker_runtime/start_completion.rs b/moli-renderer-v8/src/service_worker_runtime/start_completion.rs index 9eafad303c..783e2e7f62 100644 --- a/moli-renderer-v8/src/service_worker_runtime/start_completion.rs +++ b/moli-renderer-v8/src/service_worker_runtime/start_completion.rs @@ -49,7 +49,7 @@ enum ServiceWorkerRuntimeCompletionKind { }, MainScriptUpdateCheckCompleted { registration_id: ServiceWorkerRegistrationId, - result: ServiceWorkerScriptUpdateCheckCompletion, + result: Box, }, LifecycleEventCompleted { completion: ServiceWorkerLifecycleCompletion, @@ -219,7 +219,7 @@ impl ServiceWorkerRuntimeCompletion { runtime_service, kind: ServiceWorkerRuntimeCompletionKind::MainScriptUpdateCheckCompleted { registration_id, - result, + result: Box::new(result), }, } } @@ -637,7 +637,7 @@ impl ServiceWorkerRuntimeCompletion { result, } => self .runtime_service - .finish_main_script_update_check_completed(registration_id, result), + .finish_main_script_update_check_completed(registration_id, *result), ServiceWorkerRuntimeCompletionKind::LifecycleEventCompleted { completion } => { self.runtime_service .finish_lifecycle_event_completed(completion); diff --git a/moli-renderer-v8/src/service_worker_runtime/state.rs b/moli-renderer-v8/src/service_worker_runtime/state.rs index bbc6bdb263..8aff4607e6 100644 --- a/moli-renderer-v8/src/service_worker_runtime/state.rs +++ b/moli-renderer-v8/src/service_worker_runtime/state.rs @@ -713,7 +713,6 @@ pub(super) enum LifecycleProgress { pub(super) struct ServiceWorkerQueuedLaunch { pub(super) params: ServiceWorkerLaunchParams, pub(super) host: SharedRendererServiceWorkerHost, - pub(super) lifecycle_notifications: Vec, pub(super) preloaded_script: Option, } diff --git a/moli-renderer-v8/src/worker/global_scope/import_scripts.rs b/moli-renderer-v8/src/worker/global_scope/import_scripts.rs index c5687be3ed..7605ec7e39 100644 --- a/moli-renderer-v8/src/worker/global_scope/import_scripts.rs +++ b/moli-renderer-v8/src/worker/global_scope/import_scripts.rs @@ -65,7 +65,7 @@ pub(super) fn materialize_worker_import_source( csp.check_url(script_url, ContentSecurityPolicyRedirectStatus::NoRedirect) .map_err(WorkerImportScriptError::network)?; } - let (service_worker, cached, can_import_new) = { + let (service_worker, cached, updated, can_import_new) = { let state = state.borrow(); ( matches!( @@ -76,10 +76,21 @@ pub(super) fn materialize_worker_import_source( .service_worker_script_resources .get(script_url) .cloned(), + state + .service_worker_updated_script_resources + .get(script_url) + .cloned(), state.service_worker_can_import_new_scripts, ) }; if service_worker { + let from_update_check = cached.is_none() && updated.is_some(); + let cached = match cached.clone() { + Some(resource) => Some(resource), + None => updated + .transpose() + .map_err(WorkerImportScriptError::network)?, + }; if let Some(resource) = &cached && let Some(script) = &resource.classic_script { @@ -89,6 +100,13 @@ pub(super) fn materialize_worker_import_source( .map_err(WorkerImportScriptError::network)?; } } + if from_update_check { + state + .borrow_mut() + .service_worker_script_resources + .insert(script_url.clone(), resource.clone()); + report_service_worker_imported_script_loaded(state, resource.clone()); + } return Ok(WorkerImportScriptSource { final_url: resource.final_url.clone(), source: script.source.clone(), diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index fd928ed985..16a267ea1f 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -178,6 +178,10 @@ pub(super) const WORKER_EXCEPTION_LINE_SLOT: &str = "__moliWorkerExceptionLine"; pub(super) const WORKER_EXCEPTION_COLUMN_SLOT: &str = "__moliWorkerExceptionColumn"; const SERVICE_WORKER_REGISTRATION_SCOPE_SLOT: &str = "__moliServiceWorkerRegistrationScope"; const SERVICE_WORKER_REGISTRATION_ID_SLOT: &str = "__moliServiceWorkerRegistrationId"; +const SERVICE_WORKER_GLOBAL_REGISTRATION_SLOT: &str = "__moliServiceWorkerGlobalRegistration"; +const SERVICE_WORKER_REGISTRATION_EVENTS_SLOT: &str = "__moliServiceWorkerRegistrationEvents"; +const SERVICE_WORKER_REGISTRATION_ONUPDATEFOUND_SLOT: &str = + "__moliServiceWorkerRegistrationOnUpdateFound"; const SERVICE_WORKER_VERSION_ID_SLOT: &str = "__moliServiceWorkerVersionId"; const SERVICE_WORKER_WORKER_EVENTS_SLOT: &str = "__moliServiceWorkerWorkerEvents"; const SERVICE_WORKER_NAVIGATION_PRELOAD_MANAGER_SCOPE_SLOT: &str = @@ -655,6 +659,13 @@ struct ServiceWorkerGlobalRegistrationDeclaration<'scope> { #[webapi(data_property, readonly)] scope: String, + #[webapi( + accessor_property = "onupdatefound", + getter = service_worker_registration_onupdatefound_getter, + setter = service_worker_registration_onupdatefound_setter + )] + onupdatefound: (), + #[webapi( accessor_property = "installing", getter = service_worker_registration_installing_getter @@ -676,6 +687,9 @@ struct ServiceWorkerGlobalRegistrationDeclaration<'scope> { #[webapi(method, callback = service_worker_registration_unregister_callback, length = 0)] unregister: (), + #[webapi(method, callback = service_worker_registration_update_callback, length = 0)] + update: (), + #[webapi( method = "showNotification", callback = service_worker_registration_show_notification_callback, @@ -986,6 +1000,11 @@ pub(super) struct PendingServiceWorkerShowNotification { pub(super) resolver: v8::Global, } +pub(super) struct PendingServiceWorkerUpdate { + pub(super) resolver: v8::Global, + pub(super) registration: v8::Global, +} + pub(super) struct PendingServiceWorkerGetNotifications { pub(super) resolver: v8::Global, } @@ -1549,6 +1568,7 @@ pub(crate) struct WorkerGlobalState { /// Responses in this ServiceWorker version's script resource map, keyed by /// requested URL. Dedicated and Shared Workers leave this map empty. pub(super) service_worker_script_resources: HashMap, + pub(super) service_worker_updated_script_resources: crate::worker::WorkerScriptUpdateResources, pub(super) service_worker_can_import_new_scripts: bool, /// Referrer policy parsed from the top-level worker script response. pub(super) referrer_policy: Option, @@ -1659,6 +1679,8 @@ pub(crate) struct WorkerGlobalState { /// In-flight Service Worker `registration.showNotification()` requests keyed by request id. pub(super) pending_service_worker_show_notifications: HashMap, + pub(super) pending_service_worker_updates: HashMap, + pub(super) service_worker_update_request_ids: WorkerServiceWorkerRequestIdAllocator, /// In-flight Service Worker `registration.getNotifications()` requests keyed by request id. pub(super) pending_service_worker_get_notifications: HashMap, @@ -2523,6 +2545,45 @@ pub(super) fn drain_service_worker_clients_open_window_result( } } +pub(super) fn drain_service_worker_update_result( + scope: &mut v8::PinScope<'_, '_>, + state: &Rc>, + request_id: u64, + result: Result< + crate::service_worker_runtime::ServiceWorkerRegistrationSnapshot, + crate::service_worker_runtime::ServiceWorkerRegistrationError, + >, +) { + let Some(pending) = state + .borrow_mut() + .pending_service_worker_updates + .remove(&request_id) + else { + return; + }; + let resolver = v8::Local::new(scope, &pending.resolver); + match result { + Ok(_) => { + let registration = v8::Local::new(scope, &pending.registration); + let _ = resolver.resolve(scope, registration.into()); + } + Err(error) => { + let exception = if error.kind.rejects_as_type_error_for_update() { + let message = v8_string(scope, &error.message) + .unwrap_or_else(|| v8str(scope, "ServiceWorker update failed")); + v8::Exception::type_error(scope, message) + } else { + crate::context_bootstrap::new_dom_exception_value( + scope, + &error.message, + error.kind.dom_exception_name(), + ) + }; + let _ = resolver.reject(scope, exception); + } + } +} + pub(super) fn drain_service_worker_show_notification_result( scope: &mut v8::PinScope<'_, '_>, state: &Rc>, @@ -3265,6 +3326,12 @@ fn install_service_worker_global_runtime<'s>( ) -> Result<()> { let registration = build_service_worker_global_registration(scope, registration_id, version_id, scope_url)?; + set_private_value( + scope, + global, + SERVICE_WORKER_GLOBAL_REGISTRATION_SLOT, + registration.into(), + ); let clients = ServiceWorkerClientsDeclaration::default() .bind(scope) .map_err(|error| anyhow!("failed to build service worker clients: {error}"))?; @@ -3303,10 +3370,12 @@ fn build_service_worker_global_registration<'s>( build_service_worker_global_navigation_preload_manager(scope, scope_url)?; let registration = ServiceWorkerGlobalRegistrationDeclaration { scope: scope_url.as_str().to_owned(), + onupdatefound: (), installing: (), waiting: (), active: (), unregister: (), + update: (), show_notification: (), get_notifications: (), sync: sync_manager, @@ -3316,6 +3385,13 @@ fn build_service_worker_global_registration<'s>( } .bind(scope) .map_err(|error| anyhow!("failed to build service worker registration: {error:?}"))?; + install_simple_event_target_methods( + scope, + registration, + SERVICE_WORKER_REGISTRATION_EVENTS_SLOT, + false, + ); + install_simple_event_target_ordered_handlers(scope, registration); let scope_value = v8_string(scope, scope_url.as_str()) .ok_or_else(|| anyhow!("failed to allocate service worker registration scope"))?; set_private_value( @@ -3341,6 +3417,77 @@ fn build_service_worker_global_registration<'s>( Ok(registration) } +fn service_worker_registration_onupdatefound_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + rv.set( + get_private_value( + scope, + args.this(), + SERVICE_WORKER_REGISTRATION_ONUPDATEFOUND_SLOT, + ) + .unwrap_or_else(|| v8::null(scope).into()), + ); +} + +fn service_worker_registration_onupdatefound_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let value = args.get(0); + let active = value.is_function(); + let value = if active { + value + } else { + v8::null(scope).into() + }; + set_private_value( + scope, + args.this(), + SERVICE_WORKER_REGISTRATION_ONUPDATEFOUND_SLOT, + value, + ); + simple_object_event_set_ordered_handler( + scope, + args.this(), + SERVICE_WORKER_REGISTRATION_EVENTS_SLOT, + "updatefound", + SERVICE_WORKER_REGISTRATION_ONUPDATEFOUND_SLOT, + active, + ); +} + +pub(super) fn dispatch_service_worker_registration_update_found<'s>( + scope: &mut v8::PinScope<'s, '_>, +) { + let global = scope.get_current_context().global(scope); + let Some(registration) = + get_private_value(scope, global, SERVICE_WORKER_GLOBAL_REGISTRATION_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Ok(prototype) = + crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "Event") + else { + return; + }; + let event = v8::Object::new(scope); + let _ = event.set_prototype(scope, prototype.into()); + crate::context_bootstrap::initialize_event_object(scope, event, "updatefound", false, false); + crate::context_bootstrap::mark_event_trusted(scope, event); + crate::context_bootstrap::dispatch_simple_event_target_event( + scope, + registration, + SERVICE_WORKER_REGISTRATION_EVENTS_SLOT, + "updatefound", + event, + ); +} + fn build_service_worker_global_navigation_preload_manager<'s>( scope: &mut v8::PinScope<'s, '_>, scope_url: &Url, @@ -3450,6 +3597,98 @@ pub(super) fn build_service_worker_global_service_worker<'s>( Ok(worker) } +fn service_worker_registration_update_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some(resolver) = v8::PromiseResolver::new(scope) else { + return; + }; + rv.set(resolver.get_promise(scope).into()); + let registration_id = + get_private_value(scope, args.this(), SERVICE_WORKER_REGISTRATION_ID_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + .map(|value| value.u64_value()) + .filter(|(_, lossless)| *lossless) + .map(|(value, _)| { + crate::service_worker_runtime::ServiceWorkerRegistrationId::from_u64_for_binding( + value, + ) + }); + let Some(registration_id) = registration_id else { + let _ = resolver.reject( + scope, + v8::Exception::type_error( + scope, + v8str( + scope, + "Illegal invocation of ServiceWorkerRegistration.update", + ), + ), + ); + return; + }; + let Some(state) = get_worker_state(scope) else { + return; + }; + let Some(runtime) = worker_service_worker_runtime(scope) else { + let error = crate::context_bootstrap::new_dom_exception_value( + scope, + "The registration runtime is unavailable.", + "InvalidStateError", + ); + let _ = resolver.reject(scope, error); + return; + }; + let request = { + let mut state = state.borrow_mut(); + let caller_version_id = match state.global_kind { + super::thread::WorkerGlobalKind::Service { version_id, .. } => Some(version_id), + _ => None, + }; + let Some(document_url) = state.current_script_url.clone() else { + return; + }; + let request_id = state.service_worker_update_request_ids.allocate(); + state.pending_service_worker_updates.insert( + request_id, + PendingServiceWorkerUpdate { + resolver: v8::Global::new(scope, resolver), + registration: v8::Global::new(scope, args.this()), + }, + ); + crate::service_worker_runtime::ServiceWorkerRegistrationUpdate { + registration_id, + caller_version_id, + storage_key: state.storage_key.serialized_storage_key(), + document_url, + request_client: state.loader.request_client().clone(), + network_policy: super::handle::WorkerNetworkPolicy { + secure_context: state.secure_context, + permission_overrides: state.permission_overrides.clone(), + extra_http_headers: state.extra_http_headers.clone(), + network_offline: state.network_offline, + blocked_url_patterns: state.blocked_url_patterns.clone(), + network_partition_key: state.network_partition_key.clone(), + fetch_subresource_interception_enabled: state + .fetch_subresource_interception_enabled, + fetch_subresource_interception_resource_type: state + .fetch_subresource_interception_resource_type, + }, + worker_context_runtime: state.worker_context_runtime.clone(), + broadcast_channel_top_level_site: Some(state.storage_key.top_level_site().to_owned()), + indexed_db_manager: state.indexed_db_manager.clone(), + storage_bucket_store: state.storage_bucket_store.clone(), + completion: crate::service_worker_runtime::ServiceWorkerRegisterJob::Worker { + request_id, + completion_tx: state.worker_wake_tx.clone(), + }, + } + }; + runtime.start_registration_update(request); +} + fn service_worker_registration_unregister_callback<'s>( scope: &mut v8::PinScope<'s, '_>, _args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/worker/handle.rs b/moli-renderer-v8/src/worker/handle.rs index 0feb41204c..03b6e040be 100644 --- a/moli-renderer-v8/src/worker/handle.rs +++ b/moli-renderer-v8/src/worker/handle.rs @@ -109,6 +109,18 @@ pub(crate) enum WorkerMessage { ServiceWorkerPeriodicSyncEvent(Box), /// Dispatch `navigator.serviceWorker` `controllerchange` in this worker client. ServiceWorkerControllerChange, + /// Dispatch updatefound on the original registration in this service worker realm. + ServiceWorkerRegistrationUpdateFound, + /// Settle a registration.update() promise on the requesting worker loop. + ServiceWorkerUpdateResult { + request_id: u64, + result: Box< + Result< + crate::service_worker_runtime::ServiceWorkerRegistrationSnapshot, + crate::service_worker_runtime::ServiceWorkerRegistrationError, + >, + >, + }, /// Resolve a Service Worker `SyncManager.register()` request in the worker. ServiceWorkerSyncRegistrationResult(ServiceWorkerSyncRegistrationResult), /// Resolve a Service Worker `SyncManager.getTags()` request in the worker. @@ -942,6 +954,12 @@ impl WorkerHandle { .send(WorkerMessage::ServiceWorkerLifecycleEvent(Box::new(event))); } + pub(crate) fn dispatch_service_worker_registration_update_found(&self) { + let _ = self + .tx + .send(WorkerMessage::ServiceWorkerRegistrationUpdateFound); + } + pub(crate) fn dispatch_service_worker_fetch_event(&self, event: ServiceWorkerFetchEvent) { let _ = self .tx diff --git a/moli-renderer-v8/src/worker/mod.rs b/moli-renderer-v8/src/worker/mod.rs index 69ed991bb0..493f70e68a 100644 --- a/moli-renderer-v8/src/worker/mod.rs +++ b/moli-renderer-v8/src/worker/mod.rs @@ -22,6 +22,9 @@ mod script_mime; mod thread; mod timer_callback; +pub(crate) type WorkerScriptUpdateResources = + std::collections::HashMap>; + pub(crate) use thread::perform_callback_cleanup_checkpoint_if_worker; pub(crate) use data_url::decode_data_url_script_source; diff --git a/moli-renderer-v8/src/worker/thread/dispatch.rs b/moli-renderer-v8/src/worker/thread/dispatch.rs index 0b07b62924..dda966acc7 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -4090,6 +4090,7 @@ fn maybe_send_service_worker_lifecycle_completion( let completion = pending.completion.clone(); if completion.kind == ServiceWorkerLifecycleEventKind::Install { state.service_worker_can_import_new_scripts = false; + state.service_worker_updated_script_resources.clear(); } state .pending_service_worker_lifecycle_events diff --git a/moli-renderer-v8/src/worker/thread/mod.rs b/moli-renderer-v8/src/worker/thread/mod.rs index 0ab7cdce34..1e28aff339 100644 --- a/moli-renderer-v8/src/worker/thread/mod.rs +++ b/moli-renderer-v8/src/worker/thread/mod.rs @@ -84,6 +84,7 @@ use super::global_scope::{ drain_service_worker_push_get_subscription_result, drain_service_worker_push_subscribe_result, drain_service_worker_push_unsubscribe_result, drain_service_worker_show_notification_result, drain_service_worker_sync_get_tags_result, drain_service_worker_sync_registration_result, + drain_service_worker_update_result, drain_worker_fetch_completion, drain_worker_opfs_completion, drain_worker_webcrypto_completion, drain_worker_xhr_event, fail_pending_worker_csp_report, fail_pending_worker_fetch, fail_pending_worker_fetch_auth, fail_pending_worker_fetch_response, fail_pending_worker_xhr, @@ -204,6 +205,7 @@ pub(crate) struct WorkerSpawnOptions { pub(crate) creator_storage_key: Option, pub(crate) service_worker_runtime: Option, pub(crate) service_worker_script_resources: Vec, + pub(crate) service_worker_updated_script_resources: crate::worker::WorkerScriptUpdateResources, pub(crate) service_worker_can_import_new_scripts: bool, pub(crate) reserved_service_worker_client_id: Option, pub(crate) indexed_db_manager: Option, @@ -341,6 +343,7 @@ impl WorkerSpawnOptions { creator_storage_key: None, service_worker_runtime: None, service_worker_script_resources: Vec::new(), + service_worker_updated_script_resources: Default::default(), service_worker_can_import_new_scripts: true, reserved_service_worker_client_id: None, indexed_db_manager: None, @@ -469,6 +472,14 @@ impl WorkerSpawnOptions { self } + pub(crate) fn with_service_worker_updated_script_resources( + mut self, + resources: crate::worker::WorkerScriptUpdateResources, + ) -> Self { + self.service_worker_updated_script_resources = resources; + self + } + pub(crate) fn with_storage_key_top_level_site(self, top_level_site: Option) -> Self { self.with_broadcast_channel_top_level_site(top_level_site) } @@ -584,6 +595,7 @@ fn worker_has_pending_async(state: &Rc>) -> bool { || !state.pending_service_worker_client_focuses.is_empty() || !state.pending_service_worker_clients_open_windows.is_empty() || !state.pending_service_worker_show_notifications.is_empty() + || !state.pending_service_worker_updates.is_empty() || !state.pending_service_worker_get_notifications.is_empty() || !state.pending_service_worker_sync_registrations.is_empty() || !state.pending_service_worker_sync_get_tags.is_empty() @@ -1364,6 +1376,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa creator_storage_key, service_worker_runtime, service_worker_script_resources, + service_worker_updated_script_resources, service_worker_can_import_new_scripts, reserved_service_worker_client_id, indexed_db_manager, @@ -1418,6 +1431,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa creator_storage_key, service_worker_runtime, service_worker_script_resources, + service_worker_updated_script_resources, service_worker_can_import_new_scripts, reserved_service_worker_client_id, indexed_db_manager, @@ -1554,6 +1568,7 @@ async fn worker_main( creator_storage_key: Option, service_worker_runtime: Option, service_worker_script_resources: Vec, + service_worker_updated_script_resources: crate::worker::WorkerScriptUpdateResources, service_worker_can_import_new_scripts: bool, reserved_service_worker_client_id: Option, indexed_db_manager: Option, @@ -1678,6 +1693,7 @@ async fn worker_main( .into_iter() .map(|resource| (resource.request_url.clone(), resource)) .collect(), + service_worker_updated_script_resources, service_worker_can_import_new_scripts, referrer_policy, module_static_import_content_security_policies, @@ -1735,6 +1751,8 @@ async fn worker_main( pending_service_worker_client_focuses: std::collections::HashMap::new(), pending_service_worker_clients_open_windows: std::collections::HashMap::new(), pending_service_worker_show_notifications: std::collections::HashMap::new(), + pending_service_worker_updates: std::collections::HashMap::new(), + service_worker_update_request_ids: Default::default(), pending_service_worker_get_notifications: std::collections::HashMap::new(), pending_service_worker_sync_registrations: std::collections::HashMap::new(), pending_service_worker_sync_get_tags: std::collections::HashMap::new(), @@ -2503,6 +2521,37 @@ async fn worker_main( perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); drain_worker_dynamic_module_imports(scope, &state, &module_graph_fetch_tx); } + WorkerLoopWake::Message(Some(WorkerMessage::ServiceWorkerUpdateResult { + request_id, + result, + })) => { + if pending_module_bootstrap.is_some() { + pending_bootstrap_messages + .push_back(WorkerMessage::ServiceWorkerUpdateResult { request_id, result }); + continue; + } + let scope = pin!(v8::HandleScope::new(worker_isolate.worker_isolate_mut())); + let scope = &mut scope.init(); + let ctx = v8::Local::new(scope, &context); + let scope = &mut v8::ContextScope::new(scope, ctx); + drain_service_worker_update_result(scope, &state, request_id, *result); + perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); + drain_worker_dynamic_module_imports(scope, &state, &module_graph_fetch_tx); + } + WorkerLoopWake::Message(Some(WorkerMessage::ServiceWorkerRegistrationUpdateFound)) => { + if pending_module_bootstrap.is_some() { + pending_bootstrap_messages + .push_back(WorkerMessage::ServiceWorkerRegistrationUpdateFound); + continue; + } + let scope = pin!(v8::HandleScope::new(worker_isolate.worker_isolate_mut())); + let scope = &mut scope.init(); + let ctx = v8::Local::new(scope, &context); + let scope = &mut v8::ContextScope::new(scope, ctx); + super::global_scope::dispatch_service_worker_registration_update_found(scope); + perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); + drain_worker_dynamic_module_imports(scope, &state, &module_graph_fetch_tx); + } WorkerLoopWake::Message(Some(WorkerMessage::ServiceWorkerShowNotificationResult( result, ))) => {