diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /netwerk/cache/nsCacheService.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/cache/nsCacheService.cpp')
-rw-r--r-- | netwerk/cache/nsCacheService.cpp | 1910 |
1 files changed, 1910 insertions, 0 deletions
diff --git a/netwerk/cache/nsCacheService.cpp b/netwerk/cache/nsCacheService.cpp new file mode 100644 index 0000000000..f7cb31d010 --- /dev/null +++ b/netwerk/cache/nsCacheService.cpp @@ -0,0 +1,1910 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsCacheService.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/Assertions.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/FileUtils.h" + +#include "nsCache.h" +#include "nsCacheRequest.h" +#include "nsCacheEntry.h" +#include "nsCacheEntryDescriptor.h" +#include "nsCacheDevice.h" +#include "nsICacheVisitor.h" +#include "nsDiskCacheDeviceSQL.h" +#include "nsCacheUtils.h" +#include "../cache2/CacheObserver.h" +#include "nsIObserverService.h" +#include "nsIPrefBranch.h" +#include "nsIFile.h" +#include "nsIOService.h" +#include "nsDirectoryServiceDefs.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "nsDeleteDir.h" +#include "nsNetCID.h" +#include <math.h> // for log() +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_browser.h" + +#include "mozilla/net/NeckoCommon.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::net; + +/****************************************************************************** + * nsCacheProfilePrefObserver + *****************************************************************************/ +#define OFFLINE_CACHE_DIR_PREF "browser.cache.offline.parent_directory" +#define OFFLINE_CACHE_CAPACITY_PREF "browser.cache.offline.capacity" +#define OFFLINE_CACHE_CAPACITY 512000 + +#define CACHE_COMPRESSION_LEVEL 1 + +static const char* observerList[] = { + "profile-before-change", "profile-do-change", + NS_XPCOM_SHUTDOWN_OBSERVER_ID, "last-pb-context-exited", + "suspend_process_notification", "resume_process_notification"}; + +class nsCacheProfilePrefObserver : public nsIObserver { + virtual ~nsCacheProfilePrefObserver() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + nsCacheProfilePrefObserver() + : mHaveProfile(false), + mOfflineCacheEnabled(false), + mOfflineStorageCacheEnabled(false), + mOfflineCacheCapacity(0), + mCacheCompressionLevel(CACHE_COMPRESSION_LEVEL), + mSanitizeOnShutdown(false), + mClearCacheOnShutdown(false) {} + + nsresult Install(); + void Remove(); + nsresult ReadPrefs(nsIPrefBranch* branch); + + nsIFile* DiskCacheParentDirectory() { return mDiskCacheParentDirectory; } + + bool OfflineCacheEnabled(); + int32_t OfflineCacheCapacity() { return mOfflineCacheCapacity; } + nsIFile* OfflineCacheParentDirectory() { + return mOfflineCacheParentDirectory; + } + + int32_t CacheCompressionLevel(); + + bool SanitizeAtShutdown() { + return mSanitizeOnShutdown && mClearCacheOnShutdown; + } + + private: + bool mHaveProfile; + + nsCOMPtr<nsIFile> mDiskCacheParentDirectory; + + bool mOfflineCacheEnabled; + bool mOfflineStorageCacheEnabled; + int32_t mOfflineCacheCapacity; // in kilobytes + nsCOMPtr<nsIFile> mOfflineCacheParentDirectory; + + int32_t mCacheCompressionLevel; + + bool mSanitizeOnShutdown; + bool mClearCacheOnShutdown; +}; + +NS_IMPL_ISUPPORTS(nsCacheProfilePrefObserver, nsIObserver) + +class nsBlockOnCacheThreadEvent : public Runnable { + public: + nsBlockOnCacheThreadEvent() + : mozilla::Runnable("nsBlockOnCacheThreadEvent") {} + NS_IMETHOD Run() override { + nsCacheServiceAutoLock autoLock(LOCK_TELEM(NSBLOCKONCACHETHREADEVENT_RUN)); + CACHE_LOG_DEBUG(("nsBlockOnCacheThreadEvent [%p]\n", this)); + nsCacheService::gService->mNotified = true; + nsCacheService::gService->mCondVar.Notify(); + return NS_OK; + } +}; + +nsresult nsCacheProfilePrefObserver::Install() { + // install profile-change observer + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) return NS_ERROR_FAILURE; + + nsresult rv, rv2 = NS_OK; + for (auto& observer : observerList) { + rv = observerService->AddObserver(this, observer, false); + if (NS_FAILED(rv)) rv2 = rv; + } + + // install preferences observer + nsCOMPtr<nsIPrefBranch> branch = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!branch) return NS_ERROR_FAILURE; + + // Determine if we have a profile already + // Install() is called *after* the profile-after-change notification + // when there is only a single profile, or it is specified on the + // commandline at startup. + // In that case, we detect the presence of a profile by the existence + // of the NS_APP_USER_PROFILE_50_DIR directory. + + nsCOMPtr<nsIFile> directory; + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(directory)); + if (NS_SUCCEEDED(rv)) mHaveProfile = true; + + rv = ReadPrefs(branch); + NS_ENSURE_SUCCESS(rv, rv); + + return rv2; +} + +void nsCacheProfilePrefObserver::Remove() { + // remove Observer Service observers + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + for (auto& observer : observerList) { + obs->RemoveObserver(this, observer); + } + } +} + +NS_IMETHODIMP +nsCacheProfilePrefObserver::Observe(nsISupports* subject, const char* topic, + const char16_t* data_unicode) { + NS_ConvertUTF16toUTF8 data(data_unicode); + CACHE_LOG_INFO(("Observe [topic=%s data=%s]\n", topic, data.get())); + + if (!nsCacheService::IsInitialized()) { + if (!strcmp("resume_process_notification", topic)) { + // A suspended process has a closed cache, so re-open it here. + nsCacheService::GlobalInstance()->Init(); + } + return NS_OK; + } + + if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, topic)) { + // xpcom going away, shutdown cache service + nsCacheService::GlobalInstance()->Shutdown(); + } else if (!strcmp("profile-before-change", topic)) { + // profile before change + mHaveProfile = false; + + // XXX shutdown devices + nsCacheService::OnProfileShutdown(); + } else if (!strcmp("suspend_process_notification", topic)) { + // A suspended process may never return, so shutdown the cache to reduce + // cache corruption. + nsCacheService::GlobalInstance()->Shutdown(); + } else if (!strcmp("profile-do-change", topic)) { + // profile after change + mHaveProfile = true; + nsCOMPtr<nsIPrefBranch> branch = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!branch) { + return NS_ERROR_FAILURE; + } + (void)ReadPrefs(branch); + nsCacheService::OnProfileChanged(); + + } else if (!strcmp("last-pb-context-exited", topic)) { + nsCacheService::LeavePrivateBrowsing(); + } + + return NS_OK; +} + +static already_AddRefed<nsIFile> GetCacheDirectory(const char* aSubdir, + bool aAllowProcDirCache) { + nsCOMPtr<nsIFile> directory; + + // try to get the disk cache parent directory + Unused << NS_GetSpecialDirectory(NS_APP_CACHE_PARENT_DIR, + getter_AddRefs(directory)); + if (!directory) { + // try to get the profile directory (there may not be a profile yet) + nsCOMPtr<nsIFile> profDir; + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profDir)); + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR, + getter_AddRefs(directory)); + if (!directory) + directory = profDir; + else if (profDir) { + nsCacheService::MoveOrRemoveDiskCache(profDir, directory, aSubdir); + } + } + if (!directory && aAllowProcDirCache) { + Unused << NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, + getter_AddRefs(directory)); + } + return directory.forget(); +} + +nsresult nsCacheProfilePrefObserver::ReadPrefs(nsIPrefBranch* branch) { + if (!mDiskCacheParentDirectory) { + // use file cache in build tree only if asked, to avoid cache dir litter + bool allowProcDirCache = PR_GetEnv("NECKO_DEV_ENABLE_DISK_CACHE"); + mDiskCacheParentDirectory = GetCacheDirectory("Cache", allowProcDirCache); + } + + // read offline cache device prefs + mOfflineCacheEnabled = StaticPrefs::browser_cache_offline_enable(); + mOfflineStorageCacheEnabled = + StaticPrefs::browser_cache_offline_storage_enable(); + + mOfflineCacheCapacity = OFFLINE_CACHE_CAPACITY; + (void)branch->GetIntPref(OFFLINE_CACHE_CAPACITY_PREF, &mOfflineCacheCapacity); + mOfflineCacheCapacity = std::max(0, mOfflineCacheCapacity); + + (void)branch->GetComplexValue(OFFLINE_CACHE_DIR_PREF, // ignore error + NS_GET_IID(nsIFile), + getter_AddRefs(mOfflineCacheParentDirectory)); + + if (!mOfflineCacheParentDirectory) { +#ifdef DEBUG + bool allowProcDirCache = true; +#else + bool allowProcDirCache = false; +#endif + mOfflineCacheParentDirectory = + GetCacheDirectory("OfflineCache", allowProcDirCache); + } + + if (!mDiskCacheParentDirectory || !mOfflineCacheParentDirectory) { + return NS_ERROR_FAILURE; + } + + if (!mOfflineStorageCacheEnabled) { + // Dispatch cleanup task + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction("Delete OfflineCache", []() { + nsCOMPtr<nsIFile> dir; + nsCacheService::GetAppCacheDirectory(getter_AddRefs(dir)); + bool exists = false; + if (dir && NS_SUCCEEDED(dir->Exists(&exists)) && exists) { + // Delay delete by 1 minute to avoid IO thrash on startup. + CACHE_LOG_INFO( + ("Queuing Delete of AppCacheDirectory in 60 seconds")); + nsDeleteDir::DeleteDir(dir, false, 60000); + } + }); + Unused << nsCacheService::DispatchToCacheIOThread(runnable); + } + + return NS_OK; +} + +nsresult nsCacheService::DispatchToCacheIOThread(nsIRunnable* event) { + if (!gService || !gService->mCacheIOThread) return NS_ERROR_NOT_AVAILABLE; + return gService->mCacheIOThread->Dispatch(event, NS_DISPATCH_NORMAL); +} + +nsresult nsCacheService::SyncWithCacheIOThread() { + if (!gService || !gService->mCacheIOThread) return NS_ERROR_NOT_AVAILABLE; + gService->mLock.AssertCurrentThreadOwns(); + + nsCOMPtr<nsIRunnable> event = new nsBlockOnCacheThreadEvent(); + + // dispatch event - it will notify the monitor when it's done + nsresult rv = gService->mCacheIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_WARNING("Failed dispatching block-event"); + return NS_ERROR_UNEXPECTED; + } + + // wait until notified, then return + gService->mNotified = false; + while (!gService->mNotified) { + gService->mCondVar.Wait(); + } + + return NS_OK; +} + +bool nsCacheProfilePrefObserver::OfflineCacheEnabled() { + if ((mOfflineCacheCapacity == 0) || (!mOfflineCacheParentDirectory)) + return false; + + return mOfflineCacheEnabled && mOfflineStorageCacheEnabled; +} + +int32_t nsCacheProfilePrefObserver::CacheCompressionLevel() { + return mCacheCompressionLevel; +} + +/****************************************************************************** + * nsProcessRequestEvent + *****************************************************************************/ + +class nsProcessRequestEvent : public Runnable { + public: + explicit nsProcessRequestEvent(nsCacheRequest* aRequest) + : mozilla::Runnable("nsProcessRequestEvent") { + mRequest = aRequest; + } + + NS_IMETHOD Run() override { + nsresult rv; + + NS_ASSERTION(mRequest->mListener, + "Sync OpenCacheEntry() posted to background thread!"); + + nsCacheServiceAutoLock lock(LOCK_TELEM(NSPROCESSREQUESTEVENT_RUN)); + rv = nsCacheService::gService->ProcessRequest(mRequest, false, nullptr); + + // Don't delete the request if it was queued + if (!(mRequest->IsBlocking() && rv == NS_ERROR_CACHE_WAIT_FOR_VALIDATION)) + delete mRequest; + + return NS_OK; + } + + protected: + virtual ~nsProcessRequestEvent() = default; + + private: + nsCacheRequest* mRequest; +}; + +/****************************************************************************** + * nsDoomEvent + *****************************************************************************/ + +class nsDoomEvent : public Runnable { + public: + nsDoomEvent(nsCacheSession* session, const nsACString& key, + nsICacheListener* listener) + : mozilla::Runnable("nsDoomEvent") { + mKey = *session->ClientID(); + mKey.Append(':'); + mKey.Append(key); + mStoragePolicy = session->StoragePolicy(); + mListener = listener; + mEventTarget = GetCurrentEventTarget(); + // We addref the listener here and release it in nsNotifyDoomListener + // on the callers thread. If posting of nsNotifyDoomListener event fails + // we leak the listener which is better than releasing it on a wrong + // thread. + NS_IF_ADDREF(mListener); + } + + NS_IMETHOD Run() override { + nsCacheServiceAutoLock lock; + + bool foundActive = true; + nsresult status = NS_ERROR_NOT_AVAILABLE; + nsCacheEntry* entry; + entry = nsCacheService::gService->mActiveEntries.GetEntry(&mKey); + if (!entry) { + bool collision = false; + foundActive = false; + entry = nsCacheService::gService->SearchCacheDevices( + &mKey, mStoragePolicy, &collision); + } + + if (entry) { + status = NS_OK; + nsCacheService::gService->DoomEntry_Internal(entry, foundActive); + } + + if (mListener) { + mEventTarget->Dispatch(new nsNotifyDoomListener(mListener, status), + NS_DISPATCH_NORMAL); + // posted event will release the reference on the correct thread + mListener = nullptr; + } + + return NS_OK; + } + + private: + nsCString mKey; + nsCacheStoragePolicy mStoragePolicy; + nsICacheListener* mListener; + nsCOMPtr<nsIEventTarget> mEventTarget; +}; + +/****************************************************************************** + * nsCacheService + *****************************************************************************/ +nsCacheService* nsCacheService::gService = nullptr; + +NS_IMPL_ISUPPORTS(nsCacheService, nsICacheService, nsICacheServiceInternal) + +nsCacheService::nsCacheService() + : mObserver(nullptr), + mLock("nsCacheService.mLock"), + mCondVar(mLock, "nsCacheService.mCondVar"), + mNotified(false), + mTimeStampLock("nsCacheService.mTimeStampLock"), + mInitialized(false), + mClearingEntries(false), + mEnableOfflineDevice(false), + mOfflineDevice(nullptr), + mDoomedEntries{}, + mTotalEntries(0), + mCacheHits(0), + mCacheMisses(0), + mMaxKeyLength(0), + mMaxDataSize(0), + mMaxMetaSize(0), + mDeactivateFailures(0), + mDeactivatedUnboundEntries(0) { + NS_ASSERTION(gService == nullptr, "multiple nsCacheService instances!"); + gService = this; + + // create list of cache devices + PR_INIT_CLIST(&mDoomedEntries); +} + +nsCacheService::~nsCacheService() { + if (mInitialized) // Shutdown hasn't been called yet. + (void)Shutdown(); + + if (mObserver) { + mObserver->Remove(); + NS_RELEASE(mObserver); + } + + gService = nullptr; +} + +nsresult nsCacheService::Init() { + // Thie method must be called on the main thread because mCacheIOThread must + // only be modified on the main thread. + if (!NS_IsMainThread()) { + NS_ERROR("nsCacheService::Init called off the main thread"); + return NS_ERROR_NOT_SAME_THREAD; + } + + NS_ASSERTION(!mInitialized, "nsCacheService already initialized."); + if (mInitialized) return NS_ERROR_ALREADY_INITIALIZED; + + if (mozilla::net::IsNeckoChild()) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv; + + mStorageService = do_GetService("@mozilla.org/storage/service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewNamedThread("Cache I/O", getter_AddRefs(mCacheIOThread)); + if (NS_FAILED(rv)) { + NS_WARNING("Can't create cache IO thread"); + } + + rv = nsDeleteDir::Init(); + if (NS_FAILED(rv)) { + NS_WARNING("Can't initialize nsDeleteDir"); + } + + // initialize hashtable for active cache entries + mActiveEntries.Init(); + + // create profile/preference observer + if (!mObserver) { + mObserver = new nsCacheProfilePrefObserver(); + NS_ADDREF(mObserver); + mObserver->Install(); + } + + mEnableOfflineDevice = mObserver->OfflineCacheEnabled(); + + mInitialized = true; + return NS_OK; +} + +void nsCacheService::Shutdown() { + // This method must be called on the main thread because mCacheIOThread must + // only be modified on the main thread. + if (!NS_IsMainThread()) { + MOZ_CRASH("nsCacheService::Shutdown called off the main thread"); + } + + nsCOMPtr<nsIThread> cacheIOThread; + Telemetry::AutoTimer<Telemetry::NETWORK_DISK_CACHE_SHUTDOWN> totalTimer; + + bool shouldSanitize = false; + nsCOMPtr<nsIFile> parentDir; + + { + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SHUTDOWN)); + NS_ASSERTION( + mInitialized, + "can't shutdown nsCacheService unless it has been initialized."); + if (!mInitialized) return; + + mClearingEntries = true; + DoomActiveEntries(nullptr); + } + + CloseAllStreams(); + + { + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SHUTDOWN)); + NS_ASSERTION(mInitialized, "Bad state"); + + mInitialized = false; + + // Clear entries + ClearDoomList(); + + // Make sure to wait for any pending cache-operations before + // proceeding with destructive actions (bug #620660) + (void)SyncWithCacheIOThread(); + mActiveEntries.Shutdown(); + + // obtain the disk cache directory in case we need to sanitize it + parentDir = mObserver->DiskCacheParentDirectory(); + shouldSanitize = mObserver->SanitizeAtShutdown(); + + if (mOfflineDevice) mOfflineDevice->Shutdown(); + + NS_IF_RELEASE(mOfflineDevice); + + for (auto iter = mCustomOfflineDevices.Iter(); !iter.Done(); iter.Next()) { + iter.Data()->Shutdown(); + iter.Remove(); + } + + LogCacheStatistics(); + + mClearingEntries = false; + mCacheIOThread.swap(cacheIOThread); + } + + if (cacheIOThread) nsShutdownThread::BlockingShutdown(cacheIOThread); + + if (shouldSanitize) { + nsresult rv = parentDir->AppendNative("Cache"_ns); + if (NS_SUCCEEDED(rv)) { + bool exists; + if (NS_SUCCEEDED(parentDir->Exists(&exists)) && exists) + nsDeleteDir::DeleteDir(parentDir, false); + } + Telemetry::AutoTimer<Telemetry::NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE> + timer; + nsDeleteDir::Shutdown(shouldSanitize); + } else { + Telemetry::AutoTimer<Telemetry::NETWORK_DISK_CACHE_DELETEDIR_SHUTDOWN> + timer; + nsDeleteDir::Shutdown(shouldSanitize); + } +} + +nsresult nsCacheService::Create(nsISupports* aOuter, const nsIID& aIID, + void** aResult) { + nsresult rv; + + if (aOuter != nullptr) return NS_ERROR_NO_AGGREGATION; + + RefPtr<nsCacheService> cacheService = new nsCacheService(); + rv = cacheService->Init(); + if (NS_SUCCEEDED(rv)) { + rv = cacheService->QueryInterface(aIID, aResult); + } + return rv; +} + +NS_IMETHODIMP +nsCacheService::CreateSession(const char* clientID, + nsCacheStoragePolicy storagePolicy, + bool streamBased, nsICacheSession** result) { + *result = nullptr; + + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsCacheService::CreateSessionInternal( + const char* clientID, nsCacheStoragePolicy storagePolicy, bool streamBased, + nsICacheSession** result) { + RefPtr<nsCacheSession> session = + new nsCacheSession(clientID, storagePolicy, streamBased); + session.forget(result); + + return NS_OK; +} + +nsresult nsCacheService::EvictEntriesForSession(nsCacheSession* session) { + NS_ASSERTION(gService, "nsCacheService::gService is null."); + return gService->EvictEntriesForClient(session->ClientID()->get(), + session->StoragePolicy()); +} + +namespace { + +class EvictionNotifierRunnable : public Runnable { + public: + explicit EvictionNotifierRunnable(nsISupports* aSubject) + : mozilla::Runnable("EvictionNotifierRunnable"), mSubject(aSubject) {} + + NS_DECL_NSIRUNNABLE + + private: + nsCOMPtr<nsISupports> mSubject; +}; + +NS_IMETHODIMP +EvictionNotifierRunnable::Run() { + nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService(); + if (obsSvc) { + obsSvc->NotifyObservers(mSubject, NS_CACHESERVICE_EMPTYCACHE_TOPIC_ID, + nullptr); + } + return NS_OK; +} + +} // namespace + +nsresult nsCacheService::EvictEntriesForClient( + const char* clientID, nsCacheStoragePolicy storagePolicy) { + RefPtr<EvictionNotifierRunnable> r = + new EvictionNotifierRunnable(NS_ISUPPORTS_CAST(nsICacheService*, this)); + NS_DispatchToMainThread(r); + + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_EVICTENTRIESFORCLIENT)); + nsresult res = NS_OK; + + // Only clear the offline cache if it has been specifically asked for. + if (storagePolicy == nsICache::STORE_OFFLINE) { + if (mEnableOfflineDevice) { + nsresult rv = NS_OK; + if (!mOfflineDevice) rv = CreateOfflineDevice(); + if (mOfflineDevice) rv = mOfflineDevice->EvictEntries(clientID); + if (NS_FAILED(rv)) res = rv; + } + } + + return res; +} + +nsresult nsCacheService::IsStorageEnabledForPolicy( + nsCacheStoragePolicy storagePolicy, bool* result) { + if (gService == nullptr) return NS_ERROR_NOT_AVAILABLE; + nsCacheServiceAutoLock lock( + LOCK_TELEM(NSCACHESERVICE_ISSTORAGEENABLEDFORPOLICY)); + + *result = nsCacheService::IsStorageEnabledForPolicy_Locked(storagePolicy); + return NS_OK; +} + +nsresult nsCacheService::DoomEntry(nsCacheSession* session, + const nsACString& key, + nsICacheListener* listener) { + CACHE_LOG_DEBUG(("Dooming entry for session %p, key %s\n", session, + PromiseFlatCString(key).get())); + if (!gService || !gService->mInitialized) return NS_ERROR_NOT_INITIALIZED; + + return DispatchToCacheIOThread(new nsDoomEvent(session, key, listener)); +} + +bool nsCacheService::IsStorageEnabledForPolicy_Locked( + nsCacheStoragePolicy storagePolicy) { + if (gService->mEnableOfflineDevice && + storagePolicy == nsICache::STORE_OFFLINE) { + return true; + } + + return false; +} + +NS_IMETHODIMP nsCacheService::VisitEntries(nsICacheVisitor* visitor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsCacheService::VisitEntriesInternal(nsICacheVisitor* visitor) { + NS_ENSURE_ARG_POINTER(visitor); + + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_VISITENTRIES)); + + if (!mEnableOfflineDevice) return NS_ERROR_NOT_AVAILABLE; + + // XXX record the fact that a visitation is in progress, + // XXX i.e. keep list of visitors in progress. + + nsresult rv = NS_OK; + + if (mEnableOfflineDevice) { + if (!mOfflineDevice) { + rv = CreateOfflineDevice(); + if (NS_FAILED(rv)) return rv; + } + rv = mOfflineDevice->Visit(visitor); + if (NS_FAILED(rv)) return rv; + } + + // XXX notify any shutdown process that visitation is complete for THIS + // visitor. + // XXX keep queue of visitors + + return NS_OK; +} + +void nsCacheService::FireClearNetworkCacheStoredAnywhereNotification() { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIObserverService> obsvc = mozilla::services::GetObserverService(); + if (obsvc) { + obsvc->NotifyObservers(nullptr, "network-clear-cache-stored-anywhere", + nullptr); + } +} + +NS_IMETHODIMP nsCacheService::EvictEntries(nsCacheStoragePolicy storagePolicy) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsCacheService::EvictEntriesInternal( + nsCacheStoragePolicy storagePolicy) { + if (storagePolicy == nsICache::STORE_ANYWHERE) { + // if not called on main thread, dispatch the notification to the main + // thread to notify observers + if (!NS_IsMainThread()) { + nsCOMPtr<nsIRunnable> event = NewRunnableMethod( + "nsCacheService::FireClearNetworkCacheStoredAnywhereNotification", + this, + &nsCacheService::FireClearNetworkCacheStoredAnywhereNotification); + NS_DispatchToMainThread(event); + } else { + // else you're already on main thread - notify observers + FireClearNetworkCacheStoredAnywhereNotification(); + } + } + return EvictEntriesForClient(nullptr, storagePolicy); +} + +NS_IMETHODIMP nsCacheService::GetCacheIOTarget( + nsIEventTarget** aCacheIOTarget) { + NS_ENSURE_ARG_POINTER(aCacheIOTarget); + + // Because mCacheIOThread can only be changed on the main thread, it can be + // read from the main thread without the lock. This is useful to prevent + // blocking the main thread on other cache operations. + if (!NS_IsMainThread()) { + Lock(LOCK_TELEM(NSCACHESERVICE_GETCACHEIOTARGET)); + } + + nsresult rv; + if (mCacheIOThread) { + *aCacheIOTarget = do_AddRef(mCacheIOThread).take(); + rv = NS_OK; + } else { + *aCacheIOTarget = nullptr; + rv = NS_ERROR_NOT_AVAILABLE; + } + + if (!NS_IsMainThread()) { + Unlock(); + } + + return rv; +} + +NS_IMETHODIMP nsCacheService::GetLockHeldTime(double* aLockHeldTime) { + MutexAutoLock lock(mTimeStampLock); + + if (mLockAcquiredTimeStamp.IsNull()) { + *aLockHeldTime = 0.0; + } else { + *aLockHeldTime = + (TimeStamp::Now() - mLockAcquiredTimeStamp).ToMilliseconds(); + } + + return NS_OK; +} + +/** + * Internal Methods + */ +nsresult nsCacheService::GetOfflineDevice(nsOfflineCacheDevice** aDevice) { + if (!mOfflineDevice) { + nsresult rv = CreateOfflineDevice(); + if (NS_FAILED(rv)) { + return rv; + } + } + + *aDevice = do_AddRef(mOfflineDevice).take(); + return NS_OK; +} + +nsresult nsCacheService::GetCustomOfflineDevice( + nsIFile* aProfileDir, int32_t aQuota, nsOfflineCacheDevice** aDevice) { + nsresult rv; + + nsAutoString profilePath; + rv = aProfileDir->GetPath(profilePath); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mCustomOfflineDevices.Get(profilePath, aDevice)) { + rv = CreateCustomOfflineDevice(aProfileDir, aQuota, aDevice); + if (NS_FAILED(rv)) { + return rv; + } + + (*aDevice)->SetAutoShutdown(); + mCustomOfflineDevices.Put(profilePath, RefPtr{*aDevice}); + } + + return NS_OK; +} + +nsresult nsCacheService::CreateOfflineDevice() { + CACHE_LOG_INFO(("Creating default offline device")); + + if (mOfflineDevice) return NS_OK; + if (!nsCacheService::IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + return CreateCustomOfflineDevice(mObserver->OfflineCacheParentDirectory(), + mObserver->OfflineCacheCapacity(), + &mOfflineDevice); +} + +nsresult nsCacheService::CreateCustomOfflineDevice( + nsIFile* aProfileDir, int32_t aQuota, nsOfflineCacheDevice** aDevice) { + NS_ENSURE_ARG(aProfileDir); + + if (MOZ_LOG_TEST(gCacheLog, LogLevel::Info)) { + CACHE_LOG_INFO(("Creating custom offline device, %s, %d", + aProfileDir->HumanReadablePath().get(), aQuota)); + } + + if (!mInitialized) { + NS_WARNING("nsCacheService not initialized"); + return NS_ERROR_NOT_AVAILABLE; + } + + if (!mEnableOfflineDevice) return NS_ERROR_NOT_AVAILABLE; + + RefPtr<nsOfflineCacheDevice> device = new nsOfflineCacheDevice(); + + // set the preferences + device->SetCacheParentDirectory(aProfileDir); + device->SetCapacity(aQuota); + + nsresult rv = device->InitWithSqlite(mStorageService); + if (NS_FAILED(rv)) { + CACHE_LOG_DEBUG(("OfflineDevice->InitWithSqlite() failed (0x%.8" PRIx32 + ")\n", + static_cast<uint32_t>(rv))); + CACHE_LOG_DEBUG((" - disabling offline cache for this session.\n")); + device = nullptr; + } + + device.forget(aDevice); + return rv; +} + +nsresult nsCacheService::RemoveCustomOfflineDevice( + nsOfflineCacheDevice* aDevice) { + nsCOMPtr<nsIFile> profileDir = aDevice->BaseDirectory(); + if (!profileDir) return NS_ERROR_UNEXPECTED; + + nsAutoString profilePath; + nsresult rv = profileDir->GetPath(profilePath); + NS_ENSURE_SUCCESS(rv, rv); + + mCustomOfflineDevices.Remove(profilePath); + return NS_OK; +} + +nsresult nsCacheService::CreateRequest(nsCacheSession* session, + const nsACString& clientKey, + nsCacheAccessMode accessRequested, + bool blockingMode, + nsICacheListener* listener, + nsCacheRequest** request) { + NS_ASSERTION(request, "CreateRequest: request is null"); + + nsAutoCString key(*session->ClientID()); + key.Append(':'); + key.Append(clientKey); + + if (mMaxKeyLength < key.Length()) mMaxKeyLength = key.Length(); + + // create request + *request = + new nsCacheRequest(key, listener, accessRequested, blockingMode, session); + + if (!listener) return NS_OK; // we're sync, we're done. + + // get the request's thread + (*request)->mEventTarget = GetCurrentEventTarget(); + + return NS_OK; +} + +class nsCacheListenerEvent : public Runnable { + public: + nsCacheListenerEvent(nsICacheListener* listener, + nsICacheEntryDescriptor* descriptor, + nsCacheAccessMode accessGranted, nsresult status) + : mozilla::Runnable("nsCacheListenerEvent"), + mListener(listener) // transfers reference + , + mDescriptor(descriptor) // transfers reference (may be null) + , + mAccessGranted(accessGranted), + mStatus(status) {} + + NS_IMETHOD Run() override { + mListener->OnCacheEntryAvailable(mDescriptor, mAccessGranted, mStatus); + + NS_RELEASE(mListener); + NS_IF_RELEASE(mDescriptor); + return NS_OK; + } + + private: + // We explicitly leak mListener or mDescriptor if Run is not called + // because otherwise we cannot guarantee that they are destroyed on + // the right thread. + + nsICacheListener* mListener; + nsICacheEntryDescriptor* mDescriptor; + nsCacheAccessMode mAccessGranted; + nsresult mStatus; +}; + +nsresult nsCacheService::NotifyListener(nsCacheRequest* request, + nsICacheEntryDescriptor* descriptor, + nsCacheAccessMode accessGranted, + nsresult status) { + NS_ASSERTION(request->mEventTarget, "no thread set in async request!"); + + // Swap ownership, and release listener on target thread... + nsICacheListener* listener = request->mListener; + request->mListener = nullptr; + + nsCOMPtr<nsIRunnable> ev = + new nsCacheListenerEvent(listener, descriptor, accessGranted, status); + if (!ev) { + // Better to leak listener and descriptor if we fail because we don't + // want to destroy them inside the cache service lock or on potentially + // the wrong thread. + return NS_ERROR_OUT_OF_MEMORY; + } + + return request->mEventTarget->Dispatch(ev, NS_DISPATCH_NORMAL); +} + +nsresult nsCacheService::ProcessRequest(nsCacheRequest* request, + bool calledFromOpenCacheEntry, + nsICacheEntryDescriptor** result) { + // !!! must be called with mLock held !!! + nsresult rv; + nsCacheEntry* entry = nullptr; + nsCacheEntry* doomedEntry = nullptr; + nsCacheAccessMode accessGranted = nsICache::ACCESS_NONE; + if (result) *result = nullptr; + + while (true) { // Activate entry loop + rv = ActivateEntry(request, &entry, + &doomedEntry); // get the entry for this request + if (NS_FAILED(rv)) break; + + while (true) { // Request Access loop + NS_ASSERTION(entry, "no entry in Request Access loop!"); + // entry->RequestAccess queues request on entry + rv = entry->RequestAccess(request, &accessGranted); + if (rv != NS_ERROR_CACHE_WAIT_FOR_VALIDATION) break; + + if (request->IsBlocking()) { + if (request->mListener) { + // async exits - validate, doom, or close will resume + return rv; + } + + // XXX this is probably wrong... + Unlock(); + rv = request->WaitForValidation(); + Lock(LOCK_TELEM(NSCACHESERVICE_PROCESSREQUEST)); + } + + PR_REMOVE_AND_INIT_LINK(request); + if (NS_FAILED(rv)) + break; // non-blocking mode returns WAIT_FOR_VALIDATION error + // okay, we're ready to process this request, request access again + } + if (rv != NS_ERROR_CACHE_ENTRY_DOOMED) break; + + if (entry->IsNotInUse()) { + // this request was the last one keeping it around, so get rid of it + DeactivateEntry(entry); + } + // loop back around to look for another entry + } + + if (NS_SUCCEEDED(rv) && request->mProfileDir) { + // Custom cache directory has been demanded. Preset the cache device. + if (entry->StoragePolicy() != nsICache::STORE_OFFLINE) { + // Failsafe check: this is implemented only for offline cache atm. + rv = NS_ERROR_FAILURE; + } else { + RefPtr<nsOfflineCacheDevice> customCacheDevice; + rv = GetCustomOfflineDevice(request->mProfileDir, -1, + getter_AddRefs(customCacheDevice)); + if (NS_SUCCEEDED(rv)) entry->SetCustomCacheDevice(customCacheDevice); + } + } + + nsICacheEntryDescriptor* descriptor = nullptr; + + if (NS_SUCCEEDED(rv)) + rv = entry->CreateDescriptor(request, accessGranted, &descriptor); + + // If doomedEntry is set, ActivatEntry() doomed an existing entry and + // created a new one for that cache-key. However, any pending requests + // on the doomed entry were not processed and we need to do that here. + // This must be done after adding the created entry to list of active + // entries (which is done in ActivateEntry()) otherwise the hashkeys crash + // (see bug ##561313). It is also important to do this after creating a + // descriptor for this request, or some other request may end up being + // executed first for the newly created entry. + // Finally, it is worth to emphasize that if doomedEntry is set, + // ActivateEntry() created a new entry for the request, which will be + // initialized by RequestAccess() and they both should have returned NS_OK. + if (doomedEntry) { + (void)ProcessPendingRequests(doomedEntry); + if (doomedEntry->IsNotInUse()) DeactivateEntry(doomedEntry); + doomedEntry = nullptr; + } + + if (request->mListener) { // Asynchronous + + if (NS_FAILED(rv) && calledFromOpenCacheEntry && request->IsBlocking()) + return rv; // skip notifying listener, just return rv to caller + + // call listener to report error or descriptor + nsresult rv2 = NotifyListener(request, descriptor, accessGranted, rv); + if (NS_FAILED(rv2) && NS_SUCCEEDED(rv)) { + rv = rv2; // trigger delete request + } + } else { // Synchronous + *result = descriptor; + } + return rv; +} + +nsresult nsCacheService::OpenCacheEntry(nsCacheSession* session, + const nsACString& key, + nsCacheAccessMode accessRequested, + bool blockingMode, + nsICacheListener* listener, + nsICacheEntryDescriptor** result) { + CACHE_LOG_DEBUG( + ("Opening entry for session %p, key %s, mode %d, blocking %d\n", session, + PromiseFlatCString(key).get(), accessRequested, blockingMode)); + if (result) *result = nullptr; + + if (!gService || !gService->mInitialized) return NS_ERROR_NOT_INITIALIZED; + + nsCacheRequest* request = nullptr; + + nsresult rv = gService->CreateRequest(session, key, accessRequested, + blockingMode, listener, &request); + if (NS_FAILED(rv)) return rv; + + CACHE_LOG_DEBUG(("Created request %p\n", request)); + + // Process the request on the background thread if we are on the main thread + // and the the request is asynchronous + if (NS_IsMainThread() && listener && gService->mCacheIOThread) { + nsCOMPtr<nsIRunnable> ev = new nsProcessRequestEvent(request); + rv = DispatchToCacheIOThread(ev); + + // delete request if we didn't post the event + if (NS_FAILED(rv)) delete request; + } else { + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_OPENCACHEENTRY)); + rv = gService->ProcessRequest(request, true, result); + + // delete requests that have completed + if (!(listener && blockingMode && + (rv == NS_ERROR_CACHE_WAIT_FOR_VALIDATION))) + delete request; + } + + return rv; +} + +nsresult nsCacheService::ActivateEntry(nsCacheRequest* request, + nsCacheEntry** result, + nsCacheEntry** doomedEntry) { + CACHE_LOG_DEBUG(("Activate entry for request %p\n", request)); + if (!mInitialized || mClearingEntries) return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = NS_OK; + + NS_ASSERTION(request != nullptr, "ActivateEntry called with no request"); + if (result) *result = nullptr; + if (doomedEntry) *doomedEntry = nullptr; + if ((!request) || (!result) || (!doomedEntry)) return NS_ERROR_NULL_POINTER; + + // check if the request can be satisfied + if (!request->IsStreamBased()) return NS_ERROR_FAILURE; + if (!IsStorageEnabledForPolicy_Locked(request->StoragePolicy())) + return NS_ERROR_FAILURE; + + // search active entries (including those not bound to device) + nsCacheEntry* entry = mActiveEntries.GetEntry(&(request->mKey)); + CACHE_LOG_DEBUG(("Active entry for request %p is %p\n", request, entry)); + + if (!entry) { + // search cache devices for entry + bool collision = false; + entry = SearchCacheDevices(&(request->mKey), request->StoragePolicy(), + &collision); + CACHE_LOG_DEBUG( + ("Device search for request %p returned %p\n", request, entry)); + // When there is a hashkey collision just refuse to cache it... + if (collision) return NS_ERROR_CACHE_IN_USE; + + if (entry) entry->MarkInitialized(); + } else { + NS_ASSERTION(entry->IsActive(), "Inactive entry found in mActiveEntries!"); + } + + if (entry) { + ++mCacheHits; + entry->Fetched(); + } else { + ++mCacheMisses; + } + + if (entry && ((request->AccessRequested() == nsICache::ACCESS_WRITE) || + ((request->StoragePolicy() != nsICache::STORE_OFFLINE) && + (entry->mExpirationTime <= SecondsFromPRTime(PR_Now()) && + request->WillDoomEntriesIfExpired())))) + + { + // this is FORCE-WRITE request or the entry has expired + // we doom entry without processing pending requests, but store it in + // doomedEntry which causes pending requests to be processed below + rv = DoomEntry_Internal(entry, false); + *doomedEntry = entry; + if (NS_FAILED(rv)) { + // XXX what to do? Increment FailedDooms counter? + } + entry = nullptr; + } + + if (!entry) { + if (!(request->AccessRequested() & nsICache::ACCESS_WRITE)) { + // this is a READ-ONLY request + rv = NS_ERROR_CACHE_KEY_NOT_FOUND; + goto error; + } + + entry = new nsCacheEntry(request->mKey, request->IsStreamBased(), + request->StoragePolicy()); + if (!entry) return NS_ERROR_OUT_OF_MEMORY; + + if (request->IsPrivate()) entry->MarkPrivate(); + + entry->Fetched(); + ++mTotalEntries; + + // XXX we could perform an early bind in some cases based on storage policy + } + + if (!entry->IsActive()) { + rv = mActiveEntries.AddEntry(entry); + if (NS_FAILED(rv)) goto error; + CACHE_LOG_DEBUG(("Added entry %p to mActiveEntries\n", entry)); + entry->MarkActive(); // mark entry active, because it's now in + // mActiveEntries + } + *result = entry; + return NS_OK; + +error: + *result = nullptr; + delete entry; + return rv; +} + +nsCacheEntry* nsCacheService::SearchCacheDevices(nsCString* key, + nsCacheStoragePolicy policy, + bool* collision) { + Telemetry::AutoTimer<Telemetry::CACHE_DEVICE_SEARCH_2> timer; + nsCacheEntry* entry = nullptr; + + *collision = false; + if (policy == nsICache::STORE_OFFLINE || + (policy == nsICache::STORE_ANYWHERE && gIOService->IsOffline())) { + if (mEnableOfflineDevice) { + if (!mOfflineDevice) { + nsresult rv = CreateOfflineDevice(); + if (NS_FAILED(rv)) return nullptr; + } + + entry = mOfflineDevice->FindEntry(key, collision); + } + } + + return entry; +} + +nsCacheDevice* nsCacheService::EnsureEntryHasDevice(nsCacheEntry* entry) { + nsCacheDevice* device = entry->CacheDevice(); + // return device if found, possibly null if the entry is doomed i.e prevent + // doomed entries to bind to a device (see e.g. bugs #548406 and #596443) + if (device || entry->IsDoomed()) return device; + + if (!device && entry->IsStreamData() && entry->IsAllowedOffline() && + mEnableOfflineDevice) { + if (!mOfflineDevice) { + (void)CreateOfflineDevice(); // ignore the error (check for + // mOfflineDevice instead) + } + + device = entry->CustomCacheDevice() ? entry->CustomCacheDevice() + : mOfflineDevice; + + if (device) { + entry->MarkBinding(); + nsresult rv = device->BindEntry(entry); + entry->ClearBinding(); + if (NS_FAILED(rv)) device = nullptr; + } + } + + if (device) entry->SetCacheDevice(device); + return device; +} + +nsresult nsCacheService::DoomEntry(nsCacheEntry* entry) { + return gService->DoomEntry_Internal(entry, true); +} + +nsresult nsCacheService::DoomEntry_Internal(nsCacheEntry* entry, + bool doProcessPendingRequests) { + if (entry->IsDoomed()) return NS_OK; + + CACHE_LOG_DEBUG(("Dooming entry %p\n", entry)); + nsresult rv = NS_OK; + entry->MarkDoomed(); + + NS_ASSERTION(!entry->IsBinding(), "Dooming entry while binding device."); + nsCacheDevice* device = entry->CacheDevice(); + if (device) device->DoomEntry(entry); + + if (entry->IsActive()) { + // remove from active entries + mActiveEntries.RemoveEntry(entry); + CACHE_LOG_DEBUG(("Removed entry %p from mActiveEntries\n", entry)); + entry->MarkInactive(); + } + + // put on doom list to wait for descriptors to close + NS_ASSERTION(PR_CLIST_IS_EMPTY(entry), "doomed entry still on device list"); + PR_APPEND_LINK(entry, &mDoomedEntries); + + // handle pending requests only if we're supposed to + if (doProcessPendingRequests) { + // tell pending requests to get on with their lives... + rv = ProcessPendingRequests(entry); + + // All requests have been removed, but there may still be open descriptors + if (entry->IsNotInUse()) { + DeactivateEntry(entry); // tell device to get rid of it + } + } + return rv; +} + +void nsCacheService::OnProfileShutdown() { + if (!gService || !gService->mInitialized) { + // The cache service has been shut down, but someone is still holding + // a reference to it. Ignore this call. + return; + } + + { + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_ONPROFILESHUTDOWN)); + gService->mClearingEntries = true; + gService->DoomActiveEntries(nullptr); + } + + gService->CloseAllStreams(); + + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_ONPROFILESHUTDOWN)); + gService->ClearDoomList(); + + // Make sure to wait for any pending cache-operations before + // proceeding with destructive actions (bug #620660) + (void)SyncWithCacheIOThread(); + + if (gService->mOfflineDevice && gService->mEnableOfflineDevice) { + gService->mOfflineDevice->Shutdown(); + } + for (auto iter = gService->mCustomOfflineDevices.Iter(); !iter.Done(); + iter.Next()) { + iter.Data()->Shutdown(); + iter.Remove(); + } + + gService->mEnableOfflineDevice = false; + + gService->mClearingEntries = false; +} + +void nsCacheService::OnProfileChanged() { + if (!gService) return; + + CACHE_LOG_DEBUG(("nsCacheService::OnProfileChanged")); + + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_ONPROFILECHANGED)); + + gService->mEnableOfflineDevice = gService->mObserver->OfflineCacheEnabled(); + + if (gService->mOfflineDevice) { + gService->mOfflineDevice->SetCacheParentDirectory( + gService->mObserver->OfflineCacheParentDirectory()); + gService->mOfflineDevice->SetCapacity( + gService->mObserver->OfflineCacheCapacity()); + + // XXX initialization of mOfflineDevice could be made lazily, if + // mEnableOfflineDevice is false + nsresult rv = + gService->mOfflineDevice->InitWithSqlite(gService->mStorageService); + if (NS_FAILED(rv)) { + NS_ERROR( + "nsCacheService::OnProfileChanged: Re-initializing offline device " + "failed"); + gService->mEnableOfflineDevice = false; + // XXX delete mOfflineDevice? + } + } +} + +void nsCacheService::SetOfflineCacheEnabled(bool enabled) { + if (!gService) return; + nsCacheServiceAutoLock lock( + LOCK_TELEM(NSCACHESERVICE_SETOFFLINECACHEENABLED)); + gService->mEnableOfflineDevice = enabled; +} + +void nsCacheService::SetOfflineCacheCapacity(int32_t capacity) { + if (!gService) return; + nsCacheServiceAutoLock lock( + LOCK_TELEM(NSCACHESERVICE_SETOFFLINECACHECAPACITY)); + + if (gService->mOfflineDevice) { + gService->mOfflineDevice->SetCapacity(capacity); + } + + gService->mEnableOfflineDevice = gService->mObserver->OfflineCacheEnabled(); +} + +/****************************************************************************** + * static methods for nsCacheEntryDescriptor + *****************************************************************************/ +void nsCacheService::CloseDescriptor(nsCacheEntryDescriptor* descriptor) { + // ask entry to remove descriptor + nsCacheEntry* entry = descriptor->CacheEntry(); + bool doomEntry; + bool stillActive = entry->RemoveDescriptor(descriptor, &doomEntry); + + if (!entry->IsValid()) { + gService->ProcessPendingRequests(entry); + } + + if (doomEntry) { + gService->DoomEntry_Internal(entry, true); + return; + } + + if (!stillActive) { + gService->DeactivateEntry(entry); + } +} + +nsresult nsCacheService::GetFileForEntry(nsCacheEntry* entry, + nsIFile** result) { + nsCacheDevice* device = gService->EnsureEntryHasDevice(entry); + if (!device) return NS_ERROR_UNEXPECTED; + + return device->GetFileForEntry(entry, result); +} + +nsresult nsCacheService::OpenInputStreamForEntry(nsCacheEntry* entry, + nsCacheAccessMode mode, + uint32_t offset, + nsIInputStream** result) { + nsCacheDevice* device = gService->EnsureEntryHasDevice(entry); + if (!device) return NS_ERROR_UNEXPECTED; + + return device->OpenInputStreamForEntry(entry, mode, offset, result); +} + +nsresult nsCacheService::OpenOutputStreamForEntry(nsCacheEntry* entry, + nsCacheAccessMode mode, + uint32_t offset, + nsIOutputStream** result) { + nsCacheDevice* device = gService->EnsureEntryHasDevice(entry); + if (!device) return NS_ERROR_UNEXPECTED; + + return device->OpenOutputStreamForEntry(entry, mode, offset, result); +} + +nsresult nsCacheService::OnDataSizeChange(nsCacheEntry* entry, + int32_t deltaSize) { + nsCacheDevice* device = gService->EnsureEntryHasDevice(entry); + if (!device) return NS_ERROR_UNEXPECTED; + + return device->OnDataSizeChange(entry, deltaSize); +} + +void nsCacheService::LockAcquired() { + MutexAutoLock lock(mTimeStampLock); + mLockAcquiredTimeStamp = TimeStamp::Now(); +} + +void nsCacheService::LockReleased() { + MutexAutoLock lock(mTimeStampLock); + mLockAcquiredTimeStamp = TimeStamp(); +} + +void nsCacheService::Lock() { + gService->mLock.Lock(); + gService->LockAcquired(); +} + +void nsCacheService::Lock(mozilla::Telemetry::HistogramID mainThreadLockerID) { + mozilla::Telemetry::HistogramID lockerID; + mozilla::Telemetry::HistogramID generalID; + + if (NS_IsMainThread()) { + lockerID = mainThreadLockerID; + generalID = mozilla::Telemetry::CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_2; + } else { + lockerID = mozilla::Telemetry::HistogramCount; + generalID = mozilla::Telemetry::CACHE_SERVICE_LOCK_WAIT_2; + } + + TimeStamp start(TimeStamp::Now()); + + nsCacheService::Lock(); + + TimeStamp stop(TimeStamp::Now()); + + // Telemetry isn't thread safe on its own, but this is OK because we're + // protecting it with the cache lock. + if (lockerID != mozilla::Telemetry::HistogramCount) { + mozilla::Telemetry::AccumulateTimeDelta(lockerID, start, stop); + } + mozilla::Telemetry::AccumulateTimeDelta(generalID, start, stop); +} + +void nsCacheService::Unlock() { + gService->mLock.AssertCurrentThreadOwns(); + + nsTArray<nsISupports*> doomed = std::move(gService->mDoomedObjects); + + gService->LockReleased(); + gService->mLock.Unlock(); + + for (uint32_t i = 0; i < doomed.Length(); ++i) doomed[i]->Release(); +} + +void nsCacheService::ReleaseObject_Locked(nsISupports* obj, + nsIEventTarget* target) { + gService->mLock.AssertCurrentThreadOwns(); + + bool isCur; + if (!target || (NS_SUCCEEDED(target->IsOnCurrentThread(&isCur)) && isCur)) { + gService->mDoomedObjects.AppendElement(obj); + } else { + NS_ProxyRelease("nsCacheService::ReleaseObject_Locked::obj", target, + dont_AddRef(obj)); + } +} + +nsresult nsCacheService::SetCacheElement(nsCacheEntry* entry, + nsISupports* element) { + entry->SetData(element); + entry->TouchData(); + return NS_OK; +} + +nsresult nsCacheService::ValidateEntry(nsCacheEntry* entry) { + nsCacheDevice* device = gService->EnsureEntryHasDevice(entry); + if (!device) return NS_ERROR_UNEXPECTED; + + entry->MarkValid(); + nsresult rv = gService->ProcessPendingRequests(entry); + NS_ASSERTION(rv == NS_OK, "ProcessPendingRequests failed."); + // XXX what else should be done? + + return rv; +} + +int32_t nsCacheService::CacheCompressionLevel() { + int32_t level = gService->mObserver->CacheCompressionLevel(); + return level; +} + +void nsCacheService::DeactivateEntry(nsCacheEntry* entry) { + CACHE_LOG_DEBUG(("Deactivating entry %p\n", entry)); + nsresult rv = NS_OK; + NS_ASSERTION(entry->IsNotInUse(), "### deactivating an entry while in use!"); + nsCacheDevice* device = nullptr; + + if (mMaxDataSize < entry->DataSize()) mMaxDataSize = entry->DataSize(); + if (mMaxMetaSize < entry->MetaDataSize()) + mMaxMetaSize = entry->MetaDataSize(); + + if (entry->IsDoomed()) { + // remove from Doomed list + PR_REMOVE_AND_INIT_LINK(entry); + } else if (entry->IsActive()) { + // remove from active entries + mActiveEntries.RemoveEntry(entry); + CACHE_LOG_DEBUG( + ("Removed deactivated entry %p from mActiveEntries\n", entry)); + entry->MarkInactive(); + + // bind entry if necessary to store meta-data + device = EnsureEntryHasDevice(entry); + if (!device) { + CACHE_LOG_DEBUG( + ("DeactivateEntry: unable to bind active " + "entry %p\n", + entry)); + NS_WARNING("DeactivateEntry: unable to bind active entry\n"); + return; + } + } else { + // if mInitialized == false, + // then we're shutting down and this state is okay. + NS_ASSERTION(!mInitialized, "DeactivateEntry: bad cache entry state."); + } + + device = entry->CacheDevice(); + if (device) { + rv = device->DeactivateEntry(entry); + if (NS_FAILED(rv)) { + // increment deactivate failure count + ++mDeactivateFailures; + } + } else { + // increment deactivating unbound entry statistic + ++mDeactivatedUnboundEntries; + delete entry; // because no one else will + } +} + +nsresult nsCacheService::ProcessPendingRequests(nsCacheEntry* entry) { + nsresult rv = NS_OK; + nsCacheRequest* request = (nsCacheRequest*)PR_LIST_HEAD(&entry->mRequestQ); + nsCacheRequest* nextRequest; + bool newWriter = false; + + CACHE_LOG_DEBUG(( + "ProcessPendingRequests for %sinitialized %s %salid entry %p\n", + (entry->IsInitialized() ? "" : "Un"), (entry->IsDoomed() ? "DOOMED" : ""), + (entry->IsValid() ? "V" : "Inv"), entry)); + + if (request == &entry->mRequestQ) return NS_OK; // no queued requests + + if (!entry->IsDoomed() && entry->IsInvalid()) { + // 1st descriptor closed w/o MarkValid() + NS_ASSERTION(PR_CLIST_IS_EMPTY(&entry->mDescriptorQ), + "shouldn't be here with open descriptors"); + +#if DEBUG + // verify no ACCESS_WRITE requests(shouldn't have any of these) + while (request != &entry->mRequestQ) { + NS_ASSERTION(request->AccessRequested() != nsICache::ACCESS_WRITE, + "ACCESS_WRITE request should have been given a new entry"); + request = (nsCacheRequest*)PR_NEXT_LINK(request); + } + request = (nsCacheRequest*)PR_LIST_HEAD(&entry->mRequestQ); +#endif + // find first request with ACCESS_READ_WRITE (if any) and promote it to 1st + // writer + while (request != &entry->mRequestQ) { + if (request->AccessRequested() == nsICache::ACCESS_READ_WRITE) { + newWriter = true; + CACHE_LOG_DEBUG((" promoting request %p to 1st writer\n", request)); + break; + } + + request = (nsCacheRequest*)PR_NEXT_LINK(request); + } + + if (request == &entry->mRequestQ) // no requests asked for + // ACCESS_READ_WRITE, back to top + request = (nsCacheRequest*)PR_LIST_HEAD(&entry->mRequestQ); + + // XXX what should we do if there are only READ requests in queue? + // XXX serialize their accesses, give them only read access, but force them + // to check validate flag? + // XXX or do readers simply presume the entry is valid + // See fix for bug #467392 below + } + + nsCacheAccessMode accessGranted = nsICache::ACCESS_NONE; + + while (request != &entry->mRequestQ) { + nextRequest = (nsCacheRequest*)PR_NEXT_LINK(request); + CACHE_LOG_DEBUG((" %sync request %p for %p\n", + (request->mListener ? "As" : "S"), request, entry)); + + if (request->mListener) { + // Async request + PR_REMOVE_AND_INIT_LINK(request); + + if (entry->IsDoomed()) { + rv = ProcessRequest(request, false, nullptr); + if (rv == NS_ERROR_CACHE_WAIT_FOR_VALIDATION) + rv = NS_OK; + else + delete request; + + if (NS_FAILED(rv)) { + // XXX what to do? + } + } else if (entry->IsValid() || newWriter) { + rv = entry->RequestAccess(request, &accessGranted); + NS_ASSERTION(NS_SUCCEEDED(rv), + "if entry is valid, RequestAccess must succeed."); + // XXX if (newWriter) { + // NS_ASSERTION( accessGranted == + // request->AccessRequested(), "why not?"); + // } + + // entry->CreateDescriptor dequeues request, and queues descriptor + nsICacheEntryDescriptor* descriptor = nullptr; + rv = entry->CreateDescriptor(request, accessGranted, &descriptor); + + // post call to listener to report error or descriptor + rv = NotifyListener(request, descriptor, accessGranted, rv); + delete request; + if (NS_FAILED(rv)) { + // XXX what to do? + } + + } else { + // read-only request to an invalid entry - need to wait for + // the entry to become valid so we post an event to process + // the request again later (bug #467392) + nsCOMPtr<nsIRunnable> ev = new nsProcessRequestEvent(request); + rv = DispatchToCacheIOThread(ev); + if (NS_FAILED(rv)) { + delete request; // avoid leak + } + } + } else { + // Synchronous request + request->WakeUp(); + } + if (newWriter) break; // process remaining requests after validation + request = nextRequest; + } + + return NS_OK; +} + +bool nsCacheService::IsDoomListEmpty() { + nsCacheEntry* entry = (nsCacheEntry*)PR_LIST_HEAD(&mDoomedEntries); + return &mDoomedEntries == entry; +} + +void nsCacheService::ClearDoomList() { + nsCacheEntry* entry = (nsCacheEntry*)PR_LIST_HEAD(&mDoomedEntries); + + while (entry != &mDoomedEntries) { + nsCacheEntry* next = (nsCacheEntry*)PR_NEXT_LINK(entry); + + entry->DetachDescriptors(); + DeactivateEntry(entry); + entry = next; + } +} + +void nsCacheService::DoomActiveEntries(DoomCheckFn check) { + AutoTArray<nsCacheEntry*, 8> array; + + for (auto iter = mActiveEntries.Iter(); !iter.Done(); iter.Next()) { + nsCacheEntry* entry = + static_cast<nsCacheEntryHashTableEntry*>(iter.Get())->cacheEntry; + + if (check && !check(entry)) { + continue; + } + + array.AppendElement(entry); + + // entry is being removed from the active entry list + entry->MarkInactive(); + iter.Remove(); + } + + uint32_t count = array.Length(); + for (uint32_t i = 0; i < count; ++i) { + DoomEntry_Internal(array[i], true); + } +} + +void nsCacheService::CloseAllStreams() { + nsTArray<RefPtr<nsCacheEntryDescriptor::nsInputStreamWrapper> > inputs; + nsTArray<RefPtr<nsCacheEntryDescriptor::nsOutputStreamWrapper> > outputs; + + { + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_CLOSEALLSTREAMS)); + + nsTArray<nsCacheEntry*> entries; + +#if DEBUG + // make sure there is no active entry + for (auto iter = mActiveEntries.Iter(); !iter.Done(); iter.Next()) { + auto entry = static_cast<nsCacheEntryHashTableEntry*>(iter.Get()); + entries.AppendElement(entry->cacheEntry); + } + NS_ASSERTION(entries.IsEmpty(), "Bad state"); +#endif + + // Get doomed entries + nsCacheEntry* entry = (nsCacheEntry*)PR_LIST_HEAD(&mDoomedEntries); + while (entry != &mDoomedEntries) { + nsCacheEntry* next = (nsCacheEntry*)PR_NEXT_LINK(entry); + entries.AppendElement(entry); + entry = next; + } + + // Iterate through all entries and collect input and output streams + for (size_t i = 0; i < entries.Length(); i++) { + entry = entries.ElementAt(i); + + nsTArray<RefPtr<nsCacheEntryDescriptor> > descs; + entry->GetDescriptors(descs); + + for (uint32_t j = 0; j < descs.Length(); j++) { + if (descs[j]->mOutputWrapper) + outputs.AppendElement(descs[j]->mOutputWrapper); + + for (size_t k = 0; k < descs[j]->mInputWrappers.Length(); k++) + inputs.AppendElement(descs[j]->mInputWrappers[k]); + } + } + } + + uint32_t i; + for (i = 0; i < inputs.Length(); i++) inputs[i]->Close(); + + for (i = 0; i < outputs.Length(); i++) outputs[i]->Close(); +} + +bool nsCacheService::GetClearingEntries() { + AssertOwnsLock(); + return gService->mClearingEntries; +} + +// static +void nsCacheService::GetCacheBaseDirectoty(nsIFile** result) { + *result = nullptr; + if (!gService || !gService->mObserver) return; + + nsCOMPtr<nsIFile> directory = gService->mObserver->DiskCacheParentDirectory(); + if (!directory) return; + + directory->Clone(result); +} + +// static +void nsCacheService::GetDiskCacheDirectory(nsIFile** result) { + nsCOMPtr<nsIFile> directory; + GetCacheBaseDirectoty(getter_AddRefs(directory)); + if (!directory) return; + + nsresult rv = directory->AppendNative("Cache"_ns); + if (NS_FAILED(rv)) return; + + directory.forget(result); +} + +// static +void nsCacheService::GetAppCacheDirectory(nsIFile** result) { + nsCOMPtr<nsIFile> directory; + GetCacheBaseDirectoty(getter_AddRefs(directory)); + if (!directory) return; + + nsresult rv = directory->AppendNative("OfflineCache"_ns); + if (NS_FAILED(rv)) return; + + directory.forget(result); +} + +void nsCacheService::LogCacheStatistics() { + uint32_t hitPercentage = 0; + double sum = (double)(mCacheHits + mCacheMisses); + if (sum != 0) { + hitPercentage = (uint32_t)((((double)mCacheHits) / sum) * 100); + } + CACHE_LOG_INFO(("\nCache Service Statistics:\n\n")); + CACHE_LOG_INFO((" TotalEntries = %d\n", mTotalEntries)); + CACHE_LOG_INFO((" Cache Hits = %d\n", mCacheHits)); + CACHE_LOG_INFO((" Cache Misses = %d\n", mCacheMisses)); + CACHE_LOG_INFO((" Cache Hit %% = %d%%\n", hitPercentage)); + CACHE_LOG_INFO((" Max Key Length = %d\n", mMaxKeyLength)); + CACHE_LOG_INFO((" Max Meta Size = %d\n", mMaxMetaSize)); + CACHE_LOG_INFO((" Max Data Size = %d\n", mMaxDataSize)); + CACHE_LOG_INFO(("\n")); + CACHE_LOG_INFO( + (" Deactivate Failures = %d\n", mDeactivateFailures)); + CACHE_LOG_INFO( + (" Deactivated Unbound Entries = %d\n", mDeactivatedUnboundEntries)); +} + +void nsCacheService::MoveOrRemoveDiskCache(nsIFile* aOldCacheDir, + nsIFile* aNewCacheDir, + const char* aCacheSubdir) { + bool same; + if (NS_FAILED(aOldCacheDir->Equals(aNewCacheDir, &same)) || same) return; + + nsCOMPtr<nsIFile> aOldCacheSubdir; + aOldCacheDir->Clone(getter_AddRefs(aOldCacheSubdir)); + + nsresult rv = aOldCacheSubdir->AppendNative(nsDependentCString(aCacheSubdir)); + if (NS_FAILED(rv)) return; + + bool exists; + if (NS_FAILED(aOldCacheSubdir->Exists(&exists)) || !exists) return; + + nsCOMPtr<nsIFile> aNewCacheSubdir; + aNewCacheDir->Clone(getter_AddRefs(aNewCacheSubdir)); + + rv = aNewCacheSubdir->AppendNative(nsDependentCString(aCacheSubdir)); + if (NS_FAILED(rv)) return; + + PathString newPath = aNewCacheSubdir->NativePath(); + + if (NS_SUCCEEDED(aNewCacheSubdir->Exists(&exists)) && !exists) { + // New cache directory does not exist, try to move the old one here + // rename needs an empty target directory + + // Make sure the parent of the target sub-dir exists + rv = aNewCacheDir->Create(nsIFile::DIRECTORY_TYPE, 0777); + if (NS_SUCCEEDED(rv) || NS_ERROR_FILE_ALREADY_EXISTS == rv) { + PathString oldPath = aOldCacheSubdir->NativePath(); +#ifdef XP_WIN + if (MoveFileW(oldPath.get(), newPath.get())) +#else + if (rename(oldPath.get(), newPath.get()) == 0) +#endif + { + return; + } + } + } + + // Delay delete by 1 minute to avoid IO thrash on startup. + nsDeleteDir::DeleteDir(aOldCacheSubdir, false, 60000); +} + +static bool IsEntryPrivate(nsCacheEntry* entry) { return entry->IsPrivate(); } + +void nsCacheService::LeavePrivateBrowsing() { + nsCacheServiceAutoLock lock; + + gService->DoomActiveEntries(IsEntryPrivate); +} |