add opt-in asyncio task integration

This commit is contained in:
2026-10-02 14:13:00 -06:00
parent 44e677a973
commit 73ccded3ac
2 changed files with 130 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
"""Opt-in asyncio tasks pumped without blocking the client main loop."""
import asyncio
import operator
import traceback
import _zoitechat as api
__all__ = ['AsyncTask', 'run_async']
class AsyncTask:
"""A script-owned task and isolated event loop; cancel() is cooperative."""
def __init__(self, coroutine, plugin, context, interval):
self._plugin = plugin
self._context = context
self._loop = asyncio.new_event_loop()
self._task = self._loop.create_task(coroutine)
self._closed = False
self._unload_hook = api.hook_unload(lambda data: self._close())
self._timer_hook = api.hook_timer(interval, self._tick)
def done(self):
return self._task.done()
def cancel(self):
if not self._closed:
return self._task.cancel()
return False
def result(self):
return self._task.result()
def _pump(self):
# A ready stop callback forces the selector's timeout to zero.
self._loop.call_soon(self._loop.stop)
self._loop.run_forever()
def _close(self):
if self._closed:
return
self._closed = True
previous = api.get_context()
self._context.set()
try:
pending = asyncio.all_tasks(self._loop)
for task in pending:
task.cancel()
# Bound cleanup work; tasks must cooperate with cancellation.
for unused in range(3):
if not any(not task.done() for task in pending):
break
self._pump()
for task in pending:
if not task.done():
self._loop.call_exception_handler({
'message': 'Script task ignored cancellation during unload',
'task': task,
})
finally:
self._loop.close()
previous.set()
def _tick(self, userdata):
if self._closed:
return False
previous = api.get_context()
if not self._context.set():
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
try:
self._pump()
if not self._task.done():
return True
try:
self._task.result()
except asyncio.CancelledError:
pass
except Exception:
traceback.print_exc()
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
except Exception:
traceback.print_exc()
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
finally:
previous.set()
def run_async(coroutine, context=None, interval=10):
"""Schedule a coroutine in a captured context; return an AsyncTask handle."""
interval = operator.index(interval)
if interval < 1 or interval > 2147483647:
raise ValueError('interval must be a positive C int in milliseconds')
if not asyncio.iscoroutine(coroutine):
raise TypeError('run_async expects a coroutine object')
plugin = api.__get_current_plugin()
return AsyncTask(coroutine, plugin, context or api.get_context(), interval)