mirror of
https://github.com/BeamMP/BeamMP-Server.git
synced 2025-07-01 23:35:41 +00:00
492 lines
20 KiB
C++
492 lines
20 KiB
C++
#include "TConsole.h"
|
|
#include "Common.h"
|
|
#include "Compat.h"
|
|
|
|
#include "Client.h"
|
|
#include "CustomAssert.h"
|
|
#include "LuaAPI.h"
|
|
#include "TLuaEngine.h"
|
|
|
|
#include <ctime>
|
|
#include <sstream>
|
|
|
|
static inline bool StringStartsWith(const std::string& What, const std::string& StartsWith) {
|
|
return What.size() >= StartsWith.size() && What.substr(0, StartsWith.size()) == StartsWith;
|
|
}
|
|
|
|
static inline std::string TrimString(std::string S) {
|
|
S.erase(S.begin(), std::find_if(S.begin(), S.end(), [](unsigned char ch) {
|
|
return !std::isspace(ch);
|
|
}));
|
|
S.erase(std::find_if(S.rbegin(), S.rend(), [](unsigned char ch) {
|
|
return !std::isspace(ch);
|
|
}).base(),
|
|
S.end());
|
|
return S;
|
|
}
|
|
|
|
std::string GetDate() {
|
|
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
|
|
time_t tt = std::chrono::system_clock::to_time_t(now);
|
|
auto local_tm = std::localtime(&tt);
|
|
char buf[30];
|
|
std::string date;
|
|
if (Application::Settings.DebugModeEnabled) {
|
|
std::strftime(buf, sizeof(buf), "[%d/%m/%y %T.", local_tm);
|
|
date += buf;
|
|
auto seconds = std::chrono::time_point_cast<std::chrono::seconds>(now);
|
|
auto fraction = now - seconds;
|
|
size_t ms = std::chrono::duration_cast<std::chrono::milliseconds>(fraction).count();
|
|
char fracstr[5];
|
|
std::sprintf(fracstr, "%03lu", ms);
|
|
date += fracstr;
|
|
date += "] ";
|
|
} else {
|
|
std::strftime(buf, sizeof(buf), "[%d/%m/%y %T] ", local_tm);
|
|
date += buf;
|
|
}
|
|
|
|
return date;
|
|
}
|
|
|
|
void TConsole::BackupOldLog() {
|
|
fs::path Path = "Server.log";
|
|
if (fs::exists(Path)) {
|
|
auto OldLog = Path.filename().stem().string() + ".old.log";
|
|
try {
|
|
fs::rename(Path, OldLog);
|
|
beammp_debug("renamed old log file to '" + OldLog + "'");
|
|
} catch (const std::exception& e) {
|
|
beammp_warn(e.what());
|
|
}
|
|
/*
|
|
int err = 0;
|
|
zip* z = zip_open("ServerLogs.zip", ZIP_CREATE, &err);
|
|
if (!z) {
|
|
std::cerr << GetPlatformAgnosticErrorString() << std::endl;
|
|
return;
|
|
}
|
|
FILE* File = std::fopen(Path.string().c_str(), "r");
|
|
if (!File) {
|
|
std::cerr << GetPlatformAgnosticErrorString() << std::endl;
|
|
return;
|
|
}
|
|
std::vector<uint8_t> Buffer;
|
|
Buffer.resize(fs::file_size(Path));
|
|
std::fread(Buffer.data(), 1, Buffer.size(), File);
|
|
std::fclose(File);
|
|
|
|
auto s = zip_source_buffer(z, Buffer.data(), Buffer.size(), 0);
|
|
|
|
auto TimePoint = fs::last_write_time(Path);
|
|
auto Secs = TimePoint.time_since_epoch().count();
|
|
auto MyTimeT = std::time(&Secs);
|
|
|
|
std::string NewName = Path.stem().string();
|
|
NewName += "_";
|
|
std::string Time;
|
|
Time.resize(32);
|
|
size_t n = strftime(Time.data(), Time.size(), "%F_%H.%M.%S", localtime(&MyTimeT));
|
|
Time.resize(n);
|
|
NewName += Time;
|
|
NewName += ".log";
|
|
|
|
zip_file_add(z, NewName.c_str(), s, 0);
|
|
zip_close(z);
|
|
*/
|
|
}
|
|
}
|
|
|
|
void TConsole::ChangeToLuaConsole(const std::string& LuaStateId) {
|
|
if (!mIsLuaConsole) {
|
|
if (!mLuaEngine) {
|
|
beammp_error("Lua engine not initialized yet, please wait and try again");
|
|
return;
|
|
}
|
|
mLuaEngine->EnsureStateExists(mDefaultStateId, "Console");
|
|
mStateId = LuaStateId;
|
|
mIsLuaConsole = true;
|
|
if (mStateId != mDefaultStateId) {
|
|
Application::Console().WriteRaw("Attached to Lua state '" + mStateId + "'. For help, type `:help`. To detach, type `:detach`");
|
|
mCommandline.set_prompt("lua @" + LuaStateId + "> ");
|
|
} else {
|
|
Application::Console().WriteRaw("Attached to Lua. For help, type `:help`. To detach, type `:detach`");
|
|
mCommandline.set_prompt("lua> ");
|
|
}
|
|
mCachedRegularHistory = mCommandline.history();
|
|
mCommandline.set_history(mCachedLuaHistory);
|
|
}
|
|
}
|
|
|
|
void TConsole::ChangeToRegularConsole() {
|
|
if (mIsLuaConsole) {
|
|
mIsLuaConsole = false;
|
|
if (mStateId != mDefaultStateId) {
|
|
Application::Console().WriteRaw("Detached from Lua state '" + mStateId + "'.");
|
|
} else {
|
|
Application::Console().WriteRaw("Detached from Lua.");
|
|
}
|
|
mCachedLuaHistory = mCommandline.history();
|
|
mCommandline.set_history(mCachedRegularHistory);
|
|
mCommandline.set_prompt("> ");
|
|
mStateId = mDefaultStateId;
|
|
}
|
|
}
|
|
|
|
void TConsole::Command_Lua(const std::string& cmd) {
|
|
if (cmd.size() > 3) {
|
|
auto NewStateId = cmd.substr(4);
|
|
beammp_assert(!NewStateId.empty());
|
|
if (mLuaEngine->HasState(NewStateId)) {
|
|
ChangeToLuaConsole(NewStateId);
|
|
} else {
|
|
Application::Console().WriteRaw("Lua state '" + NewStateId + "' is not a known state. Didn't switch to Lua.");
|
|
}
|
|
} else {
|
|
ChangeToLuaConsole(mDefaultStateId);
|
|
}
|
|
}
|
|
|
|
void TConsole::Command_Help(const std::string&) {
|
|
static constexpr const char* sHelpString = R"(
|
|
Commands:
|
|
help displays this help
|
|
exit shuts down the server
|
|
kick <name> [reason] kicks specified player with an optional reason
|
|
list lists all players and info about them
|
|
say <message> sends the message to all players in chat
|
|
lua [state id] switches to lua, optionally into a specific state id's lua
|
|
status how the server is doing and what it's up to)";
|
|
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
|
|
}
|
|
|
|
void TConsole::Command_Kick(const std::string& cmd) {
|
|
if (cmd.size() > 4) {
|
|
auto Name = cmd.substr(5);
|
|
std::string Reason = "Kicked by server console";
|
|
auto SpacePos = Name.find(' ');
|
|
if (SpacePos != Name.npos) {
|
|
Reason = Name.substr(SpacePos + 1);
|
|
Name = cmd.substr(5, cmd.size() - Reason.size() - 5 - 1);
|
|
}
|
|
beammp_trace("attempt to kick '" + Name + "' for '" + Reason + "'");
|
|
bool Kicked = false;
|
|
auto NameCompare = [](std::string Name1, std::string Name2) -> bool {
|
|
std::for_each(Name1.begin(), Name1.end(), [](char& c) { c = tolower(c); });
|
|
std::for_each(Name2.begin(), Name2.end(), [](char& c) { c = tolower(c); });
|
|
return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1);
|
|
};
|
|
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
|
|
if (!Client.expired()) {
|
|
auto locked = Client.lock();
|
|
if (NameCompare(locked->GetName(), Name)) {
|
|
mLuaEngine->Network().ClientKick(*locked, Reason);
|
|
Kicked = true;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
if (!Kicked) {
|
|
Application::Console().WriteRaw("Error: No player with name matching '" + Name + "' was found.");
|
|
} else {
|
|
Application::Console().WriteRaw("Kicked player '" + Name + "' for reason: '" + Reason + "'.");
|
|
}
|
|
}
|
|
}
|
|
|
|
void TConsole::Command_Say(const std::string& cmd) {
|
|
if (cmd.size() > 3) {
|
|
auto Message = cmd.substr(4);
|
|
LuaAPI::MP::SendChatMessage(-1, Message);
|
|
}
|
|
}
|
|
|
|
void TConsole::Command_List(const std::string&) {
|
|
if (mLuaEngine->Server().ClientCount() == 0) {
|
|
Application::Console().WriteRaw("No players online.");
|
|
} else {
|
|
std::stringstream ss;
|
|
ss << std::left << std::setw(25) << "Name" << std::setw(6) << "ID" << std::setw(6) << "Cars" << std::endl;
|
|
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
|
|
if (!Client.expired()) {
|
|
auto locked = Client.lock();
|
|
ss << std::left << std::setw(25) << locked->GetName()
|
|
<< std::setw(6) << locked->GetID()
|
|
<< std::setw(6) << locked->GetCarCount() << "\n";
|
|
}
|
|
return true;
|
|
});
|
|
auto Str = ss.str();
|
|
Application::Console().WriteRaw(Str.substr(0, Str.size() - 1));
|
|
}
|
|
}
|
|
|
|
void TConsole::Command_Status(const std::string&) {
|
|
std::stringstream Status;
|
|
|
|
size_t CarCount = 0;
|
|
size_t ConnectedCount = 0;
|
|
size_t GuestCount = 0;
|
|
size_t SyncedCount = 0;
|
|
size_t SyncingCount = 0;
|
|
size_t MissedPacketQueueSum = 0;
|
|
int LargestSecondsSinceLastPing = 0;
|
|
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
|
|
if (!Client.expired()) {
|
|
auto Locked = Client.lock();
|
|
CarCount += Locked->GetCarCount();
|
|
ConnectedCount += Locked->IsConnected() ? 1 : 0;
|
|
GuestCount += Locked->IsGuest() ? 1 : 0;
|
|
SyncedCount += Locked->IsSynced() ? 1 : 0;
|
|
SyncingCount += Locked->IsSyncing() ? 1 : 0;
|
|
MissedPacketQueueSum += Locked->MissedPacketQueueSize();
|
|
if (Locked->SecondsSinceLastPing() < LargestSecondsSinceLastPing) {
|
|
LargestSecondsSinceLastPing = Locked->SecondsSinceLastPing();
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
size_t SystemsStarting = 0;
|
|
size_t SystemsGood = 0;
|
|
size_t SystemsBad = 0;
|
|
size_t SystemsShuttingDown = 0;
|
|
size_t SystemsShutdown = 0;
|
|
std::string SystemsBadList {};
|
|
std::string SystemsGoodList {};
|
|
std::string SystemsStartingList {};
|
|
std::string SystemsShuttingDownList {};
|
|
std::string SystemsShutdownList {};
|
|
auto Statuses = Application::GetSubsystemStatuses();
|
|
for (const auto& NameStatusPair : Statuses) {
|
|
switch (NameStatusPair.second) {
|
|
case Application::Status::Good:
|
|
SystemsGood++;
|
|
SystemsGoodList += NameStatusPair.first + ", ";
|
|
break;
|
|
case Application::Status::Bad:
|
|
SystemsBad++;
|
|
SystemsBadList += NameStatusPair.first + ", ";
|
|
break;
|
|
case Application::Status::Starting:
|
|
SystemsStarting++;
|
|
SystemsStartingList += NameStatusPair.first + ", ";
|
|
break;
|
|
case Application::Status::ShuttingDown:
|
|
SystemsShuttingDown++;
|
|
SystemsShuttingDownList += NameStatusPair.first + ", ";
|
|
break;
|
|
case Application::Status::Shutdown:
|
|
SystemsShutdown++;
|
|
SystemsShutdownList += NameStatusPair.first + ", ";
|
|
break;
|
|
}
|
|
}
|
|
// remove ", " at the end
|
|
SystemsBadList = SystemsBadList.substr(0, SystemsBadList.size() - 2);
|
|
SystemsGoodList = SystemsGoodList.substr(0, SystemsGoodList.size() - 2);
|
|
SystemsStartingList = SystemsStartingList.substr(0, SystemsStartingList.size() - 2);
|
|
SystemsShuttingDownList = SystemsShuttingDownList.substr(0, SystemsShuttingDownList.size() - 2);
|
|
SystemsShutdownList = SystemsShutdownList.substr(0, SystemsShutdownList.size() - 2);
|
|
|
|
auto ElapsedTime = mLuaEngine->Server().UptimeTimer.GetElapsedTime();
|
|
|
|
Status << "BeamMP-Server Status:\n"
|
|
<< "\tTotal Players: " << mLuaEngine->Server().ClientCount() << "\n"
|
|
<< "\tSyncing Players: " << SyncingCount << "\n"
|
|
<< "\tSynced Players: " << SyncedCount << "\n"
|
|
<< "\tConnected Players: " << ConnectedCount << "\n"
|
|
<< "\tGuests: " << GuestCount << "\n"
|
|
<< "\tCars: " << CarCount << "\n"
|
|
<< "\tUptime: " << ElapsedTime << "ms (~" << size_t(ElapsedTime / 1000.0 / 60.0 / 60.0) << "h) \n"
|
|
<< "\tLua:\n"
|
|
<< "\t\tQueued results to check: " << mLuaEngine->GetResultsToCheckSize() << "\n"
|
|
<< "\t\tStates: " << mLuaEngine->GetLuaStateCount() << "\n"
|
|
<< "\t\tEvent timers: " << mLuaEngine->GetTimedEventsCount() << "\n"
|
|
<< "\t\tEvent handlers: " << mLuaEngine->GetRegisteredEventHandlerCount() << "\n"
|
|
<< "\tSubsystems:\n"
|
|
<< "\t\tGood/Starting/Bad: " << SystemsGood << "/" << SystemsStarting << "/" << SystemsBad << "\n"
|
|
<< "\t\tShutting down/Shutdown: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n"
|
|
<< "\t\tGood: [ " << SystemsGoodList << " ]\n"
|
|
<< "\t\tStarting: [ " << SystemsStartingList << " ]\n"
|
|
<< "\t\tBad: [ " << SystemsBadList << " ]\n"
|
|
<< "\t\tShutting down: [ " << SystemsShuttingDownList << " ]\n"
|
|
<< "\t\tShutdown: [ " << SystemsShutdownList << " ]\n"
|
|
<< "";
|
|
|
|
Application::Console().WriteRaw(Status.str());
|
|
}
|
|
|
|
void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
|
|
auto FutureIsNonNil =
|
|
[](const std::shared_ptr<TLuaResult>& Future) {
|
|
if (!Future->Error && Future->Result.valid()) {
|
|
auto Type = Future->Result.get_type();
|
|
return Type != sol::type::lua_nil && Type != sol::type::none;
|
|
}
|
|
return false;
|
|
};
|
|
std::vector<std::shared_ptr<TLuaResult>> NonNilFutures;
|
|
{ // Futures scope
|
|
auto Futures = mLuaEngine->TriggerEvent("onConsoleInput", "", cmd);
|
|
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
|
|
size_t Count = 0;
|
|
for (auto& Future : Futures) {
|
|
if (!Future->Error) {
|
|
++Count;
|
|
}
|
|
}
|
|
for (const auto& Future : Futures) {
|
|
if (FutureIsNonNil(Future)) {
|
|
NonNilFutures.push_back(Future);
|
|
}
|
|
}
|
|
}
|
|
if (NonNilFutures.size() == 0) {
|
|
if (!IgnoreNotACommand) {
|
|
Application::Console().WriteRaw("Error: Unknown command: '" + cmd + "'. Type 'help' to see a list of valid commands.");
|
|
}
|
|
} else {
|
|
std::stringstream Reply;
|
|
if (NonNilFutures.size() > 1) {
|
|
for (size_t i = 0; i < NonNilFutures.size(); ++i) {
|
|
Reply << NonNilFutures[i]->StateId << ": \n"
|
|
<< LuaAPI::LuaToString(NonNilFutures[i]->Result);
|
|
if (i < NonNilFutures.size() - 1) {
|
|
Reply << "\n";
|
|
}
|
|
}
|
|
} else {
|
|
Reply << LuaAPI::LuaToString(NonNilFutures[0]->Result);
|
|
}
|
|
Application::Console().WriteRaw(Reply.str());
|
|
}
|
|
}
|
|
|
|
void TConsole::HandleLuaInternalCommand(const std::string& cmd) {
|
|
if (cmd == "detach") {
|
|
ChangeToRegularConsole();
|
|
} else if (cmd == "queued") {
|
|
auto QueuedFunctions = LuaAPI::MP::Engine->Debug_GetStateFunctionQueueForState(mStateId);
|
|
Application::Console().WriteRaw("Pending functions in State '" + mStateId + "'");
|
|
std::unordered_map<std::string, size_t> FunctionsCount;
|
|
std::vector<std::string> FunctionsInOrder;
|
|
while (!QueuedFunctions.empty()) {
|
|
auto Tuple = QueuedFunctions.front();
|
|
QueuedFunctions.pop();
|
|
FunctionsInOrder.push_back(std::get<0>(Tuple));
|
|
FunctionsCount[std::get<0>(Tuple)] += 1;
|
|
}
|
|
std::set<std::string> Uniques;
|
|
for (const auto& Function : FunctionsInOrder) {
|
|
if (Uniques.count(Function) == 0) {
|
|
Uniques.insert(Function);
|
|
if (FunctionsCount.at(Function) > 1) {
|
|
Application::Console().WriteRaw(" " + Function + " (" + std::to_string(FunctionsCount.at(Function)) + "x)");
|
|
} else {
|
|
Application::Console().WriteRaw(" " + Function);
|
|
}
|
|
}
|
|
}
|
|
Application::Console().WriteRaw("Executed functions waiting to be checked in State '" + mStateId + "'");
|
|
for (const auto& Function : LuaAPI::MP::Engine->Debug_GetResultsToCheckForState(mStateId)) {
|
|
Application::Console().WriteRaw(" '" + Function.Function + "' (Ready? " + (Function.Ready ? "Yes" : "No") + ", Error? " + (Function.Error ? "Yes: '" + Function.ErrorMessage + "'" : "No") + ")");
|
|
}
|
|
} else if (cmd == "events") {
|
|
auto Events = LuaAPI::MP::Engine->Debug_GetEventsForState(mStateId);
|
|
Application::Console().WriteRaw("Registered Events + Handlers for State '" + mStateId + "'");
|
|
for (const auto& EventHandlerPair : Events) {
|
|
Application::Console().WriteRaw(" Event '" + EventHandlerPair.first + "'");
|
|
for (const auto& Handler : EventHandlerPair.second) {
|
|
Application::Console().WriteRaw(" " + Handler);
|
|
}
|
|
}
|
|
} else if (cmd == "help") {
|
|
Application::Console().WriteRaw(R"(BeamMP Lua Debugger
|
|
All commands must be prefixed with a `:`. Non-prefixed commands are interpreted as Lua.
|
|
|
|
Commands
|
|
:detach detaches (exits) from this Lua console
|
|
:help displays this help
|
|
:events shows a list of currently registered events
|
|
:queued shows a list of all pending and queued functions)");
|
|
} else {
|
|
beammp_error("internal command '" + cmd + "' is not known");
|
|
}
|
|
}
|
|
|
|
TConsole::TConsole() {
|
|
mCommandline.enable_history();
|
|
mCommandline.set_history_limit(20);
|
|
mCommandline.set_prompt("> ");
|
|
BackupOldLog();
|
|
bool success = mCommandline.enable_write_to_file("Server.log");
|
|
if (!success) {
|
|
beammp_error("unable to open file for writing: \"Server.log\"");
|
|
}
|
|
mCommandline.on_command = [this](Commandline& c) {
|
|
try {
|
|
auto cmd = c.get_command();
|
|
cmd = TrimString(cmd);
|
|
mCommandline.write(mCommandline.prompt() + cmd);
|
|
if (mIsLuaConsole) {
|
|
if (!mLuaEngine) {
|
|
beammp_info("Lua not started yet, please try again in a second");
|
|
} else if (!cmd.empty() && cmd.at(0) == ':') {
|
|
HandleLuaInternalCommand(cmd.substr(1));
|
|
} else {
|
|
auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared<std::string>(cmd), "", "" });
|
|
while (!Future->Ready) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // TODO: Add a timeout
|
|
}
|
|
if (Future->Error) {
|
|
beammp_lua_error(Future->ErrorMessage);
|
|
}
|
|
}
|
|
} else {
|
|
if (!mLuaEngine) {
|
|
beammp_error("Attempted to run a command before Lua engine started. Please wait and try again.");
|
|
} else if (cmd == "exit") {
|
|
beammp_info("gracefully shutting down");
|
|
Application::GracefullyShutdown();
|
|
} else if (StringStartsWith(cmd, "lua")) {
|
|
Command_Lua(cmd);
|
|
} else if (StringStartsWith(cmd, "help")) {
|
|
RunAsCommand(cmd, true);
|
|
Command_Help(cmd);
|
|
} else if (StringStartsWith(cmd, "kick")) {
|
|
RunAsCommand(cmd, true);
|
|
Command_Kick(cmd);
|
|
} else if (StringStartsWith(cmd, "say")) {
|
|
RunAsCommand(cmd, true);
|
|
Command_Say(cmd);
|
|
} else if (StringStartsWith(cmd, "list")) {
|
|
RunAsCommand(cmd, true);
|
|
Command_List(cmd);
|
|
} else if (StringStartsWith(cmd, "status")) {
|
|
RunAsCommand(cmd, true);
|
|
Command_Status(cmd);
|
|
} else if (!cmd.empty()) {
|
|
RunAsCommand(cmd);
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
beammp_error("Console died with: " + std::string(e.what()) + ". This could be a fatal error and could cause the server to terminate.");
|
|
}
|
|
};
|
|
}
|
|
|
|
void TConsole::Write(const std::string& str) {
|
|
auto ToWrite = GetDate() + str;
|
|
mCommandline.write(ToWrite);
|
|
}
|
|
|
|
void TConsole::WriteRaw(const std::string& str) {
|
|
mCommandline.write(str);
|
|
}
|
|
|
|
void TConsole::InitializeLuaConsole(TLuaEngine& Engine) {
|
|
mLuaEngine = &Engine;
|
|
}
|