/* -*- 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 "ScriptPreloader-inl.h" #include "mozilla/URLPreloader.h" #include "mozilla/loader/AutoMemMap.h" #include "mozilla/ArrayUtils.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/FileUtils.h" #include "mozilla/IOBuffers.h" #include "mozilla/Logging.h" #include "mozilla/ScopeExit.h" #include "mozilla/Services.h" #include "mozilla/Unused.h" #include "mozilla/Vector.h" #include "MainThreadUtils.h" #include "nsPrintfCString.h" #include "nsDebug.h" #include "nsIFile.h" #include "nsIFileURL.h" #include "nsNetUtil.h" #include "nsPromiseFlatString.h" #include "nsProxyRelease.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" #include "nsZipArchive.h" #include "xpcpublic.h" namespace mozilla { namespace { static LazyLogModule gURLLog("URLPreloader"); #define LOG(level, ...) MOZ_LOG(gURLLog, LogLevel::level, (__VA_ARGS__)) template bool StartsWith(const T& haystack, const T& needle) { return StringHead(haystack, needle.Length()) == needle; } } // anonymous namespace using namespace mozilla::loader; nsresult URLPreloader::CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, bool aAnonymize) { MOZ_COLLECT_REPORT("explicit/url-preloader/other", KIND_HEAP, UNITS_BYTES, ShallowSizeOfIncludingThis(MallocSizeOf), "Memory used by the URL preloader service itself."); for (const auto& elem : mCachedURLs.Values()) { nsAutoCString pathName; pathName.Append(elem->mPath); // The backslashes will automatically be replaced with slashes in // about:memory, without splitting each path component into a separate // branch in the memory report tree. pathName.ReplaceChar('/', '\\'); nsPrintfCString path("explicit/url-preloader/cached-urls/%s/[%s]", elem->TypeString(), pathName.get()); aHandleReport->Callback( ""_ns, path, KIND_HEAP, UNITS_BYTES, elem->SizeOfIncludingThis(MallocSizeOf), nsLiteralCString("Memory used to hold cache data for files which " "have been read or pre-loaded during this session."), aData); } return NS_OK; } // static already_AddRefed URLPreloader::Create(bool* aInitialized) { // The static APIs like URLPreloader::Read work in the child process because // they fall back to a synchronous read. The actual preloader must be // explicitly initialized, and this should only be done in the parent. MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); RefPtr preloader = new URLPreloader(); if (preloader->InitInternal().isOk()) { *aInitialized = true; RegisterWeakMemoryReporter(preloader); } else { *aInitialized = false; } return preloader.forget(); } URLPreloader& URLPreloader::GetSingleton() { if (!sSingleton) { sSingleton = Create(&sInitialized); ClearOnShutdown(&sSingleton); } return *sSingleton; } bool URLPreloader::sInitialized = false; StaticRefPtr URLPreloader::sSingleton; URLPreloader::~URLPreloader() { if (sInitialized) { UnregisterWeakMemoryReporter(this); sInitialized = false; } } Result URLPreloader::InitInternal() { MOZ_RELEASE_ASSERT(NS_IsMainThread()); if (Omnijar::HasOmnijar(Omnijar::GRE)) { MOZ_TRY(Omnijar::GetURIString(Omnijar::GRE, mGREPrefix)); } if (Omnijar::HasOmnijar(Omnijar::APP)) { MOZ_TRY(Omnijar::GetURIString(Omnijar::APP, mAppPrefix)); } nsresult rv; nsCOMPtr ios = do_GetIOService(&rv); MOZ_TRY(rv); nsCOMPtr ph; MOZ_TRY(ios->GetProtocolHandler("resource", getter_AddRefs(ph))); mResProto = do_QueryInterface(ph, &rv); MOZ_TRY(rv); mChromeReg = services::GetChromeRegistry(); if (!mChromeReg) { return Err(NS_ERROR_UNEXPECTED); } MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD))); return Ok(); } URLPreloader& URLPreloader::ReInitialize() { MOZ_ASSERT(sSingleton); sSingleton = nullptr; sSingleton = Create(&sInitialized); return *sSingleton; } Result, nsresult> URLPreloader::GetCacheFile( const nsAString& suffix) { if (!mProfD) { return Err(NS_ERROR_NOT_INITIALIZED); } nsCOMPtr cacheFile; MOZ_TRY(mProfD->Clone(getter_AddRefs(cacheFile))); MOZ_TRY(cacheFile->AppendNative("startupCache"_ns)); Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777); MOZ_TRY(cacheFile->Append(u"urlCache"_ns + suffix)); return std::move(cacheFile); } static const uint8_t URL_MAGIC[] = "mozURLcachev002"; Result, nsresult> URLPreloader::FindCacheFile() { nsCOMPtr cacheFile; MOZ_TRY_VAR(cacheFile, GetCacheFile(u".bin"_ns)); bool exists; MOZ_TRY(cacheFile->Exists(&exists)); if (exists) { MOZ_TRY(cacheFile->MoveTo(nullptr, u"urlCache-current.bin"_ns)); } else { MOZ_TRY(cacheFile->SetLeafName(u"urlCache-current.bin"_ns)); MOZ_TRY(cacheFile->Exists(&exists)); if (!exists) { return Err(NS_ERROR_FILE_NOT_FOUND); } } return std::move(cacheFile); } Result URLPreloader::WriteCache() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_DIAGNOSTIC_ASSERT(mStartupFinished); // The script preloader might call us a second time, if it has to re-write // its cache after a cache flush. We don't care about cache flushes, since // our cache doesn't store any file data, only paths. And we currently clear // our cached file list after the first write, which means that a second // write would (aside from breaking the invariant that we never touch // mCachedURLs off-main-thread after the first write, and trigger a data // race) mean we get no pre-loading on the next startup. if (mCacheWritten) { return Ok(); } mCacheWritten = true; LOG(Debug, "Writing cache..."); nsCOMPtr cacheFile; MOZ_TRY_VAR(cacheFile, GetCacheFile(u"-new.bin"_ns)); bool exists; MOZ_TRY(cacheFile->Exists(&exists)); if (exists) { MOZ_TRY(cacheFile->Remove(false)); } { AutoFDClose fd; MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, &fd.rwget())); nsTArray entries; for (const auto& entry : mCachedURLs.Values()) { if (entry->mReadTime) { entries.AppendElement(entry.get()); } } entries.Sort(URLEntry::Comparator()); OutputBuffer buf; for (auto entry : entries) { entry->Code(buf); } uint8_t headerSize[4]; LittleEndian::writeUint32(headerSize, buf.cursor()); MOZ_TRY(Write(fd, URL_MAGIC, sizeof(URL_MAGIC))); MOZ_TRY(Write(fd, headerSize, sizeof(headerSize))); MOZ_TRY(Write(fd, buf.Get(), buf.cursor())); } MOZ_TRY(cacheFile->MoveTo(nullptr, u"urlCache.bin"_ns)); NS_DispatchToMainThread( NewRunnableMethod("URLPreloader::Cleanup", this, &URLPreloader::Cleanup)); return Ok(); } void URLPreloader::Cleanup() { mCachedURLs.Clear(); } Result URLPreloader::ReadCache( LinkedList& pendingURLs) { LOG(Debug, "Reading cache..."); nsCOMPtr cacheFile; MOZ_TRY_VAR(cacheFile, FindCacheFile()); AutoMemMap cache; MOZ_TRY(cache.init(cacheFile)); auto size = cache.size(); uint32_t headerSize; if (size < sizeof(URL_MAGIC) + sizeof(headerSize)) { return Err(NS_ERROR_UNEXPECTED); } auto data = cache.get(); auto end = data + size; if (memcmp(URL_MAGIC, data.get(), sizeof(URL_MAGIC))) { return Err(NS_ERROR_UNEXPECTED); } data += sizeof(URL_MAGIC); headerSize = LittleEndian::readUint32(data.get()); data += sizeof(headerSize); if (data + headerSize > end) { return Err(NS_ERROR_UNEXPECTED); } { mMonitor.AssertCurrentThreadOwns(); auto cleanup = MakeScopeExit([&]() { while (auto* elem = pendingURLs.getFirst()) { elem->remove(); } mCachedURLs.Clear(); }); Range header(data, data + headerSize); data += headerSize; InputBuffer buf(header); while (!buf.finished()) { CacheKey key(buf); LOG(Debug, "Cached file: %s %s", key.TypeString(), key.mPath.get()); // Don't bother doing anything else if the key didn't load correctly. // We're going to throw it out right away, and it is possible that this // leads to pendingURLs getting into a weird state. if (buf.error()) { return Err(NS_ERROR_UNEXPECTED); } auto entry = mCachedURLs.GetOrInsertNew(key, key); entry->mResultCode = NS_ERROR_NOT_INITIALIZED; if (entry->isInList()) { #ifdef NIGHTLY_BUILD MOZ_DIAGNOSTIC_ASSERT(pendingURLs.contains(entry), "Entry should be in pendingURLs"); MOZ_DIAGNOSTIC_ASSERT(key.mPath.Length() > 0, "Path should be non-empty"); MOZ_DIAGNOSTIC_ASSERT(false, "Entry should be new and not in any list"); #endif return Err(NS_ERROR_UNEXPECTED); } pendingURLs.insertBack(entry); } MOZ_RELEASE_ASSERT(!buf.error(), "We should have already bailed on an error"); cleanup.release(); } return Ok(); } void URLPreloader::BackgroundReadFiles() { auto cleanup = MakeScopeExit([&]() { auto lock = mReaderThread.Lock(); auto& readerThread = lock.ref(); NS_DispatchToMainThread(NewRunnableMethod( "nsIThread::AsyncShutdown", readerThread, &nsIThread::AsyncShutdown)); readerThread = nullptr; }); Vector cursors; LinkedList pendingURLs; { MonitorAutoLock mal(mMonitor); if (ReadCache(pendingURLs).isErr()) { mReaderInitialized = true; mal.NotifyAll(); return; } int numZipEntries = 0; for (auto entry : pendingURLs) { if (entry->mType != entry->TypeFile) { numZipEntries++; } } MOZ_RELEASE_ASSERT(cursors.reserve(numZipEntries)); // Initialize the zip cursors for all files in Omnijar while the monitor // is locked. Omnijar is not threadsafe, so the caller of // AutoBeginReading guard must ensure that no code accesses Omnijar // until this segment is done. Once the cursors have been initialized, // the actual reading and decompression can safely be done off-thread, // as is the case for thread-retargeted jar: channels. for (auto entry : pendingURLs) { if (entry->mType == entry->TypeFile) { continue; } RefPtr zip = entry->Archive(); if (!zip) { MOZ_CRASH_UNSAFE_PRINTF( "Failed to get Omnijar %s archive for entry (path: \"%s\")", entry->TypeString(), entry->mPath.get()); } auto item = zip->GetItem(entry->mPath.get()); if (!item) { entry->mResultCode = NS_ERROR_FILE_NOT_FOUND; continue; } size_t size = item->RealSize(); entry->mData.SetLength(size); auto data = entry->mData.BeginWriting(); cursors.infallibleEmplaceBack(item, zip, reinterpret_cast(data), size, true); } mReaderInitialized = true; mal.NotifyAll(); } // Loop over the entries, read the file's contents, store them in the // entry's mData pointer, and notify any waiting threads to check for // completion. uint32_t i = 0; for (auto entry : pendingURLs) { // If there is any other error code, the entry has already failed at // this point, so don't bother trying to read it again. if (entry->mResultCode != NS_ERROR_NOT_INITIALIZED) { continue; } nsresult rv = NS_OK; LOG(Debug, "Background reading %s file %s", entry->TypeString(), entry->mPath.get()); if (entry->mType == entry->TypeFile) { auto result = entry->Read(); if (result.isErr()) { rv = result.unwrapErr(); } } else { auto& cursor = cursors[i++]; uint32_t len; cursor.Copy(&len); if (len != entry->mData.Length()) { entry->mData.Truncate(); rv = NS_ERROR_FAILURE; } } entry->mResultCode = rv; mMonitor.NotifyAll(); } // We're done reading pending entries, so clear the list. pendingURLs.clear(); } void URLPreloader::BeginBackgroundRead() { auto lock = mReaderThread.Lock(); auto& readerThread = lock.ref(); if (!readerThread && !mReaderInitialized && sInitialized) { nsresult rv; rv = NS_NewNamedThread("BGReadURLs", getter_AddRefs(readerThread)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr runnable = NewRunnableMethod("URLPreloader::BackgroundReadFiles", this, &URLPreloader::BackgroundReadFiles); rv = readerThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { // If we can't launch the task, just destroy the thread readerThread = nullptr; return; } } } Result URLPreloader::ReadInternal(const CacheKey& key, ReadType readType) { if (mStartupFinished || !mReaderInitialized) { URLEntry entry(key); return entry.Read(); } auto entry = mCachedURLs.GetOrInsertNew(key, key); entry->UpdateUsedTime(); return entry->ReadOrWait(readType); } Result URLPreloader::ReadURIInternal(nsIURI* uri, ReadType readType) { CacheKey key; MOZ_TRY_VAR(key, ResolveURI(uri)); return ReadInternal(key, readType); } /* static */ Result URLPreloader::Read(const CacheKey& key, ReadType readType) { // If we're being called before the preloader has been initialized (i.e., // before the profile has been initialized), just fall back to a synchronous // read. This happens when we're reading .ini and preference files that are // needed to locate and initialize the profile. if (!sInitialized) { return URLEntry(key).Read(); } return GetSingleton().ReadInternal(key, readType); } /* static */ Result URLPreloader::ReadURI( nsIURI* uri, ReadType readType) { if (!sInitialized) { return Err(NS_ERROR_NOT_INITIALIZED); } return GetSingleton().ReadURIInternal(uri, readType); } /* static */ Result URLPreloader::ReadFile( nsIFile* file, ReadType readType) { return Read(CacheKey(file), readType); } /* static */ Result URLPreloader::Read( FileLocation& location, ReadType readType) { if (location.IsZip()) { if (location.GetBaseZip()) { nsCString path; location.GetPath(path); return ReadZip(location.GetBaseZip(), path); } return URLEntry::ReadLocation(location); } nsCOMPtr file = location.GetBaseFile(); return ReadFile(file, readType); } /* static */ Result URLPreloader::ReadZip( nsZipArchive* zip, const nsACString& path, ReadType readType) { // If the zip archive belongs to an Omnijar location, map it to a cache // entry, and cache it as normal. Otherwise, simply read the entry // synchronously, since other JAR archives are currently unsupported by the // cache. RefPtr reader = Omnijar::GetReader(Omnijar::GRE); if (zip == reader) { CacheKey key(CacheKey::TypeGREJar, path); return Read(key, readType); } reader = Omnijar::GetReader(Omnijar::APP); if (zip == reader) { CacheKey key(CacheKey::TypeAppJar, path); return Read(key, readType); } // Not an Omnijar archive, so just read it directly. FileLocation location(zip, PromiseFlatCString(path).BeginReading()); return URLEntry::ReadLocation(location); } Result URLPreloader::ResolveURI(nsIURI* uri) { nsCString spec; nsCString scheme; MOZ_TRY(uri->GetSpec(spec)); MOZ_TRY(uri->GetScheme(scheme)); nsCOMPtr resolved; // If the URI is a resource: or chrome: URI, first resolve it to the // underlying URI that it wraps. if (scheme.EqualsLiteral("resource")) { MOZ_TRY(mResProto->ResolveURI(uri, spec)); MOZ_TRY(NS_NewURI(getter_AddRefs(resolved), spec)); } else if (scheme.EqualsLiteral("chrome")) { MOZ_TRY(mChromeReg->ConvertChromeURL(uri, getter_AddRefs(resolved))); MOZ_TRY(resolved->GetSpec(spec)); } else { resolved = uri; } MOZ_TRY(resolved->GetScheme(scheme)); // Try the GRE and App Omnijar prefixes. if (mGREPrefix.Length() && StartsWith(spec, mGREPrefix)) { return CacheKey(CacheKey::TypeGREJar, Substring(spec, mGREPrefix.Length())); } if (mAppPrefix.Length() && StartsWith(spec, mAppPrefix)) { return CacheKey(CacheKey::TypeAppJar, Substring(spec, mAppPrefix.Length())); } // Try for a file URI. if (scheme.EqualsLiteral("file")) { nsCOMPtr fileURL = do_QueryInterface(resolved); MOZ_ASSERT(fileURL); nsCOMPtr file; MOZ_TRY(fileURL->GetFile(getter_AddRefs(file))); nsString path; MOZ_TRY(file->GetPath(path)); return CacheKey(CacheKey::TypeFile, NS_ConvertUTF16toUTF8(path)); } // Not a file or Omnijar URI, so currently unsupported. return Err(NS_ERROR_INVALID_ARG); } size_t URLPreloader::ShallowSizeOfIncludingThis( mozilla::MallocSizeOf mallocSizeOf) { return (mallocSizeOf(this) + mAppPrefix.SizeOfExcludingThisEvenIfShared(mallocSizeOf) + mGREPrefix.SizeOfExcludingThisEvenIfShared(mallocSizeOf) + mCachedURLs.ShallowSizeOfExcludingThis(mallocSizeOf)); } Result URLPreloader::CacheKey::ToFileLocation() { if (mType == TypeFile) { nsCOMPtr file; MOZ_TRY(NS_NewLocalFile(NS_ConvertUTF8toUTF16(mPath), false, getter_AddRefs(file))); return FileLocation(file); } RefPtr zip = Archive(); return FileLocation(zip, mPath.get()); } Result URLPreloader::URLEntry::Read() { FileLocation location; MOZ_TRY_VAR(location, ToFileLocation()); MOZ_TRY_VAR(mData, ReadLocation(location)); return mData; } /* static */ Result URLPreloader::URLEntry::ReadLocation( FileLocation& location) { FileLocation::Data data; MOZ_TRY(location.GetData(data)); uint32_t size; MOZ_TRY(data.GetSize(&size)); nsCString result; result.SetLength(size); MOZ_TRY(data.Copy(result.BeginWriting(), size)); return std::move(result); } Result URLPreloader::URLEntry::ReadOrWait( ReadType readType) { auto now = TimeStamp::Now(); LOG(Info, "Reading %s\n", mPath.get()); auto cleanup = MakeScopeExit([&]() { LOG(Info, "Read in %fms\n", (TimeStamp::Now() - now).ToMilliseconds()); }); if (mResultCode == NS_ERROR_NOT_INITIALIZED) { MonitorAutoLock mal(GetSingleton().mMonitor); while (mResultCode == NS_ERROR_NOT_INITIALIZED) { mal.Wait(); } } if (mResultCode == NS_OK && mData.IsVoid()) { LOG(Info, "Reading synchronously...\n"); return Read(); } if (NS_FAILED(mResultCode)) { return Err(mResultCode); } nsCString res = mData; if (readType == Forget) { mData.SetIsVoid(true); } return res; } inline URLPreloader::CacheKey::CacheKey(InputBuffer& buffer) { Code(buffer); MOZ_DIAGNOSTIC_ASSERT( mType == TypeAppJar || mType == TypeGREJar || mType == TypeFile, "mType should be valid"); } NS_IMPL_ISUPPORTS(URLPreloader, nsIMemoryReporter) #undef LOG } // namespace mozilla