Skip to content

Commit 1ca604e

Browse files
committed
Threads updates.
Implemented workaround for threads starvation issues. Added new threads module that provides various utilities. Added core.get_calling_plugin and core.autounload_disabled. Made bitbuf messages thread-safe. Fixed CachedProperty not properly wrapping its getter's docstring. Added server_game_dll.is_hibernating. Added OnServerHibernating and OnServerWakingUp listeners.
1 parent f3272be commit 1ca604e

File tree

25 files changed

+1336
-53
lines changed

25 files changed

+1336
-53
lines changed

addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,34 @@ message is logged/printed or not.
633633
return OutputReturn.CONTINUE
634634
635635
636+
OnServerHibernating
637+
-------------------
638+
639+
Called when the server starts hibernating.
640+
641+
.. code-block:: python
642+
643+
from listeners import OnServerHibernating
644+
645+
@OnServerHibernating
646+
def on_server_hibernating():
647+
...
648+
649+
650+
OnServerWakingUp
651+
----------------
652+
653+
Called when the server is waking up from hibernation.
654+
655+
.. code-block:: python
656+
657+
from listeners import OnServerWakingUp
658+
659+
@OnServerWakingUp
660+
def on_server_waking_up():
661+
...
662+
663+
636664
OnTick
637665
------
638666

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
threads module
2+
==============
3+
4+
.. automodule:: threads
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

addons/source-python/docs/source-python/source/general/known-issues.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ which causes either EventScripts to be loaded with Source.Python's Python
1111
version or vice versa. This doesn't work well and results in a crash on startup.
1212

1313
SourceMod's Accelerator incompatibility
14-
---------------------------------------------------
14+
---------------------------------------
1515
If you are running `SourceMod's Accelerator <https://forums.alliedmods.net/showthread.php?t=277703&>`_
1616
with Source.Python, you may experience random crashes that would normally be caught since this extension
1717
prevents us from catching and preventing them.
18+
19+
Hibernation issues
20+
------------------
21+
Some features (such as tick listeners, delays, Python threads, etc.) do not work on some games (e.g. CS:GO)
22+
while the server is hibernating. If you require these features at all time, please disable hibernation.

addons/source-python/packages/source-python/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def load():
9292
setup_entities_listener()
9393
setup_versioning()
9494
setup_sqlite()
95+
setup_threads()
9596

9697

9798
def unload():
@@ -534,3 +535,26 @@ def flush(self):
534535
'Source.Python should continue working, but we would like to figure '
535536
'out in which situations sys.stdout is None to be able to fix this '
536537
'issue instead of applying a workaround.')
538+
539+
540+
# =============================================================================
541+
# >> THREADS
542+
# =============================================================================
543+
def setup_threads():
544+
"""Setup threads."""
545+
import listeners.tick
546+
from threads import GameThread
547+
listeners.tick.GameThread = GameThread # Backward compatibility
548+
549+
from threads import ThreadYielder
550+
if not ThreadYielder.is_implemented():
551+
return
552+
553+
from core.settings import _core_settings
554+
from threads import sp_thread_yielding
555+
556+
sp_thread_yielding.set_string(
557+
_core_settings.get(
558+
'THREAD_SETTINGS', {}
559+
).get('enable_thread_yielding', '0'),
560+
)

addons/source-python/packages/source-python/auth/backends/sql.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
from auth.manager import ParentPermissions
1919
# Paths
2020
from paths import SP_DATA_PATH
21-
# Listeners
22-
from listeners.tick import GameThread
21+
# Threads
22+
from threads import GameThread
2323

2424
# Site-Packges Imports
2525
# SQL Alechemy

addons/source-python/packages/source-python/core/__init__.py

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@
7474
'SOURCE_ENGINE',
7575
'SOURCE_ENGINE_BRANCH',
7676
'Tokenize',
77+
'autounload_disabled',
7778
'check_info_output',
7879
'console_message',
7980
'create_checksum',
8081
'echo_console',
82+
'get_calling_plugin',
8183
'get_core_modules',
8284
'get_interface',
8385
'get_public_ip',
@@ -95,6 +97,9 @@
9597
# Get the platform the server is on
9698
PLATFORM = system().lower()
9799

