Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ readme = "readme.md"
license = {file = "copying.txt"}
dependencies = [
# NVDA's runtime dependencies
"bleak==1.1.0",
"comtypes==1.4.11",
"cryptography==44.0.1",
"pyserial==3.5",
Expand Down Expand Up @@ -127,6 +128,14 @@ ignore_packages = [
"Markdown", # BSD-3-Clause, but not in PyPI correctly
"markdown-link-attr-modifier", # GPLV3 license, but not in PyPI correctly
"pycaw", # MIT license, but not in PyPI
"winrt-Windows.Devices.Bluetooth", # MIT license, but not in PyPI
"winrt-Windows.Devices.Bluetooth.Advertisement", # MIT license, but not in PyPI
"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile", # MIT license, but not in PyPI
"winrt-Windows.Devices.Enumeration", # MIT license, but not in PyPI
"winrt-Windows.Foundation", # MIT license, but not in PyPI
"winrt-Windows.Foundation.Collections", # MIT license, but not in PyPI
"winrt-Windows.Storage.Streams", # MIT license, but not in PyPI
"winrt-runtime", # MIT license, but not in PyPI
"wxPython", # wxWindows Library License
]

Expand Down
89 changes: 89 additions & 0 deletions source/asyncioEventLoop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2025 NV Access Limited, Dot Incorporated, Bram Duvigneau
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

"""
Provide an asyncio event loop
"""

import asyncio
from collections.abc import Coroutine
from threading import Thread

from logHandler import log

TERMINATE_TIMEOUT_SECONDS = 5
"Time to wait for tasks to finish while terminating the event loop."

eventLoop: asyncio.BaseEventLoop
"The asyncio event loop used by NVDA."
asyncioThread: Thread
"Thread running the asyncio event loop."


def initialize():
"""Initialize and start the asyncio event loop."""
global eventLoop, asyncioThread
log.info("Initializing asyncio event loop")
eventLoop = asyncio.new_event_loop()
asyncio.set_event_loop(eventLoop)
asyncioThread = Thread(target=eventLoop.run_forever, daemon=True)
asyncioThread.start()


def terminate():
global eventLoop, asyncioThread
log.info("Terminating asyncio event loop")

async def cancelAllTasks():
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
log.debug(f"Stopping {len(tasks)} tasks")
[task.cancel() for task in tasks]
await asyncio.gather(*tasks, return_exceptions=True)
log.debug("Done stopping tasks")

try:
runCoroutineSync(cancelAllTasks(), TERMINATE_TIMEOUT_SECONDS)
except TimeoutError:
log.debugWarning("Timeout while stopping async tasks")
finally:
eventLoop.call_soon_threadsafe(eventLoop.stop)

asyncioThread.join()
asyncioThread = None
eventLoop.close()


def runCoroutine(coro: Coroutine) -> asyncio.Future:
"""Schedule a coroutine to be run on the asyncio event loop.

:param coro: The coroutine to run.
"""
if asyncioThread is None or not asyncioThread.is_alive():
raise RuntimeError("Asyncio event loop thread is not running")
return asyncio.run_coroutine_threadsafe(coro, eventLoop)


def runCoroutineSync(coro: Coroutine, timeout: float | None = None):
"""Schedule a coroutine to be run on the asyncio event loop and wait for the result.

This is a synchronous wrapper around runCoroutine() that blocks until the coroutine
completes and returns the result directly, or raises any exception that occurred.

:param coro: The coroutine to run.
:param timeout: Optional timeout in seconds. If None, waits indefinitely.
:return: The result of the coroutine.
:raises: Any exception raised by the coroutine.
:raises TimeoutError: If the timeout is exceeded.
:raises RuntimeError: If the asyncio event loop thread is not running.
"""
future = runCoroutine(coro)
try:
# Wait for the future to complete and get the result
# This will raise any exception that occurred in the coroutine
return future.result(timeout)
except asyncio.TimeoutError as e:
# Cancel the coroutine since it timed out
future.cancel()
raise TimeoutError(f"Coroutine execution timed out after {timeout} seconds") from e
Loading