/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * 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 "nsCache.h" #include "nsDiskCache.h" #include "nsDiskCacheDevice.h" #include "nsDiskCacheStreams.h" #include "nsCacheService.h" #include "mozilla/FileUtils.h" #include "nsThreadUtils.h" #include "mozilla/MemoryReporting.h" #include "mozilla/Telemetry.h" #include "mozilla/TimeStamp.h" #include #include "mozilla/VisualEventTracer.h" // we pick 16k as the max buffer size because that is the threshold above which // we are unable to store the data in the cache block files // see nsDiskCacheMap.[cpp,h] #define kMaxBufferSize (16 * 1024) // Assumptions: // - cache descriptors live for life of streams // - streams will only be used by FileTransport, // they will not be directly accessible to clients // - overlapped I/O is NOT supported /****************************************************************************** * nsDiskCacheInputStream *****************************************************************************/ class nsDiskCacheInputStream : public nsIInputStream { public: nsDiskCacheInputStream( nsDiskCacheStreamIO * parent, PRFileDesc * fileDesc, const char * buffer, uint32_t endOfStream); NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIINPUTSTREAM private: virtual ~nsDiskCacheInputStream(); nsDiskCacheStreamIO * mStreamIO; // backpointer to parent PRFileDesc * mFD; const char * mBuffer; uint32_t mStreamEnd; uint32_t mPos; // stream position bool mClosed; }; NS_IMPL_ISUPPORTS(nsDiskCacheInputStream, nsIInputStream) nsDiskCacheInputStream::nsDiskCacheInputStream( nsDiskCacheStreamIO * parent, PRFileDesc * fileDesc, const char * buffer, uint32_t endOfStream) : mStreamIO(parent) , mFD(fileDesc) , mBuffer(buffer) , mStreamEnd(endOfStream) , mPos(0) , mClosed(false) { NS_ADDREF(mStreamIO); mStreamIO->IncrementInputStreamCount(); } nsDiskCacheInputStream::~nsDiskCacheInputStream() { Close(); mStreamIO->DecrementInputStreamCount(); NS_RELEASE(mStreamIO); } NS_IMETHODIMP nsDiskCacheInputStream::Close() { if (!mClosed) { if (mFD) { (void) PR_Close(mFD); mFD = nullptr; } mClosed = true; } return NS_OK; } NS_IMETHODIMP nsDiskCacheInputStream::Available(uint64_t * bytesAvailable) { if (mClosed) return NS_BASE_STREAM_CLOSED; if (mStreamEnd < mPos) return NS_ERROR_UNEXPECTED; *bytesAvailable = mStreamEnd - mPos; return NS_OK; } NS_IMETHODIMP nsDiskCacheInputStream::Read(char * buffer, uint32_t count, uint32_t * bytesRead) { *bytesRead = 0; if (mClosed) { CACHE_LOG_DEBUG(("CACHE: nsDiskCacheInputStream::Read " "[stream=%p] stream was closed", this, buffer, count)); return NS_OK; } if (mPos == mStreamEnd) { CACHE_LOG_DEBUG(("CACHE: nsDiskCacheInputStream::Read " "[stream=%p] stream at end of file", this, buffer, count)); return NS_OK; } if (mPos > mStreamEnd) { CACHE_LOG_DEBUG(("CACHE: nsDiskCacheInputStream::Read " "[stream=%p] stream past end of file (!)", this, buffer, count)); return NS_ERROR_UNEXPECTED; } if (count > mStreamEnd - mPos) count = mStreamEnd - mPos; if (mFD) { // just read from file int32_t result = PR_Read(mFD, buffer, count); if (result < 0) { nsresult rv = NS_ErrorAccordingToNSPR(); CACHE_LOG_DEBUG(("CACHE: nsDiskCacheInputStream::Read PR_Read failed" "[stream=%p, rv=%d, NSPR error %s", this, int(rv), PR_ErrorToName(PR_GetError()))); return rv; } mPos += (uint32_t)result; *bytesRead = (uint32_t)result; } else if (mBuffer) { // read data from mBuffer memcpy(buffer, mBuffer + mPos, count); mPos += count; *bytesRead = count; } else { // no data source for input stream } CACHE_LOG_DEBUG(("CACHE: nsDiskCacheInputStream::Read " "[stream=%p, count=%ud, byteRead=%ud] ", this, unsigned(count), unsigned(*bytesRead))); return NS_OK; } NS_IMETHODIMP nsDiskCacheInputStream::ReadSegments(nsWriteSegmentFun writer, void * closure, uint32_t count, uint32_t * bytesRead) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsDiskCacheInputStream::IsNonBlocking(bool * nonBlocking) { *nonBlocking = false; return NS_OK; } /****************************************************************************** * nsDiskCacheStreamIO *****************************************************************************/ NS_IMPL_ISUPPORTS(nsDiskCacheStreamIO, nsIOutputStream) nsDiskCacheStreamIO::nsDiskCacheStreamIO(nsDiskCacheBinding * binding) : mBinding(binding) , mInStreamCount(0) , mFD(nullptr) , mStreamEnd(0) , mBufSize(0) , mBuffer(nullptr) , mOutputStreamIsOpen(false) { mDevice = (nsDiskCacheDevice *)mBinding->mCacheEntry->CacheDevice(); // acquire "death grip" on cache service nsCacheService *service = nsCacheService::GlobalInstance(); NS_ADDREF(service); } nsDiskCacheStreamIO::~nsDiskCacheStreamIO() { nsCacheService::AssertOwnsLock(); // Close the outputstream if (mBinding && mOutputStreamIsOpen) { (void)CloseOutputStream(); } // release "death grip" on cache service nsCacheService *service = nsCacheService::GlobalInstance(); NS_RELEASE(service); // assert streams closed NS_ASSERTION(!mOutputStreamIsOpen, "output stream still open"); NS_ASSERTION(mInStreamCount == 0, "input stream still open"); NS_ASSERTION(!mFD, "file descriptor not closed"); DeleteBuffer(); } // NOTE: called with service lock held nsresult nsDiskCacheStreamIO::GetInputStream(uint32_t offset, nsIInputStream ** inputStream) { NS_ENSURE_ARG_POINTER(inputStream); NS_ENSURE_TRUE(offset == 0, NS_ERROR_NOT_IMPLEMENTED); *inputStream = nullptr; if (!mBinding) return NS_ERROR_NOT_AVAILABLE; if (mOutputStreamIsOpen) { NS_WARNING("already have an output stream open"); return NS_ERROR_NOT_AVAILABLE; } nsresult rv; PRFileDesc * fd = nullptr; mStreamEnd = mBinding->mCacheEntry->DataSize(); if (mStreamEnd == 0) { // there's no data to read NS_ASSERTION(!mBinding->mRecord.DataLocationInitialized(), "storage allocated for zero data size"); } else if (mBinding->mRecord.DataFile() == 0) { // open file desc for data rv = OpenCacheFile(PR_RDONLY, &fd); if (NS_FAILED(rv)) return rv; // unable to open file NS_ASSERTION(fd, "cache stream lacking open file."); } else if (!mBuffer) { // read block file for data rv = ReadCacheBlocks(mStreamEnd); if (NS_FAILED(rv)) return rv; } // else, mBuffer already contains all of the data (left over from a // previous block-file read or write). NS_ASSERTION(!(fd && mBuffer), "ambiguous data sources for input stream"); // create a new input stream nsDiskCacheInputStream * inStream = new nsDiskCacheInputStream(this, fd, mBuffer, mStreamEnd); if (!inStream) return NS_ERROR_OUT_OF_MEMORY; NS_ADDREF(*inputStream = inStream); return NS_OK; } // NOTE: called with service lock held nsresult nsDiskCacheStreamIO::GetOutputStream(uint32_t offset, nsIOutputStream ** outputStream) { NS_ENSURE_ARG_POINTER(outputStream); *outputStream = nullptr; if (!mBinding) return NS_ERROR_NOT_AVAILABLE; NS_ASSERTION(!mOutputStreamIsOpen, "already have an output stream open"); NS_ASSERTION(mInStreamCount == 0, "we already have input streams open"); if (mOutputStreamIsOpen || mInStreamCount) return NS_ERROR_NOT_AVAILABLE; mStreamEnd = mBinding->mCacheEntry->DataSize(); // Inits file or buffer and truncate at the desired offset nsresult rv = SeekAndTruncate(offset); if (NS_FAILED(rv)) return rv; mOutputStreamIsOpen = true; NS_ADDREF(*outputStream = this); return NS_OK; } nsresult nsDiskCacheStreamIO::ClearBinding() { nsresult rv = NS_OK; if (mBinding && mOutputStreamIsOpen) rv = CloseOutputStream(); mBinding = nullptr; return rv; } NS_IMETHODIMP nsDiskCacheStreamIO::Close() { if (!mOutputStreamIsOpen) return NS_OK; mozilla::TimeStamp start = mozilla::TimeStamp::Now(); // grab service lock nsCacheServiceAutoLock lock(LOCK_TELEM(NSDISKCACHESTREAMIO_CLOSEOUTPUTSTREAM)); if (!mBinding) { // if we're severed, just clear member variables mOutputStreamIsOpen = false; return NS_ERROR_NOT_AVAILABLE; } nsresult rv = CloseOutputStream(); if (NS_FAILED(rv)) NS_WARNING("CloseOutputStream() failed"); mozilla::Telemetry::ID id; if (NS_IsMainThread()) id = mozilla::Telemetry::NETWORK_DISK_CACHE_STREAMIO_CLOSE_MAIN_THREAD; else id = mozilla::Telemetry::NETWORK_DISK_CACHE_STREAMIO_CLOSE; mozilla::Telemetry::AccumulateTimeDelta(id, start); return rv; } nsresult nsDiskCacheStreamIO::CloseOutputStream() { NS_ASSERTION(mBinding, "oops"); CACHE_LOG_DEBUG(("CACHE: CloseOutputStream [%x doomed=%u]\n", mBinding->mRecord.HashNumber(), mBinding->mDoomed)); // Mark outputstream as closed, even if saving the stream fails mOutputStreamIsOpen = false; // When writing to a file, just close the file if (mFD) { (void) PR_Close(mFD); mFD = nullptr; return NS_OK; } // write data to cache blocks, or flush mBuffer to file NS_ASSERTION(mStreamEnd <= kMaxBufferSize, "stream is bigger than buffer"); nsDiskCacheMap *cacheMap = mDevice->CacheMap(); // get map reference nsDiskCacheRecord * record = &mBinding->mRecord; nsresult rv = NS_OK; // delete existing storage if (record->DataLocationInitialized()) { rv = cacheMap->DeleteStorage(record, nsDiskCache::kData); NS_ENSURE_SUCCESS(rv, rv); // Only call UpdateRecord when there is no data to write, // because WriteDataCacheBlocks / FlushBufferToFile calls it. if ((mStreamEnd == 0) && (!mBinding->mDoomed)) { rv = cacheMap->UpdateRecord(record); if (NS_FAILED(rv)) { NS_WARNING("cacheMap->UpdateRecord() failed."); return rv; // XXX doom cache entry } } } if (mStreamEnd == 0) return NS_OK; // nothing to write // try to write to the cache blocks rv = cacheMap->WriteDataCacheBlocks(mBinding, mBuffer, mStreamEnd); if (NS_FAILED(rv)) { NS_WARNING("WriteDataCacheBlocks() failed."); // failed to store in cacheblocks, save as separate file rv = FlushBufferToFile(); // initializes DataFileLocation() if necessary if (mFD) { UpdateFileSize(); (void) PR_Close(mFD); mFD = nullptr; } else NS_WARNING("no file descriptor"); } return rv; } // assumptions: // only one thread writing at a time // never have both output and input streams open // OnDataSizeChanged() will have already been called to update entry->DataSize() NS_IMETHODIMP nsDiskCacheStreamIO::Write( const char * buffer, uint32_t count, uint32_t * bytesWritten) { NS_ENSURE_ARG_POINTER(buffer); NS_ENSURE_ARG_POINTER(bytesWritten); if (!mOutputStreamIsOpen) return NS_BASE_STREAM_CLOSED; *bytesWritten = 0; // always initialize to zero in case of errors NS_ASSERTION(count, "Write called with count of zero"); if (count == 0) { return NS_OK; // nothing to write } // grab service lock nsCacheServiceAutoLock lock(LOCK_TELEM(NSDISKCACHESTREAMIO_WRITE)); if (!mBinding) return NS_ERROR_NOT_AVAILABLE; if (mInStreamCount) { // we have open input streams already // this is an error until we support overlapped I/O NS_WARNING("Attempting to write to cache entry with open input streams.\n"); return NS_ERROR_NOT_AVAILABLE; } // Not writing to file, and it will fit in the cachedatablocks? if (!mFD && (mStreamEnd + count <= kMaxBufferSize)) { // We have more data than the current buffer size? if ((mStreamEnd + count > mBufSize) && (mBufSize < kMaxBufferSize)) { // Increase buffer to the maximum size. mBuffer = (char *) moz_xrealloc(mBuffer, kMaxBufferSize); mBufSize = kMaxBufferSize; } // Store in the buffer but only if it fits if (mStreamEnd + count <= mBufSize) { memcpy(mBuffer + mStreamEnd, buffer, count); mStreamEnd += count; *bytesWritten = count; return NS_OK; } } // There are more bytes than fit in the buffer/cacheblocks, switch to file if (!mFD) { // Opens a cache file and write the buffer to it nsresult rv = FlushBufferToFile(); if (NS_FAILED(rv)) { return rv; } } // Write directly to the file if (PR_Write(mFD, buffer, count) != (int32_t)count) { NS_WARNING("failed to write all data"); return NS_ERROR_UNEXPECTED; // NS_ErrorAccordingToNSPR() } mStreamEnd += count; *bytesWritten = count; UpdateFileSize(); NS_ASSERTION(mBinding->mCacheEntry->DataSize() == mStreamEnd, "bad stream"); return NS_OK; } void nsDiskCacheStreamIO::UpdateFileSize() { NS_ASSERTION(mFD, "nsDiskCacheStreamIO::UpdateFileSize should not have been called"); nsDiskCacheRecord * record = &mBinding->mRecord; const uint32_t oldSizeK = record->DataFileSize(); uint32_t newSizeK = (mStreamEnd + 0x03FF) >> 10; // make sure the size won't overflow (bug #651100) if (newSizeK > kMaxDataSizeK) newSizeK = kMaxDataSizeK; if (newSizeK == oldSizeK) return; record->SetDataFileSize(newSizeK); // update cache size totals nsDiskCacheMap * cacheMap = mDevice->CacheMap(); cacheMap->DecrementTotalSize(oldSizeK); // decrement old size cacheMap->IncrementTotalSize(newSizeK); // increment new size if (!mBinding->mDoomed) { nsresult rv = cacheMap->UpdateRecord(record); if (NS_FAILED(rv)) { NS_WARNING("cacheMap->UpdateRecord() failed."); // XXX doom cache entry? } } } nsresult nsDiskCacheStreamIO::OpenCacheFile(int flags, PRFileDesc ** fd) { NS_ENSURE_ARG_POINTER(fd); CACHE_LOG_DEBUG(("nsDiskCacheStreamIO::OpenCacheFile")); nsresult rv; nsDiskCacheMap * cacheMap = mDevice->CacheMap(); nsCOMPtr localFile; rv = cacheMap->GetLocalFileForDiskCacheRecord(&mBinding->mRecord, nsDiskCache::kData, !!(flags & PR_CREATE_FILE), getter_AddRefs(localFile)); if (NS_FAILED(rv)) return rv; // create PRFileDesc for input stream - the 00600 is just for consistency return localFile->OpenNSPRFileDesc(flags, 00600, fd); } nsresult nsDiskCacheStreamIO::ReadCacheBlocks(uint32_t bufferSize) { mozilla::eventtracer::AutoEventTracer readCacheBlocks( mBinding->mCacheEntry, mozilla::eventtracer::eExec, mozilla::eventtracer::eDone, "net::cache::ReadCacheBlocks"); NS_ASSERTION(mStreamEnd == mBinding->mCacheEntry->DataSize(), "bad stream"); NS_ASSERTION(bufferSize <= kMaxBufferSize, "bufferSize too large for buffer"); NS_ASSERTION(mStreamEnd <= bufferSize, "data too large for buffer"); nsDiskCacheRecord * record = &mBinding->mRecord; if (!record->DataLocationInitialized()) return NS_OK; NS_ASSERTION(record->DataFile() != kSeparateFile, "attempt to read cache blocks on separate file"); if (!mBuffer) { mBuffer = (char *) moz_xmalloc(bufferSize); mBufSize = bufferSize; } // read data stored in cache block files nsDiskCacheMap *map = mDevice->CacheMap(); // get map reference return map->ReadDataCacheBlocks(mBinding, mBuffer, mStreamEnd); } nsresult nsDiskCacheStreamIO::FlushBufferToFile() { mozilla::eventtracer::AutoEventTracer flushBufferToFile( mBinding->mCacheEntry, mozilla::eventtracer::eExec, mozilla::eventtracer::eDone, "net::cache::FlushBufferToFile"); nsresult rv; nsDiskCacheRecord * record = &mBinding->mRecord; if (!mFD) { if (record->DataLocationInitialized() && (record->DataFile() > 0)) { // remove cache block storage nsDiskCacheMap * cacheMap = mDevice->CacheMap(); rv = cacheMap->DeleteStorage(record, nsDiskCache::kData); if (NS_FAILED(rv)) return rv; } record->SetDataFileGeneration(mBinding->mGeneration); // allocate file rv = OpenCacheFile(PR_RDWR | PR_CREATE_FILE, &mFD); if (NS_FAILED(rv)) return rv; int64_t dataSize = mBinding->mCacheEntry->PredictedDataSize(); if (dataSize != -1) mozilla::fallocate(mFD, std::min(dataSize, kPreallocateLimit)); } // write buffer to the file when there is data in it if (mStreamEnd > 0) { if (!mBuffer) { NS_RUNTIMEABORT("Fix me!"); } if (PR_Write(mFD, mBuffer, mStreamEnd) != (int32_t)mStreamEnd) { NS_WARNING("failed to flush all data"); return NS_ERROR_UNEXPECTED; // NS_ErrorAccordingToNSPR() } } // buffer is no longer valid DeleteBuffer(); return NS_OK; } void nsDiskCacheStreamIO::DeleteBuffer() { if (mBuffer) { free(mBuffer); mBuffer = nullptr; mBufSize = 0; } } size_t nsDiskCacheStreamIO::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) { size_t usage = aMallocSizeOf(this); usage += aMallocSizeOf(mFD); usage += aMallocSizeOf(mBuffer); return usage; } nsresult nsDiskCacheStreamIO::SeekAndTruncate(uint32_t offset) { if (!mBinding) return NS_ERROR_NOT_AVAILABLE; if (uint32_t(offset) > mStreamEnd) return NS_ERROR_FAILURE; // Set the current end to the desired offset mStreamEnd = offset; // Currently stored in file? if (mBinding->mRecord.DataLocationInitialized() && (mBinding->mRecord.DataFile() == 0)) { if (!mFD) { // we need an mFD, we better open it now nsresult rv = OpenCacheFile(PR_RDWR | PR_CREATE_FILE, &mFD); if (NS_FAILED(rv)) return rv; } if (offset) { if (PR_Seek(mFD, offset, PR_SEEK_SET) == -1) return NS_ErrorAccordingToNSPR(); } nsDiskCache::Truncate(mFD, offset); UpdateFileSize(); // When we starting at zero again, close file and start with buffer. // If offset is non-zero (and within buffer) an option would be // to read the file into the buffer, but chance is high that it is // rewritten to the file anyway. if (offset == 0) { // close file descriptor (void) PR_Close(mFD); mFD = nullptr; } return NS_OK; } // read data into mBuffer if not read yet. if (offset && !mBuffer) { nsresult rv = ReadCacheBlocks(kMaxBufferSize); if (NS_FAILED(rv)) return rv; } // stream buffer sanity check NS_ASSERTION(mStreamEnd <= kMaxBufferSize, "bad stream"); return NS_OK; } NS_IMETHODIMP nsDiskCacheStreamIO::Flush() { if (!mOutputStreamIsOpen) return NS_BASE_STREAM_CLOSED; return NS_OK; } NS_IMETHODIMP nsDiskCacheStreamIO::WriteFrom(nsIInputStream *inStream, uint32_t count, uint32_t *bytesWritten) { NS_NOTREACHED("WriteFrom"); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsDiskCacheStreamIO::WriteSegments( nsReadSegmentFun reader, void * closure, uint32_t count, uint32_t * bytesWritten) { NS_NOTREACHED("WriteSegments"); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsDiskCacheStreamIO::IsNonBlocking(bool * nonBlocking) { *nonBlocking = false; return NS_OK; }