Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "CacheFile.h"
#include <algorithm>
#include <utility>
#include "CacheFileChunk.h"
#include "CacheFileInputStream.h"
#include "CacheFileOutputStream.h"
#include "CacheFileUtils.h"
#include "CacheIndex.h"
#include "CacheLog.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/glean/NetwerkCache2Metrics.h"
#include "mozilla/TelemetryHistogramEnums.h"
#include "nsComponentManagerUtils.h"
#include "nsICacheEntry.h"
#include "nsProxyRelease.h"
#include "nsThreadUtils.h"
// When CACHE_CHUNKS is defined we always cache unused chunks in mCacheChunks.
// When it is not defined, we always release the chunks ASAP, i.e. we cache
// unused chunks only when:
// - CacheFile is memory-only
// - CacheFile is still waiting for the handle
// - the chunk is preloaded
// #define CACHE_CHUNKS
namespace mozilla::net {
using CacheFileUtils::CacheFileLock;
class NotifyCacheFileListenerEvent : public Runnable {
public:
NotifyCacheFileListenerEvent(CacheFileListener* aCallback, nsresult aResult,
bool aIsNew)
: Runnable("net::NotifyCacheFileListenerEvent"),
mCallback(aCallback),
mRV(aResult),
mIsNew(aIsNew) {
LOG(
("NotifyCacheFileListenerEvent::NotifyCacheFileListenerEvent() "
"[this=%p]",
this));
}
protected:
~NotifyCacheFileListenerEvent() {
LOG(
("NotifyCacheFileListenerEvent::~NotifyCacheFileListenerEvent() "
"[this=%p]",
this));
}
public:
NS_IMETHOD Run() override {
LOG(("NotifyCacheFileListenerEvent::Run() [this=%p]", this));
mCallback->OnFileReady(mRV, mIsNew);
return NS_OK;
}
protected:
nsCOMPtr<CacheFileListener> mCallback;
nsresult mRV;
bool mIsNew;
};
class NotifyChunkListenerEvent : public Runnable {
public:
NotifyChunkListenerEvent(CacheFileChunkListener* aCallback, nsresult aResult,
uint32_t aChunkIdx, CacheFileChunk* aChunk)
: Runnable("net::NotifyChunkListenerEvent"),
mCallback(aCallback),
mRV(aResult),
mChunkIdx(aChunkIdx),
mChunk(aChunk) {
LOG(("NotifyChunkListenerEvent::NotifyChunkListenerEvent() [this=%p]",
this));
}
protected:
~NotifyChunkListenerEvent() {
LOG(("NotifyChunkListenerEvent::~NotifyChunkListenerEvent() [this=%p]",
this));
}
public:
NS_IMETHOD Run() override {
LOG(("NotifyChunkListenerEvent::Run() [this=%p]", this));
mCallback->OnChunkAvailable(mRV, mChunkIdx, mChunk);
return NS_OK;
}
protected:
nsCOMPtr<CacheFileChunkListener> mCallback;
nsresult mRV;
uint32_t mChunkIdx;
RefPtr<CacheFileChunk> mChunk;
};
class DoomFileHelper : public CacheFileIOListener {
public:
NS_DECL_THREADSAFE_ISUPPORTS
explicit DoomFileHelper(CacheFileListener* aListener)
: mListener(aListener) {}
NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override {
MOZ_CRASH("DoomFileHelper::OnFileOpened should not be called!");
return NS_ERROR_UNEXPECTED;
}
NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf,
nsresult aResult) override {
MOZ_CRASH("DoomFileHelper::OnDataWritten should not be called!");
return NS_ERROR_UNEXPECTED;
}
NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf,
nsresult aResult) override {
MOZ_CRASH("DoomFileHelper::OnDataRead should not be called!");
return NS_ERROR_UNEXPECTED;
}
NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override {
if (mListener) mListener->OnFileDoomed(aResult);
return NS_OK;
}
NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override {
MOZ_CRASH("DoomFileHelper::OnEOFSet should not be called!");
return NS_ERROR_UNEXPECTED;
}
NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle,
nsresult aResult) override {
MOZ_CRASH("DoomFileHelper::OnFileRenamed should not be called!");
return NS_ERROR_UNEXPECTED;
}
private:
virtual ~DoomFileHelper() = default;
nsCOMPtr<CacheFileListener> mListener;
};
NS_IMPL_ISUPPORTS(DoomFileHelper, CacheFileIOListener)
NS_IMPL_ADDREF(CacheFile)
NS_IMPL_RELEASE(CacheFile)
NS_INTERFACE_MAP_BEGIN(CacheFile)
NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileChunkListener)
NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileIOListener)
NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileMetadataListener)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports,
mozilla::net::CacheFileChunkListener)
NS_INTERFACE_MAP_END
CacheFile::CacheFile() : mLock(new CacheFileLock()) {
LOG(("CacheFile::CacheFile() [this=%p]", this));
}
CacheFile::~CacheFile() {
LOG(("CacheFile::~CacheFile() [this=%p]", this));
MutexAutoLock lock(mLock->Lock());
if (!mMemoryOnly && mReady && !mKill) {
// mReady flag indicates we have metadata plus in a valid state.
WriteMetadataIfNeededLocked(true);
}
}
nsresult CacheFile::Init(const nsACString& aKey, bool aCreateNew,
bool aMemoryOnly, bool aSkipSizeCheck, bool aPriority,
bool aPinned, CacheFileListener* aCallback)
MOZ_NO_THREAD_SAFETY_ANALYSIS {
MOZ_ASSERT(!mListener);
MOZ_ASSERT(!mHandle);
MOZ_ASSERT(!(aMemoryOnly && aPinned));
nsresult rv;
mKey = aKey;
mOpenAsMemoryOnly = mMemoryOnly = aMemoryOnly;
mSkipSizeCheck = aSkipSizeCheck;
mPriority = aPriority;
mPinned = aPinned;
// Some consumers (at least nsHTTPCompressConv) assume that Read() can read
// such amount of data that was announced by Available().
// CacheFileInputStream::Available() uses also preloaded chunks to compute
// number of available bytes in the input stream, so we have to make sure the
// preloadChunkCount won't change during CacheFile's lifetime since otherwise
// we could potentially release some cached chunks that was used to calculate
// available bytes but would not be available later during call to
// CacheFileInputStream::Read().
mPreloadChunkCount = CacheObserver::PreloadChunkCount();
LOG(
("CacheFile::Init() [this=%p, key=%s, createNew=%d, memoryOnly=%d, "
"priority=%d, listener=%p]",
this, mKey.get(), aCreateNew, aMemoryOnly, aPriority, aCallback));
if (mMemoryOnly) {
MOZ_ASSERT(!aCallback);
mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, false, mKey,
WrapNotNull(mLock));
mReady = true;
mDataSize = mMetadata->Offset();
return NS_OK;
}
uint32_t flags;
if (aCreateNew) {
MOZ_ASSERT(!aCallback);
flags = CacheFileIOManager::CREATE_NEW;
// make sure we can use this entry immediately
mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey,
WrapNotNull(mLock));
mReady = true;
mDataSize = mMetadata->Offset();
} else {
flags = CacheFileIOManager::CREATE;
}
if (mPriority) {
flags |= CacheFileIOManager::PRIORITY;
}
if (mPinned) {
flags |= CacheFileIOManager::PINNED;
}
mOpeningFile = true;
mListener = aCallback;
rv = CacheFileIOManager::OpenFile(mKey, flags, this);
if (NS_FAILED(rv)) {
mListener = nullptr;
mOpeningFile = false;
if (mPinned) {
LOG(
("CacheFile::Init() - CacheFileIOManager::OpenFile() failed "
"but we want to pin, fail the file opening. [this=%p]",
this));
return NS_ERROR_NOT_AVAILABLE;
}
if (aCreateNew) {
NS_WARNING("Forcing memory-only entry since OpenFile failed");
LOG(
("CacheFile::Init() - CacheFileIOManager::OpenFile() failed "
"synchronously. We can continue in memory-only mode since "
"aCreateNew == true. [this=%p]",
this));
mMemoryOnly = true;
} else if (rv == NS_ERROR_NOT_INITIALIZED) {
NS_WARNING(
"Forcing memory-only entry since CacheIOManager isn't "
"initialized.");
LOG(
("CacheFile::Init() - CacheFileIOManager isn't initialized, "
"initializing entry as memory-only. [this=%p]",
this));
mMemoryOnly = true;
mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey,
WrapNotNull(mLock));
mReady = true;
mDataSize = mMetadata->Offset();
RefPtr<NotifyCacheFileListenerEvent> ev;
ev = new NotifyCacheFileListenerEvent(aCallback, NS_OK, true);
rv = NS_DispatchToCurrentThread(ev);
NS_ENSURE_SUCCESS(rv, rv);
} else {
NS_ENSURE_SUCCESS(rv, rv);
}
}
return NS_OK;
}
void CacheFile::Key(nsACString& aKey) {
CacheFileAutoLock lock(this);
aKey = mKey;
}
bool CacheFile::IsPinned() {
CacheFileAutoLock lock(this);
return mPinned;
}
nsresult CacheFile::OnChunkRead(nsresult aResult, CacheFileChunk* aChunk) {
CacheFileAutoLock lock(this);
nsresult rv;
uint32_t index = aChunk->Index();
LOG(("CacheFile::OnChunkRead() [this=%p, rv=0x%08" PRIx32
", chunk=%p, idx=%u]",
this, static_cast<uint32_t>(aResult), aChunk, index));
if (aChunk->mDiscardedChunk) {
// We discard only unused chunks, so it must be still unused when reading
// data finishes.
MOZ_ASSERT(aChunk->mRefCnt == 2);
aChunk->mActiveChunk = false;
ReleaseOutsideLock(
RefPtr<CacheFileChunkListener>(std::move(aChunk->mFile)));
DebugOnly<bool> removed = mDiscardedChunks.RemoveElement(aChunk);
MOZ_ASSERT(removed);
return NS_OK;
}
if (NS_FAILED(aResult)) {
SetError(aResult);
}
if (HaveChunkListeners(index)) {
rv = NotifyChunkListeners(index, aResult, aChunk);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
nsresult CacheFile::OnChunkWritten(nsresult aResult, CacheFileChunk* aChunk) {
// In case the chunk was reused, made dirty and released between calls to
// CacheFileChunk::Write() and CacheFile::OnChunkWritten(), we must write
// the chunk to the disk again. When the chunk is unused and is dirty simply
// addref and release (outside the lock) the chunk which ensures that
// CacheFile::DeactivateChunk() will be called again.
RefPtr<CacheFileChunk> deactivateChunkAgain;
CacheFileAutoLock lock(this);
nsresult rv;
LOG(("CacheFile::OnChunkWritten() [this=%p, rv=0x%08" PRIx32
", chunk=%p, idx=%u]",
this, static_cast<uint32_t>(aResult), aChunk, aChunk->Index()));
MOZ_ASSERT(!mMemoryOnly);
MOZ_ASSERT(!mOpeningFile);
MOZ_ASSERT(mHandle);
if (aChunk->mDiscardedChunk) {
// We discard only unused chunks, so it must be still unused when writing
// data finishes.
MOZ_ASSERT(aChunk->mRefCnt == 2);
aChunk->mActiveChunk = false;
ReleaseOutsideLock(
RefPtr<CacheFileChunkListener>(std::move(aChunk->mFile)));
DebugOnly<bool> removed = mDiscardedChunks.RemoveElement(aChunk);
MOZ_ASSERT(removed);
return NS_OK;
}
if (NS_FAILED(aResult)) {
SetError(aResult);
}
if (NS_SUCCEEDED(aResult) && !aChunk->IsDirty()) {
// update hash value in metadata
mMetadata->SetHash(aChunk->Index(), aChunk->Hash());
}
// notify listeners if there is any
if (HaveChunkListeners(aChunk->Index())) {
// don't release the chunk since there are some listeners queued
rv = NotifyChunkListeners(aChunk->Index(), aResult, aChunk);
if (NS_SUCCEEDED(rv)) {
MOZ_ASSERT(aChunk->mRefCnt != 2);
return NS_OK;
}
}
if (aChunk->mRefCnt != 2) {
LOG(
("CacheFile::OnChunkWritten() - Chunk is still used [this=%p, chunk=%p,"
" refcnt=%" PRIuPTR "]",
this, aChunk, aChunk->mRefCnt.get()));
return NS_OK;
}
if (aChunk->IsDirty()) {
LOG(
("CacheFile::OnChunkWritten() - Unused chunk is dirty. We must go "
"through deactivation again. [this=%p, chunk=%p]",
this, aChunk));
deactivateChunkAgain = aChunk;
return NS_OK;
}
bool keepChunk = false;
if (NS_SUCCEEDED(aResult)) {
keepChunk = ShouldCacheChunk(aChunk->Index());
LOG(("CacheFile::OnChunkWritten() - %s unused chunk [this=%p, chunk=%p]",
keepChunk ? "Caching" : "Releasing", this, aChunk));
} else {
LOG(
("CacheFile::OnChunkWritten() - Releasing failed chunk [this=%p, "
"chunk=%p]",
this, aChunk));
}
RemoveChunkInternal(aChunk, keepChunk);
WriteMetadataIfNeededLocked();
return NS_OK;
}
nsresult CacheFile::OnChunkAvailable(nsresult aResult, uint32_t aChunkIdx,
CacheFileChunk* aChunk) {
MOZ_CRASH("CacheFile::OnChunkAvailable should not be called!");
return NS_ERROR_UNEXPECTED;
}
nsresult CacheFile::OnChunkUpdated(CacheFileChunk* aChunk) {
MOZ_CRASH("CacheFile::OnChunkUpdated should not be called!");
return NS_ERROR_UNEXPECTED;
}
nsresult CacheFile::OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) {
// Using an 'auto' class to perform doom or fail the listener
// outside the CacheFile's lock.
class AutoFailDoomListener {
public:
explicit AutoFailDoomListener(CacheFileHandle* aHandle)
: mHandle(aHandle), mAlreadyDoomed(false) {}
~AutoFailDoomListener() {
if (!mListener) return;
if (mHandle) {
if (mAlreadyDoomed) {
mListener->OnFileDoomed(mHandle, NS_OK);
} else {
CacheFileIOManager::DoomFile(mHandle, mListener);
}
} else {
mListener->OnFileDoomed(nullptr, NS_ERROR_NOT_AVAILABLE);
}
}
CacheFileHandle* mHandle;
nsCOMPtr<CacheFileIOListener> mListener;
bool mAlreadyDoomed;
} autoDoom(aHandle);
RefPtr<CacheFileMetadata> metadata;
nsCOMPtr<CacheFileListener> listener;
bool isNew = false;
nsresult retval = NS_OK;
{
CacheFileAutoLock lock(this);
MOZ_ASSERT(mOpeningFile);
MOZ_ASSERT((NS_SUCCEEDED(aResult) && aHandle) ||
(NS_FAILED(aResult) && !aHandle));
MOZ_ASSERT((mListener && !mMetadata) || // !createNew
(!mListener && mMetadata)); // createNew
MOZ_ASSERT(!mMemoryOnly || mMetadata); // memory-only was set on new entry
LOG(("CacheFile::OnFileOpened() [this=%p, rv=0x%08" PRIx32 ", handle=%p]",
this, static_cast<uint32_t>(aResult), aHandle));
mOpeningFile = false;
autoDoom.mListener.swap(mDoomAfterOpenListener);
if (mMemoryOnly) {
// We can be here only in case the entry was initilized as createNew and
// SetMemoryOnly() was called.
// Just don't store the handle into mHandle and exit
autoDoom.mAlreadyDoomed = true;
return NS_OK;
}
if (NS_FAILED(aResult)) {
if (mMetadata) {
// This entry was initialized as createNew, just switch to memory-only
// mode.
NS_WARNING("Forcing memory-only entry since OpenFile failed");
LOG(
("CacheFile::OnFileOpened() - CacheFileIOManager::OpenFile() "
"failed asynchronously. We can continue in memory-only mode since "
"aCreateNew == true. [this=%p]",
this));
mMemoryOnly = true;
return NS_OK;
}
if (aResult == NS_ERROR_FILE_INVALID_PATH) {
// CacheFileIOManager doesn't have mCacheDirectory, switch to
// memory-only mode.
NS_WARNING(
"Forcing memory-only entry since CacheFileIOManager doesn't "
"have mCacheDirectory.");
LOG(
("CacheFile::OnFileOpened() - CacheFileIOManager doesn't have "
"mCacheDirectory, initializing entry as memory-only. [this=%p]",
this));
mMemoryOnly = true;
mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey,
WrapNotNull(mLock));
mReady = true;
mDataSize = mMetadata->Offset();
isNew = true;
retval = NS_OK;
} else {
// CacheFileIOManager::OpenFile() failed for another reason.
isNew = false;
retval = aResult;
}
mListener.swap(listener);
} else {
mHandle = aHandle;
if (NS_FAILED(mStatus)) {
CacheFileIOManager::DoomFile(mHandle, nullptr);
}
if (mMetadata) {
InitIndexEntry();
// The entry was initialized as createNew, don't try to read metadata.
mMetadata->SetHandle(mHandle);
// Write all cached chunks, otherwise they may stay unwritten.
for (auto iter = mCachedChunks.Iter(); !iter.Done(); iter.Next()) {
uint32_t idx = iter.Key();
RefPtr<CacheFileChunk>& chunk = iter.Data();
LOG(("CacheFile::OnFileOpened() - write [this=%p, idx=%u, chunk=%p]",
this, idx, chunk.get()));
mChunks.InsertOrUpdate(idx, RefPtr{chunk});
chunk->mFile = this;
chunk->mActiveChunk = true;
MOZ_ASSERT(chunk->IsReady());
// This would be cleaner if we had an nsRefPtr constructor that took
// a RefPtr<Derived>.
ReleaseOutsideLock(std::move(chunk));
iter.Remove();
}
return NS_OK;
}
}
if (listener) {
lock.Unlock();
listener->OnFileReady(retval, isNew);
return NS_OK;
}
MOZ_ASSERT(NS_SUCCEEDED(aResult));
MOZ_ASSERT(!mMetadata);
MOZ_ASSERT(mListener);
// mMetaData is protected by a lock, but ReadMetaData has to be called
// without the lock. Alternatively we could make a
// "ReadMetaDataLocked", and temporarily unlock to call OnFileReady
metadata = mMetadata =
new CacheFileMetadata(mHandle, mKey, WrapNotNull(mLock));
}
metadata->ReadMetadata(this);
return NS_OK;
}
nsresult CacheFile::OnDataWritten(CacheFileHandle* aHandle, const char* aBuf,
nsresult aResult) {
MOZ_CRASH("CacheFile::OnDataWritten should not be called!");
return NS_ERROR_UNEXPECTED;
}
nsresult CacheFile::OnDataRead(CacheFileHandle* aHandle, char* aBuf,
nsresult aResult) {
MOZ_CRASH("CacheFile::OnDataRead should not be called!");
return NS_ERROR_UNEXPECTED;
}
nsresult CacheFile::OnMetadataRead(nsresult aResult) {
nsCOMPtr<CacheFileListener> listener;
bool isNew = false;
{
CacheFileAutoLock lock(this);
MOZ_ASSERT(mListener);
LOG(("CacheFile::OnMetadataRead() [this=%p, rv=0x%08" PRIx32 "]", this,
static_cast<uint32_t>(aResult)));
if (NS_SUCCEEDED(aResult)) {
mPinned = mMetadata->Pinned();
mReady = true;
mDataSize = mMetadata->Offset();
if (mDataSize == 0 && mMetadata->ElementsSize() == 0) {
isNew = true;
mMetadata->MarkDirty();
} else {
const char* altData =
mMetadata->GetElement(CacheFileUtils::kAltDataKey);
if (altData && (NS_FAILED(CacheFileUtils::ParseAlternativeDataInfo(
altData, &mAltDataOffset, &mAltDataType)) ||
(mAltDataOffset > mDataSize))) {
// alt-metadata cannot be parsed or alt-data offset is invalid
mMetadata->InitEmptyMetadata();
isNew = true;
mAltDataOffset = -1;
mAltDataType.Truncate();
mDataSize = 0;
} else {
PreloadChunks(0);
}
}
InitIndexEntry();
}
mListener.swap(listener);
}
listener->OnFileReady(aResult, isNew);
return NS_OK;
}
nsresult CacheFile::OnMetadataWritten(nsresult aResult) {
CacheFileAutoLock lock(this);
LOG(("CacheFile::OnMetadataWritten() [this=%p, rv=0x%08" PRIx32 "]", this,
static_cast<uint32_t>(aResult)));