Bug 1264177 - Implement FetchEvent.resultingClientId r=edenchuang,mrbkap

- Expose FetchEvent.resultingClientId on non-subresource, non-"report"-destination requests.
- Delay Clients.get(FetchEvent.resultingClientId) resolution until the resulting client is execution ready.
- Add WPTs to test for existence of resultingClientId and Clients.get promise resolution values.

Differential Revision: https://phabricator.services.mozilla.com/D5333

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Perry Jiang 2018-11-12 20:10:41 +00:00
parent b78d8dbce3
commit 84d5b8eb27
13 changed files with 364 additions and 14 deletions

View File

@ -18,7 +18,9 @@
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/MozPromise.h"
#include "mozilla/SystemGroup.h"
#include "jsfriendapi.h"
#include "nsIAsyncShutdown.h"
#include "nsIXULRuntime.h"
#include "nsProxyRelease.h"
@ -543,11 +545,34 @@ ClientManagerService::Claim(const ClientClaimArgs& aArgs)
RefPtr<ClientOpPromise>
ClientManagerService::GetInfoAndState(const ClientGetInfoAndStateArgs& aArgs)
{
RefPtr<ClientOpPromise> ref;
ClientSourceParent* source = FindSource(aArgs.id(), aArgs.principalInfo());
if (!source || !source->ExecutionReady()) {
ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
if (!source) {
RefPtr<ClientOpPromise> ref =
ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
return ref.forget();
}
if (!source->ExecutionReady()) {
RefPtr<ClientManagerService> self = this;
// rejection ultimately converted to `undefined` in Clients::Get
RefPtr<ClientOpPromise> ref =
source->ExecutionReadyPromise()
->Then(GetCurrentThreadSerialEventTarget(), __func__,
[self, aArgs] () -> RefPtr<ClientOpPromise> {
ClientSourceParent* source = self->FindSource(aArgs.id(),
aArgs.principalInfo());
if (!source) {
RefPtr<ClientOpPromise> ref =
ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
return ref.forget();
}
return source->StartOp(aArgs);
});
return ref.forget();
}

View File

@ -118,6 +118,8 @@ ClientSourceParent::RecvExecutionReady(const ClientSourceExecutionReadyArgs& aAr
Unused << handle->SendExecutionReady(mClientInfo.ToIPC());
}
mExecutionReadyPromise.ResolveIfExists(true, __func__);
return IPC_OK();
};
@ -228,6 +230,8 @@ ClientSourceParent::ClientSourceParent(const ClientSourceConstructorArgs& aArgs)
ClientSourceParent::~ClientSourceParent()
{
MOZ_DIAGNOSTIC_ASSERT(mHandleList.IsEmpty());
mExecutionReadyPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
}
void
@ -268,6 +272,15 @@ ClientSourceParent::ExecutionReady() const
return mExecutionReady;
}
RefPtr<GenericPromise>
ClientSourceParent::ExecutionReadyPromise()
{
// Only call if ClientSourceParent::ExecutionReady() is false; otherwise,
// the promise will never resolve
MOZ_ASSERT(!mExecutionReady);
return mExecutionReadyPromise.Ensure(__func__);
}
const Maybe<ServiceWorkerDescriptor>&
ClientSourceParent::GetController() const
{

View File

@ -10,6 +10,7 @@
#include "ClientOpPromise.h"
#include "mozilla/dom/PClientSourceParent.h"
#include "mozilla/dom/ServiceWorkerDescriptor.h"
#include "mozilla/MozPromise.h"
namespace mozilla {
namespace dom {
@ -23,6 +24,7 @@ class ClientSourceParent final : public PClientSourceParent
Maybe<ServiceWorkerDescriptor> mController;
RefPtr<ClientManagerService> mService;
nsTArray<ClientHandleParent*> mHandleList;
MozPromiseHolder<GenericPromise> mExecutionReadyPromise;
bool mExecutionReady;
bool mFrozen;
@ -76,6 +78,9 @@ public:
bool
ExecutionReady() const;
RefPtr<GenericPromise>
ExecutionReadyPromise();
const Maybe<ServiceWorkerDescriptor>&
GetController() const;

View File

@ -161,6 +161,7 @@ FetchEvent::Constructor(const GlobalObject& aGlobal,
e->SetComposed(aOptions.mComposed);
e->mRequest = aOptions.mRequest;
e->mClientId = aOptions.mClientId;
e->mResultingClientId = aOptions.mResultingClientId;
e->mIsReload = aOptions.mIsReload;
return e.forget();
}

View File

@ -123,6 +123,7 @@ class FetchEvent final : public ExtendableEvent
nsCString mScriptSpec;
nsCString mPreventDefaultScriptSpec;
nsString mClientId;
nsString mResultingClientId;
uint32_t mPreventDefaultLineNumber;
uint32_t mPreventDefaultColumnNumber;
bool mIsReload;
@ -169,6 +170,12 @@ public:
aClientId = mClientId;
}
void
GetResultingClientId(nsAString& aResultingClientId) const
{
aResultingClientId = mResultingClientId;
}
bool
IsReload() const
{

View File

@ -2018,21 +2018,43 @@ public:
}
nsString clientId;
nsString resultingClientId;
nsCOMPtr<nsILoadInfo> loadInfo = channel->GetLoadInfo();
if (loadInfo) {
char buf[NSID_LENGTH];
Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo();
if (clientInfo.isSome()) {
char buf[NSID_LENGTH];
clientInfo.ref().Id().ToProvidedString(buf);
NS_ConvertASCIItoUTF16 uuid(buf);
// Remove {} and the null terminator
clientId.Assign(Substring(uuid, 1, NSID_LENGTH - 3));
}
// Having an initial or reserved client are mutually exclusive events:
// either an initial client is used upon navigating an about:blank
// iframe, or a new, reserved environment/client is created (e.g.
// upon a top-level navigation). See step 4 of
// https://html.spec.whatwg.org/#process-a-navigate-fetch as well as
// https://github.com/w3c/ServiceWorker/issues/1228#issuecomment-345132444
Maybe<ClientInfo> resulting = loadInfo->GetInitialClientInfo();
if (resulting.isNothing()) {
resulting = loadInfo->GetReservedClientInfo();
} else {
MOZ_ASSERT(loadInfo->GetReservedClientInfo().isNothing());
}
if (resulting.isSome()) {
resulting.ref().Id().ToProvidedString(buf);
NS_ConvertASCIItoUTF16 uuid(buf);
resultingClientId.Assign(Substring(uuid, 1, NSID_LENGTH - 3));
}
}
rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, clientId,
mIsReload);
resultingClientId, mIsReload);
if (NS_WARN_IF(NS_FAILED(rv))) {
HandleError();
}

View File

@ -1309,6 +1309,7 @@ class FetchEventRunnable : public ExtendableFunctionalEventWorkerRunnable
nsCString mFragment;
nsCString mMethod;
nsString mClientId;
nsString mResultingClientId;
bool mIsReload;
bool mMarkLaunchServiceWorkerEnd;
RequestCache mCacheMode;
@ -1330,6 +1331,7 @@ public:
const nsACString& aScriptSpec,
nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
const nsAString& aClientId,
const nsAString& aResultingClientId,
bool aIsReload,
bool aMarkLaunchServiceWorkerEnd)
: ExtendableFunctionalEventWorkerRunnable(
@ -1337,6 +1339,7 @@ public:
, mInterceptedChannel(aChannel)
, mScriptSpec(aScriptSpec)
, mClientId(aClientId)
, mResultingClientId(aResultingClientId)
, mIsReload(aIsReload)
, mMarkLaunchServiceWorkerEnd(aMarkLaunchServiceWorkerEnd)
, mCacheMode(RequestCache::Default)
@ -1624,12 +1627,26 @@ private:
init.mBubbles = false;
init.mCancelable = true;
// Only expose the FetchEvent.clientId on subresource requests for now.
// Once we implement .resultingClientId and .targetClientId we can then
// start exposing .clientId on non-subresource requests as well. See
// bug 1264177.
// Once we implement .targetClientId we can then start exposing .clientId
// on non-subresource requests as well. See bug 1487534.
if (!mClientId.IsEmpty() && !internalReq->IsNavigationRequest()) {
init.mClientId = mClientId;
}
/*
* https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm
*
* "If request is a non-subresource request and requests
* destination is not "report", initialize es resultingClientId attribute
* to reservedClients [resultingClient's] id, and to the empty string
* otherwise." (Step 18.8)
*/
if (!mResultingClientId.IsEmpty() &&
nsContentUtils::IsNonSubresourceRequest(channel) &&
internalReq->Destination() != RequestDestination::Report) {
init.mResultingClientId = mResultingClientId;
}
init.mIsReload = mIsReload;
RefPtr<FetchEvent> event =
FetchEvent::Constructor(globalObj, NS_LITERAL_STRING("fetch"), init, result);
@ -1673,7 +1690,9 @@ NS_IMPL_ISUPPORTS_INHERITED(FetchEventRunnable, WorkerRunnable, nsIHttpHeaderVis
nsresult
ServiceWorkerPrivate::SendFetchEvent(nsIInterceptedChannel* aChannel,
nsILoadGroup* aLoadGroup,
const nsAString& aClientId, bool aIsReload)
const nsAString& aClientId,
const nsAString& aResultingClientId,
bool aIsReload)
{
MOZ_ASSERT(NS_IsMainThread());
@ -1742,7 +1761,8 @@ ServiceWorkerPrivate::SendFetchEvent(nsIInterceptedChannel* aChannel,
RefPtr<FetchEventRunnable> r =
new FetchEventRunnable(mWorkerPrivate, token, handle,
mInfo->ScriptSpec(), regInfo,
aClientId, aIsReload, newWorkerCreated);
aClientId, aResultingClientId,
aIsReload, newWorkerCreated);
rv = r->Init();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;

View File

@ -125,8 +125,11 @@ public:
const nsAString& aScope);
nsresult
SendFetchEvent(nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup,
const nsAString& aClientId, bool aIsReload);
SendFetchEvent(nsIInterceptedChannel* aChannel,
nsILoadGroup* aLoadGroup,
const nsAString& aClientId,
const nsAString& aResultingClientId,
bool aIsReload);
bool
MaybeStoreISupports(nsISupports* aSupports);

View File

@ -13,6 +13,7 @@
interface FetchEvent : ExtendableEvent {
[SameObject] readonly attribute Request request;
readonly attribute DOMString clientId;
readonly attribute DOMString resultingClientId;
readonly attribute boolean isReload;
[Throws]
@ -22,5 +23,6 @@ interface FetchEvent : ExtendableEvent {
dictionary FetchEventInit : EventInit {
required Request request;
DOMString clientId = "";
DOMString resultingClientId = "";
boolean isReload = false;
};

View File

@ -49,6 +49,162 @@ promise_test(function(t) {
});
}, 'Test Clients.get()');
promise_test((t) => {
let frame = null;
const scope = 'resources/simple.html';
const outerSwContainer = navigator.serviceWorker;
let innerSwReg = null;
let innerSw = null;
return service_worker_unregister_and_register(
t, 'resources/clients-get-resultingClientId-worker.js', scope)
.then((registration) => {
innerSwReg = registration;
add_completion_callback(function() { registration.unregister(); });
return wait_for_state(t, registration.installing, 'activated');
})
.then(() => {
// load frame and get resulting client id
let channel = new MessageChannel();
innerSw = innerSwReg.active;
let p = new Promise(resolve => {
function getResultingClientId(e) {
if (e.data.msg == 'getResultingClientId') {
const { resultingClientId } = e.data;
channel.port1.removeEventListener('message', getResultingClientId);
resolve({ resultingClientId, port: channel.port1 });
}
}
channel.port1.onmessage = getResultingClientId;
});
return with_iframe(scope).then((iframe) => {
innerSw.postMessage(
{ port: channel.port2, msg: 'getResultingClientId' },
[channel.port2],
);
frame = iframe;
frame.focus();
add_completion_callback(() => iframe.remove());
return p;
});
})
.then(({ resultingClientId, port }) => {
// query service worker for clients.get(resultingClientId)
let channel = new MessageChannel();
let p = new Promise(resolve => {
function getIsResultingClientUndefined(e) {
if (e.data.msg == 'getIsResultingClientUndefined') {
let { isResultingClientUndefined } = e.data;
port.removeEventListener('message', getIsResultingClientUndefined);
resolve(isResultingClientUndefined);
}
}
port.onmessage = getIsResultingClientUndefined;
});
innerSw.postMessage(
{ port: channel.port2, msg: 'getIsResultingClientUndefined', resultingClientId },
[channel.port2],
);
return p;
})
.then((isResultingClientUndefined) => {
assert_false(isResultingClientUndefined, 'Clients.get(FetchEvent.resultingClientId) resolved with a Client');
});
}, 'Test successful Clients.get(FetchEvent.resultingClientId)');
promise_test((t) => {
const scope = 'resources/simple.html?fail';
const outerSwContainer = navigator.serviceWorker;
let innerSwReg = null;
let innerSw = null;
return service_worker_unregister_and_register(
t, 'resources/clients-get-resultingClientId-worker.js', scope)
.then((registration) => {
innerSwReg = registration;
add_completion_callback(function() { registration.unregister(); });
return wait_for_state(t, registration.installing, 'activated');
})
.then(() => {
// load frame, destroying it while loading, and get resulting client id
innerSw = innerSwReg.active;
let iframe = document.createElement('iframe');
iframe.className = 'test-iframe';
iframe.src = scope;
function destroyIframe(e) {
if (e.data.msg == 'destroyResultingClient') {
iframe.remove();
iframe = null;
innerSw.postMessage({ msg: 'resultingClientDestroyed' });
}
}
outerSwContainer.addEventListener('message', destroyIframe);
let p = new Promise(resolve => {
function resultingClientDestroyedAck(e) {
if (e.data.msg == 'resultingClientDestroyedAck') {
let { resultingDestroyedClientId } = e.data;
outerSwContainer.removeEventListener('message', resultingClientDestroyedAck);
resolve(resultingDestroyedClientId);
}
}
outerSwContainer.addEventListener('message', resultingClientDestroyedAck);
});
document.body.appendChild(iframe);
return p;
})
.then((resultingDestroyedClientId) => {
// query service worker for clients.get(resultingDestroyedClientId)
let channel = new MessageChannel();
let p = new Promise((resolve, reject) => {
function getIsResultingClientUndefined(e) {
if (e.data.msg == 'getIsResultingClientUndefined') {
let { isResultingClientUndefined } = e.data;
channel.port1.removeEventListener('message', getIsResultingClientUndefined);
resolve(isResultingClientUndefined);
}
}
channel.port1.onmessage = getIsResultingClientUndefined;
});
innerSw.postMessage(
{ port: channel.port2, msg: 'getIsResultingClientUndefined', resultingClientId: resultingDestroyedClientId },
[channel.port2],
);
return p;
})
.then((isResultingClientUndefined) => {
assert_true(isResultingClientUndefined, 'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`');
});
}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)');
function wait_for_clientId() {
return new Promise(function(resolve, reject) {
function get_client_id(e) {

View File

@ -116,7 +116,6 @@ promise_test(t => {
})
.then(function(response) { return response.text(); })
.then(function(response_text) {
var new_client_id = response_text.substr(17);
assert_equals(
response_text.substr(0, 15),
'Client ID Found',
@ -124,6 +123,28 @@ promise_test(t => {
});
}, 'Service Worker responds to fetch event with an existing client id');
promise_test(t => {
const page_url = 'resources/simple.html?resultingClientId';
const expected_found = 'Resulting Client ID Found';
const expected_not_found = 'Resulting Client ID Not Found';
return with_iframe(page_url)
.then(function(frame) {
t.add_cleanup(() => { frame.remove(); });
assert_equals(
frame.contentDocument.body.textContent.substr(0, expected_found.length),
expected_found,
'Service Worker should respond with an existing resulting client id for non-subresource requests');
return frame.contentWindow.fetch('resources/other.html?resultingClientId');
})
.then(function(response) { return response.text(); })
.then(function(response_text) {
assert_equals(
response_text.substr(0),
expected_not_found,
'Service Worker should respond with an empty resulting client id for subresource requests');
});
}, 'Service Worker responds to fetch event with the correct resulting client id');
promise_test(t => {
const page_url = 'resources/simple.html?ignore';
return with_iframe(page_url)

View File

@ -0,0 +1,64 @@
let savedPort = null;
let savedResultingClientId = null;
async function destroyResultingClient(e) {
const outer = await self.clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
for (let c of clientList) {
if (c.url.endsWith('clients-get.https.html')) {
c.focus();
return c;
}
}
});
const p = new Promise(resolve => {
function resultingClientDestroyed(evt) {
if (evt.data.msg == 'resultingClientDestroyed') {
self.removeEventListener('message', resultingClientDestroyed);
resolve(outer);
}
}
self.addEventListener('message', resultingClientDestroyed);
});
outer.postMessage({ msg: 'destroyResultingClient' });
return await p;
}
self.addEventListener('fetch', async (e) => {
let { resultingClientId } = e;
savedResultingClientId = resultingClientId;
if (e.request.url.endsWith('simple.html?fail')) {
e.waitUntil(new Promise(async (resolve) => {
let outer = await destroyResultingClient(e);
outer.postMessage({ msg: 'resultingClientDestroyedAck',
resultingDestroyedClientId: savedResultingClientId });
resolve();
}));
} else {
e.respondWith(fetch(e.request));
}
});
self.addEventListener('message', (e) => {
let { msg, port, resultingClientId } = e.data;
savedPort = savedPort || port;
if (msg == 'getIsResultingClientUndefined') {
self.clients.get(resultingClientId).then((client) => {
let isUndefined = typeof client == 'undefined';
savedPort.postMessage({ msg: 'getIsResultingClientUndefined',
isResultingClientUndefined: isUndefined });
});
}
if (msg == 'getResultingClientId') {
savedPort.postMessage({ msg: 'getResultingClientId',
resultingClientId: savedResultingClientId });
}
});

View File

@ -37,6 +37,16 @@ function handleClientId(event) {
event.respondWith(new Response(body));
}
function handleResultingClientId(event) {
var body;
if (event.resultingClientId !== "") {
body = 'Resulting Client ID Found: ' + event.resultingClientId;
} else {
body = 'Resulting Client ID Not Found';
}
event.respondWith(new Response(body));
}
function handleNullBody(event) {
event.respondWith(new Response());
}
@ -155,6 +165,7 @@ self.addEventListener('fetch', function(event) {
{ pattern: '?referrerPolicy', fn: handleReferrerPolicy },
{ pattern: '?referrer', fn: handleReferrer },
{ pattern: '?clientId', fn: handleClientId },
{ pattern: '?resultingClientId', fn: handleResultingClientId },
{ pattern: '?ignore', fn: function() {} },
{ pattern: '?null', fn: handleNullBody },
{ pattern: '?fetch', fn: handleFetch },