Compare commits

..
Author SHA1 Message Date
deepend f33ea41573 add opt-in asyncio task integration
(cherry picked from commit 73ccded3ac)
2026-11-08 06:06:15 -06:00
6 changed files with 130 additions and 126 deletions

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
# Optional Python asyncio integration
`run_async(coroutine, context=None, interval=10)` returns an AsyncTask with
`done()`, `cancel()` and `result()`. Each invocation owns a separate event loop
pumped by an existing main-loop timer. A ready stop callback ensures selector
polling never blocks the UI. The captured context is restored after every pump;
a closed context cancels the work. No process-wide event-loop policy is changed.
Existing synchronous scripts are unaffected. Use `asyncio.get_running_loop()`
inside the coroutine. Child tasks use the same loop/context and are cancelled
when the main coroutine finishes or the script unloads. Cancellation cleanup is
bounded; tasks must not suppress cancellation indefinitely. Cleanup uses the
captured context while it remains open; after it closes, cancellation cleanup uses
the current valid context. Use explicit Context methods for context-specific
output in finally blocks, and do not issue context-dependent commands there. This provides normal
asyncio sockets/timers, not Windows subprocess support or a new HTTP library.
Asyncio hostname resolution can use its executor; use a resolver appropriate to
your library if you require no worker threads. Blocking work inside a coroutine
still blocks the client and must be avoided.
```python
import asyncio
import zoitechat
async def later():
await asyncio.sleep(1)
zoitechat.prnt('Still in the context where this task was started')
task = zoitechat.run_async(later())
```
-24
View File
@@ -1,24 +0,0 @@
# Core-filtered hooks
Python: `hook_server_filtered(name, callback, userdata=None, priority=PRI_NORM,
connection_id=None, channel=None)` and `hook_print_filtered(...)`.
Callbacks match the existing attrs variants: `(word, word_eol, userdata, attrs)`.
The connection ID distinguishes duplicate names; None/-1 means any connection
(including connection ID zero). A channel restriction uses that connection's
IRC comparator. None means any context; an empty string is an exact restriction.
C: the corresponding zoitechat_hook_*_filtered functions accept
`(ph, name, pri, flags, connection_id, channel, callback, userdata)`;
flags=0, ID=-1 for any and NULL channel for any. Check
`get_info("api_context_filters")` for "1" before accessing the new Windows slots.
Filtering takes place before Python/interpreter entry. Existing unrestricted
hooks, priority, EAT semantics and unload/unhook handling are unchanged.
Filters refer to the core-selected context, not an IRC target parsed from the
line: unknown queries/channels can still have a server-tab context, and QUIT
has its usual single server-hook context. Connection IDs last only this process.
```python
zoitechat.hook_print_filtered('Channel Message', callback,
connection_id=zoitechat.get_prefs('id'), channel='#zoitechat')
```
+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)
-36
View File
@@ -1,36 +0,0 @@
"""Context restrictions evaluated by core before entering Python."""
import operator
import _zoitechat as api
from _zoitechat_embedded import ffi, lib
__all__ = ['hook_server_filtered', 'hook_print_filtered']
def _register(name, callback, userdata, priority, connection_id, channel, server):
if not callable(callback):
raise TypeError('callback must be callable')
connection_id = -1 if connection_id is None else operator.index(connection_id)
if connection_id < -1 or connection_id > 2147483647:
raise ValueError('connection_id must fit the C API; -1 means any')
plugin = api.__get_current_plugin()
hook = plugin.add_hook(callback, userdata)
register = lib.zoitechat_hook_server_filtered if server else lib.zoitechat_hook_print_filtered
dispatch = lib._on_server_attrs_hook if server else lib._on_print_attrs_hook
handle = register(lib.ph, name.encode(), priority, 0, connection_id,
ffi.NULL if channel is None else channel.encode(), dispatch, hook.handle)
if handle == ffi.NULL:
hook.is_unload = True
plugin.remove_hook(id(hook))
raise RuntimeError('unable to install filtered hook')
hook.zoitechat_hook = handle
return id(hook)
def hook_server_filtered(name, callback, userdata=None, priority=api.PRI_NORM,
connection_id=None, channel=None):
return _register(name, callback, userdata, priority, connection_id, channel, True)
def hook_print_filtered(name, callback, userdata=None, priority=api.PRI_NORM,
connection_id=None, channel=None):
return _register(name, callback, userdata, priority, connection_id, channel, False)
-52
View File
@@ -99,9 +99,6 @@ struct _zoitechat_hook
int tag; /* for timers & FDs only */
int type; /* HOOK_* */
int pri; /* fd */ /* priority / fd for HOOK_FD only */
int connection_filter;
char *channel_filter;
gboolean context_filtered;
};
struct _zoitechat_list
@@ -354,8 +351,6 @@ plugin_add (session *sess, char *filename, void *handle, void *init_func,
pl->zoitechat_emit_print_attrs = zoitechat_emit_print_attrs;
pl->zoitechat_event_attrs_create = zoitechat_event_attrs_create;
pl->zoitechat_event_attrs_free = zoitechat_event_attrs_free;
pl->zoitechat_hook_server_filtered = zoitechat_hook_server_filtered;
pl->zoitechat_hook_print_filtered = zoitechat_hook_print_filtered;
/* run zoitechat_plugin_init, if it returns 0, close the plugin */
if (((zoitechat_init_func *)init_func) (pl, &pl->name, &pl->desc, &pl->version, arg) == 0)
@@ -626,15 +621,6 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
hook = list->data;
next = list->next;
if (hook->context_filtered &&
(!is_session (sess) ||
(hook->connection_filter != -1 && hook->connection_filter != sess->server->id) ||
(hook->channel_filter && sess->server->p_cmp (hook->channel_filter, sess->channel))))
{
list = next;
continue; /* filter in C before entering an interpreter */
}
hook->pl->context = sess;
/* run the plugin's callback function */
@@ -1038,45 +1024,10 @@ zoitechat_unhook (zoitechat_plugin *ph, zoitechat_hook *hook)
g_free (hook->name); /* NULL for timers & fds */
g_free (hook->help_text); /* NULL for non-commands */
g_free (hook->channel_filter);
hook->channel_filter = NULL;
return hook->userdata;
}
static zoitechat_hook *
plugin_add_filtered_hook (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
int type, void *callback, void *userdata)
{
zoitechat_hook *hook;
if (flags != 0 || connection_id < -1 || !name || !callback)
return NULL;
hook = plugin_add_hook (ph, type, pri, name, NULL, callback, 0, userdata);
hook->context_filtered = TRUE;
hook->connection_filter = connection_id;
hook->channel_filter = g_strdup (channel);
return hook;
}
zoitechat_hook *
zoitechat_hook_server_filtered (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
zoitechat_serv_attrs_cb *callback, void *userdata)
{
return plugin_add_filtered_hook (ph, name, pri, flags, connection_id, channel,
HOOK_SERVER_ATTRS, callback, userdata);
}
zoitechat_hook *
zoitechat_hook_print_filtered (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
zoitechat_print_attrs_cb *callback, void *userdata)
{
return plugin_add_filtered_hook (ph, name, pri, flags, connection_id, channel,
HOOK_PRINT_ATTRS, callback, userdata);
}
zoitechat_hook *
zoitechat_hook_command (zoitechat_plugin *ph, const char *name, int pri,
zoitechat_cmd_cb *callb, const char *help_text, void *userdata)
@@ -1236,9 +1187,6 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id)
return text_find_format_string (e);
}
if (strcmp (id, "api_context_filters") == 0)
return "1";
hash = str_hash (id);
/* do the session independant ones first */
switch (hash)
-14
View File
@@ -206,18 +206,6 @@ struct _zoitechat_plugin
#endif
/* api_context_filters=1: connection_id -1 is any; NULL channel is any. */
zoitechat_hook *zoitechat_hook_server_filtered (zoitechat_plugin *ph,
const char *name, int pri, int flags, int connection_id, const char *channel,
int (*callback) (char *word[], char *word_eol[],
zoitechat_event_attrs *attrs, void *userdata),
void *userdata);
zoitechat_hook *zoitechat_hook_print_filtered (zoitechat_plugin *ph,
const char *name, int pri, int flags, int connection_id, const char *channel,
int (*callback) (char *word[], zoitechat_event_attrs *attrs, void *userdata),
void *userdata);
zoitechat_hook *
zoitechat_hook_command (zoitechat_plugin *ph,
const char *name,
@@ -437,8 +425,6 @@ zoitechat_pluginpref_list (zoitechat_plugin *ph,
#define zoitechat_hook_server ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server)
#define zoitechat_hook_server_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_attrs)
#define zoitechat_hook_print ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print)
#define zoitechat_hook_server_filtered ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_filtered)
#define zoitechat_hook_print_filtered ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_filtered)
#define zoitechat_hook_print_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_attrs)
#define zoitechat_hook_timer ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_timer)
#define zoitechat_hook_fd ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_fd)