100+
# Whether auto unload classes are disabled
101+
_autounload_disabled = False
102+
98103

99104
# =============================================================================
100105
# >> CLASSES
@@ -113,29 +118,12 @@ def __new__(cls, *args, **kwargs):
113118
# Get the class instance
114119
self = super().__new__(cls)
115120

116-
# Get the calling frame
117-
frame = currentframe().f_back
118-
119-
# Get the calling path
120-
path = frame.f_code.co_filename
121-
122-
# Don't keep hostage instances that will never be unloaded
123-
while not path.startswith(PLUGIN_PATH):
124-
frame = frame.f_back
125-
if frame is None:
126-
return self
127-
path = frame.f_code.co_filename
128-
if path.startswith('<frozen'):
129-
return self
121+
# Return if auto unload classes are disabled
122+
if _autounload_disabled:
123+
return self
130124

131-
# Resolve the calling module name
132-
try:
133-
name = frame.f_globals['__name__']
134-
except KeyError:
135-
try:
136-
name = getmodule(frame).__name__
137-
except AttributeError:
138-
name = getmodulename(path)
125+
# Get the module name of the calling plugin
126+
name = get_calling_plugin()
139127

140128
# Call class-specific logic for adding the instance.
141129
if name is not None:
@@ -270,6 +258,59 @@ def __init__(
270258
# =============================================================================
271259
# >> FUNCTIONS
272260
# =============================================================================
261+
@contextmanager
262+
def autounload_disabled():
263+
"""Context that disables auto unload classes."""
264+
global _autounload_disabled
265+
prev = _autounload_disabled
266+
_autounload_disabled = True
267+
try:
268+
yield
269+
finally:
270+
_autounload_disabled = prev
271+
272+
273+
def get_calling_plugin(depth=0):
274+
"""Resolves the name of the calling plugin.
275+
276+
:param int depth:
277+
How many frame back to start looking for a plugin.
278+
279+
:rtype:
280+
str
281+
"""
282+
# Get the current frame
283+
frame = currentframe()
284+
285+
# Go back the specificed depth
286+
for _ in range(depth + 1):
287+
frame = frame.f_back
288+
289+
# Get the calling path
290+
path = frame.f_code.co_filename
291+
292+
# Don't keep hostage instances that will never be unloaded
293+
while not path.startswith(PLUGIN_PATH):
294+
frame = frame.f_back
295+
if frame is None:
296+
return
297+
path = frame.f_code.co_filename
298+
if path.startswith('<frozen'):
299+
return
300+
301+
# Resolve the calling module name
302+
try:
303+
name = frame.f_globals['__name__']
304+
except KeyError:
305+
try:
306+
name = getmodule(frame).__name__
307+
except AttributeError:
308+
name = getmodulename(path)
309+
310+
# Return the name
311+
return name
312+
313+
273314
def echo_console(text):
274315
"""Echo a message to the server's console.
275316

addons/source-python/packages/source-python/core/settings.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def _check_settings(self):
6666
self._check_logging_settings()
6767
self._check_user_settings()
6868
self._check_auth_settings()
69+
self._check_thread_settings()
6970

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

219+
def _check_thread_settings(self):
220+
"""Add thread settings if they are missing."""
221+
from threads import ThreadYielder
222+
if not ThreadYielder.is_implemented():
223+
return
224+
225+
self.setdefault(
226+
'THREAD_SETTINGS', {}
227+
).setdefault('enable_thread_yielding', '0')
228+
self['THREAD_SETTINGS'].comments[
229+
'enable_thread_yielding'
230+
] = _core_strings[
231+
'enable_thread_yielding'
232+
].get_string(self._language).splitlines()
233+
234+
218235
# Get the _CoreSettings instance
219236
_core_settings = _CoreSettings(CFG_PATH / 'core_settings.ini',
220237
encoding='utf8')

addons/source-python/packages/source-python/listeners/__init__.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@
129129
'OnTick',
130130
'OnVersionUpdate',
131131
'OnServerOutput',
132+
'OnServerHibernating',
133+
'OnServerWakingUp',
132134
'get_button_combination_status',
133135
'on_client_active_listener_manager',
134136
'on_client_connect_listener_manager',
@@ -170,6 +172,8 @@
170172
'on_player_transmit_listener_manager',
171173
'on_player_run_command_listener_manager',
172174
'on_button_state_changed_listener_manager',
175+
'on_server_hibernating_listener_manager',
176+
'on_server_waking_up_listener_manager',
173177
)
174178

175179

@@ -185,6 +189,8 @@
185189
on_plugin_loading_manager = ListenerManager()
186190
on_plugin_unloading_manager = ListenerManager()
187191
on_level_end_listener_manager = ListenerManager()
192+
on_server_hibernating_listener_manager = ListenerManager()
193+
on_server_waking_up_listener_manager = ListenerManager()
188194

189195
_check_for_update = ConVar(
190196
'sp_check_for_update',
@@ -549,6 +555,18 @@ class OnServerOutput(ListenerManagerDecorator):
549555
manager = on_server_output_listener_manager
550556

551557

558+
class OnServerHibernating(ListenerManagerDecorator):
559+
"""Register/unregister a server hibernating listener."""
560+
561+
manager = on_server_hibernating_listener_manager
562+
563+
564+
class OnServerWakingUp(ListenerManagerDecorator):
565+
"""Register/unregister a server waking up listener."""
566+
567+
manager = on_server_waking_up_listener_manager
568+
569+
552570
# =============================================================================
553571
# >> FUNCTIONS
554572
# =============================================================================
@@ -697,7 +715,13 @@ def _pre_fire_output(args):
697715
@PreHook(get_virtual_function(server_game_dll, _hibernation_function_name))
698716
def _pre_hibernation_function(stack_data):
699717
"""Called when the server is hibernating."""
700-
if not stack_data[1]:
718+
hibernating = stack_data[1]
719+
if hibernating:
720+
on_server_hibernating_listener_manager.notify()
721+
else:
722+
on_server_waking_up_listener_manager.notify()
723+
724+
if not hibernating:
701725
return
702726

703727
# Disconnect all bots...

addons/source-python/packages/source-python/listeners/tick.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
import time
1212

1313
from enum import IntEnum
14-
from threading import Thread
15-
from warnings import warn
1614

1715
# Source.Python
1816
from core import AutoUnload
@@ -28,7 +26,7 @@
2826
# =============================================================================
2927
__all__ = (
3028
'Delay',
31-
'GameThread',
29+
'GameThread', # Backward compatibility
3230
'Repeat',
3331
'RepeatStatus',
3432
)
@@ -41,26 +39,6 @@
4139
listeners_tick_logger = listeners_logger.tick
4240

4341

44-
# =============================================================================
45-
# >> THREAD WORKAROUND
46-
# =============================================================================
47-
class GameThread(WeakAutoUnload, Thread):
48-
"""A subclass of :class:`threading.Thread` that throws a warning if the
49-
plugin that created the thread has been unloaded while the thread is still
50-
running.
51-
"""
52-
53-
def _add_instance(self, caller):
54-
super()._add_instance(caller)
55-
self._caller = caller
56-
57-
def _unload_instance(self):
58-
if self.is_alive():
59-
warn(
60-
f'Thread "{self.name}" ({self.ident}) from "{self._caller}" '
61-
f'is running even though its plugin has been unloaded!')
62-
63-
6442
# =============================================================================
6543
# >> DELAY CLASSES
6644
# =============================================================================

0 commit comments

Comments
 (0)