| /* |
| * Copyright (C) 2010 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "core/fileapi/FileReader.h" |
| |
| #include "bindings/core/v8/ExceptionState.h" |
| #include "bindings/core/v8/StringOrArrayBuffer.h" |
| #include "core/dom/DOMArrayBuffer.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/ExceptionCode.h" |
| #include "core/dom/ExecutionContext.h" |
| #include "core/dom/ExecutionContextTask.h" |
| #include "core/events/ProgressEvent.h" |
| #include "core/fileapi/File.h" |
| #include "core/inspector/InspectorInstrumentation.h" |
| #include "platform/Supplementable.h" |
| #include "wtf/AutoReset.h" |
| #include "wtf/CurrentTime.h" |
| #include "wtf/Deque.h" |
| #include "wtf/HashSet.h" |
| #include "wtf/text/CString.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| const CString utf8BlobUUID(Blob* blob) { |
| return blob->uuid().utf8(); |
| } |
| |
| const CString utf8FilePath(Blob* blob) { |
| return blob->hasBackingFile() ? toFile(blob)->path().utf8() : ""; |
| } |
| |
| } // namespace |
| |
| // Embedders like chromium limit the number of simultaneous requests to avoid |
| // excessive IPC congestion. We limit this to 100 per thread to throttle the |
| // requests (the value is arbitrarily chosen). |
| static const size_t kMaxOutstandingRequestsPerThread = 100; |
| static const double progressNotificationIntervalMS = 50; |
| |
| class FileReader::ThrottlingController final |
| : public GarbageCollected<FileReader::ThrottlingController>, |
| public Supplement<ExecutionContext> { |
| USING_GARBAGE_COLLECTED_MIXIN(FileReader::ThrottlingController); |
| |
| public: |
| static ThrottlingController* from(ExecutionContext* context) { |
| if (!context) |
| return 0; |
| |
| ThrottlingController* controller = static_cast<ThrottlingController*>( |
| Supplement<ExecutionContext>::from(*context, supplementName())); |
| if (!controller) { |
| controller = new ThrottlingController; |
| provideTo(*context, supplementName(), controller); |
| } |
| return controller; |
| } |
| |
| enum FinishReaderType { DoNotRunPendingReaders, RunPendingReaders }; |
| |
| static void pushReader(ExecutionContext* context, FileReader* reader) { |
| ThrottlingController* controller = from(context); |
| if (!controller) |
| return; |
| |
| InspectorInstrumentation::asyncTaskScheduled(context, "FileReader", reader, |
| true); |
| controller->pushReader(reader); |
| } |
| |
| static FinishReaderType removeReader(ExecutionContext* context, |
| FileReader* reader) { |
| ThrottlingController* controller = from(context); |
| if (!controller) |
| return DoNotRunPendingReaders; |
| |
| return controller->removeReader(reader); |
| } |
| |
| static void finishReader(ExecutionContext* context, |
| FileReader* reader, |
| FinishReaderType nextStep) { |
| ThrottlingController* controller = from(context); |
| if (!controller) |
| return; |
| |
| controller->finishReader(reader, nextStep); |
| InspectorInstrumentation::asyncTaskCanceled(context, reader); |
| } |
| |
| DEFINE_INLINE_TRACE() { |
| visitor->trace(m_pendingReaders); |
| visitor->trace(m_runningReaders); |
| Supplement<ExecutionContext>::trace(visitor); |
| } |
| |
| private: |
| ThrottlingController() |
| : m_maxRunningReaders(kMaxOutstandingRequestsPerThread) {} |
| |
| void pushReader(FileReader* reader) { |
| if (m_pendingReaders.isEmpty() && |
| m_runningReaders.size() < m_maxRunningReaders) { |
| reader->executePendingRead(); |
| ASSERT(!m_runningReaders.contains(reader)); |
| m_runningReaders.add(reader); |
| return; |
| } |
| m_pendingReaders.append(reader); |
| executeReaders(); |
| } |
| |
| FinishReaderType removeReader(FileReader* reader) { |
| FileReaderHashSet::const_iterator hashIter = m_runningReaders.find(reader); |
| if (hashIter != m_runningReaders.end()) { |
| m_runningReaders.remove(hashIter); |
| return RunPendingReaders; |
| } |
| FileReaderDeque::const_iterator dequeEnd = m_pendingReaders.end(); |
| for (FileReaderDeque::const_iterator it = m_pendingReaders.begin(); |
| it != dequeEnd; ++it) { |
| if (*it == reader) { |
| m_pendingReaders.remove(it); |
| break; |
| } |
| } |
| return DoNotRunPendingReaders; |
| } |
| |
| void finishReader(FileReader* reader, FinishReaderType nextStep) { |
| if (nextStep == RunPendingReaders) |
| executeReaders(); |
| } |
| |
| void executeReaders() { |
| while (m_runningReaders.size() < m_maxRunningReaders) { |
| if (m_pendingReaders.isEmpty()) |
| return; |
| FileReader* reader = m_pendingReaders.takeFirst(); |
| reader->executePendingRead(); |
| m_runningReaders.add(reader); |
| } |
| } |
| |
| static const char* supplementName() { |
| return "FileReaderThrottlingController"; |
| } |
| |
| const size_t m_maxRunningReaders; |
| |
| using FileReaderDeque = HeapDeque<Member<FileReader>>; |
| using FileReaderHashSet = HeapHashSet<Member<FileReader>>; |
| |
| FileReaderDeque m_pendingReaders; |
| FileReaderHashSet m_runningReaders; |
| }; |
| |
| FileReader* FileReader::create(ExecutionContext* context) { |
| FileReader* fileReader = new FileReader(context); |
| fileReader->suspendIfNeeded(); |
| return fileReader; |
| } |
| |
| FileReader::FileReader(ExecutionContext* context) |
| : ActiveScriptWrappable(this), |
| ActiveDOMObject(context), |
| m_state(kEmpty), |
| m_loadingState(LoadingStateNone), |
| m_stillFiringEvents(false), |
| m_readType(FileReaderLoader::ReadAsBinaryString), |
| m_lastProgressNotificationTimeMS(0) {} |
| |
| FileReader::~FileReader() { |
| terminate(); |
| } |
| |
| const AtomicString& FileReader::interfaceName() const { |
| return EventTargetNames::FileReader; |
| } |
| |
| void FileReader::contextDestroyed() { |
| // The delayed abort task tidies up and advances to the DONE state. |
| if (m_loadingState == LoadingStateAborted) |
| return; |
| |
| if (hasPendingActivity()) |
| ThrottlingController::finishReader( |
| getExecutionContext(), this, |
| ThrottlingController::removeReader(getExecutionContext(), this)); |
| terminate(); |
| } |
| |
| bool FileReader::hasPendingActivity() const { |
| return m_state == kLoading || m_stillFiringEvents; |
| } |
| |
| void FileReader::readAsArrayBuffer(Blob* blob, ExceptionState& exceptionState) { |
| ASSERT(blob); |
| DVLOG(1) << "reading as array buffer: " << utf8BlobUUID(blob).data() << " " |
| << utf8FilePath(blob).data(); |
| |
| readInternal(blob, FileReaderLoader::ReadAsArrayBuffer, exceptionState); |
| } |
| |
| void FileReader::readAsBinaryString(Blob* blob, |
| ExceptionState& exceptionState) { |
| ASSERT(blob); |
| DVLOG(1) << "reading as binary: " << utf8BlobUUID(blob).data() << " " |
| << utf8FilePath(blob).data(); |
| |
| readInternal(blob, FileReaderLoader::ReadAsBinaryString, exceptionState); |
| } |
| |
| void FileReader::readAsText(Blob* blob, |
| const String& encoding, |
| ExceptionState& exceptionState) { |
| ASSERT(blob); |
| DVLOG(1) << "reading as text: " << utf8BlobUUID(blob).data() << " " |
| << utf8FilePath(blob).data(); |
| |
| m_encoding = encoding; |
| readInternal(blob, FileReaderLoader::ReadAsText, exceptionState); |
| } |
| |
| void FileReader::readAsText(Blob* blob, ExceptionState& exceptionState) { |
| readAsText(blob, String(), exceptionState); |
| } |
| |
| void FileReader::readAsDataURL(Blob* blob, ExceptionState& exceptionState) { |
| ASSERT(blob); |
| DVLOG(1) << "reading as data URL: " << utf8BlobUUID(blob).data() << " " |
| << utf8FilePath(blob).data(); |
| |
| readInternal(blob, FileReaderLoader::ReadAsDataURL, exceptionState); |
| } |
| |
| void FileReader::readInternal(Blob* blob, |
| FileReaderLoader::ReadType type, |
| ExceptionState& exceptionState) { |
| // If multiple concurrent read methods are called on the same FileReader, |
| // InvalidStateError should be thrown when the state is kLoading. |
| if (m_state == kLoading) { |
| exceptionState.throwDOMException( |
| InvalidStateError, "The object is already busy reading Blobs."); |
| return; |
| } |
| |
| if (blob->isClosed()) { |
| exceptionState.throwDOMException( |
| InvalidStateError, |
| String(blob->isFile() ? "File" : "Blob") + " has been closed."); |
| return; |
| } |
| |
| ExecutionContext* context = getExecutionContext(); |
| if (!context) { |
| exceptionState.throwDOMException( |
| AbortError, "Reading from a detached FileReader is not supported."); |
| return; |
| } |
| |
| // A document loader will not load new resources once the Document has |
| // detached from its frame. |
| if (context->isDocument() && !toDocument(context)->frame()) { |
| exceptionState.throwDOMException( |
| AbortError, |
| "Reading from a Document-detached FileReader is not supported."); |
| return; |
| } |
| |
| // "Snapshot" the Blob data rather than the Blob itself as ongoing |
| // read operations should not be affected if close() is called on |
| // the Blob being read. |
| m_blobDataHandle = blob->blobDataHandle(); |
| m_blobType = blob->type(); |
| m_readType = type; |
| m_state = kLoading; |
| m_loadingState = LoadingStatePending; |
| m_error = nullptr; |
| ASSERT(ThrottlingController::from(context)); |
| ThrottlingController::pushReader(context, this); |
| } |
| |
| void FileReader::executePendingRead() { |
| ASSERT(m_loadingState == LoadingStatePending); |
| m_loadingState = LoadingStateLoading; |
| |
| m_loader = FileReaderLoader::create(m_readType, this); |
| m_loader->setEncoding(m_encoding); |
| m_loader->setDataType(m_blobType); |
| m_loader->start(getExecutionContext(), m_blobDataHandle); |
| m_blobDataHandle = nullptr; |
| } |
| |
| static void delayedAbort(FileReader* reader) { |
| reader->doAbort(); |
| } |
| |
| void FileReader::abort() { |
| DVLOG(1) << "aborting"; |
| |
| if (m_loadingState != LoadingStateLoading && |
| m_loadingState != LoadingStatePending) { |
| return; |
| } |
| m_loadingState = LoadingStateAborted; |
| |
| // Schedule to have the abort done later since abort() might be called from |
| // the event handler and we do not want the resource loading code to be in the |
| // stack. |
| getExecutionContext()->postTask( |
| BLINK_FROM_HERE, |
| createSameThreadTask(&delayedAbort, wrapPersistent(this))); |
| } |
| |
| void FileReader::doAbort() { |
| DCHECK_NE(kDone, m_state); |
| AutoReset<bool> firingEvents(&m_stillFiringEvents, true); |
| |
| terminate(); |
| |
| m_error = FileError::createDOMException(FileError::kAbortErr); |
| |
| // Unregister the reader. |
| ThrottlingController::FinishReaderType finalStep = |
| ThrottlingController::removeReader(getExecutionContext(), this); |
| |
| fireEvent(EventTypeNames::error); |
| fireEvent(EventTypeNames::abort); |
| fireEvent(EventTypeNames::loadend); |
| |
| // All possible events have fired and we're done, no more pending activity. |
| ThrottlingController::finishReader(getExecutionContext(), this, finalStep); |
| } |
| |
| void FileReader::result(StringOrArrayBuffer& resultAttribute) const { |
| if (!m_loader || m_error) |
| return; |
| |
| if (m_readType == FileReaderLoader::ReadAsArrayBuffer) |
| resultAttribute.setArrayBuffer(m_loader->arrayBufferResult()); |
| else |
| resultAttribute.setString(m_loader->stringResult()); |
| } |
| |
| void FileReader::terminate() { |
| if (m_loader) { |
| m_loader->cancel(); |
| m_loader = nullptr; |
| } |
| m_state = kDone; |
| m_loadingState = LoadingStateNone; |
| } |
| |
| void FileReader::didStartLoading() { |
| AutoReset<bool> firingEvents(&m_stillFiringEvents, true); |
| fireEvent(EventTypeNames::loadstart); |
| } |
| |
| void FileReader::didReceiveData() { |
| // Fire the progress event at least every 50ms. |
| double now = currentTimeMS(); |
| if (!m_lastProgressNotificationTimeMS) { |
| m_lastProgressNotificationTimeMS = now; |
| } else if (now - m_lastProgressNotificationTimeMS > |
| progressNotificationIntervalMS) { |
| AutoReset<bool> firingEvents(&m_stillFiringEvents, true); |
| fireEvent(EventTypeNames::progress); |
| m_lastProgressNotificationTimeMS = now; |
| } |
| } |
| |
| void FileReader::didFinishLoading() { |
| if (m_loadingState == LoadingStateAborted) |
| return; |
| ASSERT(m_loadingState == LoadingStateLoading); |
| |
| // TODO(jochen): When we set m_state to DONE below, we still need to fire |
| // the load and loadend events. To avoid GC to collect this FileReader, we |
| // use this separate variable to keep the wrapper of this FileReader alive. |
| // An alternative would be to keep any active DOM object alive that is on |
| // the stack. |
| AutoReset<bool> firingEvents(&m_stillFiringEvents, true); |
| |
| // It's important that we change m_loadingState before firing any events |
| // since any of the events could call abort(), which internally checks |
| // if we're still loading (therefore we need abort process) or not. |
| m_loadingState = LoadingStateNone; |
| |
| fireEvent(EventTypeNames::progress); |
| |
| DCHECK_NE(kDone, m_state); |
| m_state = kDone; |
| |
| // Unregister the reader. |
| ThrottlingController::FinishReaderType finalStep = |
| ThrottlingController::removeReader(getExecutionContext(), this); |
| |
| fireEvent(EventTypeNames::load); |
| fireEvent(EventTypeNames::loadend); |
| |
| // All possible events have fired and we're done, no more pending activity. |
| ThrottlingController::finishReader(getExecutionContext(), this, finalStep); |
| } |
| |
| void FileReader::didFail(FileError::ErrorCode errorCode) { |
| if (m_loadingState == LoadingStateAborted) |
| return; |
| |
| AutoReset<bool> firingEvents(&m_stillFiringEvents, true); |
| |
| DCHECK_EQ(LoadingStateLoading, m_loadingState); |
| m_loadingState = LoadingStateNone; |
| |
| DCHECK_NE(kDone, m_state); |
| m_state = kDone; |
| |
| m_error = FileError::createDOMException( |
| static_cast<FileError::ErrorCode>(errorCode)); |
| |
| // Unregister the reader. |
| ThrottlingController::FinishReaderType finalStep = |
| ThrottlingController::removeReader(getExecutionContext(), this); |
| |
| fireEvent(EventTypeNames::error); |
| fireEvent(EventTypeNames::loadend); |
| |
| // All possible events have fired and we're done, no more pending activity. |
| ThrottlingController::finishReader(getExecutionContext(), this, finalStep); |
| } |
| |
| void FileReader::fireEvent(const AtomicString& type) { |
| InspectorInstrumentation::AsyncTask asyncTask(getExecutionContext(), this); |
| if (!m_loader) { |
| dispatchEvent(ProgressEvent::create(type, false, 0, 0)); |
| return; |
| } |
| |
| if (m_loader->totalBytes() >= 0) |
| dispatchEvent(ProgressEvent::create(type, true, m_loader->bytesLoaded(), |
| m_loader->totalBytes())); |
| else |
| dispatchEvent( |
| ProgressEvent::create(type, false, m_loader->bytesLoaded(), 0)); |
| } |
| |
| DEFINE_TRACE(FileReader) { |
| visitor->trace(m_error); |
| EventTargetWithInlineData::trace(visitor); |
| ActiveDOMObject::trace(visitor); |
| } |
| |
| } // namespace blink |