@@ -581,71 +581,113 @@ def _get_start_command(self):
581581 custom_env ["PATH" ] = f"{ java_bin_dir } { path_sep } { custom_env .get ('PATH' , '' )} "
582582 logging .info (f"Handler: Injected Environment - JAVA_HOME={ java_root } " )
583583
584- # JAR-based startup (skipping run.sh/run.bat scripts to avoid
585- # readline/library conflicts on Linux/AppImage environments)
584+ # Try to parse run.sh/run.bat to extract the java command
585+ # (avoids executing sh/bat directly, which can cause readline/library crashes)
586+ run_script = None
587+ script_names = (
588+ ["run.bat" , "start.bat" , "launch.bat" ]
589+ if sys .platform == "win32"
590+ else ["run.sh" , "start.sh" , "launch.sh" ]
591+ )
592+ for name in script_names :
593+ p = os .path .join (self .server_path , name )
594+ if os .path .exists (p ):
595+ run_script = p
596+ break
597+
598+ if run_script :
599+ parsed = self ._parse_startup_script (run_script )
600+ if parsed :
601+ if "nogui" not in parsed :
602+ parsed .append ("nogui" )
603+ command = [java_path ] + parsed
604+ self .output_callback (
605+ "Parsed startup script: using direct Java launch.\n " , "info"
606+ )
607+ return command , custom_env
608+
609+ # Fallback: JAR-based startup
586610 self .output_callback (
587- "Starting server via JAR-based method (skipping startup scripts) .\n " , "info"
611+ "Starting server via JAR-based method.\n " , "info"
588612 )
589613 all_jars = glob .glob (os .path .join (self .server_path , "*.jar" ))
590614 server_jar_path = None
591615
592- non_installer_jars = [
593- j for j in all_jars if "installer" not in os .path .basename (j ).lower ()
594- ]
595-
596- if non_installer_jars :
597- # Look for common server jar names first
598- preferred_names = [
599- "server.jar" ,
600- "minecraft_server.jar" ,
601- "paper.jar" ,
602- "spigot.jar" ,
603- "fabric-server-launch.jar" ,
616+ # Check for forge/neoforge server JAR in libraries/ first
617+ lib_fallback = None
618+ if self .server_type in ["forge" , "neoforge" ]:
619+ lib_path = os .path .join (self .server_path , "libraries" )
620+ if os .path .exists (lib_path ):
621+ candidates = []
622+ forge_path_marker = "neoforged/neoforge" if self .server_type == "neoforge" else "minecraftforge/forge"
623+ for root , dirs , files in os .walk (lib_path ):
624+ for file in files :
625+ if file .endswith (".jar" ) and "installer" not in file .lower ():
626+ full_path = os .path .join (root , file )
627+ score = 0
628+ if forge_path_marker in root .replace ("\\ " , "/" ):
629+ score = 10
630+ elif self .server_type in file .lower ():
631+ score = 5
632+ if "server" in file .lower ():
633+ score += 2
634+ candidates .append ((score , full_path ))
635+ if candidates :
636+ candidates .sort (key = lambda x : x [0 ], reverse = True )
637+ best = candidates [0 ]
638+ lib_fallback = best [1 ]
639+ # Only use library jar if it has "server" in the name
640+ if "server" in os .path .basename (best [1 ]).lower ():
641+ server_jar_path = best [1 ]
642+
643+ # Fallback to root-level jars
644+ if not server_jar_path :
645+ non_installer_jars = [
646+ j for j in all_jars if "installer" not in os .path .basename (j ).lower ()
604647 ]
605- for name in preferred_names :
606- for jar in non_installer_jars :
607- if os .path .basename (jar ).lower () == name :
608- server_jar_path = jar
648+ if non_installer_jars :
649+ preferred_names = [
650+ "server.jar" ,
651+ "minecraft_server.jar" ,
652+ "paper.jar" ,
653+ "spigot.jar" ,
654+ "fabric-server-launch.jar" ,
655+ ]
656+ for name in preferred_names :
657+ for jar in non_installer_jars :
658+ if os .path .basename (jar ).lower () == name :
659+ server_jar_path = jar
660+ break
661+ if server_jar_path :
609662 break
610- if server_jar_path :
611- break
612-
613- # If no preferred name is found, take the first non-installer jar
614- if not server_jar_path :
615- server_jar_path = non_installer_jars [0 ]
616-
617- elif all_jars : # Fallback if only installer jars are present for some reason
618- server_jar_path = all_jars [0 ]
619-
620- if not server_jar_path :
621- # Special check for Forge/NeoForge libraries if run.bat is missing
622- if self .server_type in ["forge" , "neoforge" ]:
623- self .output_callback ("Startup script missing. Checking libraries for server JAR...\n " , "info" )
624- lib_path = os .path .join (self .server_path , "libraries" )
625- if os .path .exists (lib_path ):
626- # Search for any jar that looks like a forge/neoforge server jar
627- for root , dirs , files in os .walk (lib_path ):
628- for file in files :
629- if file .endswith (".jar" ) and ("forge" in file .lower () or "neoforge" in file .lower ()) and "server" in file .lower ():
630- server_jar_path = os .path .join (root , file )
631- break
632- if server_jar_path : break
633-
634- if server_jar_path :
635- self .output_callback (f"Found server JAR in libraries: { os .path .basename (server_jar_path )} \n " , "success" )
636- else :
637- self .output_callback ("Could not find server JAR in libraries. Installation might be incomplete.\n " , "warning" )
663+ if not server_jar_path :
664+ server_jar_path = non_installer_jars [0 ]
665+ elif all_jars :
666+ server_jar_path = all_jars [0 ]
667+
668+ # If nothing found in root, fall back to best library candidate
669+ if not server_jar_path and lib_fallback :
670+ server_jar_path = lib_fallback
671+ self .output_callback (
672+ "Using library JAR as fallback. Consider reinstalling the server.\n " ,
673+ "warning" ,
674+ )
638675
639- if not server_jar_path :
676+ if server_jar_path :
677+ lib_jar = "libraries" in server_jar_path
678+ self .output_callback (
679+ f"Found server JAR: { os .path .basename (server_jar_path )} { ' (in libraries/)' if lib_jar else '' } \n " ,
680+ "success" ,
681+ )
682+ else :
640683 # Audit directory before failing
641684 try :
642685 files = os .listdir (self .server_path )
643686 logging .info (f"Handler: Server Directory Audit: { files } " )
644687 except Exception as ae :
645688 logging .error (f"Handler: Directory Audit FAILED: { ae } " )
646-
647689 self .output_callback (
648- "Error: No server .jar file or run script found in the directory.\n " ,
690+ "Error: No server .jar file found in the directory.\n " ,
649691 "error" ,
650692 )
651693 return None , None
@@ -723,12 +765,41 @@ def _get_start_command(self):
723765 )
724766 # Try to run it without nogui if it's an installer,
725767 # though it probably won't start the server.
726- command .extend (["-jar" , os . path . basename ( server_jar_path ) ])
768+ command .extend (["-jar" , server_jar_path ])
727769 else :
728- command .extend (["-jar" , os . path . basename ( server_jar_path ) , "--nogui" ])
770+ command .extend (["-jar" , server_jar_path , "--nogui" ])
729771
730772 return command , custom_env
731773
774+ def _parse_startup_script (self , script_path ):
775+ """Extract Java command args from run.sh/run.bat without executing sh/bat."""
776+ try :
777+ with open (script_path , "r" ) as f :
778+ content = f .read ()
779+ for line in content .splitlines ():
780+ stripped = line .strip ()
781+ if stripped .startswith ("#" ):
782+ continue
783+ # Find the java invocation line
784+ # Matches: "java @args.txt ... nogui" or "java @args.txt ..."
785+ if re .match (r'^\s*java\s' , stripped ):
786+ # Extract tokens after 'java'
787+ # Handle quotes and @file args
788+ import shlex
789+ tokens = shlex .split (stripped , posix = sys .platform != "win32" )
790+ # Find index of 'java' and take everything after it
791+ for i , token in enumerate (tokens ):
792+ if token == "java" :
793+ args = tokens [i + 1 :]
794+ # Remove trailing shell variables like "$@" or "%*"
795+ args = [a for a in args if a not in ('"$@"' , '$@' , '%*' )]
796+ if args :
797+ return args
798+ return None
799+ except Exception as e :
800+ logging .warning (f"Handler: Could not parse startup script { script_path } : { e } " )
801+ return None
802+
732803 def _is_java_version_compatible (
733804 self , installed_major : int , required_version : int
734805 ) -> bool :
@@ -810,15 +881,25 @@ def _run_server(self, command, env):
810881 logging .error (f"Handler: CRASH in _run_server thread:\n { error_details } " )
811882 self ._log (f"Server start failed: { e } \n " , "error" )
812883 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.
884+ # Wait for reader threads to finish naturally first
815885 try :
816886 if stdout_thread :
817887 stdout_thread .join (timeout = 2 )
818888 if stderr_thread :
819889 stderr_thread .join (timeout = 2 )
820890 except :
821891 pass
892+ # Force-close pipes only if threads are still alive (zombie prevention)
893+ try :
894+ if self .server_process :
895+ if stdout_thread and stdout_thread .is_alive ():
896+ if self .server_process .stdout and not self .server_process .stdout .closed :
897+ self .server_process .stdout .close ()
898+ if stderr_thread and stderr_thread .is_alive ():
899+ if self .server_process .stderr and not self .server_process .stderr .closed :
900+ self .server_process .stderr .close ()
901+ except :
902+ pass
822903
823904 # --- CRITICAL FIX: ACTUALIZAR ESTADO INTERNO PRIMERO ---
824905 # Guardamos el estado previo para el log
@@ -1012,7 +1093,8 @@ def _read_output(self, pipe, level):
10121093 f"Handler: Log reader thread started for { level } (Chunked Binary mode)"
10131094 )
10141095
1015- buffer = b""
1096+ buffer = bytearray ()
1097+ MAX_BUFFER = 1_048_576 # 1 MB cap to prevent memory leak
10161098 while True :
10171099 chunk = pipe .read (4096 )
10181100 if not chunk :
@@ -1029,7 +1111,20 @@ def _read_output(self, pipe, level):
10291111 )
10301112 break
10311113
1032- buffer += chunk
1114+ buffer .extend (chunk )
1115+ # Force-flush if buffer exceeds cap (prevents unbounded growth)
1116+ if len (buffer ) > MAX_BUFFER :
1117+ self ._process_log_line (
1118+ buffer .decode ("utf-8" , errors = "replace" ),
1119+ level ,
1120+ re ,
1121+ join_pattern ,
1122+ leave_pattern ,
1123+ list_inline_pattern ,
1124+ list_header_pattern ,
1125+ )
1126+ buffer = bytearray ()
1127+ continue
10331128 logging .debug (
10341129 f"Handler: Received chunk of { len (chunk )} bytes from { level } "
10351130 )
@@ -1052,7 +1147,7 @@ def _read_output(self, pipe, level):
10521147 sep_len = 2 # Handle \r\n
10531148
10541149 line_bytes = buffer [:idx ]
1055- buffer = buffer [idx + sep_len :]
1150+ buffer = bytearray ( buffer [idx + sep_len :])
10561151
10571152 line = line_bytes .decode ("utf-8" , errors = "replace" )
10581153 self ._process_log_line (
0 commit comments