Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lua HTTP Client API #195

Closed
wants to merge 14 commits into from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mono_crash.*

#VS Files
out/
.vscode/

#Clion Files
cmake-build-debug/
Expand Down
11 changes: 10 additions & 1 deletion include/TLuaEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include "TNetwork.h"
#include "TServer.h"
#include "Http.h"
#include <any>
#include <condition_variable>
#include <filesystem>
Expand All @@ -43,12 +44,13 @@ namespace fs = std::filesystem;
/**
* std::variant means, that TLuaArgTypes may be one of the Types listed as template args
*/
using TLuaArgTypes = std::variant<std::string, int, sol::variadic_args, bool, std::unordered_map<std::string, std::string>>;
using TLuaArgTypes = std::variant<std::string, int, sol::variadic_args, bool, std::unordered_map<std::string, std::string>, sol::nil_t>;
static constexpr size_t TLuaArgTypes_String = 0;
static constexpr size_t TLuaArgTypes_Int = 1;
static constexpr size_t TLuaArgTypes_VariadicArgs = 2;
static constexpr size_t TLuaArgTypes_Bool = 3;
static constexpr size_t TLuaArgTypes_StringStringMap = 4;
static constexpr size_t TLuaArgTypes_Nil = 5;

class TLuaPlugin;

Expand Down Expand Up @@ -98,6 +100,7 @@ class TLuaEngine : public std::enable_shared_from_this<TLuaEngine>, IThreaded {
std::shared_ptr<TLuaResult> Result;
std::vector<TLuaArgTypes> Args;
std::string EventName; // optional, may be empty
std::shared_ptr<sol::reference> FunctionRef;
};

TLuaEngine();
Expand Down Expand Up @@ -226,6 +229,7 @@ class TLuaEngine : public std::enable_shared_from_this<TLuaEngine>, IThreaded {
virtual ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); }
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaArgTypes>& Args);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(std::shared_ptr<sol::reference> FunctionRef, const std::vector<TLuaArgTypes>& Args);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaArgTypes>& Args, const std::string& EventName, CallStrategy Strategy);
void RegisterEvent(const std::string& EventName, const std::string& FunctionName);
void AddPath(const fs::path& Path); // to be added to path and cpath
Expand All @@ -248,6 +252,10 @@ class TLuaEngine : public std::enable_shared_from_this<TLuaEngine>, IThreaded {
sol::table Lua_GetPlayerVehicles(int ID);
std::pair<sol::table, std::string> Lua_GetPositionRaw(int PID, int VID);
sol::table Lua_HttpCreateConnection(const std::string& host, uint16_t port);
void Lua_HttpGet(const std::string& host, const std::string& path, const sol::table& headers, const sol::function& cb);
void Lua_HttpPost(const std::string& host, const std::string& path, const sol::table& body, const sol::table& headers, const sol::function& cb);
void Lua_HttpPost(const std::string& host, const std::string& path, const std::string& body, const std::string& content_type, const sol::table& headers, const sol::function& cb);
void Lua_HttpCallCallback(httplib::Result& response, std::shared_ptr<sol::reference> cb);
sol::table Lua_JsonDecode(const std::string& str);
int Lua_GetPlayerIDByName(const std::string& Name);
sol::table Lua_FS_ListFiles(const std::string& Path);
Expand Down Expand Up @@ -293,6 +301,7 @@ class TLuaEngine : public std::enable_shared_from_this<TLuaEngine>, IThreaded {
std::list<std::shared_ptr<TLuaResult>> mResultsToCheck;
std::mutex mResultsToCheckMutex;
std::condition_variable mResultsToCheckCond;
boost::asio::thread_pool http_pool;
};

// std::any TriggerLuaEvent(const std::string& Event, bool local, TLuaPlugin* Caller, std::shared_ptr<TLuaArg> arg, bool Wait);
150 changes: 127 additions & 23 deletions src/TLuaEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
#include "TLuaEngine.h"
#include "Client.h"
#include "CustomAssert.h"
#include "Http.h"
#include "LuaAPI.h"
#include "TLuaPlugin.h"
#include "sol/object.hpp"
Expand Down Expand Up @@ -624,26 +623,96 @@ std::pair<sol::table, std::string> TLuaEngine::StateThreadData::Lua_GetPositionR
}
}

