diff options
Diffstat (limited to 'netwerk/cache/nsCacheService.cpp')
-rw-r--r-- | netwerk/cache/nsCacheService.cpp | 3209 |
1 files changed, 3209 insertions, 0 deletions
diff --git a/netwerk/cache/nsCacheService.cpp b/netwerk/cache/nsCacheService.cpp new file mode 100644 index 000000000..bab67e104 --- /dev/null +++ b/netwerk/cache/nsCacheService.cpp @@ -0,0 +1,3209 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 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 "mozilla/ArrayUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/Assertions.h" +#include "mozilla/DebugOnly.h" + +#include "necko-config.h" + +#include "nsCache.h" +#include "nsCacheService.h" +#include "nsCacheRequest.h" +#include "nsCacheEntry.h" +#include "nsCacheEntryDescriptor.h" +#include "nsCacheDevice.h" +#include "nsMemoryCacheDevice.h" +#include "nsICacheVisitor.h" +#include "nsDiskCacheDevice.h" +#include "nsDiskCacheDeviceSQL.h" +#include "nsCacheUtils.h" +#include "../cache2/CacheObserver.h" + +#include "nsIObserverService.h" +#include "nsIPrefService.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 "nsITimer.h" +#include "mozIStorageService.h" + +#include "mozilla/net/NeckoCommon.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::net; + +/****************************************************************************** + * nsCacheProfilePrefObserver + *****************************************************************************/ +#define DISK_CACHE_ENABLE_PREF "browser.cache.disk.enable" +#define DISK_CACHE_DIR_PREF "browser.cache.disk.parent_directory" +#define DISK_CACHE_SMART_SIZE_FIRST_RUN_PREF\ + "browser.cache.disk.smart_size.first_run" +#define DISK_CACHE_SMART_SIZE_ENABLED_PREF \ + "browser.cache.disk.smart_size.enabled" +#define DISK_CACHE_SMART_SIZE_PREF "browser.cache.disk.smart_size_cached_value" +#define DISK_CACHE_CAPACITY_PREF "browser.cache.disk.capacity" +#define DISK_CACHE_MAX_ENTRY_SIZE_PREF "browser.cache.disk.max_entry_size" +#define DISK_CACHE_CAPACITY 256000 + +#define DISK_CACHE_USE_OLD_MAX_SMART_SIZE_PREF \ + "browser.cache.disk.smart_size.use_old_max" + +#define OFFLINE_CACHE_ENABLE_PREF "browser.cache.offline.enable" +#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 MEMORY_CACHE_ENABLE_PREF "browser.cache.memory.enable" +#define MEMORY_CACHE_CAPACITY_PREF "browser.cache.memory.capacity" +#define MEMORY_CACHE_MAX_ENTRY_SIZE_PREF "browser.cache.memory.max_entry_size" + +#define CACHE_COMPRESSION_LEVEL_PREF "browser.cache.compression_level" +#define CACHE_COMPRESSION_LEVEL 1 + +#define SANITIZE_ON_SHUTDOWN_PREF "privacy.sanitize.sanitizeOnShutdown" +#define CLEAR_ON_SHUTDOWN_PREF "privacy.clearOnShutdown.cache" + +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" +}; + +static const char * prefList[] = { + DISK_CACHE_ENABLE_PREF, + DISK_CACHE_SMART_SIZE_ENABLED_PREF, + DISK_CACHE_CAPACITY_PREF, + DISK_CACHE_DIR_PREF, + DISK_CACHE_MAX_ENTRY_SIZE_PREF, + DISK_CACHE_USE_OLD_MAX_SMART_SIZE_PREF, + OFFLINE_CACHE_ENABLE_PREF, + OFFLINE_CACHE_CAPACITY_PREF, + OFFLINE_CACHE_DIR_PREF, + MEMORY_CACHE_ENABLE_PREF, + MEMORY_CACHE_CAPACITY_PREF, + MEMORY_CACHE_MAX_ENTRY_SIZE_PREF, + CACHE_COMPRESSION_LEVEL_PREF, + SANITIZE_ON_SHUTDOWN_PREF, + CLEAR_ON_SHUTDOWN_PREF +}; + +// Cache sizes, in KB +const int32_t DEFAULT_CACHE_SIZE = 250 * 1024; // 250 MB +#ifdef ANDROID +const int32_t MAX_CACHE_SIZE = 200 * 1024; // 200 MB +const int32_t OLD_MAX_CACHE_SIZE = 200 * 1024; // 200 MB +#else +const int32_t MAX_CACHE_SIZE = 350 * 1024; // 350 MB +const int32_t OLD_MAX_CACHE_SIZE = 1024 * 1024; // 1 GB +#endif +// Default cache size was 50 MB for many years until FF 4: +const int32_t PRE_GECKO_2_0_DEFAULT_CACHE_SIZE = 50 * 1024; + +class nsCacheProfilePrefObserver : public nsIObserver +{ + virtual ~nsCacheProfilePrefObserver() {} + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + nsCacheProfilePrefObserver() + : mHaveProfile(false) + , mDiskCacheEnabled(false) + , mDiskCacheCapacity(0) + , mDiskCacheMaxEntrySize(-1) // -1 means "no limit" + , mSmartSizeEnabled(false) + , mShouldUseOldMaxSmartSize(false) + , mOfflineCacheEnabled(false) + , mOfflineCacheCapacity(0) + , mMemoryCacheEnabled(true) + , mMemoryCacheCapacity(-1) + , mMemoryCacheMaxEntrySize(-1) // -1 means "no limit" + , mCacheCompressionLevel(CACHE_COMPRESSION_LEVEL) + , mSanitizeOnShutdown(false) + , mClearCacheOnShutdown(false) + { + } + + nsresult Install(); + void Remove(); + nsresult ReadPrefs(nsIPrefBranch* branch); + + bool DiskCacheEnabled(); + int32_t DiskCacheCapacity() { return mDiskCacheCapacity; } + void SetDiskCacheCapacity(int32_t); + int32_t DiskCacheMaxEntrySize() { return mDiskCacheMaxEntrySize; } + nsIFile * DiskCacheParentDirectory() { return mDiskCacheParentDirectory; } + bool SmartSizeEnabled() { return mSmartSizeEnabled; } + + bool ShouldUseOldMaxSmartSize() { return mShouldUseOldMaxSmartSize; } + void SetUseNewMaxSmartSize(bool useNew) { mShouldUseOldMaxSmartSize = !useNew; } + + bool OfflineCacheEnabled(); + int32_t OfflineCacheCapacity() { return mOfflineCacheCapacity; } + nsIFile * OfflineCacheParentDirectory() { return mOfflineCacheParentDirectory; } + + bool MemoryCacheEnabled(); + int32_t MemoryCacheCapacity(); + int32_t MemoryCacheMaxEntrySize() { return mMemoryCacheMaxEntrySize; } + + int32_t CacheCompressionLevel(); + + bool SanitizeAtShutdown() { return mSanitizeOnShutdown && mClearCacheOnShutdown; } + + static uint32_t GetSmartCacheSize(const nsAString& cachePath, + uint32_t currentSize, + bool shouldUseOldMaxSmartSize); + + bool PermittedToSmartSize(nsIPrefBranch*, bool firstRun); + +private: + bool mHaveProfile; + + bool mDiskCacheEnabled; + int32_t mDiskCacheCapacity; // in kilobytes + int32_t mDiskCacheMaxEntrySize; // in kilobytes + nsCOMPtr<nsIFile> mDiskCacheParentDirectory; + bool mSmartSizeEnabled; + + bool mShouldUseOldMaxSmartSize; + + bool mOfflineCacheEnabled; + int32_t mOfflineCacheCapacity; // in kilobytes + nsCOMPtr<nsIFile> mOfflineCacheParentDirectory; + + bool mMemoryCacheEnabled; + int32_t mMemoryCacheCapacity; // in kilobytes + int32_t mMemoryCacheMaxEntrySize; // in kilobytes + + int32_t mCacheCompressionLevel; + + bool mSanitizeOnShutdown; + bool mClearCacheOnShutdown; +}; + +NS_IMPL_ISUPPORTS(nsCacheProfilePrefObserver, nsIObserver) + +class nsSetDiskSmartSizeCallback final : public nsITimerCallback +{ + ~nsSetDiskSmartSizeCallback() {} + +public: + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD Notify(nsITimer* aTimer) override { + if (nsCacheService::gService) { + nsCacheServiceAutoLock autoLock(LOCK_TELEM(NSSETDISKSMARTSIZECALLBACK_NOTIFY)); + nsCacheService::gService->SetDiskSmartSize_Locked(); + nsCacheService::gService->mSmartSizeTimer = nullptr; + } + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(nsSetDiskSmartSizeCallback, nsITimerCallback) + +// Runnable sent to main thread after the cache IO thread calculates available +// disk space, so that there is no race in setting mDiskCacheCapacity. +class nsSetSmartSizeEvent: public Runnable +{ +public: + explicit nsSetSmartSizeEvent(int32_t smartSize) + : mSmartSize(smartSize) {} + + NS_IMETHOD Run() + { + NS_ASSERTION(NS_IsMainThread(), + "Setting smart size data off the main thread"); + + // Main thread may have already called nsCacheService::Shutdown + if (!nsCacheService::IsInitialized()) + return NS_ERROR_NOT_AVAILABLE; + + // Ensure smart sizing wasn't switched off while event was pending. + // It is safe to access the observer without the lock since we are + // on the main thread and the value changes only on the main thread. + if (!nsCacheService::gService->mObserver->SmartSizeEnabled()) + return NS_OK; + + nsCacheService::SetDiskCacheCapacity(mSmartSize); + + nsCOMPtr<nsIPrefBranch> ps = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!ps || + NS_FAILED(ps->SetIntPref(DISK_CACHE_SMART_SIZE_PREF, mSmartSize))) + NS_WARNING("Failed to set smart size pref"); + + return NS_OK; + } + +private: + int32_t mSmartSize; +}; + + +// Runnable sent from main thread to cacheIO thread +class nsGetSmartSizeEvent: public Runnable +{ +public: + nsGetSmartSizeEvent(const nsAString& cachePath, uint32_t currentSize, + bool shouldUseOldMaxSmartSize) + : mCachePath(cachePath) + , mCurrentSize(currentSize) + , mShouldUseOldMaxSmartSize(shouldUseOldMaxSmartSize) + {} + + // Calculates user's disk space available on a background thread and + // dispatches this value back to the main thread. + NS_IMETHOD Run() override + { + uint32_t size; + size = nsCacheProfilePrefObserver::GetSmartCacheSize(mCachePath, + mCurrentSize, + mShouldUseOldMaxSmartSize); + NS_DispatchToMainThread(new nsSetSmartSizeEvent(size)); + return NS_OK; + } + +private: + nsString mCachePath; + uint32_t mCurrentSize; + bool mShouldUseOldMaxSmartSize; +}; + +class nsBlockOnCacheThreadEvent : public Runnable { +public: + 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 (unsigned int i=0; i<ArrayLength(observerList); i++) { + rv = observerService->AddObserver(this, observerList[i], false); + if (NS_FAILED(rv)) + rv2 = rv; + } + + // install preferences observer + nsCOMPtr<nsIPrefBranch> branch = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!branch) return NS_ERROR_FAILURE; + + for (unsigned int i=0; i<ArrayLength(prefList); i++) { + rv = branch->AddObserver(prefList[i], this, false); + if (NS_FAILED(rv)) + rv2 = rv; + } + + // 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 (unsigned int i=0; i<ArrayLength(observerList); i++) { + obs->RemoveObserver(this, observerList[i]); + } + } + + // remove Pref Service observers + nsCOMPtr<nsIPrefBranch> prefs = + do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!prefs) + return; + for (unsigned int i=0; i<ArrayLength(prefList); i++) + prefs->RemoveObserver(prefList[i], this); // remove cache pref observers +} + +void +nsCacheProfilePrefObserver::SetDiskCacheCapacity(int32_t capacity) +{ + mDiskCacheCapacity = std::max(0, capacity); +} + + +NS_IMETHODIMP +nsCacheProfilePrefObserver::Observe(nsISupports * subject, + const char * topic, + const char16_t * data_unicode) +{ + nsresult rv; + 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(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, topic)) { + + // ignore pref changes until we're done switch profiles + if (!mHaveProfile) + return NS_OK; + + nsCOMPtr<nsIPrefBranch> branch = do_QueryInterface(subject, &rv); + if (NS_FAILED(rv)) + return rv; + + // which preference changed? + if (!strcmp(DISK_CACHE_ENABLE_PREF, data.get())) { + + rv = branch->GetBoolPref(DISK_CACHE_ENABLE_PREF, + &mDiskCacheEnabled); + if (NS_FAILED(rv)) + return rv; + nsCacheService::SetDiskCacheEnabled(DiskCacheEnabled()); + + } else if (!strcmp(DISK_CACHE_CAPACITY_PREF, data.get())) { + + int32_t capacity = 0; + rv = branch->GetIntPref(DISK_CACHE_CAPACITY_PREF, &capacity); + if (NS_FAILED(rv)) + return rv; + mDiskCacheCapacity = std::max(0, capacity); + nsCacheService::SetDiskCacheCapacity(mDiskCacheCapacity); + + // Update the cache capacity when smart sizing is turned on/off + } else if (!strcmp(DISK_CACHE_SMART_SIZE_ENABLED_PREF, data.get())) { + // Is the update because smartsizing was turned on, or off? + rv = branch->GetBoolPref(DISK_CACHE_SMART_SIZE_ENABLED_PREF, + &mSmartSizeEnabled); + if (NS_FAILED(rv)) + return rv; + int32_t newCapacity = 0; + if (mSmartSizeEnabled) { + nsCacheService::SetDiskSmartSize(); + } else { + // Smart sizing switched off: use user specified size + rv = branch->GetIntPref(DISK_CACHE_CAPACITY_PREF, &newCapacity); + if (NS_FAILED(rv)) + return rv; + mDiskCacheCapacity = std::max(0, newCapacity); + nsCacheService::SetDiskCacheCapacity(mDiskCacheCapacity); + } + } else if (!strcmp(DISK_CACHE_USE_OLD_MAX_SMART_SIZE_PREF, data.get())) { + rv = branch->GetBoolPref(DISK_CACHE_USE_OLD_MAX_SMART_SIZE_PREF, + &mShouldUseOldMaxSmartSize); + if (NS_FAILED(rv)) + return rv; + } else if (!strcmp(DISK_CACHE_MAX_ENTRY_SIZE_PREF, data.get())) { + int32_t newMaxSize; + rv = branch->GetIntPref(DISK_CACHE_MAX_ENTRY_SIZE_PREF, + &newMaxSize); + if (NS_FAILED(rv)) + return rv; + + mDiskCacheMaxEntrySize = std::max(-1, newMaxSize); + nsCacheService::SetDiskCacheMaxEntrySize(mDiskCacheMaxEntrySize); + +#if 0 + } else if (!strcmp(DISK_CACHE_DIR_PREF, data.get())) { + // XXX We probaby don't want to respond to this pref except after + // XXX profile changes. Ideally, there should be somekind of user + // XXX notification that the pref change won't take effect until + // XXX the next time the profile changes (browser launch) +#endif + } else + + // which preference changed? + if (!strcmp(OFFLINE_CACHE_ENABLE_PREF, data.get())) { + + rv = branch->GetBoolPref(OFFLINE_CACHE_ENABLE_PREF, + &mOfflineCacheEnabled); + if (NS_FAILED(rv)) return rv; + nsCacheService::SetOfflineCacheEnabled(OfflineCacheEnabled()); + + } else if (!strcmp(OFFLINE_CACHE_CAPACITY_PREF, data.get())) { + + int32_t capacity = 0; + rv = branch->GetIntPref(OFFLINE_CACHE_CAPACITY_PREF, &capacity); + if (NS_FAILED(rv)) return rv; + mOfflineCacheCapacity = std::max(0, capacity); + nsCacheService::SetOfflineCacheCapacity(mOfflineCacheCapacity); +#if 0 + } else if (!strcmp(OFFLINE_CACHE_DIR_PREF, data.get())) { + // XXX We probaby don't want to respond to this pref except after + // XXX profile changes. Ideally, there should be some kind of user + // XXX notification that the pref change won't take effect until + // XXX the next time the profile changes (browser launch) +#endif + } else + + if (!strcmp(MEMORY_CACHE_ENABLE_PREF, data.get())) { + + rv = branch->GetBoolPref(MEMORY_CACHE_ENABLE_PREF, + &mMemoryCacheEnabled); + if (NS_FAILED(rv)) + return rv; + nsCacheService::SetMemoryCache(); + + } else if (!strcmp(MEMORY_CACHE_CAPACITY_PREF, data.get())) { + + mMemoryCacheCapacity = -1; + (void) branch->GetIntPref(MEMORY_CACHE_CAPACITY_PREF, + &mMemoryCacheCapacity); + nsCacheService::SetMemoryCache(); + } else if (!strcmp(MEMORY_CACHE_MAX_ENTRY_SIZE_PREF, data.get())) { + int32_t newMaxSize; + rv = branch->GetIntPref(MEMORY_CACHE_MAX_ENTRY_SIZE_PREF, + &newMaxSize); + if (NS_FAILED(rv)) + return rv; + + mMemoryCacheMaxEntrySize = std::max(-1, newMaxSize); + nsCacheService::SetMemoryCacheMaxEntrySize(mMemoryCacheMaxEntrySize); + } else if (!strcmp(CACHE_COMPRESSION_LEVEL_PREF, data.get())) { + mCacheCompressionLevel = CACHE_COMPRESSION_LEVEL; + (void)branch->GetIntPref(CACHE_COMPRESSION_LEVEL_PREF, + &mCacheCompressionLevel); + mCacheCompressionLevel = std::max(0, mCacheCompressionLevel); + mCacheCompressionLevel = std::min(9, mCacheCompressionLevel); + } else if (!strcmp(SANITIZE_ON_SHUTDOWN_PREF, data.get())) { + rv = branch->GetBoolPref(SANITIZE_ON_SHUTDOWN_PREF, + &mSanitizeOnShutdown); + if (NS_FAILED(rv)) + return rv; + nsCacheService::SetDiskCacheEnabled(DiskCacheEnabled()); + } else if (!strcmp(CLEAR_ON_SHUTDOWN_PREF, data.get())) { + rv = branch->GetBoolPref(CLEAR_ON_SHUTDOWN_PREF, + &mClearCacheOnShutdown); + if (NS_FAILED(rv)) + return rv; + nsCacheService::SetDiskCacheEnabled(DiskCacheEnabled()); + } + } else if (!strcmp("last-pb-context-exited", topic)) { + nsCacheService::LeavePrivateBrowsing(); + } + + return NS_OK; +} + +// Returns default ("smart") size (in KB) of cache, given available disk space +// (also in KB) +static uint32_t +SmartCacheSize(const uint32_t availKB, bool shouldUseOldMaxSmartSize) +{ + uint32_t maxSize = shouldUseOldMaxSmartSize ? OLD_MAX_CACHE_SIZE : MAX_CACHE_SIZE; + + if (availKB > 100 * 1024 * 1024) + return maxSize; // skip computing if we're over 100 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); + + // .5% of space above 25 GB + if (avail10MBs > 2500) { + sz10MBs += static_cast<uint32_t>((avail10MBs - 2500)*.005); + avail10MBs = 2500; + } + // 1% of space between 7GB -> 25 GB + if (avail10MBs > 700) { + sz10MBs += static_cast<uint32_t>((avail10MBs - 700)*.01); + avail10MBs = 700; + } + // 5% of space between 500 MB -> 7 GB + if (avail10MBs > 50) { + sz10MBs += static_cast<uint32_t>((avail10MBs - 50)*.05); + 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. + + // 20% of space up to 500 MB (10 MB min) + sz10MBs += std::max<uint32_t>(1, static_cast<uint32_t>(avail10MBs * .2)); +#else + // 40% of space up to 500 MB (50 MB min) + sz10MBs += std::max<uint32_t>(5, static_cast<uint32_t>(avail10MBs * .4)); +#endif + + return std::min<uint32_t>(maxSize, sz10MBs * 10 * 1024); +} + + /* Computes our best guess for the default size of the user's disk cache, + * based on the amount of space they have free on their hard drive. + * We use a tiered scheme: the more space available, + * the larger the disk cache will be. However, we do not want + * to enable the disk cache to grow to an unbounded size, so the larger the + * user's available space is, the smaller of a percentage we take. We set a + * lower bound of 50MB and an upper bound of 1GB. + * + *@param: None. + *@return: The size that the user's disk cache should default to, in kBytes. + */ +uint32_t +nsCacheProfilePrefObserver::GetSmartCacheSize(const nsAString& cachePath, + uint32_t currentSize, + bool shouldUseOldMaxSmartSize) +{ + // Check for free space on device where cache directory lives + nsresult rv; + nsCOMPtr<nsIFile> + cacheDirectory (do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv) || !cacheDirectory) + return DEFAULT_CACHE_SIZE; + rv = cacheDirectory->InitWithPath(cachePath); + if (NS_FAILED(rv)) + return DEFAULT_CACHE_SIZE; + int64_t bytesAvailable; + rv = cacheDirectory->GetDiskSpaceAvailable(&bytesAvailable); + if (NS_FAILED(rv)) + return DEFAULT_CACHE_SIZE; + + return SmartCacheSize(static_cast<uint32_t>((bytesAvailable / 1024) + + currentSize), + shouldUseOldMaxSmartSize); +} + +/* Determine if we are permitted to dynamically size the user's disk cache based + * on their disk space available. We may do this so long as the pref + * smart_size.enabled is true. + */ +bool +nsCacheProfilePrefObserver::PermittedToSmartSize(nsIPrefBranch* branch, bool + firstRun) +{ + nsresult rv; + if (firstRun) { + // check if user has set cache size in the past + bool userSet; + rv = branch->PrefHasUserValue(DISK_CACHE_CAPACITY_PREF, &userSet); + if (NS_FAILED(rv)) userSet = true; + if (userSet) { + int32_t oldCapacity; + // If user explicitly set cache size to be smaller than old default + // of 50 MB, then keep user's value. Otherwise use smart sizing. + rv = branch->GetIntPref(DISK_CACHE_CAPACITY_PREF, &oldCapacity); + if (oldCapacity < PRE_GECKO_2_0_DEFAULT_CACHE_SIZE) { + mSmartSizeEnabled = false; + branch->SetBoolPref(DISK_CACHE_SMART_SIZE_ENABLED_PREF, + mSmartSizeEnabled); + return mSmartSizeEnabled; + } + } + // Set manual setting to MAX cache size as starting val for any + // adjustment by user: (bug 559942 comment 65) + int32_t maxSize = mShouldUseOldMaxSmartSize ? OLD_MAX_CACHE_SIZE : MAX_CACHE_SIZE; + branch->SetIntPref(DISK_CACHE_CAPACITY_PREF, maxSize); + } + + rv = branch->GetBoolPref(DISK_CACHE_SMART_SIZE_ENABLED_PREF, + &mSmartSizeEnabled); + if (NS_FAILED(rv)) + mSmartSizeEnabled = false; + return mSmartSizeEnabled; +} + + +nsresult +nsCacheProfilePrefObserver::ReadPrefs(nsIPrefBranch* branch) +{ + nsresult rv = NS_OK; + + // read disk cache device prefs + mDiskCacheEnabled = true; // presume disk cache is enabled + (void) branch->GetBoolPref(DISK_CACHE_ENABLE_PREF, &mDiskCacheEnabled); + + mDiskCacheCapacity = DISK_CACHE_CAPACITY; + (void)branch->GetIntPref(DISK_CACHE_CAPACITY_PREF, &mDiskCacheCapacity); + mDiskCacheCapacity = std::max(0, mDiskCacheCapacity); + + (void) branch->GetIntPref(DISK_CACHE_MAX_ENTRY_SIZE_PREF, + &mDiskCacheMaxEntrySize); + mDiskCacheMaxEntrySize = std::max(-1, mDiskCacheMaxEntrySize); + + (void) branch->GetComplexValue(DISK_CACHE_DIR_PREF, // ignore error + NS_GET_IID(nsIFile), + getter_AddRefs(mDiskCacheParentDirectory)); + + (void) branch->GetBoolPref(DISK_CACHE_USE_OLD_MAX_SMART_SIZE_PREF, + &mShouldUseOldMaxSmartSize); + + if (!mDiskCacheParentDirectory) { + nsCOMPtr<nsIFile> directory; + + // try to get the disk cache parent directory + rv = NS_GetSpecialDirectory(NS_APP_CACHE_PARENT_DIR, + getter_AddRefs(directory)); + if (NS_FAILED(rv)) { + // 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, + "Cache"); + } + } + // use file cache in build tree only if asked, to avoid cache dir litter + if (!directory && PR_GetEnv("NECKO_DEV_ENABLE_DISK_CACHE")) { + rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, + getter_AddRefs(directory)); + } + if (directory) + mDiskCacheParentDirectory = do_QueryInterface(directory, &rv); + } + if (mDiskCacheParentDirectory) { + bool firstSmartSizeRun; + rv = branch->GetBoolPref(DISK_CACHE_SMART_SIZE_FIRST_RUN_PREF, + &firstSmartSizeRun); + if (NS_FAILED(rv)) + firstSmartSizeRun = false; + if (PermittedToSmartSize(branch, firstSmartSizeRun)) { + // Avoid evictions: use previous cache size until smart size event + // updates mDiskCacheCapacity + rv = branch->GetIntPref(firstSmartSizeRun ? + DISK_CACHE_CAPACITY_PREF : + DISK_CACHE_SMART_SIZE_PREF, + &mDiskCacheCapacity); + if (NS_FAILED(rv)) + mDiskCacheCapacity = DEFAULT_CACHE_SIZE; + } + + if (firstSmartSizeRun) { + // It is no longer our first run + rv = branch->SetBoolPref(DISK_CACHE_SMART_SIZE_FIRST_RUN_PREF, + false); + if (NS_FAILED(rv)) + NS_WARNING("Failed setting first_run pref in ReadPrefs."); + } + } + + // read offline cache device prefs + mOfflineCacheEnabled = true; // presume offline cache is enabled + (void) branch->GetBoolPref(OFFLINE_CACHE_ENABLE_PREF, + &mOfflineCacheEnabled); + + 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) { + nsCOMPtr<nsIFile> directory; + + // try to get the offline cache parent directory + rv = NS_GetSpecialDirectory(NS_APP_CACHE_PARENT_DIR, + getter_AddRefs(directory)); + if (NS_FAILED(rv)) { + // 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, + "OfflineCache"); + } + } +#if DEBUG + if (!directory) { + // use current process directory during development + rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, + getter_AddRefs(directory)); + } +#endif + if (directory) + mOfflineCacheParentDirectory = do_QueryInterface(directory, &rv); + } + + // read memory cache device prefs + (void) branch->GetBoolPref(MEMORY_CACHE_ENABLE_PREF, &mMemoryCacheEnabled); + + mMemoryCacheCapacity = -1; + (void) branch->GetIntPref(MEMORY_CACHE_CAPACITY_PREF, + &mMemoryCacheCapacity); + + (void) branch->GetIntPref(MEMORY_CACHE_MAX_ENTRY_SIZE_PREF, + &mMemoryCacheMaxEntrySize); + mMemoryCacheMaxEntrySize = std::max(-1, mMemoryCacheMaxEntrySize); + + // read cache compression level pref + mCacheCompressionLevel = CACHE_COMPRESSION_LEVEL; + (void)branch->GetIntPref(CACHE_COMPRESSION_LEVEL_PREF, + &mCacheCompressionLevel); + mCacheCompressionLevel = std::max(0, mCacheCompressionLevel); + mCacheCompressionLevel = std::min(9, mCacheCompressionLevel); + + // read cache shutdown sanitization prefs + (void) branch->GetBoolPref(SANITIZE_ON_SHUTDOWN_PREF, + &mSanitizeOnShutdown); + (void) branch->GetBoolPref(CLEAR_ON_SHUTDOWN_PREF, + &mClearCacheOnShutdown); + + return rv; +} + +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::DiskCacheEnabled() +{ + if ((mDiskCacheCapacity == 0) || (!mDiskCacheParentDirectory)) return false; + return mDiskCacheEnabled && (!mSanitizeOnShutdown || !mClearCacheOnShutdown); +} + + +bool +nsCacheProfilePrefObserver::OfflineCacheEnabled() +{ + if ((mOfflineCacheCapacity == 0) || (!mOfflineCacheParentDirectory)) + return false; + + return mOfflineCacheEnabled; +} + + +bool +nsCacheProfilePrefObserver::MemoryCacheEnabled() +{ + if (mMemoryCacheCapacity == 0) return false; + return mMemoryCacheEnabled; +} + + +/** + * MemoryCacheCapacity + * + * If the browser.cache.memory.capacity preference is positive, we use that + * value for the amount of memory available for the cache. + * + * If browser.cache.memory.capacity is zero, the memory cache is disabled. + * + * If browser.cache.memory.capacity is negative or not present, we use a + * formula that grows less than linearly with the amount of system memory, + * with an upper limit on the cache size. No matter how much physical RAM is + * present, the default cache size would not exceed 32 MB. This maximum would + * apply only to systems with more than 4 GB of RAM (e.g. terminal servers) + * + * RAM Cache + * --- ----- + * 32 Mb 2 Mb + * 64 Mb 4 Mb + * 128 Mb 6 Mb + * 256 Mb 10 Mb + * 512 Mb 14 Mb + * 1024 Mb 18 Mb + * 2048 Mb 24 Mb + * 4096 Mb 30 Mb + * + * The equation for this is (for cache size C and memory size K (kbytes)): + * x = log2(K) - 14 + * C = x^2/3 + x + 2/3 + 0.1 (0.1 for rounding) + * if (C > 32) C = 32 + */ + +int32_t +nsCacheProfilePrefObserver::MemoryCacheCapacity() +{ + int32_t capacity = mMemoryCacheCapacity; + if (capacity >= 0) { + CACHE_LOG_DEBUG(("Memory cache capacity forced to %d\n", capacity)); + return capacity; + } + + static uint64_t bytes = PR_GetPhysicalMemorySize(); + CACHE_LOG_DEBUG(("Physical Memory size is %llu\n", bytes)); + + // If getting the physical memory failed, arbitrarily assume + // 32 MB of RAM. We use a low default to have a reasonable + // size on all the devices we support. + if (bytes == 0) + bytes = 32 * 1024 * 1024; + + // Conversion from unsigned int64_t to double doesn't work on all platforms. + // We need to truncate the value at INT64_MAX to make sure we don't + // overflow. + if (bytes > INT64_MAX) + bytes = INT64_MAX; + + uint64_t kbytes = bytes >> 10; + + double kBytesD = double(kbytes); + + double x = log(kBytesD)/log(2.0) - 14; + if (x > 0) { + capacity = (int32_t)(x * x / 3.0 + x + 2.0 / 3 + 0.1); // 0.1 for rounding + if (capacity > 32) + capacity = 32; + capacity *= 1024; + } else { + capacity = 0; + } + + return capacity; +} + +int32_t +nsCacheProfilePrefObserver::CacheCompressionLevel() +{ + return mCacheCompressionLevel; +} + +/****************************************************************************** + * nsProcessRequestEvent + *****************************************************************************/ + +class nsProcessRequestEvent : public Runnable { +public: + explicit nsProcessRequestEvent(nsCacheRequest *aRequest) + { + 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() {} + +private: + nsCacheRequest *mRequest; +}; + +/****************************************************************************** + * nsDoomEvent + *****************************************************************************/ + +class nsDoomEvent : public Runnable { +public: + nsDoomEvent(nsCacheSession *session, + const nsACString &key, + nsICacheListener *listener) + { + mKey = *session->ClientID(); + mKey.Append(':'); + mKey.Append(key); + mStoragePolicy = session->StoragePolicy(); + mListener = listener; + mThread = do_GetCurrentThread(); + // 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) { + mThread->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<nsIThread> mThread; +}; + +/****************************************************************************** + * nsCacheService + *****************************************************************************/ +nsCacheService * nsCacheService::gService = nullptr; + +NS_IMPL_ISUPPORTS(nsCacheService, nsICacheService, nsICacheServiceInternal, + nsIMemoryReporter) + +nsCacheService::nsCacheService() + : mObserver(nullptr), + mLock("nsCacheService.mLock"), + mCondVar(mLock, "nsCacheService.mCondVar"), + mNotified(false), + mTimeStampLock("nsCacheService.mTimeStampLock"), + mInitialized(false), + mClearingEntries(false), + mEnableMemoryDevice(true), + mEnableDiskDevice(true), + mMemoryDevice(nullptr), + mDiskDevice(nullptr), + mOfflineDevice(nullptr), + 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_RUNTIMEABORT("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(); + } + + mEnableDiskDevice = mObserver->DiskCacheEnabled(); + mEnableOfflineDevice = mObserver->OfflineCacheEnabled(); + mEnableMemoryDevice = mObserver->MemoryCacheEnabled(); + + RegisterWeakMemoryReporter(this); + + 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()) { + NS_RUNTIMEABORT("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(); + + UnregisterWeakMemoryReporter(this); + + { + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SHUTDOWN)); + NS_ASSERTION(mInitialized, "Bad state"); + + mInitialized = false; + + // Clear entries + ClearDoomList(); + + if (mSmartSizeTimer) { + mSmartSizeTimer->Cancel(); + mSmartSizeTimer = nullptr; + } + + // 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(); + + // deallocate memory and disk caches + delete mMemoryDevice; + mMemoryDevice = nullptr; + + delete mDiskDevice; + mDiskDevice = nullptr; + + 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(NS_LITERAL_CSTRING("Cache")); + 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; + + nsCacheService * cacheService = new nsCacheService(); + if (cacheService == nullptr) + return NS_ERROR_OUT_OF_MEMORY; + + NS_ADDREF(cacheService); + rv = cacheService->Init(); + if (NS_SUCCEEDED(rv)) { + rv = cacheService->QueryInterface(aIID, aResult); + } + NS_RELEASE(cacheService); + return rv; +} + + +NS_IMETHODIMP +nsCacheService::CreateSession(const char * clientID, + nsCacheStoragePolicy storagePolicy, + bool streamBased, + nsICacheSession **result) +{ + *result = nullptr; + + if (net::CacheObserver::UseNewCache()) + return NS_ERROR_NOT_IMPLEMENTED; + + return CreateSessionInternal(clientID, storagePolicy, streamBased, result); +} + +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) + : 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; + + if (storagePolicy == nsICache::STORE_ANYWHERE || + storagePolicy == nsICache::STORE_ON_DISK) { + + if (mEnableDiskDevice) { + nsresult rv = NS_OK; + if (!mDiskDevice) + rv = CreateDiskDevice(); + if (mDiskDevice) + rv = mDiskDevice->EvictEntries(clientID); + if (NS_FAILED(rv)) + res = rv; + } + } + + // 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; + } + } + + if (storagePolicy == nsICache::STORE_ANYWHERE || + storagePolicy == nsICache::STORE_IN_MEMORY) { + // If there is no memory device, there is no need to evict it... + if (mMemoryDevice) { + nsresult rv = mMemoryDevice->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 = gService->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->mEnableMemoryDevice && + (storagePolicy == nsICache::STORE_ANYWHERE || + storagePolicy == nsICache::STORE_IN_MEMORY)) { + return true; + } + if (gService->mEnableDiskDevice && + (storagePolicy == nsICache::STORE_ANYWHERE || + storagePolicy == nsICache::STORE_ON_DISK)) { + return true; + } + if (gService->mEnableOfflineDevice && + storagePolicy == nsICache::STORE_OFFLINE) { + return true; + } + + return false; +} + +NS_IMETHODIMP nsCacheService::VisitEntries(nsICacheVisitor *visitor) +{ + if (net::CacheObserver::UseNewCache()) + return NS_ERROR_NOT_IMPLEMENTED; + + return VisitEntriesInternal(visitor); +} + +nsresult nsCacheService::VisitEntriesInternal(nsICacheVisitor *visitor) +{ + NS_ENSURE_ARG_POINTER(visitor); + + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_VISITENTRIES)); + + if (!(mEnableDiskDevice || mEnableMemoryDevice)) + 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 there is no memory device, there are then also no entries to visit... + if (mMemoryDevice) { + rv = mMemoryDevice->Visit(visitor); + if (NS_FAILED(rv)) return rv; + } + + if (mEnableDiskDevice) { + if (!mDiskDevice) { + rv = CreateDiskDevice(); + if (NS_FAILED(rv)) return rv; + } + rv = mDiskDevice->Visit(visitor); + if (NS_FAILED(rv)) return rv; + } + + 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) +{ + if (net::CacheObserver::UseNewCache()) + return NS_ERROR_NOT_IMPLEMENTED; + + return EvictEntriesInternal(storagePolicy); +} + +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(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) { + NS_ADDREF(*aCacheIOTarget = mCacheIOThread); + 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::CreateDiskDevice() +{ + if (!mInitialized) return NS_ERROR_NOT_AVAILABLE; + if (!mEnableDiskDevice) return NS_ERROR_NOT_AVAILABLE; + if (mDiskDevice) return NS_OK; + + mDiskDevice = new nsDiskCacheDevice; + if (!mDiskDevice) return NS_ERROR_OUT_OF_MEMORY; + + // set the preferences + mDiskDevice->SetCacheParentDirectory(mObserver->DiskCacheParentDirectory()); + mDiskDevice->SetCapacity(mObserver->DiskCacheCapacity()); + mDiskDevice->SetMaxEntrySize(mObserver->DiskCacheMaxEntrySize()); + + nsresult rv = mDiskDevice->Init(); + if (NS_FAILED(rv)) { +#if DEBUG + printf("###\n"); + printf("### mDiskDevice->Init() failed (0x%.8x)\n", + static_cast<uint32_t>(rv)); + printf("### - disabling disk cache for this session.\n"); + printf("###\n"); +#endif + mEnableDiskDevice = false; + delete mDiskDevice; + mDiskDevice = nullptr; + return rv; + } + + NS_ASSERTION(!mSmartSizeTimer, "Smartsize timer was already fired!"); + + // Disk device is usually created during the startup. Delay smart size + // calculation to avoid possible massive IO caused by eviction of entries + // in case the new smart size is smaller than current cache usage. + mSmartSizeTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); + if (NS_SUCCEEDED(rv)) { + rv = mSmartSizeTimer->InitWithCallback(new nsSetDiskSmartSizeCallback(), + 1000*60*3, + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to post smart size timer"); + mSmartSizeTimer = nullptr; + } + } else { + NS_WARNING("Can't create smart size timer"); + } + // Ignore state of the timer and return success since the purpose of the + // method (create the disk-device) has been fulfilled + + return NS_OK; +} + +// Runnable sent from cache thread to main thread +class nsDisableOldMaxSmartSizePrefEvent: public Runnable +{ +public: + nsDisableOldMaxSmartSizePrefEvent() {} + + NS_IMETHOD Run() override + { + // Main thread may have already called nsCacheService::Shutdown + if (!nsCacheService::IsInitialized()) + return NS_ERROR_NOT_AVAILABLE; + + nsCOMPtr<nsIPrefBranch> branch = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!branch) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = branch->SetBoolPref(DISK_CACHE_USE_OLD_MAX_SMART_SIZE_PREF, false); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to disable old max smart size"); + return rv; + } + + // It is safe to call SetDiskSmartSize_Locked() without holding the lock + // when we are on main thread and nsCacheService is initialized. + nsCacheService::gService->SetDiskSmartSize_Locked(); + + if (nsCacheService::gService->mObserver->PermittedToSmartSize(branch, false)) { + rv = branch->SetIntPref(DISK_CACHE_CAPACITY_PREF, MAX_CACHE_SIZE); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to set cache capacity pref"); + } + } + + return NS_OK; + } +}; + +void +nsCacheService::MarkStartingFresh() +{ + if (!gService || !gService->mObserver->ShouldUseOldMaxSmartSize()) { + // Already using new max, nothing to do here + return; + } + + gService->mObserver->SetUseNewMaxSmartSize(true); + + // We always dispatch an event here because we don't want to deal with lock + // reentrance issues. + NS_DispatchToMainThread(new nsDisableOldMaxSmartSizePrefEvent()); +} + +nsresult +nsCacheService::GetOfflineDevice(nsOfflineCacheDevice **aDevice) +{ + if (!mOfflineDevice) { + nsresult rv = CreateOfflineDevice(); + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_ADDREF(*aDevice = mOfflineDevice); + 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); + NS_ENSURE_SUCCESS(rv, rv); + + (*aDevice)->SetAutoShutdown(); + mCustomOfflineDevices.Put(profilePath, *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; + } + + nsresult rv = CreateCustomOfflineDevice( + mObserver->OfflineCacheParentDirectory(), + mObserver->OfflineCacheCapacity(), + &mOfflineDevice); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsCacheService::CreateCustomOfflineDevice(nsIFile *aProfileDir, + int32_t aQuota, + nsOfflineCacheDevice **aDevice) +{ + NS_ENSURE_ARG(aProfileDir); + + if (MOZ_LOG_TEST(gCacheLog, LogLevel::Info)) { + nsAutoCString profilePath; + aProfileDir->GetNativePath(profilePath); + CACHE_LOG_INFO(("Creating custom offline device, %s, %d", + profilePath.BeginReading(), aQuota)); + } + + if (!mInitialized) return NS_ERROR_NOT_AVAILABLE; + if (!mEnableOfflineDevice) return NS_ERROR_NOT_AVAILABLE; + + *aDevice = new nsOfflineCacheDevice; + + NS_ADDREF(*aDevice); + + // set the preferences + (*aDevice)->SetCacheParentDirectory(aProfileDir); + (*aDevice)->SetCapacity(aQuota); + + nsresult rv = (*aDevice)->InitWithSqlite(mStorageService); + if (NS_FAILED(rv)) { + CACHE_LOG_DEBUG(("OfflineDevice->InitWithSqlite() failed (0x%.8x)\n", rv)); + CACHE_LOG_DEBUG((" - disabling offline cache for this session.\n")); + + NS_RELEASE(*aDevice); + } + return rv; +} + +nsresult +nsCacheService::CreateMemoryDevice() +{ + if (!mInitialized) return NS_ERROR_NOT_AVAILABLE; + if (!mEnableMemoryDevice) return NS_ERROR_NOT_AVAILABLE; + if (mMemoryDevice) return NS_OK; + + mMemoryDevice = new nsMemoryCacheDevice; + if (!mMemoryDevice) return NS_ERROR_OUT_OF_MEMORY; + + // set preference + int32_t capacity = mObserver->MemoryCacheCapacity(); + CACHE_LOG_DEBUG(("Creating memory device with capacity %d\n", capacity)); + mMemoryDevice->SetCapacity(capacity); + mMemoryDevice->SetMaxEntrySize(mObserver->MemoryCacheMaxEntrySize()); + + nsresult rv = mMemoryDevice->Init(); + if (NS_FAILED(rv)) { + NS_WARNING("Initialization of Memory Cache failed."); + delete mMemoryDevice; + mMemoryDevice = nullptr; + } + + 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)->mThread = do_GetCurrentThread(); + + return NS_OK; +} + + +class nsCacheListenerEvent : public Runnable +{ +public: + nsCacheListenerEvent(nsICacheListener *listener, + nsICacheEntryDescriptor *descriptor, + nsCacheAccessMode accessGranted, + nsresult status) + : 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->mThread, "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->mThread->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(1) { // Activate entry loop + rv = ActivateEntry(request, &entry, &doomedEntry); // get the entry for this request + if (NS_FAILED(rv)) break; + + while(1) { // 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 (!mEnableMemoryDevice && !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; + + CACHE_LOG_DEBUG(("mMemoryDevice: 0x%p\n", mMemoryDevice)); + + *collision = false; + if ((policy == nsICache::STORE_ANYWHERE) || (policy == nsICache::STORE_IN_MEMORY)) { + // If there is no memory device, then there is nothing to search... + if (mMemoryDevice) { + entry = mMemoryDevice->FindEntry(key, collision); + CACHE_LOG_DEBUG(("Searching mMemoryDevice for key %s found: 0x%p, " + "collision: %d\n", key->get(), entry, collision)); + } + } + + if (!entry && + ((policy == nsICache::STORE_ANYWHERE) || (policy == nsICache::STORE_ON_DISK))) { + + if (mEnableDiskDevice) { + if (!mDiskDevice) { + nsresult rv = CreateDiskDevice(); + if (NS_FAILED(rv)) + return nullptr; + } + + entry = mDiskDevice->FindEntry(key, collision); + } + } + + if (!entry && (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; + + int64_t predictedDataSize = entry->PredictedDataSize(); + if (entry->IsStreamData() && entry->IsAllowedOnDisk() && mEnableDiskDevice) { + // this is the default + if (!mDiskDevice) { + (void)CreateDiskDevice(); // ignore the error (check for mDiskDevice instead) + } + + if (mDiskDevice) { + // Bypass the cache if Content-Length says the entry will be too big + if (predictedDataSize != -1 && + mDiskDevice->EntryIsTooBig(predictedDataSize)) { + DebugOnly<nsresult> rv = nsCacheService::DoomEntry(entry); + NS_ASSERTION(NS_SUCCEEDED(rv),"DoomEntry() failed."); + return nullptr; + } + + entry->MarkBinding(); // enter state of binding + nsresult rv = mDiskDevice->BindEntry(entry); + entry->ClearBinding(); // exit state of binding + if (NS_SUCCEEDED(rv)) + device = mDiskDevice; + } + } + + // if we can't use mDiskDevice, try mMemoryDevice + if (!device && mEnableMemoryDevice && entry->IsAllowedInMemory()) { + if (!mMemoryDevice) { + (void)CreateMemoryDevice(); // ignore the error (check for mMemoryDevice instead) + } + if (mMemoryDevice) { + // Bypass the cache if Content-Length says entry will be too big + if (predictedDataSize != -1 && + mMemoryDevice->EntryIsTooBig(predictedDataSize)) { + DebugOnly<nsresult> rv = nsCacheService::DoomEntry(entry); + NS_ASSERTION(NS_SUCCEEDED(rv),"DoomEntry() failed."); + return nullptr; + } + + entry->MarkBinding(); // enter state of binding + nsresult rv = mMemoryDevice->BindEntry(entry); + entry->ClearBinding(); // exit state of binding + if (NS_SUCCEEDED(rv)) + device = mMemoryDevice; + } + } + + 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->mDiskDevice && gService->mEnableDiskDevice) { + gService->mDiskDevice->Shutdown(); + } + gService->mEnableDiskDevice = false; + + 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; + + if (gService->mMemoryDevice) { + // clear memory cache + gService->mMemoryDevice->EvictEntries(nullptr); + } + + gService->mClearingEntries = false; +} + + +void +nsCacheService::OnProfileChanged() +{ + if (!gService) return; + + CACHE_LOG_DEBUG(("nsCacheService::OnProfileChanged")); + + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_ONPROFILECHANGED)); + + gService->mEnableDiskDevice = gService->mObserver->DiskCacheEnabled(); + gService->mEnableOfflineDevice = gService->mObserver->OfflineCacheEnabled(); + gService->mEnableMemoryDevice = gService->mObserver->MemoryCacheEnabled(); + + if (gService->mDiskDevice) { + gService->mDiskDevice->SetCacheParentDirectory(gService->mObserver->DiskCacheParentDirectory()); + gService->mDiskDevice->SetCapacity(gService->mObserver->DiskCacheCapacity()); + + // XXX initialization of mDiskDevice could be made lazily, if mEnableDiskDevice is false + nsresult rv = gService->mDiskDevice->Init(); + if (NS_FAILED(rv)) { + NS_ERROR("nsCacheService::OnProfileChanged: Re-initializing disk device failed"); + gService->mEnableDiskDevice = false; + // XXX delete mDiskDevice? + } + } + + 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? + } + } + + // If memoryDevice exists, reset its size to the new profile + if (gService->mMemoryDevice) { + if (gService->mEnableMemoryDevice) { + // make sure that capacity is reset to the right value + int32_t capacity = gService->mObserver->MemoryCacheCapacity(); + CACHE_LOG_DEBUG(("Resetting memory device capacity to %d\n", + capacity)); + gService->mMemoryDevice->SetCapacity(capacity); + } else { + // tell memory device to evict everything + CACHE_LOG_DEBUG(("memory device disabled\n")); + gService->mMemoryDevice->SetCapacity(0); + // Don't delete memory device, because some entries may be active still... + } + } +} + + +void +nsCacheService::SetDiskCacheEnabled(bool enabled) +{ + if (!gService) return; + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SETDISKCACHEENABLED)); + gService->mEnableDiskDevice = enabled; +} + + +void +nsCacheService::SetDiskCacheCapacity(int32_t capacity) +{ + if (!gService) return; + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SETDISKCACHECAPACITY)); + + if (gService->mDiskDevice) { + gService->mDiskDevice->SetCapacity(capacity); + } + + gService->mEnableDiskDevice = gService->mObserver->DiskCacheEnabled(); +} + +void +nsCacheService::SetDiskCacheMaxEntrySize(int32_t maxSize) +{ + if (!gService) return; + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SETDISKCACHEMAXENTRYSIZE)); + + if (gService->mDiskDevice) { + gService->mDiskDevice->SetMaxEntrySize(maxSize); + } +} + +void +nsCacheService::SetMemoryCacheMaxEntrySize(int32_t maxSize) +{ + if (!gService) return; + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SETMEMORYCACHEMAXENTRYSIZE)); + + if (gService->mMemoryDevice) { + gService->mMemoryDevice->SetMaxEntrySize(maxSize); + } +} + +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(); +} + + +void +nsCacheService::SetMemoryCache() +{ + if (!gService) return; + + CACHE_LOG_DEBUG(("nsCacheService::SetMemoryCache")); + + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SETMEMORYCACHE)); + + gService->mEnableMemoryDevice = gService->mObserver->MemoryCacheEnabled(); + + if (gService->mEnableMemoryDevice) { + if (gService->mMemoryDevice) { + int32_t capacity = gService->mObserver->MemoryCacheCapacity(); + // make sure that capacity is reset to the right value + CACHE_LOG_DEBUG(("Resetting memory device capacity to %d\n", + capacity)); + gService->mMemoryDevice->SetCapacity(capacity); + } + } else { + if (gService->mMemoryDevice) { + // tell memory device to evict everything + CACHE_LOG_DEBUG(("memory device disabled\n")); + gService->mMemoryDevice->SetCapacity(0); + // Don't delete memory device, because some entries may be active still... + } + } +} + + +/****************************************************************************** + * 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::ID mainThreadLockerID) +{ + mozilla::Telemetry::ID lockerID; + mozilla::Telemetry::ID 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; + doomed.SwapElements(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(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(NS_LITERAL_CSTRING("Cache")); + 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(NS_LITERAL_CSTRING("OfflineCache")); + if (NS_FAILED(rv)) + return; + + directory.forget(result); +} + + +void +nsCacheService::LogCacheStatistics() +{ + uint32_t hitPercentage = (uint32_t)((((double)mCacheHits) / + ((double)(mCacheHits + mCacheMisses))) * 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)); +} + +nsresult +nsCacheService::SetDiskSmartSize() +{ + nsCacheServiceAutoLock lock(LOCK_TELEM(NSCACHESERVICE_SETDISKSMARTSIZE)); + + if (!gService) return NS_ERROR_NOT_AVAILABLE; + + return gService->SetDiskSmartSize_Locked(); +} + +nsresult +nsCacheService::SetDiskSmartSize_Locked() +{ + nsresult rv; + + if (mozilla::net::CacheObserver::UseNewCache()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!mObserver->DiskCacheParentDirectory()) + return NS_ERROR_NOT_AVAILABLE; + + if (!mDiskDevice) + return NS_ERROR_NOT_AVAILABLE; + + if (!mObserver->SmartSizeEnabled()) + return NS_ERROR_NOT_AVAILABLE; + + nsAutoString cachePath; + rv = mObserver->DiskCacheParentDirectory()->GetPath(cachePath); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIRunnable> event = + new nsGetSmartSizeEvent(cachePath, mDiskDevice->getCacheSize(), + mObserver->ShouldUseOldMaxSmartSize()); + DispatchToCacheIOThread(event); + } else { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +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; + + nsAutoCString newPath; + rv = aNewCacheSubdir->GetNativePath(newPath); + if (NS_FAILED(rv)) + return; + + 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) { + nsAutoCString oldPath; + rv = aOldCacheSubdir->GetNativePath(oldPath); + if (NS_FAILED(rv)) + return; + if (rename(oldPath.get(), newPath.get()) == 0) + 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); + + if (gService->mMemoryDevice) { + // clear memory cache + gService->mMemoryDevice->EvictPrivateEntries(); + } +} + +MOZ_DEFINE_MALLOC_SIZE_OF(DiskCacheDeviceMallocSizeOf) + +NS_IMETHODIMP +nsCacheService::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) +{ + size_t disk = 0; + if (mDiskDevice) { + nsCacheServiceAutoLock + lock(LOCK_TELEM(NSCACHESERVICE_DISKDEVICEHEAPSIZE)); + disk = mDiskDevice->SizeOfIncludingThis(DiskCacheDeviceMallocSizeOf); + } + + size_t memory = mMemoryDevice ? mMemoryDevice->TotalSize() : 0; + + MOZ_COLLECT_REPORT( + "explicit/network/disk-cache", KIND_HEAP, UNITS_BYTES, disk, + "Memory used by the network disk cache."); + + MOZ_COLLECT_REPORT( + "explicit/network/memory-cache", KIND_HEAP, UNITS_BYTES, memory, + "Memory used by the network memory cache."); + + return NS_OK; +} |