Skip to content

Threads updates. #491

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
threads module
==============

.. automodule:: threads
:members:
:undoc-members:
:show-inheritance:
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://forums.alliedmods.net/showthread.php?t=277703&>`_
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.
24 changes: 24 additions & 0 deletions addons/source-python/packages/source-python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def load():
setup_entities_listener()
setup_versioning()
setup_sqlite()
setup_threads()


def unload():
Expand Down Expand Up @@ -534,3 +535,26 @@ def flush(self):
'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.')


# =============================================================================
# >> 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'),
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 63 additions & 22 deletions addons/source-python/packages/source-python/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -95,6 +97,9 @@
# Get the platform the server is on
PLATFORM = system().lower()

# Whether auto unload classes are disabled
_autounload_disabled = False


# =============================================================================
# >> CLASSES
Expand All @@ -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('<frozen'):
return self
# Return if auto unload classes are disabled
if _autounload_disabled:
return self

# Resolve the calling module name
try:
name = frame.f_globals['__name__']
except KeyError:
try:
name = getmodule(frame).__name__
except AttributeError:
name = getmodulename(path)
# Get the module name of the calling plugin
name = get_calling_plugin()

# Call class-specific logic for adding the instance.
if name is not None:
Expand Down Expand Up @@ -270,6 +258,59 @@ def __init__(
# =============================================================================
# >> 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('<frozen'):
return

# Resolve the calling module name
try:
name = frame.f_globals['__name__']
except KeyError:
try:
name = getmodule(frame).__name__
except AttributeError:
name = getmodulename(path)

# Return the name
return name


def echo_console(text):
"""Echo a message to the server's console.

Expand Down
17 changes: 17 additions & 0 deletions addons/source-python/packages/source-python/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def _check_settings(self):
self._check_logging_settings()
self._check_user_settings()
self._check_auth_settings()
self._check_thread_settings()

def _check_base_settings(self):
"""Add base settings if they are missing."""
Expand Down Expand Up @@ -215,6 +216,22 @@ def _check_backend_settings(self, backend):
else:
backend_settings[option] = value

def _check_thread_settings(self):
"""Add thread settings if they are missing."""
from threads import ThreadYielder
if not ThreadYielder.is_implemented():
return

self.setdefault(
'THREAD_SETTINGS', {}
).setdefault('enable_thread_yielding', '0')
self['THREAD_SETTINGS'].comments[
'enable_thread_yielding'
] = _core_strings[
'enable_thread_yielding'
].get_string(self._language).splitlines()


# Get the _CoreSettings instance
_core_settings = _CoreSettings(CFG_PATH / 'core_settings.ini',
encoding='utf8')
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@
'OnTick',
'OnVersionUpdate',
'OnServerOutput',
'OnServerHibernating',
'OnServerWakingUp',
'get_button_combination_status',
'on_client_active_listener_manager',
'on_client_connect_listener_manager',
Expand Down Expand Up @@ -170,6 +172,8 @@
'on_player_transmit_listener_manager',
'on_player_run_command_listener_manager',
'on_button_state_changed_listener_manager',
'on_server_hibernating_listener_manager',
'on_server_waking_up_listener_manager',
)


Expand All @@ -185,6 +189,8 @@
on_plugin_loading_manager = ListenerManager()
on_plugin_unloading_manager = ListenerManager()
on_level_end_listener_manager = ListenerManager()
on_server_hibernating_listener_manager = ListenerManager()
on_server_waking_up_listener_manager = ListenerManager()

_check_for_update = ConVar(
'sp_check_for_update',
Expand Down Expand Up @@ -549,6 +555,18 @@ class OnServerOutput(ListenerManagerDecorator):
manager = on_server_output_listener_manager


class OnServerHibernating(ListenerManagerDecorator):
"""Register/unregister a server hibernating listener."""

manager = on_server_hibernating_listener_manager


class OnServerWakingUp(ListenerManagerDecorator):
"""Register/unregister a server waking up listener."""

manager = on_server_waking_up_listener_manager


# =============================================================================
# >> FUNCTIONS
# =============================================================================
Expand Down Expand Up @@ -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...
Expand Down
24 changes: 1 addition & 23 deletions addons/source-python/packages/source-python/listeners/tick.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
import time

from enum import IntEnum
from threading import Thread
from warnings import warn

# Source.Python
from core import AutoUnload
Expand All @@ -28,7 +26,7 @@
# =============================================================================
__all__ = (
'Delay',
'GameThread',
'GameThread', # Backward compatibility
'Repeat',
'RepeatStatus',
)
Expand All @@ -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
# =============================================================================
Expand Down
Loading