sol::table TLuaEngine::StateThreadData::Lua_HttpCreateConnection(const std::string& host, uint16_t port) {
auto table = mStateView.create_table();
constexpr const char* InternalClient = "__InternalClient";
table["host"] = host;
table["port"] = port;
auto client = std::make_shared<httplib::Client>(host, port);
table[InternalClient] = client;
table.set_function("Get", [&InternalClient](const sol::table& table, const std::string& path, const sol::table& headers) {
httplib::Headers GetHeaders;
static httplib::Headers table_to_headers(const sol::table& headers) {
auto http_headers = httplib::Headers();
if (!headers.empty()) {
for (const auto& pair : headers) {
if (pair.first.is<std::string>() && pair.second.is<std::string>()) {
GetHeaders.insert(std::pair(pair.first.as<std::string>(), pair.second.as<std::string>()));
http_headers.insert(std::pair(pair.first.as<std::string>(), pair.second.as<std::string>()));
} else {
beammp_lua_error("Http:Get: Expected string-string pairs for headers, got something else, ignoring that header");
beammp_lua_error("HTTP Error: Expected string-string pairs for headers, got something else, ignoring that header");
}
}
auto client = table[InternalClient].get<std::shared_ptr<httplib::Client>>();
client->Get(path.c_str(), GetHeaders);
}

return http_headers;
}

static std::string trim_host(const std::string& host) {
if (host.back() == '/') {
return host.substr(0, host.size() - 1);
} else {
return host;
}
}

void TLuaEngine::StateThreadData::Lua_HttpCallCallback(httplib::Result& response, std::shared_ptr<sol::reference> cb_ref) {
auto args = std::vector<TLuaArgTypes>();
if (response) {
args.push_back(sol::nil);
args.push_back(response->status);
args.push_back(response->body);

auto headers = std::unordered_map<std::string, std::string>();
for (auto& pair : response->headers) {
headers.emplace(pair.first, pair.second);
}
args.push_back(headers);
} else {
auto err = response.error();
args.push_back(httplib::to_string(err));
}
// AS FAR AS I CAN TELL this shared_ptr MUST be dropped immediately, because this function runs in the context of the http thread pool.
// If we hang on to this pointer by using res.WaitUntilReady() this thread will be the last one to drop the pointer and will corrupt
// the lua stack by destructing the sol::object owned by TLuaResult.
// There is still likely a small chance of that, ideally EnqueueFunctionCall wouldn't return any references to sol objects.
auto res = this->EnqueueFunctionCall(cb_ref, args);
}

void TLuaEngine::StateThreadData::Lua_HttpGet(const std::string& host, const std::string& path, const sol::table& headers, const sol::function& cb) {
auto http_headers = table_to_headers(headers);
// This method and Lua_HttpPost create a sol::reference to save the callback function in the lua registry, and then
// wrap it in a shared_ptr because passing any sol object by-value involves accessing the lua state. It is NOT safe
// to derefence this pointer inside the http thread pool
auto cb_ref = std::make_shared<sol::reference>(cb);
boost::asio::post(this->mEngine->http_pool, [this, host, path, http_headers, cb_ref]() {
auto client = httplib::Client(trim_host(host));
client.set_follow_location(true);
auto response = client.Get(path, http_headers);
this->Lua_HttpCallCallback(response, cb_ref);
});
}

void TLuaEngine::StateThreadData::Lua_HttpPost(const std::string& host, const std::string& path, const sol::table& body, const sol::table& headers, const sol::function& cb) {
auto http_headers = table_to_headers(headers);
httplib::Params params;
for (const auto& pair : body) {
if (pair.first.is<std::string>() && pair.second.is<std::string>()) {
params.emplace(pair.first.as<std::string>(), pair.second.as<std::string>());
} else {
beammp_lua_error("Http:Get: Expected string-string pairs for headers, got something else, ignoring that header");
}
}

auto cb_ref = std::make_shared<sol::reference>(cb);
boost::asio::post(this->mEngine->http_pool, [this, host, path, http_headers, cb_ref, params]() {
auto client = httplib::Client(trim_host(host));
client.set_follow_location(true);
auto response = client.Post(path, http_headers, params);
this->Lua_HttpCallCallback(response, cb_ref);
});
}

void TLuaEngine::StateThreadData::Lua_HttpPost(const std::string& host, const std::string& path, const std::string& body, const std::string& content_type, const sol::table& headers, const sol::function& cb) {
auto http_headers = table_to_headers(headers);

auto cb_ref = std::make_shared<sol::reference>(cb);
boost::asio::post(this->mEngine->http_pool, [this, host, path, http_headers, cb_ref, body = std::move(body), content_type = std::move(content_type)]() {
auto client = httplib::Client(trim_host(host));
client.set_follow_location(true);
auto response = client.Post(path, http_headers, body.c_str(), body.length(), content_type);
this->Lua_HttpCallCallback(response, cb_ref);
});
return table;
}

template <typename T>
Expand Down Expand Up @@ -849,9 +918,28 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
});

auto HttpTable = StateView.create_named_table("Http");
HttpTable.set_function("CreateConnection", [this](const std::string& host, uint16_t port) {
return Lua_HttpCreateConnection(host, port);
});
HttpTable.set_function("Get", sol::overload(
[this](const std::string& url, const std::string& path, const sol::function& cb) {
return Lua_HttpGet(url, path, this->mStateView.create_table(), cb);
},
[this](const std::string& url, const std::string& path, const sol::table& headers, const sol::function& cb) {
return Lua_HttpGet(url, path, headers, cb);
}
));
HttpTable.set_function("Post", sol::overload(
[this](const std::string& url, const std::string& path, const sol::table& body, const sol::function& cb) {
return Lua_HttpPost(url, path, body, this->mStateView.create_table(), cb);
},
[this](const std::string& url, const std::string& path, const sol::table& body, const sol::table& headers, const sol::function& cb) {
return Lua_HttpPost(url, path, body, headers, cb);
},
[this](const std::string& url, const std::string& path, const std::string& body, const std::string& content_type, const sol::function& cb) {
return Lua_HttpPost(url, path, body, content_type, this->mStateView.create_table(), cb);
},
[this](const std::string& url, const std::string& path, const std::string& body, const std::string& content_type, const sol::table& headers, const sol::function& cb) {
return Lua_HttpPost(url, path, body, content_type, headers, cb);
}
));

MPTable.create_named("Settings",
"Debug", 0,
Expand Down Expand Up @@ -896,6 +984,8 @@ std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueScript(const TLu

std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaArgTypes>& Args, const std::string& EventName, CallStrategy Strategy) {
// TODO: Document all this
// mStateFunctionQueue needs to be locked here. Calls modifying mStateFunctionQueue from other threads can invalidate this iterator mid-execution
std::unique_lock Lock(mStateFunctionQueueMutex);
decltype(mStateFunctionQueue)::iterator Iter = mStateFunctionQueue.end();
if (Strategy == CallStrategy::BestEffort) {
Iter = std::find_if(mStateFunctionQueue.begin(), mStateFunctionQueue.end(),
Expand All @@ -907,8 +997,7 @@ std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFrom
auto Result = std::make_shared<TLuaResult>();
Result->StateId = mStateId;
Result->Function = FunctionName;
std::unique_lock Lock(mStateFunctionQueueMutex);
mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName });
mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName, NULL });
mStateFunctionQueueCond.notify_all();
return Result;
} else {
Expand All @@ -921,7 +1010,17 @@ std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCall(con
Result->StateId = mStateId;
Result->Function = FunctionName;
std::unique_lock Lock(mStateFunctionQueueMutex);
mStateFunctionQueue.push_back({ FunctionName, Result, Args, "" });
mStateFunctionQueue.push_back({ FunctionName, Result, Args, "", NULL });
mStateFunctionQueueCond.notify_all();
return Result;
}

std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCall(std::shared_ptr<sol::reference> FunctionRef, const std::vector<TLuaArgTypes>& Args) {
auto Result = std::make_shared<TLuaResult>();
Result->StateId = mStateId;
Result->Function = fmt::format("[[function_ref {:p}]]", FunctionRef->pointer());
std::unique_lock Lock(mStateFunctionQueueMutex);
mStateFunctionQueue.push_back({ "anonymous", Result, Args, "", FunctionRef });
mStateFunctionQueueCond.notify_all();
return Result;
}
Expand Down Expand Up @@ -993,8 +1092,9 @@ void TLuaEngine::StateThreadData::operator()() {
// TODO: Use TheQueuedFunction.EventName for errors, warnings, etc
Result->StateId = mStateId;
sol::state_view StateView(mState);
auto Fn = StateView[FnName];
if (Fn.valid() && Fn.get_type() == sol::type::function) {
auto FnRef = TheQueuedFunction.FunctionRef ? *TheQueuedFunction.FunctionRef : StateView[FnName];
if (FnRef.valid() && FnRef.get_type() == sol::type::function) {
sol::function Fn = FnRef;
std::vector<sol::object> LuaArgs;
for (const auto& Arg : Args) {
if (Arg.valueless_by_exception()) {
Expand Down Expand Up @@ -1022,6 +1122,10 @@ void TLuaEngine::StateThreadData::operator()() {
LuaArgs.push_back(sol::make_object(StateView, Table));
break;
}
case TLuaArgTypes_Nil: {
LuaArgs.push_back(sol::lua_nil);
break;
}
default:
beammp_error("Unknown argument type, passed as nil");
break;
Expand Down