import changes from `dev' branch of rmottola/Arctic-Fox:

- Backed out changeset b7653e3d5f91 (bug 1174381) for widespread assertion failures. (f529b680d)
- Bug 1174381 - ServiceWorkerManager::TeardownRunnable should be called when xpcom-shutdown notification is received, r=nsm (c744d9a80)
- Bug 1134671 Keep sqlite connection open between Cache API operations. r=ehsan (43a57decc)
- Bug 1134671: Add 'override' keyword to method Context::Data::GetConnection() (in DOM Cache code). rs=ehsan (669c6eac3)
- Bug 1164100 P1 Cache API should use correct base dir even when reusing sqlite connection. r=ehsan (c1bdf85d3)
- Bug 1164100 P2 Fix defunct assertion in Cache API ActionRunnable. r=ehsan (e345b2a76)
- Bug 1161288 - Support app:// origins on Fetch API. r=baku,nsm (9c237bcdd)
- Bug 1168135 P1 Execute Cache init Action on same target thread used for other Actions. r=ehsan (30fcee443)
- Bug 1168135 P2 Add Cache Context::Init() method. r=ehsan (41dfd427a)
- Bug 1168135 P3 Cache Context should pass shared Data container to init Action. r=ehsan (2e8f19d7c)
- Bug 1169994 Fix Cache to close connection on right thread when init is canceled. r=ehsan (ca7b96b24)
- Bug 1174768 Cache should check if QuotaManager is shutting down before calling GetOrCreate. r=janv (7b06ad874)
- Bug 1110446 P1 Create marker files when Cache API context is open. r=ehsan (bb94b92ff)
- Bug 1146612 - Add a test to ensure that Cache.put() with an existing request will reorder it in the DB; r=bkelly (228ff808c)
- Bug 1162365 - Cache API does not calculate usage in QuotaClient::InitOrigin(). r=bkelly (3fa71ee24)
- Bug 1156033 - Add some missing error handling to the DOM Cache code; r=bkelly (67cd67987)
- Bug 1118298 - Client uses unknown command property session_id. r=ato (081eb8f2d)
This commit is contained in:
2021-02-20 12:01:33 +08:00
parent 3b1d2cdd8a
commit bb3b80359f
34 changed files with 959 additions and 135 deletions
+18 -1
View File
@@ -11,6 +11,8 @@
#include "mozilla/dom/cache/Types.h"
#include "nsISupportsImpl.h"
class mozIStorageConnection;
namespace mozilla {
namespace dom {
namespace cache {
@@ -33,12 +35,27 @@ public:
Release(void) = 0;
};
// Class containing data that can be opportunistically shared between
// multiple Actions running on the same thread/Context. In theory
// this could be abstracted to a generic key/value map, but for now
// just explicitly provide accessors for the data we need.
class Data
{
public:
virtual mozIStorageConnection*
GetConnection() const = 0;
virtual void
SetConnection(mozIStorageConnection* aConn) = 0;
};
// Execute operations on the target thread. Once complete call
// Resolver::Resolve(). This can be done sync or async.
// Note: Action should hold Resolver ref until its ready to call Resolve().
// Note: The "target" thread is determined when the Action is scheduled on
// Context. The Action should not assume any particular thread is used.
virtual void RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo) = 0;
virtual void RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo,
Data* aOptionalData) = 0;
// Called on initiating thread when the Action is canceled. The Action is
// responsible for calling Resolver::Resolve() as normal; either with a
+198 -43
View File
@@ -9,11 +9,13 @@
#include "mozilla/AutoRestore.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/dom/cache/Action.h"
#include "mozilla/dom/cache/FileUtils.h"
#include "mozilla/dom/cache/Manager.h"
#include "mozilla/dom/cache/ManagerId.h"
#include "mozilla/dom/cache/OfflineStorage.h"
#include "mozilla/dom/quota/OriginOrPatternString.h"
#include "mozilla/dom/quota/QuotaManager.h"
#include "mozIStorageConnection.h"
#include "nsIFile.h"
#include "nsIPrincipal.h"
#include "nsIRunnable.h"
@@ -22,6 +24,7 @@
namespace {
using mozilla::dom::Nullable;
using mozilla::dom::cache::Action;
using mozilla::dom::cache::QuotaInfo;
using mozilla::dom::quota::Client;
using mozilla::dom::quota::OriginOrPatternString;
@@ -54,6 +57,22 @@ private:
const QuotaInfo mQuotaInfo;
};
class NullAction final : public Action
{
public:
NullAction()
{
}
virtual void
RunOnTarget(Resolver* aResolver, const QuotaInfo&, Data*) override
{
// Resolve success immediately. This Action does no actual work.
MOZ_ASSERT(aResolver);
aResolver->Resolve(NS_OK);
}
};
} // namespace
namespace mozilla {
@@ -66,6 +85,49 @@ using mozilla::dom::quota::QuotaManager;
using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
using mozilla::dom::quota::PersistenceType;
class Context::Data final : public Action::Data
{
public:
explicit Data(nsIThread* aTarget)
: mTarget(aTarget)
{
MOZ_ASSERT(mTarget);
}
virtual mozIStorageConnection*
GetConnection() const override
{
MOZ_ASSERT(mTarget == NS_GetCurrentThread());
return mConnection;
}
virtual void
SetConnection(mozIStorageConnection* aConn) override
{
MOZ_ASSERT(mTarget == NS_GetCurrentThread());
MOZ_ASSERT(!mConnection);
mConnection = aConn;
MOZ_ASSERT(mConnection);
}
private:
~Data()
{
// We could proxy release our data here, but instead just assert. The
// Context code should guarantee that we are destroyed on the target
// thread. If we're not, then QuotaManager might race and try to clear the
// origin out from under us.
MOZ_ASSERT(mTarget == NS_GetCurrentThread());
}
nsCOMPtr<nsIThread> mTarget;
nsCOMPtr<mozIStorageConnection> mConnection;
// Threadsafe counting because we're created on the PBackground thread
// and destroyed on the target IO thread.
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Context::Data)
};
// Executed to perform the complicated dance of steps necessary to initialize
// the QuotaManager. This must be performed for each origin before any disk
// IO occurrs.
@@ -74,11 +136,15 @@ class Context::QuotaInitRunnable final : public nsIRunnable
public:
QuotaInitRunnable(Context* aContext,
Manager* aManager,
Action* aQuotaIOThreadAction)
Data* aData,
nsIThread* aTarget,
Action* aInitAction)
: mContext(aContext)
, mThreadsafeHandle(aContext->CreateThreadsafeHandle())
, mManager(aManager)
, mQuotaIOThreadAction(aQuotaIOThreadAction)
, mData(aData)
, mTarget(aTarget)
, mInitAction(aInitAction)
, mInitiatingThread(NS_GetCurrentThread())
, mResult(NS_OK)
, mState(STATE_INIT)
@@ -87,7 +153,10 @@ public:
{
MOZ_ASSERT(mContext);
MOZ_ASSERT(mManager);
MOZ_ASSERT(mData);
MOZ_ASSERT(mTarget);
MOZ_ASSERT(mInitiatingThread);
MOZ_ASSERT(mInitAction);
}
nsresult Dispatch()
@@ -109,7 +178,7 @@ public:
NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
MOZ_ASSERT(!mCanceled);
mCanceled = true;
mQuotaIOThreadAction->CancelOnInitiatingThread();
mInitAction->CancelOnInitiatingThread();
}
private:
@@ -145,7 +214,7 @@ private:
{
MOZ_ASSERT(mState == STATE_COMPLETE);
MOZ_ASSERT(!mContext);
MOZ_ASSERT(!mQuotaIOThreadAction);
MOZ_ASSERT(!mInitAction);
}
enum State
@@ -154,6 +223,7 @@ private:
STATE_CALL_WAIT_FOR_OPEN_ALLOWED,
STATE_WAIT_FOR_OPEN_ALLOWED,
STATE_ENSURE_ORIGIN_INITIALIZED,
STATE_RUN_ON_TARGET,
STATE_RUNNING,
STATE_COMPLETING,
STATE_COMPLETE
@@ -165,13 +235,15 @@ private:
MOZ_ASSERT(mContext);
mContext = nullptr;
mManager = nullptr;
mQuotaIOThreadAction = nullptr;
mInitAction = nullptr;
}
nsRefPtr<Context> mContext;
nsRefPtr<ThreadsafeHandle> mThreadsafeHandle;
nsRefPtr<Manager> mManager;
nsRefPtr<Action> mQuotaIOThreadAction;
nsRefPtr<Data> mData;
nsCOMPtr<nsIThread> mTarget;
nsRefPtr<Action> mInitAction;
nsCOMPtr<nsIThread> mInitiatingThread;
nsresult mResult;
QuotaInfo mQuotaInfo;
@@ -209,9 +281,14 @@ NS_IMPL_ISUPPORTS(mozilla::dom::cache::Context::QuotaInitRunnable, nsIRunnable);
// | (Quota IO Thread) +----------------+
// +----------+------------+ |
// | |
// +----------v------------+ |
// | RunOnTarget | Resolve(error) |
// | (Target Thread) +----------------+
// +----------+------------+ |
// | |
// +---------v---------+ +------v------+
// | Running | | Completing |
// | (Quota IO Thread) +------------>(Orig Thread)|
// | (Target Thread) +------------>(Orig Thread)|
// +-------------------+ +------+------+
// |
// +-----v----+
@@ -234,7 +311,7 @@ Context::QuotaInitRunnable::Run()
{
MOZ_ASSERT(NS_IsMainThread());
if (mCanceled) {
if (mCanceled || QuotaManager::IsShuttingDown()) {
resolver->Resolve(NS_ERROR_ABORT);
break;
}
@@ -327,27 +404,40 @@ Context::QuotaInitRunnable::Run()
break;
}
mState = STATE_RUNNING;
mState = STATE_RUN_ON_TARGET;
if (!mQuotaIOThreadAction) {
resolver->Resolve(NS_OK);
break;
}
MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
mTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL)));
break;
}
// -------------------
case STATE_RUN_ON_TARGET:
{
MOZ_ASSERT(NS_GetCurrentThread() == mTarget);
mState = STATE_RUNNING;
// Execute the provided initialization Action. The Action must Resolve()
// before returning.
mQuotaIOThreadAction->RunOnTarget(resolver, mQuotaInfo);
mInitAction->RunOnTarget(resolver, mQuotaInfo, mData);
MOZ_ASSERT(resolver->Resolved());
mData = nullptr;
// If the database was opened, then we should always succeed when creating
// the marker file. If it wasn't opened successfully, then no need to
// create a marker file anyway.
if (NS_SUCCEEDED(resolver->Result())) {
MOZ_ALWAYS_TRUE(NS_SUCCEEDED(CreateMarkerFile(mQuotaInfo)));
}
break;
}
// -------------------
case STATE_COMPLETING:
{
NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
if (mQuotaIOThreadAction) {
mQuotaIOThreadAction->CompleteOnInitiatingThread(mResult);
}
mInitAction->CompleteOnInitiatingThread(mResult);
mContext->OnQuotaInit(mResult, mQuotaInfo, mOfflineStorage);
mState = STATE_COMPLETE;
@@ -391,9 +481,10 @@ class Context::ActionRunnable final : public nsIRunnable
, public Context::Activity
{
public:
ActionRunnable(Context* aContext, nsIEventTarget* aTarget, Action* aAction,
const QuotaInfo& aQuotaInfo)
ActionRunnable(Context* aContext, Data* aData, nsIEventTarget* aTarget,
Action* aAction, const QuotaInfo& aQuotaInfo)
: mContext(aContext)
, mData(aData)
, mTarget(aTarget)
, mAction(aAction)
, mQuotaInfo(aQuotaInfo)
@@ -403,9 +494,10 @@ public:
, mExecutingRunOnTarget(false)
{
MOZ_ASSERT(mContext);
// mData may be nullptr
MOZ_ASSERT(mTarget);
MOZ_ASSERT(mAction);
MOZ_ASSERT(mQuotaInfo.mDir);
// mQuotaInfo.mDir may be nullptr if QuotaInitRunnable failed
MOZ_ASSERT(mInitiatingThread);
}
@@ -492,6 +584,7 @@ private:
};
nsRefPtr<Context> mContext;
nsRefPtr<Data> mData;
nsCOMPtr<nsIEventTarget> mTarget;
nsRefPtr<Action> mAction;
const QuotaInfo mQuotaInfo;
@@ -558,7 +651,9 @@ Context::ActionRunnable::Run()
mExecutingRunOnTarget = true;
mState = STATE_RUNNING;
mAction->RunOnTarget(this, mQuotaInfo);
mAction->RunOnTarget(this, mQuotaInfo, mData);
mData = nullptr;
// Resolve was called synchronously from RunOnTarget(). We can
// immediately move to completing now since we are sure RunOnTarget()
@@ -667,10 +762,22 @@ void
Context::ThreadsafeHandle::AllowToCloseOnOwningThread()
{
MOZ_ASSERT(mOwningThread == NS_GetCurrentThread());
// A Context "closes" when its ref count drops to zero. Dropping this
// strong ref is necessary, but not sufficient for the close to occur.
// Any outstanding IO will continue and keep the Context alive. Once
// the Context is idle, it will be destroyed.
// First, tell the context to flush any target thread shared data. This
// data must be released on the target thread prior to running the Context
// destructor. This will schedule an Action which ensures that the
// ~Context() is not immediately executed when we drop the strong ref.
if (mStrongRef) {
mStrongRef->DoomTargetData();
}
// Now drop our strong ref and let Context finish running any outstanding
// Actions.
mStrongRef = nullptr;
}
@@ -701,36 +808,28 @@ Context::ThreadsafeHandle::ContextDestroyed(Context* aContext)
// static
already_AddRefed<Context>
Context::Create(Manager* aManager, Action* aQuotaIOThreadAction,
Context* aOldContext)
Context::Create(Manager* aManager, nsIThread* aTarget,
Action* aInitAction, Context* aOldContext)
{
nsRefPtr<Context> context = new Context(aManager);
// Do this here to avoid doing an AddRef() in the constructor
context->mInitRunnable = new QuotaInitRunnable(context, aManager,
aQuotaIOThreadAction);
if (aOldContext) {
aOldContext->SetNextContext(context);
} else {
context->Start();
}
nsRefPtr<Context> context = new Context(aManager, aTarget);
context->Init(aInitAction, aOldContext);
return context.forget();
}
Context::Context(Manager* aManager)
Context::Context(Manager* aManager, nsIThread* aTarget)
: mManager(aManager)
, mTarget(aTarget)
, mData(new Data(aTarget))
, mState(STATE_CONTEXT_PREINIT)
, mOrphanedData(false)
{
MOZ_ASSERT(mManager);
}
void
Context::Dispatch(nsIEventTarget* aTarget, Action* aAction)
Context::Dispatch(Action* aAction)
{
NS_ASSERT_OWNINGTHREAD(Context);
MOZ_ASSERT(aTarget);
MOZ_ASSERT(aAction);
MOZ_ASSERT(mState != STATE_CONTEXT_CANCELED);
@@ -739,13 +838,12 @@ Context::Dispatch(nsIEventTarget* aTarget, Action* aAction)
} else if (mState == STATE_CONTEXT_INIT ||
mState == STATE_CONTEXT_PREINIT) {
PendingAction* pending = mPendingActions.AppendElement();
pending->mTarget = aTarget;
pending->mAction = aAction;
return;
}
MOZ_ASSERT(STATE_CONTEXT_READY);
DispatchAction(aTarget, aAction);
DispatchAction(aAction);
}
void
@@ -826,18 +924,42 @@ Context::~Context()
{
NS_ASSERT_OWNINGTHREAD(Context);
MOZ_ASSERT(mManager);
MOZ_ASSERT(!mData);
if (mThreadsafeHandle) {
mThreadsafeHandle->ContextDestroyed(this);
}
// Note, this may set the mOrphanedData flag.
mManager->RemoveContext(this);
if (mQuotaInfo.mDir && !mOrphanedData) {
MOZ_ALWAYS_TRUE(NS_SUCCEEDED(DeleteMarkerFile(mQuotaInfo)));
}
if (mNextContext) {
mNextContext->Start();
}
}
void
Context::Init(Action* aInitAction, Context* aOldContext)
{
NS_ASSERT_OWNINGTHREAD(Context);
MOZ_ASSERT(!mInitRunnable);
// Do this here to avoid doing an AddRef() in the constructor
mInitRunnable = new QuotaInitRunnable(this, mManager, mData, mTarget,
aInitAction);
if (aOldContext) {
aOldContext->SetNextContext(this);
return;
}
Start();
}
void
Context::Start()
{
@@ -863,12 +985,17 @@ Context::Start()
}
void
Context::DispatchAction(nsIEventTarget* aTarget, Action* aAction)
Context::DispatchAction(Action* aAction, bool aDoomData)
{
NS_ASSERT_OWNINGTHREAD(Context);
nsRefPtr<ActionRunnable> runnable =
new ActionRunnable(this, aTarget, aAction, mQuotaInfo);
new ActionRunnable(this, mData, mTarget, aAction, mQuotaInfo);
if (aDoomData) {
mData = nullptr;
}
nsresult rv = runnable->Dispatch();
if (NS_FAILED(rv)) {
// Shutdown must be delayed until all Contexts are destroyed. Crash
@@ -908,7 +1035,7 @@ Context::OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo,
mState = STATE_CONTEXT_READY;
for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
DispatchAction(mPendingActions[i].mTarget, mPendingActions[i].mAction);
DispatchAction(mPendingActions[i].mAction);
}
mPendingActions.Clear();
}
@@ -931,6 +1058,14 @@ Context::RemoveActivity(Activity* aActivity)
MOZ_ASSERT(!mActivityList.Contains(aActivity));
}
void
Context::NoteOrphanedData()
{
NS_ASSERT_OWNINGTHREAD(Context);
// This may be called more than once
mOrphanedData = true;
}
already_AddRefed<Context::ThreadsafeHandle>
Context::CreateThreadsafeHandle()
{
@@ -951,6 +1086,26 @@ Context::SetNextContext(Context* aNextContext)
mNextContext = aNextContext;
}
void
Context::DoomTargetData()
{
NS_ASSERT_OWNINGTHREAD(Context);
MOZ_ASSERT(mData);
// We are about to drop our reference to the Data. We need to ensure that
// the ~Context() destructor does not run until contents of Data have been
// released on the Target thread.
// Dispatch a no-op Action. This will hold the Context alive through a
// roundtrip to the target thread and back to the owning thread. The
// ref to the Data object is cleared on the owning thread after creating
// the ActionRunnable, but before dispatching it.
nsRefPtr<Action> action = new NullAction();
DispatchAction(action, true /* doomed data */);
MOZ_ASSERT(!mData);
}
} // namespace cache
} // namespace dom
} // namespace mozilla
+18 -4
View File
@@ -111,13 +111,14 @@ public:
// will run on the QuotaManager IO thread. Note, this Action must
// be execute synchronously.
static already_AddRefed<Context>
Create(Manager* aManager, Action* aQuotaIOThreadAction, Context* aOldContext);
Create(Manager* aManager, nsIThread* aTarget,
Action* aInitAction, Context* aOldContext);
// Execute given action on the target once the quota manager has been
// initialized.
//
// Only callable from the thread that created the Context.
void Dispatch(nsIEventTarget* aTarget, Action* aAction);
void Dispatch(Action* aAction);
// Cancel any Actions running or waiting to run. This should allow the
// Context to be released and Listener::RemoveContext() will be called
@@ -151,7 +152,13 @@ public:
return mQuotaInfo;
}
// Tell the Context that some state information has been orphaned in the
// data store and won't be cleaned up. The Context will leave the marker
// in place to trigger cleanup the next times its opened.
void NoteOrphanedData();
private:
class Data;
class QuotaInitRunnable;
class ActionRunnable;
@@ -169,10 +176,11 @@ private:
nsRefPtr<Action> mAction;
};
explicit Context(Manager* aManager);
Context(Manager* aManager, nsIThread* aTarget);
~Context();
void Init(Action* aInitAction, Context* aOldContext);
void Start();
void DispatchAction(nsIEventTarget* aTarget, Action* aAction);
void DispatchAction(Action* aAction, bool aDoomData = false);
void OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo,
nsMainThreadPtrHandle<OfflineStorage>& aOfflineStorage);
@@ -182,8 +190,14 @@ private:
void
SetNextContext(Context* aNextContext);
void
DoomTargetData();
nsRefPtr<Manager> mManager;
nsCOMPtr<nsIThread> mTarget;
nsRefPtr<Data> mData;
State mState;
bool mOrphanedData;
QuotaInfo mQuotaInfo;
nsRefPtr<QuotaInitRunnable> mInitRunnable;
nsTArray<PendingAction> mPendingActions;
+23 -6
View File
@@ -35,7 +35,8 @@ DBAction::~DBAction()
}
void
DBAction::RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo)
DBAction::RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo,
Data* aOptionalData)
{
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aResolver);
@@ -60,12 +61,27 @@ DBAction::RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo)
}
nsCOMPtr<mozIStorageConnection> conn;
rv = OpenConnection(aQuotaInfo, dbDir, getter_AddRefs(conn));
if (NS_WARN_IF(NS_FAILED(rv))) {
aResolver->Resolve(rv);
return;
// Attempt to reuse the connection opened by a previous Action.
if (aOptionalData) {
conn = aOptionalData->GetConnection();
}
// If there is no previous Action, then we must open one.
if (!conn) {
rv = OpenConnection(aQuotaInfo, dbDir, getter_AddRefs(conn));
if (NS_WARN_IF(NS_FAILED(rv))) {
aResolver->Resolve(rv);
return;
}
MOZ_ASSERT(conn);
// Save this connection in the shared Data object so later Actions can
// use it. This avoids opening a new connection for every Action.
if (aOptionalData) {
aOptionalData->SetConnection(conn);
}
}
MOZ_ASSERT(conn);
RunWithDBOnTarget(aResolver, aQuotaInfo, dbDir, conn);
}
@@ -153,6 +169,7 @@ DBAction::OpenConnection(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = ss->OpenDatabaseWithFileURL(dbFileUrl, getter_AddRefs(conn));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
}
rv = db::InitializeConnection(conn);
+2 -1
View File
@@ -43,7 +43,8 @@ protected:
private:
virtual void
RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo) override;
RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo,
Data* aOptionalData) override;
nsresult OpenConnection(const QuotaInfo& aQuotaInfo, nsIFile* aQuotaDir,
mozIStorageConnection** aConnOut);
+54
View File
@@ -319,6 +319,60 @@ BodyIdToFile(nsIFile* aBaseDir, const nsID& aId, BodyFileType aType,
} // namespace
nsresult
CreateMarkerFile(const QuotaInfo& aQuotaInfo)
{
nsCOMPtr<nsIFile> marker;
nsresult rv = aQuotaInfo.mDir->Clone(getter_AddRefs(marker));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = marker->Append(NS_LITERAL_STRING("cache"));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = marker->Append(NS_LITERAL_STRING("context_open.marker"));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = marker->Create(nsIFile::NORMAL_FILE_TYPE, 0644);
if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
rv = NS_OK;
}
// Note, we don't need to fsync here. We only care about actually
// writing the marker if later modifications to the Cache are
// actually flushed to the disk. If the OS crashes before the marker
// is written then we are ensured no other changes to the Cache were
// flushed either.
return rv;
}
nsresult
DeleteMarkerFile(const QuotaInfo& aQuotaInfo)
{
nsCOMPtr<nsIFile> marker;
nsresult rv = aQuotaInfo.mDir->Clone(getter_AddRefs(marker));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = marker->Append(NS_LITERAL_STRING("cache"));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = marker->Append(NS_LITERAL_STRING("context_open.marker"));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = marker->Remove(/* recursive = */ false);
if (rv == NS_ERROR_FILE_NOT_FOUND ||
rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
rv = NS_OK;
}
// Again, no fsync is necessary. If the OS crashes before the file
// removal is flushed, then the Cache will search for stale data on
// startup. This will cause the next Cache access to be a bit slow, but
// it seems appropriate after an OS crash.
return NS_OK;
}
} // namespace cache
} // namespace dom
} // namespace mozilla
+6
View File
@@ -50,6 +50,12 @@ BodyOpen(const QuotaInfo& aQuotaInfo, nsIFile* aBaseDir, const nsID& aId,
nsresult
BodyDeleteFiles(nsIFile* aBaseDir, const nsTArray<nsID>& aIdList);
nsresult
CreateMarkerFile(const QuotaInfo& aQuotaInfo);
nsresult
DeleteMarkerFile(const QuotaInfo& aQuotaInfo);
} // namespace cache
} // namespace dom
} // namespace mozilla
+49 -20
View File
@@ -93,7 +93,7 @@ public:
}
virtual void
RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo) override
RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo, Data*) override
{
MOZ_ASSERT(aResolver);
MOZ_ASSERT(aQuotaInfo.mDir);
@@ -1303,12 +1303,13 @@ public:
// no outstanding references, delete immediately
nsRefPtr<Context> context = mManager->mContext;
// TODO: note that we need to check this cache for staleness on startup (bug 1110446)
if (!context->IsCanceled()) {
if (context->IsCanceled()) {
context->NoteOrphanedData();
} else {
context->CancelForCacheId(mCacheId);
nsRefPtr<Action> action =
new DeleteOrphanedCacheAction(mManager, mCacheId);
context->Dispatch(mManager->mIOThread, action);
context->Dispatch(action);
}
}
}
@@ -1457,9 +1458,26 @@ Manager::RemoveContext(Context* aContext)
// Whether the Context destruction was triggered from the Manager going
// idle or the underlying storage being invalidated, we should know we
// are closing before the Conext is destroyed.
// are closing before the Context is destroyed.
MOZ_ASSERT(mState == Closing);
// Before forgetting the Context, check to see if we have any outstanding
// cache or body objects waiting for deletion. If so, note that we've
// orphaned data so it will be cleaned up on the next open.
for (uint32_t i = 0; i < mCacheIdRefs.Length(); ++i) {
if (mCacheIdRefs[i].mOrphaned) {
aContext->NoteOrphanedData();
break;
}
}
for (uint32_t i = 0; i < mBodyIdRefs.Length(); ++i) {
if (mBodyIdRefs[i].mOrphaned) {
aContext->NoteOrphanedData();
break;
}
}
mContext = nullptr;
// Once the context is gone, we can immediately remove ourself from the
@@ -1511,13 +1529,18 @@ Manager::ReleaseCacheId(CacheId aCacheId)
if (mCacheIdRefs[i].mCount == 0) {
bool orphaned = mCacheIdRefs[i].mOrphaned;
mCacheIdRefs.RemoveElementAt(i);
// TODO: note that we need to check this cache for staleness on startup (bug 1110446)
nsRefPtr<Context> context = mContext;
if (orphaned && context && !context->IsCanceled()) {
context->CancelForCacheId(aCacheId);
nsRefPtr<Action> action = new DeleteOrphanedCacheAction(this,
aCacheId);
context->Dispatch(mIOThread, action);
// If the context is already gone, then orphan flag should have been
// set in RemoveContext().
if (orphaned && context) {
if (context->IsCanceled()) {
context->NoteOrphanedData();
} else {
context->CancelForCacheId(aCacheId);
nsRefPtr<Action> action = new DeleteOrphanedCacheAction(this,
aCacheId);
context->Dispatch(action);
}
}
}
MaybeAllowContextToClose();
@@ -1555,11 +1578,16 @@ Manager::ReleaseBodyId(const nsID& aBodyId)
if (mBodyIdRefs[i].mCount < 1) {
bool orphaned = mBodyIdRefs[i].mOrphaned;
mBodyIdRefs.RemoveElementAt(i);
// TODO: note that we need to check this body for staleness on startup (bug 1110446)
nsRefPtr<Context> context = mContext;
if (orphaned && context && !context->IsCanceled()) {
nsRefPtr<Action> action = new DeleteOrphanedBodyAction(aBodyId);
context->Dispatch(mIOThread, action);
// If the context is already gone, then orphan flag should have been
// set in RemoveContext().
if (orphaned && context) {
if (context->IsCanceled()) {
context->NoteOrphanedData();
} else {
nsRefPtr<Action> action = new DeleteOrphanedBodyAction(aBodyId);
context->Dispatch(action);
}
}
}
MaybeAllowContextToClose();
@@ -1635,7 +1663,7 @@ Manager::ExecuteCacheOp(Listener* aListener, CacheId aCacheId,
MOZ_CRASH("Unknown Cache operation!");
}
context->Dispatch(mIOThread, action);
context->Dispatch(action);
}
void
@@ -1682,7 +1710,7 @@ Manager::ExecuteStorageOp(Listener* aListener, Namespace aNamespace,
MOZ_CRASH("Unknown CacheStorage operation!");
}
context->Dispatch(mIOThread, action);
context->Dispatch(action);
}
void
@@ -1708,7 +1736,7 @@ Manager::ExecutePutAll(Listener* aListener, CacheId aCacheId,
aPutList, aRequestStreamList,
aResponseStreamList);
context->Dispatch(mIOThread, action);
context->Dispatch(action);
}
Manager::Manager(ManagerId* aManagerId, nsIThread* aIOThread)
@@ -1752,7 +1780,8 @@ Manager::Init(Manager* aOldManager)
// per Manager now, this lets us cleanly call Factory::Remove() once the
// Context goes away.
nsRefPtr<Action> setupAction = new SetupAction();
nsRefPtr<Context> ref = Context::Create(this, setupAction, oldContext);
nsRefPtr<Context> ref = Context::Create(this, mIOThread, setupAction,
oldContext);
mContext = ref;
}
@@ -1870,7 +1899,7 @@ Manager::NoteOrphanedBodyIdList(const nsTArray<nsID>& aDeletedBodyIdList)
nsRefPtr<Context> context = mContext;
if (!deleteNowList.IsEmpty() && context && !context->IsCanceled()) {
nsRefPtr<Action> action = new DeleteOrphanedBodyAction(deleteNowList);
context->Dispatch(mIOThread, action);
context->Dispatch(action);
}
}
+5 -3
View File
@@ -110,7 +110,7 @@ public:
InitOrigin(PersistenceType aPersistenceType, const nsACString& aGroup,
const nsACString& aOrigin, UsageInfo* aUsageInfo) override
{
return NS_OK;
return GetUsageForOrigin(aPersistenceType, aGroup, aOrigin, aUsageInfo);
}
virtual nsresult
@@ -153,6 +153,7 @@ public:
if (isDir) {
if (leafName.EqualsLiteral("morgue")) {
rv = GetBodyUsage(file, aUsageInfo);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
} else {
NS_WARNING("Unknown Cache directory found!");
}
@@ -160,10 +161,11 @@ public:
continue;
}
// Ignore transient sqlite files
// Ignore transient sqlite files and marker files
if (leafName.EqualsLiteral("caches.sqlite-journal") ||
leafName.EqualsLiteral("caches.sqlite-shm") ||
leafName.Find(NS_LITERAL_CSTRING("caches.sqlite-mj"), false, 0, 0) == 0) {
leafName.Find(NS_LITERAL_CSTRING("caches.sqlite-mj"), false, 0, 0) == 0 ||
leafName.EqualsLiteral("context_open.marker")) {
continue;
}
+2
View File
@@ -19,6 +19,7 @@ support-files =
test_cache_put.js
test_cache_requestCache.js
test_cache_delete.js
test_cache_put_reorder.js
[test_cache.html]
[test_cache_add.html]
@@ -31,3 +32,4 @@ support-files =
[test_cache_put.html]
[test_cache_requestCache.html]
[test_cache_delete.html]
[test_cache_put_reorder.html]
+20
View File
@@ -0,0 +1,20 @@
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html>
<head>
<title>Ensure that using Cache.put to overwrite an entry will change its insertion order</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
<script type="text/javascript" src="driver.js"></script>
</head>
<body>
<iframe id="frame"></iframe>
<script class="testbody" type="text/javascript">
runTests("test_cache_put_reorder.js")
.then(function() {
SimpleTest.finish();
});
</script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
var name = "putreorder" + context;
var c;
var reqs = [
"//mochi.test:8888/?foo" + context,
"//mochi.test:8888/?bar" + context,
"//mochi.test:8888/?baz" + context,
];
caches.open(name).then(function(cache) {
c = cache;
return c.addAll(reqs);
}).then(function() {
return c.put(reqs[1], new Response("overwritten"));
}).then(function() {
return c.keys();
}).then(function(keys) {
is(keys.length, 3, "Correct number of entries expected");
ok(keys[0].url.indexOf(reqs[0]) >= 0, "The first entry should be untouched");
ok(keys[2].url.indexOf(reqs[1]) >= 0, "The second entry should be moved to the end");
ok(keys[1].url.indexOf(reqs[2]) >= 0, "The third entry should now be the second one");
return c.match(reqs[1]);
}).then(function(r) {
return r.text();
}).then(function(body) {
is(body, "overwritten", "The body should be overwritten");
return caches.delete(name);
}).then(function(deleted) {
ok(deleted, "The cache should be deleted successfully");
testDone();
});
+61
View File
@@ -0,0 +1,61 @@
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html>
<head>
<title>Test Cache with QuotaManager Restart</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script class="testbody" type="text/javascript">
function resetStorage() {
return new Promise(function(resolve, reject) {
var principal = SpecialPowers.wrap(document).nodePrincipal;
var appId, inBrowser;
var nsIPrincipal = SpecialPowers.Components.interfaces.nsIPrincipal;
if (principal.appId != nsIPrincipal.UNKNOWN_APP_ID &&
principal.appId != nsIPrincipal.NO_APP_ID) {
appId = principal.appId;
inBrowser = principal.isInBrowserElement;
}
SpecialPowers.resetStorageForURI(document.documentURI, resolve, appId,
inBrowser);
});
}
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv({
"set": [["dom.caches.enabled", true],
["dom.quotaManager.testing", true]],
}, function() {
var name = 'foo';
var url = './test_cache_add.js';
var cache;
caches.open(name).then(function(c) {
cache = c;
return cache.add(url);
}).then(function() {
return resetStorage();
}).then(function() {
return cache.match(url).then(function(resp) {
ok(false, 'old cache reference should not work after reset');
}).catch(function(err) {
ok(true, 'old cache reference should not work after reset');
});
}).then(function() {
return caches.open(name);
}).then(function(c) {
cache = c;
return cache.match(url);
}).then(function(resp) {
ok(!!resp, 'cache should work after QM reset');
return caches.delete(name);
}).then(function(success) {
ok(success, 'cache should be deleted');
SimpleTest.finish();
});
});
</script>
</body>
</html>
+31 -24
View File
@@ -12,6 +12,7 @@
#include "nsIHttpChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsIHttpHeaderVisitor.h"
#include "nsIJARChannel.h"
#include "nsIScriptSecurityManager.h"
#include "nsIThreadRetargetableRequest.h"
#include "nsIUploadChannel2.h"
@@ -291,9 +292,9 @@ FetchDriver::BasicFetch()
return FailWithNetworkError();
}
if (scheme.LowerCaseEqualsLiteral("file")) {
} else if (scheme.LowerCaseEqualsLiteral("http") ||
scheme.LowerCaseEqualsLiteral("https")) {
if (scheme.LowerCaseEqualsLiteral("http") ||
scheme.LowerCaseEqualsLiteral("https") ||
scheme.LowerCaseEqualsLiteral("app")) {
return HttpFetch();
}
@@ -484,8 +485,10 @@ FetchDriver::HttpFetch(bool aCORSFlag, bool aCORSPreflightFlag, bool aAuthentica
// While the spec also gates on the client being a ServiceWorker, we can't
// infer that here. Instead we rely on callers to set the flag correctly.
if (mRequest->SkipServiceWorker()) {
nsCOMPtr<nsIHttpChannelInternal> internalChan = do_QueryInterface(httpChan);
internalChan->ForceNoIntercept();
if (httpChan) {
nsCOMPtr<nsIHttpChannelInternal> internalChan = do_QueryInterface(httpChan);
internalChan->ForceNoIntercept();
}
}
nsCOMPtr<nsIStreamListener> listener = this;
@@ -523,7 +526,7 @@ FetchDriver::HttpFetch(bool aCORSFlag, bool aCORSPreflightFlag, bool aAuthentica
unsafeHeaders,
getter_AddRefs(preflightChannel));
} else {
rv = chan->AsyncOpen(listener, nullptr);
rv = chan->AsyncOpen(listener, nullptr);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
@@ -656,28 +659,31 @@ FetchDriver::OnStartRequest(nsIRequest* aRequest,
return rv;
}
nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest);
// For now we only support HTTP.
MOZ_ASSERT(channel);
nsRefPtr<InternalResponse> response;
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
if (httpChannel) {
uint32_t responseStatus;
httpChannel->GetResponseStatus(&responseStatus);
aRequest->GetStatus(&rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
FailWithNetworkError();
return rv;
}
nsAutoCString statusText;
httpChannel->GetResponseStatusText(statusText);
uint32_t responseStatus;
channel->GetResponseStatus(&responseStatus);
response = new InternalResponse(responseStatus, statusText);
nsAutoCString statusText;
channel->GetResponseStatusText(statusText);
nsRefPtr<FillResponseHeaders> visitor = new FillResponseHeaders(response);
rv = httpChannel->VisitResponseHeaders(visitor);
if (NS_WARN_IF(NS_FAILED(rv))) {
NS_WARNING("Failed to visit all headers.");
}
} else {
nsCOMPtr<nsIJARChannel> jarChannel = do_QueryInterface(aRequest);
// If it is not an http channel, it has to be a jar one.
MOZ_ASSERT(jarChannel);
nsRefPtr<InternalResponse> response = new InternalResponse(responseStatus, statusText);
nsRefPtr<FillResponseHeaders> visitor = new FillResponseHeaders(response);
rv = channel->VisitResponseHeaders(visitor);
if (NS_WARN_IF(NS_FAILED(rv))) {
NS_WARNING("Failed to visit all headers.");
// We simulate the http protocol for jar/app requests
uint32_t responseStatus = 200;
nsAutoCString statusText;
response = new InternalResponse(responseStatus, NS_LITERAL_CSTRING("OK"));
}
// We open a pipe so that we can immediately set the pipe's read end as the
@@ -699,6 +705,7 @@ FetchDriver::OnStartRequest(nsIRequest* aRequest,
response->SetBody(pipeInputStream);
nsCOMPtr<nsISupports> securityInfo;
nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
rv = channel->GetSecurityInfo(getter_AddRefs(securityInfo));
if (securityInfo) {
response->SetSecurityInfo(securityInfo);
@@ -41,7 +41,7 @@ interface nsINotificationStorageCallback : nsISupports
/**
* Interface for notification persistence layer.
*/
[scriptable, uuid(f5145be6-e34b-468b-84da-c8c4c1ad60fe)]
[scriptable, uuid(cac01fb0-c2eb-4252-b2f4-5b1fac933bd4)]
interface nsINotificationStorage : nsISupports
{
@@ -94,6 +94,20 @@ interface nsINotificationStorage : nsISupports
*/
void delete(in DOMString origin,
in DOMString id);
/**
* Notifications are not supposed to be persistent, according to spec, at
* least for now. But we want to be able to have this behavior on B2G. Thus,
* this method will check if the origin sending the notifications is a valid
* registered app with a manifest or not. Hence, a webpage that has none
* will have its notification sent and available (via Notification.get())
* during the life time of the page.
*
* @param origin: Origin from which the notification is sent.
*
* @return boolean
*/
boolean canPut(in DOMString origin);
};
%{C++
+21 -1
View File
@@ -24,6 +24,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
"@mozilla.org/parentprocessmessagemanager;1",
"nsIMessageListenerManager");
XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage",
"@mozilla.org/notificationStorage;1",
"nsINotificationStorage");
const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir;
const NOTIFICATION_STORE_PATH =
OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json");
@@ -77,14 +81,29 @@ let NotificationDB = {
}
},
filterNonAppNotifications: function(notifications) {
let origins = Object.keys(notifications);
for (let origin of origins) {
let canPut = notificationStorage.canPut(origin);
if (!canPut) {
if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
delete notifications[origin];
}
}
return notifications;
},
// Attempt to read notification file, if it's not there we will create it.
load: function() {
var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"});
return promise.then(
function onSuccess(data) {
if (data.length > 0) {
this.notifications = JSON.parse(data);
// Preprocessing phase intends to cleanly separate any migration-related
// tasks.
this.notifications = this.filterNonAppNotifications(JSON.parse(data));
}
// populate the list of notifications by tag
if (this.notifications) {
for (var origin in this.notifications) {
@@ -97,6 +116,7 @@ let NotificationDB = {
}
}
}
this.loaded = true;
}.bind(this),
+19 -5
View File
@@ -23,6 +23,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
"@mozilla.org/childprocessmessagemanager;1",
"nsIMessageSender");
XPCOMUtils.defineLazyServiceGetter(this, "appsService",
"@mozilla.org/AppsService;1",
"nsIAppsService");
const kMessageNotificationGetAllOk = "Notification:GetAll:Return:OK";
const kMessageNotificationGetAllKo = "Notification:GetAll:Return:KO";
const kMessageNotificationSaveKo = "Notification:Save:Return:KO";
@@ -45,6 +49,7 @@ function NotificationStorage() {
this._requestCount = 0;
Services.obs.addObserver(this, "xpcom-shutdown", false);
// Register for message listeners.
this.registerListeners();
}
@@ -65,12 +70,19 @@ NotificationStorage.prototype = {
observe: function(aSubject, aTopic, aData) {
if (DEBUG) debug("Topic: " + aTopic);
if (aTopic == "xpcom-shutdown") {
if (aTopic === "xpcom-shutdown") {
Services.obs.removeObserver(this, "xpcom-shutdown");
this.unregisterListeners();
}
},
canPut: function(aOrigin) {
if (DEBUG) debug("Querying appService for: " + aOrigin);
let rv = !!appsService.getAppByManifestURL(aOrigin);
if (DEBUG) debug("appService returned: " + rv);
return rv;
},
put: function(origin, id, title, dir, lang, body, tag, icon, alertName,
data, behavior) {
if (DEBUG) { debug("PUT: " + id + ": " + title); }
@@ -105,10 +117,12 @@ NotificationStorage.prototype = {
this._byTag[origin][tag] = notification;
};
cpmm.sendAsyncMessage("Notification:Save", {
origin: origin,
notification: notification
});
if (this.canPut(origin)) {
cpmm.sendAsyncMessage("Notification:Save", {
origin: origin,
notification: notification
});
}
},
get: function(origin, tag, callback) {
@@ -0,0 +1,2 @@
application.zip contains foo.txt index.html and manifest.webapp.
Any change to one of these three files should be added to application.zip as well.
@@ -0,0 +1 @@
english sentence
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>Test app for bug 1161288</title>
<script type='application/javascript;version=1.7'>
function ok(aCondition, aMessage) {
if (aCondition) {
alert('OK: ' + aMessage);
} else {
alert('KO: ' + aMessage);
}
}
function done() {
alert('DONE');
}
function testFetchAppResource() {
return fetch('foo.txt').then((res) => {
ok(true, 'fetch should resolve');
if (res.type == 'error') {
ok(false, 'fetch failed');
}
ok(res.status == 200, 'status should be 200');
ok(res.statusText == 'OK', 'statusText should be OK');
return res.text().then((body) => {
ok(body == 'english sentence', 'body should match');
});
});
}
function testFetchInvalidAppResource() {
return fetch('foo.bar.invalid').then(() => {
ok(false, 'fetch should fail');
}, (e) => {
ok(e instanceof TypeError, 'fetch should fail');
});
}
function runTests() {
return Promise.resolve()
.then(testFetchAppResource)
.then(testFetchInvalidAppResource)
.then(done)
// Put more promise based tests here.
}
</script>
</head>
<body onload='runTests()'>
</body>
</html>
@@ -0,0 +1,5 @@
{
"name": "App",
"launch_path": "/index.html",
"description": "Test app for bug 1161288"
}
@@ -0,0 +1,6 @@
{
"name": "App",
"launch_path": "/index.html",
"description": "Test app for bug 1161288",
"package_path": "application.zip"
}
@@ -0,0 +1 @@
Content-Type: application/manifest+json
+2
View File
@@ -1,5 +1,6 @@
[DEFAULT]
support-files =
app-protocol/*
fetch_test_framework.js
test_fetch_basic.js
test_fetch_basic_http.js
@@ -13,6 +14,7 @@ support-files =
[test_headers.html]
[test_headers_mainthread.html]
[test_fetch_app_protocol.html]
[test_fetch_basic.html]
[test_fetch_basic_http.html]
[test_fetch_cors.html]
@@ -0,0 +1,126 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<!DOCTYPE HTML>
<html>
<head>
<title>Bug 1161288 - Support app:// origins on Fetch API</title>
<script type='text/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
<link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css' />
</head>
<body onload='runTests()'>
<p id='display'></p>
<div id='content' style='display: none'></div>
<pre id='test'></pre>
<script class='testbody' type='application/javascript;version=1.7'>
SimpleTest.waitForExplicitFinish();
const appManifestURL =
'http://mochi.test:8888/tests/dom/tests/mochitest/fetch/app-protocol/update.webapp';
let gApp;
function setup() {
info('Setting up');
return new Promise((resolve, reject) => {
SpecialPowers.setAllAppsLaunchable(true);
SpecialPowers.pushPrefEnv({'set': [
['dom.mozBrowserFramesEnabled', true],
['security.uri.allow_scheme_mismatch', true]
]}, () => {
SpecialPowers.pushPermissions([
{ 'type': 'webapps-manage', 'allow': 1, 'context': document },
{ 'type': 'browser', 'allow': 1, 'context': document },
{ 'type': 'embed-apps', 'allow': 1, 'context': document }
], () => {
SpecialPowers.autoConfirmAppInstall(() => {
SpecialPowers.autoConfirmAppUninstall(resolve);
});
});
});
});
}
function installApp() {
return new Promise((resolve, reject) => {
let req = navigator.mozApps.installPackage(appManifestURL);
req.onsuccess = function() {
gApp = req.result;
is(req.result.manifestURL, appManifestURL, 'app installed');
if (req.result.installState == 'installed') {
is(req.result.installState, 'installed', 'app downloaded');
resolve()
} else {
req.result.ondownloadapplied = function() {
is(req.result.installState, 'installed', 'app downloaded');
resolve();
}
}
}
req.onerror = reject;
});
}
function launchApp() {
return new Promise((resolve, reject) => {
if (!gApp) {
ok(false, 'No test application to launch');
reject();
}
let expectedOkCount = 5;
let okCount = 0;
let iframe = document.createElement('iframe');
iframe.setAttribute('mozbrowser', 'true');
iframe.setAttribute('mozapp', gApp.manifestURL);
let domParent = document.getElementById('container');
iframe.addEventListener('mozbrowsershowmodalprompt', function listener(e) {
let message = e.detail.message;
info(message);
if (/OK/.exec(message)) {
ok(true, "Message from app: " + message);
okCount++;
} else if (/KO/.exec(message)) {
ok(false, "Message from app: " + message);
} else if (/DONE/.exec(message)) {
ok(true, "Messaging from app complete");
iframe.removeEventListener('mozbrowsershowmodalprompt', listener);
domParent.removeChild(iframe);
ok(okCount == expectedOkCount, "Everything's fine");
resolve();
}
}, false);
domParent.appendChild(iframe);
SpecialPowers.wrap(iframe.contentWindow).location =
gApp.origin + gApp.manifest.launch_path;
});
}
function uninstallApp() {
return new Promise((resolve, reject) => {
if (!gApp) {
return reject();
}
let req = navigator.mozApps.mgmt.uninstall(gApp);
req.onsuccess = resolve;
req.onerror = reject;
});
}
function runTests() {
setup()
.then(installApp)
.then(launchApp)
.then(uninstallApp)
.then(SimpleTest.finish)
.catch((e) => {
ok(false, 'Unexpected error ' + e);
SimpleTest.finish();
});
}
</script>
<div id='container'></div>
</body>
</html>
@@ -44,6 +44,19 @@ var NotificationTest = (function () {
})(tests);
}
function fakeApp(aManifest) {
var aApp = {
"origin": "{mochitest}",
"manifestURL": aManifest
};
SpecialPowers.injectApp("{mochitest}", aApp);
}
function unfakeApp() {
SpecialPowers.rejectApp("{mochitest}");
}
var fakeCustomData = (function () {
var buffer = new ArrayBuffer(2);
var dv = new DataView(buffer).setInt16(0, 42, true);
@@ -107,6 +120,10 @@ var NotificationTest = (function () {
info: info,
fakeApp: fakeApp,
unfakeApp: unfakeApp,
customDataMatches: function(dataObj) {
var url = "http://www.domain.com";
try {
@@ -8,3 +8,6 @@ support-files =
[test_notification_storage.html]
[test_bug931307.html]
[test_notification_resend.html]
skip-if = e10s # On e10s, faking the app seems to be failing
[test_notification_noresend.html]
skip-if = (toolkit == 'gonk') # Mochitest on Gonk registers an app manifest that messes with the logic
@@ -0,0 +1,88 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Testing mozResendAllNotifications() resend behavior for Pages</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="MockServices.js"></script>
<script type="text/javascript" src="NotificationTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1159128">Bug 1159128</a>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
<script type="text/javascript">
var info = NotificationTest.info;
var notifications = [];
SimpleTest.requestFlakyTimeout("untriaged");
var steps = [
function (done) {
if (window.Notification) {
SpecialPowers.pushPrefEnv({"set": [
["dom.ignore_webidl_scope_checks", true],
]}, done);
} else {
ok(true, "Notifications are not enabled on the platform.");
done();
}
},
function (done) {
info("Test that we have mozChromeNotifications API");
ok(('mozChromeNotifications' in navigator), "should have mozChromeNotifications API");
ok(('mozResendAllNotifications' in navigator.mozChromeNotifications), "should have mozResendAllNotifications()");
done();
},
function (done) {
info("Making sure we have no previous notification pending");
var promise = Notification.get();
promise.then(function (notifications) {
is(notifications.length, 0, "notifications are all cleaned");
done();
});
},
// The notification is expected to be created and living properly
// so it will be accessible via Notification.get(), but NotificationStorage
// should not have sent it to NotificationDB.
function (done) {
info("Sending one notification");
var notif = new Notification("title");
ok(notif, "Notification object is valid");
notifications.push(notif);
var promise = Notification.get();
promise.then(function (notifications) {
is(notifications.length, 1, "one notification has been sent");
done();
});
},
// mozResendAllNotifications will poke directly NotificationDB, so we
// expect our notification to NOT have been put there and thus not being
// resent.
function (done) {
info("Trying to resend the notification");
var notif = notifications.pop();
notif.onclose = function() {
done();
};
navigator.mozChromeNotifications.mozResendAllNotifications(function(number) {
is(number, 0, "No notification resent");
notif.close();
});
}
];
MockServices.register();
NotificationTest.run(steps, function () {
MockServices.unregister();
});
</script>
</body>
</html>
@@ -1,7 +1,7 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Testing mozResendAllNotifications() resend behavior</title>
<title>Testing mozResendAllNotifications() resend behavior for Apps</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="MockServices.js"></script>
<script type="text/javascript" src="NotificationTest.js"></script>
@@ -21,17 +21,6 @@
SimpleTest.requestFlakyTimeout("untriaged");
var steps = [
function (done) {
if (window.Notification) {
SpecialPowers.pushPrefEnv({"set": [
["dom.ignore_webidl_scope_checks", true],
]}, done);
} else {
ok(true, "Notifications are not enabled on the platform.");
done();
}
},
function (done) {
info("Set manifestURL");
var request = window.navigator.mozApps.getSelf();
@@ -46,6 +35,18 @@
};
},
function (done) {
if (window.Notification) {
NotificationTest.fakeApp(manifestURL);
SpecialPowers.pushPrefEnv({"set": [
["dom.ignore_webidl_scope_checks", true],
]}, done);
} else {
ok(true, "Notifications are not enabled on the platform.");
done();
}
},
function (done) {
info("Test that we have mozChromeNotifications API");
ok(('mozChromeNotifications' in navigator), "should have mozChromeNotifications API");
@@ -194,11 +195,12 @@
notif2.close();
});
}
},
];
MockServices.register();
NotificationTest.run(steps, function () {
NotificationTest.unfakeApp();
MockServices.unregister();
});
</script>
+24 -12
View File
@@ -388,12 +388,20 @@ ServiceWorkerManager::~ServiceWorkerManager()
{
// The map will assert if it is not empty when destroyed.
mRegistrationInfos.Clear();
MOZ_ASSERT(!mActor);
}
void
ServiceWorkerManager::Init()
{
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
DebugOnly<nsresult> rv;
rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false /* ownsWeak */);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
if (XRE_GetProcessType() == GeckoProcessType_Default) {
nsRefPtr<ServiceWorkerRegistrar> swr = ServiceWorkerRegistrar::Get();
MOZ_ASSERT(swr);
@@ -402,11 +410,8 @@ ServiceWorkerManager::Init()
swr->GetRegistrations(data);
LoadRegistrations(data);
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
DebugOnly<nsresult> rv;
rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false /* ownsWeak */);
MOZ_ASSERT(NS_SUCCEEDED(rv));
rv = obs->AddObserver(this, PURGE_SESSION_HISTORY, false /* ownsWeak */);
MOZ_ASSERT(NS_SUCCEEDED(rv));
rv = obs->AddObserver(this, PURGE_DOMAIN_DATA, false /* ownsWeak */);
@@ -2319,7 +2324,7 @@ private:
nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
// We could be shutting down.
// Could it be that we are shutting down.
if (swm->mActor) {
swm->mActor->SendUnregister(principalInfo, NS_ConvertUTF8toUTF16(mScope));
}
@@ -4414,21 +4419,22 @@ ServiceWorkerManager::Observe(nsISupports* aSubject,
const char* aTopic,
const char16_t* aData)
{
MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
if (strcmp(aTopic, PURGE_SESSION_HISTORY) == 0) {
MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
RemoveAll();
PropagateRemoveAll();
return NS_OK;
}
if (strcmp(aTopic, PURGE_DOMAIN_DATA) == 0) {
MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
nsAutoString domain(aData);
RemoveAndPropagate(NS_ConvertUTF16toUTF8(domain));
return NS_OK;
}
if (strcmp(aTopic, WEBAPPS_CLEAR_DATA) == 0) {
MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
nsCOMPtr<mozIApplicationClearPrivateDataParams> params =
do_QueryInterface(aSubject);
if (NS_WARN_IF(!params)) {
@@ -4458,15 +4464,21 @@ ServiceWorkerManager::Observe(nsISupports* aSubject,
}
RemoveAllRegistrations(principal);
} else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
return NS_OK;
}
if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
mShuttingDown = true;
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
obs->RemoveObserver(this, PURGE_SESSION_HISTORY);
obs->RemoveObserver(this, PURGE_DOMAIN_DATA);
obs->RemoveObserver(this, WEBAPPS_CLEAR_DATA);
if (XRE_GetProcessType() == GeckoProcessType_Default) {
obs->RemoveObserver(this, PURGE_SESSION_HISTORY);
obs->RemoveObserver(this, PURGE_DOMAIN_DATA);
obs->RemoveObserver(this, WEBAPPS_CLEAR_DATA);
}
}
if (mActor) {
@@ -4477,10 +4489,10 @@ ServiceWorkerManager::Observe(nsISupports* aSubject,
unused << NS_WARN_IF(NS_FAILED(rv));
mActor = nullptr;
}
} else {
MOZ_CRASH("Received message we aren't supposed to be registered for!");
return NS_OK;
}
MOZ_CRASH("Received message we aren't supposed to be registered for!");
return NS_OK;
}
+1 -1
View File
@@ -617,7 +617,7 @@ MarionetteServerConnection.prototype = {
this.scriptTimeout = 10000;
if (aRequest && aRequest.parameters) {
this.sessionId = aRequest.parameters.session_id ? aRequest.parameters.session_id : null;
this.sessionId = aRequest.parameters.sessionId || aRequest.parameters.session_id || null;
logger.info("Session Id is set to: " + this.sessionId);
try {
this.setSessionCapabilities(aRequest.parameters.capabilities);
@@ -376,6 +376,33 @@ SpecialPowersObserverAPI.prototype = {
scope.UserCustomizations._debug = aMessage.json.value;
return;
}
case "inject-app":
{
let aAppId = aMessage.json.appId;
let aApp = aMessage.json.app;
let keys = Object.keys(Webapps.DOMApplicationRegistry.webapps);
let exists = keys.indexOf(aAppId) !== -1;
if (exists) {
return false;
}
Webapps.DOMApplicationRegistry.webapps[aAppId] = aApp;
return true;
}
case "reject-app":
{
let aAppId = aMessage.json.appId;
let keys = Object.keys(Webapps.DOMApplicationRegistry.webapps);
let exists = keys.indexOf(aAppId) !== -1;
if (!exists) {
return false;
}
delete Webapps.DOMApplicationRegistry.webapps[aAppId];
return true;
}
default:
throw new SpecialPowersException("Invalid operation for SPWebAppsService");
}
@@ -1135,6 +1135,23 @@ SpecialPowersAPI.prototype = {
});
},
// Force-registering an app in the registry
injectApp: function(aAppId, aApp) {
this._sendSyncMessage("SPWebAppService", {
op: "inject-app",
appId: aAppId,
app: aApp
});
},
// Removing app from the registry
rejectApp: function(aAppId) {
this._sendSyncMessage("SPWebAppService", {
op: "reject-app",
appId: aAppId
});
},
_proxiedObservers: {
"specialpowers-http-notify-request": function(aMessage) {
let uri = aMessage.json.uri;