
PyRunner
Run your Python Discord bot directly alongside your Minecraft server! Features auto-setup, live web dashboard, and in-game controls
Список изменений
🆕 What's new in 1.2.0
Dashboard — Settings & Bot Management
The web dashboard has been completely overhauled. It is no longer just a log viewer — it now gives you full control over the plugin without ever touching config.yml or SSHing into the server.
New Settings tab — every config value is editable live from the browser:
- Process behaviour: auto-restart toggle, restart delay, max restarts, crash window, startup delay
- Python runtime: version pin and custom executable path
- Discord webhook: URL, enable/disable, and individual toggles for each event type (start, stop, crash, log lines)
- Dashboard: log history line count and password (changing the password immediately invalidates all active sessions)
New Bots tab — full bot instance management with TunnelMC-style modal dialogs:
- Add a new bot (name, entry file, env vars) — starts automatically
- Edit an existing bot — stops it, applies the new config, and restarts
- Delete a bot — stops it and removes it from
config.yml - All changes persist to disk immediately
Download logs — a new button on the sidebar downloads the current bot.log as a file attachment.
SSE push-based streaming — log lines are now pushed directly to the browser the moment they are written, rather than polled. This eliminates latency and CPU overhead.
Log counter fix — the line counter now reflects what is actually visible in the DOM (capped at 3000) rather than a stale incrementing integer.
switchBot() race fixed — historical logs are fully loaded before the SSE stream connects, preventing out-of-order or duplicate lines when switching bots.
Bug Fixes
-
failedAttemptsmap unbounded growth — swept hourly alongside the session map. -
JSON injection in status/bots API — bot names and entry file paths serialised with Gson instead of string concatenation.
-
setsidpath now covers Alpine Linux / Pterodactyl — checks/usr/bin/setsid,/bin/setsid, and/usr/local/bin/setsid. Previously process group killing silently did nothing on Alpine-based containers. -
FileReaderplatform charset —/proc/<pid>/statusread with explicit UTF-8. -
FileUtils.deleteDir()returnsboolean— failures logged per-file instead of silently swallowed. -
GET endpoints reject non-GET requests — 405 returned for wrong-method calls.
-
Log timestamps include the date —
MM-dd HH:mm:ssformat so logs spanning midnight are unambiguous. -
Python downloader fetches 20 releases — up from 5, so rare platform/version combos are found correctly.
-
Content-Security-Policy header added to all dashboard responses.
-
Gradle performance flags —
parallel,caching, andG1GCJVM args added togradle.properties. -
Fixed state race condition —
BotStateis now managed viaAtomicReferenceinstead of a plainvolatilefield, eliminating a class of bugs where background threads could write the state without memory visibility guarantees. -
Fixed
forceReinstall()not actually reinstalling — The command now passes--force-reinstallto pip, guaranteeing packages are freshly installed. Previously it only deleted the flag file and ran a normalpip install, which would skip already-installed packages. -
Fixed
restart()busy-wait — The polling loop has been replaced with aCountDownLatch, so restarts are more reliable and use zero CPU while waiting for the process to fully stop. -
Fixed GitHub API rate limiting causing confusing errors —
findReleaseAsset()now checks the HTTP status code before attempting to parse JSON. A 403 or 429 from GitHub now produces a clear "rate limit reached" message instead of a crypticJsonParseException. -
Fixed config values not validated — All numeric config getters (
restart-delay-seconds,max-restarts,crash-window-seconds,startup-delay-seconds) now clamp to valid ranges, preventingThread.sleep(-N)exceptions from negative values. -
Fixed log-history cap not enforced —
getDashboardLogHistory()is now capped at 500 (matchingLogManager's ring buffer size), so requesting more lines than the buffer holds no longer silently returns a truncated result without warning. -
Fixed static
instancenotvolatile— The singleton is now declaredvolatilefor safe cross-thread visibility, and is nulled out inonDisable()to prevent classloader leaks on plugin reload. -
Fixed Discord embed JSON escaping — Webhook embeds now use Gson's
toJson()for string serialisation instead of a manualreplace()chain, correctly escaping all Unicode control characters (\u0000–\u001F) that could cause Discord to reject payloads. -
Fixed malformed bot entries crashing plugin load — Each entry in the
bots:YAML list is now parsed inside a try-catch; a bad entry logs a warning and is skipped rather than throwing aClassCastExceptionthat aborts the whole enable sequence. -
Added default password warning — A prominent console warning is now printed at startup if the dashboard is enabled and the password is still set to
"changeme". -
Improved
getBotDir()fallback — Whenentry-filehas no directory component (e.g.main.pyinstead ofbot/main.py), the plugin now derives the directory from the actual file path and logs a descriptive warning, rather than silently assuming"bot". -
Fixed dashboard SSE exhausting the HTTP thread pool — The server now uses Java 21 virtual threads (
newVirtualThreadPerTaskExecutor) instead of a fixed 8-thread pool. Previously, 8+ simultaneous SSE log-stream connections could fill the pool and cause all other API requests to hang indefinitely. -
Fixed
DashboardServer.jsonString()unsafe escaping — Dashboard log API responses now useGSON.toJson()instead of a manualreplace()chain, correctly handling Unicode control characters (\u0000–\u001F) that could previously produce malformed JSON. -
Fixed memory cache non-atomic reads — Three separate
volatilefields replaced with a singlevolatile MemCacherecord swapped atomically, preventing threads from observing a new timestamp with stale memory values. -
Fixed SSE handler busy-polling —
while (!heartbeatFuture.isDone()) Thread.sleep(1000)replaced withheartbeatFuture.get(), eliminating unnecessary 1-second wakeups while waiting for client disconnection. -
Fixed session map growing without bound — A scheduled hourly sweep now removes expired sessions. Previously, repeated logins accumulated stale entries indefinitely.
-
Fixed dashboard password using string equality — Login now uses
MessageDigest.isEqual()for constant-time comparison, preventing timing attacks. -
Fixed duplicate
deleteDir()logic — A new sharedutil/FileUtils.deleteDir()(NIO-based) replaces the identical recursive-delete duplicated acrossPyBotCommandandDashboardServer. -
Fixed
plugin.ymlapi-versionoutdated — Updated from'1.13'to'1.21'to match the actual target platform and suppress Paper deprecation warnings on load. -
Fixed Gson used as implicit transitive dependency — Gson is now declared explicitly as
compileOnlyinbuild.gradle.kts. -
Fixed legacy
§color codes in commands — All in-game messages inPyBotCommandnow use the Paper Adventure API (MiniMessage), eliminating deprecation warnings and ensuring correct rendering in modern clients.
Config changes
No new config keys. Existing configs remain fully compatible.
