2016-04-16 22:44:24 -07:00
|
|
|
/*
|
2016-08-11 20:38:53 -07:00
|
|
|
* Copyright 2016 The PeerConnect Project Authors. All rights reserved.
|
2016-04-16 22:44:24 -07:00
|
|
|
*
|
2016-08-11 20:38:53 -07:00
|
|
|
* Ryan Lee
|
2016-04-16 22:44:24 -07:00
|
|
|
*/
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
/*
|
|
|
|
Reference: socket.io-client-cpp
|
|
|
|
|
|
|
|
Copyright (c) 2015, Melo Yao
|
|
|
|
All rights reserved.
|
|
|
|
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
|
|
in the Software without restriction, including without limitation the rights
|
|
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
|
|
furnished to do so, subject to all conditions.
|
|
|
|
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
|
|
copies or substantial portions of the Software.
|
|
|
|
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
SOFTWARE.
|
|
|
|
*/
|
|
|
|
|
2016-05-21 17:52:05 -07:00
|
|
|
#if defined(WEBRTC_WIN)
|
|
|
|
#pragma warning(disable:4503)
|
|
|
|
#endif
|
2016-04-17 16:30:55 -07:00
|
|
|
|
2016-04-16 22:44:24 -07:00
|
|
|
#include <map>
|
2016-04-23 22:16:01 -07:00
|
|
|
#include <list>
|
2016-04-16 22:44:24 -07:00
|
|
|
#include "signalconnection.h"
|
2016-08-16 10:49:24 -07:00
|
|
|
#include "logging.h"
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-08-11 20:38:53 -07:00
|
|
|
namespace pc {
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
Signal::Signal() :
|
|
|
|
con_state_(con_closed),
|
|
|
|
network_thread_(),
|
|
|
|
reconn_attempts_(0xFFFFFFFF),
|
|
|
|
reconn_made_(0),
|
|
|
|
reconn_delay_(5000),
|
|
|
|
reconn_delay_max_(25000) {
|
|
|
|
|
2016-05-08 21:09:14 -07:00
|
|
|
#if _DEBUG || DEBUG
|
2016-05-16 00:13:47 -07:00
|
|
|
client_.clear_access_channels(websocketpp::log::alevel::all);
|
|
|
|
client_.set_access_channels(websocketpp::log::alevel::fail);
|
2016-05-07 00:02:25 -07:00
|
|
|
#else
|
2016-06-02 00:36:15 -07:00
|
|
|
client_.clear_access_channels(websocketpp::log::elevel::all);
|
2016-08-11 20:38:53 -07:00
|
|
|
client_.clear_error_channels(websocketpp::log::alevel::fail);
|
2016-04-17 16:30:55 -07:00
|
|
|
#endif
|
2016-04-16 22:44:24 -07:00
|
|
|
|
|
|
|
// Initialize ASIO
|
|
|
|
client_.init_asio();
|
|
|
|
|
|
|
|
// Bind the handlers we are using
|
|
|
|
using websocketpp::lib::placeholders::_1;
|
|
|
|
using websocketpp::lib::placeholders::_2;
|
|
|
|
using websocketpp::lib::bind;
|
2016-04-17 16:30:55 -07:00
|
|
|
|
|
|
|
client_.set_open_handler(bind(&Signal::OnOpen, this, _1));
|
|
|
|
client_.set_close_handler(bind(&Signal::OnClose, this, _1));
|
|
|
|
client_.set_fail_handler(bind(&Signal::OnFail, this, _1));
|
|
|
|
client_.set_message_handler(bind(&Signal::OnMessage, this, _1, _2));
|
|
|
|
client_.set_tls_init_handler(bind(&Signal::OnTlsInit, this, _1));
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
Signal::~Signal() {
|
2016-06-02 02:04:07 -07:00
|
|
|
#if _DEBUG || DEBUG
|
|
|
|
client_.clear_access_channels(websocketpp::log::alevel::all);
|
|
|
|
client_.set_access_channels(websocketpp::log::alevel::fail);
|
|
|
|
#else
|
|
|
|
client_.clear_access_channels(websocketpp::log::elevel::all);
|
|
|
|
client_.clear_access_channels(websocketpp::log::alevel::all);
|
|
|
|
#endif
|
|
|
|
|
2016-05-24 09:59:25 -07:00
|
|
|
Teardown();
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-05-21 16:43:10 -07:00
|
|
|
void Signal::SetConfig(const std::string& url) {
|
2016-04-16 22:44:24 -07:00
|
|
|
url_ = url;
|
2016-05-21 12:27:55 -07:00
|
|
|
|
|
|
|
// Default settings
|
|
|
|
if (url_.empty()) {
|
|
|
|
url_ = "wss://signal.throughnet.com/hello";
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-05-21 16:43:10 -07:00
|
|
|
void Signal::SignIn(const std::string& id, const std::string& password) {
|
|
|
|
user_id_ = id;
|
|
|
|
user_password_ = password;
|
2016-04-17 16:30:55 -07:00
|
|
|
Connect();
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-05-21 17:52:05 -07:00
|
|
|
void Signal::SignOut() {
|
2016-05-24 09:59:25 -07:00
|
|
|
if (opened()) Close();
|
2016-04-23 22:16:01 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 21:36:58 -07:00
|
|
|
void Signal::SendCommand(const std::string channel,
|
2016-04-24 12:40:09 -07:00
|
|
|
const std::string commandname,
|
2016-04-17 21:36:58 -07:00
|
|
|
const Json::Value& data) {
|
|
|
|
|
2016-04-24 12:40:09 -07:00
|
|
|
if (commandname.empty()) {
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_WARNING) << "SendCommand with empty commandname";
|
2016-04-17 21:36:58 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-05-24 09:59:25 -07:00
|
|
|
if (!opened()) {
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_WARNING) << "Signal server is not opened";
|
2016-05-24 09:59:25 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-17 21:36:58 -07:00
|
|
|
Json::Value message;
|
|
|
|
Json::FastWriter writer;
|
2016-04-24 12:40:09 -07:00
|
|
|
message["command"] = commandname;
|
2016-04-17 21:36:58 -07:00
|
|
|
message["data"] = data;
|
|
|
|
if (!channel.empty()) message["channel"] = channel;
|
2016-04-24 12:40:09 -07:00
|
|
|
|
2016-05-21 11:39:56 -07:00
|
|
|
try {
|
|
|
|
client_.send(con_hdl_, writer.write(message), websocketpp::frame::opcode::text);
|
|
|
|
}
|
|
|
|
catch (websocketpp::lib::error_code& ec) {
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_ERROR) << "SendCommand Error: " << ec;
|
2016-05-21 11:39:56 -07:00
|
|
|
}
|
|
|
|
catch (std::exception& e) {
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_ERROR) << "SendCommand Error: " << e.what();
|
2016-05-21 11:39:56 -07:00
|
|
|
}
|
|
|
|
catch (...) {
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_ERROR) << "SendCommand Error: ";
|
2016-05-21 11:39:56 -07:00
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-24 12:40:09 -07:00
|
|
|
void Signal::SendGlobalCommand(const std::string commandname,
|
2016-04-17 21:36:58 -07:00
|
|
|
const Json::Value& data) {
|
2016-04-24 12:40:09 -07:00
|
|
|
SendCommand("", commandname, data);
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
|
|
|
|
|
|
|
|
void Signal::Connect()
|
|
|
|
{
|
|
|
|
if (reconn_timer_)
|
|
|
|
{
|
|
|
|
reconn_timer_->cancel();
|
|
|
|
reconn_timer_.reset();
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
if (network_thread_)
|
|
|
|
{
|
|
|
|
if (con_state_ == con_closing || con_state_ == con_closed)
|
|
|
|
{
|
|
|
|
//if client is closing, join to wait.
|
|
|
|
//if client is closed, still need to join,
|
|
|
|
//but in closed case,join will return immediately.
|
|
|
|
network_thread_->join();
|
|
|
|
network_thread_.reset();//defensive
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
//if we are connected, do nothing.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
con_state_ = con_opening;
|
|
|
|
reconn_made_ = 0;
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
this->ResetState();
|
|
|
|
client_.get_io_service().dispatch(websocketpp::lib::bind(&Signal::ConnectInternal, this));
|
|
|
|
network_thread_.reset(new websocketpp::lib::thread(websocketpp::lib::bind(&Signal::RunLoop, this)));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Signal::Close()
|
|
|
|
{
|
|
|
|
con_state_ = con_closing;
|
|
|
|
client_.get_io_service().dispatch(websocketpp::lib::bind(&Signal::CloseInternal,
|
2016-05-21 17:52:05 -07:00
|
|
|
this,
|
|
|
|
websocketpp::close::status::normal,
|
|
|
|
"End by user"));
|
2016-04-17 16:30:55 -07:00
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
void Signal::SyncClose()
|
|
|
|
{
|
|
|
|
con_state_ = con_closing;
|
|
|
|
client_.get_io_service().dispatch(websocketpp::lib::bind(&Signal::CloseInternal,
|
2016-05-21 17:52:05 -07:00
|
|
|
this,
|
|
|
|
websocketpp::close::status::normal,
|
|
|
|
"End by user"));
|
2016-04-17 16:30:55 -07:00
|
|
|
if (network_thread_)
|
|
|
|
{
|
|
|
|
network_thread_->join();
|
|
|
|
network_thread_.reset();
|
|
|
|
}
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-05-24 09:59:25 -07:00
|
|
|
void Signal::Teardown()
|
|
|
|
{
|
2016-08-11 20:38:53 -07:00
|
|
|
// TODO: Asyncronous close with PeerConnect::Stop()
|
2016-05-24 09:59:25 -07:00
|
|
|
SyncClose();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
asio::io_service& Signal::GetIoService()
|
|
|
|
{
|
|
|
|
return client_.get_io_service();
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
|
|
|
|
|
2016-04-17 21:36:58 -07:00
|
|
|
void Signal::SendSignInCommand() {
|
|
|
|
Json::Value data;
|
|
|
|
|
|
|
|
data["user_id"] = user_id_;
|
|
|
|
data["user_password"] = user_password_;
|
|
|
|
|
|
|
|
SendGlobalCommand("signin", data);
|
|
|
|
}
|
|
|
|
|
2016-04-24 12:40:09 -07:00
|
|
|
void Signal::OnCommandReceived(Json::Value& message) {
|
|
|
|
SignalOnCommandReceived_(message);
|
2016-04-23 22:16:01 -07:00
|
|
|
return;
|
|
|
|
}
|
2016-04-17 16:30:55 -07:00
|
|
|
|
|
|
|
void Signal::RunLoop()
|
|
|
|
{
|
|
|
|
client_.run();
|
|
|
|
client_.reset();
|
|
|
|
client_.get_alog().write(websocketpp::log::alevel::devel,
|
|
|
|
"run loop end");
|
|
|
|
}
|
|
|
|
|
|
|
|
void Signal::ConnectInternal()
|
|
|
|
{
|
|
|
|
websocketpp::lib::error_code ec;
|
|
|
|
client_type::connection_ptr con = client_.get_connection(url_, ec);
|
|
|
|
if (ec) {
|
|
|
|
client_.get_alog().write(websocketpp::log::alevel::app,
|
|
|
|
"Get Connection Error: " + ec.message());
|
|
|
|
return;
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
client_.connect(con);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
void Signal::CloseInternal(websocketpp::close::status::value const& code, std::string const& reason)
|
|
|
|
{
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_WARNING) << "Close by reason:" << reason;
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
if (reconn_timer_)
|
|
|
|
{
|
|
|
|
reconn_timer_->cancel();
|
|
|
|
reconn_timer_.reset();
|
|
|
|
}
|
|
|
|
if (con_hdl_.expired())
|
|
|
|
{
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_ERROR) << "Error: No active session";
|
2016-04-17 16:30:55 -07:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
websocketpp::lib::error_code ec;
|
|
|
|
client_.close(con_hdl_, code, reason, ec);
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
2016-04-17 16:30:55 -07:00
|
|
|
|
|
|
|
void Signal::TimeoutReconnect(websocketpp::lib::asio::error_code const& ec)
|
|
|
|
{
|
|
|
|
if (ec)
|
|
|
|
{
|
2016-04-16 22:44:24 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
if (con_state_ == con_closed)
|
|
|
|
{
|
|
|
|
con_state_ = con_opening;
|
|
|
|
reconn_made_++;
|
|
|
|
this->ResetState();
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_WARNING) << "Reconnecting..";
|
2016-04-17 16:30:55 -07:00
|
|
|
client_.get_io_service().dispatch(websocketpp::lib::bind(&Signal::ConnectInternal, this));
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
unsigned Signal::NextDelay() const
|
|
|
|
{
|
|
|
|
//no jitter, fixed power root.
|
|
|
|
unsigned reconn_made = std::min<unsigned>(reconn_made_, 32);//protect the pow result to be too big.
|
|
|
|
return static_cast<unsigned>(std::min<double>(reconn_delay_ * pow(1.5, reconn_made), reconn_delay_max_));
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
|
|
|
|
void Signal::OnFail(websocketpp::connection_hdl con)
|
|
|
|
{
|
|
|
|
con_hdl_.reset();
|
|
|
|
con_state_ = con_closed;
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_ERROR) << "Connection failed.";
|
2016-04-17 16:30:55 -07:00
|
|
|
|
|
|
|
if (reconn_made_<reconn_attempts_)
|
|
|
|
{
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_WARNING) << "Reconnect for attempt:" << reconn_made_;
|
2016-04-17 16:30:55 -07:00
|
|
|
unsigned delay = this->NextDelay();
|
|
|
|
reconn_timer_.reset(new asio::steady_timer(client_.get_io_service()));
|
|
|
|
websocketpp::lib::asio::error_code ec;
|
|
|
|
reconn_timer_->expires_from_now(websocketpp::lib::asio::milliseconds(delay), ec);
|
|
|
|
reconn_timer_->async_wait(websocketpp::lib::bind(&Signal::TimeoutReconnect, this, websocketpp::lib::placeholders::_1));
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
void Signal::OnOpen(websocketpp::connection_hdl con)
|
|
|
|
{
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_WARNING) << "Connected.";
|
2016-04-17 16:30:55 -07:00
|
|
|
con_state_ = con_opened;
|
|
|
|
con_hdl_ = con;
|
|
|
|
reconn_made_ = 0;
|
2016-04-17 21:36:58 -07:00
|
|
|
|
|
|
|
SendSignInCommand();
|
2016-04-17 16:30:55 -07:00
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
void Signal::OnClose(websocketpp::connection_hdl con)
|
|
|
|
{
|
|
|
|
con_state_ = con_closed;
|
|
|
|
websocketpp::lib::error_code ec;
|
|
|
|
websocketpp::close::status::value code = websocketpp::close::status::normal;
|
|
|
|
client_type::connection_ptr conn_ptr = client_.get_con_from_hdl(con, ec);
|
|
|
|
if (ec) {
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_ERROR) << "OnClose get conn failed" << ec;
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
2016-04-17 16:30:55 -07:00
|
|
|
else
|
|
|
|
{
|
|
|
|
code = conn_ptr->get_local_close_code();
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
con_hdl_.reset();
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-05-24 09:59:25 -07:00
|
|
|
SignalOnClosed_(code);
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
if (code == websocketpp::close::status::normal)
|
|
|
|
{
|
|
|
|
// NOTHING
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (reconn_made_<reconn_attempts_)
|
2016-04-16 22:44:24 -07:00
|
|
|
{
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_WARNING) << "Reconnect for attempt:" << reconn_made_;
|
2016-04-17 16:30:55 -07:00
|
|
|
unsigned delay = this->NextDelay();
|
|
|
|
reconn_timer_.reset(new websocketpp::lib::asio::steady_timer(client_.get_io_service()));
|
|
|
|
websocketpp::lib::asio::error_code ec;
|
|
|
|
reconn_timer_->expires_from_now(websocketpp::lib::asio::milliseconds(delay), ec);
|
|
|
|
reconn_timer_->async_wait(websocketpp::lib::bind(&Signal::TimeoutReconnect, this, websocketpp::lib::placeholders::_1));
|
|
|
|
return;
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
2016-04-17 16:30:55 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Signal::OnMessage(websocketpp::connection_hdl con, client_type::message_ptr msg)
|
|
|
|
{
|
2016-04-17 21:36:58 -07:00
|
|
|
Json::Reader reader;
|
|
|
|
Json::Value jmessage;
|
|
|
|
|
|
|
|
if (!reader.parse(msg->get_payload(), jmessage)) {
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(WARNING) << "Received unknown message: " << msg->get_payload();
|
2016-04-17 21:36:58 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-24 12:40:09 -07:00
|
|
|
OnCommandReceived(jmessage);
|
2016-04-17 16:30:55 -07:00
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
|
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
void Signal::ResetState()
|
|
|
|
{
|
|
|
|
client_.reset();
|
|
|
|
}
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-04-17 16:30:55 -07:00
|
|
|
Signal::context_ptr Signal::OnTlsInit(websocketpp::connection_hdl conn)
|
|
|
|
{
|
|
|
|
context_ptr ctx = context_ptr(new asio::ssl::context(asio::ssl::context::tlsv1));
|
|
|
|
websocketpp::lib::asio::error_code ec;
|
|
|
|
ctx->set_options(asio::ssl::context::default_workarounds |
|
2016-04-17 21:36:58 -07:00
|
|
|
asio::ssl::context::no_sslv2 |
|
|
|
|
asio::ssl::context::single_dh_use, ec);
|
2016-04-17 16:30:55 -07:00
|
|
|
if (ec)
|
|
|
|
{
|
2016-08-16 10:49:24 -07:00
|
|
|
LOGP(LS_ERROR) << "Init tls failed,reason:" << ec.message();
|
2016-04-16 22:44:24 -07:00
|
|
|
}
|
2016-04-17 16:30:55 -07:00
|
|
|
|
|
|
|
return ctx;
|
|
|
|
}
|
|
|
|
|
2016-04-16 22:44:24 -07:00
|
|
|
|
2016-08-11 20:38:53 -07:00
|
|
|
} // namespace pc
|