From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- netwerk/cache2/CacheFileIOManager.cpp | 4447 +++++++++++++++++++++++++++++++++ 1 file changed, 4447 insertions(+) create mode 100644 netwerk/cache2/CacheFileIOManager.cpp (limited to 'netwerk/cache2/CacheFileIOManager.cpp') diff --git a/netwerk/cache2/CacheFileIOManager.cpp b/netwerk/cache2/CacheFileIOManager.cpp new file mode 100644 index 0000000000..41c775ff68 --- /dev/null +++ b/netwerk/cache2/CacheFileIOManager.cpp @@ -0,0 +1,4447 @@ +/* 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 +#include "CacheLog.h" +#include "CacheFileIOManager.h" + +#include "CacheHashUtils.h" +#include "CacheStorageService.h" +#include "CacheIndex.h" +#include "CacheFileUtils.h" +#include "nsError.h" +#include "nsThreadUtils.h" +#include "CacheFile.h" +#include "CacheObserver.h" +#include "nsIFile.h" +#include "CacheFileContextEvictor.h" +#include "nsITimer.h" +#include "nsIDirectoryEnumerator.h" +#include "nsEffectiveTLDService.h" +#include "nsIObserverService.h" +#include "nsISizeOf.h" +#include "mozilla/net/MozURL.h" +#include "mozilla/Telemetry.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsDirectoryServiceUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "private/pprio.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Preferences.h" +#include "nsNetUtil.h" + +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasksRunner.h" +# include "nsIBackgroundTasks.h" +#endif + +// include files for ftruncate (or equivalent) +#if defined(XP_UNIX) +# include +#elif defined(XP_WIN) +# include +# undef CreateFile +# undef CREATE_NEW +#else +// XXX add necessary include file for ftruncate (or equivalent) +#endif + +namespace mozilla::net { + +#define kOpenHandlesLimit 128 +#define kMetadataWriteDelay 5000 +#define kRemoveTrashStartDelay 60000 // in milliseconds +#define kSmartSizeUpdateInterval 60000 // in milliseconds + +#ifdef ANDROID +const uint32_t kMaxCacheSizeKB = 512 * 1024; // 512 MB +#else +const uint32_t kMaxCacheSizeKB = 1024 * 1024; // 1 GB +#endif +const uint32_t kMaxClearOnShutdownCacheSizeKB = 150 * 1024; // 150 MB +const auto kPurgeExtension = ".purge.bg_rm"_ns; + +bool CacheFileHandle::DispatchRelease() { + if (CacheFileIOManager::IsOnIOThreadOrCeased()) { + return false; + } + + nsCOMPtr ioTarget = CacheFileIOManager::IOTarget(); + if (!ioTarget) { + return false; + } + + nsresult rv = ioTarget->Dispatch( + NewNonOwningRunnableMethod("net::CacheFileHandle::Release", this, + &CacheFileHandle::Release), + nsIEventTarget::DISPATCH_NORMAL); + return NS_SUCCEEDED(rv); +} + +NS_IMPL_ADDREF(CacheFileHandle) +NS_IMETHODIMP_(MozExternalRefCountType) +CacheFileHandle::Release() { + nsrefcnt count = mRefCnt - 1; + if (DispatchRelease()) { + // Redispatched to the IO thread. + return count; + } + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + LOG(("CacheFileHandle::Release() [this=%p, refcnt=%" PRIuPTR "]", this, + mRefCnt.get())); + MOZ_ASSERT(0 != mRefCnt, "dup release"); + count = --mRefCnt; + NS_LOG_RELEASE(this, count, "CacheFileHandle"); + + if (0 == count) { + mRefCnt = 1; + delete (this); + return 0; + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(CacheFileHandle) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +CacheFileHandle::CacheFileHandle(const SHA1Sum::Hash* aHash, bool aPriority, + PinningStatus aPinning) + : mHash(aHash), + mIsDoomed(false), + mClosed(false), + mPriority(aPriority), + mSpecialFile(false), + mInvalid(false), + mFileExists(false), + mDoomWhenFoundPinned(false), + mDoomWhenFoundNonPinned(false), + mKilled(false), + mPinning(aPinning), + mFileSize(-1), + mFD(nullptr) { + // If we initialize mDoomed in the initialization list, that initialization is + // not guaranteeded to be atomic. Whereas this assignment here is guaranteed + // to be atomic. TSan will see this (atomic) assignment and be satisfied + // that cross-thread accesses to mIsDoomed are properly synchronized. + mIsDoomed = false; + LOG(( + "CacheFileHandle::CacheFileHandle() [this=%p, hash=%08x%08x%08x%08x%08x]", + this, LOGSHA1(aHash))); +} + +CacheFileHandle::CacheFileHandle(const nsACString& aKey, bool aPriority, + PinningStatus aPinning) + : mHash(nullptr), + mIsDoomed(false), + mClosed(false), + mPriority(aPriority), + mSpecialFile(true), + mInvalid(false), + mFileExists(false), + mDoomWhenFoundPinned(false), + mDoomWhenFoundNonPinned(false), + mKilled(false), + mPinning(aPinning), + mFileSize(-1), + mFD(nullptr), + mKey(aKey) { + // See comment above about the initialization of mIsDoomed. + mIsDoomed = false; + LOG(("CacheFileHandle::CacheFileHandle() [this=%p, key=%s]", this, + PromiseFlatCString(aKey).get())); +} + +CacheFileHandle::~CacheFileHandle() { + LOG(("CacheFileHandle::~CacheFileHandle() [this=%p]", this)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + RefPtr ioMan = CacheFileIOManager::gInstance; + if (!IsClosed() && ioMan) { + ioMan->CloseHandleInternal(this); + } +} + +void CacheFileHandle::Log() { + nsAutoCString leafName; + if (mFile) { + mFile->GetNativeLeafName(leafName); + } + + if (mSpecialFile) { + LOG( + ("CacheFileHandle::Log() - special file [this=%p, " + "isDoomed=%d, priority=%d, closed=%d, invalid=%d, " + "pinning=%" PRIu32 ", fileExists=%d, fileSize=%" PRId64 + ", leafName=%s, key=%s]", + this, bool(mIsDoomed), bool(mPriority), bool(mClosed), bool(mInvalid), + static_cast(mPinning), bool(mFileExists), int64_t(mFileSize), + leafName.get(), mKey.get())); + } else { + LOG( + ("CacheFileHandle::Log() - entry file [this=%p, " + "hash=%08x%08x%08x%08x%08x, " + "isDoomed=%d, priority=%d, closed=%d, invalid=%d, " + "pinning=%" PRIu32 ", fileExists=%d, fileSize=%" PRId64 + ", leafName=%s, key=%s]", + this, LOGSHA1(mHash), bool(mIsDoomed), bool(mPriority), bool(mClosed), + bool(mInvalid), static_cast(mPinning), bool(mFileExists), + int64_t(mFileSize), leafName.get(), mKey.get())); + } +} + +uint32_t CacheFileHandle::FileSizeInK() const { + MOZ_ASSERT(mFileSize != -1); + uint64_t size64 = mFileSize; + + size64 += 0x3FF; + size64 >>= 10; + + uint32_t size; + if (size64 >> 32) { + NS_WARNING( + "CacheFileHandle::FileSizeInK() - FileSize is too large, " + "truncating to PR_UINT32_MAX"); + size = PR_UINT32_MAX; + } else { + size = static_cast(size64); + } + + return size; +} + +bool CacheFileHandle::SetPinned(bool aPinned) { + LOG(("CacheFileHandle::SetPinned [this=%p, pinned=%d]", this, aPinned)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + mPinning = aPinned ? PinningStatus::PINNED : PinningStatus::NON_PINNED; + + if ((MOZ_UNLIKELY(mDoomWhenFoundPinned) && aPinned) || + (MOZ_UNLIKELY(mDoomWhenFoundNonPinned) && !aPinned)) { + LOG((" dooming, when: pinned=%d, non-pinned=%d, found: pinned=%d", + bool(mDoomWhenFoundPinned), bool(mDoomWhenFoundNonPinned), aPinned)); + + mDoomWhenFoundPinned = false; + mDoomWhenFoundNonPinned = false; + + return false; + } + + return true; +} + +// Memory reporting + +size_t CacheFileHandle::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + size_t n = 0; + nsCOMPtr sizeOf; + + sizeOf = do_QueryInterface(mFile); + if (sizeOf) { + n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + } + + n += mallocSizeOf(mFD); + n += mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf); + return n; +} + +size_t CacheFileHandle::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +/****************************************************************************** + * CacheFileHandles::HandleHashKey + *****************************************************************************/ + +void CacheFileHandles::HandleHashKey::AddHandle(CacheFileHandle* aHandle) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + mHandles.InsertElementAt(0, aHandle); +} + +void CacheFileHandles::HandleHashKey::RemoveHandle(CacheFileHandle* aHandle) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + DebugOnly found{}; + found = mHandles.RemoveElement(aHandle); + MOZ_ASSERT(found); +} + +already_AddRefed +CacheFileHandles::HandleHashKey::GetNewestHandle() { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + RefPtr handle; + if (mHandles.Length()) { + handle = mHandles[0]; + } + + return handle.forget(); +} + +void CacheFileHandles::HandleHashKey::GetHandles( + nsTArray>& aResult) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + for (uint32_t i = 0; i < mHandles.Length(); ++i) { + CacheFileHandle* handle = mHandles[i]; + aResult.AppendElement(handle); + } +} + +#ifdef DEBUG + +void CacheFileHandles::HandleHashKey::AssertHandlesState() { + for (uint32_t i = 0; i < mHandles.Length(); ++i) { + CacheFileHandle* handle = mHandles[i]; + MOZ_ASSERT(handle->IsDoomed()); + } +} + +#endif + +size_t CacheFileHandles::HandleHashKey::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + size_t n = 0; + n += mallocSizeOf(mHash.get()); + for (uint32_t i = 0; i < mHandles.Length(); ++i) { + n += mHandles[i]->SizeOfIncludingThis(mallocSizeOf); + } + + return n; +} + +/****************************************************************************** + * CacheFileHandles + *****************************************************************************/ + +CacheFileHandles::CacheFileHandles() { + LOG(("CacheFileHandles::CacheFileHandles() [this=%p]", this)); + MOZ_COUNT_CTOR(CacheFileHandles); +} + +CacheFileHandles::~CacheFileHandles() { + LOG(("CacheFileHandles::~CacheFileHandles() [this=%p]", this)); + MOZ_COUNT_DTOR(CacheFileHandles); +} + +nsresult CacheFileHandles::GetHandle(const SHA1Sum::Hash* aHash, + CacheFileHandle** _retval) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(aHash); + +#ifdef DEBUG_HANDLES + LOG(("CacheFileHandles::GetHandle() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHash))); +#endif + + // find hash entry for key + HandleHashKey* entry = mTable.GetEntry(*aHash); + if (!entry) { + LOG( + ("CacheFileHandles::GetHandle() hash=%08x%08x%08x%08x%08x " + "no handle entries found", + LOGSHA1(aHash))); + return NS_ERROR_NOT_AVAILABLE; + } + +#ifdef DEBUG_HANDLES + Log(entry); +#endif + + // Check if the entry is doomed + RefPtr handle = entry->GetNewestHandle(); + if (!handle) { + LOG( + ("CacheFileHandles::GetHandle() hash=%08x%08x%08x%08x%08x " + "no handle found %p, entry %p", + LOGSHA1(aHash), handle.get(), entry)); + return NS_ERROR_NOT_AVAILABLE; + } + + if (handle->IsDoomed()) { + LOG( + ("CacheFileHandles::GetHandle() hash=%08x%08x%08x%08x%08x " + "found doomed handle %p, entry %p", + LOGSHA1(aHash), handle.get(), entry)); + return NS_ERROR_NOT_AVAILABLE; + } + + LOG( + ("CacheFileHandles::GetHandle() hash=%08x%08x%08x%08x%08x " + "found handle %p, entry %p", + LOGSHA1(aHash), handle.get(), entry)); + + handle.forget(_retval); + return NS_OK; +} + +already_AddRefed CacheFileHandles::NewHandle( + const SHA1Sum::Hash* aHash, bool aPriority, + CacheFileHandle::PinningStatus aPinning) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(aHash); + +#ifdef DEBUG_HANDLES + LOG(("CacheFileHandles::NewHandle() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHash))); +#endif + + // find hash entry for key + HandleHashKey* entry = mTable.PutEntry(*aHash); + +#ifdef DEBUG_HANDLES + Log(entry); +#endif + +#ifdef DEBUG + entry->AssertHandlesState(); +#endif + + RefPtr handle = + new CacheFileHandle(entry->Hash(), aPriority, aPinning); + entry->AddHandle(handle); + + LOG( + ("CacheFileHandles::NewHandle() hash=%08x%08x%08x%08x%08x " + "created new handle %p, entry=%p", + LOGSHA1(aHash), handle.get(), entry)); + return handle.forget(); +} + +void CacheFileHandles::RemoveHandle(CacheFileHandle* aHandle) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(aHandle); + + if (!aHandle) { + return; + } + +#ifdef DEBUG_HANDLES + LOG(( + "CacheFileHandles::RemoveHandle() [handle=%p, hash=%08x%08x%08x%08x%08x]", + aHandle, LOGSHA1(aHandle->Hash()))); +#endif + + // find hash entry for key + HandleHashKey* entry = mTable.GetEntry(*aHandle->Hash()); + if (!entry) { + MOZ_ASSERT(CacheFileIOManager::IsShutdown(), + "Should find entry when removing a handle before shutdown"); + + LOG( + ("CacheFileHandles::RemoveHandle() hash=%08x%08x%08x%08x%08x " + "no entries found", + LOGSHA1(aHandle->Hash()))); + return; + } + +#ifdef DEBUG_HANDLES + Log(entry); +#endif + + LOG( + ("CacheFileHandles::RemoveHandle() hash=%08x%08x%08x%08x%08x " + "removing handle %p", + LOGSHA1(entry->Hash()), aHandle)); + entry->RemoveHandle(aHandle); + + if (entry->IsEmpty()) { + LOG( + ("CacheFileHandles::RemoveHandle() hash=%08x%08x%08x%08x%08x " + "list is empty, removing entry %p", + LOGSHA1(entry->Hash()), entry)); + mTable.RemoveEntry(entry); + } +} + +void CacheFileHandles::GetAllHandles( + nsTArray>* _retval) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->GetHandles(*_retval); + } +} + +void CacheFileHandles::GetActiveHandles( + nsTArray>* _retval) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + RefPtr handle = iter.Get()->GetNewestHandle(); + MOZ_ASSERT(handle); + + if (!handle->IsDoomed()) { + _retval->AppendElement(handle); + } + } +} + +void CacheFileHandles::ClearAll() { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + mTable.Clear(); +} + +uint32_t CacheFileHandles::HandleCount() { return mTable.Count(); } + +#ifdef DEBUG_HANDLES +void CacheFileHandles::Log(CacheFileHandlesEntry* entry) { + LOG(("CacheFileHandles::Log() BEGIN [entry=%p]", entry)); + + nsTArray> array; + aEntry->GetHandles(array); + + for (uint32_t i = 0; i < array.Length(); ++i) { + CacheFileHandle* handle = array[i]; + handle->Log(); + } + + LOG(("CacheFileHandles::Log() END [entry=%p]", entry)); +} +#endif + +// Memory reporting + +size_t CacheFileHandles::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + return mTable.SizeOfExcludingThis(mallocSizeOf); +} + +// Events + +class ShutdownEvent : public Runnable, nsITimerCallback { + NS_DECL_ISUPPORTS_INHERITED + public: + ShutdownEvent() : Runnable("net::ShutdownEvent") {} + + protected: + ~ShutdownEvent() = default; + + public: + NS_IMETHOD Run() override { + CacheFileIOManager::gInstance->ShutdownInternal(); + + mNotified = true; + + NS_DispatchToMainThread( + NS_NewRunnableFunction("CacheFileIOManager::ShutdownEvent::Run", []() { + // This empty runnable is dispatched just in case the MT event loop + // becomes empty - we need to process a task to break out of + // SpinEventLoopUntil. + })); + + return NS_OK; + } + + NS_IMETHOD Notify(nsITimer* timer) override { + if (mNotified) { + return NS_OK; + } + + // If there is any IO blocking on the IO thread, this will + // try to cancel it. + CacheFileIOManager::gInstance->mIOThread->CancelBlockingIO(); + + // After this runs the first time, the browser_cache_max_shutdown_io_lag + // time has elapsed. The CacheIO thread may pick up more blocking IO tasks + // so we want to block those too if necessary. + mTimer->SetDelay( + StaticPrefs::browser_cache_shutdown_io_time_between_cancellations_ms()); + return NS_OK; + } + + void PostAndWait() { + nsresult rv = CacheFileIOManager::gInstance->mIOThread->Dispatch( + this, + CacheIOThread::WRITE); // When writes and closing of handles is done + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // If we failed to post the even there's no reason to go into the loop + // because mNotified will never be set to true. + if (NS_FAILED(rv)) { + NS_WARNING("Posting ShutdownEvent task failed"); + return; + } + + rv = NS_NewTimerWithCallback( + getter_AddRefs(mTimer), this, + StaticPrefs::browser_cache_max_shutdown_io_lag() * 1000, + nsITimer::TYPE_REPEATING_SLACK); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + mozilla::SpinEventLoopUntil("CacheFileIOManager::ShutdownEvent"_ns, + [&]() { return bool(mNotified); }); + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + } + + protected: + Atomic mNotified{false}; + nsCOMPtr mTimer; +}; + +NS_IMPL_ISUPPORTS_INHERITED(ShutdownEvent, Runnable, nsITimerCallback) + +// Class responsible for reporting IO performance stats +class IOPerfReportEvent { + public: + explicit IOPerfReportEvent(CacheFileUtils::CachePerfStats::EDataType aType) + : mType(aType), mEventCounter(0) {} + + void Start(CacheIOThread* aIOThread) { + mStartTime = TimeStamp::Now(); + mEventCounter = aIOThread->EventCounter(); + } + + void Report(CacheIOThread* aIOThread) { + if (mStartTime.IsNull()) { + return; + } + + // Single IO operations can take less than 1ms. So we use microseconds to + // keep a good resolution of data. + uint32_t duration = (TimeStamp::Now() - mStartTime).ToMicroseconds(); + + // This is a simple prefiltering of values that might differ a lot from the + // average value. Do not add the value to the filtered stats when the event + // had to wait in a long queue. + uint32_t eventCounter = aIOThread->EventCounter(); + bool shortOnly = eventCounter - mEventCounter >= 5; + + CacheFileUtils::CachePerfStats::AddValue(mType, duration, shortOnly); + } + + protected: + CacheFileUtils::CachePerfStats::EDataType mType; + TimeStamp mStartTime; + uint32_t mEventCounter; +}; + +class OpenFileEvent : public Runnable, public IOPerfReportEvent { + public: + OpenFileEvent(const nsACString& aKey, uint32_t aFlags, + CacheFileIOListener* aCallback) + : Runnable("net::OpenFileEvent"), + IOPerfReportEvent(CacheFileUtils::CachePerfStats::IO_OPEN), + mFlags(aFlags), + mCallback(aCallback), + mKey(aKey) { + mIOMan = CacheFileIOManager::gInstance; + if (!(mFlags & CacheFileIOManager::SPECIAL_FILE)) { + Start(mIOMan->mIOThread); + } + } + + protected: + ~OpenFileEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv = NS_OK; + + if (!(mFlags & CacheFileIOManager::SPECIAL_FILE)) { + SHA1Sum sum; + sum.update(mKey.BeginReading(), mKey.Length()); + sum.finish(mHash); + } + + if (!mIOMan) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + if (mFlags & CacheFileIOManager::SPECIAL_FILE) { + rv = mIOMan->OpenSpecialFileInternal(mKey, mFlags, + getter_AddRefs(mHandle)); + } else { + rv = mIOMan->OpenFileInternal(&mHash, mKey, mFlags, + getter_AddRefs(mHandle)); + if (NS_SUCCEEDED(rv)) { + Report(mIOMan->mIOThread); + } + } + mIOMan = nullptr; + if (mHandle) { + if (mHandle->Key().IsEmpty()) { + mHandle->Key() = mKey; + } + } + } + + mCallback->OnFileOpened(mHandle, rv); + return NS_OK; + } + + protected: + SHA1Sum::Hash mHash{}; + uint32_t mFlags; + nsCOMPtr mCallback; + RefPtr mIOMan; + RefPtr mHandle; + nsCString mKey; +}; + +class ReadEvent : public Runnable, public IOPerfReportEvent { + public: + ReadEvent(CacheFileHandle* aHandle, int64_t aOffset, char* aBuf, + int32_t aCount, CacheFileIOListener* aCallback) + : Runnable("net::ReadEvent"), + IOPerfReportEvent(CacheFileUtils::CachePerfStats::IO_READ), + mHandle(aHandle), + mOffset(aOffset), + mBuf(aBuf), + mCount(aCount), + mCallback(aCallback) { + if (!mHandle->IsSpecialFile()) { + Start(CacheFileIOManager::gInstance->mIOThread); + } + } + + protected: + ~ReadEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed() || (mCallback && mCallback->IsKilled())) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->ReadInternal(mHandle, mOffset, mBuf, + mCount); + if (NS_SUCCEEDED(rv)) { + Report(CacheFileIOManager::gInstance->mIOThread); + } + } + + mCallback->OnDataRead(mHandle, mBuf, rv); + return NS_OK; + } + + protected: + RefPtr mHandle; + int64_t mOffset; + char* mBuf; + int32_t mCount; + nsCOMPtr mCallback; +}; + +class WriteEvent : public Runnable, public IOPerfReportEvent { + public: + WriteEvent(CacheFileHandle* aHandle, int64_t aOffset, const char* aBuf, + int32_t aCount, bool aValidate, bool aTruncate, + CacheFileIOListener* aCallback) + : Runnable("net::WriteEvent"), + IOPerfReportEvent(CacheFileUtils::CachePerfStats::IO_WRITE), + mHandle(aHandle), + mOffset(aOffset), + mBuf(aBuf), + mCount(aCount), + mValidate(aValidate), + mTruncate(aTruncate), + mCallback(aCallback) { + if (!mHandle->IsSpecialFile()) { + Start(CacheFileIOManager::gInstance->mIOThread); + } + } + + protected: + ~WriteEvent() { + if (!mCallback && mBuf) { + free(const_cast(mBuf)); + } + } + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed() || (mCallback && mCallback->IsKilled())) { + // We usually get here only after the internal shutdown + // (i.e. mShuttingDown == true). Pretend write has succeeded + // to avoid any past-shutdown file dooming. + rv = (CacheObserver::IsPastShutdownIOLag() || + CacheFileIOManager::gInstance->mShuttingDown) + ? NS_OK + : NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->WriteInternal( + mHandle, mOffset, mBuf, mCount, mValidate, mTruncate); + if (NS_SUCCEEDED(rv)) { + Report(CacheFileIOManager::gInstance->mIOThread); + } + if (NS_FAILED(rv) && !mCallback) { + // No listener is going to handle the error, doom the file + CacheFileIOManager::gInstance->DoomFileInternal(mHandle); + } + } + if (mCallback) { + mCallback->OnDataWritten(mHandle, mBuf, rv); + } else { + free(const_cast(mBuf)); + mBuf = nullptr; + } + + return NS_OK; + } + + protected: + RefPtr mHandle; + int64_t mOffset; + const char* mBuf; + int32_t mCount; + bool mValidate : 1; + bool mTruncate : 1; + nsCOMPtr mCallback; +}; + +class DoomFileEvent : public Runnable { + public: + DoomFileEvent(CacheFileHandle* aHandle, CacheFileIOListener* aCallback) + : Runnable("net::DoomFileEvent"), + mCallback(aCallback), + mHandle(aHandle) {} + + protected: + ~DoomFileEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed()) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->DoomFileInternal(mHandle); + } + + if (mCallback) { + mCallback->OnFileDoomed(mHandle, rv); + } + + return NS_OK; + } + + protected: + nsCOMPtr mCallback; + nsCOMPtr mTarget; + RefPtr mHandle; +}; + +class DoomFileByKeyEvent : public Runnable { + public: + DoomFileByKeyEvent(const nsACString& aKey, CacheFileIOListener* aCallback) + : Runnable("net::DoomFileByKeyEvent"), mCallback(aCallback) { + SHA1Sum sum; + sum.update(aKey.BeginReading(), aKey.Length()); + sum.finish(mHash); + + mIOMan = CacheFileIOManager::gInstance; + } + + protected: + ~DoomFileByKeyEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (!mIOMan) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = mIOMan->DoomFileByKeyInternal(&mHash); + mIOMan = nullptr; + } + + if (mCallback) { + mCallback->OnFileDoomed(nullptr, rv); + } + + return NS_OK; + } + + protected: + SHA1Sum::Hash mHash{}; + nsCOMPtr mCallback; + RefPtr mIOMan; +}; + +class ReleaseNSPRHandleEvent : public Runnable { + public: + explicit ReleaseNSPRHandleEvent(CacheFileHandle* aHandle) + : Runnable("net::ReleaseNSPRHandleEvent"), mHandle(aHandle) {} + + protected: + ~ReleaseNSPRHandleEvent() = default; + + public: + NS_IMETHOD Run() override { + if (!mHandle->IsClosed()) { + CacheFileIOManager::gInstance->MaybeReleaseNSPRHandleInternal(mHandle); + } + + return NS_OK; + } + + protected: + RefPtr mHandle; +}; + +class TruncateSeekSetEOFEvent : public Runnable { + public: + TruncateSeekSetEOFEvent(CacheFileHandle* aHandle, int64_t aTruncatePos, + int64_t aEOFPos, CacheFileIOListener* aCallback) + : Runnable("net::TruncateSeekSetEOFEvent"), + mHandle(aHandle), + mTruncatePos(aTruncatePos), + mEOFPos(aEOFPos), + mCallback(aCallback) {} + + protected: + ~TruncateSeekSetEOFEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed() || (mCallback && mCallback->IsKilled())) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->TruncateSeekSetEOFInternal( + mHandle, mTruncatePos, mEOFPos); + } + + if (mCallback) { + mCallback->OnEOFSet(mHandle, rv); + } + + return NS_OK; + } + + protected: + RefPtr mHandle; + int64_t mTruncatePos; + int64_t mEOFPos; + nsCOMPtr mCallback; +}; + +class RenameFileEvent : public Runnable { + public: + RenameFileEvent(CacheFileHandle* aHandle, const nsACString& aNewName, + CacheFileIOListener* aCallback) + : Runnable("net::RenameFileEvent"), + mHandle(aHandle), + mNewName(aNewName), + mCallback(aCallback) {} + + protected: + ~RenameFileEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed()) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->RenameFileInternal(mHandle, mNewName); + } + + if (mCallback) { + mCallback->OnFileRenamed(mHandle, rv); + } + + return NS_OK; + } + + protected: + RefPtr mHandle; + nsCString mNewName; + nsCOMPtr mCallback; +}; + +class InitIndexEntryEvent : public Runnable { + public: + InitIndexEntryEvent(CacheFileHandle* aHandle, + OriginAttrsHash aOriginAttrsHash, bool aAnonymous, + bool aPinning) + : Runnable("net::InitIndexEntryEvent"), + mHandle(aHandle), + mOriginAttrsHash(aOriginAttrsHash), + mAnonymous(aAnonymous), + mPinning(aPinning) {} + + protected: + ~InitIndexEntryEvent() = default; + + public: + NS_IMETHOD Run() override { + if (mHandle->IsClosed() || mHandle->IsDoomed()) { + return NS_OK; + } + + CacheIndex::InitEntry(mHandle->Hash(), mOriginAttrsHash, mAnonymous, + mPinning); + + // We cannot set the filesize before we init the entry. If we're opening + // an existing entry file, frecency will be set after parsing the entry + // file, but we must set the filesize here since nobody is going to set it + // if there is no write to the file. + uint32_t sizeInK = mHandle->FileSizeInK(); + CacheIndex::UpdateEntry(mHandle->Hash(), nullptr, nullptr, nullptr, nullptr, + nullptr, &sizeInK); + + return NS_OK; + } + + protected: + RefPtr mHandle; + OriginAttrsHash mOriginAttrsHash; + bool mAnonymous; + bool mPinning; +}; + +class UpdateIndexEntryEvent : public Runnable { + public: + UpdateIndexEntryEvent(CacheFileHandle* aHandle, const uint32_t* aFrecency, + const bool* aHasAltData, const uint16_t* aOnStartTime, + const uint16_t* aOnStopTime, + const uint8_t* aContentType) + : Runnable("net::UpdateIndexEntryEvent"), + mHandle(aHandle), + mHasFrecency(false), + mHasHasAltData(false), + mHasOnStartTime(false), + mHasOnStopTime(false), + mHasContentType(false), + mFrecency(0), + mHasAltData(false), + mOnStartTime(0), + mOnStopTime(0), + mContentType(nsICacheEntry::CONTENT_TYPE_UNKNOWN) { + if (aFrecency) { + mHasFrecency = true; + mFrecency = *aFrecency; + } + if (aHasAltData) { + mHasHasAltData = true; + mHasAltData = *aHasAltData; + } + if (aOnStartTime) { + mHasOnStartTime = true; + mOnStartTime = *aOnStartTime; + } + if (aOnStopTime) { + mHasOnStopTime = true; + mOnStopTime = *aOnStopTime; + } + if (aContentType) { + mHasContentType = true; + mContentType = *aContentType; + } + } + + protected: + ~UpdateIndexEntryEvent() = default; + + public: + NS_IMETHOD Run() override { + if (mHandle->IsClosed() || mHandle->IsDoomed()) { + return NS_OK; + } + + CacheIndex::UpdateEntry(mHandle->Hash(), + mHasFrecency ? &mFrecency : nullptr, + mHasHasAltData ? &mHasAltData : nullptr, + mHasOnStartTime ? &mOnStartTime : nullptr, + mHasOnStopTime ? &mOnStopTime : nullptr, + mHasContentType ? &mContentType : nullptr, nullptr); + return NS_OK; + } + + protected: + RefPtr mHandle; + + bool mHasFrecency; + bool mHasHasAltData; + bool mHasOnStartTime; + bool mHasOnStopTime; + bool mHasContentType; + + uint32_t mFrecency; + bool mHasAltData; + uint16_t mOnStartTime; + uint16_t mOnStopTime; + uint8_t mContentType; +}; + +class MetadataWriteScheduleEvent : public Runnable { + public: + enum EMode { SCHEDULE, UNSCHEDULE, SHUTDOWN } mMode; + + RefPtr mFile; + RefPtr mIOMan; + + MetadataWriteScheduleEvent(CacheFileIOManager* aManager, CacheFile* aFile, + EMode aMode) + : Runnable("net::MetadataWriteScheduleEvent"), + mMode(aMode), + mFile(aFile), + mIOMan(aManager) {} + + virtual ~MetadataWriteScheduleEvent() = default; + + NS_IMETHOD Run() override { + RefPtr ioMan = CacheFileIOManager::gInstance; + if (!ioMan) { + NS_WARNING( + "CacheFileIOManager already gone in " + "MetadataWriteScheduleEvent::Run()"); + return NS_OK; + } + + switch (mMode) { + case SCHEDULE: + ioMan->ScheduleMetadataWriteInternal(mFile); + break; + case UNSCHEDULE: + ioMan->UnscheduleMetadataWriteInternal(mFile); + break; + case SHUTDOWN: + ioMan->ShutdownMetadataWriteSchedulingInternal(); + break; + } + return NS_OK; + } +}; + +StaticRefPtr CacheFileIOManager::gInstance; + +NS_IMPL_ISUPPORTS(CacheFileIOManager, nsITimerCallback, nsINamed) + +CacheFileIOManager::CacheFileIOManager() + +{ + LOG(("CacheFileIOManager::CacheFileIOManager [this=%p]", this)); + MOZ_ASSERT(!gInstance, "multiple CacheFileIOManager instances!"); +} + +CacheFileIOManager::~CacheFileIOManager() { + LOG(("CacheFileIOManager::~CacheFileIOManager [this=%p]", this)); +} + +// static +nsresult CacheFileIOManager::Init() { + LOG(("CacheFileIOManager::Init()")); + + MOZ_ASSERT(NS_IsMainThread()); + + if (gInstance) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + RefPtr ioMan = new CacheFileIOManager(); + + nsresult rv = ioMan->InitInternal(); + NS_ENSURE_SUCCESS(rv, rv); + + gInstance = std::move(ioMan); + return NS_OK; +} + +nsresult CacheFileIOManager::InitInternal() { + nsresult rv; + + mIOThread = new CacheIOThread(); + + rv = mIOThread->Init(); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Can't create background thread"); + NS_ENSURE_SUCCESS(rv, rv); + + mStartTime = TimeStamp::NowLoRes(); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::Shutdown() { + LOG(("CacheFileIOManager::Shutdown() [gInstance=%p]", gInstance.get())); + + MOZ_ASSERT(NS_IsMainThread()); + + if (!gInstance) { + return NS_ERROR_NOT_INITIALIZED; + } + + Telemetry::AutoTimer shutdownTimer; + + CacheIndex::PreShutdown(); + + ShutdownMetadataWriteScheduling(); + + RefPtr ev = new ShutdownEvent(); + ev->PostAndWait(); + + MOZ_ASSERT(gInstance->mHandles.HandleCount() == 0); + MOZ_ASSERT(gInstance->mHandlesByLastUsed.Length() == 0); + + if (gInstance->mIOThread) { + gInstance->mIOThread->Shutdown(); + } + + CacheIndex::Shutdown(); + + if (CacheObserver::ClearCacheOnShutdown()) { + Telemetry::AutoTimer + totalTimer; + gInstance->SyncRemoveAllCacheFiles(); + } + + gInstance = nullptr; + + return NS_OK; +} + +void CacheFileIOManager::ShutdownInternal() { + LOG(("CacheFileIOManager::ShutdownInternal() [this=%p]", this)); + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + // No new handles can be created after this flag is set + mShuttingDown = true; + + if (mTrashTimer) { + mTrashTimer->Cancel(); + mTrashTimer = nullptr; + } + + // close all handles and delete all associated files + nsTArray> handles; + mHandles.GetAllHandles(&handles); + handles.AppendElements(mSpecialHandles); + + for (uint32_t i = 0; i < handles.Length(); i++) { + CacheFileHandle* h = handles[i]; + h->mClosed = true; + + h->Log(); + + // Close completely written files. + MaybeReleaseNSPRHandleInternal(h); + // Don't bother removing invalid and/or doomed files to improve + // shutdown perfomrance. + // Doomed files are already in the doomed directory from which + // we never reuse files and delete the dir on next session startup. + // Invalid files don't have metadata and thus won't load anyway + // (hashes won't match). + + if (!h->IsSpecialFile() && !h->mIsDoomed && !h->mFileExists) { + CacheIndex::RemoveEntry(h->Hash()); + } + + // Remove the handle from mHandles/mSpecialHandles + if (h->IsSpecialFile()) { + mSpecialHandles.RemoveElement(h); + } else { + mHandles.RemoveHandle(h); + } + + // Pointer to the hash is no longer valid once the last handle with the + // given hash is released. Null out the pointer so that we crash if there + // is a bug in this code and we dereference the pointer after this point. + if (!h->IsSpecialFile()) { + h->mHash = nullptr; + } + } + + // Assert the table is empty. When we are here, no new handles can be added + // and handles will no longer remove them self from this table and we don't + // want to keep invalid handles here. Also, there is no lookup after this + // point to happen. + MOZ_ASSERT(mHandles.HandleCount() == 0); + + // Release trash directory enumerator + if (mTrashDirEnumerator) { + mTrashDirEnumerator->Close(); + mTrashDirEnumerator = nullptr; + } + + if (mContextEvictor) { + mContextEvictor->Shutdown(); + mContextEvictor = nullptr; + } +} + +// static +nsresult CacheFileIOManager::OnProfile() { + LOG(("CacheFileIOManager::OnProfile() [gInstance=%p]", gInstance.get())); + + RefPtr ioMan = gInstance; + if (!ioMan) { + // CacheFileIOManager::Init() failed, probably could not create the IO + // thread, just go with it... + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv; + + nsCOMPtr directory; + + CacheObserver::ParentDirOverride(getter_AddRefs(directory)); + +#if defined(MOZ_WIDGET_ANDROID) + nsCOMPtr profilelessDirectory; + char* cachePath = getenv("CACHE_DIRECTORY"); + if (!directory && cachePath && *cachePath) { + rv = NS_NewNativeLocalFile(nsDependentCString(cachePath), true, + getter_AddRefs(directory)); + if (NS_SUCCEEDED(rv)) { + // Save this directory as the profileless path. + rv = directory->Clone(getter_AddRefs(profilelessDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + + // Add profile leaf name to the directory name to distinguish + // multiple profiles Fennec supports. + nsCOMPtr profD; + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profD)); + + nsAutoCString leafName; + if (NS_SUCCEEDED(rv)) { + rv = profD->GetNativeLeafName(leafName); + } + if (NS_SUCCEEDED(rv)) { + rv = directory->AppendNative(leafName); + } + if (NS_FAILED(rv)) { + directory = nullptr; + } + } + } +#endif + + if (!directory) { + rv = NS_GetSpecialDirectory(NS_APP_CACHE_PARENT_DIR, + getter_AddRefs(directory)); + } + + if (!directory) { + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR, + getter_AddRefs(directory)); + } + + if (directory) { + rv = directory->Append(u"cache2"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + // All functions return a clone. + ioMan->mCacheDirectory.swap(directory); + +#if defined(MOZ_WIDGET_ANDROID) + if (profilelessDirectory) { + rv = profilelessDirectory->Append(u"cache2"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + ioMan->mCacheProfilelessDirectory.swap(profilelessDirectory); +#endif + + if (ioMan->mCacheDirectory) { + CacheIndex::Init(ioMan->mCacheDirectory); + } + + return NS_OK; +} + +static bool inBackgroundTask() { + MOZ_ASSERT(NS_IsMainThread(), "backgroundtasks are main thread only"); +#if defined(MOZ_BACKGROUNDTASKS) + nsCOMPtr backgroundTaskService = + do_GetService("@mozilla.org/backgroundtasks;1"); + if (!backgroundTaskService) { + return false; + } + bool isBackgroundTask = false; + backgroundTaskService->GetIsBackgroundTaskMode(&isBackgroundTask); + return isBackgroundTask; +#else + return false; +#endif +} + +// static +nsresult CacheFileIOManager::OnDelayedStartupFinished() { + // If we don't clear the cache at shutdown, or we don't use a + // background task then there's no need to dispatch a cleanup task + // at startup + if (!CacheObserver::ClearCacheOnShutdown()) { + return NS_OK; + } + if (!StaticPrefs::network_cache_shutdown_purge_in_background_task()) { + return NS_OK; + } + + // If this is a background task already, there's no need to + // dispatch another one. + if (inBackgroundTask()) { + return NS_OK; + } + + RefPtr ioMan = gInstance; + nsCOMPtr target = IOTarget(); + if (NS_WARN_IF(!ioMan || !target)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return target->Dispatch( + NS_NewRunnableFunction("CacheFileIOManager::OnDelayedStartupFinished", + [ioMan = std::move(ioMan)] { + ioMan->DispatchPurgeTask(""_ns, "0"_ns, + kPurgeExtension); + }), + nsIEventTarget::DISPATCH_NORMAL); +} + +// static +already_AddRefed CacheFileIOManager::IOTarget() { + nsCOMPtr target; + if (gInstance && gInstance->mIOThread) { + target = gInstance->mIOThread->Target(); + } + + return target.forget(); +} + +// static +already_AddRefed CacheFileIOManager::IOThread() { + RefPtr thread; + if (gInstance) { + thread = gInstance->mIOThread; + } + + return thread.forget(); +} + +// static +bool CacheFileIOManager::IsOnIOThread() { + RefPtr ioMan = gInstance; + if (ioMan && ioMan->mIOThread) { + return ioMan->mIOThread->IsCurrentThread(); + } + + return false; +} + +// static +bool CacheFileIOManager::IsOnIOThreadOrCeased() { + RefPtr ioMan = gInstance; + if (ioMan && ioMan->mIOThread) { + return ioMan->mIOThread->IsCurrentThread(); + } + + // Ceased... + return true; +} + +// static +bool CacheFileIOManager::IsShutdown() { + if (!gInstance) { + return true; + } + return gInstance->mShuttingDown; +} + +// static +nsresult CacheFileIOManager::ScheduleMetadataWrite(CacheFile* aFile) { + RefPtr ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_TRUE(!ioMan->mShuttingDown, NS_ERROR_NOT_INITIALIZED); + + RefPtr event = new MetadataWriteScheduleEvent( + ioMan, aFile, MetadataWriteScheduleEvent::SCHEDULE); + nsCOMPtr target = ioMan->IOTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + return target->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +nsresult CacheFileIOManager::ScheduleMetadataWriteInternal(CacheFile* aFile) { + MOZ_ASSERT(IsOnIOThreadOrCeased()); + + nsresult rv; + + if (!mMetadataWritesTimer) { + rv = NS_NewTimerWithCallback(getter_AddRefs(mMetadataWritesTimer), this, + kMetadataWriteDelay, nsITimer::TYPE_ONE_SHOT); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mScheduledMetadataWrites.IndexOf(aFile) != + nsTArray>::NoIndex) { + return NS_OK; + } + + mScheduledMetadataWrites.AppendElement(aFile); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::UnscheduleMetadataWrite(CacheFile* aFile) { + RefPtr ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_TRUE(!ioMan->mShuttingDown, NS_ERROR_NOT_INITIALIZED); + + RefPtr event = new MetadataWriteScheduleEvent( + ioMan, aFile, MetadataWriteScheduleEvent::UNSCHEDULE); + nsCOMPtr target = ioMan->IOTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + return target->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +void CacheFileIOManager::UnscheduleMetadataWriteInternal(CacheFile* aFile) { + MOZ_ASSERT(IsOnIOThreadOrCeased()); + + mScheduledMetadataWrites.RemoveElement(aFile); + + if (mScheduledMetadataWrites.Length() == 0 && mMetadataWritesTimer) { + mMetadataWritesTimer->Cancel(); + mMetadataWritesTimer = nullptr; + } +} + +// static +nsresult CacheFileIOManager::ShutdownMetadataWriteScheduling() { + RefPtr ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + RefPtr event = new MetadataWriteScheduleEvent( + ioMan, nullptr, MetadataWriteScheduleEvent::SHUTDOWN); + nsCOMPtr target = ioMan->IOTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + return target->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +void CacheFileIOManager::ShutdownMetadataWriteSchedulingInternal() { + MOZ_ASSERT(IsOnIOThreadOrCeased()); + + nsTArray> files = std::move(mScheduledMetadataWrites); + for (uint32_t i = 0; i < files.Length(); ++i) { + CacheFile* file = files[i]; + file->WriteMetadataIfNeeded(); + } + + if (mMetadataWritesTimer) { + mMetadataWritesTimer->Cancel(); + mMetadataWritesTimer = nullptr; + } +} + +NS_IMETHODIMP +CacheFileIOManager::Notify(nsITimer* aTimer) { + MOZ_ASSERT(IsOnIOThreadOrCeased()); + MOZ_ASSERT(mMetadataWritesTimer == aTimer); + + mMetadataWritesTimer = nullptr; + + nsTArray> files = std::move(mScheduledMetadataWrites); + for (uint32_t i = 0; i < files.Length(); ++i) { + CacheFile* file = files[i]; + file->WriteMetadataIfNeeded(); + } + + return NS_OK; +} + +NS_IMETHODIMP +CacheFileIOManager::GetName(nsACString& aName) { + aName.AssignLiteral("CacheFileIOManager"); + return NS_OK; +} + +// static +nsresult CacheFileIOManager::OpenFile(const nsACString& aKey, uint32_t aFlags, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::OpenFile() [key=%s, flags=%d, listener=%p]", + PromiseFlatCString(aKey).get(), aFlags, aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + bool priority = aFlags & CacheFileIOManager::PRIORITY; + RefPtr ev = new OpenFileEvent(aKey, aFlags, aCallback); + rv = ioMan->mIOThread->Dispatch( + ev, priority ? CacheIOThread::OPEN_PRIORITY : CacheIOThread::OPEN); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::OpenFileInternal(const SHA1Sum::Hash* aHash, + const nsACString& aKey, + uint32_t aFlags, + CacheFileHandle** _retval) { + LOG( + ("CacheFileIOManager::OpenFileInternal() [hash=%08x%08x%08x%08x%08x, " + "key=%s, flags=%d]", + LOGSHA1(aHash), PromiseFlatCString(aKey).get(), aFlags)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + CacheIOThread::Cancelable cancelable( + true /* never called for special handles */); + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) return rv; + } + + CacheFileHandle::PinningStatus pinning = + aFlags & PINNED ? CacheFileHandle::PinningStatus::PINNED + : CacheFileHandle::PinningStatus::NON_PINNED; + + nsCOMPtr file; + rv = GetFile(aHash, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr handle; + mHandles.GetHandle(aHash, getter_AddRefs(handle)); + + if ((aFlags & (OPEN | CREATE | CREATE_NEW)) == CREATE_NEW) { + if (handle) { + rv = DoomFileInternal(handle); + NS_ENSURE_SUCCESS(rv, rv); + handle = nullptr; + } + + handle = mHandles.NewHandle(aHash, aFlags & PRIORITY, pinning); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + CacheIndex::RemoveEntry(aHash); + + LOG( + ("CacheFileIOManager::OpenFileInternal() - Removing old file from " + "disk")); + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot remove old entry from the disk"); + LOG( + ("CacheFileIOManager::OpenFileInternal() - Removing old file failed" + ". [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + } + + CacheIndex::AddEntry(aHash); + handle->mFile.swap(file); + handle->mFileSize = 0; + } + + if (handle) { + handle.swap(*_retval); + return NS_OK; + } + + bool exists, evictedAsPinned = false, evictedAsNonPinned = false; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists && mContextEvictor) { + if (mContextEvictor->ContextsCount() == 0) { + mContextEvictor = nullptr; + } else { + mContextEvictor->WasEvicted(aKey, file, &evictedAsPinned, + &evictedAsNonPinned); + } + } + + if (!exists && (aFlags & (OPEN | CREATE | CREATE_NEW)) == OPEN) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (exists) { + // For existing files we determine the pinning status later, after the + // metadata gets parsed. + pinning = CacheFileHandle::PinningStatus::UNKNOWN; + } + + handle = mHandles.NewHandle(aHash, aFlags & PRIORITY, pinning); + if (exists) { + // If this file has been found evicted through the context file evictor + // above for any of pinned or non-pinned state, these calls ensure we doom + // the handle ASAP we know the real pinning state after metadta has been + // parsed. DoomFileInternal on the |handle| doesn't doom right now, since + // the pinning state is unknown and we pass down a pinning restriction. + if (evictedAsPinned) { + rv = DoomFileInternal(handle, DOOM_WHEN_PINNED); + MOZ_ASSERT(!handle->IsDoomed() && NS_SUCCEEDED(rv)); + } + if (evictedAsNonPinned) { + rv = DoomFileInternal(handle, DOOM_WHEN_NON_PINNED); + MOZ_ASSERT(!handle->IsDoomed() && NS_SUCCEEDED(rv)); + } + + int64_t fileSize = -1; + rv = file->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, rv); + + handle->mFileSize = fileSize; + handle->mFileExists = true; + + CacheIndex::EnsureEntryExists(aHash); + } else { + handle->mFileSize = 0; + + CacheIndex::AddEntry(aHash); + } + + handle->mFile.swap(file); + handle.swap(*_retval); + return NS_OK; +} + +nsresult CacheFileIOManager::OpenSpecialFileInternal( + const nsACString& aKey, uint32_t aFlags, CacheFileHandle** _retval) { + LOG(("CacheFileIOManager::OpenSpecialFileInternal() [key=%s, flags=%d]", + PromiseFlatCString(aKey).get(), aFlags)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) return rv; + } + + nsCOMPtr file; + rv = GetSpecialFile(aKey, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr handle; + for (uint32_t i = 0; i < mSpecialHandles.Length(); i++) { + if (!mSpecialHandles[i]->IsDoomed() && mSpecialHandles[i]->Key() == aKey) { + handle = mSpecialHandles[i]; + break; + } + } + + if ((aFlags & (OPEN | CREATE | CREATE_NEW)) == CREATE_NEW) { + if (handle) { + rv = DoomFileInternal(handle); + NS_ENSURE_SUCCESS(rv, rv); + handle = nullptr; + } + + handle = new CacheFileHandle(aKey, aFlags & PRIORITY, + CacheFileHandle::PinningStatus::NON_PINNED); + mSpecialHandles.AppendElement(handle); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + LOG( + ("CacheFileIOManager::OpenSpecialFileInternal() - Removing file from " + "disk")); + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot remove old entry from the disk"); + LOG( + ("CacheFileIOManager::OpenSpecialFileInternal() - Removing file " + "failed. [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + } + + handle->mFile.swap(file); + handle->mFileSize = 0; + } + + if (handle) { + handle.swap(*_retval); + return NS_OK; + } + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists && (aFlags & (OPEN | CREATE | CREATE_NEW)) == OPEN) { + return NS_ERROR_NOT_AVAILABLE; + } + + handle = new CacheFileHandle(aKey, aFlags & PRIORITY, + CacheFileHandle::PinningStatus::NON_PINNED); + mSpecialHandles.AppendElement(handle); + + if (exists) { + int64_t fileSize = -1; + rv = file->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, rv); + + handle->mFileSize = fileSize; + handle->mFileExists = true; + } else { + handle->mFileSize = 0; + } + + handle->mFile.swap(file); + handle.swap(*_retval); + return NS_OK; +} + +void CacheFileIOManager::CloseHandleInternal(CacheFileHandle* aHandle) { + nsresult rv; + LOG(("CacheFileIOManager::CloseHandleInternal() [handle=%p]", aHandle)); + + MOZ_ASSERT(!aHandle->IsClosed()); + + aHandle->Log(); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + // Maybe close file handle (can be legally bypassed after shutdown) + rv = MaybeReleaseNSPRHandleInternal(aHandle); + + // Delete the file if the entry was doomed or invalid and + // filedesc properly closed + if ((aHandle->mIsDoomed || aHandle->mInvalid) && aHandle->mFileExists && + NS_SUCCEEDED(rv)) { + LOG( + ("CacheFileIOManager::CloseHandleInternal() - Removing file from " + "disk")); + + rv = aHandle->mFile->Remove(false); + if (NS_SUCCEEDED(rv)) { + aHandle->mFileExists = false; + } else { + LOG((" failed to remove the file [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + } + + if (!aHandle->IsSpecialFile() && !aHandle->mIsDoomed && + (aHandle->mInvalid || !aHandle->mFileExists)) { + CacheIndex::RemoveEntry(aHandle->Hash()); + } + + // Don't remove handles after shutdown + if (!mShuttingDown) { + if (aHandle->IsSpecialFile()) { + mSpecialHandles.RemoveElement(aHandle); + } else { + mHandles.RemoveHandle(aHandle); + } + } +} + +// static +nsresult CacheFileIOManager::Read(CacheFileHandle* aHandle, int64_t aOffset, + char* aBuf, int32_t aCount, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::Read() [handle=%p, offset=%" PRId64 ", count=%d, " + "listener=%p]", + aHandle, aOffset, aCount, aCallback)); + + if (CacheObserver::ShuttingDown()) { + LOG((" no reads after shutdown")); + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = + new ReadEvent(aHandle, aOffset, aBuf, aCount, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->IsPriority() + ? CacheIOThread::READ_PRIORITY + : CacheIOThread::READ); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::ReadInternal(CacheFileHandle* aHandle, + int64_t aOffset, char* aBuf, + int32_t aCount) { + LOG(("CacheFileIOManager::ReadInternal() [handle=%p, offset=%" PRId64 + ", count=%d]", + aHandle, aOffset, aCount)); + + nsresult rv; + + if (CacheObserver::ShuttingDown()) { + LOG((" no reads after shutdown")); + return NS_ERROR_NOT_INITIALIZED; + } + + if (!aHandle->mFileExists) { + NS_WARNING("Trying to read from non-existent file"); + return NS_ERROR_NOT_AVAILABLE; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + if (!aHandle->mFD) { + rv = OpenNSPRHandle(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NSPRHandleUsed(aHandle); + } + + // Check again, OpenNSPRHandle could figure out the file was gone. + if (!aHandle->mFileExists) { + NS_WARNING("Trying to read from non-existent file"); + return NS_ERROR_NOT_AVAILABLE; + } + + int64_t offset = PR_Seek64(aHandle->mFD, aOffset, PR_SEEK_SET); + if (offset == -1) { + return NS_ERROR_FAILURE; + } + + int32_t bytesRead = PR_Read(aHandle->mFD, aBuf, aCount); + if (bytesRead != aCount) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::Write(CacheFileHandle* aHandle, int64_t aOffset, + const char* aBuf, int32_t aCount, + bool aValidate, bool aTruncate, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::Write() [handle=%p, offset=%" PRId64 ", count=%d, " + "validate=%d, truncate=%d, listener=%p]", + aHandle, aOffset, aCount, aValidate, aTruncate, aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || (aCallback && aCallback->IsKilled()) || !ioMan) { + if (!aCallback) { + // When no callback is provided, CacheFileIOManager is responsible for + // releasing the buffer. We must release it even in case of failure. + free(const_cast(aBuf)); + } + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = new WriteEvent(aHandle, aOffset, aBuf, aCount, + aValidate, aTruncate, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +static nsresult TruncFile(PRFileDesc* aFD, int64_t aEOF) { +#if defined(XP_UNIX) + if (ftruncate(PR_FileDesc2NativeHandle(aFD), aEOF) != 0) { + NS_ERROR("ftruncate failed"); + return NS_ERROR_FAILURE; + } +#elif defined(XP_WIN) + int64_t cnt = PR_Seek64(aFD, aEOF, PR_SEEK_SET); + if (cnt == -1) { + return NS_ERROR_FAILURE; + } + if (!SetEndOfFile((HANDLE)PR_FileDesc2NativeHandle(aFD))) { + NS_ERROR("SetEndOfFile failed"); + return NS_ERROR_FAILURE; + } +#else + MOZ_ASSERT(false, "Not implemented!"); + return NS_ERROR_NOT_IMPLEMENTED; +#endif + + return NS_OK; +} + +nsresult CacheFileIOManager::WriteInternal(CacheFileHandle* aHandle, + int64_t aOffset, const char* aBuf, + int32_t aCount, bool aValidate, + bool aTruncate) { + LOG(("CacheFileIOManager::WriteInternal() [handle=%p, offset=%" PRId64 + ", count=%d, " + "validate=%d, truncate=%d]", + aHandle, aOffset, aCount, aValidate, aTruncate)); + + nsresult rv; + + if (aHandle->mKilled) { + LOG((" handle already killed, nothing written")); + return NS_OK; + } + + if (CacheObserver::ShuttingDown() && (!aValidate || !aHandle->mFD)) { + aHandle->mKilled = true; + LOG((" killing the handle, nothing written")); + return NS_OK; + } + + if (CacheObserver::IsPastShutdownIOLag()) { + LOG((" past the shutdown I/O lag, nothing written")); + // Pretend the write has succeeded, otherwise upper layers will doom + // the file and we end up with I/O anyway. + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + if (!aHandle->mFileExists) { + rv = CreateFile(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!aHandle->mFD) { + rv = OpenNSPRHandle(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NSPRHandleUsed(aHandle); + } + + // Check again, OpenNSPRHandle could figure out the file was gone. + if (!aHandle->mFileExists) { + return NS_ERROR_NOT_AVAILABLE; + } + + // When this operation would increase cache size, check whether the cache size + // reached the hard limit and whether it would cause critical low disk space. + if (aHandle->mFileSize < aOffset + aCount) { + if (mOverLimitEvicting && mCacheSizeOnHardLimit) { + LOG( + ("CacheFileIOManager::WriteInternal() - failing because cache size " + "reached hard limit!")); + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + + int64_t freeSpace; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + freeSpace = -1; + LOG( + ("CacheFileIOManager::WriteInternal() - GetDiskSpaceAvailable() " + "failed! [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } else { + freeSpace >>= 10; // bytes to kilobytes + uint32_t limit = CacheObserver::DiskFreeSpaceHardLimit(); + if (freeSpace - aOffset - aCount + aHandle->mFileSize < limit) { + LOG( + ("CacheFileIOManager::WriteInternal() - Low free space, refusing " + "to write! [freeSpace=%" PRId64 "kB, limit=%ukB]", + freeSpace, limit)); + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + } + } + + // Write invalidates the entry by default + aHandle->mInvalid = true; + + int64_t offset = PR_Seek64(aHandle->mFD, aOffset, PR_SEEK_SET); + if (offset == -1) { + return NS_ERROR_FAILURE; + } + + int32_t bytesWritten = PR_Write(aHandle->mFD, aBuf, aCount); + + if (bytesWritten != -1) { + uint32_t oldSizeInK = aHandle->FileSizeInK(); + int64_t writeEnd = aOffset + bytesWritten; + + if (aTruncate) { + rv = TruncFile(aHandle->mFD, writeEnd); + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mFileSize = writeEnd; + } else { + if (aHandle->mFileSize < writeEnd) { + aHandle->mFileSize = writeEnd; + } + } + + uint32_t newSizeInK = aHandle->FileSizeInK(); + + if (oldSizeInK != newSizeInK && !aHandle->IsDoomed() && + !aHandle->IsSpecialFile()) { + CacheIndex::UpdateEntry(aHandle->Hash(), nullptr, nullptr, nullptr, + nullptr, nullptr, &newSizeInK); + + if (oldSizeInK < newSizeInK) { + EvictIfOverLimitInternal(); + } + } + + CacheIndex::UpdateTotalBytesWritten(bytesWritten); + } + + if (bytesWritten != aCount) { + return NS_ERROR_FAILURE; + } + + // Write was successful and this write validates the entry (i.e. metadata) + if (aValidate) { + aHandle->mInvalid = false; + } + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::DoomFile(CacheFileHandle* aHandle, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::DoomFile() [handle=%p, listener=%p]", aHandle, + aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = new DoomFileEvent(aHandle, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->IsPriority() + ? CacheIOThread::OPEN_PRIORITY + : CacheIOThread::OPEN); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::DoomFileInternal( + CacheFileHandle* aHandle, PinningDoomRestriction aPinningDoomRestriction) { + LOG(("CacheFileIOManager::DoomFileInternal() [handle=%p]", aHandle)); + aHandle->Log(); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + nsresult rv; + + if (aHandle->IsDoomed()) { + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + if (aPinningDoomRestriction > NO_RESTRICTION) { + switch (aHandle->mPinning) { + case CacheFileHandle::PinningStatus::NON_PINNED: + if (MOZ_LIKELY(aPinningDoomRestriction != DOOM_WHEN_NON_PINNED)) { + LOG((" not dooming, it's a non-pinned handle")); + return NS_OK; + } + // Doom now + break; + + case CacheFileHandle::PinningStatus::PINNED: + if (MOZ_UNLIKELY(aPinningDoomRestriction != DOOM_WHEN_PINNED)) { + LOG((" not dooming, it's a pinned handle")); + return NS_OK; + } + // Doom now + break; + + case CacheFileHandle::PinningStatus::UNKNOWN: + if (MOZ_LIKELY(aPinningDoomRestriction == DOOM_WHEN_NON_PINNED)) { + LOG((" doom when non-pinned set")); + aHandle->mDoomWhenFoundNonPinned = true; + } else if (MOZ_UNLIKELY(aPinningDoomRestriction == DOOM_WHEN_PINNED)) { + LOG((" doom when pinned set")); + aHandle->mDoomWhenFoundPinned = true; + } + + LOG((" pinning status not known, deferring doom decision")); + return NS_OK; + } + } + + if (aHandle->mFileExists) { + // we need to move the current file to the doomed directory + rv = MaybeReleaseNSPRHandleInternal(aHandle, true); + NS_ENSURE_SUCCESS(rv, rv); + + // find unused filename + nsCOMPtr file; + rv = GetDoomedFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr parentDir; + rv = file->GetParent(getter_AddRefs(parentDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString leafName; + rv = file->GetNativeLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aHandle->mFile->MoveToNative(parentDir, leafName); + if (NS_ERROR_FILE_NOT_FOUND == rv) { + LOG((" file already removed under our hands")); + aHandle->mFileExists = false; + rv = NS_OK; + } else { + NS_ENSURE_SUCCESS(rv, rv); + aHandle->mFile.swap(file); + } + } + + if (!aHandle->IsSpecialFile()) { + CacheIndex::RemoveEntry(aHandle->Hash()); + } + + aHandle->mIsDoomed = true; + + if (!aHandle->IsSpecialFile()) { + RefPtr storageService = CacheStorageService::Self(); + if (storageService) { + nsAutoCString idExtension, url; + nsCOMPtr info = + CacheFileUtils::ParseKey(aHandle->Key(), &idExtension, &url); + MOZ_ASSERT(info); + if (info) { + storageService->CacheFileDoomed(info, idExtension, url); + } + } + } + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::DoomFileByKey(const nsACString& aKey, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::DoomFileByKey() [key=%s, listener=%p]", + PromiseFlatCString(aKey).get(), aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = new DoomFileByKeyEvent(aKey, aCallback); + rv = ioMan->mIOThread->DispatchAfterPendingOpens(ev); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::DoomFileByKeyInternal(const SHA1Sum::Hash* aHash) { + LOG(( + "CacheFileIOManager::DoomFileByKeyInternal() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHash))); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + nsresult rv; + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mCacheDirectory) { + return NS_ERROR_FILE_INVALID_PATH; + } + + // Find active handle + RefPtr handle; + mHandles.GetHandle(aHash, getter_AddRefs(handle)); + + if (handle) { + handle->Log(); + + return DoomFileInternal(handle); + } + + CacheIOThread::Cancelable cancelable(true); + + // There is no handle for this file, delete the file if exists + nsCOMPtr file; + rv = GetFile(aHash, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + return NS_ERROR_NOT_AVAILABLE; + } + + LOG( + ("CacheFileIOManager::DoomFileByKeyInternal() - Removing file from " + "disk")); + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot remove old entry from the disk"); + LOG( + ("CacheFileIOManager::DoomFileByKeyInternal() - Removing file failed. " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + + CacheIndex::RemoveEntry(aHash); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::ReleaseNSPRHandle(CacheFileHandle* aHandle) { + LOG(("CacheFileIOManager::ReleaseNSPRHandle() [handle=%p]", aHandle)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = new ReleaseNSPRHandleEvent(aHandle); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::MaybeReleaseNSPRHandleInternal( + CacheFileHandle* aHandle, bool aIgnoreShutdownLag) { + LOG( + ("CacheFileIOManager::MaybeReleaseNSPRHandleInternal() [handle=%p, " + "ignore shutdown=%d]", + aHandle, aIgnoreShutdownLag)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + if (aHandle->mFD) { + DebugOnly found{}; + found = mHandlesByLastUsed.RemoveElement(aHandle); + MOZ_ASSERT(found); + } + + PRFileDesc* fd = aHandle->mFD; + aHandle->mFD = nullptr; + + // Leak invalid (w/o metadata) and doomed handles immediately after shutdown. + // Leak other handles when past the shutdown time maximum lag. + if ( +#ifndef DEBUG + ((aHandle->mInvalid || aHandle->mIsDoomed) && + MOZ_UNLIKELY(CacheObserver::ShuttingDown())) || +#endif + MOZ_UNLIKELY(!aIgnoreShutdownLag && + CacheObserver::IsPastShutdownIOLag())) { + // Don't bother closing this file. Return a failure code from here will + // cause any following IO operation on the file (mainly removal) to be + // bypassed, which is what we want. + // For mInvalid == true the entry will never be used, since it doesn't + // have correct metadata, thus we don't need to worry about removing it. + // For mIsDoomed == true the file is already in the doomed sub-dir and + // will be removed on next session start. + LOG((" past the shutdown I/O lag, leaking file handle")); + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + if (!fd) { + // The filedesc has already been closed before, just let go. + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + PRStatus status = PR_Close(fd); + if (status != PR_SUCCESS) { + LOG( + ("CacheFileIOManager::MaybeReleaseNSPRHandleInternal() " + "failed to close [handle=%p, status=%u]", + aHandle, status)); + return NS_ERROR_FAILURE; + } + + LOG(("CacheFileIOManager::MaybeReleaseNSPRHandleInternal() DONE")); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::TruncateSeekSetEOF( + CacheFileHandle* aHandle, int64_t aTruncatePos, int64_t aEOFPos, + CacheFileIOListener* aCallback) { + LOG( + ("CacheFileIOManager::TruncateSeekSetEOF() [handle=%p, " + "truncatePos=%" PRId64 ", " + "EOFPos=%" PRId64 ", listener=%p]", + aHandle, aTruncatePos, aEOFPos, aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || (aCallback && aCallback->IsKilled()) || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = + new TruncateSeekSetEOFEvent(aHandle, aTruncatePos, aEOFPos, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +void CacheFileIOManager::GetCacheDirectory(nsIFile** result) { + *result = nullptr; + + RefPtr ioMan = gInstance; + if (!ioMan || !ioMan->mCacheDirectory) { + return; + } + + ioMan->mCacheDirectory->Clone(result); +} + +#if defined(MOZ_WIDGET_ANDROID) + +// static +void CacheFileIOManager::GetProfilelessCacheDirectory(nsIFile** result) { + *result = nullptr; + + RefPtr ioMan = gInstance; + if (!ioMan || !ioMan->mCacheProfilelessDirectory) { + return; + } + + ioMan->mCacheProfilelessDirectory->Clone(result); +} + +#endif + +// static +nsresult CacheFileIOManager::GetEntryInfo( + const SHA1Sum::Hash* aHash, + CacheStorageService::EntryInfoCallback* aCallback) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + RefPtr ioMan = gInstance; + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsAutoCString enhanceId; + nsAutoCString uriSpec; + + RefPtr handle; + ioMan->mHandles.GetHandle(aHash, getter_AddRefs(handle)); + if (handle) { + RefPtr info = + CacheFileUtils::ParseKey(handle->Key(), &enhanceId, &uriSpec); + + MOZ_ASSERT(info); + if (!info) { + return NS_OK; // ignore + } + + RefPtr service = CacheStorageService::Self(); + if (!service) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Invokes OnCacheEntryInfo when an existing entry is found + if (service->GetCacheEntryInfo(info, enhanceId, uriSpec, aCallback)) { + return NS_OK; + } + + // When we are here, there is no existing entry and we need + // to synchrnously load metadata from a disk file. + } + + // Locate the actual file + nsCOMPtr file; + ioMan->GetFile(aHash, getter_AddRefs(file)); + + // Read metadata from the file synchronously + RefPtr metadata = new CacheFileMetadata(); + rv = metadata->SyncReadMetadata(file); + if (NS_FAILED(rv)) { + return NS_OK; + } + + // Now get the context + enhance id + URL from the key. + RefPtr info = + CacheFileUtils::ParseKey(metadata->GetKey(), &enhanceId, &uriSpec); + MOZ_ASSERT(info); + if (!info) { + return NS_OK; + } + + // Pick all data to pass to the callback. + int64_t dataSize = metadata->Offset(); + int64_t altDataSize = 0; + uint32_t fetchCount = metadata->GetFetchCount(); + uint32_t expirationTime = metadata->GetExpirationTime(); + uint32_t lastModified = metadata->GetLastModified(); + + const char* altDataElement = + metadata->GetElement(CacheFileUtils::kAltDataKey); + if (altDataElement) { + int64_t altDataOffset = std::numeric_limits::max(); + if (NS_SUCCEEDED(CacheFileUtils::ParseAlternativeDataInfo( + altDataElement, &altDataOffset, nullptr)) && + altDataOffset < dataSize) { + dataSize = altDataOffset; + altDataSize = metadata->Offset() - altDataOffset; + } else { + LOG(("CacheFileIOManager::GetEntryInfo() invalid alternative data info")); + return NS_OK; + } + } + + // Call directly on the callback. + aCallback->OnEntryInfo(uriSpec, enhanceId, dataSize, altDataSize, fetchCount, + lastModified, expirationTime, metadata->Pinned(), + info); + + return NS_OK; +} + +nsresult CacheFileIOManager::TruncateSeekSetEOFInternal( + CacheFileHandle* aHandle, int64_t aTruncatePos, int64_t aEOFPos) { + LOG( + ("CacheFileIOManager::TruncateSeekSetEOFInternal() [handle=%p, " + "truncatePos=%" PRId64 ", EOFPos=%" PRId64 "]", + aHandle, aTruncatePos, aEOFPos)); + + nsresult rv; + + if (aHandle->mKilled) { + LOG((" handle already killed, file not truncated")); + return NS_OK; + } + + if (CacheObserver::ShuttingDown() && !aHandle->mFD) { + aHandle->mKilled = true; + LOG((" killing the handle, file not truncated")); + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + if (!aHandle->mFileExists) { + rv = CreateFile(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!aHandle->mFD) { + rv = OpenNSPRHandle(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NSPRHandleUsed(aHandle); + } + + // Check again, OpenNSPRHandle could figure out the file was gone. + if (!aHandle->mFileExists) { + return NS_ERROR_NOT_AVAILABLE; + } + + // When this operation would increase cache size, check whether the cache size + // reached the hard limit and whether it would cause critical low disk space. + if (aHandle->mFileSize < aEOFPos) { + if (mOverLimitEvicting && mCacheSizeOnHardLimit) { + LOG( + ("CacheFileIOManager::TruncateSeekSetEOFInternal() - failing because " + "cache size reached hard limit!")); + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + + int64_t freeSpace; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + freeSpace = -1; + LOG( + ("CacheFileIOManager::TruncateSeekSetEOFInternal() - " + "GetDiskSpaceAvailable() failed! [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } else { + freeSpace >>= 10; // bytes to kilobytes + uint32_t limit = CacheObserver::DiskFreeSpaceHardLimit(); + if (freeSpace - aEOFPos + aHandle->mFileSize < limit) { + LOG( + ("CacheFileIOManager::TruncateSeekSetEOFInternal() - Low free space" + ", refusing to write! [freeSpace=%" PRId64 "kB, limit=%ukB]", + freeSpace, limit)); + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + } + } + + // This operation always invalidates the entry + aHandle->mInvalid = true; + + rv = TruncFile(aHandle->mFD, aTruncatePos); + NS_ENSURE_SUCCESS(rv, rv); + + if (aTruncatePos != aEOFPos) { + rv = TruncFile(aHandle->mFD, aEOFPos); + NS_ENSURE_SUCCESS(rv, rv); + } + + uint32_t oldSizeInK = aHandle->FileSizeInK(); + aHandle->mFileSize = aEOFPos; + uint32_t newSizeInK = aHandle->FileSizeInK(); + + if (oldSizeInK != newSizeInK && !aHandle->IsDoomed() && + !aHandle->IsSpecialFile()) { + CacheIndex::UpdateEntry(aHandle->Hash(), nullptr, nullptr, nullptr, nullptr, + nullptr, &newSizeInK); + + if (oldSizeInK < newSizeInK) { + EvictIfOverLimitInternal(); + } + } + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::RenameFile(CacheFileHandle* aHandle, + const nsACString& aNewName, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::RenameFile() [handle=%p, newName=%s, listener=%p]", + aHandle, PromiseFlatCString(aNewName).get(), aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr ev = + new RenameFileEvent(aHandle, aNewName, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::RenameFileInternal(CacheFileHandle* aHandle, + const nsACString& aNewName) { + LOG(("CacheFileIOManager::RenameFileInternal() [handle=%p, newName=%s]", + aHandle, PromiseFlatCString(aNewName).get())); + + nsresult rv; + + MOZ_ASSERT(aHandle->IsSpecialFile()); + + if (aHandle->IsDoomed()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Doom old handle if it exists and is not doomed + for (uint32_t i = 0; i < mSpecialHandles.Length(); i++) { + if (!mSpecialHandles[i]->IsDoomed() && + mSpecialHandles[i]->Key() == aNewName) { + MOZ_ASSERT(aHandle != mSpecialHandles[i]); + rv = DoomFileInternal(mSpecialHandles[i]); + NS_ENSURE_SUCCESS(rv, rv); + break; + } + } + + nsCOMPtr file; + rv = GetSpecialFile(aNewName, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + LOG( + ("CacheFileIOManager::RenameFileInternal() - Removing old file from " + "disk")); + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot remove file from the disk"); + LOG( + ("CacheFileIOManager::RenameFileInternal() - Removing old file failed" + ". [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + } + + if (!aHandle->FileExists()) { + aHandle->mKey = aNewName; + return NS_OK; + } + + rv = MaybeReleaseNSPRHandleInternal(aHandle, true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aHandle->mFile->MoveToNative(nullptr, aNewName); + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mKey = aNewName; + return NS_OK; +} + +// static +nsresult CacheFileIOManager::EvictIfOverLimit() { + LOG(("CacheFileIOManager::EvictIfOverLimit()")); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr ev; + ev = NewRunnableMethod("net::CacheFileIOManager::EvictIfOverLimitInternal", + ioMan, &CacheFileIOManager::EvictIfOverLimitInternal); + + rv = ioMan->mIOThread->Dispatch(ev, CacheIOThread::EVICT); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::EvictIfOverLimitInternal() { + LOG(("CacheFileIOManager::EvictIfOverLimitInternal()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (mOverLimitEvicting) { + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - Eviction already " + "running.")); + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(true); + + int64_t freeSpace; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + freeSpace = -1; + + // Do not change smart size. + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - " + "GetDiskSpaceAvailable() failed! [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } else { + freeSpace >>= 10; // bytes to kilobytes + UpdateSmartCacheSize(freeSpace); + } + + uint32_t cacheUsage; + rv = CacheIndex::GetCacheSize(&cacheUsage); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t cacheLimit = CacheObserver::DiskCacheCapacity(); + uint32_t freeSpaceLimit = CacheObserver::DiskFreeSpaceSoftLimit(); + + if (cacheUsage <= cacheLimit && + (freeSpace == -1 || freeSpace >= freeSpaceLimit)) { + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - Cache size and free " + "space in limits. [cacheSize=%ukB, cacheSizeLimit=%ukB, " + "freeSpace=%" PRId64 "kB, freeSpaceLimit=%ukB]", + cacheUsage, cacheLimit, freeSpace, freeSpaceLimit)); + return NS_OK; + } + + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - Cache size exceeded " + "limit. Starting overlimit eviction. [cacheSize=%ukB, limit=%ukB]", + cacheUsage, cacheLimit)); + + nsCOMPtr ev; + ev = NewRunnableMethod("net::CacheFileIOManager::OverLimitEvictionInternal", + this, &CacheFileIOManager::OverLimitEvictionInternal); + + rv = mIOThread->Dispatch(ev, CacheIOThread::EVICT); + NS_ENSURE_SUCCESS(rv, rv); + + mOverLimitEvicting = true; + return NS_OK; +} + +nsresult CacheFileIOManager::OverLimitEvictionInternal() { + LOG(("CacheFileIOManager::OverLimitEvictionInternal()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + // mOverLimitEvicting is accessed only on IO thread, so we can set it to false + // here and set it to true again once we dispatch another event that will + // continue with the eviction. The reason why we do so is that we can fail + // early anywhere in this method and the variable will contain a correct + // value. Otherwise we would need to set it to false on every failing place. + mOverLimitEvicting = false; + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + while (true) { + int64_t freeSpace; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + freeSpace = -1; + + // Do not change smart size. + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - " + "GetDiskSpaceAvailable() failed! [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } else { + freeSpace >>= 10; // bytes to kilobytes + UpdateSmartCacheSize(freeSpace); + } + + uint32_t cacheUsage; + rv = CacheIndex::GetCacheSize(&cacheUsage); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t cacheLimit = CacheObserver::DiskCacheCapacity(); + uint32_t freeSpaceLimit = CacheObserver::DiskFreeSpaceSoftLimit(); + + if (cacheUsage > cacheLimit) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Cache size over " + "limit. [cacheSize=%ukB, limit=%ukB]", + cacheUsage, cacheLimit)); + + // We allow cache size to go over the specified limit. Eviction should + // keep the size within the limit in a long run, but in case the eviction + // is too slow, the cache could go way over the limit. To prevent this we + // set flag mCacheSizeOnHardLimit when the size reaches 105% of the limit + // and WriteInternal() and TruncateSeekSetEOFInternal() fail to cache + // additional data. + if ((cacheUsage - cacheLimit) > (cacheLimit / 20)) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Cache size " + "reached hard limit.")); + mCacheSizeOnHardLimit = true; + } else { + mCacheSizeOnHardLimit = false; + } + } else if (freeSpace != -1 && freeSpace < freeSpaceLimit) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Free space under " + "limit. [freeSpace=%" PRId64 "kB, freeSpaceLimit=%ukB]", + freeSpace, freeSpaceLimit)); + } else { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Cache size and " + "free space in limits. [cacheSize=%ukB, cacheSizeLimit=%ukB, " + "freeSpace=%" PRId64 "kB, freeSpaceLimit=%ukB]", + cacheUsage, cacheLimit, freeSpace, freeSpaceLimit)); + + mCacheSizeOnHardLimit = false; + return NS_OK; + } + + if (CacheIOThread::YieldAndRerun()) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Breaking loop " + "for higher level events.")); + mOverLimitEvicting = true; + return NS_OK; + } + + SHA1Sum::Hash hash; + uint32_t cnt; + static uint32_t consecutiveFailures = 0; + rv = CacheIndex::GetEntryForEviction(false, &hash, &cnt); + NS_ENSURE_SUCCESS(rv, rv); + + rv = DoomFileByKeyInternal(&hash); + if (NS_SUCCEEDED(rv)) { + consecutiveFailures = 0; + } else if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - " + "DoomFileByKeyInternal() failed. [rv=0x%08" PRIx32 "]", + static_cast(rv))); + // TODO index is outdated, start update + + // Make sure index won't return the same entry again + CacheIndex::RemoveEntry(&hash); + consecutiveFailures = 0; + } else { + // This shouldn't normally happen, but the eviction must not fail + // completely if we ever encounter this problem. + NS_WARNING( + "CacheFileIOManager::OverLimitEvictionInternal() - Unexpected " + "failure of DoomFileByKeyInternal()"); + + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - " + "DoomFileByKeyInternal() failed. [rv=0x%08" PRIx32 "]", + static_cast(rv))); + + // Normally, CacheIndex::UpdateEntry() is called only to update newly + // created/opened entries which are always fresh and UpdateEntry() expects + // and checks this flag. The way we use UpdateEntry() here is a kind of + // hack and we must make sure the flag is set by calling + // EnsureEntryExists(). + rv = CacheIndex::EnsureEntryExists(&hash); + NS_ENSURE_SUCCESS(rv, rv); + + // Move the entry at the end of both lists to make sure we won't end up + // failing on one entry forever. + uint32_t frecency = 0; + rv = CacheIndex::UpdateEntry(&hash, &frecency, nullptr, nullptr, nullptr, + nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + consecutiveFailures++; + if (consecutiveFailures >= cnt) { + // This doesn't necessarily mean that we've tried to doom every entry + // but we've reached a sane number of tries. It is likely that another + // eviction will start soon. And as said earlier, this normally doesn't + // happen at all. + return NS_OK; + } + } + } + + MOZ_ASSERT_UNREACHABLE("We should never get here"); + return NS_OK; +} + +// static +nsresult CacheFileIOManager::EvictAll() { + LOG(("CacheFileIOManager::EvictAll()")); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr ev; + ev = NewRunnableMethod("net::CacheFileIOManager::EvictAllInternal", ioMan, + &CacheFileIOManager::EvictAllInternal); + + rv = ioMan->mIOThread->DispatchAfterPendingOpens(ev); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +namespace { + +class EvictionNotifierRunnable : public Runnable { + public: + EvictionNotifierRunnable() : Runnable("EvictionNotifierRunnable") {} + NS_DECL_NSIRUNNABLE +}; + +NS_IMETHODIMP +EvictionNotifierRunnable::Run() { + nsCOMPtr obsSvc = mozilla::services::GetObserverService(); + if (obsSvc) { + obsSvc->NotifyObservers(nullptr, "cacheservice:empty-cache", nullptr); + } + return NS_OK; +} + +} // namespace + +nsresult CacheFileIOManager::EvictAllInternal() { + LOG(("CacheFileIOManager::EvictAllInternal()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + RefPtr r = new EvictionNotifierRunnable(); + + if (!mCacheDirectory) { + // This is a kind of hack. Somebody called EvictAll() without a profile. + // This happens in xpcshell tests that use cache without profile. We need + // to notify observers in this case since the tests are waiting for it. + NS_DispatchToMainThread(r); + return NS_ERROR_FILE_INVALID_PATH; + } + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) { + return rv; + } + } + + // Doom all active handles + nsTArray> handles; + mHandles.GetActiveHandles(&handles); + + for (uint32_t i = 0; i < handles.Length(); ++i) { + rv = DoomFileInternal(handles[i]); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::EvictAllInternal() - Cannot doom handle " + "[handle=%p]", + handles[i].get())); + } + } + + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->AppendNative(nsLiteralCString(ENTRIES_DIR)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Trash current entries directory + rv = TrashDirectory(file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Files are now inaccessible in entries directory, notify observers. + NS_DispatchToMainThread(r); + + // Create a new empty entries directory + rv = CheckAndCreateDir(mCacheDirectory, ENTRIES_DIR, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + CacheIndex::RemoveAll(); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::EvictByContext( + nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin, const nsAString& aBaseDomain) { + LOG(("CacheFileIOManager::EvictByContext() [loadContextInfo=%p]", + aLoadContextInfo)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr ev; + ev = + NewRunnableMethod, bool, nsString, nsString>( + "net::CacheFileIOManager::EvictByContextInternal", ioMan, + &CacheFileIOManager::EvictByContextInternal, aLoadContextInfo, + aPinned, aOrigin, aBaseDomain); + + rv = ioMan->mIOThread->DispatchAfterPendingOpens(ev); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult CacheFileIOManager::EvictByContextInternal( + nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin, const nsAString& aBaseDomain) { + LOG( + ("CacheFileIOManager::EvictByContextInternal() [loadContextInfo=%p, " + "pinned=%d]", + aLoadContextInfo, aPinned)); + + nsresult rv; + + if (aLoadContextInfo) { + nsAutoCString suffix; + aLoadContextInfo->OriginAttributesPtr()->CreateSuffix(suffix); + LOG((" anonymous=%u, suffix=%s]", aLoadContextInfo->IsAnonymous(), + suffix.get())); + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + MOZ_ASSERT(!aLoadContextInfo->IsPrivate()); + if (aLoadContextInfo->IsPrivate()) { + return NS_ERROR_INVALID_ARG; + } + } + + if (!mCacheDirectory) { + // This is a kind of hack. Somebody called EvictAll() without a profile. + // This happens in xpcshell tests that use cache without profile. We need + // to notify observers in this case since the tests are waiting for it. + // Also notify for aPinned == true, those are interested as well. + if (!aLoadContextInfo) { + RefPtr r = new EvictionNotifierRunnable(); + NS_DispatchToMainThread(r); + } + return NS_ERROR_FILE_INVALID_PATH; + } + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) { + return rv; + } + } + + NS_ConvertUTF16toUTF8 origin(aOrigin); + NS_ConvertUTF16toUTF8 baseDomain(aBaseDomain); + + // Doom all active handles that matches the load context + nsTArray> handles; + mHandles.GetActiveHandles(&handles); + + for (uint32_t i = 0; i < handles.Length(); ++i) { + CacheFileHandle* handle = handles[i]; + + const bool shouldRemove = [&] { + nsAutoCString uriSpec; + RefPtr info = + CacheFileUtils::ParseKey(handle->Key(), nullptr, &uriSpec); + if (!info) { + LOG( + ("CacheFileIOManager::EvictByContextInternal() - Cannot parse key " + "in " + "handle! [handle=%p, key=%s]", + handle, handle->Key().get())); + MOZ_CRASH("Unexpected error!"); + } + + // Filter by base domain. + if (!aBaseDomain.IsEmpty()) { + if (StoragePrincipalHelper::PartitionKeyHasBaseDomain( + info->OriginAttributesPtr()->mPartitionKey, aBaseDomain)) { + return true; + } + + // If the partitionKey does not match, check the entry URI next. + + // Get host portion of uriSpec. + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), uriSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + nsAutoCString host; + rv = uri->GetHost(host); + // Some entries may not have valid hosts. We can skip them. + if (NS_FAILED(rv) || host.IsEmpty()) { + return false; + } + + // Clear entry if the host belongs to the given base domain. + bool hasRootDomain = false; + rv = HasRootDomain(host, baseDomain, &hasRootDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return hasRootDomain; + } + + // Filter by LoadContextInfo. + if (aLoadContextInfo && !info->Equals(aLoadContextInfo)) { + return false; + } + + // Filter by origin. + if (!origin.IsEmpty()) { + RefPtr url; + rv = MozURL::Init(getter_AddRefs(url), uriSpec); + if (NS_FAILED(rv)) { + return false; + } + + nsAutoCString urlOrigin; + url->Origin(urlOrigin); + + if (!urlOrigin.Equals(origin)) { + return false; + } + } + + return true; + }(); + + if (!shouldRemove) { + continue; + } + + // handle will be doomed only when pinning status is known and equal or + // doom decision will be deferred until pinning status is determined. + rv = DoomFileInternal(handle, + aPinned ? CacheFileIOManager::DOOM_WHEN_PINNED + : CacheFileIOManager::DOOM_WHEN_NON_PINNED); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::EvictByContextInternal() - Cannot doom handle" + " [handle=%p]", + handle)); + } + } + + if (!aLoadContextInfo) { + RefPtr r = new EvictionNotifierRunnable(); + NS_DispatchToMainThread(r); + } + + if (!mContextEvictor) { + mContextEvictor = new CacheFileContextEvictor(); + mContextEvictor->Init(mCacheDirectory); + } + + mContextEvictor->AddContext(aLoadContextInfo, aPinned, aOrigin); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::CacheIndexStateChanged() { + LOG(("CacheFileIOManager::CacheIndexStateChanged()")); + + nsresult rv; + + // CacheFileIOManager lives longer than CacheIndex so gInstance must be + // non-null here. + MOZ_ASSERT(gInstance); + + // We have to re-distatch even if we are on IO thread to prevent reentering + // the lock in CacheIndex + nsCOMPtr ev = NewRunnableMethod( + "net::CacheFileIOManager::CacheIndexStateChangedInternal", + gInstance.get(), &CacheFileIOManager::CacheIndexStateChangedInternal); + + nsCOMPtr ioTarget = IOTarget(); + MOZ_ASSERT(ioTarget); + + rv = ioTarget->Dispatch(ev, nsIEventTarget::DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void CacheFileIOManager::CacheIndexStateChangedInternal() { + if (mShuttingDown) { + // ignore notification during shutdown + return; + } + + if (!mContextEvictor) { + return; + } + + mContextEvictor->CacheIndexStateChanged(); +} + +nsresult CacheFileIOManager::TrashDirectory(nsIFile* aFile) { + LOG(("CacheFileIOManager::TrashDirectory() [file=%s]", + aFile->HumanReadablePath().get())); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + MOZ_ASSERT(mCacheDirectory); + + // When the directory is empty, it is cheaper to remove it directly instead of + // using the trash mechanism. + bool isEmpty; + rv = IsEmptyDirectory(aFile, &isEmpty); + NS_ENSURE_SUCCESS(rv, rv); + + if (isEmpty) { + rv = aFile->Remove(false); + LOG( + ("CacheFileIOManager::TrashDirectory() - Directory removed " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + return rv; + } + +#ifdef DEBUG + nsCOMPtr dirCheck; + rv = aFile->GetParent(getter_AddRefs(dirCheck)); + NS_ENSURE_SUCCESS(rv, rv); + + bool equals = false; + rv = dirCheck->Equals(mCacheDirectory, &equals); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(equals); +#endif + + nsCOMPtr dir, trash; + nsAutoCString leaf; + + rv = aFile->Clone(getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aFile->Clone(getter_AddRefs(trash)); + NS_ENSURE_SUCCESS(rv, rv); + + const int32_t kMaxTries = 16; + srand(static_cast(PR_Now())); + for (int32_t triesCount = 0;; ++triesCount) { + leaf = TRASH_DIR; + leaf.AppendInt(rand()); + rv = trash->SetNativeLeafName(leaf); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + if (NS_SUCCEEDED(trash->Exists(&exists)) && !exists) { + break; + } + + LOG( + ("CacheFileIOManager::TrashDirectory() - Trash directory already " + "exists [leaf=%s]", + leaf.get())); + + if (triesCount == kMaxTries) { + LOG( + ("CacheFileIOManager::TrashDirectory() - Could not find unused trash " + "directory in %d tries.", + kMaxTries)); + return NS_ERROR_FAILURE; + } + } + + LOG(("CacheFileIOManager::TrashDirectory() - Renaming directory [leaf=%s]", + leaf.get())); + + rv = dir->MoveToNative(nullptr, leaf); + NS_ENSURE_SUCCESS(rv, rv); + + StartRemovingTrash(); + return NS_OK; +} + +// static +void CacheFileIOManager::OnTrashTimer(nsITimer* aTimer, void* aClosure) { + LOG(("CacheFileIOManager::OnTrashTimer() [timer=%p, closure=%p]", aTimer, + aClosure)); + + RefPtr ioMan = gInstance; + + if (!ioMan) { + return; + } + + ioMan->mTrashTimer = nullptr; + ioMan->StartRemovingTrash(); +} + +nsresult CacheFileIOManager::StartRemovingTrash() { + LOG(("CacheFileIOManager::StartRemovingTrash()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mCacheDirectory) { + return NS_ERROR_FILE_INVALID_PATH; + } + + if (mTrashTimer) { + LOG(("CacheFileIOManager::StartRemovingTrash() - Trash timer exists.")); + return NS_OK; + } + + if (mRemovingTrashDirs) { + LOG( + ("CacheFileIOManager::StartRemovingTrash() - Trash removing in " + "progress.")); + return NS_OK; + } + + uint32_t elapsed = (TimeStamp::NowLoRes() - mStartTime).ToMilliseconds(); + if (elapsed < kRemoveTrashStartDelay) { + nsCOMPtr ioTarget = IOTarget(); + MOZ_ASSERT(ioTarget); + + return NS_NewTimerWithFuncCallback( + getter_AddRefs(mTrashTimer), CacheFileIOManager::OnTrashTimer, nullptr, + kRemoveTrashStartDelay - elapsed, nsITimer::TYPE_ONE_SHOT, + "net::CacheFileIOManager::StartRemovingTrash", ioTarget); + } + + nsCOMPtr ev; + ev = NewRunnableMethod("net::CacheFileIOManager::RemoveTrashInternal", this, + &CacheFileIOManager::RemoveTrashInternal); + + rv = mIOThread->Dispatch(ev, CacheIOThread::EVICT); + NS_ENSURE_SUCCESS(rv, rv); + + mRemovingTrashDirs = true; + return NS_OK; +} + +nsresult CacheFileIOManager::RemoveTrashInternal() { + LOG(("CacheFileIOManager::RemoveTrashInternal()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + CacheIOThread::Cancelable cancelable(true); + + MOZ_ASSERT(!mTrashTimer); + MOZ_ASSERT(mRemovingTrashDirs); + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) { + return rv; + } + } + + // mRemovingTrashDirs is accessed only on IO thread, so we can drop the flag + // here and set it again once we dispatch a continuation event. By doing so, + // we don't have to drop the flag on any possible early return. + mRemovingTrashDirs = false; + + while (true) { + if (CacheIOThread::YieldAndRerun()) { + LOG( + ("CacheFileIOManager::RemoveTrashInternal() - Breaking loop for " + "higher level events.")); + mRemovingTrashDirs = true; + return NS_OK; + } + + // Find some trash directory + if (!mTrashDir) { + MOZ_ASSERT(!mTrashDirEnumerator); + + rv = FindTrashDirToRemove(); + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG( + ("CacheFileIOManager::RemoveTrashInternal() - No trash directory " + "found.")); + return NS_OK; + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = mTrashDir->GetDirectoryEntries(getter_AddRefs(mTrashDirEnumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + continue; // check elapsed time + } + + // We null out mTrashDirEnumerator once we remove all files in the + // directory, so remove the trash directory if we don't have enumerator. + if (!mTrashDirEnumerator) { + rv = mTrashDir->Remove(false); + if (NS_FAILED(rv)) { + // There is no reason why removing an empty directory should fail, but + // if it does, we should continue and try to remove all other trash + // directories. + nsAutoCString leafName; + mTrashDir->GetNativeLeafName(leafName); + mFailedTrashDirs.AppendElement(leafName); + LOG( + ("CacheFileIOManager::RemoveTrashInternal() - Cannot remove " + "trashdir. [name=%s]", + leafName.get())); + } + + mTrashDir = nullptr; + continue; // check elapsed time + } + + nsCOMPtr file; + rv = mTrashDirEnumerator->GetNextFile(getter_AddRefs(file)); + if (!file) { + mTrashDirEnumerator->Close(); + mTrashDirEnumerator = nullptr; + continue; // check elapsed time + } + bool isDir = false; + file->IsDirectory(&isDir); + if (isDir) { + NS_WARNING( + "Found a directory in a trash directory! It will be removed " + "recursively, but this can block IO thread for a while!"); + if (LOG_ENABLED()) { + LOG( + ("CacheFileIOManager::RemoveTrashInternal() - Found a directory in " + "a trash " + "directory! It will be removed recursively, but this can block IO " + "thread for a while! [file=%s]", + file->HumanReadablePath().get())); + } + } + file->Remove(isDir); + } + + MOZ_ASSERT_UNREACHABLE("We should never get here"); + return NS_OK; +} + +nsresult CacheFileIOManager::FindTrashDirToRemove() { + LOG(("CacheFileIOManager::FindTrashDirToRemove()")); + + nsresult rv; + + if (!mCacheDirectory) { + return NS_ERROR_UNEXPECTED; + } + + // We call this method on the main thread during shutdown when user wants to + // remove all cache files. + MOZ_ASSERT(mIOThread->IsCurrentThread() || mShuttingDown); + + nsCOMPtr iter; + rv = mCacheDirectory->GetDirectoryEntries(getter_AddRefs(iter)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr file; + while (NS_SUCCEEDED(iter->GetNextFile(getter_AddRefs(file))) && file) { + bool isDir = false; + file->IsDirectory(&isDir); + if (!isDir) { + continue; + } + + nsAutoCString leafName; + rv = file->GetNativeLeafName(leafName); + if (NS_FAILED(rv)) { + continue; + } + + if (leafName.Length() < strlen(TRASH_DIR)) { + continue; + } + + if (!StringBeginsWith(leafName, nsLiteralCString(TRASH_DIR))) { + continue; + } + + if (mFailedTrashDirs.Contains(leafName)) { + continue; + } + + LOG(("CacheFileIOManager::FindTrashDirToRemove() - Returning directory %s", + leafName.get())); + + mTrashDir = file; + return NS_OK; + } + + // When we're here we've tried to delete all trash directories. Clear + // mFailedTrashDirs so we will try to delete them again when we start removing + // trash directories next time. + mFailedTrashDirs.Clear(); + return NS_ERROR_NOT_AVAILABLE; +} + +// static +nsresult CacheFileIOManager::InitIndexEntry(CacheFileHandle* aHandle, + OriginAttrsHash aOriginAttrsHash, + bool aAnonymous, bool aPinning) { + LOG( + ("CacheFileIOManager::InitIndexEntry() [handle=%p, " + "originAttrsHash=%" PRIx64 ", " + "anonymous=%d, pinning=%d]", + aHandle, aOriginAttrsHash, aAnonymous, aPinning)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr ev = + new InitIndexEntryEvent(aHandle, aOriginAttrsHash, aAnonymous, aPinning); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::UpdateIndexEntry(CacheFileHandle* aHandle, + const uint32_t* aFrecency, + const bool* aHasAltData, + const uint16_t* aOnStartTime, + const uint16_t* aOnStopTime, + const uint8_t* aContentType) { + LOG( + ("CacheFileIOManager::UpdateIndexEntry() [handle=%p, frecency=%s, " + "hasAltData=%s, onStartTime=%s, onStopTime=%s, contentType=%s]", + aHandle, aFrecency ? nsPrintfCString("%u", *aFrecency).get() : "", + aHasAltData ? (*aHasAltData ? "true" : "false") : "", + aOnStartTime ? nsPrintfCString("%u", *aOnStartTime).get() : "", + aOnStopTime ? nsPrintfCString("%u", *aOnStopTime).get() : "", + aContentType ? nsPrintfCString("%u", *aContentType).get() : "")); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr ev = new UpdateIndexEntryEvent( + aHandle, aFrecency, aHasAltData, aOnStartTime, aOnStopTime, aContentType); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::CreateFile(CacheFileHandle* aHandle) { + MOZ_ASSERT(!aHandle->mFD); + MOZ_ASSERT(aHandle->mFile); + + nsresult rv; + + if (aHandle->IsDoomed()) { + nsCOMPtr file; + + rv = GetDoomedFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mFile.swap(file); + } else { + bool exists; + if (NS_SUCCEEDED(aHandle->mFile->Exists(&exists)) && exists) { + NS_WARNING("Found a file that should not exist!"); + } + } + + rv = OpenNSPRHandle(aHandle, true); + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mFileSize = 0; + return NS_OK; +} + +// static +void CacheFileIOManager::HashToStr(const SHA1Sum::Hash* aHash, + nsACString& _retval) { + _retval.Truncate(); + const char hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + for (uint32_t i = 0; i < sizeof(SHA1Sum::Hash); i++) { + _retval.Append(hexChars[(*aHash)[i] >> 4]); + _retval.Append(hexChars[(*aHash)[i] & 0xF]); + } +} + +// static +nsresult CacheFileIOManager::StrToHash(const nsACString& aHash, + SHA1Sum::Hash* _retval) { + if (aHash.Length() != 2 * sizeof(SHA1Sum::Hash)) { + return NS_ERROR_INVALID_ARG; + } + + for (uint32_t i = 0; i < aHash.Length(); i++) { + uint8_t value; + + if (aHash[i] >= '0' && aHash[i] <= '9') { + value = aHash[i] - '0'; + } else if (aHash[i] >= 'A' && aHash[i] <= 'F') { + value = aHash[i] - 'A' + 10; + } else if (aHash[i] >= 'a' && aHash[i] <= 'f') { + value = aHash[i] - 'a' + 10; + } else { + return NS_ERROR_INVALID_ARG; + } + + if (i % 2 == 0) { + (reinterpret_cast(_retval))[i / 2] = value << 4; + } else { + (reinterpret_cast(_retval))[i / 2] += value; + } + } + + return NS_OK; +} + +nsresult CacheFileIOManager::GetFile(const SHA1Sum::Hash* aHash, + nsIFile** _retval) { + nsresult rv; + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(nsLiteralCString(ENTRIES_DIR)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString leafName; + HashToStr(aHash, leafName); + + rv = file->AppendNative(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + file.swap(*_retval); + return NS_OK; +} + +nsresult CacheFileIOManager::GetSpecialFile(const nsACString& aKey, + nsIFile** _retval) { + nsresult rv; + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(aKey); + NS_ENSURE_SUCCESS(rv, rv); + + file.swap(*_retval); + return NS_OK; +} + +nsresult CacheFileIOManager::GetDoomedFile(nsIFile** _retval) { + nsresult rv; + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(nsLiteralCString(DOOMED_DIR)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative("dummyleaf"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + const int32_t kMaxTries = 64; + srand(static_cast(PR_Now())); + nsAutoCString leafName; + for (int32_t triesCount = 0;; ++triesCount) { + leafName.AppendInt(rand()); + rv = file->SetNativeLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + if (NS_SUCCEEDED(file->Exists(&exists)) && !exists) { + break; + } + + if (triesCount == kMaxTries) { + LOG( + ("CacheFileIOManager::GetDoomedFile() - Could not find unused file " + "name in %d tries.", + kMaxTries)); + return NS_ERROR_FAILURE; + } + + leafName.Truncate(); + } + + file.swap(*_retval); + return NS_OK; +} + +nsresult CacheFileIOManager::IsEmptyDirectory(nsIFile* aFile, bool* _retval) { + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + nsresult rv; + + nsCOMPtr enumerator; + rv = aFile->GetDirectoryEntries(getter_AddRefs(enumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMoreElements = false; + rv = enumerator->HasMoreElements(&hasMoreElements); + NS_ENSURE_SUCCESS(rv, rv); + + *_retval = !hasMoreElements; + return NS_OK; +} + +nsresult CacheFileIOManager::CheckAndCreateDir(nsIFile* aFile, const char* aDir, + bool aEnsureEmptyDir) { + nsresult rv; + + nsCOMPtr file; + if (!aDir) { + file = aFile; + } else { + nsAutoCString dir(aDir); + rv = aFile->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative(dir); + NS_ENSURE_SUCCESS(rv, rv); + } + + bool exists = false; + rv = file->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) { + bool isDirectory = false; + rv = file->IsDirectory(&isDirectory); + if (NS_FAILED(rv) || !isDirectory) { + // Try to remove the file + rv = file->Remove(false); + if (NS_SUCCEEDED(rv)) { + exists = false; + } + } + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aEnsureEmptyDir && NS_SUCCEEDED(rv) && exists) { + bool isEmpty; + rv = IsEmptyDirectory(file, &isEmpty); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isEmpty) { + // Don't check the result, if this fails, it's OK. We do this + // only for the doomed directory that doesn't need to be deleted + // for the cost of completely disabling the whole browser. + TrashDirectory(file); + } + } + + if (NS_SUCCEEDED(rv) && !exists) { + rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700); + } + if (NS_FAILED(rv)) { + NS_WARNING("Cannot create directory"); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult CacheFileIOManager::CreateCacheTree() { + MOZ_ASSERT(mIOThread->IsCurrentThread()); + MOZ_ASSERT(!mTreeCreated); + + if (!mCacheDirectory || mTreeCreationFailed) { + return NS_ERROR_FILE_INVALID_PATH; + } + + nsresult rv; + + // Set the flag here and clear it again below when the tree is created + // successfully. + mTreeCreationFailed = true; + + // ensure parent directory exists + nsCOMPtr parentDir; + rv = mCacheDirectory->GetParent(getter_AddRefs(parentDir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = CheckAndCreateDir(parentDir, nullptr, false); + NS_ENSURE_SUCCESS(rv, rv); + + // ensure cache directory exists + rv = CheckAndCreateDir(mCacheDirectory, nullptr, false); + NS_ENSURE_SUCCESS(rv, rv); + + // ensure entries directory exists + rv = CheckAndCreateDir(mCacheDirectory, ENTRIES_DIR, false); + NS_ENSURE_SUCCESS(rv, rv); + + // ensure doomed directory exists + rv = CheckAndCreateDir(mCacheDirectory, DOOMED_DIR, true); + NS_ENSURE_SUCCESS(rv, rv); + + mTreeCreated = true; + mTreeCreationFailed = false; + + if (!mContextEvictor) { + RefPtr contextEvictor; + contextEvictor = new CacheFileContextEvictor(); + + // Init() method will try to load unfinished contexts from the disk. Store + // the evictor as a member only when there is some unfinished job. + contextEvictor->Init(mCacheDirectory); + if (contextEvictor->ContextsCount()) { + contextEvictor.swap(mContextEvictor); + } + } + + StartRemovingTrash(); + + return NS_OK; +} + +nsresult CacheFileIOManager::OpenNSPRHandle(CacheFileHandle* aHandle, + bool aCreate) { + LOG(("CacheFileIOManager::OpenNSPRHandle BEGIN, handle=%p", aHandle)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(!aHandle->mFD); + MOZ_ASSERT(mHandlesByLastUsed.IndexOf(aHandle) == mHandlesByLastUsed.NoIndex); + MOZ_ASSERT(mHandlesByLastUsed.Length() <= kOpenHandlesLimit); + MOZ_ASSERT((aCreate && !aHandle->mFileExists) || + (!aCreate && aHandle->mFileExists)); + + nsresult rv; + + if (mHandlesByLastUsed.Length() == kOpenHandlesLimit) { + // close handle that hasn't been used for the longest time + rv = MaybeReleaseNSPRHandleInternal(mHandlesByLastUsed[0], true); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aCreate) { + rv = aHandle->mFile->OpenNSPRFileDesc( + PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0600, &aHandle->mFD); + if (rv == NS_ERROR_FILE_ALREADY_EXISTS || // error from nsLocalFileWin + rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { // error from nsLocalFileUnix + LOG( + ("CacheFileIOManager::OpenNSPRHandle() - Cannot create a new file, we" + " might reached a limit on FAT32. Will evict a single entry and try " + "again. [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHandle->Hash()))); + + SHA1Sum::Hash hash; + uint32_t cnt; + + rv = CacheIndex::GetEntryForEviction(true, &hash, &cnt); + if (NS_SUCCEEDED(rv)) { + rv = DoomFileByKeyInternal(&hash); + } + if (NS_SUCCEEDED(rv)) { + rv = aHandle->mFile->OpenNSPRFileDesc( + PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0600, &aHandle->mFD); + LOG( + ("CacheFileIOManager::OpenNSPRHandle() - Successfully evicted entry" + " with hash %08x%08x%08x%08x%08x. %s to create the new file.", + LOGSHA1(&hash), NS_SUCCEEDED(rv) ? "Succeeded" : "Failed")); + + // Report the full size only once per session + static bool sSizeReported = false; + if (!sSizeReported) { + uint32_t cacheUsage; + if (NS_SUCCEEDED(CacheIndex::GetCacheSize(&cacheUsage))) { + cacheUsage >>= 10; + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_SIZE_FULL_FAT, + cacheUsage); + sSizeReported = true; + } + } + } else { + LOG( + ("CacheFileIOManager::OpenNSPRHandle() - Couldn't evict an existing" + " entry.")); + rv = NS_ERROR_FILE_NO_DEVICE_SPACE; + } + } + if (NS_FAILED(rv)) { + LOG( + ("CacheFileIOManager::OpenNSPRHandle() Create failed with " + "0x%08" PRIx32, + static_cast(rv))); + } + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mFileExists = true; + } else { + rv = aHandle->mFile->OpenNSPRFileDesc(PR_RDWR, 0600, &aHandle->mFD); + if (NS_ERROR_FILE_NOT_FOUND == rv) { + LOG((" file doesn't exists")); + aHandle->mFileExists = false; + return DoomFileInternal(aHandle); + } + if (NS_FAILED(rv)) { + LOG(("CacheFileIOManager::OpenNSPRHandle() Open failed with 0x%08" PRIx32, + static_cast(rv))); + } + NS_ENSURE_SUCCESS(rv, rv); + } + + mHandlesByLastUsed.AppendElement(aHandle); + + LOG(("CacheFileIOManager::OpenNSPRHandle END, handle=%p", aHandle)); + + return NS_OK; +} + +void CacheFileIOManager::NSPRHandleUsed(CacheFileHandle* aHandle) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(aHandle->mFD); + + DebugOnly found{}; + found = mHandlesByLastUsed.RemoveElement(aHandle); + MOZ_ASSERT(found); + + mHandlesByLastUsed.AppendElement(aHandle); +} + +nsresult CacheFileIOManager::SyncRemoveDir(nsIFile* aFile, const char* aDir) { + nsresult rv; + nsCOMPtr file; + + if (!aFile) { + return NS_ERROR_INVALID_ARG; + } + + if (!aDir) { + file = aFile; + } else { + rv = aFile->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->AppendNative(nsDependentCString(aDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (LOG_ENABLED()) { + LOG(("CacheFileIOManager::SyncRemoveDir() - Removing directory %s", + file->HumanReadablePath().get())); + } + + rv = file->Remove(true); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::SyncRemoveDir() - Removing failed! " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + + return rv; +} + +nsresult CacheFileIOManager::DispatchPurgeTask( + const nsCString& aCacheDirName, const nsCString& aSecondsToWait, + const nsCString& aPurgeExtension) { +#if !defined(MOZ_BACKGROUNDTASKS) + // If background tasks are disabled, then we should just bail out early. + return NS_ERROR_NOT_IMPLEMENTED; +#else + nsCOMPtr cacheDir; + nsresult rv = mCacheDirectory->Clone(getter_AddRefs(cacheDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr profileDir; + rv = cacheDir->GetParent(getter_AddRefs(profileDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr lf; + rv = XRE_GetBinaryPath(getter_AddRefs(lf)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString path; +# if !defined(XP_WIN) + rv = profileDir->GetNativePath(path); +# else + rv = profileDir->GetNativeTarget(path); +# endif + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr runner = + do_GetService("@mozilla.org/backgroundtasksrunner;1"); + + return runner->RemoveDirectoryInDetachedProcess( + path, aCacheDirName, aSecondsToWait, aPurgeExtension, "HttpCache"_ns); +#endif +} + +void CacheFileIOManager::SyncRemoveAllCacheFiles() { + LOG(("CacheFileIOManager::SyncRemoveAllCacheFiles()")); + nsresult rv; + + // If we are already running in a background task, we + // don't want to spawn yet another one at shutdown. + if (inBackgroundTask()) { + return; + } + + if (StaticPrefs::network_cache_shutdown_purge_in_background_task()) { + rv = [&]() -> nsresult { + nsresult rv; + + // If there is no cache directory, there's nothing to remove. + if (!mCacheDirectory) { + return NS_OK; + } + + nsAutoCString leafName; + rv = mCacheDirectory->GetNativeLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + leafName.Append('.'); + + PRExplodedTime now; + PR_ExplodeTime(PR_Now(), PR_GMTParameters, &now); + leafName.Append(nsPrintfCString( + "%04d-%02d-%02d-%02d-%02d-%02d", now.tm_year, now.tm_month + 1, + now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec)); + leafName.Append(kPurgeExtension); + + nsAutoCString secondsToWait; + secondsToWait.AppendInt( + StaticPrefs::network_cache_shutdown_purge_folder_wait_seconds()); + + rv = DispatchPurgeTask(leafName, secondsToWait, kPurgeExtension); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mCacheDirectory->RenameToNative(nullptr, leafName); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + }(); + + // Dispatching to the background task has succeeded. This is finished. + if (NS_SUCCEEDED(rv)) { + return; + } + } + + SyncRemoveDir(mCacheDirectory, ENTRIES_DIR); + SyncRemoveDir(mCacheDirectory, DOOMED_DIR); + + // Clear any intermediate state of trash dir enumeration. + mFailedTrashDirs.Clear(); + mTrashDir = nullptr; + + while (true) { + // FindTrashDirToRemove() fills mTrashDir if there is any trash directory. + rv = FindTrashDirToRemove(); + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG( + ("CacheFileIOManager::SyncRemoveAllCacheFiles() - No trash directory " + "found.")); + break; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::SyncRemoveAllCacheFiles() - " + "FindTrashDirToRemove() returned an unexpected error. " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + break; + } + + rv = SyncRemoveDir(mTrashDir, nullptr); + if (NS_FAILED(rv)) { + nsAutoCString leafName; + mTrashDir->GetNativeLeafName(leafName); + mFailedTrashDirs.AppendElement(leafName); + } + } +} + +// Returns default ("smart") size (in KB) of cache, given available disk space +// (also in KB) +static uint32_t SmartCacheSize(const int64_t availKB) { + uint32_t maxSize; + + if (CacheObserver::ClearCacheOnShutdown()) { + maxSize = kMaxClearOnShutdownCacheSizeKB; + } else { + maxSize = kMaxCacheSizeKB; + } + + if (availKB > 25 * 1024 * 1024) { + return maxSize; // skip computing if we're over 25 GB + } + + // Grow/shrink in 10 MB units, deliberately, so that in the common case we + // don't shrink cache and evict items every time we startup (it's important + // that we don't slow down startup benchmarks). + uint32_t sz10MBs = 0; + uint32_t avail10MBs = availKB / (1024 * 10); + + // 2.5% of space above 7GB + if (avail10MBs > 700) { + sz10MBs += static_cast((avail10MBs - 700) * .025); + avail10MBs = 700; + } + // 7.5% of space between 500 MB -> 7 GB + if (avail10MBs > 50) { + sz10MBs += static_cast((avail10MBs - 50) * .075); + avail10MBs = 50; + } + +#ifdef ANDROID + // On Android, smaller/older devices may have very little storage and + // device owners may be sensitive to storage footprint: Use a smaller + // percentage of available space and a smaller minimum. + + // 16% of space up to 500 MB (10 MB min) + sz10MBs += std::max(1, static_cast(avail10MBs * .16)); +#else + // 30% of space up to 500 MB (50 MB min) + sz10MBs += std::max(5, static_cast(avail10MBs * .3)); +#endif + + return std::min(maxSize, sz10MBs * 10 * 1024); +} + +nsresult CacheFileIOManager::UpdateSmartCacheSize(int64_t aFreeSpace) { + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + nsresult rv; + + if (!CacheObserver::SmartCacheSizeEnabled()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Wait at least kSmartSizeUpdateInterval before recomputing smart size. + static const TimeDuration kUpdateLimit = + TimeDuration::FromMilliseconds(kSmartSizeUpdateInterval); + if (!mLastSmartSizeTime.IsNull() && + (TimeStamp::NowLoRes() - mLastSmartSizeTime) < kUpdateLimit) { + return NS_OK; + } + + // Do not compute smart size when cache size is not reliable. + bool isUpToDate = false; + CacheIndex::IsUpToDate(&isUpToDate); + if (!isUpToDate) { + return NS_ERROR_NOT_AVAILABLE; + } + + uint32_t cacheUsage; + rv = CacheIndex::GetCacheSize(&cacheUsage); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::UpdateSmartCacheSize() - Cannot get cacheUsage! " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + return rv; + } + + mLastSmartSizeTime = TimeStamp::NowLoRes(); + + uint32_t smartSize = SmartCacheSize(aFreeSpace + cacheUsage); + + if (smartSize == CacheObserver::DiskCacheCapacity()) { + // Smart size has not changed. + return NS_OK; + } + + CacheObserver::SetSmartDiskCacheCapacity(smartSize); + + return NS_OK; +} + +// Memory reporting + +namespace { + +// A helper class that dispatches and waits for an event that gets result of +// CacheFileIOManager->mHandles.SizeOfExcludingThis() on the I/O thread +// to safely get handles memory report. +// We must do this, since the handle list is only accessed and managed w/o +// locking on the I/O thread. That is by design. +class SizeOfHandlesRunnable : public Runnable { + public: + SizeOfHandlesRunnable(mozilla::MallocSizeOf mallocSizeOf, + CacheFileHandles const& handles, + nsTArray const& specialHandles) + : Runnable("net::SizeOfHandlesRunnable"), + mMonitor("SizeOfHandlesRunnable.mMonitor"), + mMonitorNotified(false), + mMallocSizeOf(mallocSizeOf), + mHandles(handles), + mSpecialHandles(specialHandles), + mSize(0) {} + + size_t Get(CacheIOThread* thread) { + nsCOMPtr target = thread->Target(); + if (!target) { + NS_ERROR("If we have the I/O thread we also must have the I/O target"); + return 0; + } + + mozilla::MonitorAutoLock mon(mMonitor); + mMonitorNotified = false; + nsresult rv = target->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_ERROR("Dispatch failed, cannot do memory report of CacheFileHandles"); + return 0; + } + + while (!mMonitorNotified) { + mon.Wait(); + } + return mSize; + } + + NS_IMETHOD Run() override { + mozilla::MonitorAutoLock mon(mMonitor); + // Excluding this since the object itself is a member of CacheFileIOManager + // reported in CacheFileIOManager::SizeOfIncludingThis as part of |this|. + mSize = mHandles.SizeOfExcludingThis(mMallocSizeOf); + for (uint32_t i = 0; i < mSpecialHandles.Length(); ++i) { + mSize += mSpecialHandles[i]->SizeOfIncludingThis(mMallocSizeOf); + } + + mMonitorNotified = true; + mon.Notify(); + return NS_OK; + } + + private: + mozilla::Monitor mMonitor MOZ_UNANNOTATED; + bool mMonitorNotified; + mozilla::MallocSizeOf mMallocSizeOf; + CacheFileHandles const& mHandles; + nsTArray const& mSpecialHandles; + size_t mSize; +}; + +} // namespace + +size_t CacheFileIOManager::SizeOfExcludingThisInternal( + mozilla::MallocSizeOf mallocSizeOf) const { + size_t n = 0; + nsCOMPtr sizeOf; + + if (mIOThread) { + n += mIOThread->SizeOfIncludingThis(mallocSizeOf); + + // mHandles and mSpecialHandles must be accessed only on the I/O thread, + // must sync dispatch. + RefPtr sizeOfHandlesRunnable = + new SizeOfHandlesRunnable(mallocSizeOf, mHandles, mSpecialHandles); + n += sizeOfHandlesRunnable->Get(mIOThread); + } + + // mHandlesByLastUsed just refers handles reported by mHandles. + + sizeOf = do_QueryInterface(mCacheDirectory); + if (sizeOf) n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + + sizeOf = do_QueryInterface(mMetadataWritesTimer); + if (sizeOf) n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + + sizeOf = do_QueryInterface(mTrashTimer); + if (sizeOf) n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + + sizeOf = do_QueryInterface(mTrashDir); + if (sizeOf) n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + + for (uint32_t i = 0; i < mFailedTrashDirs.Length(); ++i) { + n += mFailedTrashDirs[i].SizeOfExcludingThisIfUnshared(mallocSizeOf); + } + + return n; +} + +// static +size_t CacheFileIOManager::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) { + if (!gInstance) return 0; + + return gInstance->SizeOfExcludingThisInternal(mallocSizeOf); +} + +// static +size_t CacheFileIOManager::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) { + return mallocSizeOf(gInstance) + SizeOfExcludingThis(mallocSizeOf); +} + +} // namespace mozilla::net -- cgit v1.2.3