Implementing error, close, finish, abort events management.

This commit is contained in:
ali.ibrahim 2016-10-04 17:12:17 +02:00
parent bde30b90e8
commit ec1fc5a17b
7 changed files with 391 additions and 117 deletions

View file

@ -29,7 +29,9 @@ void Net::BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype) {
prototype->SetClassName(mate::StringToV8(isolate, "Net"));
mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetProperty("URLRequest", &Net::URLRequest);
.SetProperty("URLRequest", &Net::URLRequest)
.SetMethod("RequestGarbageCollectionForTesting",
&Net::RequestGarbageCollectionForTesting);
}
v8::Local<v8::Value> Net::URLRequest(v8::Isolate* isolate) {
@ -37,6 +39,11 @@ v8::Local<v8::Value> Net::URLRequest(v8::Isolate* isolate) {
}
void Net::RequestGarbageCollectionForTesting() {
isolate()->RequestGarbageCollectionForTesting(
v8::Isolate::GarbageCollectionType::kFullGarbageCollection);
}
} // namespace api

View file

@ -24,6 +24,7 @@ class Net : public mate::EventEmitter<Net> {
~Net() override;
private:
void RequestGarbageCollectionForTesting();
DISALLOW_COPY_AND_ASSIGN(Net);
};

View file

