@@ -114,6 +114,9 @@ def _log(self, message, level="normal"):
114114
115115 def _setup_java_for_minecraft (self , minecraft_version ):
116116 """Configura automáticamente la versión correcta de Java para Minecraft."""
117+ # Skip auto-setup if user has explicitly set a custom Java path
118+ if self .java_path and self .java_path != "java" :
119+ return
117120 try :
118121 self ._log (
119122 f"Configuring Java for Minecraft { minecraft_version } ...\n " , "info"
@@ -581,71 +584,113 @@ def _get_start_command(self):
581584 custom_env ["PATH" ] = f"{ java_bin_dir } { path_sep } { custom_env .get ('PATH' , '' )} "
582585 logging .info (f"Handler: Injected Environment - JAVA_HOME={ java_root } " )
583586
584- # JAR-based startup (skipping run.sh/run.bat scripts to avoid
585- # readline/library conflicts on Linux/AppImage environments)
587+ # Try to parse run.sh/run.bat to extract the java command
588+ # (avoids executing sh/bat directly, which can cause readline/library crashes)
589+ run_script = None
590+ script_names = (
591+ ["run.bat" , "start.bat" , "launch.bat" ]
592+ if sys .platform == "win32"
593+ else ["run.sh" , "start.sh" , "launch.sh" ]
594+ )
595+ for name in script_names :
596+ p = os .path .join (self .server_path , name )
597+ if os .path .exists (p ):
598+ run_script = p
599+ break
600+
601+ if run_script :
602+ parsed = self ._parse_startup_script (run_script )
603+ if parsed :
604+ if "nogui" not in parsed :
605+ parsed .append ("nogui" )
606+ command = [java_path ] + parsed
607+ self .output_callback (
608+ "Parsed startup script: using direct Java launch.\n " , "info"
609+ )
610+ return command , custom_env
611+
612+ # Fallback: JAR-based startup
586613 self .output_callback (
587- "Starting server via JAR-based method (skipping startup scripts) .\n " , "info"
614+ "Starting server via JAR-based method.\n " , "info"
588615 )
589616 all_jars = glob .glob (os .path .join (self .server_path , "*.jar" ))
590617 server_jar_path = None
591618
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" ,
619+ # Check for forge/neoforge server JAR in libraries/ first
620+ lib_fallback = None
621+ if self .server_type in ["forge" , "neoforge" ]:
622+ lib_path = os .path .join (self .server_path , "libraries" )
623+ if os .path .exists (lib_path ):
624+ candidates = []
625+ forge_path_marker = "neoforged/neoforge" if self .server_type == "neoforge" else "minecraftforge/forge"
626+ for root , dirs , files in os .walk (lib_path ):
627+ for file in files :
628+ if file .endswith (".jar" ) and "installer" not in file .lower ():
629+ full_path = os .path .join (root , file )
630+ score = 0
631+ if forge_path_marker in root .replace ("\\ " , "/" ):
632+ score = 10
633+ elif self .server_type in file .lower ():
634+ score = 5
635+ if "server" in file .lower ():
636+ score += 2
637+ candidates .append ((score , full_path ))
638+ if candidates :
639+ candidates .sort (key = lambda x : x [0 ], reverse = True )
640+ best = candidates [0 ]
641+ lib_fallback = best [1 ]
642+ # Only use library jar if it has "server" in the name
643+ if "server" in os .path .basename (best [1 ]).lower ():
644+ server_jar_path = best [1 ]
645+
646+ # Fallback to root-level jars
647+ if not server_jar_path :
648+ non_installer_jars = [
649+ j for j in all_jars if "installer" not in os .path .basename (j ).lower ()
604650 ]
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
651+ if non_installer_jars :
652+ preferred_names = [
653+ "server.jar" ,
654+ "minecraft_server.jar" ,
655+ "paper.jar" ,
656+ "spigot.jar" ,
657+ "fabric-server-launch.jar" ,
658+ ]
659+ for name in preferred_names :
660+ for jar in non_installer_jars :
661+ if os .path .basename (jar ).lower () == name :
662+ server_jar_path = jar
663+ break
664+ if server_jar_path :
609665 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" )
666+ if not server_jar_path :
667+ server_jar_path = non_installer_jars [0 ]
668+ elif all_jars :
669+ server_jar_path = all_jars [0 ]
670+
671+ # If nothing found in root, fall back to best library candidate
672+ if not server_jar_path and lib_fallback :
673+ server_jar_path = lib_fallback
674+ self .output_callback (
675+ "Using library JAR as fallback. Consider reinstalling the server.\n " ,
676+ "warning" ,
677+ )
638678
639- if not server_jar_path :
679+ if server_jar_path :
680+ lib_jar = "libraries" in server_jar_path
681+ self .output_callback (
682+ f"Found server JAR: { os .path .basename (server_jar_path )} { ' (in libraries/)' if lib_jar else '' } \n " ,
683+ "success" ,
684+ )
685+ else :
640686 # Audit directory before failing
641687 try :
642688 files = os .listdir (self .server_path )
643689 logging .info (f"Handler: Server Directory Audit: { files } " )
644690 except Exception as ae :
645691 logging .error (f"Handler: Directory Audit FAILED: { ae } " )
646-
647692 self .output_callback (
648- "Error: No server .jar file or run script found in the directory.\n " ,
693+ "Error: No server .jar file found in the directory.\n " ,
649694 "error" ,
650695 )
651696 return None , None
@@ -723,12 +768,41 @@ def _get_start_command(self):
723768 )
724769 # Try to run it without nogui if it's an installer,
725770 # though it probably won't start the server.
726- command .extend (["-jar" , os . path . basename ( server_jar_path ) ])
771+ command .extend (["-jar" , server_jar_path ])
727772 else :
728- command .extend (["-jar" , os . path . basename ( server_jar_path ) , "--nogui" ])
773+ command .extend (["-jar" , server_jar_path , "--nogui" ])
729774
730775 return command , custom_env
731776
777+ def _parse_startup_script (self , script_path ):
778+ """Extract Java command args from run.sh/run.bat without executing sh/bat."""
779+ try :
780+ with open (script_path , "r" ) as f :
781+ content = f .read ()
782+ for line in content .splitlines ():
783+ stripped = line .strip ()
784+ if stripped .startswith ("#" ):
785+ continue
786+ # Find the java invocation line
787+ # Matches: "java @args.txt ... nogui" or "java @args.txt ..."
788+ if re .match (r'^\s*java\s' , stripped ):
789+ # Extract tokens after 'java'
790+ # Handle quotes and @file args
791+ import shlex
792+ tokens = shlex .split (stripped , posix = sys .platform != "win32" )
793+ # Find index of 'java' and take everything after it
794+ for i , token in enumerate (tokens ):
795+ if token == "java" :
796+ args = tokens [i + 1 :]
797+ # Remove trailing shell variables like "$@" or "%*"
798+ args = [a for a in args if a not in ('"$@"' , '$@' , '%*' )]
799+ if args :
800+ return args
801+ return None
802+ except Exception as e :
803+ logging .warning (f"Handler: Could not parse startup script { script_path } : { e } " )
804+ return None
805+
732806 def _is_java_version_compatible (
733807 self , installed_major : int , required_version : int
734808 ) -> bool :
@@ -810,15 +884,25 @@ def _run_server(self, command, env):
810884 logging .error (f"Handler: CRASH in _run_server thread:\n { error_details } " )
811885 self ._log (f"Server start failed: { e } \n " , "error" )
812886 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.
887+ # Wait for reader threads to finish naturally first
815888 try :
816889 if stdout_thread :
817890 stdout_thread .join (timeout = 2 )
818891 if stderr_thread :
819892 stderr_thread .join (timeout = 2 )
820893 except :
821894 pass
895+ # Force-close pipes only if threads are still alive (zombie prevention)
896+ try :
897+ if self .server_process :
898+ if stdout_thread and stdout_thread .is_alive ():
899+ if self .server_process .stdout and not self .server_process .stdout .closed :
900+ self .server_process .stdout .close ()
901+ if stderr_thread and stderr_thread .is_alive ():
902+ if self .server_process .stderr and not self .server_process .stderr .closed :
903+ self .server_process .stderr .close ()
904+ except :
905+ pass
822906
823907 # --- CRITICAL FIX: ACTUALIZAR ESTADO INTERNO PRIMERO ---
824908 # Guardamos el estado previo para el log
@@ -1012,7 +1096,8 @@ def _read_output(self, pipe, level):
10121096 f"Handler: Log reader thread started for { level } (Chunked Binary mode)"
10131097 )
10141098
1015- buffer = b""
1099+ buffer = bytearray ()
1100+ MAX_BUFFER = 1_048_576 # 1 MB cap to prevent memory leak
10161101 while True :
10171102 chunk = pipe .read (4096 )
10181103 if not chunk :
@@ -1029,7 +1114,20 @@ def _read_output(self, pipe, level):
10291114 )
10301115 break
10311116
1032- buffer += chunk
1117+ buffer .extend (chunk )
1118+ # Force-flush if buffer exceeds cap (prevents unbounded growth)
1119+ if len (buffer ) > MAX_BUFFER :
1120+ self ._process_log_line (
1121+ buffer .decode ("utf-8" , errors = "replace" ),
1122+ level ,
1123+ re ,
1124+ join_pattern ,
1125+ leave_pattern ,
1126+ list_inline_pattern ,
1127+ list_header_pattern ,
1128+ )
1129+ buffer = bytearray ()
1130+ continue
10331131 logging .debug (
10341132 f"Handler: Received chunk of { len (chunk )} bytes from { level } "
10351133 )
@@ -1052,7 +1150,7 @@ def _read_output(self, pipe, level):
10521150 sep_len = 2 # Handle \r\n
10531151
10541152 line_bytes = buffer [:idx ]
1055- buffer = buffer [idx + sep_len :]
1153+ buffer = bytearray ( buffer [idx + sep_len :])
10561154
10571155 line = line_bytes .decode ("utf-8" , errors = "replace" )
10581156 self ._process_log_line (
0 commit comments