fune/netwerk/cache/nsCacheService.cpp
Valentin Gosu 277da76cef Bug 1629825 - Delete Appcache directory when storage pref is turned off r=necko-reviewers,kershaw
This bug was initially about adding a test that we can still clear appcache
data even when the storage pref is off.
However, due to the fact that we only check the pref at startup and then
are unable to access the storage device if disabled, it's best to just
delete the OfflineCache folder shortly after startup.

Differential Revision: https://phabricator.services.mozilla.com/D90395
2020-09-17 08:19:49 +00:00

1910 lines
60 KiB
C++

/* -*- 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);
}