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 00ba84107..daf562598 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 @@ -633,6 +633,34 @@ message is logged/printed or not. return OutputReturn.CONTINUE +OnServerHibernating +------------------- + +Called when the server starts hibernating. + +.. code-block:: python + + from listeners import OnServerHibernating + + @OnServerHibernating + def on_server_hibernating(): + ... + + +OnServerWakingUp +---------------- + +Called when the server is waking up from hibernation. + +.. code-block:: python + + from listeners import OnServerWakingUp + + @OnServerWakingUp + def on_server_waking_up(): + ... + + OnTick ------ diff --git a/addons/source-python/docs/source-python/source/developing/modules/threads.rst b/addons/source-python/docs/source-python/source/developing/modules/threads.rst new file mode 100644 index 000000000..40f0a0dc9 --- /dev/null +++ b/addons/source-python/docs/source-python/source/developing/modules/threads.rst @@ -0,0 +1,7 @@ +threads module +============== + +.. automodule:: threads + :members: + :undoc-members: + :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/general/known-issues.rst b/addons/source-python/docs/source-python/source/general/known-issues.rst index d4e085b90..debd26e04 100644 --- a/addons/source-python/docs/source-python/source/general/known-issues.rst +++ b/addons/source-python/docs/source-python/source/general/known-issues.rst @@ -11,7 +11,12 @@ which causes either EventScripts to be loaded with Source.Python's Python version or vice versa. This doesn't work well and results in a crash on startup. SourceMod's Accelerator incompatibility ---------------------------------------------------- +--------------------------------------- If you are running `SourceMod's Accelerator `_ with Source.Python, you may experience random crashes that would normally be caught since this extension prevents us from catching and preventing them. + +Hibernation issues +------------------ +Some features (such as tick listeners, delays, Python threads, etc.) do not work on some games (e.g. CS:GO) +while the server is hibernating. If you require these features at all time, please disable hibernation. diff --git a/addons/source-python/packages/source-python/__init__.py b/addons/source-python/packages/source-python/__init__.py index 63b9270c8..0d2b05b98 100644 --- a/addons/source-python/packages/source-python/__init__.py +++ b/addons/source-python/packages/source-python/__init__.py @@ -92,6 +92,7 @@ def load(): setup_entities_listener() setup_versioning() setup_sqlite() + setup_threads() def unload(): @@ -534,4 +535,27 @@ def flush(self): 'https://github.com/Source-Python-Dev-Team/Source.Python/issues/175. ' 'Source.Python should continue working, but we would like to figure ' 'out in which situations sys.stdout is None to be able to fix this ' - 'issue instead of applying a workaround.') \ No newline at end of file + 'issue instead of applying a workaround.') + + +# ============================================================================= +# >> THREADS +# ============================================================================= +def setup_threads(): + """Setup threads.""" + import listeners.tick + from threads import GameThread + listeners.tick.GameThread = GameThread # Backward compatibility + + from threads import ThreadYielder + if not ThreadYielder.is_implemented(): + return + + from core.settings import _core_settings + from threads import sp_thread_yielding + + sp_thread_yielding.set_string( + _core_settings.get( + 'THREAD_SETTINGS', {} + ).get('enable_thread_yielding', '0'), + ) diff --git a/addons/source-python/packages/source-python/auth/backends/sql.py b/addons/source-python/packages/source-python/auth/backends/sql.py index 0bead183c..425dfa187 100644 --- a/addons/source-python/packages/source-python/auth/backends/sql.py +++ b/addons/source-python/packages/source-python/auth/backends/sql.py @@ -18,8 +18,8 @@ from auth.manager import ParentPermissions # Paths from paths import SP_DATA_PATH -# Listeners -from listeners.tick import GameThread +# Threads +from threads import GameThread # Site-Packges Imports # SQL Alechemy diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index a8d1d17ae..6c19b45ef 100755 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -74,10 +74,12 @@ 'SOURCE_ENGINE', 'SOURCE_ENGINE_BRANCH', 'Tokenize', + 'autounload_disabled', 'check_info_output', 'console_message', 'create_checksum', 'echo_console', + 'get_calling_plugin', 'get_core_modules', 'get_interface', 'get_public_ip', @@ -95,6 +97,9 @@ # Get the platform the server is on PLATFORM = system().lower() +# Whether auto unload classes are disabled +_autounload_disabled = False + # ============================================================================= # >> CLASSES @@ -113,29 +118,12 @@ def __new__(cls, *args, **kwargs): # Get the class instance self = super().__new__(cls) - # Get the calling frame - frame = currentframe().f_back - - # Get the calling path - path = frame.f_code.co_filename - - # Don't keep hostage instances that will never be unloaded - while not path.startswith(PLUGIN_PATH): - frame = frame.f_back - if frame is None: - return self - path = frame.f_code.co_filename - if path.startswith('> FUNCTIONS # ============================================================================= +@contextmanager +def autounload_disabled(): + """Context that disables auto unload classes.""" + global _autounload_disabled + prev = _autounload_disabled + _autounload_disabled = True + try: + yield + finally: + _autounload_disabled = prev + + +def get_calling_plugin(depth=0): + """Resolves the name of the calling plugin. + + :param int depth: + How many frame back to start looking for a plugin. + + :rtype: + str + """ + # Get the current frame + frame = currentframe() + + # Go back the specificed depth + for _ in range(depth + 1): + frame = frame.f_back + + # Get the calling path + path = frame.f_code.co_filename + + # Don't keep hostage instances that will never be unloaded + while not path.startswith(PLUGIN_PATH): + frame = frame.f_back + if frame is None: + return + path = frame.f_code.co_filename + if path.startswith('> FUNCTIONS # ============================================================================= @@ -697,7 +715,13 @@ def _pre_fire_output(args): @PreHook(get_virtual_function(server_game_dll, _hibernation_function_name)) def _pre_hibernation_function(stack_data): """Called when the server is hibernating.""" - if not stack_data[1]: + hibernating = stack_data[1] + if hibernating: + on_server_hibernating_listener_manager.notify() + else: + on_server_waking_up_listener_manager.notify() + + if not hibernating: return # Disconnect all bots... diff --git a/addons/source-python/packages/source-python/listeners/tick.py b/addons/source-python/packages/source-python/listeners/tick.py index 69ad6ce67..1dcc5cd74 100644 --- a/addons/source-python/packages/source-python/listeners/tick.py +++ b/addons/source-python/packages/source-python/listeners/tick.py @@ -11,8 +11,6 @@ import time from enum import IntEnum -from threading import Thread -from warnings import warn # Source.Python from core import AutoUnload @@ -28,7 +26,7 @@ # ============================================================================= __all__ = ( 'Delay', - 'GameThread', + 'GameThread', # Backward compatibility 'Repeat', 'RepeatStatus', ) @@ -41,26 +39,6 @@ listeners_tick_logger = listeners_logger.tick -# ============================================================================= -# >> THREAD WORKAROUND -# ============================================================================= -class GameThread(WeakAutoUnload, Thread): - """A subclass of :class:`threading.Thread` that throws a warning if the - plugin that created the thread has been unloaded while the thread is still - running. - """ - - def _add_instance(self, caller): - super()._add_instance(caller) - self._caller = caller - - def _unload_instance(self): - if self.is_alive(): - warn( - f'Thread "{self.name}" ({self.ident}) from "{self._caller}" ' - f'is running even though its plugin has been unloaded!') - - # ============================================================================= # >> DELAY CLASSES # ============================================================================= diff --git a/addons/source-python/packages/source-python/messages/base.py b/addons/source-python/packages/source-python/messages/base.py index d4b02284a..d82812994 100755 --- a/addons/source-python/packages/source-python/messages/base.py +++ b/addons/source-python/packages/source-python/messages/base.py @@ -7,6 +7,8 @@ # ============================================================================ # Python Imports import collections +# Threading +from threading import Lock # Source.Python Imports # Colors @@ -78,6 +80,10 @@ def send(self, *player_indexes, **tokens): self._get_translated_kwargs(language, tokens)) self._send(indexes, translated_kwargs) + # Get a lock to ensure thread-safety for bitbuf messages + if not UserMessage.is_protobuf(): + _bitbuf_lock = Lock() + def _send(self, player_indexes, translated_kwargs): """Send the user message to the given players. @@ -87,12 +93,14 @@ def _send(self, player_indexes, translated_kwargs): """ recipients = RecipientFilter(*player_indexes) recipients.reliable = self.reliable - user_message = UserMessage(recipients, self.message_name) - if user_message.is_protobuf(): + if UserMessage.is_protobuf(): + user_message = UserMessage(recipients, self.message_name) self.protobuf(user_message.buffer, translated_kwargs) user_message.send() else: + self._bitbuf_lock.acquire() + user_message = UserMessage(recipients, self.message_name) try: self.bitbuf(user_message.buffer, translated_kwargs) except: @@ -109,6 +117,7 @@ def _send(self, player_indexes, translated_kwargs): raise finally: user_message.send() + self._bitbuf_lock.release() @staticmethod def _categorize_players_by_language(player_indexes): diff --git a/addons/source-python/packages/source-python/threads.py b/addons/source-python/packages/source-python/threads.py new file mode 100644 index 000000000..428b79680 --- /dev/null +++ b/addons/source-python/packages/source-python/threads.py @@ -0,0 +1,571 @@ +# ../threads.py + +"""Provides threads functionality. + +.. data:: sp_thread_yielding + + If enabled, yields remaining cycles to Python threads every frame. + + .. note:: + + Not all games are currently supported. + See also: :func:`ThreadYielder.is_implemented` +""" + +# ============================================================================= +# >> IMPORTS +# ============================================================================= +# Python Imports +# FuncTools +from functools import partial +from functools import wraps +# Threading +from threading import Event +from threading import Thread +from threading import current_thread +from threading import main_thread +# Queue +from queue import Queue +# Sys +from sys import getswitchinterval +# Time +from time import sleep +# Warnings +from warnings import warn + +# Source.Python Imports +# Core +from core import get_calling_plugin +from core import WeakAutoUnload +from core import autounload_disabled +from core.cache import cached_property +# Cvars +from cvars import ConVar +# Engines +from engines.server import global_vars +# Hooks +from hooks.exceptions import except_hooks +# Listeners +from listeners.tick import Delay + + +# ============================================================================= +# >> FORWARD IMPORTS +# ============================================================================= +# Source.Python Imports +# Core +from _threads import ThreadYielder + + +# ============================================================================= +# >> ALL DECLARATION +# ============================================================================= +__all__ = ( + 'GameThread', + 'InfiniteThread', + 'Partial', + 'Queued', + 'ThreadYielder', + 'queued', + 'sp_thread_yielding', + 'threaded', +) + + +# ============================================================================= +# >> GLOBAL VARIABLES +# ============================================================================= +sp_thread_yielding = ConVar('sp_thread_yielding') + + +# ============================================================================= +# >> CLASSES +# ============================================================================= +class GameThread(WeakAutoUnload, Thread): + """A subclass of :class:`threading.Thread` that throws a warning if the + plugin that created the thread has been unloaded while the thread is still + running. + + Example: + + .. code:: python + + from threads import GameThread + + def function(): + ... + + thread = GameThread(function).start() + + .. warning:: + + Multiple active threads or threading routines that are heavy on CPU + can have a huge impact on the networking of the server. + """ + + def __init__(self, target=None, *args, **kwargs): + """Initializes the thread. + + :param callable target: + The target function to execute. + :param tuple *args: + The arguments to pass to ``Thread.__init__``. + :param dict **kwargs: + The keyword arguments to pass to ``Thread.__init__``. + """ + super().__init__(None, target, *args, **kwargs) + + def _add_instance(self, caller): + """Adds the instance and store the caller.""" + super()._add_instance(caller) + self._caller = caller + + def _unload_instance(self): + """Unloads the instance.""" + # Give it at least a frame and a switch to conclude + if self._started.is_set(): + self.join(global_vars.interval_per_tick + getswitchinterval()) + + # Raise awarness if that was not enough time to conclude + if self.is_alive(): + warn( + f'Thread "{self.name}" ({self.ident}) from "{self._caller}" ' + f'is running even though its plugin has been unloaded!') + + @cached_property + def yielder(self): + """Returns the yielder for this thread. + + :rtype: + ThreadYielder + """ + return ThreadYielder() + + def start(self): + """Starts the thread. + + :return: + Return itself so that it can be inlined. + """ + super().start() + return self + + def run(self): + """Runs the thread.""" + with self.yielder: + super().run() + + +class InfiniteThread(GameThread): + """Thread that runs infinitely. + + Example: + + .. code:: python + + from threads import InfiniteThread + + class MyInfiniteThread(InfiniteThread): + def __call__(self): + with self.yielder: + ... + + thread = MyInfiniteThread().start(1) + """ + + @cached_property + def wait_signal(self): + """When clear, the current iteration is done waiting. + + :rtype: + threading.Event + """ + return Event() + + @cached_property + def exit_signal(self): + """When set, the thread has been stopped and terminated. + + :rtype: + threading.Event + """ + return Event() + + @cached_property + def interval(self): + """The time between each iteration. + + :rtype: + float + """ + return getswitchinterval() + + def __call__(self, *args, **kwargs): + """Calls the target with the given arguments. + + :param tuple *args: + The arguments to pass to the target function. + :param dict **kwargs: + The keyword arguments to pass to the target function. + + :return: + The value returned by the target function. + + .. note:: + + The thread will stop looping if a ``SystemExit`` is caught. + """ + with self.yielder: + return self._target(*args, **kwargs) + + def start(self, interval=None, wait=False): + """Starts iterating. + + :param float interval: + The time between each iteration. + :param bool wait: + Whether we should wait an interval for the first iteration. + + :return: + Return itself so that it can be inlined. + """ + # Set our interval if any was given + if interval is not None: + type(self).interval.set_cached_value(self, interval) + + # Set our wait signal so that we start iterating immediately + if not wait: + self.wait_signal.set() + + # Start iterating + super().start() + + # Clear our wait signal so that we start waiting again + if not wait: + self.wait_signal.clear() + + # Return ourself so that it can be inlined + return self + + def stop(self): + """Stops iterating.""" + # Set our exit signal so that we stop iterating + self.exit_signal.set() + + # Stop waiting for the next iteration + self.wait_signal.set() + + def run(self): + """Runs the thread. + + :raise SystemExit: + When the asynchronous state is being terminated. + """ + # Get our interval + interval = self.interval + + # Get our signals + exit_signal, wait_signal = self.exit_signal, self.wait_signal + + # Get our arguments + args, kwargs = self._args, self._kwargs + + # Start iterating + while True: + + # Wait the interval + if interval: + wait_signal.wait(interval) + + # Have we been stopped since the last iteration? + if exit_signal.is_set(): + + # Delete our target and arguments + del self._target, self._args, self._kwargs + + # Terminate the asynchronous state + raise SystemExit(f'{self.name} was terminated.') + + # Call the target with the given arguments + try: + self(*args, **kwargs) + except SystemExit: + self.stop() + except Exception: + except_hooks.print_exception() + + def _unload_instance(self): + """Stops iterating and unloads the instance.""" + # Stop iterating + self.stop() + + # Unload the instance + super()._unload_instance() + + +class Partial(partial): + """Represents a partial that can have a callback bound to it.""" + + def waitable(self): + """Makes the partial waitable when called from a parallel thread. + + :return: + Return itself so that it can be inlined. + """ + self._waitable = True + return self + + def unloadable(self): + """Makes the partial unloadable by the calling plugin. + + :return: + Return itself so that it can be inlined. + """ + # Get the module name of the calling plugin + caller = get_calling_plugin() + + # Flag the partial as unloadable + if caller is not None: + WeakAutoUnload._module_instances[caller][id(self)] = self + + # Return ourself so that it can be inlined + return self + + def with_callback(self, callback, *args, **kwargs): + """Binds the given callback to the instance. + + :param callable callback: + The callback to bind to this partial. + :param tuple *args: + The argument to pass to the callback. + :param dict **kwargs: + The keyword arguments to pass to the callback. + + :return: + Return itself so that it can be inlined. + + .. note:: + + The callback will be called in the main thread. + """ + self.callback = partial(callback, *args, **kwargs) + return self + + def __call__(self, *args, **kwargs): + """Calls the partial and pass the result to its callback. + + :param tuple *args: + The argument to pass to the partial. + :param dict **kwargs: + The keyword arguments to pass to the partial. + + :raise RuntimeError: + If the plugin that owns this partial was unloaded. + """ + # Sleep it off until the next context switch + try: + if self._waitable and current_thread() is not main_thread(): + sleep(getswitchinterval()) + except AttributeError: + pass + + # Raise if we were unloaded + try: + if self._unloaded: + raise RuntimeError('Partial was unloaded.') + except AttributeError: + pass + + # Call the partial and get the result + result = super().__call__(*args, **kwargs) + + # Try to resolve its callback + try: + callback = self.callback + except AttributeError: + return result + + # Call the callback with the result in the main thread + Delay(0, callback, args=(result,)) + + def _unload_instance(self): + """Flags the instance as unloaded.""" + self._unloaded = True + + +class Queued(WeakAutoUnload, Queue): + """Callables added to this queue are invoked one at a time + from a parallel thread. + + Example: + + .. code:: python + + from threads import Queued + + queue = Queued() + queue(print, 'This print was queued!') + + .. note:: + + If you don't need to manage your own queue, you should consider + using :func:`threads.queued` instead. + + .. warning:: + + If you mutate the internal queue, you are responsible to manage + the internal thread as well. + """ + + @cached_property + def thread(self): + """The internal thread that processes the queue. + + .. warning:: + + This should be left alone unless you manually mutate + the internal queue. + """ + with autounload_disabled(): + return InfiniteThread(self.process).start(wait=True) + + @thread.deleter + def _(self): + """Stops the internal thread.""" + self.thread.stop() + + @wraps(Queue.put) + def put(self, *args, **kwargs): + """Wrapper around `Queue.put` to ensure the thread is running.""" + super().put(*args, **kwargs) + self.thread + + @wraps(Queue.get) + def get(self, *args, **kwargs): + """Wrapper around `Queue.get` to stop the thread when empty.""" + result = super().get(*args, **kwargs) + if self.empty(): + del self.thread + self.task_done() + return result + + def clear(self): + """Empties the internal queue and invalidates the internal thread.""" + with self.mutex: + self.queue.clear() + del self.thread + + def __call__(self, function, *args, **kwargs): + """Adds the given function and argument to the queue. + + :param callable function: + The function to add to the queue. + :param tuple *args: + The arguments to pass to the given function. + :param dict **kwargs: + The keyword arguments to pass to the given function. + + :return: + The partial generated from the given function and arguments. + + :rtype: + Partial + """ + partial = Partial(function, *args, **kwargs) + self.put(partial) + return partial + + def process(self): + """Calls the next partial in the queue. + + :return: + The value returned by the called partial. + """ + partial = self.get() + if partial is None: + return + try: + if partial._unloaded: + return + except AttributeError: + pass + return partial() + + def _unload_instance(self): + """Empties the internal queue and invalidates the internal thread.""" + self.clear() + + +# ============================================================================= +# >> FUNCTIONS +# ============================================================================= +def queued(function, *args, **kwargs): + """Queues a call to the given function. + + :param callable function: + The function to call. + :param tuple *args: + The arguments to pass to the given function. + :param dict **kwargs: + The keyword arguments to pass to the given function. + + :rtype: + Partial + + Example: + + .. code:: python + + import time + from threads import queued + + def done_sleeping(start, result): + print(f'Done waiting and sleeping for {time.time() - start} seconds!') + + queued(time.sleep, 3).with_callback(done_sleeping, time.time()) + """ + global _queued + try: + _queued + except NameError: + with autounload_disabled(): + _queued = Queued() + return _queued(function, *args, **kwargs).unloadable() + + +def threaded(function, *args, **kwargs): + """Calls the given function with the given arguments in a new thread. + + :param callable function: + The function to call. + :param tuple *args: + The arguments to pass to the given function. + :param dict **kwargs: + The keyword arguments to pass to the given function. + + :rtype: + Partial + + Example: + + .. code:: python + + import time + from threads import threaded + + def sleep(seconds): + time.sleep(seconds) + return seconds + + def done_sleeping(result): + print(f'Done sleeping for {result} seconds!') + + threaded(sleep, 2).with_callback(done_sleeping) + + .. note:: + + If the call can wait its turn, consider :func:`threads.queued` instead. + """ + partial = Partial(function, *args, **kwargs).waitable() + GameThread(partial).start() + return partial diff --git a/resource/source-python/translations/_core/core_settings_strings.ini b/resource/source-python/translations/_core/core_settings_strings.ini index bd81f37bb..746b87dce 100644 --- a/resource/source-python/translations/_core/core_settings_strings.ini +++ b/resource/source-python/translations/_core/core_settings_strings.ini @@ -216,3 +216,9 @@ fr = 'Enregistre un avertissement lorsqu'une mise à jour pour Source.Python est ru = 'Писать сообщение в лог, если доступно обновление Source.Python. check_for_update должно быть установлено в 1.' es = 'Registar un aviso cuando haya una actualización disponible de Source.Python. Requiere check_for_updates estar en 1.' zh-cn = '当Source.Python的更新可用时,在日志记录旧版本警告. 需要check_for_update设置为1.' + +[enable_thread_yielding] +en = '''If enabled, yields remaining cycles to Python threads every frame. +If you don't know what you are doing, that probably means you don't need to enable it.''' +fr='''Lorsque activé, donne les cycles restants aux threads Python à chaque tick. +Si vous ne savez pas ce que vous faites, cela signifie probablement que vous n'avez pas besoin de l'activer.''' diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 88e767b4d..af32606ac 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -467,6 +467,19 @@ Set(SOURCEPYTHON_STUDIO_MODULE_SOURCES core/modules/studio/studio_cache_wrap.cpp ) +# ------------------------------------------------------------------ +# Threads module +# ------------------------------------------------------------------ +Set(SOURCEPYTHON_THREADS_MODULE_HEADERS + core/modules/threads/threads.h + core/modules/threads/${SOURCE_ENGINE}/threads.h +) + +Set(SOURCEPYTHON_THREADS_MODULE_SOURCES + core/modules/threads/threads.cpp + core/modules/threads/threads_wrap.cpp +) + # ------------------------------------------------------------------ # Weapons module. # ------------------------------------------------------------------ @@ -566,6 +579,9 @@ Set(SOURCEPYTHON_MODULE_FILES ${SOURCEPYTHON_STUDIO_MODULE_HEADERS} ${SOURCEPYTHON_STUDIO_MODULE_SOURCES} + ${SOURCEPYTHON_THREADS_MODULE_HEADERS} + ${SOURCEPYTHON_THREADS_MODULE_SOURCES} + ${SOURCEPYTHON_WEAPONS_MODULE_HEADERS} ${SOURCEPYTHON_WEAPONS_MODULE_SOURCES} ) diff --git a/src/core/modules/core/core_cache.cpp b/src/core/modules/core/core_cache.cpp index ac92534fa..b9eb2e7cb 100644 --- a/src/core/modules/core/core_cache.cpp +++ b/src/core/modules/core/core_cache.cpp @@ -38,12 +38,12 @@ CCachedProperty::CCachedProperty( object fget=object(), object fset=object(), object fdel=object(), object doc=object(), boost::python::tuple args=boost::python::tuple(), object kwargs=object()) { + m_doc = doc; + set_getter(fget); set_setter(fset); set_deleter(fdel); - m_doc = doc; - m_args = args; if (!kwargs.is_none()) @@ -73,6 +73,11 @@ object CCachedProperty::get_getter() object CCachedProperty::set_getter(object fget) { m_fget = _callable_check(fget, "getter"); + + if (m_doc.is_none()) { + m_doc = m_fget.attr("__doc__"); + } + return fget; } diff --git a/src/core/modules/engines/engines.h b/src/core/modules/engines/engines.h index 41a713a42..a9d6fc2c8 100644 --- a/src/core/modules/engines/engines.h +++ b/src/core/modules/engines/engines.h @@ -199,4 +199,29 @@ class Ray_tExt }; +//----------------------------------------------------------------------------- +// IServerGameDLL wrapper class. +//----------------------------------------------------------------------------- +class IServerGameDLLWrapper : public IServerGameDLL +{ +public: + float m_fAutoSaveDangerousTime; + float m_fAutoSaveDangerousMinHealthToCommit; + bool m_bIsHibernating; +}; + + +//----------------------------------------------------------------------------- +// IServerGameDLL extension class. +//----------------------------------------------------------------------------- +class IServerGameDLLExt +{ +public: + static bool IsHibernating(IServerGameDLL *pServerGameDLL) + { + return reinterpret_cast(pServerGameDLL)->m_bIsHibernating; + } +}; + + #endif // _ENGINES_H diff --git a/src/core/modules/engines/engines_server_wrap.cpp b/src/core/modules/engines/engines_server_wrap.cpp index 1113d8368..5570b5af4 100644 --- a/src/core/modules/engines/engines_server_wrap.cpp +++ b/src/core/modules/engines/engines_server_wrap.cpp @@ -787,6 +787,13 @@ static void export_server_game_dll(scope _server) class_ ServerGameDLL("_ServerGameDLL", no_init); // Methods... + ServerGameDLL.def( + "is_hibernating", + &IServerGameDLLExt::IsHibernating, + "Returns whether the server is currently hibernating." + ); + + // Properties... ServerGameDLL.add_property( "all_server_classes", make_function( diff --git a/src/core/modules/threads/blade/threads.h b/src/core/modules/threads/blade/threads.h new file mode 100644 index 000000000..f9040e710 --- /dev/null +++ b/src/core/modules/threads/blade/threads.h @@ -0,0 +1,38 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +#ifndef _THREADS_BLADE_H +#define _THREADS_BLADE_H + +#if defined(_WIN32) + #define TY_NotImplemented +#elif defined(__linux__) + #define TY_NotImplemented +#else + #define TY_NotImplemented +#endif + +#endif // _THREADS_BLADE_H diff --git a/src/core/modules/threads/bms/threads.h b/src/core/modules/threads/bms/threads.h new file mode 100644 index 000000000..94fe75db1 --- /dev/null +++ b/src/core/modules/threads/bms/threads.h @@ -0,0 +1,38 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +#ifndef _THREADS_BMS_H +#define _THREADS_BMS_H + +#if defined(_WIN32) + #define TY_NotImplemented +#elif defined(__linux__) + #define TY_NotImplemented +#else + #define TY_NotImplemented +#endif + +#endif // _THREADS_BMS_H diff --git a/src/core/modules/threads/csgo/threads.h b/src/core/modules/threads/csgo/threads.h new file mode 100644 index 000000000..4cc8afb3c --- /dev/null +++ b/src/core/modules/threads/csgo/threads.h @@ -0,0 +1,53 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +#ifndef _THREADS_CSGO_H +#define _THREADS_CSGO_H + +#if defined(_WIN32) + #include + #define TY_Unit ms + #define TY_Hook ThreadSleep + #define TY_Sleep Sleep + #define TY_Yield if (!SwitchToThread()) YieldProcessor +#elif defined(__linux__) + #include + #include + #define TY_Unit ns + extern "C" void ThreadNanoSleep(unsigned ns); + #define TY_Hook ThreadNanoSleep + #define TY_Sleep(ns) \ + struct timespec ts = { \ + .tv_sec = 0, \ + .tv_nsec = ns \ + }; \ + nanosleep(&ts, NULL); + #define TY_Yield ThreadPause +#else + #define TY_NotImplemented +#endif + +#endif // _THREADS_CSGO_H diff --git a/src/core/modules/threads/gmod/threads.h b/src/core/modules/threads/gmod/threads.h new file mode 100644 index 000000000..1851ec49b --- /dev/null +++ b/src/core/modules/threads/gmod/threads.h @@ -0,0 +1,38 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +#ifndef _THREADS_GMOD_H +#define _THREADS_GMOD_H + +#if defined(_WIN32) + #define TY_NotImplemented +#elif defined(__linux__) + #define TY_NotImplemented +#else + #define TY_NotImplemented +#endif + +#endif // _THREADS_GMOD_H diff --git a/src/core/modules/threads/l4d2/threads.h b/src/core/modules/threads/l4d2/threads.h new file mode 100644 index 000000000..955b59de5 --- /dev/null +++ b/src/core/modules/threads/l4d2/threads.h @@ -0,0 +1,38 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +#ifndef _THREADS_L4D2_H +#define _THREADS_L4D2_H + +#if defined(_WIN32) + #define TY_NotImplemented +#elif defined(__linux__) + #define TY_NotImplemented +#else + #define TY_NotImplemented +#endif + +#endif // _THREADS_L4D2_H diff --git a/src/core/modules/threads/orangebox/threads.h b/src/core/modules/threads/orangebox/threads.h new file mode 100644 index 000000000..790ffa265 --- /dev/null +++ b/src/core/modules/threads/orangebox/threads.h @@ -0,0 +1,52 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +#ifndef _THREADS_ORANGEBOX_H +#define _THREADS_ORANGEBOX_H + +#if defined(_WIN32) + #include + #define TY_Unit ms + #define TY_Hook ThreadSleep + #define TY_Sleep Sleep + #define TY_Yield if (!SwitchToThread()) YieldProcessor +#elif defined(__linux__) + #include + #include + #define TY_Unit us + #define TY_Hook usleep + #define TY_Sleep(us) \ + struct timespec ts = { \ + .tv_sec = (long int) (us / 1000000), \ + .tv_nsec = (long int) (us % 1000000) * 1000ul \ + }; \ + nanosleep(&ts, NULL); + #define TY_Yield ThreadPause +#else + #define TY_NotImplemented +#endif + +#endif // _THREADS_ORANGEBOX_H diff --git a/src/core/modules/threads/threads.cpp b/src/core/modules/threads/threads.cpp new file mode 100644 index 000000000..fecb69ad1 --- /dev/null +++ b/src/core/modules/threads/threads.cpp @@ -0,0 +1,127 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +//----------------------------------------------------------------------------- +// Includes. +//----------------------------------------------------------------------------- +#include "sp_main.h" +#include "threads.h" +#include "utilities.h" +#include "utilities/wrap_macros.h" + +#include "dbg.h" +#include "eiface.h" +#include "convar.h" +#include "tier0/threadtools.h" + +#include ENGINE_INCLUDE_PATH(threads.h) + + +//----------------------------------------------------------------------------- +// Externals. +//----------------------------------------------------------------------------- +extern CGlobalVars *gpGlobals; + + +//----------------------------------------------------------------------------- +// ConVar "sp_thread_yielding" registration. +//----------------------------------------------------------------------------- +ConVar sp_thread_yielding( + "sp_thread_yielding", "0", FCVAR_NONE, +#ifndef TY_NotImplemented + "If enabled, yields remaining cycles to Python threads every frame." +#else + "Thread yielding is not implemented on '" XSTRINGIFY(SOURCE_ENGINE) "' at this time." +#endif +); + + +//----------------------------------------------------------------------------- +// CThreadYielder initialization. +//----------------------------------------------------------------------------- +unsigned long CThreadYielder::s_nRefCount = 0; + + +//----------------------------------------------------------------------------- +// CThreadYielder class. +//----------------------------------------------------------------------------- +PyObject *CThreadYielder::__enter__(PyObject *pSelf) +{ + if (!sp_thread_yielding.GetBool()) { + return incref(pSelf); + } + +#ifndef TY_NotImplemented + ++s_nRefCount; + + static bool s_bHooked = false; + if (!s_bHooked) { + WriteJMP((unsigned char *)::TY_Hook, (void *)CThreadYielder::ThreadSleep); + s_bHooked = true; + } +#else + static object warn = import("warnings").attr("warn"); + warn("Thread yielding is not implemented on '" XSTRINGIFY(SOURCE_ENGINE) "' at this time."); +#endif + + return incref(pSelf); +} + +void CThreadYielder::__exit__(PyObject *, PyObject *, PyObject *, PyObject *) +{ +#ifndef TY_NotImplemented + if (!s_nRefCount) { + return; + } + + --s_nRefCount; +#endif +} + +void CThreadYielder::ThreadSleep(unsigned nTime) +{ +#ifndef TY_NotImplemented + if (!s_nRefCount || !ThreadInMainThread() || !PyGILState_Check()) { + TY_Sleep(nTime); + } + else { + Py_BEGIN_ALLOW_THREADS; + TY_Yield(); + TY_Sleep(nTime); + Py_END_ALLOW_THREADS; + DevMsg(2, MSG_PREFIX "Yielded %d%s on tick %d!\n", nTime, XSTRINGIFY(TY_Unit), gpGlobals->framecount); + } +#endif +} + +bool CThreadYielder::IsImplemented() +{ +#ifndef TY_NotImplemented + return true; +#else + return false; +#endif +} diff --git a/src/core/modules/threads/threads.h b/src/core/modules/threads/threads.h new file mode 100644 index 000000000..c005d680c --- /dev/null +++ b/src/core/modules/threads/threads.h @@ -0,0 +1,53 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +#ifndef _THREADS_H +#define _THREADS_H + +//----------------------------------------------------------------------------- +// Includes. +//----------------------------------------------------------------------------- +#include "boost/python.hpp" + + +//----------------------------------------------------------------------------- +// CThreadYielder class. +//----------------------------------------------------------------------------- +class CThreadYielder +{ +private: + static unsigned long s_nRefCount; + +public: + static PyObject *__enter__(PyObject *pSelf); + static void __exit__(PyObject *, PyObject *, PyObject *, PyObject *); + + static void ThreadSleep(unsigned nTime = 0); + static bool IsImplemented(); +}; + + +#endif // _THREADS_H diff --git a/src/core/modules/threads/threads_wrap.cpp b/src/core/modules/threads/threads_wrap.cpp new file mode 100644 index 000000000..a59542427 --- /dev/null +++ b/src/core/modules/threads/threads_wrap.cpp @@ -0,0 +1,83 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2023 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +//----------------------------------------------------------------------------- +// Includes. +//----------------------------------------------------------------------------- +#include "export_main.h" +#include "threads.h" +#include "modules/memory/memory_tools.h" + + +//----------------------------------------------------------------------------- +// Namespaces. +//----------------------------------------------------------------------------- +using namespace boost::python; + + +//----------------------------------------------------------------------------- +// Forward declarations. +//----------------------------------------------------------------------------- +static void export_thread_yielder(scope); + + +//----------------------------------------------------------------------------- +// Declare the _threads module. +//----------------------------------------------------------------------------- +DECLARE_SP_MODULE(_threads) +{ + export_thread_yielder(_threads); +} + + +//----------------------------------------------------------------------------- +// Exports CThreadYielder. +//----------------------------------------------------------------------------- +void export_thread_yielder(scope _threads) +{ + class_ ThreadYielder( + "ThreadYielder", + "When in context, yields remaining cycles to Python threads every frame.\n" + "\n" + ".. note::\n" + "\n" + " :data:`threads.sp_thread_yielding` must be enabled for it to be effective." + ); + + // Special methods... + ThreadYielder.def("__enter__", &CThreadYielder::__enter__); + ThreadYielder.def("__exit__", &CThreadYielder::__exit__); + + // Methods... + ThreadYielder.def( + "is_implemented", + &CThreadYielder::IsImplemented, + "Returns whether thread yielding is implemented on the current game." + ).staticmethod("is_implemented"); + + // Add memory tools... + ThreadYielder ADD_MEM_TOOLS(CThreadYielder); +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy