michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et cindent: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsDeleteDir.h" michael@0: #include "nsIFile.h" michael@0: #include "nsString.h" michael@0: #include "mozilla/Telemetry.h" michael@0: #include "nsITimer.h" michael@0: #include "nsISimpleEnumerator.h" michael@0: #include "nsAutoPtr.h" michael@0: #include "nsThreadUtils.h" michael@0: #include "nsISupportsPriority.h" michael@0: #include "nsCacheUtils.h" michael@0: #include "prtime.h" michael@0: #include michael@0: michael@0: using namespace mozilla; michael@0: michael@0: class nsBlockOnBackgroundThreadEvent : public nsRunnable { michael@0: public: michael@0: nsBlockOnBackgroundThreadEvent() {} michael@0: NS_IMETHOD Run() michael@0: { michael@0: MutexAutoLock lock(nsDeleteDir::gInstance->mLock); michael@0: nsDeleteDir::gInstance->mCondVar.Notify(); michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: michael@0: michael@0: nsDeleteDir * nsDeleteDir::gInstance = nullptr; michael@0: michael@0: nsDeleteDir::nsDeleteDir() michael@0: : mLock("nsDeleteDir.mLock"), michael@0: mCondVar(mLock, "nsDeleteDir.mCondVar"), michael@0: mShutdownPending(false), michael@0: mStopDeleting(false) michael@0: { michael@0: NS_ASSERTION(gInstance==nullptr, "multiple nsCacheService instances!"); michael@0: } michael@0: michael@0: nsDeleteDir::~nsDeleteDir() michael@0: { michael@0: gInstance = nullptr; michael@0: } michael@0: michael@0: nsresult michael@0: nsDeleteDir::Init() michael@0: { michael@0: if (gInstance) michael@0: return NS_ERROR_ALREADY_INITIALIZED; michael@0: michael@0: gInstance = new nsDeleteDir(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDeleteDir::Shutdown(bool finishDeleting) michael@0: { michael@0: if (!gInstance) michael@0: return NS_ERROR_NOT_INITIALIZED; michael@0: michael@0: nsCOMArray dirsToRemove; michael@0: nsCOMPtr thread; michael@0: { michael@0: MutexAutoLock lock(gInstance->mLock); michael@0: NS_ASSERTION(!gInstance->mShutdownPending, michael@0: "Unexpected state in nsDeleteDir::Shutdown()"); michael@0: gInstance->mShutdownPending = true; michael@0: michael@0: if (!finishDeleting) michael@0: gInstance->mStopDeleting = true; michael@0: michael@0: // remove all pending timers michael@0: for (int32_t i = gInstance->mTimers.Count(); i > 0; i--) { michael@0: nsCOMPtr timer = gInstance->mTimers[i-1]; michael@0: gInstance->mTimers.RemoveObjectAt(i-1); michael@0: timer->Cancel(); michael@0: michael@0: nsCOMArray *arg; michael@0: timer->GetClosure((reinterpret_cast(&arg))); michael@0: michael@0: if (finishDeleting) michael@0: dirsToRemove.AppendObjects(*arg); michael@0: michael@0: // delete argument passed to the timer michael@0: delete arg; michael@0: } michael@0: michael@0: thread.swap(gInstance->mThread); michael@0: if (thread) { michael@0: // dispatch event and wait for it to run and notify us, so we know thread michael@0: // has completed all work and can be shutdown michael@0: nsCOMPtr event = new nsBlockOnBackgroundThreadEvent(); michael@0: nsresult rv = thread->Dispatch(event, NS_DISPATCH_NORMAL); michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Failed dispatching block-event"); michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: michael@0: rv = gInstance->mCondVar.Wait(); michael@0: nsShutdownThread::BlockingShutdown(thread); michael@0: } michael@0: } michael@0: michael@0: delete gInstance; michael@0: michael@0: for (int32_t i = 0; i < dirsToRemove.Count(); i++) michael@0: dirsToRemove[i]->Remove(true); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDeleteDir::InitThread() michael@0: { michael@0: if (mThread) michael@0: return NS_OK; michael@0: michael@0: nsresult rv = NS_NewNamedThread("Cache Deleter", getter_AddRefs(mThread)); michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Can't create background thread"); michael@0: return rv; michael@0: } michael@0: michael@0: nsCOMPtr p = do_QueryInterface(mThread); michael@0: if (p) { michael@0: p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: nsDeleteDir::DestroyThread() michael@0: { michael@0: if (!mThread) michael@0: return; michael@0: michael@0: if (mTimers.Count()) michael@0: // more work to do, so don't delete thread. michael@0: return; michael@0: michael@0: nsShutdownThread::Shutdown(mThread); michael@0: mThread = nullptr; michael@0: } michael@0: michael@0: void michael@0: nsDeleteDir::TimerCallback(nsITimer *aTimer, void *arg) michael@0: { michael@0: Telemetry::AutoTimer timer; michael@0: { michael@0: MutexAutoLock lock(gInstance->mLock); michael@0: michael@0: int32_t idx = gInstance->mTimers.IndexOf(aTimer); michael@0: if (idx == -1) { michael@0: // Timer was canceled and removed during shutdown. michael@0: return; michael@0: } michael@0: michael@0: gInstance->mTimers.RemoveObjectAt(idx); michael@0: } michael@0: michael@0: nsAutoPtr > dirList; michael@0: dirList = static_cast *>(arg); michael@0: michael@0: bool shuttingDown = false; michael@0: michael@0: // Intentional extra braces to control variable sope. michael@0: { michael@0: // Low IO priority can only be set when running in the context of the michael@0: // current thread. So this shouldn't be moved to where we set the priority michael@0: // of the Cache deleter thread using the nsThread's NSPR priority constants. michael@0: nsAutoLowPriorityIO autoLowPriority; michael@0: for (int32_t i = 0; i < dirList->Count() && !shuttingDown; i++) { michael@0: gInstance->RemoveDir((*dirList)[i], &shuttingDown); michael@0: } michael@0: } michael@0: michael@0: { michael@0: MutexAutoLock lock(gInstance->mLock); michael@0: gInstance->DestroyThread(); michael@0: } michael@0: } michael@0: michael@0: nsresult michael@0: nsDeleteDir::DeleteDir(nsIFile *dirIn, bool moveToTrash, uint32_t delay) michael@0: { michael@0: Telemetry::AutoTimer timer; michael@0: michael@0: if (!gInstance) michael@0: return NS_ERROR_NOT_INITIALIZED; michael@0: michael@0: nsresult rv; michael@0: nsCOMPtr trash, dir; michael@0: michael@0: // Need to make a clone of this since we don't want to modify the input michael@0: // file object. michael@0: rv = dirIn->Clone(getter_AddRefs(dir)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: if (moveToTrash) { michael@0: rv = GetTrashDir(dir, &trash); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: nsAutoCString origLeaf; michael@0: rv = trash->GetNativeLeafName(origLeaf); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: // Append random number to the trash directory and check if it exists. michael@0: srand(static_cast(PR_Now())); michael@0: nsAutoCString leaf; michael@0: for (int32_t i = 0; i < 10; i++) { michael@0: leaf = origLeaf; michael@0: leaf.AppendInt(rand()); michael@0: rv = trash->SetNativeLeafName(leaf); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: bool exists; michael@0: if (NS_SUCCEEDED(trash->Exists(&exists)) && !exists) { michael@0: break; michael@0: } michael@0: michael@0: leaf.Truncate(); michael@0: } michael@0: michael@0: // Fail if we didn't find unused trash directory within the limit michael@0: if (!leaf.Length()) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: #if defined(MOZ_WIDGET_ANDROID) michael@0: nsCOMPtr parent; michael@0: rv = trash->GetParent(getter_AddRefs(parent)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: rv = dir->MoveToNative(parent, leaf); michael@0: #else michael@0: // Important: must rename directory w/o changing parent directory: else on michael@0: // NTFS we'll wait (with cache lock) while nsIFile's ACL reset walks file michael@0: // tree: was hanging GUI for *minutes* on large cache dirs. michael@0: rv = dir->MoveToNative(nullptr, leaf); michael@0: #endif michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: } else { michael@0: // we want to pass a clone of the original off to the worker thread. michael@0: trash.swap(dir); michael@0: } michael@0: michael@0: nsAutoPtr > arg(new nsCOMArray); michael@0: arg->AppendObject(trash); michael@0: michael@0: rv = gInstance->PostTimer(arg, delay); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: arg.forget(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDeleteDir::GetTrashDir(nsIFile *target, nsCOMPtr *result) michael@0: { michael@0: nsresult rv; michael@0: #if defined(MOZ_WIDGET_ANDROID) michael@0: // Try to use the app cache folder for cache trash on Android michael@0: char* cachePath = getenv("CACHE_DIRECTORY"); michael@0: if (cachePath) { michael@0: rv = NS_NewNativeLocalFile(nsDependentCString(cachePath), michael@0: true, getter_AddRefs(*result)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: // Add a sub folder with the cache folder name michael@0: nsAutoCString leaf; michael@0: rv = target->GetNativeLeafName(leaf); michael@0: (*result)->AppendNative(leaf); michael@0: } else michael@0: #endif michael@0: { michael@0: rv = target->Clone(getter_AddRefs(*result)); michael@0: } michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: nsAutoCString leaf; michael@0: rv = (*result)->GetNativeLeafName(leaf); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: leaf.AppendLiteral(".Trash"); michael@0: michael@0: return (*result)->SetNativeLeafName(leaf); michael@0: } michael@0: michael@0: nsresult michael@0: nsDeleteDir::RemoveOldTrashes(nsIFile *cacheDir) michael@0: { michael@0: if (!gInstance) michael@0: return NS_ERROR_NOT_INITIALIZED; michael@0: michael@0: nsresult rv; michael@0: michael@0: static bool firstRun = true; michael@0: michael@0: if (!firstRun) michael@0: return NS_OK; michael@0: michael@0: firstRun = false; michael@0: michael@0: nsCOMPtr trash; michael@0: rv = GetTrashDir(cacheDir, &trash); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: nsAutoString trashName; michael@0: rv = trash->GetLeafName(trashName); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: nsCOMPtr parent; michael@0: #if defined(MOZ_WIDGET_ANDROID) michael@0: rv = trash->GetParent(getter_AddRefs(parent)); michael@0: #else michael@0: rv = cacheDir->GetParent(getter_AddRefs(parent)); michael@0: #endif michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: nsCOMPtr iter; michael@0: rv = parent->GetDirectoryEntries(getter_AddRefs(iter)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: bool more; michael@0: nsCOMPtr elem; michael@0: nsAutoPtr > dirList; michael@0: michael@0: while (NS_SUCCEEDED(iter->HasMoreElements(&more)) && more) { michael@0: rv = iter->GetNext(getter_AddRefs(elem)); michael@0: if (NS_FAILED(rv)) michael@0: continue; michael@0: michael@0: nsCOMPtr file = do_QueryInterface(elem); michael@0: if (!file) michael@0: continue; michael@0: michael@0: nsAutoString leafName; michael@0: rv = file->GetLeafName(leafName); michael@0: if (NS_FAILED(rv)) michael@0: continue; michael@0: michael@0: // match all names that begin with the trash name (i.e. "Cache.Trash") michael@0: if (Substring(leafName, 0, trashName.Length()).Equals(trashName)) { michael@0: if (!dirList) michael@0: dirList = new nsCOMArray; michael@0: dirList->AppendObject(file); michael@0: } michael@0: } michael@0: michael@0: if (dirList) { michael@0: rv = gInstance->PostTimer(dirList, 90000); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: dirList.forget(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDeleteDir::PostTimer(void *arg, uint32_t delay) michael@0: { michael@0: nsresult rv; michael@0: michael@0: nsCOMPtr timer = do_CreateInstance("@mozilla.org/timer;1", &rv); michael@0: if (NS_FAILED(rv)) michael@0: return NS_ERROR_UNEXPECTED; michael@0: michael@0: MutexAutoLock lock(mLock); michael@0: michael@0: rv = InitThread(); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: rv = timer->SetTarget(mThread); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: rv = timer->InitWithFuncCallback(TimerCallback, arg, delay, michael@0: nsITimer::TYPE_ONE_SHOT); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: mTimers.AppendObject(timer); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDeleteDir::RemoveDir(nsIFile *file, bool *stopDeleting) michael@0: { michael@0: nsresult rv; michael@0: bool isLink; michael@0: michael@0: rv = file->IsSymlink(&isLink); michael@0: if (NS_FAILED(rv) || isLink) michael@0: return NS_ERROR_UNEXPECTED; michael@0: michael@0: bool isDir; michael@0: rv = file->IsDirectory(&isDir); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: if (isDir) { michael@0: nsCOMPtr iter; michael@0: rv = file->GetDirectoryEntries(getter_AddRefs(iter)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: bool more; michael@0: nsCOMPtr elem; michael@0: while (NS_SUCCEEDED(iter->HasMoreElements(&more)) && more) { michael@0: rv = iter->GetNext(getter_AddRefs(elem)); michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Unexpected failure in nsDeleteDir::RemoveDir"); michael@0: continue; michael@0: } michael@0: michael@0: nsCOMPtr file2 = do_QueryInterface(elem); michael@0: if (!file2) { michael@0: NS_WARNING("Unexpected failure in nsDeleteDir::RemoveDir"); michael@0: continue; michael@0: } michael@0: michael@0: RemoveDir(file2, stopDeleting); michael@0: // No check for errors to remove as much as possible michael@0: michael@0: if (*stopDeleting) michael@0: return NS_OK; michael@0: } michael@0: } michael@0: michael@0: file->Remove(false); michael@0: // No check for errors to remove as much as possible michael@0: michael@0: MutexAutoLock lock(mLock); michael@0: if (mStopDeleting) michael@0: *stopDeleting = true; michael@0: michael@0: return NS_OK; michael@0: }