2023-02-11 20:49:42 +00:00
|
|
|
// This file Copyright © 2014-2023 Mnemosyne LLC.
|
2022-02-07 16:25:02 +00:00
|
|
|
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
|
2022-01-20 18:27:56 +00:00
|
|
|
// or any future license endorsed by Mnemosyne LLC.
|
|
|
|
// License text can be found in the licenses/ folder.
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2022-01-13 02:13:58 +00:00
|
|
|
#include <string_view>
|
2020-08-11 18:11:55 +00:00
|
|
|
|
2023-04-16 20:34:19 +00:00
|
|
|
#include <fmt/core.h>
|
2023-03-02 06:33:49 +00:00
|
|
|
|
2022-01-13 02:13:58 +00:00
|
|
|
#include "RpcClient.h"
|
2014-12-27 20:03:10 +00:00
|
|
|
|
|
|
|
#include <QApplication>
|
2020-10-31 18:56:12 +00:00
|
|
|
#include <QAuthenticator>
|
2014-12-27 20:03:10 +00:00
|
|
|
#include <QHostAddress>
|
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
#include <QNetworkReply>
|
|
|
|
#include <QNetworkRequest>
|
|
|
|
|
|
|
|
#include <libtransmission/rpcimpl.h>
|
2020-08-11 18:11:55 +00:00
|
|
|
#include <libtransmission/transmission.h>
|
2014-12-27 20:03:10 +00:00
|
|
|
#include <libtransmission/version.h> // LONG_VERSION_STRING
|
|
|
|
|
2020-07-27 04:30:58 +00:00
|
|
|
#include "VariantHelpers.h"
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2020-07-27 04:30:58 +00:00
|
|
|
using ::trqt::variant_helpers::dictAdd;
|
|
|
|
using ::trqt::variant_helpers::dictFind;
|
|
|
|
using ::trqt::variant_helpers::variantInit;
|
|
|
|
|
2015-07-13 00:32:48 +00:00
|
|
|
namespace
|
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
|
2021-08-15 09:41:48 +00:00
|
|
|
char const constexpr* const RequestDataPropertyKey{ "requestData" };
|
|
|
|
char const constexpr* const RequestFutureinterfacePropertyKey{ "requestReplyFutureInterface" };
|
2020-07-28 15:56:40 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
TrVariantPtr createVariant()
|
|
|
|
{
|
2023-08-21 04:15:23 +00:00
|
|
|
return std::make_shared<tr_variant>();
|
2015-07-13 00:32:48 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
} // namespace
|
|
|
|
|
2021-08-15 09:41:48 +00:00
|
|
|
RpcClient::RpcClient(QObject* parent)
|
2023-07-18 15:20:17 +00:00
|
|
|
: QObject{ parent }
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
qRegisterMetaType<TrVariantPtr>("TrVariantPtr");
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
void RpcClient::stop()
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2020-05-27 21:53:12 +00:00
|
|
|
session_ = nullptr;
|
|
|
|
session_id_.clear();
|
|
|
|
url_.clear();
|
2020-07-28 15:56:40 +00:00
|
|
|
request_.reset();
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2020-05-27 21:53:12 +00:00
|
|
|
if (nam_ != nullptr)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2020-05-27 21:53:12 +00:00
|
|
|
nam_->deleteLater();
|
|
|
|
nam_ = nullptr;
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
void RpcClient::start(tr_session* session)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2020-05-27 21:53:12 +00:00
|
|
|
session_ = session;
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2017-04-20 16:02:19 +00:00
|
|
|
void RpcClient::start(QUrl const& url)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2020-05-27 21:53:12 +00:00
|
|
|
url_ = url;
|
2020-07-28 15:56:40 +00:00
|
|
|
request_.reset();
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
bool RpcClient::isLocal() const
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2020-05-27 21:53:12 +00:00
|
|
|
if (session_ != nullptr)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2020-05-27 21:53:12 +00:00
|
|
|
if (QHostAddress(url_.host()).isLoopback())
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
return false;
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2017-04-20 16:02:19 +00:00
|
|
|
QUrl const& RpcClient::url() const
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2020-05-27 21:53:12 +00:00
|
|
|
return url_;
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
RpcResponseFuture RpcClient::exec(tr_quark method, tr_variant* args)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2022-08-28 21:17:07 +00:00
|
|
|
return exec(tr_quark_get_string_view(method), args);
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2020-07-27 04:30:58 +00:00
|
|
|
RpcResponseFuture RpcClient::exec(std::string_view method, tr_variant* args)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2022-07-27 14:03:13 +00:00
|
|
|
TrVariantPtr const json = createVariant();
|
2017-04-19 12:04:45 +00:00
|
|
|
tr_variantInitDict(json.get(), 3);
|
2020-07-27 04:30:58 +00:00
|
|
|
dictAdd(json.get(), TR_KEY_method, method);
|
2017-04-19 12:04:45 +00:00
|
|
|
|
2023-08-21 04:15:23 +00:00
|
|
|
if (args != nullptr) // if args were passed in, use them
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2023-08-21 04:15:23 +00:00
|
|
|
auto* child = tr_variantDictFind(json.get(), TR_KEY_arguments);
|
|
|
|
|
|
|
|
if (child == nullptr)
|
|
|
|
{
|
|
|
|
child = tr_variantDictAddDict(json.get(), TR_KEY_arguments, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::swap(*child, *args);
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
return sendRequest(json);
|
2016-04-19 20:41:59 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
int64_t RpcClient::getNextTag()
|
2016-04-19 20:41:59 +00:00
|
|
|
{
|
2020-05-27 21:53:12 +00:00
|
|
|
return next_tag_++;
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2017-04-20 16:02:19 +00:00
|
|
|
void RpcClient::sendNetworkRequest(TrVariantPtr json, QFutureInterface<RpcResponse> const& promise)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2020-07-28 15:56:40 +00:00
|
|
|
if (!request_)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2020-07-28 15:56:40 +00:00
|
|
|
QNetworkRequest request;
|
|
|
|
request.setUrl(url_);
|
2021-08-15 09:41:48 +00:00
|
|
|
request.setRawHeader(
|
|
|
|
"User-Agent",
|
|
|
|
(QApplication::applicationName() + QLatin1Char('/') + QString::fromUtf8(LONG_VERSION_STRING)).toUtf8());
|
2020-07-28 15:56:40 +00:00
|
|
|
request.setRawHeader("Content-Type", "application/json; charset=UTF-8");
|
|
|
|
if (!session_id_.isEmpty())
|
|
|
|
{
|
|
|
|
request.setRawHeader(TR_RPC_SESSION_ID_HEADER, session_id_.toUtf8());
|
|
|
|
}
|
|
|
|
|
|
|
|
request_ = request;
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2023-08-17 16:02:45 +00:00
|
|
|
auto const json_data = QByteArray::fromStdString(tr_variant_serde::json().compact().to_string(*json));
|
2020-07-28 15:56:40 +00:00
|
|
|
QNetworkReply* reply = networkAccessManager()->post(*request_, json_data);
|
|
|
|
reply->setProperty(RequestDataPropertyKey, QVariant::fromValue(json));
|
|
|
|
reply->setProperty(RequestFutureinterfacePropertyKey, QVariant::fromValue(promise));
|
2015-07-13 00:32:48 +00:00
|
|
|
|
2020-10-31 18:56:12 +00:00
|
|
|
connect(reply, &QNetworkReply::downloadProgress, this, &RpcClient::dataReadProgress);
|
|
|
|
connect(reply, &QNetworkReply::uploadProgress, this, &RpcClient::dataSendProgress);
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2022-02-08 03:56:04 +00:00
|
|
|
if (verbose_)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2021-08-15 09:41:48 +00:00
|
|
|
qInfo() << "sending"
|
|
|
|
<< "POST" << qPrintable(url_.path());
|
2020-09-08 22:20:46 +00:00
|
|
|
|
|
|
|
for (QByteArray const& b : request_->rawHeaderList())
|
|
|
|
{
|
2020-11-02 15:16:12 +00:00
|
|
|
qInfo() << b.constData() << ": " << request_->rawHeader(b).constData();
|
2020-09-08 22:20:46 +00:00
|
|
|
}
|
2017-04-19 12:04:45 +00:00
|
|
|
|
2020-11-02 15:16:12 +00:00
|
|
|
qInfo() << "Body:";
|
|
|
|
qInfo() << json_data.constData();
|
2020-09-08 22:20:46 +00:00
|
|
|
}
|
2016-04-19 20:41:59 +00:00
|
|
|
}
|
|
|
|
|
2017-04-20 16:02:19 +00:00
|
|
|
void RpcClient::sendLocalRequest(TrVariantPtr json, QFutureInterface<RpcResponse> const& promise, int64_t tag)
|
2016-04-19 20:41:59 +00:00
|
|
|
{
|
2023-03-02 06:33:49 +00:00
|
|
|
if (verbose_)
|
|
|
|
{
|
2023-08-17 16:02:45 +00:00
|
|
|
fmt::print("{:s}:{:d} sending req:\n{:s}\n", __FILE__, __LINE__, tr_variant_serde::json().to_string(*json));
|
2023-03-02 06:33:49 +00:00
|
|
|
}
|
|
|
|
|
2023-06-30 19:36:08 +00:00
|
|
|
local_requests_.try_emplace(tag, promise);
|
2020-05-27 21:53:12 +00:00
|
|
|
tr_rpc_request_exec_json(session_, json.get(), localSessionCallback, this);
|
2016-04-19 20:41:59 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
RpcResponseFuture RpcClient::sendRequest(TrVariantPtr json)
|
2016-04-19 20:41:59 +00:00
|
|
|
{
|
2022-07-27 14:03:13 +00:00
|
|
|
int64_t const tag = getNextTag();
|
2020-07-27 04:30:58 +00:00
|
|
|
dictAdd(json.get(), TR_KEY_tag, tag);
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
QFutureInterface<RpcResponse> promise;
|
|
|
|
promise.setExpectedResultCount(1);
|
|
|
|
promise.setProgressRange(0, 1);
|
|
|
|
promise.setProgressValue(0);
|
|
|
|
promise.reportStarted();
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2020-05-27 21:53:12 +00:00
|
|
|
if (session_ != nullptr)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
|
|
|
sendLocalRequest(json, promise, tag);
|
|
|
|
}
|
2020-05-27 21:53:12 +00:00
|
|
|
else if (!url_.isEmpty())
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
|
|
|
sendNetworkRequest(json, promise);
|
|
|
|
}
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
return promise.future();
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
QNetworkAccessManager* RpcClient::networkAccessManager()
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2020-05-27 21:53:12 +00:00
|
|
|
if (nam_ == nullptr)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2023-07-18 15:20:17 +00:00
|
|
|
nam_ = new QNetworkAccessManager{};
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2020-10-31 18:56:12 +00:00
|
|
|
connect(nam_, &QNetworkAccessManager::finished, this, &RpcClient::networkRequestFinished);
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2020-10-31 18:56:12 +00:00
|
|
|
connect(nam_, &QNetworkAccessManager::authenticationRequired, this, &RpcClient::httpAuthenticationRequired);
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2020-05-27 21:53:12 +00:00
|
|
|
return nam_;
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2020-08-11 18:11:55 +00:00
|
|
|
void RpcClient::localSessionCallback(tr_session* s, tr_variant* response, void* vself) noexcept
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2019-11-12 01:37:05 +00:00
|
|
|
Q_UNUSED(s)
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2020-05-20 01:32:51 +00:00
|
|
|
auto* self = static_cast<RpcClient*>(vself);
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2023-03-22 01:23:08 +00:00
|
|
|
if (self->verbose_)
|
|
|
|
{
|
2023-08-17 16:02:45 +00:00
|
|
|
fmt::print("{:s}:{:d} got response:\n{:s}\n", __FILE__, __LINE__, tr_variant_serde::json().to_string(*response));
|
2023-03-22 01:23:08 +00:00
|
|
|
}
|
|
|
|
|
2022-07-27 14:03:13 +00:00
|
|
|
TrVariantPtr const json = createVariant();
|
2023-08-21 04:15:23 +00:00
|
|
|
std::swap(*json, *response);
|
2023-03-22 01:23:08 +00:00
|
|
|
|
2020-07-27 04:30:58 +00:00
|
|
|
variantInit(response, false);
|
2015-07-13 00:32:48 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
// this callback is invoked in the libtransmission thread, so we don't want
|
|
|
|
// to process the response here... let's push it over to the Qt thread.
|
|
|
|
QMetaObject::invokeMethod(self, "localRequestFinished", Qt::QueuedConnection, Q_ARG(TrVariantPtr, json));
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
void RpcClient::networkRequestFinished(QNetworkReply* reply)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
reply->deleteLater();
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2021-08-15 09:41:48 +00:00
|
|
|
auto promise = reply->property(RequestFutureinterfacePropertyKey).value<QFutureInterface<RpcResponse>>();
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2022-02-08 03:56:04 +00:00
|
|
|
if (verbose_)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2020-11-02 15:16:12 +00:00
|
|
|
qInfo() << "http response header:";
|
2017-04-19 12:04:45 +00:00
|
|
|
|
2020-09-08 22:20:46 +00:00
|
|
|
for (QByteArray const& b : reply->rawHeaderList())
|
|
|
|
{
|
2020-11-02 15:16:12 +00:00
|
|
|
qInfo() << b.constData() << ": " << reply->rawHeader(b).constData();
|
2020-09-08 22:20:46 +00:00
|
|
|
}
|
|
|
|
|
2020-11-02 15:16:12 +00:00
|
|
|
qInfo() << "json:";
|
|
|
|
qInfo() << reply->peek(reply->bytesAvailable()).constData();
|
2020-09-08 22:20:46 +00:00
|
|
|
}
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409 &&
|
|
|
|
reply->hasRawHeader(TR_RPC_SESSION_ID_HEADER))
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
// we got a 409 telling us our session id has expired.
|
|
|
|
// update it and resubmit the request.
|
2020-05-27 21:53:12 +00:00
|
|
|
session_id_ = QString::fromUtf8(reply->rawHeader(TR_RPC_SESSION_ID_HEADER));
|
2020-07-28 15:56:40 +00:00
|
|
|
request_.reset();
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2020-07-28 15:56:40 +00:00
|
|
|
sendNetworkRequest(reply->property(RequestDataPropertyKey).value<TrVariantPtr>(), promise);
|
2017-04-19 12:04:45 +00:00
|
|
|
return;
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
emit networkResponse(reply->error(), reply->errorString());
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
if (reply->error() != QNetworkReply::NoError)
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
RpcResponse result;
|
|
|
|
result.networkError = reply->error();
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
promise.setProgressValueAndText(1, reply->errorString());
|
|
|
|
promise.reportFinished(&result);
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
2017-04-19 12:04:45 +00:00
|
|
|
else
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2023-08-17 16:02:45 +00:00
|
|
|
auto const json_data = reply->readAll().trimmed().toStdString();
|
2022-08-05 21:12:45 +00:00
|
|
|
auto const json = createVariant();
|
2023-08-17 16:02:45 +00:00
|
|
|
auto result = RpcResponse{};
|
|
|
|
|
|
|
|
if (auto top = tr_variant_serde::json().parse(json_data); top)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2023-08-17 16:02:45 +00:00
|
|
|
std::swap(*json, *top);
|
2017-04-19 12:04:45 +00:00
|
|
|
result = parseResponseData(*json);
|
|
|
|
}
|
2015-07-13 00:32:48 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
promise.setProgressValue(1);
|
|
|
|
promise.reportFinished(&result);
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
void RpcClient::localRequestFinished(TrVariantPtr response)
|
2016-04-19 20:41:59 +00:00
|
|
|
{
|
2023-06-30 19:36:08 +00:00
|
|
|
if (auto node = local_requests_.extract(parseResponseTag(*response)); node)
|
|
|
|
{
|
|
|
|
auto const result = parseResponseData(*response);
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2023-06-30 19:36:08 +00:00
|
|
|
auto& promise = node.mapped();
|
|
|
|
promise.setProgressRange(0, 1);
|
|
|
|
promise.setProgressValue(1);
|
|
|
|
promise.reportFinished(&result);
|
|
|
|
}
|
2016-04-19 20:41:59 +00:00
|
|
|
}
|
|
|
|
|
2022-09-08 23:26:18 +00:00
|
|
|
int64_t RpcClient::parseResponseTag(tr_variant& response) const
|
2014-12-27 20:03:10 +00:00
|
|
|
{
|
2022-11-15 16:25:12 +00:00
|
|
|
return dictFind<int>(&response, TR_KEY_tag).value_or(-1);
|
2016-04-19 20:41:59 +00:00
|
|
|
}
|
|
|
|
|
2022-09-08 23:26:18 +00:00
|
|
|
RpcResponse RpcClient::parseResponseData(tr_variant& response) const
|
2016-04-19 20:41:59 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
RpcResponse ret;
|
2016-04-19 20:41:59 +00:00
|
|
|
|
2022-09-08 23:26:18 +00:00
|
|
|
if (auto const result = dictFind<QString>(&response, TR_KEY_result); result)
|
2016-04-19 20:41:59 +00:00
|
|
|
{
|
2020-07-27 04:30:58 +00:00
|
|
|
ret.result = *result;
|
|
|
|
ret.success = *result == QStringLiteral("success");
|
2016-04-19 20:41:59 +00:00
|
|
|
}
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2022-09-08 23:26:18 +00:00
|
|
|
if (tr_variant* args = nullptr; tr_variantDictFindDict(&response, TR_KEY_arguments, &args))
|
2016-04-19 20:41:59 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
ret.args = createVariant();
|
2023-08-21 04:15:23 +00:00
|
|
|
std::swap(*ret.args, *args);
|
2020-07-27 04:30:58 +00:00
|
|
|
variantInit(args, false);
|
2016-04-19 20:41:59 +00:00
|
|
|
}
|
2014-12-27 20:03:10 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
return ret;
|
2014-12-27 20:03:10 +00:00
|
|
|
}
|