|
|
|
@ -3,23 +3,40 @@
|
|
|
|
|
#include "webcc/logger.h"
|
|
|
|
|
|
|
|
|
|
using boost::asio::ip::tcp;
|
|
|
|
|
using namespace std::placeholders;
|
|
|
|
|
|
|
|
|
|
namespace webcc {
|
|
|
|
|
|
|
|
|
|
Client::Client()
|
|
|
|
|
: timer_(io_context_),
|
|
|
|
|
ssl_verify_(true),
|
|
|
|
|
buffer_size_(kBufferSize),
|
|
|
|
|
timeout_(kMaxReadSeconds),
|
|
|
|
|
closed_(false),
|
|
|
|
|
timer_canceled_(false) {
|
|
|
|
|
#if WEBCC_ENABLE_SSL
|
|
|
|
|
|
|
|
|
|
Client::Client(boost::asio::io_context& io_context,
|
|
|
|
|
boost::asio::ssl::context& ssl_context)
|
|
|
|
|
: io_context_(io_context),
|
|
|
|
|
ssl_context_(ssl_context),
|
|
|
|
|
resolver_(io_context),
|
|
|
|
|
deadline_timer_(io_context) {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#else
|
|
|
|
|
|
|
|
|
|
Client::Client(boost::asio::io_context& io_context)
|
|
|
|
|
: io_context_(io_context),
|
|
|
|
|
resolver_(io_context),
|
|
|
|
|
deadline_timer_(io_context) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Error Client::Request(RequestPtr request, bool connect, bool stream) {
|
|
|
|
|
closed_ = false;
|
|
|
|
|
timer_canceled_ = false;
|
|
|
|
|
#endif // WEBCC_ENABLE_SSL
|
|
|
|
|
|
|
|
|
|
Error Client::Request(RequestPtr request, bool stream) {
|
|
|
|
|
LOG_VERB("Request begin");
|
|
|
|
|
|
|
|
|
|
request_finished_ = false;
|
|
|
|
|
error_ = Error{};
|
|
|
|
|
|
|
|
|
|
request_ = request;
|
|
|
|
|
|
|
|
|
|
length_read_ = 0;
|
|
|
|
|
response_.reset(new Response{});
|
|
|
|
|
response_parser_.Init(response_.get(), stream);
|
|
|
|
|
|
|
|
|
@ -35,246 +52,312 @@ Error Client::Request(RequestPtr request, bool connect, bool stream) {
|
|
|
|
|
// have Content-Length;
|
|
|
|
|
// - If request.Accept-Encoding is "identity", the response will have
|
|
|
|
|
// Content-Length.
|
|
|
|
|
if (request->method() == methods::kHead) {
|
|
|
|
|
if (request_->method() == methods::kHead) {
|
|
|
|
|
response_parser_.set_ignore_body(true);
|
|
|
|
|
} else {
|
|
|
|
|
// Reset in case the connection is persistent.
|
|
|
|
|
response_parser_.set_ignore_body(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
io_context_.restart();
|
|
|
|
|
|
|
|
|
|
if (connect) {
|
|
|
|
|
// No existing socket connection was specified, create a new one.
|
|
|
|
|
Connect(request);
|
|
|
|
|
|
|
|
|
|
if (error_) {
|
|
|
|
|
return error_;
|
|
|
|
|
}
|
|
|
|
|
if (!connected_) {
|
|
|
|
|
AsyncConnect();
|
|
|
|
|
} else {
|
|
|
|
|
AsyncWrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WriteRequest(request);
|
|
|
|
|
|
|
|
|
|
if (error_) {
|
|
|
|
|
return error_;
|
|
|
|
|
}
|
|
|
|
|
// Wait for the request to be finished.
|
|
|
|
|
std::unique_lock<std::mutex> response_lock{ request_mutex_ };
|
|
|
|
|
request_cv_.wait(response_lock, [=] { return request_finished_; });
|
|
|
|
|
|
|
|
|
|
ReadResponse();
|
|
|
|
|
LOG_VERB("Request end");
|
|
|
|
|
|
|
|
|
|
return error_;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::Close() {
|
|
|
|
|
if (closed_) {
|
|
|
|
|
if (!connected_) {
|
|
|
|
|
//resolver_.cancel(); // TODO
|
|
|
|
|
if (socket_) {
|
|
|
|
|
// Cancel any async operations on the socket.
|
|
|
|
|
LOG_VERB("Close socket");
|
|
|
|
|
socket_->Close();
|
|
|
|
|
// Make sure the current request, if any, could be finished.
|
|
|
|
|
FinishRequest();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closed_ = true;
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Close socket...");
|
|
|
|
|
connected_ = false;
|
|
|
|
|
|
|
|
|
|
if (socket_) {
|
|
|
|
|
LOG_INFO("Shutdown & close socket");
|
|
|
|
|
socket_->Shutdown();
|
|
|
|
|
socket_->Close();
|
|
|
|
|
// Make sure the current request, if any, could be finished.
|
|
|
|
|
FinishRequest();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Socket closed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::Connect(RequestPtr request) {
|
|
|
|
|
if (request->url().scheme() == "https") {
|
|
|
|
|
void Client::AsyncConnect() {
|
|
|
|
|
if (request_->url().scheme() == "https") {
|
|
|
|
|
#if WEBCC_ENABLE_SSL
|
|
|
|
|
socket_.reset(new SslSocket{ io_context_, ssl_verify_ });
|
|
|
|
|
DoConnect(request, "443");
|
|
|
|
|
socket_.reset(new SslSocket{ io_context_, ssl_context_, ssl_verify_ });
|
|
|
|
|
AsyncResolve("443");
|
|
|
|
|
#else
|
|
|
|
|
LOG_ERRO("SSL/HTTPS support is not enabled.");
|
|
|
|
|
error_.Set(Error::kSyntaxError, "SSL/HTTPS is not supported");
|
|
|
|
|
FinishRequest();
|
|
|
|
|
#endif // WEBCC_ENABLE_SSL
|
|
|
|
|
} else {
|
|
|
|
|
socket_.reset(new Socket{ io_context_ });
|
|
|
|
|
DoConnect(request, "80");
|
|
|
|
|
AsyncResolve("80");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::DoConnect(RequestPtr request, const std::string& default_port) {
|
|
|
|
|
tcp::resolver resolver(io_context_);
|
|
|
|
|
|
|
|
|
|
std::string port = request->port();
|
|
|
|
|
void Client::AsyncResolve(const std::string& default_port) {
|
|
|
|
|
std::string port = request_->port();
|
|
|
|
|
if (port.empty()) {
|
|
|
|
|
port = default_port;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_VERB("Resolve host (%s)...", request->host().c_str());
|
|
|
|
|
|
|
|
|
|
boost::system::error_code ec;
|
|
|
|
|
LOG_VERB("Resolve host (%s)", request_->host().c_str());
|
|
|
|
|
|
|
|
|
|
// The protocol depends on the `host`, both V4 and V6 are supported.
|
|
|
|
|
auto endpoints = resolver.resolve(request->host(), port, ec);
|
|
|
|
|
resolver_.async_resolve(request_->host(), port,
|
|
|
|
|
std::bind(&Client::OnResolve, this, _1, _2));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::OnResolve(boost::system::error_code ec,
|
|
|
|
|
tcp::resolver::results_type endpoints) {
|
|
|
|
|
if (ec) {
|
|
|
|
|
LOG_ERRO("Host resolve error (%s): %s, %s.", ec.message().c_str(),
|
|
|
|
|
request->host().c_str(), port.c_str());
|
|
|
|
|
LOG_ERRO("Host resolve error (%s)", ec.message().c_str());
|
|
|
|
|
error_.Set(Error::kResolveError, "Host resolve error");
|
|
|
|
|
FinishRequest();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_VERB("Connect to server...");
|
|
|
|
|
LOG_VERB("Connect socket");
|
|
|
|
|
|
|
|
|
|
// Use sync API directly since we don't need timeout control.
|
|
|
|
|
AsyncWaitDeadlineTimer(connect_timeout_);
|
|
|
|
|
|
|
|
|
|
if (!socket_->Connect(request->host(), endpoints)) {
|
|
|
|
|
error_.Set(Error::kConnectError, "Endpoint connect error");
|
|
|
|
|
Close();
|
|
|
|
|
return;
|
|
|
|
|
socket_->AsyncConnect(request_->host(), endpoints,
|
|
|
|
|
std::bind(&Client::OnConnect, this, _1, _2));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::OnConnect(boost::system::error_code ec, tcp::endpoint) {
|
|
|
|
|
LOG_VERB("On connect");
|
|
|
|
|
|
|
|
|
|
StopDeadlineTimer();
|
|
|
|
|
|
|
|
|
|
if (ec) {
|
|
|
|
|
if (ec == boost::asio::error::operation_aborted) {
|
|
|
|
|
// Socket has been closed by OnDeadlineTimer() or Close().
|
|
|
|
|
LOG_WARN("Connect operation aborted");
|
|
|
|
|
} else {
|
|
|
|
|
LOG_INFO("Connect error");
|
|
|
|
|
// No need to close socket since no async operation is on it.
|
|
|
|
|
// socket_->Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_VERB("Socket connected.");
|
|
|
|
|
}
|
|
|
|
|
error_.Set(Error::kConnectError, "Socket connect error");
|
|
|
|
|
FinishRequest();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::WriteRequest(RequestPtr request) {
|
|
|
|
|
LOG_VERB("HTTP request:\n%s", request->Dump().c_str());
|
|
|
|
|
LOG_INFO("Socket connected");
|
|
|
|
|
|
|
|
|
|
// NOTE:
|
|
|
|
|
// It doesn't make much sense to set a timeout for socket write.
|
|
|
|
|
// I find that it's almost impossible to simulate a situation in the server
|
|
|
|
|
// side to test this timeout.
|
|
|
|
|
connected_ = true;
|
|
|
|
|
|
|
|
|
|
// Use sync API directly since we don't need timeout control.
|
|
|
|
|
AsyncWrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boost::system::error_code ec;
|
|
|
|
|
void Client::AsyncWrite() {
|
|
|
|
|
LOG_VERB("Request:\n%s", request_->Dump().c_str());
|
|
|
|
|
|
|
|
|
|
if (socket_->Write(request->GetPayload(), &ec)) {
|
|
|
|
|
// Write request body.
|
|
|
|
|
auto body = request->body();
|
|
|
|
|
body->InitPayload();
|
|
|
|
|
for (auto p = body->NextPayload(true); !p.empty();
|
|
|
|
|
p = body->NextPayload(true)) {
|
|
|
|
|
if (!socket_->Write(p, &ec)) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
socket_->AsyncWrite(request_->GetPayload(),
|
|
|
|
|
std::bind(&Client::OnWrite, this, _1, _2));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::OnWrite(boost::system::error_code ec, std::size_t length) {
|
|
|
|
|
if (ec) {
|
|
|
|
|
LOG_ERRO("Socket write error (%s).", ec.message().c_str());
|
|
|
|
|
Close();
|
|
|
|
|
error_.Set(Error::kSocketWriteError, "Socket write error");
|
|
|
|
|
HandleWriteError(ec);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Request sent.");
|
|
|
|
|
request_->body()->InitPayload();
|
|
|
|
|
|
|
|
|
|
AsyncWriteBody();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::ReadResponse() {
|
|
|
|
|
LOG_VERB("Read response (timeout: %ds)...", timeout_);
|
|
|
|
|
void Client::AsyncWriteBody() {
|
|
|
|
|
auto p = request_->body()->NextPayload(true);
|
|
|
|
|
|
|
|
|
|
if (!p.empty()) {
|
|
|
|
|
socket_->AsyncWrite(p, std::bind(&Client::OnWriteBody, this, _1, _2));
|
|
|
|
|
} else {
|
|
|
|
|
LOG_INFO("Request send");
|
|
|
|
|
|
|
|
|
|
DoReadResponse();
|
|
|
|
|
// Start the read deadline timer.
|
|
|
|
|
AsyncWaitDeadlineTimer(read_timeout_);
|
|
|
|
|
|
|
|
|
|
if (!error_) {
|
|
|
|
|
LOG_VERB("HTTP response:\n%s", response_->Dump().c_str());
|
|
|
|
|
// Start to read response.
|
|
|
|
|
AsyncRead();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::DoReadResponse() {
|
|
|
|
|
boost::system::error_code ec = boost::asio::error::would_block;
|
|
|
|
|
std::size_t length = 0;
|
|
|
|
|
|
|
|
|
|
// The read handler.
|
|
|
|
|
auto handler = [&ec, &length](boost::system::error_code inner_ec,
|
|
|
|
|
std::size_t inner_length) {
|
|
|
|
|
ec = inner_ec;
|
|
|
|
|
length = inner_length;
|
|
|
|
|
};
|
|
|
|
|
void Client::OnWriteBody(boost::system::error_code ec, std::size_t legnth) {
|
|
|
|
|
if (ec) {
|
|
|
|
|
HandleWriteError(ec);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
ec = boost::asio::error::would_block;
|
|
|
|
|
length = 0;
|
|
|
|
|
// Continue to write the next payload of body.
|
|
|
|
|
AsyncWriteBody();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket_->AsyncReadSome(std::move(handler), &buffer_);
|
|
|
|
|
void Client::HandleWriteError(boost::system::error_code ec) {
|
|
|
|
|
if (ec == boost::asio::error::operation_aborted) {
|
|
|
|
|
// Socket has been closed by OnDeadlineTimer() or Close().
|
|
|
|
|
LOG_WARN("Write operation aborted");
|
|
|
|
|
} else {
|
|
|
|
|
LOG_ERRO("Socket write error (%s)", ec.message().c_str());
|
|
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start the timer.
|
|
|
|
|
DoWaitTimer();
|
|
|
|
|
error_.Set(Error::kSocketWriteError, "Socket write error");
|
|
|
|
|
FinishRequest();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Block until the asynchronous operation has completed.
|
|
|
|
|
do {
|
|
|
|
|
io_context_.run_one();
|
|
|
|
|
} while (ec == boost::asio::error::would_block);
|
|
|
|
|
void Client::AsyncRead() {
|
|
|
|
|
socket_->AsyncReadSome(std::bind(&Client::OnRead, this, _1, _2), &buffer_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop the timer.
|
|
|
|
|
CancelTimer();
|
|
|
|
|
void Client::OnRead(boost::system::error_code ec, std::size_t length) {
|
|
|
|
|
StopDeadlineTimer();
|
|
|
|
|
|
|
|
|
|
// The error normally is caused by timeout. See OnTimer().
|
|
|
|
|
if (ec || length == 0) {
|
|
|
|
|
if (ec) {
|
|
|
|
|
if (ec == boost::asio::error::operation_aborted) {
|
|
|
|
|
// Socket has been closed by OnDeadlineTimer() or Close().
|
|
|
|
|
LOG_WARN("Read operation aborted");
|
|
|
|
|
} else {
|
|
|
|
|
LOG_ERRO("Socket read error (%s)", ec.message().c_str());
|
|
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error_.Set(Error::kSocketReadError, "Socket read error");
|
|
|
|
|
LOG_ERRO("Socket read error (%s).", ec.message().c_str());
|
|
|
|
|
break;
|
|
|
|
|
FinishRequest();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Read data, length: %u.", length);
|
|
|
|
|
length_read_ += length;
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Read length: %u", length);
|
|
|
|
|
|
|
|
|
|
// Parse the piece of data just read.
|
|
|
|
|
if (!response_parser_.Parse(buffer_.data(), length)) {
|
|
|
|
|
LOG_ERRO("Failed to parse the response");
|
|
|
|
|
Close();
|
|
|
|
|
error_.Set(Error::kParseError, "HTTP parse error");
|
|
|
|
|
LOG_ERRO("Failed to parse the HTTP response.");
|
|
|
|
|
break;
|
|
|
|
|
error_.Set(Error::kParseError, "Response parse error");
|
|
|
|
|
FinishRequest();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inform progress callback if it's specified.
|
|
|
|
|
if (progress_callback_) {
|
|
|
|
|
if (response_parser_.header_ended()) {
|
|
|
|
|
// NOTE: Need to get rid of the header length.
|
|
|
|
|
progress_callback_(length_read_ - response_parser_.header_length(),
|
|
|
|
|
response_parser_.content_length());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response_parser_.finished()) {
|
|
|
|
|
// Stop trying to read once all content has been received, because
|
|
|
|
|
// some servers will block extra call to read_some().
|
|
|
|
|
LOG_VERB("Response:\n%s", response_->Dump().c_str());
|
|
|
|
|
|
|
|
|
|
if (response_->IsConnectionKeepAlive()) {
|
|
|
|
|
// Close the timer but keep the socket connection.
|
|
|
|
|
LOG_INFO("Keep the socket connection alive.");
|
|
|
|
|
LOG_INFO("Keep the socket connection alive");
|
|
|
|
|
} else {
|
|
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop reading.
|
|
|
|
|
LOG_INFO("Finished to read the HTTP response.");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// Stop trying to read once all content has been received, because some
|
|
|
|
|
// servers will block extra call to read_some().
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Finished to read the response");
|
|
|
|
|
FinishRequest();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Continue to read the response.
|
|
|
|
|
AsyncRead();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::DoWaitTimer() {
|
|
|
|
|
LOG_VERB("Wait timer asynchronously.");
|
|
|
|
|
timer_.expires_after(std::chrono::seconds(timeout_));
|
|
|
|
|
timer_.async_wait(std::bind(&Client::OnTimer, this, std::placeholders::_1));
|
|
|
|
|
void Client::AsyncWaitDeadlineTimer(int seconds) {
|
|
|
|
|
if (seconds <= 0) {
|
|
|
|
|
deadline_timer_stopped_ = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_VERB("Async wait deadline timer");
|
|
|
|
|
|
|
|
|
|
deadline_timer_stopped_ = false;
|
|
|
|
|
|
|
|
|
|
deadline_timer_.expires_after(std::chrono::seconds(seconds));
|
|
|
|
|
deadline_timer_.async_wait(std::bind(&Client::OnDeadlineTimer, this, _1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::OnTimer(boost::system::error_code ec) {
|
|
|
|
|
LOG_VERB("On timer.");
|
|
|
|
|
void Client::OnDeadlineTimer(boost::system::error_code ec) {
|
|
|
|
|
LOG_VERB("On deadline timer");
|
|
|
|
|
|
|
|
|
|
deadline_timer_stopped_ = true;
|
|
|
|
|
|
|
|
|
|
// timer_.cancel() was called.
|
|
|
|
|
// deadline_timer_.cancel() was called.
|
|
|
|
|
if (ec == boost::asio::error::operation_aborted) {
|
|
|
|
|
LOG_VERB("Timer canceled.");
|
|
|
|
|
LOG_VERB("Deadline timer canceled");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (closed_) {
|
|
|
|
|
LOG_VERB("Socket has been closed.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
LOG_WARN("Timeout");
|
|
|
|
|
|
|
|
|
|
if (timer_.expiry() <= boost::asio::steady_timer::clock_type::now()) {
|
|
|
|
|
// The deadline has passed. The socket is closed so that any outstanding
|
|
|
|
|
// asynchronous operations are canceled.
|
|
|
|
|
LOG_WARN("HTTP client timed out.");
|
|
|
|
|
error_.set_timeout(true);
|
|
|
|
|
// Cancel the async operations on the socket.
|
|
|
|
|
// OnXxx() will be called with `error::operation_aborted`.
|
|
|
|
|
if (connected_) {
|
|
|
|
|
Close();
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
socket_->Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Put the actor back to sleep.
|
|
|
|
|
DoWaitTimer();
|
|
|
|
|
error_.set_timeout(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Client::CancelTimer() {
|
|
|
|
|
if (timer_canceled_) {
|
|
|
|
|
void Client::StopDeadlineTimer() {
|
|
|
|
|
if (deadline_timer_stopped_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Cancel timer...");
|
|
|
|
|
timer_.cancel();
|
|
|
|
|
LOG_INFO("Cancel deadline timer");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Cancel the async wait operation on this timer.
|
|
|
|
|
deadline_timer_.cancel();
|
|
|
|
|
} catch (const boost::system::system_error&) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deadline_timer_stopped_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timer_canceled_ = true;
|
|
|
|
|
void Client::FinishRequest() {
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock{ request_mutex_ };
|
|
|
|
|
if (!request_finished_) {
|
|
|
|
|
request_finished_ = true;
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
request_cv_.notify_one();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace webcc
|
|
|
|
|