mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-11-08 17:05:38 +00:00
Compare commits
2
Commits
master
...
api/asyncio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f33ea41573 | ||
|
|
2355ffc5d0 |
No files matched your search
@@ -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())
|
||||||
|
```
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Optional plugin API additions (discussion #144)
|
||||||
|
|
||||||
|
Existing hooks, callbacks, return values, context lookup, strings and menu behaviour
|
||||||
|
remain unchanged. The C plugin function table gains four fixed append-only slots;
|
||||||
|
unused slots are NULL. Older binaries use the unchanged prefix. A plugin must check
|
||||||
|
the documented get_info() feature key before accessing a new slot on an older host.
|
||||||
|
New Python modules are installed by Meson and the existing Windows copy/installer
|
||||||
|
pipeline. Only modules present in the installation are exported; imports of
|
||||||
|
zoitechat, hexchat and xchat retain their compatibility aliases.
|
||||||
|
|
||||||
|
All APIs run on the client main thread unless explicitly documented otherwise.
|
||||||
|
New reserved flags must be zero. These changes do not make old API calls thread-safe.
|
||||||
|
|
||||||
|
Nested hook dispatch defers deleted-hook reclamation until its outermost return,
|
||||||
|
so new observers can safely emit events and unhook themselves or other hooks.
|
||||||
|
|
||||||
|
The optional asyncio module requires Python 3.7 or newer; older supported
|
||||||
|
interpreters skip that module and retain the existing scripting API.
|
||||||
|
|
||||||
|
Python reload drops the script/API modules while retaining the embedding module
|
||||||
|
so callback and lib.ph pointer types keep the same CFFI identity.
|
||||||
|
|
||||||
|
Server-hook insertion matches both raw and attrs hooks so documented priorities
|
||||||
|
are honoured for mixed registrations, including new filtered attrs callbacks.
|
||||||
@@ -383,3 +383,24 @@ def find_context(server=None, channel=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return Context(ctx)
|
return Context(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# Optional additive APIs live in separate modules so builds can select features.
|
||||||
|
def __load_additive_features():
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
for suffix in ('fd', 'events', 'async', 'names', 'contexts',
|
||||||
|
'post', 'filters', 'caps'):
|
||||||
|
if suffix == 'async' and sys.version_info < (3, 7):
|
||||||
|
continue
|
||||||
|
name = '_zoitechat_' + suffix
|
||||||
|
if not os.path.isfile(os.path.join(os.path.dirname(__file__), name + '.py')):
|
||||||
|
continue
|
||||||
|
module = importlib.import_module(name)
|
||||||
|
for symbol in module.__all__:
|
||||||
|
globals()[symbol] = getattr(module, symbol)
|
||||||
|
__all__.append(symbol)
|
||||||
|
|
||||||
|
|
||||||
|
__load_additive_features()
|
||||||
@@ -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)
|
||||||
@@ -27,7 +27,11 @@ python3_source = custom_target('python-bindings',
|
|||||||
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
|
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
|
||||||
)
|
)
|
||||||
|
|
||||||
install_data(['_zoitechat.py', 'zoitechat.py', 'xchat.py', 'hexchat.py'],
|
# Include any selected additive API modules alongside the compatibility modules.
|
||||||
|
python_api_files = run_command(python3, '-c',
|
||||||
|
'import glob,sys; print("\\n".join(sorted(glob.glob(sys.argv[1] + "/_zoitechat*.py"))))',
|
||||||
|
meson.current_source_dir(), check: true).stdout().strip().split('\n')
|
||||||
|
install_data(python_api_files + ['zoitechat.py', 'xchat.py', 'hexchat.py'],
|
||||||
install_dir: join_paths(get_option('libdir'), 'zoitechat/python')
|
install_dir: join_paths(get_option('libdir'), 'zoitechat/python')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -590,7 +590,12 @@ def _on_plugin_deinit():
|
|||||||
sys.stderr = sys.__stderr__
|
sys.stderr = sys.__stderr__
|
||||||
pydoc.help = pydoc.Helper()
|
pydoc.help = pydoc.Helper()
|
||||||
|
|
||||||
for mod in ('_zoitechat', 'zoitechat', 'xchat', '_zoitechat_embedded'):
|
# CFFI callbacks keep the embedding module's ffi/lib identity for the
|
||||||
|
# process lifetime; deleting it creates incompatible types on reload.
|
||||||
|
for mod in ('_zoitechat', 'zoitechat', 'hexchat', 'xchat',
|
||||||
|
'_zoitechat_fd', '_zoitechat_events', '_zoitechat_async',
|
||||||
|
'_zoitechat_names', '_zoitechat_contexts', '_zoitechat_post',
|
||||||
|
'_zoitechat_filters', '_zoitechat_caps'):
|
||||||
try:
|
try:
|
||||||
del sys.modules[mod]
|
del sys.modules[mod]
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<None Include="xchat.py" />
|
<None Include="xchat.py" />
|
||||||
<None Include="hexchat.py" />
|
<None Include="hexchat.py" />
|
||||||
<None Include="_zoitechat.py" />
|
<None Include="_zoitechat.py" />
|
||||||
|
<None Include="_zoitechat_*.py" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="$(IntDir)python.c" />
|
<ClCompile Include="$(IntDir)python.c" />
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<None Include="xchat.py" />
|
<None Include="xchat.py" />
|
||||||
<None Include="hexchat.py" />
|
<None Include="hexchat.py" />
|
||||||
<None Include="_zoitechat.py" />
|
<None Include="_zoitechat.py" />
|
||||||
|
<None Include="_zoitechat_*.py" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="$(IntDir)python.c" />
|
<ClCompile Include="$(IntDir)python.c" />
|
||||||
|
|||||||
+8
-2
@@ -176,6 +176,7 @@ enum
|
|||||||
|
|
||||||
GSList *plugin_list = NULL; /* export for plugingui.c */
|
GSList *plugin_list = NULL; /* export for plugingui.c */
|
||||||
static GSList *hook_list = NULL;
|
static GSList *hook_list = NULL;
|
||||||
|
static unsigned int plugin_dispatch_depth;
|
||||||
|
|
||||||
extern const struct prefs vars[]; /* cfgfiles.c */
|
extern const struct prefs vars[]; /* cfgfiles.c */
|
||||||
|
|
||||||
@@ -243,7 +244,7 @@ plugin_list_add (zoitechat_context *ctx, char *filename, const char *name,
|
|||||||
{
|
{
|
||||||
zoitechat_plugin *pl;
|
zoitechat_plugin *pl;
|
||||||
|
|
||||||
pl = g_new (zoitechat_plugin, 1);
|
pl = g_new0 (zoitechat_plugin, 1);
|
||||||
pl->handle = handle;
|
pl->handle = handle;
|
||||||
pl->filename = filename;
|
pl->filename = filename;
|
||||||
pl->context = ctx;
|
pl->context = ctx;
|
||||||
@@ -610,6 +611,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
|
|||||||
zoitechat_hook *hook;
|
zoitechat_hook *hook;
|
||||||
int ret, eat = 0;
|
int ret, eat = 0;
|
||||||
|
|
||||||
|
plugin_dispatch_depth++;
|
||||||
list = hook_list;
|
list = hook_list;
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
@@ -655,6 +657,10 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
|
|||||||
}
|
}
|
||||||
|
|
||||||
xit:
|
xit:
|
||||||
|
/* Nested callbacks can unhook entries still used by an outer dispatch.
|
||||||
|
* Keep both hook objects and list links alive until the outermost return. */
|
||||||
|
if (--plugin_dispatch_depth != 0)
|
||||||
|
return eat;
|
||||||
/* really remove deleted hooks now */
|
/* really remove deleted hooks now */
|
||||||
list = hook_list;
|
list = hook_list;
|
||||||
while (list)
|
while (list)
|
||||||
@@ -806,7 +812,7 @@ plugin_insert_hook (zoitechat_hook *new_hook)
|
|||||||
break;
|
break;
|
||||||
case HOOK_SERVER:
|
case HOOK_SERVER:
|
||||||
case HOOK_SERVER_ATTRS:
|
case HOOK_SERVER_ATTRS:
|
||||||
new_hook_type = HOOK_SERVER | HOOK_PRINT_ATTRS;
|
new_hook_type = HOOK_SERVER | HOOK_SERVER_ATTRS;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
new_hook_type = new_hook->type;
|
new_hook_type = new_hook->type;
|
||||||
|
|||||||
@@ -150,6 +150,15 @@ struct _zoitechat_plugin
|
|||||||
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
|
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
|
||||||
zoitechat_event_attrs *attrs);
|
zoitechat_event_attrs *attrs);
|
||||||
|
|
||||||
|
/* Additive API slots: append only; old binary offsets are unchanged. */
|
||||||
|
zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int,
|
||||||
|
int, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
|
||||||
|
zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int,
|
||||||
|
int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *);
|
||||||
|
zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int,
|
||||||
|
int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
|
||||||
|
zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *);
|
||||||
|
|
||||||
/* PRIVATE FIELDS! */
|
/* PRIVATE FIELDS! */
|
||||||
void *handle; /* from dlopen */
|
void *handle; /* from dlopen */
|
||||||
char *filename; /* loaded from */
|
char *filename; /* loaded from */
|
||||||
|
|||||||
@@ -193,6 +193,15 @@ struct _zoitechat_plugin
|
|||||||
zoitechat_event_attrs *(*zoitechat_event_attrs_create) (zoitechat_plugin *ph);
|
zoitechat_event_attrs *(*zoitechat_event_attrs_create) (zoitechat_plugin *ph);
|
||||||
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
|
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
|
||||||
zoitechat_event_attrs *attrs);
|
zoitechat_event_attrs *attrs);
|
||||||
|
/* Additive API slots: append only; old binary offsets are unchanged. */
|
||||||
|
zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int,
|
||||||
|
int, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
|
||||||
|
zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int,
|
||||||
|
int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *);
|
||||||
|
zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int,
|
||||||
|
int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
|
||||||
|
zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *);
|
||||||
|
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
|
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
|
||||||
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\index.theme" />
|
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\index.theme" />
|
||||||
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\**\*.*" />
|
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\**\*.*" />
|
||||||
|
<PythonAdditiveApis Include="..\..\plugins\python\_zoitechat_*.py" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" />
|
<Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" />
|
||||||
@@ -115,6 +116,7 @@
|
|||||||
<Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
<Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
||||||
<Copy SourceFiles="..\..\plugins\python\zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
<Copy SourceFiles="..\..\plugins\python\zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
||||||
<Copy SourceFiles="..\..\plugins\python\_zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
<Copy SourceFiles="..\..\plugins\python\_zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
||||||
|
<Copy SourceFiles="@(PythonAdditiveApis)" DestinationFolder="$(ZoiteChatRel)\python" Condition="('$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true') and '@(PythonAdditiveApis)' != ''" />
|
||||||
|
|
||||||
<WriteLinesToFile File="$(ZoiteChatRel)portable-mode" Lines="2" Overwrite="true" />
|
<WriteLinesToFile File="$(ZoiteChatRel)portable-mode" Lines="2" Overwrite="true" />
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user