mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-11-08 17:05:38 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
420adde173 |
No files matched your search
@@ -1,30 +0,0 @@
|
|||||||
# 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 @@
|
|||||||
|
# Connection and context navigation
|
||||||
|
|
||||||
|
`get_connections()` returns one Connection snapshot per live connection:
|
||||||
|
`id`, `network`, `server`, `context`. Connection IDs distinguish simultaneous
|
||||||
|
connections with identical names. Prefer its server-tab context where present.
|
||||||
|
`get_connection(context=None)` finds the owner of a live context.
|
||||||
|
|
||||||
|
`get_contexts(connection_id=None, types=None)` lists live contexts, optionally
|
||||||
|
filtered by connection ID (or a Connection object) and session types: server=1,
|
||||||
|
channel=2, query=3. `connection.contexts(types=None)` is a convenience method.
|
||||||
|
`find_context_by_id(connection_id, channel=None)` / `connection.find(channel)`
|
||||||
|
resolve using that connection's IRC comparator; no channel selects the server tab.
|
||||||
|
Missing/closed contexts return None. Existing find_context() remains unchanged.
|
||||||
|
|
||||||
|
These helpers use the existing channels list's ID/context fields, so no new C
|
||||||
|
context ABI is needed. Snapshots are not persistent IDs across client restarts;
|
||||||
|
refresh them after closing/recreating connections. Always check Context.set()
|
||||||
|
when explicitly selecting a context that might have closed.
|
||||||
|
|
||||||
|
```python
|
||||||
|
connection = zoitechat.get_connection()
|
||||||
|
for context in connection.contexts(types=(2,)):
|
||||||
|
context.prnt('A channel on this exact connection')
|
||||||
|
```
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Navigate live connections by ID instead of ambiguous server/network names."""
|
||||||
|
from collections import namedtuple
|
||||||
|
import operator
|
||||||
|
import _zoitechat as api
|
||||||
|
|
||||||
|
__all__ = ['Connection', 'get_connections', 'get_connection', 'get_contexts',
|
||||||
|
'find_context_by_id']
|
||||||
|
|
||||||
|
|
||||||
|
class Connection(namedtuple('_Connection', 'id network server context')):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def contexts(self, types=None):
|
||||||
|
return get_contexts(self.id, types)
|
||||||
|
|
||||||
|
def find(self, channel=None):
|
||||||
|
return find_context_by_id(self.id, channel)
|
||||||
|
|
||||||
|
|
||||||
|
def _id(connection):
|
||||||
|
return operator.index(connection.id if isinstance(connection, Connection) else connection)
|
||||||
|
|
||||||
|
|
||||||
|
def get_connections():
|
||||||
|
"""Return one snapshot per live connection, including disconnected ones."""
|
||||||
|
entries = api.get_list('channels')
|
||||||
|
by_id = {}
|
||||||
|
order = []
|
||||||
|
for entry in entries:
|
||||||
|
if entry.id not in by_id:
|
||||||
|
order.append(entry.id)
|
||||||
|
if entry.id not in by_id or entry.type == 1:
|
||||||
|
by_id[entry.id] = Connection(entry.id, entry.network, entry.server,
|
||||||
|
entry.context)
|
||||||
|
return [by_id[connection_id] for connection_id in order]
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection(context=None):
|
||||||
|
"""Find the owning connection of a Context; return None when it has closed."""
|
||||||
|
context = context or api.get_context()
|
||||||
|
for entry in api.get_list('channels'):
|
||||||
|
if entry.context == context:
|
||||||
|
for connection in get_connections():
|
||||||
|
if connection.id == entry.id:
|
||||||
|
return connection
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_contexts(connection_id=None, types=None):
|
||||||
|
"""Return contexts, optionally restricted to a connection and session types."""
|
||||||
|
if connection_id is not None:
|
||||||
|
connection_id = _id(connection_id)
|
||||||
|
if types is not None:
|
||||||
|
types = frozenset(types)
|
||||||
|
return [entry.context for entry in api.get_list('channels')
|
||||||
|
if (connection_id is None or entry.id == connection_id)
|
||||||
|
and (types is None or entry.type in types)]
|
||||||
|
|
||||||
|
|
||||||
|
def find_context_by_id(connection_id, channel=None):
|
||||||
|
"""Resolve a live context on exactly one connection; None selects its server tab."""
|
||||||
|
connection_id = _id(connection_id)
|
||||||
|
previous = api.get_context()
|
||||||
|
try:
|
||||||
|
for entry in api.get_list('channels'):
|
||||||
|
if entry.id != connection_id:
|
||||||
|
continue
|
||||||
|
if channel is None:
|
||||||
|
if entry.type == 1:
|
||||||
|
return entry.context
|
||||||
|
elif entry.context.set() and api.nickcmp(entry.channel, channel) == 0:
|
||||||
|
return entry.context
|
||||||
|
finally:
|
||||||
|
previous.set()
|
||||||
|
return None
|
||||||
Reference in new issue
Block a user