Commit e7dfd329 authored by mpcomplete's avatar mpcomplete Committed by Commit bot

Mojo: Add a WebSocket/Client interface and hook it up to the HTML viewer.

It's only half-implemented, but it's the half that lets you connect and
send/receive messages, so this seemed like a good stopping point.

BUG=403930

Review URL: https://codereview.chromium.org/515923003

Cr-Commit-Position: refs/heads/master@{#293096}
parent b53144f3
......@@ -40,6 +40,8 @@
'services/html_viewer/webcookiejar_impl.h',
'services/html_viewer/webmimeregistry_impl.cc',
'services/html_viewer/webmimeregistry_impl.h',
'services/html_viewer/websockethandle_impl.cc',
'services/html_viewer/websockethandle_impl.h',
'services/html_viewer/webstoragenamespace_impl.cc',
'services/html_viewer/webstoragenamespace_impl.h',
'services/html_viewer/webthemeengine_impl.cc',
......@@ -365,6 +367,7 @@
'services/public/interfaces/network/network_error.mojom',
'services/public/interfaces/network/network_service.mojom',
'services/public/interfaces/network/url_loader.mojom',
'services/public/interfaces/network/web_socket.mojom',
],
'includes': [ 'public/tools/bindings/mojom_bindings_generator.gypi' ],
'export_dependent_settings': [
......@@ -398,6 +401,8 @@
'services/network/network_service_impl.h',
'services/network/url_loader_impl.cc',
'services/network/url_loader_impl.h',
'services/network/web_socket_impl.cc',
'services/network/web_socket_impl.h',
],
},
{
......
......@@ -12,6 +12,7 @@
#include "base/time/time.h"
#include "mojo/public/cpp/application/application_impl.h"
#include "mojo/services/html_viewer/webcookiejar_impl.h"
#include "mojo/services/html_viewer/websockethandle_impl.h"
#include "mojo/services/html_viewer/webthread_impl.h"
#include "mojo/services/html_viewer/weburlloader_impl.h"
#include "net/base/data_url.h"
......@@ -150,6 +151,10 @@ blink::WebURLLoader* BlinkPlatformImpl::createURLLoader() {
return new WebURLLoaderImpl(network_service_.get());
}
blink::WebSocketHandle* BlinkPlatformImpl::createWebSocketHandle() {
return new WebSocketHandleImpl(network_service_.get());
}
blink::WebString BlinkPlatformImpl::userAgent() {
return blink::WebString::fromUTF8(kUserAgentString);
}
......
......@@ -38,6 +38,7 @@ class BlinkPlatformImpl : public blink::Platform {
virtual void stopSharedTimer();
virtual void callOnMainThread(void (*func)(void*), void* context);
virtual blink::WebURLLoader* createURLLoader();
virtual blink::WebSocketHandle* createWebSocketHandle();
virtual blink::WebString userAgent();
virtual blink::WebData parseDataURL(
const blink::WebURL& url, blink::WebString& mime_type,
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "mojo/services/html_viewer/websockethandle_impl.h"
#include <vector>
#include "mojo/services/public/interfaces/network/network_service.mojom.h"
#include "third_party/WebKit/public/platform/WebSerializedOrigin.h"
#include "third_party/WebKit/public/platform/WebSocketHandleClient.h"
#include "third_party/WebKit/public/platform/WebString.h"
#include "third_party/WebKit/public/platform/WebURL.h"
#include "third_party/WebKit/public/platform/WebVector.h"
using blink::WebSerializedOrigin;
using blink::WebSocketHandle;
using blink::WebSocketHandleClient;
using blink::WebString;
using blink::WebURL;
using blink::WebVector;
namespace mojo {
template<>
struct TypeConverter<String, WebString> {
static String Convert(const WebString& str) {
return String(str.utf8());
}
};
template<>
struct TypeConverter<WebString, String> {
static WebString Convert(const String& str) {
return WebString::fromUTF8(str.get());
}
};
template<typename T, typename U>
struct TypeConverter<Array<T>, WebVector<U> > {
static Array<T> Convert(const WebVector<U>& vector) {
Array<T> array(vector.size());
for (size_t i = 0; i < vector.size(); ++i)
array[i] = TypeConverter<T, U>::Convert(vector[i]);
return array.Pass();
}
};
template<>
struct TypeConverter<WebSocket::MessageType, WebSocketHandle::MessageType> {
static WebSocket::MessageType Convert(WebSocketHandle::MessageType type) {
DCHECK(type == WebSocketHandle::MessageTypeContinuation ||
type == WebSocketHandle::MessageTypeText ||
type == WebSocketHandle::MessageTypeBinary);
typedef WebSocket::MessageType MessageType;
COMPILE_ASSERT(
static_cast<MessageType>(WebSocketHandle::MessageTypeContinuation) ==
WebSocket::MESSAGE_TYPE_CONTINUATION,
enum_values_must_match_for_message_type);
COMPILE_ASSERT(
static_cast<MessageType>(WebSocketHandle::MessageTypeText) ==
WebSocket::MESSAGE_TYPE_TEXT,
enum_values_must_match_for_message_type);
COMPILE_ASSERT(
static_cast<MessageType>(WebSocketHandle::MessageTypeBinary) ==
WebSocket::MESSAGE_TYPE_BINARY,
enum_values_must_match_for_message_type);
return static_cast<WebSocket::MessageType>(type);
}
};
template<>
struct TypeConverter<WebSocketHandle::MessageType, WebSocket::MessageType> {
static WebSocketHandle::MessageType Convert(WebSocket::MessageType type) {
DCHECK(type == WebSocket::MESSAGE_TYPE_CONTINUATION ||
type == WebSocket::MESSAGE_TYPE_TEXT ||
type == WebSocket::MESSAGE_TYPE_BINARY);
return static_cast<WebSocketHandle::MessageType>(type);
}
};
// This class forms a bridge from the mojo WebSocketClient interface and the
// Blink WebSocketHandleClient interface.
class WebSocketClientImpl : public InterfaceImpl<WebSocketClient> {
public:
explicit WebSocketClientImpl(WebSocketHandleImpl* handle,
blink::WebSocketHandleClient* client)
: handle_(handle), client_(client) {}
virtual ~WebSocketClientImpl() {}
private:
// WebSocketClient methods:
virtual void DidConnect(
bool fail,
const String& selected_subprotocol,
const String& extensions) OVERRIDE {
client_->didConnect(handle_,
fail,
selected_subprotocol.To<WebString>(),
extensions.To<WebString>());
}
virtual void DidReceiveData(bool fin,
WebSocket::MessageType type,
ScopedDataPipeConsumerHandle data_pipe) OVERRIDE {
uint32_t num_bytes;
ReadDataRaw(data_pipe.get(), NULL, &num_bytes, MOJO_READ_DATA_FLAG_QUERY);
std::vector<char> data(num_bytes);
ReadDataRaw(
data_pipe.get(), &data[0], &num_bytes, MOJO_READ_DATA_FLAG_NONE);
const char* data_ptr = data.empty() ? NULL : &data[0];
client_->didReceiveData(handle_,
fin,
ConvertTo<WebSocketHandle::MessageType>(type),
data_ptr,
data.size());
}
virtual void DidReceiveFlowControl(int64_t quota) OVERRIDE {
client_->didReceiveFlowControl(handle_, quota);
// |handle_| can be deleted here.
}
WebSocketHandleImpl* handle_;
blink::WebSocketHandleClient* client_;
DISALLOW_COPY_AND_ASSIGN(WebSocketClientImpl);
};
WebSocketHandleImpl::WebSocketHandleImpl(NetworkService* network_service)
: did_close_(false) {
network_service->CreateWebSocket(Get(&web_socket_));
}
WebSocketHandleImpl::~WebSocketHandleImpl() {
if (!did_close_) {
// The connection is abruptly disconnected by the renderer without
// closing handshake.
web_socket_->Close(WebSocket::kAbnormalCloseCode, String());
}
}
void WebSocketHandleImpl::connect(const WebURL& url,
const WebVector<WebString>& protocols,
const WebSerializedOrigin& origin,
WebSocketHandleClient* client) {
client_.reset(new WebSocketClientImpl(this, client));
WebSocketClientPtr client_ptr;
// TODO(mpcomplete): Is this the right ownership model? Or should mojo own
// |client_|?
WeakBindToProxy(client_.get(), &client_ptr);
web_socket_->Connect(url.string().utf8(),
Array<String>::From(protocols),
origin.string().utf8(),
client_ptr.Pass());
}
void WebSocketHandleImpl::send(bool fin,
WebSocketHandle::MessageType type,
const char* data,
size_t size) {
if (!client_)
return;
// TODO(mpcomplete): reuse the data pipe for subsequent sends.
uint32_t num_bytes = static_cast<uint32_t>(size);
MojoCreateDataPipeOptions options;
options.struct_size = sizeof(MojoCreateDataPipeOptions);
options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE;
options.element_num_bytes = 1;
options.capacity_num_bytes = num_bytes;
DataPipe data_pipe(options);
WriteDataRaw(data_pipe.producer_handle.get(),
data,
&num_bytes,
MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
web_socket_->Send(
fin,
ConvertTo<WebSocket::MessageType>(type),
data_pipe.consumer_handle.Pass());
}
void WebSocketHandleImpl::flowControl(int64_t quota) {
if (!client_)
return;
web_socket_->FlowControl(quota);
}
void WebSocketHandleImpl::close(unsigned short code, const WebString& reason) {
did_close_ = true;
web_socket_->Close(code, reason.utf8());
}
} // namespace mojo
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef MOJO_SERVICES_HTML_VIEWER_WEBSOCKETHANDLE_IMPL_H_
#define MOJO_SERVICES_HTML_VIEWER_WEBSOCKETHANDLE_IMPL_H_
#include "base/memory/scoped_ptr.h"
#include "base/memory/weak_ptr.h"
#include "mojo/common/handle_watcher.h"
#include "mojo/services/public/interfaces/network/web_socket.mojom.h"
#include "third_party/WebKit/public/platform/WebSocketHandle.h"
namespace mojo {
class NetworkService;
class WebSocketClientImpl;
// Implements WebSocketHandle by talking to the mojo WebSocket interface.
class WebSocketHandleImpl : public blink::WebSocketHandle {
public:
explicit WebSocketHandleImpl(NetworkService* network_service);
private:
virtual ~WebSocketHandleImpl();
// blink::WebSocketHandle methods:
virtual void connect(const blink::WebURL& url,
const blink::WebVector<blink::WebString>& protocols,
const blink::WebSerializedOrigin& origin,
blink::WebSocketHandleClient*) OVERRIDE;
virtual void send(bool fin,
MessageType,
const char* data,
size_t size) OVERRIDE;
virtual void flowControl(int64_t quota) OVERRIDE;
virtual void close(unsigned short code,
const blink::WebString& reason) OVERRIDE;
WebSocketPtr web_socket_;
scoped_ptr<WebSocketClientImpl> client_;
// True if close() was called.
bool did_close_;
DISALLOW_COPY_AND_ASSIGN(WebSocketHandleImpl);
};
} // namespace mojo
#endif // MOJO_SERVICES_HTML_VIEWER_WEBSOCKETHANDLE_IMPL_H_
......@@ -38,5 +38,7 @@ source_set("lib") {
"network_service_impl.h",
"url_loader_impl.cc",
"url_loader_impl.h",
"web_socket_impl.cc",
"web_socket_impl.h",
]
}
......@@ -7,6 +7,7 @@
#include "mojo/public/cpp/application/application_connection.h"
#include "mojo/services/network/cookie_store_impl.h"
#include "mojo/services/network/url_loader_impl.h"
#include "mojo/services/network/web_socket_impl.h"
namespace mojo {
......@@ -28,4 +29,8 @@ void NetworkServiceImpl::GetCookieStore(InterfaceRequest<CookieStore> store) {
BindToRequest(new CookieStoreImpl(context_, origin_), &store);
}
void NetworkServiceImpl::CreateWebSocket(InterfaceRequest<WebSocket> socket) {
BindToRequest(new WebSocketImpl(context_), &socket);
}
} // namespace mojo
......@@ -23,6 +23,7 @@ class NetworkServiceImpl : public InterfaceImpl<NetworkService> {
// NetworkService methods:
virtual void CreateURLLoader(InterfaceRequest<URLLoader> loader) OVERRIDE;
virtual void GetCookieStore(InterfaceRequest<CookieStore> store) OVERRIDE;
virtual void CreateWebSocket(InterfaceRequest<WebSocket> socket) OVERRIDE;
private:
NetworkContext* context_;
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "mojo/services/network/web_socket_impl.h"
#include "base/logging.h"
#include "mojo/services/network/network_context.h"
#include "net/websockets/websocket_channel.h"
#include "net/websockets/websocket_errors.h"
#include "net/websockets/websocket_event_interface.h"
#include "net/websockets/websocket_frame.h" // for WebSocketFrameHeader::OpCode
#include "net/websockets/websocket_handshake_request_info.h"
#include "net/websockets/websocket_handshake_response_info.h"
#include "url/origin.h"
namespace mojo {
template <>
struct TypeConverter<net::WebSocketFrameHeader::OpCode,
WebSocket::MessageType> {
static net::WebSocketFrameHeader::OpCode Convert(
WebSocket::MessageType type) {
DCHECK(type == WebSocket::MESSAGE_TYPE_CONTINUATION ||
type == WebSocket::MESSAGE_TYPE_TEXT ||
type == WebSocket::MESSAGE_TYPE_BINARY);
typedef net::WebSocketFrameHeader::OpCode OpCode;
// These compile asserts verify that the same underlying values are used for
// both types, so we can simply cast between them.
COMPILE_ASSERT(static_cast<OpCode>(WebSocket::MESSAGE_TYPE_CONTINUATION) ==
net::WebSocketFrameHeader::kOpCodeContinuation,
enum_values_must_match_for_opcode_continuation);
COMPILE_ASSERT(static_cast<OpCode>(WebSocket::MESSAGE_TYPE_TEXT) ==
net::WebSocketFrameHeader::kOpCodeText,
enum_values_must_match_for_opcode_text);
COMPILE_ASSERT(static_cast<OpCode>(WebSocket::MESSAGE_TYPE_BINARY) ==
net::WebSocketFrameHeader::kOpCodeBinary,
enum_values_must_match_for_opcode_binary);
return static_cast<OpCode>(type);
}
};
template <>
struct TypeConverter<WebSocket::MessageType,
net::WebSocketFrameHeader::OpCode> {
static WebSocket::MessageType Convert(
net::WebSocketFrameHeader::OpCode type) {
DCHECK(type == net::WebSocketFrameHeader::kOpCodeContinuation ||
type == net::WebSocketFrameHeader::kOpCodeText ||
type == net::WebSocketFrameHeader::kOpCodeBinary);
return static_cast<WebSocket::MessageType>(type);
}
};
namespace {
typedef net::WebSocketEventInterface::ChannelState ChannelState;
struct WebSocketEventHandler : public net::WebSocketEventInterface {
public:
WebSocketEventHandler(WebSocketClientPtr client)
: client_(client.Pass()) {
}
virtual ~WebSocketEventHandler() {}
private:
// net::WebSocketEventInterface methods:
virtual ChannelState OnAddChannelResponse(
bool fail,
const std::string& selected_subprotocol,
const std::string& extensions) OVERRIDE;
virtual ChannelState OnDataFrame(bool fin,
WebSocketMessageType type,
const std::vector<char>& data) OVERRIDE;
virtual ChannelState OnClosingHandshake() OVERRIDE;
virtual ChannelState OnFlowControl(int64 quota) OVERRIDE;
virtual ChannelState OnDropChannel(bool was_clean,
uint16 code,
const std::string& reason) OVERRIDE;
virtual ChannelState OnFailChannel(const std::string& message) OVERRIDE;
virtual ChannelState OnStartOpeningHandshake(
scoped_ptr<net::WebSocketHandshakeRequestInfo> request) OVERRIDE;
virtual ChannelState OnFinishOpeningHandshake(
scoped_ptr<net::WebSocketHandshakeResponseInfo> response) OVERRIDE;
virtual ChannelState OnSSLCertificateError(
scoped_ptr<net::WebSocketEventInterface::SSLErrorCallbacks> callbacks,
const GURL& url,
const net::SSLInfo& ssl_info,
bool fatal) OVERRIDE;
WebSocketClientPtr client_;
DISALLOW_COPY_AND_ASSIGN(WebSocketEventHandler);
};
ChannelState WebSocketEventHandler::OnAddChannelResponse(
bool fail,
const std::string& selected_protocol,
const std::string& extensions) {
client_->DidConnect(fail, selected_protocol, extensions);
return WebSocketEventInterface::CHANNEL_ALIVE;
}
ChannelState WebSocketEventHandler::OnDataFrame(
bool fin,
net::WebSocketFrameHeader::OpCode type,
const std::vector<char>& data) {
// TODO(mpcomplete): reuse the data pipe for subsequent frames.
uint32_t num_bytes = static_cast<uint32_t>(data.size());
MojoCreateDataPipeOptions options;
options.struct_size = sizeof(MojoCreateDataPipeOptions);
options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE;
options.element_num_bytes = 1;
options.capacity_num_bytes = num_bytes;
DataPipe data_pipe(options);
WriteDataRaw(data_pipe.producer_handle.get(),
&data[0],
&num_bytes,
MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
client_->DidReceiveData(fin, ConvertTo<WebSocket::MessageType>(type),
data_pipe.consumer_handle.Pass());
return WebSocketEventInterface::CHANNEL_ALIVE;
}
ChannelState WebSocketEventHandler::OnClosingHandshake() {
return WebSocketEventInterface::CHANNEL_ALIVE;
}
ChannelState WebSocketEventHandler::OnFlowControl(int64 quota) {
client_->DidReceiveFlowControl(quota);
return WebSocketEventInterface::CHANNEL_ALIVE;
}
ChannelState WebSocketEventHandler::OnDropChannel(bool was_clean,
uint16 code,
const std::string& reason) {
return WebSocketEventInterface::CHANNEL_ALIVE;
}
ChannelState WebSocketEventHandler::OnFailChannel(const std::string& message) {
return WebSocketEventInterface::CHANNEL_ALIVE;
}
ChannelState WebSocketEventHandler::OnStartOpeningHandshake(
scoped_ptr<net::WebSocketHandshakeRequestInfo> request) {
return WebSocketEventInterface::CHANNEL_ALIVE;
}
ChannelState WebSocketEventHandler::OnFinishOpeningHandshake(
scoped_ptr<net::WebSocketHandshakeResponseInfo> response) {
return WebSocketEventInterface::CHANNEL_ALIVE;
}
ChannelState WebSocketEventHandler::OnSSLCertificateError(
scoped_ptr<net::WebSocketEventInterface::SSLErrorCallbacks> callbacks,
const GURL& url,
const net::SSLInfo& ssl_info,
bool fatal) {
// The above method is always asynchronous.
return WebSocketEventInterface::CHANNEL_ALIVE;
}
} // namespace mojo
WebSocketImpl::WebSocketImpl(NetworkContext* context) : context_(context) {
}
WebSocketImpl::~WebSocketImpl() {
}
void WebSocketImpl::Connect(const String& url,
Array<String> protocols,
const String& origin,
WebSocketClientPtr client) {
DCHECK(!channel_);
scoped_ptr<net::WebSocketEventInterface> event_interface(
new WebSocketEventHandler(client.Pass()));
channel_.reset(new net::WebSocketChannel(event_interface.Pass(),
context_->url_request_context()));
channel_->SendAddChannelRequest(GURL(url.get()),
protocols.To<std::vector<std::string> >(),
url::Origin(origin.get()));
}
void WebSocketImpl::Send(bool fin,
WebSocket::MessageType type,
ScopedDataPipeConsumerHandle data_pipe) {
DCHECK(channel_);
uint32_t num_bytes;
ReadDataRaw(data_pipe.get(), NULL, &num_bytes, MOJO_READ_DATA_FLAG_QUERY);
std::vector<char> data(num_bytes);
ReadDataRaw(data_pipe.get(), &data[0], &num_bytes, MOJO_READ_DATA_FLAG_NONE);
channel_->SendFrame(
fin, ConvertTo<net::WebSocketFrameHeader::OpCode>(type), data);
}
void WebSocketImpl::FlowControl(int64_t quota) {
DCHECK(channel_);
channel_->SendFlowControl(quota);
}
void WebSocketImpl::Close(int16_t code, const String& reason) {
DCHECK(channel_);
channel_->StartClosingHandshake(code, reason);
}
} // namespace mojo
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef MOJO_SERVICES_NETWORK_WEB_SOCKET_IMPL_H_
#define MOJO_SERVICES_NETWORK_WEB_SOCKET_IMPL_H_
#include "base/compiler_specific.h"
#include "base/memory/scoped_ptr.h"
#include "mojo/public/cpp/bindings/interface_impl.h"
#include "mojo/services/public/interfaces/network/web_socket.mojom.h"
namespace net {
class WebSocketChannel;
} // namespace net
namespace mojo {
class NetworkContext;
// Forms a bridge between the WebSocket mojo interface and the net::WebSocket
// implementation.
class WebSocketImpl : public InterfaceImpl<WebSocket> {
public:
explicit WebSocketImpl(NetworkContext* context);
virtual ~WebSocketImpl();
private:
class PendingWriteToDataPipe;
class DependentIOBuffer;
// WebSocket methods:
virtual void Connect(const String& url,
Array<String> protocols,
const String& origin,
WebSocketClientPtr client) OVERRIDE;
virtual void Send(bool fin,
WebSocket::MessageType type,
ScopedDataPipeConsumerHandle data) OVERRIDE;
virtual void FlowControl(int64_t quota) OVERRIDE;
virtual void Close(int16_t code, const String& reason) OVERRIDE;
// The channel we use to send events to the network.
scoped_ptr<net::WebSocketChannel> channel_;
NetworkContext* context_;
};
} // namespace mojo
#endif // MOJO_SERVICES_NETWORK_WEB_SOCKET_IMPL_H_
......@@ -11,5 +11,6 @@ mojom("network") {
"network_error.mojom",
"network_service.mojom",
"url_loader.mojom",
"web_socket.mojom",
]
}
......@@ -4,6 +4,7 @@
import "mojo/services/public/interfaces/network/cookie_store.mojom"
import "mojo/services/public/interfaces/network/url_loader.mojom"
import "mojo/services/public/interfaces/network/web_socket.mojom"
module mojo {
......@@ -12,6 +13,8 @@ interface NetworkService {
GetCookieStore(CookieStore&? cookie_store);
CreateWebSocket(WebSocket& socket);
// TODO(darin): Add other methods here.
};
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import "mojo/services/public/interfaces/network/network_error.mojom"
module mojo {
interface WebSocket {
enum MessageType {
CONTINUATION,
TEXT,
BINARY
};
const int16 kAbnormalCloseCode = 1006; // stolen from websocket_bridge
Connect(
string url, string[] protocols, string origin, WebSocketClient client);
Send(bool fin, MessageType type, handle<data_pipe_consumer> data);
FlowControl(int64 quota);
Close(int16 code, string reason);
};
interface WebSocketClient {
DidConnect(bool fail, string selected_subprotocol, string extensions);
DidReceiveData(
bool fin, WebSocket.MessageType type, handle<data_pipe_consumer> data);
DidReceiveFlowControl(int64 quota);
// TODO(mpcomplete): add these methods from blink:
// void didStartOpeningHandshake(WebSocketHandle*, const
// WebSocketHandshakeRequestInfo&) = 0;
//
// void didFinishOpeningHandshake(WebSocketHandle*, const
// WebSocketHandshakeResponseInfo&) = 0;
//
// void didFail(WebSocketHandle* /* handle */, const WebString&
// message) = 0;
//
// void didClose(WebSocketHandle* /* handle */, bool wasClean,
// unsigned short code, const WebString& reason) = 0;
//
// void didStartClosingHandshake(WebSocketHandle*) = 0;
};
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment