184 lines
6.0 KiB
Python
184 lines
6.0 KiB
Python
# Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
|
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
# WARNING: make sure to not top-level import the media_cache module here,
|
|
# directly or indirectly via another module import (e.g. backend).
|
|
# See https://stackoverflow.com/a/55918049
|
|
|
|
"""Provide `BRIDGE`, main object accessed by QML to interact with Python.
|
|
|
|
PyOtherSide, the library that handles interaction between our Python backend
|
|
and QML UI, will access the `BRIDGE` object and call its methods directly.
|
|
|
|
The `BRIDGE` object should be the only existing instance of the `QMLBridge`
|
|
class.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging as log
|
|
import os
|
|
import traceback
|
|
from concurrent.futures import Future
|
|
from operator import attrgetter
|
|
from threading import Thread
|
|
from typing import Coroutine, Dict, Sequence, Set
|
|
|
|
import pyotherside
|
|
|
|
from .pyotherside_events import CoroutineDone, LoopException
|
|
|
|
|
|
class QMLBridge:
|
|
"""Setup asyncio and provide methods to call coroutines from QML.
|
|
|
|
A thread is created to run the asyncio loop in, to ensure all calls from
|
|
QML return instantly.
|
|
Synchronous methods are provided for QML to call coroutines using
|
|
PyOtherSide, which doesn't have this ability out of the box.
|
|
|
|
Attributes:
|
|
backend: The `backend.Backend` object containing general coroutines
|
|
for QML and that manages `MatrixClient` objects.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
try:
|
|
self._loop = asyncio.get_event_loop()
|
|
except RuntimeError:
|
|
self._loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(self._loop)
|
|
self._loop.set_exception_handler(self._loop_exception_handler)
|
|
|
|
from .backend import Backend
|
|
self.backend: Backend = Backend()
|
|
|
|
self._running_futures: Dict[str, Future] = {}
|
|
self._cancelled_early: Set[str] = set()
|
|
|
|
Thread(target=self._start_asyncio_loop).start()
|
|
|
|
|
|
def _loop_exception_handler(
|
|
self, loop: asyncio.AbstractEventLoop, context: dict,
|
|
) -> None:
|
|
if "exception" in context:
|
|
err = context["exception"]
|
|
trace = "".join(
|
|
traceback.format_exception(type(err), err, err.__traceback__),
|
|
)
|
|
LoopException(context["message"], err, trace)
|
|
|
|
loop.default_exception_handler(context)
|
|
|
|
|
|
def _start_asyncio_loop(self) -> None:
|
|
asyncio.set_event_loop(self._loop)
|
|
self._loop.run_forever()
|
|
|
|
|
|
def _call_coro(self, coro: Coroutine, uuid: str) -> None:
|
|
"""Schedule a coroutine to run in our thread and return a `Future`."""
|
|
|
|
if uuid in self._cancelled_early:
|
|
self._cancelled_early.remove(uuid)
|
|
return
|
|
|
|
def on_done(future: Future) -> None:
|
|
"""Send a PyOtherSide event with the coro's result/exception."""
|
|
result = exception = trace = None
|
|
|
|
try:
|
|
result = future.result()
|
|
except Exception as err: # noqa
|
|
exception = err
|
|
trace = traceback.format_exc().rstrip()
|
|
|
|
CoroutineDone(uuid, result, exception, trace)
|
|
del self._running_futures[uuid]
|
|
|
|
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
self._running_futures[uuid] = future
|
|
future.add_done_callback(on_done)
|
|
|
|
|
|
def call_backend_coro(
|
|
self, name: str, uuid: str, args: Sequence[str] = (),
|
|
) -> None:
|
|
"""Schedule a coroutine from the `QMLBridge.backend` object."""
|
|
|
|
if uuid in self._cancelled_early:
|
|
self._cancelled_early.remove(uuid)
|
|
else:
|
|
self._call_coro(attrgetter(name)(self.backend)(*args), uuid)
|
|
|
|
|
|
def call_client_coro(
|
|
self, user_id: str, name: str, uuid: str, args: Sequence[str] = (),
|
|
) -> None:
|
|
"""Schedule a coroutine from a `QMLBridge.backend.clients` client."""
|
|
|
|
if uuid in self._cancelled_early:
|
|
self._cancelled_early.remove(uuid)
|
|
else:
|
|
client = self.backend.clients[user_id]
|
|
self._call_coro(attrgetter(name)(client)(*args), uuid)
|
|
|
|
|
|
def cancel_coro(self, uuid: str) -> None:
|
|
"""Cancel a couroutine scheduled by the `QMLBridge` methods."""
|
|
|
|
if uuid in self._running_futures:
|
|
self._running_futures[uuid].cancel()
|
|
else:
|
|
self._cancelled_early.add(uuid)
|
|
|
|
|
|
def pdb(self, extra_data: Sequence = (), remote: bool = False) -> None:
|
|
"""Call the python debugger, defining some conveniance variables."""
|
|
|
|
ad = extra_data # noqa
|
|
ba = self.backend # noqa
|
|
mo = self.backend.models # noqa
|
|
cl = self.backend.clients
|
|
gcl = lambda user: cl[f"@{user}"] # noqa
|
|
|
|
rc = lambda c: asyncio.run_coroutine_threadsafe(c, self._loop) # noqa
|
|
|
|
try:
|
|
from devtools import debug # noqa
|
|
d = debug # noqa
|
|
except ModuleNotFoundError:
|
|
log.warning("Module python-devtools not found, can't use debug()")
|
|
|
|
if remote:
|
|
# Run `socat readline tcp:127.0.0.1:4444` in a terminal to connect
|
|
import remote_pdb
|
|
remote_pdb.RemotePdb("127.0.0.1", 4444).set_trace()
|
|
else:
|
|
import pdb
|
|
pdb.set_trace()
|
|
|
|
|
|
def exit(self) -> None:
|
|
try:
|
|
asyncio.run_coroutine_threadsafe(
|
|
self.backend.terminate_clients(), self._loop,
|
|
).result()
|
|
except Exception as e: # noqa
|
|
print(e)
|
|
|
|
|
|
# The AppImage AppRun script overwrites some environment path variables to
|
|
# correctly work, and sets RESTORE_<name> equivalents with the original values.
|
|
# If the app is launched from an AppImage, now restore the original values
|
|
# to prevent problems like QML Qt.openUrlExternally() failing because
|
|
# the external launched program is affected by our AppImage-specific variables.
|
|
for var in ("LD_LIBRARY_PATH", "PYTHONHOME", "PYTHONUSERBASE"):
|
|
if f"RESTORE_{var}" in os.environ:
|
|
os.environ[var] = os.environ[f"RESTORE_{var}"]
|
|
|
|
|
|
BRIDGE = QMLBridge()
|
|
|
|
pyotherside.atexit(BRIDGE.exit)
|