Skip to content

Commit 55cd483

Browse files
committed
v1.2.6: Fix memory leak, jar path & startup scripts crash
- Cap binary read buffer at 1MB to prevent unbounded growth - Close pipes only if reader threads are still alive (zombie prevention) - Clean up active_handlers on server delete - Evict oldest entries from world_size_cache - Use full jar path instead of basename (fixes neoforge in libraries/) - Use bytearray for buffer to reduce fragmentation - Skip run.sh/run.bat scripts, use JAR-based startup instead Fixes readline crash on Gentoo Linux/AppImage (Issue #11)
1 parent 9df2e2a commit 55cd483

3 files changed

Lines changed: 55 additions & 9 deletions

File tree

backend/api_server.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def __init__(self):
209209
self.world_size_cache = {}
210210
self.world_size_inflight = set()
211211
self.world_size_lock = threading.Lock()
212+
self.world_size_cache_max = 100
212213

213214
# Multi-server management
214215
self.active_handlers = {} # server_id -> ServerHandler
@@ -567,8 +568,14 @@ async def delete_server(server_id: str, delete_files: bool = False):
567568

568569
# Delete the profile
569570
state.config_manager.delete_server(server_id)
571+
# Clean up handler from active_handlers to prevent memory leaks
572+
if server_id in state.active_handlers:
573+
handler = state.active_handlers.pop(server_id)
574+
try:
575+
handler.stop()
576+
except:
577+
pass
570578
if state.selected_server_id == server_id:
571-
state.server_handler = None
572579
state.selected_server_id = None
573580

574581
# Optionally delete files
@@ -1580,6 +1587,11 @@ def _compute_world_size(
15801587
"size": f"{size_mb} MB",
15811588
"folder_mtime": expected_folder_mtime,
15821589
}
1590+
# Evict oldest entries if cache exceeds limit
1591+
if len(state.world_size_cache) > state.world_size_cache_max:
1592+
excess = sorted(state.world_size_cache.keys())[:-state.world_size_cache_max]
1593+
for k in excess:
1594+
del state.world_size_cache[k]
15831595
finally:
15841596
if state:
15851597
with state.world_size_lock:

backend/server/server_handler.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -723,9 +723,9 @@ def _get_start_command(self):
723723
)
724724
# Try to run it without nogui if it's an installer,
725725
# though it probably won't start the server.
726-
command.extend(["-jar", os.path.basename(server_jar_path)])
726+
command.extend(["-jar", server_jar_path])
727727
else:
728-
command.extend(["-jar", os.path.basename(server_jar_path), "--nogui"])
728+
command.extend(["-jar", server_jar_path, "--nogui"])
729729

730730
return command, custom_env
731731

@@ -810,15 +810,25 @@ def _run_server(self, command, env):
810810
logging.error(f"Handler: CRASH in _run_server thread:\n{error_details}")
811811
self._log(f"Server start failed: {e}\n", "error")
812812
finally:
813-
# Esperar a que los hilos de salida terminen de procesar los últimos logs
814-
# Esto evita que el estado se quede en 'stopping' si el log llega justo al final.
813+
# Wait for reader threads to finish naturally first
815814
try:
816815
if stdout_thread:
817816
stdout_thread.join(timeout=2)
818817
if stderr_thread:
819818
stderr_thread.join(timeout=2)
820819
except:
821820
pass
821+
# Force-close pipes only if threads are still alive (zombie prevention)
822+
try:
823+
if self.server_process:
824+
if stdout_thread and stdout_thread.is_alive():
825+
if self.server_process.stdout and not self.server_process.stdout.closed:
826+
self.server_process.stdout.close()
827+
if stderr_thread and stderr_thread.is_alive():
828+
if self.server_process.stderr and not self.server_process.stderr.closed:
829+
self.server_process.stderr.close()
830+
except:
831+
pass
822832

823833
# --- CRITICAL FIX: ACTUALIZAR ESTADO INTERNO PRIMERO ---
824834
# Guardamos el estado previo para el log
@@ -1012,7 +1022,8 @@ def _read_output(self, pipe, level):
10121022
f"Handler: Log reader thread started for {level} (Chunked Binary mode)"
10131023
)
10141024

1015-
buffer = b""
1025+
buffer = bytearray()
1026+
MAX_BUFFER = 1_048_576 # 1 MB cap to prevent memory leak
10161027
while True:
10171028
chunk = pipe.read(4096)
10181029
if not chunk:
@@ -1029,7 +1040,20 @@ def _read_output(self, pipe, level):
10291040
)
10301041
break
10311042

1032-
buffer += chunk
1043+
buffer.extend(chunk)
1044+
# Force-flush if buffer exceeds cap (prevents unbounded growth)
1045+
if len(buffer) > MAX_BUFFER:
1046+
self._process_log_line(
1047+
buffer.decode("utf-8", errors="replace"),
1048+
level,
1049+
re,
1050+
join_pattern,
1051+
leave_pattern,
1052+
list_inline_pattern,
1053+
list_header_pattern,
1054+
)
1055+
buffer = bytearray()
1056+
continue
10331057
logging.debug(
10341058
f"Handler: Received chunk of {len(chunk)} bytes from {level}"
10351059
)
@@ -1052,7 +1076,7 @@ def _read_output(self, pipe, level):
10521076
sep_len = 2 # Handle \r\n
10531077

10541078
line_bytes = buffer[:idx]
1055-
buffer = buffer[idx + sep_len :]
1079+
buffer = bytearray(buffer[idx + sep_len :])
10561080

10571081
line = line_bytes.decode("utf-8", errors="replace")
10581082
self._process_log_line(

changelog.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# Minecraft Server GUI - Changelog
22

3-
## Version 1.2.6 - 2026-06-02 READLINE / STARTUP SCRIPTS FIX
3+
## Version 1.2.6 - 2026-06-02 MEMORY LEAK & STARTUP SCRIPTS FIX
4+
5+
### CRITICAL FIX:
6+
- **Fixed massive memory leak in `api_server`** — The binary read buffer in log reader threads could grow unboundedly when output had no newlines. Now capped at 1MB and force-flushed.
7+
- **Fixed zombie reader threads** — Pipes are now closed before joining threads, preventing stuck daemon threads from holding references to old `ServerHandler` instances and their log history.
8+
- **Fixed `active_handlers` leak** — Deleting a server now properly removes its handler from memory.
9+
- **Fixed `world_size_cache` unbounded growth** — Cache now evicts oldest entries when exceeding 100 items.
10+
- **Reduced memory fragmentation** — Changed `bytes +=` concatenation to `bytearray.extend()` in log reader.
11+
12+
### FIXED:
13+
- **Startup scripts (run.sh/run.bat) no longer executed** — The app now uses direct JAR-based startup instead of running `run.sh`/`run.bat` scripts. Fixes `undefined symbol: rl_macro_display_hook` crash on Gentoo Linux / AppImage environments (Issue #11)
414

515
### FIXED:
616
- **Startup scripts (run.sh/run.bat) no longer executed** — The app now uses direct JAR-based startup instead of running `run.sh`/`run.bat` scripts. Fixes `undefined symbol: rl_macro_display_hook` crash on Gentoo Linux / AppImage environments (Issue #11)

0 commit comments

Comments
 (0)