From b0cfe049aef564ce4485e7beea91d96befd78a67 Mon Sep 17 00:00:00 2001 From: "kyssta-exe 25470058+kyssta-exe@users.noreply.github.com" Date: Thu, 9 Jul 2026 14:36:42 +0000 Subject: [PATCH 1/2] fix(cron): pass gateway loop to run_one_job so manual Matrix cron delivery uses live-adapter path (#61495) Manual cron runs triggered from a Matrix gateway session (cronjob action='run') failed during delivery with: Timeout context manager should be used inside a task Root cause: _execute_job_now called run_one_job(job) without adapters or loop, so _deliver_result fell back to the standalone asyncio.run() path. That path created a new event loop, but _send_matrix_via_adapter re-acquired the live Matrix adapter (whose internal aiohttp session was bound to the gateway's original loop) from the new loop, causing the cross-loop RuntimeError. Fix: 1. _execute_job_now now resolves the gateway runner's adapters and _gateway_loop and passes them to run_one_job, so the delivery uses the live-adapter path (safe_schedule_threadsafe on the correct gateway loop). 2. _send_matrix_via_adapter adds a defense-in-depth RuntimeError catch around the live-adapter branch, falling back to the ephemeral adapter when the live adapter is unusable from the current event loop. --- tools/cronjob_tools.py | 26 +++++++++++++++++++++++++- tools/send_message_tool.py | 21 ++++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/tools/cronjob_tools.py b/tools/cronjob_tools.py index 430cd1dda4014..ece887038ff1a 100644 --- a/tools/cronjob_tools.py +++ b/tools/cronjob_tools.py @@ -615,6 +615,15 @@ def _execute_job_now(job: Dict[str, Any]) -> Dict[str, Any]: failure delivery, ``[SILENT]`` handling, and live-adapter delivery stay identical across paths and can't drift. + When a live gateway is running, this function retrieves the gateway's + adapters and event loop and passes them to ``run_one_job`` so that job + delivery goes through the live-adapter path (``safe_schedule_threadsafe`` + on the gateway loop) rather than creating a new event loop via + ``asyncio.run()``. The standalone path would send adapter HTTP calls + from a different loop than the adapter's internal aiohttp session was + created on, causing "Timeout context manager should be used inside a task" + failures for platforms such as Matrix (#61495). + Returns {"claimed": bool, "success": bool, "error": str|None}. """ job_id = job["id"] @@ -636,9 +645,24 @@ def _execute_job_now(job: Dict[str, Any]) -> Dict[str, Any]: reason = "Job is already being fired by the scheduler; not run again." return {"claimed": False, "success": False, "error": reason} + # Resolve the gateway's adapters and event loop so delivery uses the + # live-adapter path (safe_schedule_threadsafe on the gateway loop). + # Without these, _deliver_result falls back to asyncio.run() in a new + # event loop, causing cross-loop adapter usage failures (#61495). + _adapters = None + _loop = None + try: + from gateway.run import _gateway_runner_ref + _runner = _gateway_runner_ref() + if _runner is not None: + _adapters = getattr(_runner, "adapters", None) + _loop = getattr(_runner, "_gateway_loop", None) + except Exception: + pass + # run_one_job records last_run_at/last_status via mark_job_run (which # also clears the fire claim) and returns True iff it processed the job. - processed = run_one_job(job) + processed = run_one_job(job, adapters=_adapters, loop=_loop) refreshed = get_job(job_id) or {} ok = refreshed.get("last_status") == "ok" return { diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index c143ea8064f16..1c6cdd81d656d 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -1695,9 +1695,24 @@ async def _send_matrix_via_adapter(pconfig, chat_id, message, media_files=None, # disconnect it. Correctness here depends on this branch returning # before the ephemeral ``adapter`` is constructed below, so the # ephemeral ``finally`` disconnect never touches the live session. - return await _matrix_send_core( - live_adapter, chat_id, message, media_files, metadata - ) + try: + return await _matrix_send_core( + live_adapter, chat_id, message, media_files, metadata + ) + except RuntimeError as _matrix_loop_err: + # When the cron standalone path (asyncio.run() in a new event + # loop) uses the live adapter, the adapter's internal aiohttp + # session was created on the gateway's loop and cannot be used + # from a different loop. Fall through to the ephemeral adapter + # rather than failing delivery (#61495). The primary fix is in + # _execute_job_now (which passes the gateway loop so delivery + # uses safe_schedule_threadsafe), but this cross-loop guard + # catches any remaining path. + logger.warning( + "Matrix: live adapter unusable from this event loop " + "(%s: %s); falling back to ephemeral adapter", + type(_matrix_loop_err).__name__, _matrix_loop_err, + ) # --- Fallback: ephemeral adapter (standalone / cron context) --- try: From d00702895321c68bed268a2b3b75f70323bf8e06 Mon Sep 17 00:00:00 2001 From: "kyssta-exe 25470058+kyssta-exe@users.noreply.github.com" Date: Thu, 9 Jul 2026 14:39:41 +0000 Subject: [PATCH 2/2] fix(desktop): discover UGit's git.exe in resolveGitBinary via directory scan (#61494) UGit installs a portable Git under a versioned app-* subdirectory of %LOCALAPPDATA%\UGit. The standard resolveGitBinary() candidate list did not include this path, so check-for-updates and other git operations failed silently on systems that use UGit instead of Git-for-Windows. Fix: enumerate %LOCALAPPDATA%\UGit\app-*\resources\app\git\cmd\git.exe dynamically rather than hard-coding a single version, so the discovery survives UGit updates that change the app-* suffix. --- apps/desktop/electron/main.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index b8c2d03da3806..bdb88f0611137 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -1926,6 +1926,23 @@ function resolveGitBinary() { if (localAppData) { candidates.push(path.join(localAppData, 'Programs', 'Git', 'cmd', 'git.exe')) + + // UGit ships a portable Git under %LOCALAPPDATA%\UGit\app-*\resources\app\git\cmd\git.exe. + // The versioned app-* subdirectory changes with each UGit update, so enumerate + // all app-* directories dynamically instead of hard-coding a version (#61494). + const ugitDir = path.join(localAppData, 'UGit') + try { + const entries = fs.readdirSync(ugitDir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.isDirectory() && entry.name.startsWith('app-')) { + candidates.push( + path.join(ugitDir, entry.name, 'resources', 'app', 'git', 'cmd', 'git.exe') + ) + } + } + } catch { + // UGit not installed or inaccessible — skip gracefully + } } _gitBinaryCache = candidates.find(fileExists) || findOnPath('git') || 'git'