diff --git a/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst b/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst index 701bc8044..ce79c9bb1 100644 --- a/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst +++ b/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst @@ -448,6 +448,33 @@ Called when a map starts and the server is ready to accept clients. pass +OnServerOutput +---------------- + +Called when something is printed to the console. You can decide whether the +message is logged/printed or not. + +.. warning:: + + Be careful when printing something within this listener. It can easily + result into a recursion, which results into a crash of the server. + + +.. code-block:: python + + from listeners import OnServerOutput + from core import OutputReturn + + @OnServerOutput + def on_server_output(severity, msg): + # Block everything starting with 'sv_' or 'mp_' from being logged. + # This keeps the console clean in CS:GO. + if msg.startswith(('sv_', 'mp_')): + return OutputReturn.BLOCK + + return OutputReturn.CONTINUE + + OnTick ------ diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index cb7482baa..950019f4e 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -47,6 +47,7 @@ # Core from _core import console_message from _core import get_interface +from _core import OutputReturn from _core import SOURCE_ENGINE from _core import SOURCE_ENGINE_BRANCH @@ -58,6 +59,7 @@ 'GameConfigObj', 'WeakAutoUnload', 'GAME_NAME', + 'OutputReturn', 'PLATFORM', 'SOURCE_ENGINE', 'SOURCE_ENGINE_BRANCH', @@ -66,6 +68,7 @@ 'get_interface', 'get_public_ip', 'ignore_unicode_errors', + 'server_output', ) @@ -166,6 +169,7 @@ def echo_console(text): for line in text.split('\n'): console_message(line + '\n') + @contextmanager def ignore_unicode_errors(errors='ignore'): """Overwrite the ``strict`` codecs error handler temporarily. @@ -218,3 +222,62 @@ def get_public_ip(): This functions makes a call to http://ip.42.pl/raw to retrieve the public IP. """ return urlopen('http://ip.42.pl/raw').read().decode() + + +@contextmanager +def server_output(action=OutputReturn.CONTINUE): + """Gather all server output sent during the execution of the with-statement. + + :param OutputReturn action: + Determines what happens with the output. + :rtype: list + :return: + A list that will be filled with a tuple for every line that is being + logged. The tuple contains the severity and the message. + + Example: + + .. code:: python + + from cvars import cvar + from core import server_output + from core import OutputReturn + + status = cvar.find_command('status') + + with server_output(OutputReturn.BLOCK) as output: + status() + + # Print everything that was logged by the 'status' command + print(output) + + + Output: + + .. code:: python + + [(_core.MessageSeverity.MESSAGE, 'hostname: Counter-Strike: Global Offensive\\n'), + (_core.MessageSeverity.MESSAGE, 'version : 1.35.8.4/13584 513/6771 secure [A:1:2435270659:8640] \\n'), + (_core.MessageSeverity.MESSAGE, 'udp/ip : 192.168.178.60:27015 (public ip: 46.83.158.27)\\n'), + (_core.MessageSeverity.MESSAGE, 'os : Windows\\n'), + (_core.MessageSeverity.MESSAGE, 'type : community dedicated\\n'), + (_core.MessageSeverity.MESSAGE, 'players : 0 humans, 0 bots (20/0 max) (hibernating)\\n\\n'), + (_core.MessageSeverity.MESSAGE, '# userid name uniqueid connected ping loss state rate'), + (_core.MessageSeverity.MESSAGE, ' adr'), + (_core.MessageSeverity.MESSAGE, '\\n'), + (_core.MessageSeverity.MESSAGE, '#end\\n')] + """ + # Import this here to fix a cyclic import + from listeners import OnServerOutput + + msg_buffer = [] + + def intercepter(severity, msg): + msg_buffer.append((severity, msg)) + return action + + OnServerOutput.manager.register_listener(intercepter) + try: + yield msg_buffer + finally: + OnServerOutput.manager.unregister_listener(intercepter) diff --git a/addons/source-python/packages/source-python/listeners/__init__.py b/addons/source-python/packages/source-python/listeners/__init__.py index 0ac1c0730..bcec7db4a 100644 --- a/addons/source-python/packages/source-python/listeners/__init__.py +++ b/addons/source-python/packages/source-python/listeners/__init__.py @@ -62,6 +62,7 @@ from _listeners import on_query_cvar_value_finished_listener_manager from _listeners import on_server_activate_listener_manager from _listeners import on_tick_listener_manager +from _listeners import on_server_output_listener_manager # Entity output from listeners._entity_output import on_entity_output_listener_manager @@ -103,6 +104,7 @@ 'OnServerActivate', 'OnTick', 'OnVersionUpdate', + 'OnServerOutput', 'get_button_combination_status', 'on_client_active_listener_manager', 'on_client_connect_listener_manager', @@ -133,6 +135,7 @@ 'on_server_activate_listener_manager', 'on_tick_listener_manager', 'on_version_update_listener_manager', + 'on_server_output_listener_manager', ) @@ -427,6 +430,12 @@ class OnButtonStateChanged(ListenerManagerDecorator): manager = on_button_state_changed_listener_manager +class OnServerOutput(ListenerManagerDecorator): + """Register/unregister a server output listener.""" + + manager = on_server_output_listener_manager + + # ============================================================================= # >> FUNCTIONS # ============================================================================= diff --git a/src/core/modules/core/core.h b/src/core/modules/core/core.h index 4ab6af2e5..a709479ab 100644 --- a/src/core/modules/core/core.h +++ b/src/core/modules/core/core.h @@ -35,6 +35,29 @@ #include "dynload.h" +//----------------------------------------------------------------------------- +// MessageSeverity +//----------------------------------------------------------------------------- +enum MessageSeverity +{ + SEVERITY_MESSAGE = 0, + SEVERITY_WARNING, + SEVERITY_ASSERT, + SEVERITY_ERROR, + SEVERITY_LOG, +}; + + +//----------------------------------------------------------------------------- +// OutputReturn +//----------------------------------------------------------------------------- +enum OutputReturn +{ + OUTPUT_BLOCK = 0, + OUTPUT_CONTINUE +}; + + //----------------------------------------------------------------------------- // ConMsg wrapper //----------------------------------------------------------------------------- diff --git a/src/core/modules/core/core_wrap.cpp b/src/core/modules/core/core_wrap.cpp index a8701b6f6..cce3ef3bb 100644 --- a/src/core/modules/core/core_wrap.cpp +++ b/src/core/modules/core/core_wrap.cpp @@ -42,6 +42,8 @@ extern CSourcePython g_SourcePythonPlugin; // Forward declarations. //----------------------------------------------------------------------------- static void export_source_python_plugin(scope); +static void export_message_severity(scope); +static void export_output_return(scope); static void export_constants(scope); static void export_functions(scope); @@ -52,6 +54,8 @@ static void export_functions(scope); DECLARE_SP_MODULE(_core) { export_source_python_plugin(_core); + export_message_severity(_core); + export_output_return(_core); export_constants(_core); export_functions(_core); } @@ -69,6 +73,33 @@ void export_source_python_plugin(scope _core) } +//----------------------------------------------------------------------------- +// Expose MessageSeverity. +//----------------------------------------------------------------------------- +void export_message_severity(scope _core) +{ + enum_ _MessageSeverity("MessageSeverity"); + + _MessageSeverity.value("MESSAGE", SEVERITY_MESSAGE); + _MessageSeverity.value("WARNING", SEVERITY_WARNING); + _MessageSeverity.value("ASSERT", SEVERITY_ASSERT); + _MessageSeverity.value("ERROR", SEVERITY_ERROR); + _MessageSeverity.value("LOG", SEVERITY_LOG); +} + + +//----------------------------------------------------------------------------- +// Expose OutputReturn. +//----------------------------------------------------------------------------- +void export_output_return(scope _core) +{ + enum_ _OutputReturn("OutputReturn"); + + _OutputReturn.value("BLOCK", OUTPUT_BLOCK); + _OutputReturn.value("CONTINUE", OUTPUT_CONTINUE); +} + + //----------------------------------------------------------------------------- // Expose constants. //----------------------------------------------------------------------------- diff --git a/src/core/modules/listeners/listeners_wrap.cpp b/src/core/modules/listeners/listeners_wrap.cpp index 01e25865e..e9e1b7d5c 100644 --- a/src/core/modules/listeners/listeners_wrap.cpp +++ b/src/core/modules/listeners/listeners_wrap.cpp @@ -57,6 +57,7 @@ DEFINE_MANAGER_ACCESSOR(OnEntityDeleted) DEFINE_MANAGER_ACCESSOR(OnDataLoaded) DEFINE_MANAGER_ACCESSOR(OnCombinerPreCache) DEFINE_MANAGER_ACCESSOR(OnDataUnloaded) +DEFINE_MANAGER_ACCESSOR(OnServerOutput) //----------------------------------------------------------------------------- @@ -147,4 +148,6 @@ void export_listener_managers(scope _listeners) _listeners.attr("on_data_loaded_listener_manager") = object(ptr(GetOnDataLoadedListenerManager())); _listeners.attr("on_combiner_pre_cache_listener_manager") = object(ptr(GetOnCombinerPreCacheListenerManager())); _listeners.attr("on_data_unloaded_listener_manager") = object(ptr(GetOnDataUnloadedListenerManager())); + + _listeners.attr("on_server_output_listener_manager") = object(ptr(GetOnServerOutputListenerManager())); } diff --git a/src/core/sp_main.cpp b/src/core/sp_main.cpp index d950aaa5c..7f1003813 100644 --- a/src/core/sp_main.cpp +++ b/src/core/sp_main.cpp @@ -59,6 +59,7 @@ #include "modules/listeners/listeners_manager.h" #include "utilities/conversions.h" #include "modules/entities/entities_entity.h" +#include "modules/core/core.h" //----------------------------------------------------------------------------- @@ -177,6 +178,84 @@ bool GetInterfaces( InterfaceHelper_t* pInterfaceList, CreateInterfaceFn factory return true; } + +//----------------------------------------------------------------------------- +// Server output hook. +//----------------------------------------------------------------------------- +#if defined(ENGINE_ORANGEBOX) || defined(ENGINE_BMS) +SpewRetval_t SP_SpewOutput( SpewType_t spewType, const tchar *pMsg ) +{ + extern CListenerManager* GetOnServerOutputListenerManager(); + bool block = false; + + for(int i = 0; i < GetOnServerOutputListenerManager()->m_vecCallables.Count(); i++) + { + BEGIN_BOOST_PY() + object return_value = CALL_PY_FUNC( + GetOnServerOutputListenerManager()->m_vecCallables[i].ptr(), + (MessageSeverity) spewType, + pMsg); + + if (!return_value.is_none() && extract(return_value) == OUTPUT_BLOCK) + block = true; + + END_BOOST_PY_NORET() + } + + if (!block && g_SourcePythonPlugin.m_pOldSpewOutputFunc) + return g_SourcePythonPlugin.m_pOldSpewOutputFunc(spewType, pMsg); + + return SPEW_CONTINUE; +} +#else +class SPLoggingListener: public ILoggingListener +{ +public: + virtual void Log( const LoggingContext_t *pContext, const tchar *pMessage ) + { + extern CListenerManager* GetOnServerOutputListenerManager(); + bool block = false; + + for(int i = 0; i < GetOnServerOutputListenerManager()->m_vecCallables.Count(); i++) + { + BEGIN_BOOST_PY() + object return_value = CALL_PY_FUNC( + GetOnServerOutputListenerManager()->m_vecCallables[i].ptr(), + (MessageSeverity) pContext->m_Severity, + pMessage); + + if (!return_value.is_none() && extract(return_value) == OUTPUT_BLOCK) + block = true; + + END_BOOST_PY_NORET() + } + + if (!block) + { + // Restore the old logging state before SP has been loaded + LoggingSystem_PopLoggingState(false); + + // Resend the log message. Out listener won't get called anymore + LoggingSystem_Log( + pContext->m_ChannelID, + pContext->m_Severity, + pContext->m_Color, + pMessage); + + // Create a new logging state with only our listener being active +#if defined(ENGINE_LEFT4DEAD2) + LoggingSystem_PushLoggingState(false); +#else + LoggingSystem_PushLoggingState(false, true); +#endif + LoggingSystem_RegisterLoggingListener(this); + + } + } +} g_LoggingListener; + +#endif + //----------------------------------------------------------------------------- // Purpose: constructor/destructor //----------------------------------------------------------------------------- @@ -184,6 +263,10 @@ CSourcePython::CSourcePython() { m_iClientCommandIndex = 0; m_pOldMDLCacheNotifier = NULL; + +#if defined(ENGINE_ORANGEBOX) || defined(ENGINE_BMS) + m_pOldSpewOutputFunc = NULL; +#endif } CSourcePython::~CSourcePython() @@ -238,6 +321,22 @@ bool CSourcePython::Load( CreateInterfaceFn interfaceFactory, CreateInterfaceFn return false; } +#if defined(ENGINE_ORANGEBOX) || defined(ENGINE_BMS) + DevMsg(1, MSG_PREFIX "Retrieving old output function...\n"); + m_pOldSpewOutputFunc = GetSpewOutputFunc(); + + DevMsg(1, MSG_PREFIX "Setting new output function...\n"); + SpewOutputFunc(SP_SpewOutput); +#else + DevMsg(1, MSG_PREFIX "Registering logging listener...\n"); +#if defined(ENGINE_LEFT4DEAD2) + LoggingSystem_PushLoggingState(false); +#else + LoggingSystem_PushLoggingState(false, true); +#endif + LoggingSystem_RegisterLoggingListener(&g_LoggingListener); +#endif + // TODO: Don't hardcode the 64 bytes offset #ifdef ENGINE_LEFT4DEAD2 #define CACHE_NOTIFY_OFFSET 68 @@ -264,6 +363,17 @@ void CSourcePython::Unload( void ) DevMsg(1, MSG_PREFIX "Unhooking all functions...\n"); GetHookManager()->UnhookAllFunctions(); + +#if defined(ENGINE_ORANGEBOX) || defined(ENGINE_BMS) + if (m_pOldSpewOutputFunc) + { + DevMsg(1, MSG_PREFIX "Restoring old output function...\n"); + SpewOutputFunc(m_pOldSpewOutputFunc); + } +#else + DevMsg(1, MSG_PREFIX "Restoring old logging state...\n"); + LoggingSystem_PopLoggingState(false); +#endif DevMsg(1, MSG_PREFIX "Shutting down python...\n"); g_PythonManager.Shutdown(); diff --git a/src/core/sp_main.h b/src/core/sp_main.h index 12c4ed96a..8f6eb3c83 100644 --- a/src/core/sp_main.h +++ b/src/core/sp_main.h @@ -123,9 +123,13 @@ class CSourcePython: public IServerPluginCallbacks, public IEntityListener, publ virtual bool ShouldSupressLoadWarning(MDLHandle_t handle); #endif -private: +public: int m_iClientCommandIndex; IMDLCacheNotify* m_pOldMDLCacheNotifier; + +#if defined(ENGINE_ORANGEBOX) || defined(ENGINE_BMS) + SpewOutputFunc_t m_pOldSpewOutputFunc; +#endif };