fix(matrixbot): replace mcrcon with a thread-safe RCON client

mcrcon implements its read timeout with signal.SIGALRM, which only works
on the main thread. The Minecraft commands call it from a worker thread
via loop.run_in_executor, so it raised "signal only works in main thread
of the main interpreter" on every invocation. The replacement uses
socket.settimeout(), which has no such restriction.

This code was already deployed on LXC 151 but had never been committed,
so any matrixbot deploy would have reverted it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-16 01:37:38 -04:00
co-authored by Claude Opus 5
parent 5d420e318c
commit 7c05827dbc
3 changed files with 73 additions and 16 deletions
+26 -15
View File
@@ -13,7 +13,7 @@ import aiohttp
from nio import AsyncClient
from utils import send_text, send_html, send_reaction, edit_html, sanitize_input
from utils import send_text, send_html, send_reaction, edit_html, sanitize_input, rcon_command, RconError
from wordle import handle_wordle, wordle_stats as _wordle_stats
from welcome import clean_stale_dm_messages
from config import (
@@ -1317,29 +1317,40 @@ async def cmd_minecraft(client: AsyncClient, room_id: str, sender: str, args: st
await send_text(client, room_id, f"Whitelisting {username}...")
try:
from mcrcon import MCRcon
def _rcon():
with MCRcon(MINECRAFT_RCON_HOST, MINECRAFT_RCON_PASSWORD, port=MINECRAFT_RCON_PORT, timeout=3) as mcr:
return mcr.command(f"whitelist add {username}")
return rcon_command(
MINECRAFT_RCON_HOST,
MINECRAFT_RCON_PASSWORD,
f"whitelist add {username}",
port=MINECRAFT_RCON_PORT,
timeout=RCON_TIMEOUT,
)
loop = asyncio.get_running_loop()
response = await asyncio.wait_for(loop.run_in_executor(None, _rcon), timeout=RCON_TIMEOUT)
logger.info(f"RCON response: {response}")
plain = f"Minecraft\nYou have been whitelisted on the SMP!\nServer: minecraft.lotusguild.org\nUsername: {username}"
already = "already whitelisted" in response.lower()
status_line = (
f"↺ <strong>{username}</strong> was already whitelisted."
if already
else f"✅ <strong>{username}</strong> is whitelisted and ready to join."
)
plain = (
f"✿ Minecraft — Lotus SMP ✿\n"
f"{'Already whitelisted' if already else 'Whitelisted'}: {username}\n"
f"Server: minecraft.lotusguild.org"
)
html = (
f"<strong>Minecraft</strong><br>"
f"You have been whitelisted on the SMP!<br>"
f"Server: <strong>minecraft.lotusguild.org</strong><br>"
f"Username: <strong>{username}</strong>"
f'<font color="#980000"><strong>Minecraft — Lotus SMP ✿</strong></font><br>'
f"<blockquote>"
f"{status_line}<br>"
f'<strong>Server:</strong> <code>minecraft.lotusguild.org</code><br>'
f"<sup><em>fresh world · hard mode · no takebacks</em></sup>"
f"</blockquote>"
)
await send_html(client, room_id, plain, html)
except ImportError:
await send_text(client, room_id, "mcrcon is not installed. Ask an admin to install it.")
except asyncio.TimeoutError:
await send_text(client, room_id, "Minecraft server timed out. It may be offline.")
except Exception as e:
except (asyncio.TimeoutError, RconError, OSError) as e:
logger.error(f"RCON error: {e}", exc_info=True)
await send_text(client, room_id, "Failed to whitelist. The server may be offline (let jared know).")