@ -83,6 +83,84 @@ struct Converter<scoped_refptr<const net::IOBufferWithSize>> {
namespace atom {
namespace api {
template <typename Flags>
URLRequest::StateBase<Flags>::StateBase(Flags initialState)
: state_(initialState) {
}
template <typename Flags>
void URLRequest::StateBase<Flags>::SetFlag(Flags flag) {
state_ = static_cast<Flags>(static_cast<int>(state_) &
static_cast<int>(flag));
}
template <typename Flags>
bool URLRequest::StateBase<Flags>::operator==(Flags flag) const {
return state_ == flag;
}
template <typename Flags>
bool URLRequest::StateBase<Flags>::IsFlagSet(Flags flag) const {
return static_cast<int>(state_) & static_cast<int>(flag);
}
URLRequest::RequestState::RequestState()
: StateBase(RequestStateFlags::kNotStarted) {
}
bool URLRequest::RequestState::NotStarted() const {
return *this == RequestStateFlags::kNotStarted;
}
bool URLRequest::RequestState::Started() const {
return IsFlagSet(RequestStateFlags::kStarted);
}
bool URLRequest::RequestState::Finished() const {
return IsFlagSet(RequestStateFlags::kFinished);
}
bool URLRequest::RequestState::Canceled() const {
return IsFlagSet(RequestStateFlags::kCanceled);
}
bool URLRequest::RequestState::Failed() const {
return IsFlagSet(RequestStateFlags::kFailed);
}
bool URLRequest::RequestState::Closed() const {
return IsFlagSet(RequestStateFlags::kClosed);
}
URLRequest::ResponseState::ResponseState()
: StateBase(ResponseStateFlags::kNotStarted) {
}
bool URLRequest::ResponseState::NotStarted() const {
return *this == ResponseStateFlags::kNotStarted;
}
bool URLRequest::ResponseState::Started() const {
return IsFlagSet(ResponseStateFlags::kStarted);
}
bool URLRequest::ResponseState::Ended() const {
return IsFlagSet(ResponseStateFlags::kEnded);
}
bool URLRequest::ResponseState::Canceled() const {
return IsFlagSet(ResponseStateFlags::kCanceled);
}
bool URLRequest::ResponseState::Failed() const {
return IsFlagSet(ResponseStateFlags::kFailed);
}
bool URLRequest::ResponseState::Closed() const {
return IsFlagSet(ResponseStateFlags::kClosed);
}
URLRequest::URLRequest(v8::Isolate* isolate, v8::Local<v8::Object> wrapper)
: weak_ptr_factory_(this) {
InitWith(isolate, wrapper);
@ -127,10 +205,12 @@ void URLRequest::BuildPrototype(v8::Isolate* isolate,
// Request API
.MakeDestroyable()
.SetMethod("write", &URLRequest::Write)
.SetMethod("abort", &URLRequest::Abort)
.SetMethod("cancel", &URLRequest::Cancel)
.SetMethod("setExtraHeader", &URLRequest::SetExtraHeader)
.SetMethod("removeExtraHeader", &URLRequest::RemoveExtraHeader)
.SetMethod("setChunkedUpload", &URLRequest::SetChunkedUpload)
.SetProperty("notStarted", &URLRequest::NotStarted)
.SetProperty("finished", &URLRequest::Finished)
// Response APi
.SetProperty("statusCode", &URLRequest::StatusCode)
.SetProperty("statusMessage", &URLRequest::StatusMessage)
@ -139,19 +219,81 @@ void URLRequest::BuildPrototype(v8::Isolate* isolate,
.SetProperty("httpVersionMinor", &URLRequest::ResponseHttpVersionMinor);
}
bool URLRequest::NotStarted() const {
return request_state_.NotStarted();
}
bool URLRequest::Finished() const {
return request_state_.Finished();
}
bool URLRequest::Canceled() const {
return request_state_.Canceled();
}
bool URLRequest::Write(
scoped_refptr<const net::IOBufferWithSize> buffer,
bool is_last) {
return atom_request_->Write(buffer, is_last);
if (request_state_.Canceled() ||
request_state_.Failed() ||
request_state_.Finished() ||
request_state_.Closed()) {
return false;
}
if (request_state_.NotStarted()) {
request_state_.SetFlag(RequestStateFlags::kStarted);
// Pin on first write.
pin();
}
if (is_last) {
request_state_.SetFlag(RequestStateFlags::kFinished);
EmitRequestEvent(true, "finish");
}
DCHECK(atom_request_);
if (atom_request_) {
return atom_request_->Write(buffer, is_last);
}
return false;
}
void URLRequest::Abort() {
atom_request_->Abort();
void URLRequest::Cancel() {
if (request_state_.Canceled()) {
// Cancel only once.
return;
}
// Mark as canceled.
request_state_.SetFlag(RequestStateFlags::kCanceled);
if (request_state_.Started()) {
// Really cancel if it was started.
atom_request_->Cancel();
}
if (!request_state_.Closed()) {
EmitRequestEvent(true, "abort");
}
response_state_.SetFlag(ResponseStateFlags::kCanceled);
if (response_state_.Started() && !response_state_.Closed()) {
EmitResponseEvent(true, "aborted");
}
Close();
}
bool URLRequest::SetExtraHeader(const std::string& name,
const std::string& value) {
// State must be equal to not started.
if (!request_state_.NotStarted()) {
// Cannot change headers after send.
return false;
}
if (!net::HttpUtil::IsValidHeaderName(name)) {
return false;
}
@ -165,45 +307,84 @@ bool URLRequest::SetExtraHeader(const std::string& name,
}
void URLRequest::RemoveExtraHeader(const std::string& name) {
// State must be equal to not started.
if (!request_state_.NotStarted()) {
// Cannot change headers after send.
return;
}
atom_request_->RemoveExtraHeader(name);
}
void URLRequest::SetChunkedUpload(bool is_chunked_upload) {
// State must be equal to not started.
if (!request_state_.NotStarted()) {
// Cannot change headers after send.
return;
}
atom_request_->SetChunkedUpload(is_chunked_upload);
}
void URLRequest::OnAuthenticationRequired(
scoped_refptr<const net::AuthChallengeInfo> auth_info) {
EmitRequestEvent(
false,
"login",
auth_info.get(),
base::Bind(&AtomURLRequest::PassLoginInformation, atom_request_));
}
void URLRequest::OnResponseStarted() {
if (request_state_.Canceled() ||
request_state_.Failed() ||
request_state_.Closed()) {
// Don't emit any event after request cancel.
return;
}
response_state_.SetFlag(ResponseStateFlags::kStarted);
Emit("response");
}
void URLRequest::OnResponseData(
scoped_refptr<const net::IOBufferWithSize> buffer) {
if (request_state_.Canceled()) {
// Don't emit any event after request cancel.
return;
}
if (!buffer || !buffer->data() || !buffer->size()) {
return;
}
EmitResponseEvent("data", buffer);
if (!response_state_.Closed()) {
EmitResponseEvent(false, "data", buffer);
}
}
void URLRequest::OnResponseCompleted() {
EmitResponseEvent("end");
unpin();
atom_request_ = nullptr;
response_state_.SetFlag(ResponseStateFlags::kEnded);
if (request_state_.Canceled()) {
// Don't emit any event after request cancel.
return;
}
if (!response_state_.Closed()) {
EmitResponseEvent(false, "end");
}
Close();
}
void URLRequest::OnError(const std::string& error) {
void URLRequest::OnRequestError(const std::string& error) {
request_state_.SetFlag(RequestStateFlags::kFailed);
auto error_object = v8::Exception::Error(mate::StringToV8(isolate(), error));
EmitRequestEvent("error", error_object);
EmitRequestEvent(false, "error", error_object);
Close();
}
void URLRequest::OnResponseError(const std::string& error) {
response_state_.SetFlag(ResponseStateFlags::kFailed);
auto error_object = v8::Exception::Error(mate::StringToV8(isolate(), error));
EmitResponseEvent(false, "error", error_object);
Close();
}
int URLRequest::StatusCode() const {
if (auto response_headers = atom_request_->GetResponseHeaders()) {
return response_headers->response_code();
@ -238,6 +419,22 @@ uint32_t URLRequest::ResponseHttpVersionMinor() const {
return 0;
}
void URLRequest::Close() {
if (!response_state_.Closed()) {
response_state_.SetFlag(ResponseStateFlags::kClosed);
if (response_state_.Started()) {
// Emit a close event if we really have a response object.
EmitResponseEvent(true, "close");
}
}
if (!request_state_.Closed()) {
request_state_.SetFlag(RequestStateFlags::kClosed);
EmitRequestEvent(true, "close");
}
unpin();
atom_request_ = nullptr;
}
void URLRequest::pin() {
if (wrapper_.IsEmpty()) {
wrapper_.Reset(isolate(), GetWrapper());

View file

@ -25,28 +25,28 @@ namespace api {
//
// The URLRequest class implements the V8 binding between the JavaScript API
// and Chromium native net library. It is responsible for handling HTTP/HTTPS
// and Chromium native net library. It is responsible for handling HTTP/HTTPS
// requests.
//
// The current class provides only the binding layer. Two other JavaScript
// classes (ClientRequest and IncomingMessage) in the net module provide the
// The current class provides only the binding layer. Two other JavaScript
// classes (ClientRequest and IncomingMessage) in the net module provide the
// final API, including some state management and arguments validation.
//
// URLRequest's methods fall into two main categories: command and event methods.
// They are always executed on the Browser's UI thread.
// URLRequest's methods fall into two main categories: command and event
// methods. They are always executed on the Browser's UI thread.
// Command methods are called directly from JavaScript code via the API defined
// in BuildPrototype. A command method is generally implemented by forwarding
// the call to a corresponding method on AtomURLRequest which does the
// synchronization on the Browser IO thread. The latter then calls into Chromium
// net library. On the other hand, net library events originate on the IO
// in BuildPrototype. A command method is generally implemented by forwarding
// the call to a corresponding method on AtomURLRequest which does the
// synchronization on the Browser IO thread. The latter then calls into Chromium
// net library. On the other hand, net library events originate on the IO
// thread in AtomURLRequest and are synchronized back on the UI thread, then
// forwarded to a corresponding event method in URLRequest and then to
// forwarded to a corresponding event method in URLRequest and then to
// JavaScript via the EmitRequestEvent/EmitResponseEvent helpers.
//
// URLRequest lifetime management: we followed the Wrapper/Wrappable pattern
// URLRequest lifetime management: we followed the Wrapper/Wrappable pattern
// defined in native_mate. However, we augment that pattern with a pin/unpin
// mechanism. The main reason is that we want the JS API to provide a similar
// lifetime guarantees as the XMLHttpRequest.
// lifetime guarantees as the XMLHttpRequest.
// https://xhr.spec.whatwg.org/#garbage-collection
//
// The primary motivation is to not garbage collect a URLInstance as long as the
@ -61,32 +61,32 @@ namespace api {
// });
// })();
//
// we still want data to be logged even if the response/request objects are no
// we still want data to be logged even if the response/request objects are n
// more referenced in JavaScript.
//
// Binding by simply following the native_mate Wrapper/Wrappable pattern will
// delete the URLRequest object when the corresponding JS object is collected.
// The v8 handle is a private member in WrappableBase and it is always weak,
// The v8 handle is a private member in WrappableBase and it is always weak,
// there is no way to make it strong without changing native_mate.
// The solution we implement consists of maintaining some kind of state that
// The solution we implement consists of maintaining some kind of state that
// prevents collection of JS wrappers as long as the request is emitting network
// events. At initialization, the object is unpinned. When the request starts,
// it is pinned. When no more events would be emitted, the object is unpinned
// and lifetime is again managed by the standard native mate Wrapper/Wrappable
// events. At initialization, the object is unpinned. When the request starts,
// it is pinned. When no more events would be emitted, the object is unpinned
// and lifetime is again managed by the standard native mate Wrapper/Wrappable
// pattern.
//
// pin/unpin: are implemented by constructing/reseting a V8 strong persistent
// pin/unpin: are implemented by constructing/reseting a V8 strong persistent
// handle.
//
// The URLRequest/AtmURLRequest interaction could have been implemented in a
// The URLRequest/AtmURLRequest interaction could have been implemented in a
// single class. However, it implies that the resulting class lifetime will be
// managed by two conflicting mechanisms: JavaScript garbage collection and
// Chromium reference counting. Reasoning about lifetime issues become much
// managed by two conflicting mechanisms: JavaScript garbage collection and
// Chromium reference counting. Reasoning about lifetime issues become much
// more complex.
//
// We chose to split the implementation into two classes linked via a strong/weak
// pointers. A URLRequest instance is deleted if it is unpinned and the
// corresponding JS wrapper object is garbage collected. On the other hand,
// We chose to split the implementation into two classes linked via a
// strong/weak pointers. A URLRequest instance is deleted if it is unpinned and
// the corresponding JS wrapper object is garbage collected. On the other hand,
// an AtmURLRequest instance lifetime is totally governed by reference counting.
//
class URLRequest : public mate::EventEmitter<URLRequest> {
@ -103,7 +103,8 @@ class URLRequest : public mate::EventEmitter<URLRequest> {
void OnResponseStarted();
void OnResponseData(scoped_refptr<const net::IOBufferWithSize> data);
void OnResponseCompleted();
void OnError(const std::string& error);
void OnRequestError(const std::string& error);
void OnResponseError(const std::string& error);
protected:
explicit URLRequest(v8::Isolate* isolate,
@ -111,13 +112,70 @@ class URLRequest : public mate::EventEmitter<URLRequest> {
~URLRequest() override;
private:
template <typename Flags>
class StateBase {
public:
void SetFlag(Flags flag);
protected:
explicit StateBase(Flags initialState);
bool operator==(Flags flag) const;
bool IsFlagSet(Flags flag) const;
private:
Flags state_;
};
enum class RequestStateFlags {
kNotStarted = 0x0,
kStarted = 0x1,
kFinished = 0x2,
kCanceled = 0x4,
kFailed = 0x8,
kClosed = 0x10
};
class RequestState : public StateBase<RequestStateFlags> {
public:
RequestState();
bool NotStarted() const;
bool Started() const;
bool Finished() const;
bool Canceled() const;
bool Failed() const;
bool Closed() const;
};
enum class ResponseStateFlags {
kNotStarted = 0x0,
kStarted = 0x1,
kEnded = 0x2,
kCanceled = 0x4,
kFailed = 0x8,
kClosed = 0x10
};
class ResponseState : public StateBase<ResponseStateFlags> {
public:
ResponseState();
bool NotStarted() const;
bool Started() const;
bool Ended() const;
bool Canceled() const;
bool Failed() const;
bool Closed() const;
};
bool NotStarted() const;
bool Finished() const;
bool Canceled() const;
bool Failed() const;
bool Write(scoped_refptr<const net::IOBufferWithSize> buffer,
bool is_last);
void Abort();
void Cancel();
bool SetExtraHeader(const std::string& name, const std::string& value);
void RemoveExtraHeader(const std::string& name);
void SetChunkedUpload(bool is_chunked_upload);
bool CanReadHeaders() const;
int StatusCode() const;
std::string StatusMessage() const;
scoped_refptr<const net::HttpResponseHeaders> RawResponseHeaders() const;
@ -134,16 +192,20 @@ class URLRequest : public mate::EventEmitter<URLRequest> {
template <typename ... ArgTypes>
void EmitResponseEvent(ArgTypes... args);
void Close();
void pin();
void unpin();
scoped_refptr<AtomURLRequest> atom_request_;
RequestState request_state_;
ResponseState response_state_;
// Used to implement pin/unpin.
v8::Global<v8::Object> wrapper_;
base::WeakPtrFactory<URLRequest> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(URLRequest);
};