diff --git a/r5dev/engine/shared/base_rcon.h b/r5dev/engine/shared/base_rcon.h index f46ad20e..5ce54733 100644 --- a/r5dev/engine/shared/base_rcon.h +++ b/r5dev/engine/shared/base_rcon.h @@ -11,7 +11,7 @@ public: CNetConBase(void) {} - virtual bool Connect(const char* pHostAdr, const int nHostPort = SOCKET_ERROR); + virtual bool Connect(const char* pHostName, const int nHostPort = SOCKET_ERROR); virtual void Disconnect(const char* szReason = nullptr) { NOTE_UNUSED(szReason); }; virtual bool ProcessBuffer(CConnectedNetConsoleData& data, const char* pRecvBuf, int nRecvLen, const int nMaxLen = SOCKET_ERROR); diff --git a/r5dev/netconsole/netconsole.cpp b/r5dev/netconsole/netconsole.cpp index 27c584b2..4d3fb8bc 100644 --- a/r5dev/netconsole/netconsole.cpp +++ b/r5dev/netconsole/netconsole.cpp @@ -23,7 +23,7 @@ //----------------------------------------------------------------------------- CNetCon::CNetCon(void) : m_bInitialized(false) - , m_bQuitApplication(false) + , m_bQuitting(false) , m_bPromptConnect(true) , m_flTickInterval(0.05f) { @@ -44,9 +44,12 @@ CNetCon::~CNetCon(void) // Purpose: WSA and NETCON systems init // Output : true on success, false otherwise //----------------------------------------------------------------------------- -bool CNetCon::Init(const bool bAnsiColor) +bool CNetCon::Init(const bool bAnsiColor, const char* pHostName, const int nPort) { + std::lock_guard<std::mutex> l(m_Mutex); + g_CoreMsgVCallback = &EngineLoggerSink; + TermSetup(bAnsiColor); WSAData wsaData; const int nError = ::WSAStartup(MAKEWORD(2, 2), &wsaData); @@ -57,20 +60,42 @@ bool CNetCon::Init(const bool bAnsiColor) return false; } - m_bInitialized = true; + // Try to connect from given parameters, these are passed in from the + // command line. If we fail, return out as this allows the user to + // quickly retry again. + if (pHostName && nPort != SOCKET_ERROR) + { + if (!Connect(pHostName, nPort)) + { + return false; + } + } - TermSetup(bAnsiColor); + m_bInitialized = true; Msg(eDLL_T::NONE, "R5 TCP net console [Version %s]\n", NETCON_VERSION); static std::thread frame([this]() { - for (;;) - { - RunFrame(); - } + while (RunFrame()) + {} }); frame.detach(); + + static std::thread input([this]() + { + while (!NetConsole()->GetQuitting()) + { + std::string lineInput; + + if (std::getline(std::cin, lineInput)) + { + NetConsole()->RunInput(lineInput); + } + } + }); + input.detach(); + return true; } @@ -80,9 +105,19 @@ bool CNetCon::Init(const bool bAnsiColor) //----------------------------------------------------------------------------- bool CNetCon::Shutdown(void) { + if (!m_bInitialized) + { + // Called twice! + Assert(0); + return false; + } + + m_bInitialized = false; + + std::lock_guard<std::mutex> l(m_Mutex); bool bResult = false; - m_Socket.CloseAllAcceptedSockets(); + GetSocketCreator()->CloseAllAcceptedSockets(); const int nError = ::WSACleanup(); if (nError == 0) @@ -96,11 +131,33 @@ bool CNetCon::Shutdown(void) } SpdLog_Shutdown(); - Console_Shutdown(); - return bResult; } +//----------------------------------------------------------------------------- +// Purpose: handle close events outside application routines +//----------------------------------------------------------------------------- +BOOL WINAPI CNetCon::CloseHandler(DWORD eventCode) +{ + switch (eventCode) + { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + case CTRL_CLOSE_EVENT: + case CTRL_LOGOFF_EVENT: + case CTRL_SHUTDOWN_EVENT: + + NetConsole()->SetQuitting(true); + + // Give it time to shutdown properly, value is set to the max possible + // of SPI_GETWAITTOKILLSERVICETIMEOUT, which is 20000ms by default. + Sleep(20000); + return TRUE; + } + + return FALSE; +} + //----------------------------------------------------------------------------- // Purpose: terminal setup //----------------------------------------------------------------------------- @@ -108,136 +165,165 @@ void CNetCon::TermSetup(const bool bAnsiColor) { SpdLog_Init(bAnsiColor); Console_Init(bAnsiColor); + + // Handle ctrl+x or X close events, give the application time to shutdown + // properly and flush all logging buffers. + SetConsoleCtrlHandler(CloseHandler, true); + + // Write console output to a file, this includes everything from local logs + // to messages over the wire. Note that logs emitted locally are prefixed + // with timestamps in () while messages over the wire are in []. + SpdLog_InstallSupplementalLogger("supplemental_logger_mt", "netconsole.log"); } //----------------------------------------------------------------------------- // Purpose: gets input IP and port for initialization //----------------------------------------------------------------------------- -void CNetCon::UserInput(void) +void CNetCon::RunInput(const string& lineInput) { - if (std::getline(std::cin, m_Input)) + std::lock_guard<std::mutex> l(m_Mutex); + + if (lineInput.compare("nquit") == 0) { - if (m_Input.compare("nquit") == 0) + SetQuitting(true); + return; + } + + if (IsConnected()) + { + CCommand cmd; + cmd.Tokenize(lineInput.c_str()); + + if (V_strcmp(cmd.Arg(0), "disconnect") == 0) { - m_bQuitApplication = true; + Disconnect("user closed connection"); return; } - std::lock_guard<std::mutex> l(m_Mutex); + vector<char> vecMsg; - if (IsConnected()) + const SocketHandle_t hSocket = GetSocket(); + bool bSend = false; + + if (cmd.ArgC() > 1) { - CCommand cmd; - cmd.Tokenize(m_Input.c_str()); - - if (V_strcmp(cmd.Arg(0), "disconnect") == 0) + if (V_strcmp(cmd.Arg(0), "PASS") == 0) // Auth with RCON server. { - Disconnect("user closed connection"); - return; + bSend = Serialize(vecMsg, cmd.Arg(1), "", + cl_rcon::request_t::SERVERDATA_REQUEST_AUTH); } - - vector<char> vecMsg; - - const SocketHandle_t hSocket = GetSocket(); - bool bSend = false; - - if (cmd.ArgC() > 1) + else // Execute command query. { - if (V_strcmp(cmd.Arg(0), "PASS") == 0) // Auth with RCON server. - { - bSend = Serialize(vecMsg, cmd.Arg(1), "", - cl_rcon::request_t::SERVERDATA_REQUEST_AUTH); - } - else // Execute command query. - { - bSend = Serialize(vecMsg, cmd.Arg(0), cmd.GetCommandString(), - cl_rcon::request_t::SERVERDATA_REQUEST_EXECCOMMAND); - } - } - else if (!m_Input.empty()) // Single arg command query. - { - bSend = Serialize(vecMsg, m_Input.c_str(), "", cl_rcon::request_t::SERVERDATA_REQUEST_EXECCOMMAND); - } - - if (bSend) // Only send if serialization process was successful. - { - if (!Send(hSocket, vecMsg.data(), int(vecMsg.size()))) - { - Error(eDLL_T::CLIENT, NO_ERROR, "Failed to send RCON message: (%s)\n", "SOCKET_ERROR"); - } + bSend = Serialize(vecMsg, cmd.Arg(0), cmd.GetCommandString(), + cl_rcon::request_t::SERVERDATA_REQUEST_EXECCOMMAND); } } - else // Setup connection from input. + else if (!lineInput.empty()) // Single arg command query. { - CCommand cmd; - cmd.Tokenize(m_Input.c_str(), cmd_source_t::kCommandSrcCode, &m_CharacterSet); + bSend = Serialize(vecMsg, lineInput.c_str(), "", cl_rcon::request_t::SERVERDATA_REQUEST_EXECCOMMAND); + } - if (cmd.ArgC() > 1) + if (bSend) // Only send if serialization process was successful. + { + if (!Send(hSocket, vecMsg.data(), int(vecMsg.size()))) { - const char* inAddr = cmd.Arg(0); - const char* inPort = cmd.Arg(1); - - if (!*inAddr || !*inPort) - { - Warning(eDLL_T::CLIENT, "No IP address or port provided\n"); - m_bPromptConnect = true; - return; - } - - if (!Connect(inAddr, atoi(inPort))) - { - m_bPromptConnect = true; - return; - } - } - else - { - if (!Connect(cmd.GetCommandString())) - { - m_bPromptConnect = true; - return; - } + Error(eDLL_T::CLIENT, NO_ERROR, "Failed to send RCON message: (%s)\n", "SOCKET_ERROR"); } } } -} + else // Setup connection from input. + { + CCommand cmd; + cmd.Tokenize(lineInput.c_str(), cmd_source_t::kCommandSrcCode, &m_CharacterSet); -//----------------------------------------------------------------------------- -// Purpose: clears the input buffer -//----------------------------------------------------------------------------- -void CNetCon::ClearInput(void) -{ - m_Input.clear(); + if (cmd.ArgC() > 1) + { + const char* inAddr = cmd.Arg(0); + const char* inPort = cmd.Arg(1); + + if (!*inAddr || !*inPort) + { + Warning(eDLL_T::CLIENT, "No IP address or port provided\n"); + SetPrompting(true); + return; + } + + if (!Connect(inAddr, atoi(inPort))) + { + SetPrompting(true); + return; + } + } + else + { + if (!Connect(cmd.GetCommandString())) + { + SetPrompting(true); + return; + } + } + } } //----------------------------------------------------------------------------- // Purpose: client's main processing loop //----------------------------------------------------------------------------- -void CNetCon::RunFrame(void) +bool CNetCon::RunFrame(void) { - if (IsInitialized() && IsConnected()) + if (IsInitialized()) { - std::lock_guard<std::mutex> l(m_Mutex); + if (IsConnected()) + { + std::lock_guard<std::mutex> l(m_Mutex); - CConnectedNetConsoleData& pData = m_Socket.GetAcceptedSocketData(0); - Recv(pData); - } - else if (m_bPromptConnect) - { - Msg(eDLL_T::NONE, "Enter [<IP>]:<PORT> or <IP> <PORT>: "); - m_bPromptConnect = false; + CConnectedNetConsoleData& pData = GetSocketCreator()->GetAcceptedSocketData(0); + Recv(pData); + } + else if (GetPrompting()) + { + Msg(eDLL_T::NONE, "Enter [<IP>]:<PORT> or <IP> <PORT>: "); + SetPrompting(false); + } } std::this_thread::sleep_for(IntervalToDuration(m_flTickInterval)); + return true; } //----------------------------------------------------------------------------- // Purpose: checks if application should be terminated // Output : true for termination, false otherwise //----------------------------------------------------------------------------- -bool CNetCon::ShouldQuit(void) const +bool CNetCon::GetQuitting(void) const { - return m_bQuitApplication; + return m_bQuitting; +} + +//----------------------------------------------------------------------------- +// Purpose: set whether we should quit +// input : bQuit +//----------------------------------------------------------------------------- +void CNetCon::SetQuitting(const bool bQuit) +{ + m_bQuitting = bQuit; +} + +//----------------------------------------------------------------------------- +// Purpose: checks if we should prompt the connect message +// Output : true for prompting, false otherwise +//----------------------------------------------------------------------------- +bool CNetCon::GetPrompting(void) const +{ + return m_bPromptConnect; +} + +//----------------------------------------------------------------------------- +// Purpose: set whether we should prompt the connect message +// input : bPrompt +//----------------------------------------------------------------------------- +void CNetCon::SetPrompting(const bool bPrompt) +{ + m_bPromptConnect = bPrompt; } //----------------------------------------------------------------------------- @@ -254,10 +340,10 @@ void CNetCon::Disconnect(const char* szReason) } Msg(eDLL_T::CLIENT, "Disconnect: (%s)\n", szReason); - m_Socket.CloseAcceptedSocket(0); + GetSocketCreator()->CloseAcceptedSocket(0); } - m_bPromptConnect = true; + SetPrompting(true); } //----------------------------------------------------------------------------- @@ -374,23 +460,25 @@ int main(int argc, char* argv[]) } } - if (!NetConsole()->Init(bEnableColor)) + const char* pHostName = nullptr; + int nPort = SOCKET_ERROR; + + if (argc >= 3) // Get IP and Port from command line. + { + pHostName = argv[1]; + nPort = atoi(argv[2]); + } + + if (!NetConsole()->Init(bEnableColor, pHostName, nPort)) { return EXIT_FAILURE; } - if (argc >= 3) // Get IP and Port from command line. + while (!NetConsole()->GetQuitting()) { - if (!NetConsole()->Connect(argv[1], atoi(argv[2]))) - { - return EXIT_FAILURE; - } - } - - while (!NetConsole()->ShouldQuit()) - { - NetConsole()->UserInput(); - NetConsole()->ClearInput(); + // Run with a reasonable tick rate so we don't eat all the CPU. + std::this_thread::sleep_for( + IntervalToDuration(NetConsole()->GetTickInterval())); } if (!NetConsole()->Shutdown()) diff --git a/r5dev/netconsole/netconsole.h b/r5dev/netconsole/netconsole.h index 2a0926a6..0d77ab48 100644 --- a/r5dev/netconsole/netconsole.h +++ b/r5dev/netconsole/netconsole.h @@ -17,15 +17,21 @@ public: CNetCon(void); ~CNetCon(void); - bool Init(const bool bAnsiColor); + bool Init(const bool bAnsiColor, const char* pHostName = nullptr, const int nPort = SOCKET_ERROR); bool Shutdown(void); - void TermSetup(const bool bAnsiColor); - void UserInput(void); - void ClearInput(void); - void RunFrame(void); - bool ShouldQuit(void) const; + void RunInput(const string& lineInput); + bool RunFrame(void); + + bool GetQuitting(void) const; + void SetQuitting(const bool bQuit); + + bool GetPrompting(void) const; + void SetPrompting(const bool bPrompt); + + inline float GetTickInterval() const { return m_flTickInterval; } + static BOOL WINAPI CloseHandler(DWORD eventCode); virtual void Disconnect(const char* szReason = nullptr); virtual bool ProcessMessage(const char* pMsgBuf, const int nMsgLen) override; @@ -39,13 +45,11 @@ public: private: bool m_bInitialized; - bool m_bQuitApplication; + bool m_bQuitting; bool m_bPromptConnect; float m_flTickInterval; characterset_t m_CharacterSet; - - std::string m_Input; mutable std::mutex m_Mutex; };