blob: 7b595c43579ae18c8ddb8cb8eca637553934f75d [file] [log] [blame]
/*
* 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 "third_party/blink/renderer/core/fileapi/file_reader.h"
#include "base/auto_reset.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/string_or_array_buffer.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/events/progress_event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/file.h"
#include "third_party/blink/renderer/core/fileapi/file_error.h"
#include "third_party/blink/renderer/core/frame/use_counter.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/supplementable.h"
#include "third_party/blink/renderer/platform/wtf/deque.h"
#include "third_party/blink/renderer/platform/wtf/hash_set.h"
#include "third_party/blink/renderer/platform/wtf/text/cstring.h"
#include "third_party/blink/renderer/platform/wtf/time.h"
namespace blink {
namespace {
const CString Utf8BlobUUID(Blob* blob) {
return blob->Uuid().Utf8();
}
const CString Utf8FilePath(Blob* blob) {
return blob->HasBackingFile() ? ToFile(blob)->GetPath().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 kProgressNotificationIntervalMS = 50;
class FileReader::ThrottlingController final
: public GarbageCollected<FileReader::ThrottlingController>,
public Supplement<ExecutionContext> {
USING_GARBAGE_COLLECTED_MIXIN(FileReader::ThrottlingController);
public:
static const char kSupplementName[];
static ThrottlingController* From(ExecutionContext* context) {
if (!context)
return nullptr;
ThrottlingController* controller =
Supplement<ExecutionContext>::From<ThrottlingController>(*context);
if (!controller) {
controller = MakeGarbageCollected<ThrottlingController>(*context);
ProvideTo(*context, controller);
}
return controller;
}
enum FinishReaderType { kDoNotRunPendingReaders, kRunPendingReaders };
static void PushReader(ExecutionContext* context, FileReader* reader) {
ThrottlingController* controller = From(context);
if (!controller)
return;
probe::AsyncTaskScheduled(context, "FileReader", reader);
controller->PushReader(reader);
}
static FinishReaderType RemoveReader(ExecutionContext* context,
FileReader* reader) {
ThrottlingController* controller = From(context);
if (!controller)
return kDoNotRunPendingReaders;
return controller->RemoveReader(reader);
}
static void FinishReader(ExecutionContext* context,
FileReader* reader,
FinishReaderType next_step) {
ThrottlingController* controller = From(context);
if (!controller)
return;
controller->FinishReader(reader, next_step);
probe::AsyncTaskCanceled(context, reader);
}
explicit ThrottlingController(ExecutionContext& context)
: Supplement<ExecutionContext>(context),
max_running_readers_(kMaxOutstandingRequestsPerThread) {}
void Trace(blink::Visitor* visitor) override {
visitor->Trace(pending_readers_);
visitor->Trace(running_readers_);
Supplement<ExecutionContext>::Trace(visitor);
}
private:
void PushReader(FileReader* reader) {
if (pending_readers_.IsEmpty() &&
running_readers_.size() < max_running_readers_) {
reader->ExecutePendingRead();
DCHECK(!running_readers_.Contains(reader));
running_readers_.insert(reader);
return;
}
pending_readers_.push_back(reader);
ExecuteReaders();
}
FinishReaderType RemoveReader(FileReader* reader) {
FileReaderHashSet::const_iterator hash_iter = running_readers_.find(reader);
if (hash_iter != running_readers_.end()) {
running_readers_.erase(hash_iter);
return kRunPendingReaders;
}
FileReaderDeque::const_iterator deque_end = pending_readers_.end();
for (FileReaderDeque::const_iterator it = pending_readers_.begin();
it != deque_end; ++it) {
if (*it == reader) {
pending_readers_.erase(it);
break;
}
}
return kDoNotRunPendingReaders;
}
void FinishReader(FileReader* reader, FinishReaderType next_step) {
if (next_step == kRunPendingReaders)
ExecuteReaders();
}
void ExecuteReaders() {
// Dont execute more readers if the context is already destroyed (or in the
// process of being destroyed).
if (GetSupplementable()->IsContextDestroyed())
return;
while (running_readers_.size() < max_running_readers_) {
if (pending_readers_.IsEmpty())
return;
FileReader* reader = pending_readers_.TakeFirst();
reader->ExecutePendingRead();
running_readers_.insert(reader);
}
}
const size_t max_running_readers_;
using FileReaderDeque = HeapDeque<Member<FileReader>>;
using FileReaderHashSet = HeapHashSet<Member<FileReader>>;
FileReaderDeque pending_readers_;
FileReaderHashSet running_readers_;
};
// static
const char FileReader::ThrottlingController::kSupplementName[] =
"FileReaderThrottlingController";
FileReader* FileReader::Create(ExecutionContext* context) {
return MakeGarbageCollected<FileReader>(context);
}
FileReader::FileReader(ExecutionContext* context)
: ContextLifecycleObserver(context),
state_(kEmpty),
loading_state_(kLoadingStateNone),
still_firing_events_(false),
read_type_(FileReaderLoader::kReadAsBinaryString),
last_progress_notification_time_ms_(0) {}
FileReader::~FileReader() {
Terminate();
}
const AtomicString& FileReader::InterfaceName() const {
return event_target_names::kFileReader;
}
void FileReader::ContextDestroyed(ExecutionContext* destroyed_context) {
// The delayed abort task tidies up and advances to the DONE state.
if (loading_state_ == kLoadingStateAborted)
return;
if (HasPendingActivity()) {
ThrottlingController::FinishReader(
destroyed_context, this,
ThrottlingController::RemoveReader(destroyed_context, this));
}
Terminate();
}
bool FileReader::HasPendingActivity() const {
return state_ == kLoading || still_firing_events_;
}
void FileReader::readAsArrayBuffer(Blob* blob,
ExceptionState& exception_state) {
DCHECK(blob);
DVLOG(1) << "reading as array buffer: " << Utf8BlobUUID(blob).data() << " "
<< Utf8FilePath(blob).data();
ReadInternal(blob, FileReaderLoader::kReadAsArrayBuffer, exception_state);
}
void FileReader::readAsBinaryString(Blob* blob,
ExceptionState& exception_state) {
DCHECK(blob);
DVLOG(1) << "reading as binary: " << Utf8BlobUUID(blob).data() << " "
<< Utf8FilePath(blob).data();
ReadInternal(blob, FileReaderLoader::kReadAsBinaryString, exception_state);
}
void FileReader::readAsText(Blob* blob,
const String& encoding,
ExceptionState& exception_state) {
DCHECK(blob);
DVLOG(1) << "reading as text: " << Utf8BlobUUID(blob).data() << " "
<< Utf8FilePath(blob).data();
encoding_ = encoding;
ReadInternal(blob, FileReaderLoader::kReadAsText, exception_state);
}
void FileReader::readAsText(Blob* blob, ExceptionState& exception_state) {
readAsText(blob, String(), exception_state);
}
void FileReader::readAsDataURL(Blob* blob, ExceptionState& exception_state) {
DCHECK(blob);
DVLOG(1) << "reading as data URL: " << Utf8BlobUUID(blob).data() << " "
<< Utf8FilePath(blob).data();
ReadInternal(blob, FileReaderLoader::kReadAsDataURL, exception_state);
}
void FileReader::ReadInternal(Blob* blob,
FileReaderLoader::ReadType type,
ExceptionState& exception_state) {
// If multiple concurrent read methods are called on the same FileReader,
// InvalidStateError should be thrown when the state is kLoading.
if (state_ == kLoading) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The object is already busy reading Blobs.");
return;
}
ExecutionContext* context = GetExecutionContext();
if (!context) {
exception_state.ThrowDOMException(
DOMExceptionCode::kAbortError,
"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 (IsA<Document>(context) && !To<Document>(context)->GetFrame()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kAbortError,
"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.
blob_data_handle_ = blob->GetBlobDataHandle();
blob_type_ = blob->type();
read_type_ = type;
state_ = kLoading;
loading_state_ = kLoadingStatePending;
error_ = nullptr;
DCHECK(ThrottlingController::From(context));
ThrottlingController::PushReader(context, this);
}
void FileReader::ExecutePendingRead() {
DCHECK_EQ(loading_state_, kLoadingStatePending);
loading_state_ = kLoadingStateLoading;
loader_ = FileReaderLoader::Create(read_type_, this);
loader_->SetEncoding(encoding_);
loader_->SetDataType(blob_type_);
loader_->Start(blob_data_handle_);
blob_data_handle_ = nullptr;
}
void FileReader::abort() {
DVLOG(1) << "aborting";
if (loading_state_ != kLoadingStateLoading &&
loading_state_ != kLoadingStatePending) {
return;
}
loading_state_ = kLoadingStateAborted;
DCHECK_NE(kDone, state_);
state_ = kDone;
base::AutoReset<bool> firing_events(&still_firing_events_, true);
// Setting error implicitly makes |result| return null.
error_ = file_error::CreateDOMException(FileErrorCode::kAbortErr);
// Unregister the reader.
ThrottlingController::FinishReaderType final_step =
ThrottlingController::RemoveReader(GetExecutionContext(), this);
FireEvent(event_type_names::kAbort);
FireEvent(event_type_names::kLoadend);
// All possible events have fired and we're done, no more pending activity.
ThrottlingController::FinishReader(GetExecutionContext(), this, final_step);
// Also synchronously cancel the loader, as script might initiate a new load
// right after this method returns, in which case an async termination would
// terminate the wrong loader.
Terminate();
}
void FileReader::result(ScriptState* state,
StringOrArrayBuffer& result_attribute) const {
if (error_ || !loader_)
return;
if (!loader_->HasFinishedLoading()) {
UseCounter::Count(ExecutionContext::From(state),
WebFeature::kFileReaderResultBeforeCompletion);
}
if (read_type_ == FileReaderLoader::kReadAsArrayBuffer)
result_attribute.SetArrayBuffer(loader_->ArrayBufferResult());
else
result_attribute.SetString(loader_->StringResult());
}
void FileReader::Terminate() {
if (loader_) {
loader_->Cancel();
loader_ = nullptr;
}
state_ = kDone;
loading_state_ = kLoadingStateNone;
}
void FileReader::DidStartLoading() {
base::AutoReset<bool> firing_events(&still_firing_events_, true);
FireEvent(event_type_names::kLoadstart);
}
void FileReader::DidReceiveData() {
// Fire the progress event at least every 50ms.
double now = CurrentTimeMS();
if (!last_progress_notification_time_ms_) {
last_progress_notification_time_ms_ = now;
} else if (now - last_progress_notification_time_ms_ >
kProgressNotificationIntervalMS) {
base::AutoReset<bool> firing_events(&still_firing_events_, true);
FireEvent(event_type_names::kProgress);
last_progress_notification_time_ms_ = now;
}
}
void FileReader::DidFinishLoading() {
if (loading_state_ == kLoadingStateAborted)
return;
DCHECK_EQ(loading_state_, kLoadingStateLoading);
// 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 ActiveScriptWrappables alive that is on
// the stack.
base::AutoReset<bool> firing_events(&still_firing_events_, 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.
loading_state_ = kLoadingStateNone;
FireEvent(event_type_names::kProgress);
DCHECK_NE(kDone, state_);
state_ = kDone;
// Unregister the reader.
ThrottlingController::FinishReaderType final_step =
ThrottlingController::RemoveReader(GetExecutionContext(), this);
FireEvent(event_type_names::kLoad);
FireEvent(event_type_names::kLoadend);
// All possible events have fired and we're done, no more pending activity.
ThrottlingController::FinishReader(GetExecutionContext(), this, final_step);
}
void FileReader::DidFail(FileErrorCode error_code) {
if (loading_state_ == kLoadingStateAborted)
return;
base::AutoReset<bool> firing_events(&still_firing_events_, true);
DCHECK_EQ(kLoadingStateLoading, loading_state_);
loading_state_ = kLoadingStateNone;
DCHECK_NE(kDone, state_);
state_ = kDone;
error_ = file_error::CreateDOMException(error_code);
// Unregister the reader.
ThrottlingController::FinishReaderType final_step =
ThrottlingController::RemoveReader(GetExecutionContext(), this);
FireEvent(event_type_names::kError);
FireEvent(event_type_names::kLoadend);
// All possible events have fired and we're done, no more pending activity.
ThrottlingController::FinishReader(GetExecutionContext(), this, final_step);
}
void FileReader::FireEvent(const AtomicString& type) {
probe::AsyncTask async_task(GetExecutionContext(), this, "event");
if (!loader_) {
DispatchEvent(*ProgressEvent::Create(type, false, 0, 0));
return;
}
if (loader_->TotalBytes()) {
DispatchEvent(*ProgressEvent::Create(type, true, loader_->BytesLoaded(),
*loader_->TotalBytes()));
} else {
DispatchEvent(
*ProgressEvent::Create(type, false, loader_->BytesLoaded(), 0));
}
}
void FileReader::Trace(blink::Visitor* visitor) {
visitor->Trace(error_);
EventTargetWithInlineData::Trace(visitor);
ContextLifecycleObserver::Trace(visitor);
}
} // namespace blink