From 5980982f24589af496a8d629529d7ed5110f30ac Mon Sep 17 00:00:00 2001 From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:21:03 +0000 Subject: [PATCH 1/9] Update and rename launch.py to portablemc.py --- launch.py => portablemc.py | 138 ++++++++++++------------------------- 1 file changed, 44 insertions(+), 94 deletions(-) rename launch.py => portablemc.py (90%) diff --git a/launch.py b/portablemc.py similarity index 90% rename from launch.py rename to portablemc.py index 5d1d725..021c7c9 100644 --- a/launch.py +++ b/portablemc.py @@ -18,14 +18,12 @@ app = Flask(__name__) socketio = SocketIO(app, cors_allowed_origins="*") - def escape_html(s): """Escape only &, <, > for safe innerHTML – leaves quotes and apostrophes untouched.""" - return s.replace("&", "&").replace("<", "<").replace(">", ">") - + return s.replace('&', '&').replace('<', '<').replace('>', '>') # --- CONFIGURATION --- -VALID_USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{3,16}$") +VALID_USERNAME_REGEX = re.compile(r'^[a-zA-Z0-9_]{3,16}$') FORBIDDEN_LIST = ["CubeUniform840", "Admin", "Owner"] PASS_KEY = "1234" SERVER_IP = "77.103.184.72" @@ -44,7 +42,6 @@ def escape_html(s): # Fallbacks when certain libraries are restricted/corrupted/disabled try: from ansi2html import Ansi2HTMLConverter - ansi_converter = Ansi2HTMLConverter(dark_bg=True, inline=True) use_server_conversion = True except ImportError: @@ -54,7 +51,6 @@ def escape_html(s): try: import psutil - PSUTIL_AVAILABLE = True except ImportError: psutil = None @@ -915,29 +911,26 @@ def escape_html(s): # --- ROUTES --- - -@socketio.on("connect") +@socketio.on('connect') def handle_connect(): global connected_clients with clients_lock: connected_clients += 1 client_id = request.sid - print(f"Client connected: {client_id} (Total: {connected_clients})") - + print(f'Client connected: {client_id} (Total: {connected_clients})') + # Send initial status - emit("status", {"core": "online", "minecraft": "checking"}) + emit('status', {'core': 'online', 'minecraft': 'checking'}) - -@socketio.on("disconnect") +@socketio.on('disconnect') def handle_disconnect(): global connected_clients with clients_lock: connected_clients -= 1 client_id = request.sid - print(f"Client disconnected: {client_id} (Total: {connected_clients})") - + print(f'Client disconnected: {client_id} (Total: {connected_clients})') -@socketio.on("ping_minecraft") +@socketio.on('ping_minecraft') def handle_ping_minecraft(): """Check Minecraft server status on demand""" try: @@ -945,10 +938,9 @@ def handle_ping_minecraft(): s.settimeout(1.5) s.connect((SERVER_IP, 25565)) s.close() - emit("minecraft_status", {"online": True}) + emit('minecraft_status', {'online': True}) except Exception: - emit("minecraft_status", {"online": False}) - + emit('minecraft_status', {'online': False}) @app.route("/ping") def ping(): @@ -963,7 +955,6 @@ def ping(): # Always return 200 OK with online=false return jsonify(online=False), 200 - @app.route("/stream") def stream(): # --- PORTABLEMC AVAILABILITY CHECK --- @@ -973,17 +964,15 @@ def is_portablemc_available(): return importlib.util.find_spec("portablemc") is not None if not is_portablemc_available(): - def error_gen(): lines = [ "\x1b[91m[!] PORTABLEMC NOT FOUND\x1b[0m", - "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m", + "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m" ] for line in lines: - payload = json.dumps({"type": "ansi", "content": line}) + payload = json.dumps({'type': 'ansi', 'content': line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") # --- GET USER INPUT --- @@ -992,44 +981,31 @@ def error_gen(): # --- VALIDATIONS (JSON wrapped) --- if not user: - def error_gen(): - payload = json.dumps( - {"type": "ansi", "content": "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"} - ) + payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") if not VALID_USERNAME_REGEX.match(user): - def error_gen(): lines = [ "\x1b[91m[!] INVALID USERNAME\x1b[0m", - "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m", + "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m" ] for line in lines: - payload = json.dumps({"type": "ansi", "content": line}) + payload = json.dumps({'type': 'ansi', 'content': line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") user_lower = user.lower() forbidden_lower = [name.lower() for name in FORBIDDEN_LIST] if user_lower in forbidden_lower and password != PASS_KEY: - def error_gen(): - payload = json.dumps( - { - "type": "ansi", - "content": "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m", - } - ) + payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m"}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") # --- PREVENT MULTIPLE LAUNCHES (thread-safe) --- @@ -1038,15 +1014,13 @@ def error_gen(): lines = [ "\x1b[91m[!] CORE BUSY\x1b[0m", "\x1b[93mAnother Minecraft instance is already running.\x1b[0m", - "\x1b[90mPlease close the game before launching again.\x1b[0m", + "\x1b[90mPlease close the game before launching again.\x1b[0m" ] - def error_gen(): for line in lines: - payload = json.dumps({"type": "ansi", "content": line}) + payload = json.dumps({'type': 'ansi', 'content': line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") if user in active_processes: del active_processes[user] @@ -1057,8 +1031,17 @@ def error_gen(): else: launcher_cmd = [sys.executable, "-m", "portablemc"] - global_args = ["--main-dir", ".", "--timeout", "60", "--output", "human-color"] - start_args = ["--server", SERVER_IP, "--jvm-args", JVM_OPTS, "fabric:", "-u", user] + global_args = [ + "--main-dir", ".", + "--timeout", "60", + "--output", "human-color" + ] + start_args = [ + "--server", SERVER_IP, + "--jvm-args", JVM_OPTS, + "fabric:", + "-u", user + ] # Custom Java path java_exe = "java.exe" if os.name == "nt" else "java" @@ -1081,9 +1064,7 @@ def generate(): with processes_lock: # Double-check that no process for this user is still running if user in active_processes and active_processes[user].poll() is None: - busy_payload = json.dumps( - {"type": "ansi", "content": "\x1b[91m[!] CORE BUSY\x1b[0m"} - ) + busy_payload = json.dumps({'type': 'ansi', 'content': '\x1b[91m[!] CORE BUSY\x1b[0m'}) yield f"data: {busy_payload}\n\n" yield "data: CLOSE\n\n" return @@ -1092,7 +1073,7 @@ def generate(): environment["__compat_layer"] = "runasinvoker" startupinfo = None - if os.name == "nt": + if os.name == 'nt': startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE @@ -1110,7 +1091,7 @@ def generate(): bufsize=1, creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, startupinfo=startupinfo, - env=environment, + env=environment ) active_processes[user] = process logging.info(f"Started process for {user} with PID {process.pid}") @@ -1122,12 +1103,7 @@ def generate(): while True: if closed_event.is_set(): - disc_msg = json.dumps( - { - "type": "ansi", - "content": "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m", - } - ) + disc_msg = json.dumps({'type': 'ansi', 'content': "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m"}) try: yield f"data: {disc_msg}\n\n" except (BrokenPipeError, OSError, GeneratorExit): @@ -1140,7 +1116,7 @@ def generate(): break continue - raw_line = line.rstrip("\n") + raw_line = line.rstrip('\n') now = time.perf_counter() if not ok_reached and "[ OK ]" in raw_line: @@ -1151,23 +1127,18 @@ def generate(): if use_server_conversion and ansi_converter: try: safe_line = escape_html(raw_line) - html_line = ansi_converter.convert( - safe_line, full=False - ).strip() - payload = json.dumps({"type": "html", "content": html_line}) + html_line = ansi_converter.convert(safe_line, full=False).strip() + payload = json.dumps({'type': 'html', 'content': html_line}) except Exception as e: logging.error(f"Conversion failed: {e}, sending raw ANSI") - payload = json.dumps({"type": "ansi", "content": raw_line}) + payload = json.dumps({'type': 'ansi', 'content': raw_line}) else: - payload = json.dumps({"type": "ansi", "content": raw_line}) + payload = json.dumps({'type': 'ansi', 'content': raw_line}) try: if progress_match and not ok_reached: current_file = progress_match.group(1) - if ( - current_file != last_progress - and (now - last_send_time) > update_interval - ): + if current_file != last_progress and (now - last_send_time) > update_interval: yield f"data: {payload}\n\n" last_progress = current_file last_send_time = now @@ -1181,21 +1152,14 @@ def generate(): except FileNotFoundError as e: if not closed_event.is_set(): try: - payload = json.dumps( - { - "type": "ansi", - "content": f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m", - } - ) + payload = json.dumps({'type': 'ansi', 'content': f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m"}) yield f"data: {payload}\n\n" except Exception: pass except Exception as e: if not closed_event.is_set(): try: - payload = json.dumps( - {"type": "ansi", "content": f"[SYSTEM ERROR] {str(e)}"} - ) + payload = json.dumps({'type': 'ansi', 'content': f"[SYSTEM ERROR] {str(e)}"}) yield f"data: {payload}\n\n" except Exception: pass @@ -1210,18 +1174,8 @@ def generate(): if not got_generator_exit: try: - ended_msg = json.dumps( - { - "type": "ansi", - "content": "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m", - } - ) - tip_msg = json.dumps( - { - "type": "ansi", - "content": "\x1b[34m[TIP] Click the console to return to login.\x1b[0m", - } - ) + ended_msg = json.dumps({'type': 'ansi', 'content': "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m"}) + tip_msg = json.dumps({'type': 'ansi', 'content': "\x1b[34m[TIP] Click the console to return to login.\x1b[0m"}) yield f"data: {ended_msg}\n\n" yield f"data: {tip_msg}\n\n" yield "data: CLOSE\n\n" @@ -1234,12 +1188,10 @@ def generate(): response.call_on_close(closed_event.set) return response - @app.route("/") def home(): return render_template_string(HTML_TEMPLATE, forbidden_list=FORBIDDEN_LIST) - def kill_process_tree(proc): """Kill a process and all its children. Falls back to simple kill if psutil missing.""" if proc.poll() is not None: @@ -1268,7 +1220,6 @@ def kill_process_tree(proc): proc.kill() logging.info(f"Terminated process PID {proc.pid} (psutil unavailable)") - def graceful_shutdown(sig, frame): logging.info("SHUTTING DOWN CORE...") with processes_lock: @@ -1276,7 +1227,6 @@ def graceful_shutdown(sig, frame): kill_process_tree(proc) sys.exit(0) - # Set signal handler for SIGINT (Ctrl+C) signal.signal(signal.SIGINT, graceful_shutdown) From 301fe519491ee4d15eaa3d1085a96f459018dfef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Mar 2026 22:21:10 +0000 Subject: [PATCH 2/9] Apply automatic ruff fixes and formatting [skip ci] --- portablemc.py | 138 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 94 insertions(+), 44 deletions(-) diff --git a/portablemc.py b/portablemc.py index 021c7c9..5d1d725 100644 --- a/portablemc.py +++ b/portablemc.py @@ -18,12 +18,14 @@ app = Flask(__name__) socketio = SocketIO(app, cors_allowed_origins="*") + def escape_html(s): """Escape only &, <, > for safe innerHTML – leaves quotes and apostrophes untouched.""" - return s.replace('&', '&').replace('<', '<').replace('>', '>') + return s.replace("&", "&").replace("<", "<").replace(">", ">") + # --- CONFIGURATION --- -VALID_USERNAME_REGEX = re.compile(r'^[a-zA-Z0-9_]{3,16}$') +VALID_USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{3,16}$") FORBIDDEN_LIST = ["CubeUniform840", "Admin", "Owner"] PASS_KEY = "1234" SERVER_IP = "77.103.184.72" @@ -42,6 +44,7 @@ def escape_html(s): # Fallbacks when certain libraries are restricted/corrupted/disabled try: from ansi2html import Ansi2HTMLConverter + ansi_converter = Ansi2HTMLConverter(dark_bg=True, inline=True) use_server_conversion = True except ImportError: @@ -51,6 +54,7 @@ def escape_html(s): try: import psutil + PSUTIL_AVAILABLE = True except ImportError: psutil = None @@ -911,26 +915,29 @@ def escape_html(s): # --- ROUTES --- -@socketio.on('connect') + +@socketio.on("connect") def handle_connect(): global connected_clients with clients_lock: connected_clients += 1 client_id = request.sid - print(f'Client connected: {client_id} (Total: {connected_clients})') - + print(f"Client connected: {client_id} (Total: {connected_clients})") + # Send initial status - emit('status', {'core': 'online', 'minecraft': 'checking'}) + emit("status", {"core": "online", "minecraft": "checking"}) -@socketio.on('disconnect') + +@socketio.on("disconnect") def handle_disconnect(): global connected_clients with clients_lock: connected_clients -= 1 client_id = request.sid - print(f'Client disconnected: {client_id} (Total: {connected_clients})') + print(f"Client disconnected: {client_id} (Total: {connected_clients})") + -@socketio.on('ping_minecraft') +@socketio.on("ping_minecraft") def handle_ping_minecraft(): """Check Minecraft server status on demand""" try: @@ -938,9 +945,10 @@ def handle_ping_minecraft(): s.settimeout(1.5) s.connect((SERVER_IP, 25565)) s.close() - emit('minecraft_status', {'online': True}) + emit("minecraft_status", {"online": True}) except Exception: - emit('minecraft_status', {'online': False}) + emit("minecraft_status", {"online": False}) + @app.route("/ping") def ping(): @@ -955,6 +963,7 @@ def ping(): # Always return 200 OK with online=false return jsonify(online=False), 200 + @app.route("/stream") def stream(): # --- PORTABLEMC AVAILABILITY CHECK --- @@ -964,15 +973,17 @@ def is_portablemc_available(): return importlib.util.find_spec("portablemc") is not None if not is_portablemc_available(): + def error_gen(): lines = [ "\x1b[91m[!] PORTABLEMC NOT FOUND\x1b[0m", - "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m" + "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m", ] for line in lines: - payload = json.dumps({'type': 'ansi', 'content': line}) + payload = json.dumps({"type": "ansi", "content": line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") # --- GET USER INPUT --- @@ -981,31 +992,44 @@ def error_gen(): # --- VALIDATIONS (JSON wrapped) --- if not user: + def error_gen(): - payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"}) + payload = json.dumps( + {"type": "ansi", "content": "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"} + ) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") if not VALID_USERNAME_REGEX.match(user): + def error_gen(): lines = [ "\x1b[91m[!] INVALID USERNAME\x1b[0m", - "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m" + "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m", ] for line in lines: - payload = json.dumps({'type': 'ansi', 'content': line}) + payload = json.dumps({"type": "ansi", "content": line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") user_lower = user.lower() forbidden_lower = [name.lower() for name in FORBIDDEN_LIST] if user_lower in forbidden_lower and password != PASS_KEY: + def error_gen(): - payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m"}) + payload = json.dumps( + { + "type": "ansi", + "content": "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m", + } + ) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") # --- PREVENT MULTIPLE LAUNCHES (thread-safe) --- @@ -1014,13 +1038,15 @@ def error_gen(): lines = [ "\x1b[91m[!] CORE BUSY\x1b[0m", "\x1b[93mAnother Minecraft instance is already running.\x1b[0m", - "\x1b[90mPlease close the game before launching again.\x1b[0m" + "\x1b[90mPlease close the game before launching again.\x1b[0m", ] + def error_gen(): for line in lines: - payload = json.dumps({'type': 'ansi', 'content': line}) + payload = json.dumps({"type": "ansi", "content": line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") if user in active_processes: del active_processes[user] @@ -1031,17 +1057,8 @@ def error_gen(): else: launcher_cmd = [sys.executable, "-m", "portablemc"] - global_args = [ - "--main-dir", ".", - "--timeout", "60", - "--output", "human-color" - ] - start_args = [ - "--server", SERVER_IP, - "--jvm-args", JVM_OPTS, - "fabric:", - "-u", user - ] + global_args = ["--main-dir", ".", "--timeout", "60", "--output", "human-color"] + start_args = ["--server", SERVER_IP, "--jvm-args", JVM_OPTS, "fabric:", "-u", user] # Custom Java path java_exe = "java.exe" if os.name == "nt" else "java" @@ -1064,7 +1081,9 @@ def generate(): with processes_lock: # Double-check that no process for this user is still running if user in active_processes and active_processes[user].poll() is None: - busy_payload = json.dumps({'type': 'ansi', 'content': '\x1b[91m[!] CORE BUSY\x1b[0m'}) + busy_payload = json.dumps( + {"type": "ansi", "content": "\x1b[91m[!] CORE BUSY\x1b[0m"} + ) yield f"data: {busy_payload}\n\n" yield "data: CLOSE\n\n" return @@ -1073,7 +1092,7 @@ def generate(): environment["__compat_layer"] = "runasinvoker" startupinfo = None - if os.name == 'nt': + if os.name == "nt": startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE @@ -1091,7 +1110,7 @@ def generate(): bufsize=1, creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, startupinfo=startupinfo, - env=environment + env=environment, ) active_processes[user] = process logging.info(f"Started process for {user} with PID {process.pid}") @@ -1103,7 +1122,12 @@ def generate(): while True: if closed_event.is_set(): - disc_msg = json.dumps({'type': 'ansi', 'content': "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m"}) + disc_msg = json.dumps( + { + "type": "ansi", + "content": "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m", + } + ) try: yield f"data: {disc_msg}\n\n" except (BrokenPipeError, OSError, GeneratorExit): @@ -1116,7 +1140,7 @@ def generate(): break continue - raw_line = line.rstrip('\n') + raw_line = line.rstrip("\n") now = time.perf_counter() if not ok_reached and "[ OK ]" in raw_line: @@ -1127,18 +1151,23 @@ def generate(): if use_server_conversion and ansi_converter: try: safe_line = escape_html(raw_line) - html_line = ansi_converter.convert(safe_line, full=False).strip() - payload = json.dumps({'type': 'html', 'content': html_line}) + html_line = ansi_converter.convert( + safe_line, full=False + ).strip() + payload = json.dumps({"type": "html", "content": html_line}) except Exception as e: logging.error(f"Conversion failed: {e}, sending raw ANSI") - payload = json.dumps({'type': 'ansi', 'content': raw_line}) + payload = json.dumps({"type": "ansi", "content": raw_line}) else: - payload = json.dumps({'type': 'ansi', 'content': raw_line}) + payload = json.dumps({"type": "ansi", "content": raw_line}) try: if progress_match and not ok_reached: current_file = progress_match.group(1) - if current_file != last_progress and (now - last_send_time) > update_interval: + if ( + current_file != last_progress + and (now - last_send_time) > update_interval + ): yield f"data: {payload}\n\n" last_progress = current_file last_send_time = now @@ -1152,14 +1181,21 @@ def generate(): except FileNotFoundError as e: if not closed_event.is_set(): try: - payload = json.dumps({'type': 'ansi', 'content': f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m"}) + payload = json.dumps( + { + "type": "ansi", + "content": f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m", + } + ) yield f"data: {payload}\n\n" except Exception: pass except Exception as e: if not closed_event.is_set(): try: - payload = json.dumps({'type': 'ansi', 'content': f"[SYSTEM ERROR] {str(e)}"}) + payload = json.dumps( + {"type": "ansi", "content": f"[SYSTEM ERROR] {str(e)}"} + ) yield f"data: {payload}\n\n" except Exception: pass @@ -1174,8 +1210,18 @@ def generate(): if not got_generator_exit: try: - ended_msg = json.dumps({'type': 'ansi', 'content': "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m"}) - tip_msg = json.dumps({'type': 'ansi', 'content': "\x1b[34m[TIP] Click the console to return to login.\x1b[0m"}) + ended_msg = json.dumps( + { + "type": "ansi", + "content": "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m", + } + ) + tip_msg = json.dumps( + { + "type": "ansi", + "content": "\x1b[34m[TIP] Click the console to return to login.\x1b[0m", + } + ) yield f"data: {ended_msg}\n\n" yield f"data: {tip_msg}\n\n" yield "data: CLOSE\n\n" @@ -1188,10 +1234,12 @@ def generate(): response.call_on_close(closed_event.set) return response + @app.route("/") def home(): return render_template_string(HTML_TEMPLATE, forbidden_list=FORBIDDEN_LIST) + def kill_process_tree(proc): """Kill a process and all its children. Falls back to simple kill if psutil missing.""" if proc.poll() is not None: @@ -1220,6 +1268,7 @@ def kill_process_tree(proc): proc.kill() logging.info(f"Terminated process PID {proc.pid} (psutil unavailable)") + def graceful_shutdown(sig, frame): logging.info("SHUTTING DOWN CORE...") with processes_lock: @@ -1227,6 +1276,7 @@ def graceful_shutdown(sig, frame): kill_process_tree(proc) sys.exit(0) + # Set signal handler for SIGINT (Ctrl+C) signal.signal(signal.SIGINT, graceful_shutdown) From 3a59c6af0db190b436d894adc70ace0ca043b435 Mon Sep 17 00:00:00 2001 From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:45:33 +0000 Subject: [PATCH 3/9] Completion of splitting of launch.py --- __main__.py | 6 -- main.py | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+), 6 deletions(-) delete mode 100644 __main__.py create mode 100644 main.py diff --git a/__main__.py b/__main__.py deleted file mode 100644 index 707ab47..0000000 --- a/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys -from portablemc.cli import main - -if __name__ == "__main__": - sys.argv = ["portablemc"] + sys.argv[1:] - sys.exit(main()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..0c49e2b --- /dev/null +++ b/main.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +""" +Cross‑platform bootstrap script for embedded Python 3.14. +Attempts to download the native portablemc binary, falls back to pip. +Run with any Python (e.g., system 3.11) to set up and launch the launcher. +""" + +import os +import sys +import subprocess +import urllib.request +import zipfile +import tarfile +import shutil +import platform +from pathlib import Path + +# --- Configuration --- +EMBEDDED_DIR = Path(__file__).parent / "python" +EMBEDDED_PYTHON = EMBEDDED_DIR / "python.exe" +PYTHON_VERSION = "3.14.3" +PYTHON_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip" +GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" +BASE_PACKAGES = ["flask", "flask-socketio", "psutil", "ansi2html"] +PORTABLEMC_BIN_DIR = Path(__file__).parent / "portablemc_bin" + +# --- OS and architecture detection --- +SYSTEM = platform.system().lower() +MACHINE = platform.machine().lower() + +ARCH_MAP = { + 'x86_64': 'x86_64', + 'amd64': 'x86_64', + 'i686': 'i686', + 'i386': 'i686', + 'aarch64': 'aarch64', + 'arm64': 'aarch64', + 'armv7l': 'arm-gnueabihf', + 'arm': 'arm-gnueabihf', +} + +OS_MAP = { + 'windows': 'windows', + 'linux': 'linux', + 'darwin': 'macos', +} + +def get_portablemc_url(): + """Return the download URL for the native portablemc binary, or None.""" + os_name = OS_MAP.get(SYSTEM) + if not os_name: + print(f"⚠️ Unsupported OS: {SYSTEM}") + return None + arch = ARCH_MAP.get(MACHINE, 'x86_64') + if os_name == 'macos': + arch = 'aarch64' if arch == 'aarch64' else 'x86_64' + if os_name == 'linux' and arch not in ('arm-gnueabihf',): + arch += '-gnu' + base = "https://github.com/mindstorm38/portablemc/releases/download/v5.0.2/" + if os_name == 'windows': + ext = "zip" + filename = f"portablemc-5.0.2-{os_name}-{arch}-msvc.{ext}" + else: + ext = "tar.gz" + filename = f"portablemc-5.0.2-{os_name}-{arch}.{ext}" + return base + filename + +# --- Helper functions --- +def ensure_embedded_python(): + if EMBEDDED_PYTHON.exists(): + print("✅ Embedded Python already exists.") + return True + print("📥 Downloading embedded Python...") + try: + urllib.request.urlretrieve(PYTHON_URL, "python-embed.zip") + except Exception as e: + print(f"❌ Failed to download Python: {e}") + return False + print("📦 Extracting...") + with zipfile.ZipFile("python-embed.zip", "r") as zip_ref: + zip_ref.extractall(EMBEDDED_DIR) + os.remove("python-embed.zip") + print("✅ Embedded Python ready.") + return True + +def fix_pth_file(): + pth_files = list(EMBEDDED_DIR.glob("*._pth")) + if not pth_files: + print("❌ No ._pth file found; cannot enable site-packages.") + return False + pth = pth_files[0] + with open(pth, "r") as f: + content = f.read() + if "import site" in content and "#import site" in content: + content = content.replace("#import site", "import site") + with open(pth, "w") as f: + f.write(content) + print("✅ Enabled site-packages in ._pth file.") + else: + print("ℹ️ site-packages already enabled.") + return True + +def download_get_pip(): + pip_script = EMBEDDED_DIR / "get-pip.py" + if pip_script.exists(): + print("✅ get-pip.py already present.") + return pip_script + print("📥 Downloading get-pip.py...") + try: + urllib.request.urlretrieve(GET_PIP_URL, pip_script) + except Exception as e: + print(f"❌ Failed to download get-pip.py: {e}") + return None + return pip_script + +def run_pip_command(args, isolated=True): + env = os.environ.copy() + if isolated: + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONPATH"] = "" + cmd = [str(EMBEDDED_PYTHON)] + args + result = subprocess.run(cmd, env=env, capture_output=True, text=True) + if result.returncode != 0: + print(f"❌ Pip command failed: {' '.join(args)}") + print(result.stderr) + return False + print(result.stdout) + return True + +def install_pip(): + pip_script = download_get_pip() + if not pip_script: + return False + env = os.environ.copy() + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONPATH"] = "" + cmd = [str(EMBEDDED_PYTHON), str(pip_script), + "--trusted-host=files.pythonhosted.org", + "--trusted-host=pypi.org"] + result = subprocess.run(cmd, env=env, capture_output=True, text=True) + if result.returncode != 0: + print("❌ Failed to install pip.") + print(result.stderr) + return False + print("✅ pip installed.") + return True + +def install_base_packages(): + print("📦 Installing base packages...") + if not run_pip_command(["-m", "pip", "install", "--upgrade", "pip"], isolated=True): + print("⚠️ Pip upgrade failed, continuing anyway.") + for pkg in BASE_PACKAGES: + print(f" Installing {pkg}...") + if not run_pip_command(["-m", "pip", "install", pkg], isolated=True): + print(f"❌ Failed to install {pkg}.") + return False + return True + +def download_portablemc_binary(): + url = get_portablemc_url() + if not url: + return False + print(f"📥 Downloading portablemc from {url}...") + archive_path = Path(__file__).parent / "portablemc_download" + try: + urllib.request.urlretrieve(url, archive_path) + except Exception as e: + print(f"❌ Failed to download: {e}") + return False + PORTABLEMC_BIN_DIR.mkdir(exist_ok=True) + try: + if url.endswith(".zip"): + with zipfile.ZipFile(archive_path, "r") as zip_ref: + zip_ref.extractall(PORTABLEMC_BIN_DIR) + else: + with tarfile.open(archive_path, "r:gz") as tar_ref: + tar_ref.extractall(PORTABLEMC_BIN_DIR) + except Exception as e: + print(f"❌ Failed to extract archive: {e}") + return False + finally: + archive_path.unlink(missing_ok=True) + # Move all extracted files to the root of PORTABLEMC_BIN_DIR + for item in PORTABLEMC_BIN_DIR.iterdir(): + if item.is_dir(): + for sub in item.iterdir(): + sub.rename(PORTABLEMC_BIN_DIR / sub.name) + item.rmdir() + print(f"✅ portablemc binary extracted to {PORTABLEMC_BIN_DIR}") + return True + +def test_portablemc(): + """Check if portablemc is available (binary or module). Returns 'binary' or 'module' or None.""" + # Try binary first + exe_name = "portablemc.exe" if SYSTEM == "windows" else "portablemc" + binary_path = PORTABLEMC_BIN_DIR / exe_name + if binary_path.exists(): + if SYSTEM != "windows": + binary_path.chmod(binary_path.stat().st_mode | 0o111) + env = os.environ.copy() + env["PATH"] = str(PORTABLEMC_BIN_DIR) + os.pathsep + env.get("PATH", "") + try: + result = subprocess.run([str(binary_path), "--help"], env=env, + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print("✅ portablemc binary works.") + return "binary" + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + # Try module + env = os.environ.copy() + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONPATH"] = "" + cmd = [str(EMBEDDED_PYTHON), "-m", "portablemc", "--help"] + result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print("✅ portablemc module works.") + return "module" + print("❌ portablemc not available.") + return None + +def ensure_portablemc(): + """Make portablemc available – try binary, fallback to pip. Returns method string or None.""" + method = test_portablemc() + if method: + return method + if download_portablemc_binary(): + method = test_portablemc() + if method: + return method + print("⚠️ Binary download failed, falling back to pip.") + print("📦 Installing portablemc via pip...") + if run_pip_command(["-m", "pip", "install", "portablemc"], isolated=True): + method = test_portablemc() + if method: + return method + return None + +def launch_launcher(method): + launcher_script = Path(__file__).parent / "portablemc.py" + if not launcher_script.exists(): + print("❌ portablemc.py not found in the same directory.") + return False + + env = os.environ.copy() + paths = [str(EMBEDDED_DIR), str(EMBEDDED_DIR / "Scripts"), str(PORTABLEMC_BIN_DIR)] + env["PATH"] = os.pathsep.join(paths) + os.pathsep + env.get("PATH", "") + env["__compat_layer"] = "runasinvoker" + env["PYTHONHOME"] = str(EMBEDDED_DIR) + env["CLICOLOR_FORCE"] = "1" + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONPATH"] = "" + env["PORTABLEMC_METHOD"] = method + + cmd = [str(EMBEDDED_PYTHON), str(launcher_script)] + print(f"🚀 Launching: {' '.join(cmd)}") + try: + subprocess.run(cmd, env=env, check=True) + except subprocess.CalledProcessError as e: + print(f"❌ Launcher exited with error: {e}") + return False + except KeyboardInterrupt: + print("⏹️ Interrupted by user.") + return True + +def main(): + print("=== Embedded Python Bootstrap (Cross‑platform) ===") + if not ensure_embedded_python(): + sys.exit(1) + if not fix_pth_file(): + sys.exit(1) + # Check pip + env_check = os.environ.copy() + env_check["PYTHONNOUSERSITE"] = "1" + env_check["PYTHONPATH"] = "" + pip_check = subprocess.run([str(EMBEDDED_PYTHON), "-m", "pip", "--version"], + env=env_check, capture_output=True, text=True) + if pip_check.returncode != 0: + print("📦 pip not found, installing...") + if not install_pip(): + sys.exit(1) + else: + print(f"✅ pip already installed: {pip_check.stdout.strip()}") + if not install_base_packages(): + sys.exit(1) + method = ensure_portablemc() + if not method: + sys.exit(1) + print("✅ Setup complete. Launching portablemc.py...") + launch_launcher(method) + +if __name__ == "__main__": + main() From b27c090b2c2ca5e381feecf4d4a78ec397049236 Mon Sep 17 00:00:00 2001 From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:46:04 +0000 Subject: [PATCH 4/9] Fix --jvm-args --- portablemc.py | 209 ++++++++++++++++++++------------------------------ 1 file changed, 84 insertions(+), 125 deletions(-) diff --git a/portablemc.py b/portablemc.py index 5d1d725..5055a0f 100644 --- a/portablemc.py +++ b/portablemc.py @@ -10,7 +10,7 @@ import shutil import threading import logging -import psutil +import traceback import os import json import importlib.util @@ -18,14 +18,12 @@ app = Flask(__name__) socketio = SocketIO(app, cors_allowed_origins="*") - def escape_html(s): """Escape only &, <, > for safe innerHTML – leaves quotes and apostrophes untouched.""" - return s.replace("&", "&").replace("<", "<").replace(">", ">") - + return s.replace('&', '&').replace('<', '<').replace('>', '>') # --- CONFIGURATION --- -VALID_USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{3,16}$") +VALID_USERNAME_REGEX = re.compile(r'^[a-zA-Z0-9_]{3,16}$') FORBIDDEN_LIST = ["CubeUniform840", "Admin", "Owner"] PASS_KEY = "1234" SERVER_IP = "77.103.184.72" @@ -41,20 +39,9 @@ def escape_html(s): clients_lock = threading.Lock() logging.basicConfig(level=logging.INFO) -# Fallbacks when certain libraries are restricted/corrupted/disabled -try: - from ansi2html import Ansi2HTMLConverter - - ansi_converter = Ansi2HTMLConverter(dark_bg=True, inline=True) - use_server_conversion = True -except ImportError: - # ansi2html not installed – fallback to raw ANSI - use_server_conversion = False - logging.warning("ansi2html not installed; client will handle ANSI conversion.") - +# Optional psutil for process tree killing try: import psutil - PSUTIL_AVAILABLE = True except ImportError: psutil = None @@ -64,6 +51,15 @@ def escape_html(s): "Install psutil for full cleanup." ) +# Fallbacks when certain libraries are restricted/corrupted/disabled +try: + from ansi2html import Ansi2HTMLConverter + ansi_converter = Ansi2HTMLConverter(dark_bg=True, inline=True) + use_server_conversion = True +except ImportError: + # ansi2html not installed – fallback to raw ANSI + use_server_conversion = False + logging.warning("ansi2html not installed; client will handle ANSI conversion.") # --- HTML TEMPLATE (with 4-space indented JavaScript) --- HTML_TEMPLATE = r""" @@ -914,45 +910,36 @@ def escape_html(s): """ # --- ROUTES --- - - -@socketio.on("connect") +@socketio.on('connect') def handle_connect(): global connected_clients with clients_lock: connected_clients += 1 client_id = request.sid - print(f"Client connected: {client_id} (Total: {connected_clients})") + print(f'Client connected: {client_id} (Total: {connected_clients})') + emit('status', {'core': 'online', 'minecraft': 'checking'}) - # Send initial status - emit("status", {"core": "online", "minecraft": "checking"}) - - -@socketio.on("disconnect") +@socketio.on('disconnect') def handle_disconnect(): global connected_clients with clients_lock: connected_clients -= 1 client_id = request.sid - print(f"Client disconnected: {client_id} (Total: {connected_clients})") - + print(f'Client disconnected: {client_id} (Total: {connected_clients})') -@socketio.on("ping_minecraft") +@socketio.on('ping_minecraft') def handle_ping_minecraft(): - """Check Minecraft server status on demand""" try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(1.5) s.connect((SERVER_IP, 25565)) s.close() - emit("minecraft_status", {"online": True}) + emit('minecraft_status', {'online': True}) except Exception: - emit("minecraft_status", {"online": False}) - + emit('minecraft_status', {'online': False}) @app.route("/ping") def ping(): - """Checks if the Minecraft server is online.""" try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(1.5) @@ -960,10 +947,8 @@ def ping(): s.close() return jsonify(online=True) except Exception: - # Always return 200 OK with online=false return jsonify(online=False), 200 - @app.route("/stream") def stream(): # --- PORTABLEMC AVAILABILITY CHECK --- @@ -973,17 +958,15 @@ def is_portablemc_available(): return importlib.util.find_spec("portablemc") is not None if not is_portablemc_available(): - def error_gen(): lines = [ "\x1b[91m[!] PORTABLEMC NOT FOUND\x1b[0m", - "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m", + "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m" ] for line in lines: - payload = json.dumps({"type": "ansi", "content": line}) + payload = json.dumps({'type': 'ansi', 'content': line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") # --- GET USER INPUT --- @@ -992,44 +975,31 @@ def error_gen(): # --- VALIDATIONS (JSON wrapped) --- if not user: - def error_gen(): - payload = json.dumps( - {"type": "ansi", "content": "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"} - ) + payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") if not VALID_USERNAME_REGEX.match(user): - def error_gen(): lines = [ "\x1b[91m[!] INVALID USERNAME\x1b[0m", - "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m", + "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m" ] for line in lines: - payload = json.dumps({"type": "ansi", "content": line}) + payload = json.dumps({'type': 'ansi', 'content': line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") user_lower = user.lower() forbidden_lower = [name.lower() for name in FORBIDDEN_LIST] if user_lower in forbidden_lower and password != PASS_KEY: - def error_gen(): - payload = json.dumps( - { - "type": "ansi", - "content": "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m", - } - ) + payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m"}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") # --- PREVENT MULTIPLE LAUNCHES (thread-safe) --- @@ -1038,35 +1008,67 @@ def error_gen(): lines = [ "\x1b[91m[!] CORE BUSY\x1b[0m", "\x1b[93mAnother Minecraft instance is already running.\x1b[0m", - "\x1b[90mPlease close the game before launching again.\x1b[0m", + "\x1b[90mPlease close the game before launching again.\x1b[0m" ] - def error_gen(): for line in lines: - payload = json.dumps({"type": "ansi", "content": line}) + payload = json.dumps({'type': 'ansi', 'content': line}) yield f"data: {payload}\n\n" yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") if user in active_processes: del active_processes[user] - # --- BUILD COMMAND --- + # --- BUILD COMMAND (with binary vs module adaptation) --- + using_binary = os.environ.get("PORTABLEMC_METHOD") == "binary" + + # Determine launcher command if shutil.which("portablemc"): launcher_cmd = ["portablemc"] else: launcher_cmd = [sys.executable, "-m", "portablemc"] - global_args = ["--main-dir", ".", "--timeout", "60", "--output", "human-color"] - start_args = ["--server", SERVER_IP, "--jvm-args", JVM_OPTS, "fabric:", "-u", user] + # Global arguments (before 'start') + global_args = ["--main-dir", "."] + if not using_binary: + # Python module supports these extras + global_args.extend(["--timeout", "60", "--output", "human-color"]) + + # Start‑specific arguments + start_args = [] + if using_binary: + # Binary uses --join-server + start_args.extend(["--join-server", SERVER_IP]) + else: + # Python module uses --server + start_args.extend(["--server", SERVER_IP]) + + # JVM arguments + if using_binary: + # Binary expects --jvm-arg=OPT1,OPT2,OPT3 + jvm_opts_combined = ",".join(JVM_OPTS.split()) + start_args.append(f"--jvm-arg={jvm_opts_combined}") + else: + # Python module accepts a single --jvm-args string + start_args.extend(["--jvm-args", JVM_OPTS]) + + # Version (fabric:) + start_args.append("fabric:") + + # Username + start_args.extend(["-u", user]) - # Custom Java path + # Custom Java path (supported by both) java_exe = "java.exe" if os.name == "nt" else "java" java_bin = Path.cwd() / "jvm" / "java-runtime-delta" / "bin" / java_exe if java_bin.exists(): - start_args.insert(0, "--jvm") - start_args.insert(1, str(java_bin)) + # Insert --jvm and its path right after the server arguments + # Current start_args: [server args, jvm args, version, -u user] + # We want to insert before the version, so after the server args. + # We'll insert at position 2 (after the two server args) + start_args[2:2] = ["--jvm", str(java_bin)] + # Full command cmd = launcher_cmd + global_args + ["start"] + start_args # --- DISCONNECT DETECTION --- @@ -1074,31 +1076,24 @@ def error_gen(): progress_re = re.compile(r"(\d+/\d+)") def generate(): + # ... (the rest of the generator remains unchanged) ... process = None got_generator_exit = False try: - # --- ATOMIC PROCESS CREATION AND TRACKING --- with processes_lock: - # Double-check that no process for this user is still running if user in active_processes and active_processes[user].poll() is None: - busy_payload = json.dumps( - {"type": "ansi", "content": "\x1b[91m[!] CORE BUSY\x1b[0m"} - ) + busy_payload = json.dumps({'type': 'ansi', 'content': '\x1b[91m[!] CORE BUSY\x1b[0m'}) yield f"data: {busy_payload}\n\n" yield "data: CLOSE\n\n" return - environment = os.environ.copy() - environment["__compat_layer"] = "runasinvoker" - startupinfo = None - if os.name == "nt": + if os.name == 'nt': startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE # SAFE: shell=False, all arguments are separate list elements. - # The 'user' input is passed as a single argument, never executed. process = subprocess.Popen( cmd, stdout=subprocess.PIPE, @@ -1109,8 +1104,7 @@ def generate(): errors="replace", bufsize=1, creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, - startupinfo=startupinfo, - env=environment, + startupinfo=startupinfo ) active_processes[user] = process logging.info(f"Started process for {user} with PID {process.pid}") @@ -1122,12 +1116,7 @@ def generate(): while True: if closed_event.is_set(): - disc_msg = json.dumps( - { - "type": "ansi", - "content": "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m", - } - ) + disc_msg = json.dumps({'type': 'ansi', 'content': "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m"}) try: yield f"data: {disc_msg}\n\n" except (BrokenPipeError, OSError, GeneratorExit): @@ -1140,7 +1129,7 @@ def generate(): break continue - raw_line = line.rstrip("\n") + raw_line = line.rstrip('\n') now = time.perf_counter() if not ok_reached and "[ OK ]" in raw_line: @@ -1151,23 +1140,18 @@ def generate(): if use_server_conversion and ansi_converter: try: safe_line = escape_html(raw_line) - html_line = ansi_converter.convert( - safe_line, full=False - ).strip() - payload = json.dumps({"type": "html", "content": html_line}) + html_line = ansi_converter.convert(safe_line, full=False).strip() + payload = json.dumps({'type': 'html', 'content': html_line}) except Exception as e: logging.error(f"Conversion failed: {e}, sending raw ANSI") - payload = json.dumps({"type": "ansi", "content": raw_line}) + payload = json.dumps({'type': 'ansi', 'content': raw_line}) else: - payload = json.dumps({"type": "ansi", "content": raw_line}) + payload = json.dumps({'type': 'ansi', 'content': raw_line}) try: if progress_match and not ok_reached: current_file = progress_match.group(1) - if ( - current_file != last_progress - and (now - last_send_time) > update_interval - ): + if current_file != last_progress and (now - last_send_time) > update_interval: yield f"data: {payload}\n\n" last_progress = current_file last_send_time = now @@ -1181,21 +1165,14 @@ def generate(): except FileNotFoundError as e: if not closed_event.is_set(): try: - payload = json.dumps( - { - "type": "ansi", - "content": f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m", - } - ) + payload = json.dumps({'type': 'ansi', 'content': f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m"}) yield f"data: {payload}\n\n" except Exception: pass except Exception as e: if not closed_event.is_set(): try: - payload = json.dumps( - {"type": "ansi", "content": f"[SYSTEM ERROR] {str(e)}"} - ) + payload = json.dumps({'type': 'ansi', 'content': f"[SYSTEM ERROR] {str(e)}"}) yield f"data: {payload}\n\n" except Exception: pass @@ -1210,18 +1187,8 @@ def generate(): if not got_generator_exit: try: - ended_msg = json.dumps( - { - "type": "ansi", - "content": "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m", - } - ) - tip_msg = json.dumps( - { - "type": "ansi", - "content": "\x1b[34m[TIP] Click the console to return to login.\x1b[0m", - } - ) + ended_msg = json.dumps({'type': 'ansi', 'content': "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m"}) + tip_msg = json.dumps({'type': 'ansi', 'content': "\x1b[34m[TIP] Click the console to return to login.\x1b[0m"}) yield f"data: {ended_msg}\n\n" yield f"data: {tip_msg}\n\n" yield "data: CLOSE\n\n" @@ -1234,18 +1201,15 @@ def generate(): response.call_on_close(closed_event.set) return response - @app.route("/") def home(): return render_template_string(HTML_TEMPLATE, forbidden_list=FORBIDDEN_LIST) - def kill_process_tree(proc): """Kill a process and all its children. Falls back to simple kill if psutil missing.""" if proc.poll() is not None: logging.debug(f"Process {proc.pid} already dead") return - if PSUTIL_AVAILABLE: try: parent = psutil.Process(proc.pid) @@ -1260,7 +1224,6 @@ def kill_process_tree(proc): except psutil.NoSuchProcess: logging.debug(f"Process {proc.pid} already gone") else: - # Fallback: terminate the main process only try: proc.terminate() proc.wait(timeout=2) @@ -1268,7 +1231,6 @@ def kill_process_tree(proc): proc.kill() logging.info(f"Terminated process PID {proc.pid} (psutil unavailable)") - def graceful_shutdown(sig, frame): logging.info("SHUTTING DOWN CORE...") with processes_lock: @@ -1276,13 +1238,10 @@ def graceful_shutdown(sig, frame): kill_process_tree(proc) sys.exit(0) - -# Set signal handler for SIGINT (Ctrl+C) signal.signal(signal.SIGINT, graceful_shutdown) if __name__ == "__main__": try: socketio.run(app, port=5000, debug=False, allow_unsafe_werkzeug=True) except KeyboardInterrupt: - # Fallback if signal handler doesn't catch it graceful_shutdown(None, None) From c1c0e67bb2795bd17b0b88a714ff5d220e4c395a Mon Sep 17 00:00:00 2001 From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:52:15 +0000 Subject: [PATCH 5/9] Update portablemc.py --- portablemc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/portablemc.py b/portablemc.py index 5055a0f..db8e38a 100644 --- a/portablemc.py +++ b/portablemc.py @@ -1076,7 +1076,6 @@ def error_gen(): progress_re = re.compile(r"(\d+/\d+)") def generate(): - # ... (the rest of the generator remains unchanged) ... process = None got_generator_exit = False try: From 6409e8867c9bd7129bf9bcdbb1cf73383e350add Mon Sep 17 00:00:00 2001 From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:01:19 +0000 Subject: [PATCH 6/9] Support for SSL --- portablemc.py | 1533 ++++++++++--------------------------------------- 1 file changed, 298 insertions(+), 1235 deletions(-) diff --git a/portablemc.py b/portablemc.py index db8e38a..10cae83 100644 --- a/portablemc.py +++ b/portablemc.py @@ -1,1246 +1,309 @@ -from pathlib import Path -from flask import Flask, request, render_template_string, jsonify, Response -from flask_socketio import SocketIO, emit -import subprocess -import sys -import socket -import time -import re -import signal -import shutil -import threading -import logging -import traceback -import os -import json -import importlib.util - -app = Flask(__name__) -socketio = SocketIO(app, cors_allowed_origins="*") - -def escape_html(s): - """Escape only &, <, > for safe innerHTML – leaves quotes and apostrophes untouched.""" - return s.replace('&', '&').replace('<', '<').replace('>', '>') - -# --- CONFIGURATION --- -VALID_USERNAME_REGEX = re.compile(r'^[a-zA-Z0-9_]{3,16}$') -FORBIDDEN_LIST = ["CubeUniform840", "Admin", "Owner"] -PASS_KEY = "1234" -SERVER_IP = "77.103.184.72" -# SERVER_IP = "eu.chickencraft.nl" -JVM_OPTS = "-Xmx3G -Xms3G -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1NewSizePercent=20 -XX:G1ReservePercent=20 -XX:MaxGCPauseMillis=50 -XX:G1HeapRegionSize=32M -XX:+AlwaysPreTouch -XX:+ParallelRefProcEnabled -XX:+DisableExplicitGC" - -# Thread-safe process tracking -active_processes = {} -processes_lock = threading.Lock() - -# Track connected clients -connected_clients = 0 -clients_lock = threading.Lock() -logging.basicConfig(level=logging.INFO) - -# Optional psutil for process tree killing -try: - import psutil - PSUTIL_AVAILABLE = True -except ImportError: - psutil = None - PSUTIL_AVAILABLE = False - logging.warning( - "psutil not installed; process tree killing is limited. " - "Install psutil for full cleanup." - ) - -# Fallbacks when certain libraries are restricted/corrupted/disabled -try: - from ansi2html import Ansi2HTMLConverter - ansi_converter = Ansi2HTMLConverter(dark_bg=True, inline=True) - use_server_conversion = True -except ImportError: - # ansi2html not installed – fallback to raw ANSI - use_server_conversion = False - logging.warning("ansi2html not installed; client will handle ANSI conversion.") - -# --- HTML TEMPLATE (with 4-space indented JavaScript) --- -HTML_TEMPLATE = r""" - - - - - Minecraft - Java Edition - - - -
- -
- - Ambient Mode -
- -
- -
-
-
-
Minecraft Java 1.21.11
-
-
- Flask: Connected -
- {% if error %}
{{ error }}
{% endif %} - -
-
- - -
- - - - -
-
- -
-
SYSTEM_CORE // LIVE_LOGS
-
-
-
- -
PORTABLE_MC // APEX_V6.5
- - - - - - - - - - +#!/usr/bin/env python3 +""" +Cross‑platform bootstrap script for embedded Python 3.14 (Windows only). +Attempts to download the native portablemc binary, falls back to pip. +Run with any Python (e.g., system 3.11) to set up and launch the launcher. """ -# --- ROUTES --- -@socketio.on('connect') -def handle_connect(): - global connected_clients - with clients_lock: - connected_clients += 1 - client_id = request.sid - print(f'Client connected: {client_id} (Total: {connected_clients})') - emit('status', {'core': 'online', 'minecraft': 'checking'}) - -@socketio.on('disconnect') -def handle_disconnect(): - global connected_clients - with clients_lock: - connected_clients -= 1 - client_id = request.sid - print(f'Client disconnected: {client_id} (Total: {connected_clients})') - -@socketio.on('ping_minecraft') -def handle_ping_minecraft(): - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1.5) - s.connect((SERVER_IP, 25565)) - s.close() - emit('minecraft_status', {'online': True}) - except Exception: - emit('minecraft_status', {'online': False}) - -@app.route("/ping") -def ping(): - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1.5) - s.connect((SERVER_IP, 25565)) - s.close() - return jsonify(online=True) - except Exception: - return jsonify(online=False), 200 - -@app.route("/stream") -def stream(): - # --- PORTABLEMC AVAILABILITY CHECK --- - def is_portablemc_available(): - if shutil.which("portablemc"): - return True - return importlib.util.find_spec("portablemc") is not None - - if not is_portablemc_available(): - def error_gen(): - lines = [ - "\x1b[91m[!] PORTABLEMC NOT FOUND\x1b[0m", - "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m" - ] - for line in lines: - payload = json.dumps({'type': 'ansi', 'content': line}) - yield f"data: {payload}\n\n" - yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") - - # --- GET USER INPUT --- - user = request.args.get("username", "Player").strip() - password = request.args.get("password", "") - - # --- VALIDATIONS (JSON wrapped) --- - if not user: - def error_gen(): - payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"}) - yield f"data: {payload}\n\n" - yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") - - if not VALID_USERNAME_REGEX.match(user): - def error_gen(): - lines = [ - "\x1b[91m[!] INVALID USERNAME\x1b[0m", - "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m" - ] - for line in lines: - payload = json.dumps({'type': 'ansi', 'content': line}) - yield f"data: {payload}\n\n" - yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") - - user_lower = user.lower() - forbidden_lower = [name.lower() for name in FORBIDDEN_LIST] - if user_lower in forbidden_lower and password != PASS_KEY: - def error_gen(): - payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m"}) - yield f"data: {payload}\n\n" - yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") - - # --- PREVENT MULTIPLE LAUNCHES (thread-safe) --- - with processes_lock: - if user in active_processes and active_processes[user].poll() is None: - lines = [ - "\x1b[91m[!] CORE BUSY\x1b[0m", - "\x1b[93mAnother Minecraft instance is already running.\x1b[0m", - "\x1b[90mPlease close the game before launching again.\x1b[0m" - ] - def error_gen(): - for line in lines: - payload = json.dumps({'type': 'ansi', 'content': line}) - yield f"data: {payload}\n\n" - yield "data: CLOSE\n\n" - return Response(error_gen(), mimetype="text/event-stream") - if user in active_processes: - del active_processes[user] - - # --- BUILD COMMAND (with binary vs module adaptation) --- - using_binary = os.environ.get("PORTABLEMC_METHOD") == "binary" - - # Determine launcher command - if shutil.which("portablemc"): - launcher_cmd = ["portablemc"] - else: - launcher_cmd = [sys.executable, "-m", "portablemc"] - - # Global arguments (before 'start') - global_args = ["--main-dir", "."] - if not using_binary: - # Python module supports these extras - global_args.extend(["--timeout", "60", "--output", "human-color"]) +import os +import sys +import ssl +import subprocess +import urllib.request +import zipfile +import tarfile +import shutil +import platform +from pathlib import Path - # Start‑specific arguments - start_args = [] - if using_binary: - # Binary uses --join-server - start_args.extend(["--join-server", SERVER_IP]) +# --- Windows-only guard --- +if platform.system().lower() != 'windows': + print("❌ This bootstrap script currently only supports Windows.") + sys.exit(1) + +# --- Configuration --- +EMBEDDED_DIR = Path(__file__).parent / "python" +EMBEDDED_PYTHON = EMBEDDED_DIR / "python.exe" +PYTHON_VERSION = "3.14.3" +PYTHON_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip" +GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" +BASE_PACKAGES = ["flask", "flask-socketio", "psutil", "ansi2html"] +PORTABLEMC_BIN_DIR = Path(__file__).parent / "portablemc_bin" + +# --- OS and architecture detection (for portablemc binary) --- +SYSTEM = platform.system().lower() +MACHINE = platform.machine().lower() + +ARCH_MAP = { + 'x86_64': 'x86_64', + 'amd64': 'x86_64', + 'i686': 'i686', + 'i386': 'i686', + 'aarch64': 'aarch64', + 'arm64': 'aarch64', + 'armv7l': 'arm-gnueabihf', + 'arm': 'arm-gnueabihf', +} + +OS_MAP = { + 'windows': 'windows', + 'linux': 'linux', + 'darwin': 'macos', +} + +def get_portablemc_url(): + """Return the download URL for the native portablemc binary, or None.""" + os_name = OS_MAP.get(SYSTEM) + if not os_name: + print(f"⚠️ Unsupported OS: {SYSTEM}") + return None + arch = ARCH_MAP.get(MACHINE, 'x86_64') + if os_name == 'macos': + arch = 'aarch64' if arch == 'aarch64' else 'x86_64' + if os_name == 'linux' and arch not in ('arm-gnueabihf',): + arch += '-gnu' + base = "https://github.com/mindstorm38/portablemc/releases/download/v5.0.2/" + if os_name == 'windows': + ext = "zip" + filename = f"portablemc-5.0.2-{os_name}-{arch}-msvc.{ext}" else: - # Python module uses --server - start_args.extend(["--server", SERVER_IP]) - - # JVM arguments - if using_binary: - # Binary expects --jvm-arg=OPT1,OPT2,OPT3 - jvm_opts_combined = ",".join(JVM_OPTS.split()) - start_args.append(f"--jvm-arg={jvm_opts_combined}") + ext = "tar.gz" + filename = f"portablemc-5.0.2-{os_name}-{arch}.{ext}" + return base + filename + +# --- Helper functions --- +def ensure_embedded_python(): + if EMBEDDED_PYTHON.exists(): + print("✅ Embedded Python already exists.") + return True + print("📥 Downloading embedded Python...") + try: + urllib.request.urlretrieve(PYTHON_URL, "python-embed.zip") + except Exception as e: + print(f"❌ Failed to download Python: {e}") + return False + print("📦 Extracting...") + with zipfile.ZipFile("python-embed.zip", "r") as zip_ref: + zip_ref.extractall(EMBEDDED_DIR) + os.remove("python-embed.zip") + print("✅ Embedded Python ready.") + return True + +def fix_pth_file(): + pth_files = list(EMBEDDED_DIR.glob("*._pth")) + if not pth_files: + print("❌ No ._pth file found; cannot enable site-packages.") + return False + pth = pth_files[0] + with open(pth, "r") as f: + content = f.read() + if "import site" in content and "#import site" in content: + content = content.replace("#import site", "import site") + with open(pth, "w") as f: + f.write(content) + print("✅ Enabled site-packages in ._pth file.") else: - # Python module accepts a single --jvm-args string - start_args.extend(["--jvm-args", JVM_OPTS]) - - # Version (fabric:) - start_args.append("fabric:") - - # Username - start_args.extend(["-u", user]) - - # Custom Java path (supported by both) - java_exe = "java.exe" if os.name == "nt" else "java" - java_bin = Path.cwd() / "jvm" / "java-runtime-delta" / "bin" / java_exe - if java_bin.exists(): - # Insert --jvm and its path right after the server arguments - # Current start_args: [server args, jvm args, version, -u user] - # We want to insert before the version, so after the server args. - # We'll insert at position 2 (after the two server args) - start_args[2:2] = ["--jvm", str(java_bin)] - - # Full command - cmd = launcher_cmd + global_args + ["start"] + start_args - - # --- DISCONNECT DETECTION --- - closed_event = threading.Event() - progress_re = re.compile(r"(\d+/\d+)") - - def generate(): - process = None - got_generator_exit = False - try: - with processes_lock: - if user in active_processes and active_processes[user].poll() is None: - busy_payload = json.dumps({'type': 'ansi', 'content': '\x1b[91m[!] CORE BUSY\x1b[0m'}) - yield f"data: {busy_payload}\n\n" - yield "data: CLOSE\n\n" - return - - startupinfo = None - if os.name == 'nt': - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - startupinfo.wShowWindow = subprocess.SW_HIDE - - # SAFE: shell=False, all arguments are separate list elements. - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - shell=False, - text=True, - encoding="utf-8", - errors="replace", - bufsize=1, - creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, - startupinfo=startupinfo - ) - active_processes[user] = process - logging.info(f"Started process for {user} with PID {process.pid}") - - last_progress = "" - last_send_time = time.perf_counter() - update_interval = 0.15 - ok_reached = False - - while True: - if closed_event.is_set(): - disc_msg = json.dumps({'type': 'ansi', 'content': "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m"}) - try: - yield f"data: {disc_msg}\n\n" - except (BrokenPipeError, OSError, GeneratorExit): - pass - break - - line = process.stdout.readline() - if not line: - if process.poll() is not None: - break - continue - - raw_line = line.rstrip('\n') - now = time.perf_counter() + print("ℹ️ site-packages already enabled.") + return True - if not ok_reached and "[ OK ]" in raw_line: - ok_reached = True +def download_get_pip(): + pip_script = EMBEDDED_DIR / "get-pip.py" + if pip_script.exists(): + print("✅ get-pip.py already present.") + return pip_script - progress_match = progress_re.search(raw_line) - - if use_server_conversion and ansi_converter: - try: - safe_line = escape_html(raw_line) - html_line = ansi_converter.convert(safe_line, full=False).strip() - payload = json.dumps({'type': 'html', 'content': html_line}) - except Exception as e: - logging.error(f"Conversion failed: {e}, sending raw ANSI") - payload = json.dumps({'type': 'ansi', 'content': raw_line}) - else: - payload = json.dumps({'type': 'ansi', 'content': raw_line}) - - try: - if progress_match and not ok_reached: - current_file = progress_match.group(1) - if current_file != last_progress and (now - last_send_time) > update_interval: - yield f"data: {payload}\n\n" - last_progress = current_file - last_send_time = now - else: - yield f"data: {payload}\n\n" - except (BrokenPipeError, OSError, GeneratorExit) as e: - if isinstance(e, GeneratorExit): - got_generator_exit = True - break - - except FileNotFoundError as e: - if not closed_event.is_set(): - try: - payload = json.dumps({'type': 'ansi', 'content': f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m"}) - yield f"data: {payload}\n\n" - except Exception: - pass - except Exception as e: - if not closed_event.is_set(): - try: - payload = json.dumps({'type': 'ansi', 'content': f"[SYSTEM ERROR] {str(e)}"}) - yield f"data: {payload}\n\n" - except Exception: - pass - finally: - if process: - process.stdout.close() - kill_process_tree(process) - with processes_lock: - if user in active_processes: - del active_processes[user] - logging.info(f"Removed process entry for {user}") - - if not got_generator_exit: - try: - ended_msg = json.dumps({'type': 'ansi', 'content': "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m"}) - tip_msg = json.dumps({'type': 'ansi', 'content': "\x1b[34m[TIP] Click the console to return to login.\x1b[0m"}) - yield f"data: {ended_msg}\n\n" - yield f"data: {tip_msg}\n\n" - yield "data: CLOSE\n\n" - except GeneratorExit: - pass - except Exception: - pass - - response = Response(generate(), mimetype="text/event-stream") - response.call_on_close(closed_event.set) - return response - -@app.route("/") -def home(): - return render_template_string(HTML_TEMPLATE, forbidden_list=FORBIDDEN_LIST) - -def kill_process_tree(proc): - """Kill a process and all its children. Falls back to simple kill if psutil missing.""" - if proc.poll() is not None: - logging.debug(f"Process {proc.pid} already dead") - return - if PSUTIL_AVAILABLE: + print("📥 Downloading get-pip.py (ignoring SSL cert for this request)...") + try: + # Create an unverified SSL context to bypass the certificate error + ssl_context = ssl._create_unverified_context() + with urllib.request.urlopen(GET_PIP_URL, context=ssl_context) as response: + with open(pip_script, 'wb') as out_file: + out_file.write(response.read()) + print("✅ get-pip.py downloaded successfully.") + except Exception as e: + print(f"❌ Failed to download get-pip.py: {e}") + return None + return pip_script + +def run_pip_command(args, isolated=True): + env = os.environ.copy() + if isolated: + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONPATH"] = "" + cmd = [str(EMBEDDED_PYTHON)] + args + # SAFE: args are static or come from trusted BASE_PACKAGES + result = subprocess.run(cmd, env=env, capture_output=True, text=True) + if result.returncode != 0: + print(f"❌ Pip command failed: {' '.join(args)}") + print(result.stderr) + return False + print(result.stdout) + return True + +def install_pip(): + pip_script = download_get_pip() + if not pip_script: + return False + env = os.environ.copy() + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONPATH"] = "" + cmd = [str(EMBEDDED_PYTHON), str(pip_script), + "--trusted-host=files.pythonhosted.org", + "--trusted-host=pypi.org"] + result = subprocess.run(cmd, env=env, capture_output=True, text=True) + if result.returncode != 0: + print("❌ Failed to install pip.") + print(result.stderr) + return False + print("✅ pip installed.") + return True + +def install_base_packages(): + print("📦 Installing base packages...") + if not run_pip_command(["-m", "pip", "install", "--upgrade", "pip"], isolated=True): + print("⚠️ Pip upgrade failed, continuing anyway.") + for pkg in BASE_PACKAGES: + print(f" Installing {pkg}...") + if not run_pip_command(["-m", "pip", "install", pkg], isolated=True): + print(f"❌ Failed to install {pkg}.") + return False + return True + +def download_portablemc_binary(): + url = get_portablemc_url() + if not url: + return False + print(f"📥 Downloading portablemc from {url}...") + archive_path = Path(__file__).parent / "portablemc_download" + try: + urllib.request.urlretrieve(url, archive_path) + except Exception as e: + print(f"❌ Failed to download: {e}") + return False + PORTABLEMC_BIN_DIR.mkdir(exist_ok=True) + try: + if url.endswith(".zip"): + with zipfile.ZipFile(archive_path, "r") as zip_ref: + zip_ref.extractall(PORTABLEMC_BIN_DIR) + else: + with tarfile.open(archive_path, "r:gz") as tar_ref: + tar_ref.extractall(PORTABLEMC_BIN_DIR) + except Exception as e: + print(f"❌ Failed to extract archive: {e}") + return False + finally: + archive_path.unlink(missing_ok=True) + # Move all extracted files to the root of PORTABLEMC_BIN_DIR + for item in PORTABLEMC_BIN_DIR.iterdir(): + if item.is_dir(): + for sub in item.iterdir(): + sub.rename(PORTABLEMC_BIN_DIR / sub.name) + item.rmdir() + print(f"✅ portablemc binary extracted to {PORTABLEMC_BIN_DIR}") + return True + +def test_portablemc(): + """Check if portablemc is available (binary or module). Returns 'binary' or 'module' or None.""" + # Try binary first + exe_name = "portablemc.exe" if SYSTEM == "windows" else "portablemc" + binary_path = PORTABLEMC_BIN_DIR / exe_name + if binary_path.exists(): + if SYSTEM != "windows": + binary_path.chmod(binary_path.stat().st_mode | 0o111) + env = os.environ.copy() + env["PATH"] = str(PORTABLEMC_BIN_DIR) + os.pathsep + env.get("PATH", "") try: - parent = psutil.Process(proc.pid) - children = parent.children(recursive=True) - for child in children: - try: - child.kill() - except psutil.NoSuchProcess: - pass - parent.kill() - logging.info(f"Killed process tree for PID {proc.pid}") - except psutil.NoSuchProcess: - logging.debug(f"Process {proc.pid} already gone") + result = subprocess.run([str(binary_path), "--help"], env=env, + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print("✅ portablemc binary works.") + return "binary" + except (subprocess.TimeoutExpired, FileNotFoundError): + print("⏱️ portablemc binary check timed out or not found, trying module.") + + # Try module + env = os.environ.copy() + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONPATH"] = "" + cmd = [str(EMBEDDED_PYTHON), "-m", "portablemc", "--help"] + try: + result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print("✅ portablemc module works.") + return "module" + except subprocess.TimeoutExpired: + print("⏱️ portablemc module check timed out, assuming not available.") + return None + +def ensure_portablemc(): + """Make portablemc available – try binary, fallback to pip. Returns method string or None.""" + method = test_portablemc() + if method: + return method + if download_portablemc_binary(): + method = test_portablemc() + if method: + return method + print("⚠️ Binary download failed, falling back to pip.") + print("📦 Installing portablemc via pip...") + if run_pip_command(["-m", "pip", "install", "portablemc"], isolated=True): + method = test_portablemc() + if method: + return method + return None + +def launch_launcher(method): + launcher_script = Path(__file__).parent / "portablemc.py" + if not launcher_script.exists(): + print("❌ portablemc.py not found in the same directory.") + return False + + env = os.environ.copy() + paths = [str(EMBEDDED_DIR), str(EMBEDDED_DIR / "Scripts"), str(PORTABLEMC_BIN_DIR)] + env["PATH"] = os.pathsep.join(paths) + os.pathsep + env.get("PATH", "") + env["__compat_layer"] = "runasinvoker" + env["PYTHONHOME"] = str(EMBEDDED_DIR) + env["CLICOLOR_FORCE"] = "1" + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONPATH"] = "" + env["PORTABLEMC_METHOD"] = method + + cmd = [str(EMBEDDED_PYTHON), str(launcher_script)] + print(f"🚀 Launching: {' '.join(cmd)}") + try: + subprocess.run(cmd, env=env, check=True) + except subprocess.CalledProcessError as e: + print(f"❌ Launcher exited with error: {e}") + return False + except KeyboardInterrupt: + print("⏹️ Interrupted by user.") + return True + +def main(): + print("=== Embedded Python Bootstrap (Windows only) ===") + if not ensure_embedded_python(): + sys.exit(1) + if not fix_pth_file(): + sys.exit(1) + # Check pip + env_check = os.environ.copy() + env_check["PYTHONNOUSERSITE"] = "1" + env_check["PYTHONPATH"] = "" + pip_check = subprocess.run([str(EMBEDDED_PYTHON), "-m", "pip", "--version"], + env=env_check, capture_output=True, text=True) + if pip_check.returncode != 0: + print("📦 pip not found, installing...") + if not install_pip(): + sys.exit(1) else: - try: - proc.terminate() - proc.wait(timeout=2) - except subprocess.TimeoutExpired: - proc.kill() - logging.info(f"Terminated process PID {proc.pid} (psutil unavailable)") - -def graceful_shutdown(sig, frame): - logging.info("SHUTTING DOWN CORE...") - with processes_lock: - for user, proc in list(active_processes.items()): - kill_process_tree(proc) - sys.exit(0) - -signal.signal(signal.SIGINT, graceful_shutdown) + print(f"✅ pip already installed: {pip_check.stdout.strip()}") + if not install_base_packages(): + sys.exit(1) + method = ensure_portablemc() + if not method: + sys.exit(1) + print("✅ Setup complete. Launching portablemc.py...") + launch_launcher(method) if __name__ == "__main__": - try: - socketio.run(app, port=5000, debug=False, allow_unsafe_werkzeug=True) - except KeyboardInterrupt: - graceful_shutdown(None, None) + main() From 03af0a81a076313ad9392e373cf2a0cb6a6e13dc Mon Sep 17 00:00:00 2001 From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:01:32 +0000 Subject: [PATCH 7/9] Update portablemc.py --- portablemc.py | 1533 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 1235 insertions(+), 298 deletions(-) diff --git a/portablemc.py b/portablemc.py index 10cae83..035b83c 100644 --- a/portablemc.py +++ b/portablemc.py @@ -1,309 +1,1246 @@ -#!/usr/bin/env python3 -""" -Cross‑platform bootstrap script for embedded Python 3.14 (Windows only). -Attempts to download the native portablemc binary, falls back to pip. -Run with any Python (e.g., system 3.11) to set up and launch the launcher. -""" - -import os -import sys -import ssl +from pathlib import Path +from flask import Flask, request, render_template_string, jsonify, Response +from flask_socketio import SocketIO, emit import subprocess -import urllib.request -import zipfile -import tarfile +import sys +import socket +import time +import re +import signal import shutil -import platform -from pathlib import Path +import threading +import logging +import traceback +import os +import json +import importlib.util -# --- Windows-only guard --- -if platform.system().lower() != 'windows': - print("❌ This bootstrap script currently only supports Windows.") - sys.exit(1) - -# --- Configuration --- -EMBEDDED_DIR = Path(__file__).parent / "python" -EMBEDDED_PYTHON = EMBEDDED_DIR / "python.exe" -PYTHON_VERSION = "3.14.3" -PYTHON_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip" -GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -BASE_PACKAGES = ["flask", "flask-socketio", "psutil", "ansi2html"] -PORTABLEMC_BIN_DIR = Path(__file__).parent / "portablemc_bin" - -# --- OS and architecture detection (for portablemc binary) --- -SYSTEM = platform.system().lower() -MACHINE = platform.machine().lower() - -ARCH_MAP = { - 'x86_64': 'x86_64', - 'amd64': 'x86_64', - 'i686': 'i686', - 'i386': 'i686', - 'aarch64': 'aarch64', - 'arm64': 'aarch64', - 'armv7l': 'arm-gnueabihf', - 'arm': 'arm-gnueabihf', -} - -OS_MAP = { - 'windows': 'windows', - 'linux': 'linux', - 'darwin': 'macos', -} - -def get_portablemc_url(): - """Return the download URL for the native portablemc binary, or None.""" - os_name = OS_MAP.get(SYSTEM) - if not os_name: - print(f"⚠️ Unsupported OS: {SYSTEM}") - return None - arch = ARCH_MAP.get(MACHINE, 'x86_64') - if os_name == 'macos': - arch = 'aarch64' if arch == 'aarch64' else 'x86_64' - if os_name == 'linux' and arch not in ('arm-gnueabihf',): - arch += '-gnu' - base = "https://github.com/mindstorm38/portablemc/releases/download/v5.0.2/" - if os_name == 'windows': - ext = "zip" - filename = f"portablemc-5.0.2-{os_name}-{arch}-msvc.{ext}" - else: - ext = "tar.gz" - filename = f"portablemc-5.0.2-{os_name}-{arch}.{ext}" - return base + filename - -# --- Helper functions --- -def ensure_embedded_python(): - if EMBEDDED_PYTHON.exists(): - print("✅ Embedded Python already exists.") - return True - print("📥 Downloading embedded Python...") - try: - urllib.request.urlretrieve(PYTHON_URL, "python-embed.zip") - except Exception as e: - print(f"❌ Failed to download Python: {e}") - return False - print("📦 Extracting...") - with zipfile.ZipFile("python-embed.zip", "r") as zip_ref: - zip_ref.extractall(EMBEDDED_DIR) - os.remove("python-embed.zip") - print("✅ Embedded Python ready.") - return True - -def fix_pth_file(): - pth_files = list(EMBEDDED_DIR.glob("*._pth")) - if not pth_files: - print("❌ No ._pth file found; cannot enable site-packages.") - return False - pth = pth_files[0] - with open(pth, "r") as f: - content = f.read() - if "import site" in content and "#import site" in content: - content = content.replace("#import site", "import site") - with open(pth, "w") as f: - f.write(content) - print("✅ Enabled site-packages in ._pth file.") - else: - print("ℹ️ site-packages already enabled.") - return True +app = Flask(__name__) +socketio = SocketIO(app, cors_allowed_origins="*") -def download_get_pip(): - pip_script = EMBEDDED_DIR / "get-pip.py" - if pip_script.exists(): - print("✅ get-pip.py already present.") - return pip_script +def escape_html(s): + """Escape only &, <, > for safe innerHTML – leaves quotes and apostrophes untouched.""" + return s.replace('&', '&').replace('<', '<').replace('>', '>') - print("📥 Downloading get-pip.py (ignoring SSL cert for this request)...") - try: - # Create an unverified SSL context to bypass the certificate error - ssl_context = ssl._create_unverified_context() - with urllib.request.urlopen(GET_PIP_URL, context=ssl_context) as response: - with open(pip_script, 'wb') as out_file: - out_file.write(response.read()) - print("✅ get-pip.py downloaded successfully.") - except Exception as e: - print(f"❌ Failed to download get-pip.py: {e}") - return None - return pip_script - -def run_pip_command(args, isolated=True): - env = os.environ.copy() - if isolated: - env["PYTHONNOUSERSITE"] = "1" - env["PYTHONPATH"] = "" - cmd = [str(EMBEDDED_PYTHON)] + args - # SAFE: args are static or come from trusted BASE_PACKAGES - result = subprocess.run(cmd, env=env, capture_output=True, text=True) - if result.returncode != 0: - print(f"❌ Pip command failed: {' '.join(args)}") - print(result.stderr) - return False - print(result.stdout) - return True - -def install_pip(): - pip_script = download_get_pip() - if not pip_script: - return False - env = os.environ.copy() - env["PYTHONNOUSERSITE"] = "1" - env["PYTHONPATH"] = "" - cmd = [str(EMBEDDED_PYTHON), str(pip_script), - "--trusted-host=files.pythonhosted.org", - "--trusted-host=pypi.org"] - result = subprocess.run(cmd, env=env, capture_output=True, text=True) - if result.returncode != 0: - print("❌ Failed to install pip.") - print(result.stderr) - return False - print("✅ pip installed.") - return True - -def install_base_packages(): - print("📦 Installing base packages...") - if not run_pip_command(["-m", "pip", "install", "--upgrade", "pip"], isolated=True): - print("⚠️ Pip upgrade failed, continuing anyway.") - for pkg in BASE_PACKAGES: - print(f" Installing {pkg}...") - if not run_pip_command(["-m", "pip", "install", pkg], isolated=True): - print(f"❌ Failed to install {pkg}.") - return False - return True - -def download_portablemc_binary(): - url = get_portablemc_url() - if not url: - return False - print(f"📥 Downloading portablemc from {url}...") - archive_path = Path(__file__).parent / "portablemc_download" +# --- CONFIGURATION --- +VALID_USERNAME_REGEX = re.compile(r'^[a-zA-Z0-9_]{3,16}$') +FORBIDDEN_LIST = ["CubeUniform840", "Admin", "Owner"] +PASS_KEY = "1234" +SERVER_IP = "77.103.184.72" +# SERVER_IP = "eu.chickencraft.nl" +JVM_OPTS = "-Xmx3G -Xms3G -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1NewSizePercent=20 -XX:G1ReservePercent=20 -XX:MaxGCPauseMillis=50 -XX:G1HeapRegionSize=32M -XX:+AlwaysPreTouch -XX:+ParallelRefProcEnabled -XX:+DisableExplicitGC" + +# Thread-safe process tracking +active_processes = {} +processes_lock = threading.Lock() + +# Track connected clients +connected_clients = 0 +clients_lock = threading.Lock() +logging.basicConfig(level=logging.INFO) + +# Optional psutil for process tree killing +try: + import psutil + PSUTIL_AVAILABLE = True +except ImportError: + psutil = None + PSUTIL_AVAILABLE = False + logging.warning( + "psutil not installed; process tree killing is limited. " + "Install psutil for full cleanup." + ) + +# Fallbacks when certain libraries are restricted/corrupted/disabled +try: + from ansi2html import Ansi2HTMLConverter + ansi_converter = Ansi2HTMLConverter(dark_bg=True, inline=True) + use_server_conversion = True +except ImportError: + # ansi2html not installed – fallback to raw ANSI + use_server_conversion = False + logging.warning("ansi2html not installed; client will handle ANSI conversion.") + +# --- HTML TEMPLATE (with 4-space indented JavaScript) --- +HTML_TEMPLATE = r""" + + + + + Minecraft - Java Edition + + + +
+ +
+ + Ambient Mode +
+ +
+ +
+
+
+
Minecraft Java 1.21.11
+
+
+ Flask: Connected +
+ {% if error %}
{{ error }}
{% endif %} + +
+
+ + +
+ + + + +
+
+ +
+
SYSTEM_CORE // LIVE_LOGS
+
+
+
+ +
PORTABLE_MC // APEX_V6.5
+ + + + + + + + + + +""" + +# --- ROUTES --- +@socketio.on('connect') +def handle_connect(): + global connected_clients + with clients_lock: + connected_clients += 1 + client_id = request.sid + print(f'Client connected: {client_id} (Total: {connected_clients})') + emit('status', {'core': 'online', 'minecraft': 'checking'}) + +@socketio.on('disconnect') +def handle_disconnect(): + global connected_clients + with clients_lock: + connected_clients -= 1 + client_id = request.sid + print(f'Client disconnected: {client_id} (Total: {connected_clients})') + +@socketio.on('ping_minecraft') +def handle_ping_minecraft(): try: - urllib.request.urlretrieve(url, archive_path) - except Exception as e: - print(f"❌ Failed to download: {e}") - return False - PORTABLEMC_BIN_DIR.mkdir(exist_ok=True) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1.5) + s.connect((SERVER_IP, 25565)) + s.close() + emit('minecraft_status', {'online': True}) + except Exception: + emit('minecraft_status', {'online': False}) + +@app.route("/ping") +def ping(): try: - if url.endswith(".zip"): - with zipfile.ZipFile(archive_path, "r") as zip_ref: - zip_ref.extractall(PORTABLEMC_BIN_DIR) - else: - with tarfile.open(archive_path, "r:gz") as tar_ref: - tar_ref.extractall(PORTABLEMC_BIN_DIR) - except Exception as e: - print(f"❌ Failed to extract archive: {e}") - return False - finally: - archive_path.unlink(missing_ok=True) - # Move all extracted files to the root of PORTABLEMC_BIN_DIR - for item in PORTABLEMC_BIN_DIR.iterdir(): - if item.is_dir(): - for sub in item.iterdir(): - sub.rename(PORTABLEMC_BIN_DIR / sub.name) - item.rmdir() - print(f"✅ portablemc binary extracted to {PORTABLEMC_BIN_DIR}") - return True - -def test_portablemc(): - """Check if portablemc is available (binary or module). Returns 'binary' or 'module' or None.""" - # Try binary first - exe_name = "portablemc.exe" if SYSTEM == "windows" else "portablemc" - binary_path = PORTABLEMC_BIN_DIR / exe_name - if binary_path.exists(): - if SYSTEM != "windows": - binary_path.chmod(binary_path.stat().st_mode | 0o111) - env = os.environ.copy() - env["PATH"] = str(PORTABLEMC_BIN_DIR) + os.pathsep + env.get("PATH", "") + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1.5) + s.connect((SERVER_IP, 25565)) + s.close() + return jsonify(online=True) + except Exception: + return jsonify(online=False), 200 + +@app.route("/stream") +def stream(): + # --- PORTABLEMC AVAILABILITY CHECK --- + def is_portablemc_available(): + if shutil.which("portablemc"): + return True + return importlib.util.find_spec("portablemc") is not None + + if not is_portablemc_available(): + def error_gen(): + lines = [ + "\x1b[91m[!] PORTABLEMC NOT FOUND\x1b[0m", + "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m" + ] + for line in lines: + payload = json.dumps({'type': 'ansi', 'content': line}) + yield f"data: {payload}\n\n" + yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") + + # --- GET USER INPUT --- + user = request.args.get("username", "Player").strip() + password = request.args.get("password", "") + + # --- VALIDATIONS (JSON wrapped) --- + if not user: + def error_gen(): + payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"}) + yield f"data: {payload}\n\n" + yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") + + if not VALID_USERNAME_REGEX.match(user): + def error_gen(): + lines = [ + "\x1b[91m[!] INVALID USERNAME\x1b[0m", + "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m" + ] + for line in lines: + payload = json.dumps({'type': 'ansi', 'content': line}) + yield f"data: {payload}\n\n" + yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") + + user_lower = user.lower() + forbidden_lower = [name.lower() for name in FORBIDDEN_LIST] + if user_lower in forbidden_lower and password != PASS_KEY: + def error_gen(): + payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m"}) + yield f"data: {payload}\n\n" + yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") + + # --- PREVENT MULTIPLE LAUNCHES (thread-safe) --- + with processes_lock: + if user in active_processes and active_processes[user].poll() is None: + lines = [ + "\x1b[91m[!] CORE BUSY\x1b[0m", + "\x1b[93mAnother Minecraft instance is already running.\x1b[0m", + "\x1b[90mPlease close the game before launching again.\x1b[0m" + ] + def error_gen(): + for line in lines: + payload = json.dumps({'type': 'ansi', 'content': line}) + yield f"data: {payload}\n\n" + yield "data: CLOSE\n\n" + return Response(error_gen(), mimetype="text/event-stream") + if user in active_processes: + del active_processes[user] + + # --- BUILD COMMAND (with binary vs module adaptation) --- + using_binary = os.environ.get("PORTABLEMC_METHOD") == "binary" + + # Launcher command – tied directly to the method, not PATH + if using_binary: + launcher_cmd = ["portablemc"] + else: + launcher_cmd = [sys.executable, "-m", "portablemc"] + + # Global arguments (before 'start') + global_args = ["--main-dir", "."] + if not using_binary: + # Python module supports these extras + global_args.extend(["--timeout", "60", "--output", "human-color"]) + + # Start‑specific arguments + start_args = [] + if using_binary: + # Binary uses --join-server + start_args.extend(["--join-server", SERVER_IP]) + else: + # Python module uses --server + start_args.extend(["--server", SERVER_IP]) + + # JVM arguments + if using_binary: + # Binary expects --jvm-arg=OPT1,OPT2,OPT3 + jvm_opts_combined = ",".join(JVM_OPTS.split()) + start_args.append(f"--jvm-arg={jvm_opts_combined}") + else: + # Python module accepts a single --jvm-args string + start_args.extend(["--jvm-args", JVM_OPTS]) + + # Version (fabric:) + start_args.append("fabric:") + + # Username + start_args.extend(["-u", user]) + + # Custom Java path (supported by both) + java_exe = "java.exe" if os.name == "nt" else "java" + java_bin = Path.cwd() / "jvm" / "java-runtime-delta" / "bin" / java_exe + if java_bin.exists(): + # Insert --jvm and its path right after the server arguments + # Current start_args: [server args, jvm args, version, -u user] + # We want to insert before the version, so after the server args. + # Server args are two elements, so index 2. + start_args[2:2] = ["--jvm", str(java_bin)] + + # Full command + cmd = launcher_cmd + global_args + ["start"] + start_args + + # --- DISCONNECT DETECTION --- + closed_event = threading.Event() + progress_re = re.compile(r"(\d+/\d+)") + + def generate(): + process = None + got_generator_exit = False try: - result = subprocess.run([str(binary_path), "--help"], env=env, - capture_output=True, text=True, timeout=5) - if result.returncode == 0: - print("✅ portablemc binary works.") - return "binary" - except (subprocess.TimeoutExpired, FileNotFoundError): - print("⏱️ portablemc binary check timed out or not found, trying module.") - - # Try module - env = os.environ.copy() - env["PYTHONNOUSERSITE"] = "1" - env["PYTHONPATH"] = "" - cmd = [str(EMBEDDED_PYTHON), "-m", "portablemc", "--help"] - try: - result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=5) - if result.returncode == 0: - print("✅ portablemc module works.") - return "module" - except subprocess.TimeoutExpired: - print("⏱️ portablemc module check timed out, assuming not available.") - return None - -def ensure_portablemc(): - """Make portablemc available – try binary, fallback to pip. Returns method string or None.""" - method = test_portablemc() - if method: - return method - if download_portablemc_binary(): - method = test_portablemc() - if method: - return method - print("⚠️ Binary download failed, falling back to pip.") - print("📦 Installing portablemc via pip...") - if run_pip_command(["-m", "pip", "install", "portablemc"], isolated=True): - method = test_portablemc() - if method: - return method - return None - -def launch_launcher(method): - launcher_script = Path(__file__).parent / "portablemc.py" - if not launcher_script.exists(): - print("❌ portablemc.py not found in the same directory.") - return False - - env = os.environ.copy() - paths = [str(EMBEDDED_DIR), str(EMBEDDED_DIR / "Scripts"), str(PORTABLEMC_BIN_DIR)] - env["PATH"] = os.pathsep.join(paths) + os.pathsep + env.get("PATH", "") - env["__compat_layer"] = "runasinvoker" - env["PYTHONHOME"] = str(EMBEDDED_DIR) - env["CLICOLOR_FORCE"] = "1" - env["PYTHONNOUSERSITE"] = "1" - env["PYTHONPATH"] = "" - env["PORTABLEMC_METHOD"] = method - - cmd = [str(EMBEDDED_PYTHON), str(launcher_script)] - print(f"🚀 Launching: {' '.join(cmd)}") - try: - subprocess.run(cmd, env=env, check=True) - except subprocess.CalledProcessError as e: - print(f"❌ Launcher exited with error: {e}") - return False - except KeyboardInterrupt: - print("⏹️ Interrupted by user.") - return True - -def main(): - print("=== Embedded Python Bootstrap (Windows only) ===") - if not ensure_embedded_python(): - sys.exit(1) - if not fix_pth_file(): - sys.exit(1) - # Check pip - env_check = os.environ.copy() - env_check["PYTHONNOUSERSITE"] = "1" - env_check["PYTHONPATH"] = "" - pip_check = subprocess.run([str(EMBEDDED_PYTHON), "-m", "pip", "--version"], - env=env_check, capture_output=True, text=True) - if pip_check.returncode != 0: - print("📦 pip not found, installing...") - if not install_pip(): - sys.exit(1) + with processes_lock: + if user in active_processes and active_processes[user].poll() is None: + busy_payload = json.dumps({'type': 'ansi', 'content': '\x1b[91m[!] CORE BUSY\x1b[0m'}) + yield f"data: {busy_payload}\n\n" + yield "data: CLOSE\n\n" + return + + startupinfo = None + if os.name == 'nt': + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + + # SAFE: shell=False, all arguments are separate list elements. + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=False, + text=True, + encoding="utf-8", + errors="replace", + bufsize=1, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + startupinfo=startupinfo + ) + active_processes[user] = process + logging.info(f"Started process for {user} with PID {process.pid}") + + last_progress = "" + last_send_time = time.perf_counter() + update_interval = 0.15 + ok_reached = False + + while True: + if closed_event.is_set(): + disc_msg = json.dumps({'type': 'ansi', 'content': "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m"}) + try: + yield f"data: {disc_msg}\n\n" + except (BrokenPipeError, OSError, GeneratorExit): + pass + break + + line = process.stdout.readline() + if not line: + if process.poll() is not None: + break + continue + + raw_line = line.rstrip('\n') + now = time.perf_counter() + + if not ok_reached and "[ OK ]" in raw_line: + ok_reached = True + + progress_match = progress_re.search(raw_line) + + if use_server_conversion and ansi_converter: + try: + safe_line = escape_html(raw_line) + html_line = ansi_converter.convert(safe_line, full=False).strip() + payload = json.dumps({'type': 'html', 'content': html_line}) + except Exception as e: + logging.error(f"Conversion failed: {e}, sending raw ANSI") + payload = json.dumps({'type': 'ansi', 'content': raw_line}) + else: + payload = json.dumps({'type': 'ansi', 'content': raw_line}) + + try: + if progress_match and not ok_reached: + current_file = progress_match.group(1) + if current_file != last_progress and (now - last_send_time) > update_interval: + yield f"data: {payload}\n\n" + last_progress = current_file + last_send_time = now + else: + yield f"data: {payload}\n\n" + except (BrokenPipeError, OSError, GeneratorExit) as e: + if isinstance(e, GeneratorExit): + got_generator_exit = True + break + + except FileNotFoundError as e: + if not closed_event.is_set(): + try: + payload = json.dumps({'type': 'ansi', 'content': f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m"}) + yield f"data: {payload}\n\n" + except Exception: + pass + except Exception as e: + if not closed_event.is_set(): + try: + payload = json.dumps({'type': 'ansi', 'content': f"[SYSTEM ERROR] {str(e)}"}) + yield f"data: {payload}\n\n" + except Exception: + pass + finally: + if process: + process.stdout.close() + kill_process_tree(process) + with processes_lock: + if user in active_processes: + del active_processes[user] + logging.info(f"Removed process entry for {user}") + + if not got_generator_exit: + try: + ended_msg = json.dumps({'type': 'ansi', 'content': "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m"}) + tip_msg = json.dumps({'type': 'ansi', 'content': "\x1b[34m[TIP] Click the console to return to login.\x1b[0m"}) + yield f"data: {ended_msg}\n\n" + yield f"data: {tip_msg}\n\n" + yield "data: CLOSE\n\n" + except GeneratorExit: + pass + except Exception: + pass + + response = Response(generate(), mimetype="text/event-stream") + response.call_on_close(closed_event.set) + return response + +@app.route("/") +def home(): + return render_template_string(HTML_TEMPLATE, forbidden_list=FORBIDDEN_LIST) + +def kill_process_tree(proc): + """Kill a process and all its children. Falls back to simple kill if psutil missing.""" + if proc.poll() is not None: + logging.debug(f"Process {proc.pid} already dead") + return + if PSUTIL_AVAILABLE: + try: + parent = psutil.Process(proc.pid) + children = parent.children(recursive=True) + for child in children: + try: + child.kill() + except psutil.NoSuchProcess: + pass + parent.kill() + logging.info(f"Killed process tree for PID {proc.pid}") + except psutil.NoSuchProcess: + logging.debug(f"Process {proc.pid} already gone") else: - print(f"✅ pip already installed: {pip_check.stdout.strip()}") - if not install_base_packages(): - sys.exit(1) - method = ensure_portablemc() - if not method: - sys.exit(1) - print("✅ Setup complete. Launching portablemc.py...") - launch_launcher(method) + try: + proc.terminate() + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + logging.info(f"Terminated process PID {proc.pid} (psutil unavailable)") + +def graceful_shutdown(sig, frame): + logging.info("SHUTTING DOWN CORE...") + with processes_lock: + for user, proc in list(active_processes.items()): + kill_process_tree(proc) + sys.exit(0) + +signal.signal(signal.SIGINT, graceful_shutdown) if __name__ == "__main__": - main() + try: + socketio.run(app, port=5000, debug=False, allow_unsafe_werkzeug=True) + except KeyboardInterrupt: + graceful_shutdown(None, None) From 747543b3533001236ff64a36172001f9b738b148 Mon Sep 17 00:00:00 2001 From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:01:46 +0000 Subject: [PATCH 8/9] Add support for SSL --- main.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index 0c49e2b..10cae83 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 """ -Cross‑platform bootstrap script for embedded Python 3.14. +Cross‑platform bootstrap script for embedded Python 3.14 (Windows only). Attempts to download the native portablemc binary, falls back to pip. Run with any Python (e.g., system 3.11) to set up and launch the launcher. """ import os import sys +import ssl import subprocess import urllib.request import zipfile @@ -15,6 +16,11 @@ import platform from pathlib import Path +# --- Windows-only guard --- +if platform.system().lower() != 'windows': + print("❌ This bootstrap script currently only supports Windows.") + sys.exit(1) + # --- Configuration --- EMBEDDED_DIR = Path(__file__).parent / "python" EMBEDDED_PYTHON = EMBEDDED_DIR / "python.exe" @@ -24,7 +30,7 @@ BASE_PACKAGES = ["flask", "flask-socketio", "psutil", "ansi2html"] PORTABLEMC_BIN_DIR = Path(__file__).parent / "portablemc_bin" -# --- OS and architecture detection --- +# --- OS and architecture detection (for portablemc binary) --- SYSTEM = platform.system().lower() MACHINE = platform.machine().lower() @@ -105,9 +111,15 @@ def download_get_pip(): if pip_script.exists(): print("✅ get-pip.py already present.") return pip_script - print("📥 Downloading get-pip.py...") + + print("📥 Downloading get-pip.py (ignoring SSL cert for this request)...") try: - urllib.request.urlretrieve(GET_PIP_URL, pip_script) + # Create an unverified SSL context to bypass the certificate error + ssl_context = ssl._create_unverified_context() + with urllib.request.urlopen(GET_PIP_URL, context=ssl_context) as response: + with open(pip_script, 'wb') as out_file: + out_file.write(response.read()) + print("✅ get-pip.py downloaded successfully.") except Exception as e: print(f"❌ Failed to download get-pip.py: {e}") return None @@ -119,6 +131,7 @@ def run_pip_command(args, isolated=True): env["PYTHONNOUSERSITE"] = "1" env["PYTHONPATH"] = "" cmd = [str(EMBEDDED_PYTHON)] + args + # SAFE: args are static or come from trusted BASE_PACKAGES result = subprocess.run(cmd, env=env, capture_output=True, text=True) if result.returncode != 0: print(f"❌ Pip command failed: {' '.join(args)}") @@ -206,17 +219,20 @@ def test_portablemc(): print("✅ portablemc binary works.") return "binary" except (subprocess.TimeoutExpired, FileNotFoundError): - pass + print("⏱️ portablemc binary check timed out or not found, trying module.") + # Try module env = os.environ.copy() env["PYTHONNOUSERSITE"] = "1" env["PYTHONPATH"] = "" cmd = [str(EMBEDDED_PYTHON), "-m", "portablemc", "--help"] - result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=5) - if result.returncode == 0: - print("✅ portablemc module works.") - return "module" - print("❌ portablemc not available.") + try: + result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print("✅ portablemc module works.") + return "module" + except subprocess.TimeoutExpired: + print("⏱️ portablemc module check timed out, assuming not available.") return None def ensure_portablemc(): @@ -264,7 +280,7 @@ def launch_launcher(method): return True def main(): - print("=== Embedded Python Bootstrap (Cross‑platform) ===") + print("=== Embedded Python Bootstrap (Windows only) ===") if not ensure_embedded_python(): sys.exit(1) if not fix_pth_file(): From d731c6d60c684fd9a1ecb33741e1792af7d9d72a Mon Sep 17 00:00:00 2001 From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:52:11 +0000 Subject: [PATCH 9/9] Fix support for SSL --- main.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 10cae83..c9bd6bc 100644 --- a/main.py +++ b/main.py @@ -7,13 +7,13 @@ import os import sys -import ssl import subprocess import urllib.request import zipfile import tarfile import shutil import platform +import ssl from pathlib import Path # --- Windows-only guard --- @@ -27,7 +27,7 @@ PYTHON_VERSION = "3.14.3" PYTHON_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip" GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -BASE_PACKAGES = ["flask", "flask-socketio", "psutil", "ansi2html"] +BASE_PACKAGES = ["flask", "flask-socketio", "psutil", "ansi2html", "certifi"] PORTABLEMC_BIN_DIR = Path(__file__).parent / "portablemc_bin" # --- OS and architecture detection (for portablemc binary) --- @@ -76,12 +76,24 @@ def ensure_embedded_python(): if EMBEDDED_PYTHON.exists(): print("✅ Embedded Python already exists.") return True + print("📥 Downloading embedded Python...") + # Try with verification first try: urllib.request.urlretrieve(PYTHON_URL, "python-embed.zip") except Exception as e: - print(f"❌ Failed to download Python: {e}") - return False + print(f"⚠️ First download attempt failed: {e}") + print("📥 Retrying with SSL verification disabled (insecure)...") + try: + # Create an unverified SSL context + ssl_context = ssl._create_unverified_context() + with urllib.request.urlopen(PYTHON_URL, context=ssl_context) as response: + with open("python-embed.zip", 'wb') as out_file: + out_file.write(response.read()) + except Exception as e2: + print(f"❌ Failed to download Python even with unverified SSL: {e2}") + return False + print("📦 Extracting...") with zipfile.ZipFile("python-embed.zip", "r") as zip_ref: zip_ref.extractall(EMBEDDED_DIR) @@ -106,6 +118,25 @@ def fix_pth_file(): print("ℹ️ site-packages already enabled.") return True +def test_embedded_python(): + """Test if the embedded Python executable can be run.""" + try: + result = subprocess.run([str(EMBEDDED_PYTHON), "--version"], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print(f"✅ Embedded Python runs: {result.stdout.strip()}") + return True + else: + print(f"❌ Embedded Python test failed: {result.stderr}") + return False + except OSError as e: + # Common error: WinError 1260 (group policy block) + print(f"❌ Cannot run embedded Python (blocked by group policy?): {e}") + return False + except Exception as e: + print(f"❌ Embedded Python test error: {e}") + return False + def download_get_pip(): pip_script = EMBEDDED_DIR / "get-pip.py" if pip_script.exists(): @@ -114,7 +145,7 @@ def download_get_pip(): print("📥 Downloading get-pip.py (ignoring SSL cert for this request)...") try: - # Create an unverified SSL context to bypass the certificate error + # Create an unverified SSL context to bypass certificate errors ssl_context = ssl._create_unverified_context() with urllib.request.urlopen(GET_PIP_URL, context=ssl_context) as response: with open(pip_script, 'wb') as out_file: @@ -169,6 +200,21 @@ def install_base_packages(): return False return True +def get_certifi_path(): + """Return the path to certifi's CA bundle, or None if certifi not installed.""" + try: + result = subprocess.run( + [str(EMBEDDED_PYTHON), "-c", "import certifi; print(certifi.where())"], + capture_output=True, text=True, check=True, + env={"PYTHONNOUSERSITE": "1"} # ensure isolation + ) + path = result.stdout.strip() + if path and Path(path).exists(): + return path + except Exception: + pass + return None + def download_portablemc_binary(): url = get_portablemc_url() if not url: @@ -176,10 +222,20 @@ def download_portablemc_binary(): print(f"📥 Downloading portablemc from {url}...") archive_path = Path(__file__).parent / "portablemc_download" try: + # Attempt with verification first urllib.request.urlretrieve(url, archive_path) except Exception as e: - print(f"❌ Failed to download: {e}") - return False + print(f"⚠️ First download attempt failed: {e}") + print("📥 Retrying with SSL verification disabled (insecure)...") + try: + ssl_context = ssl._create_unverified_context() + with urllib.request.urlopen(url, context=ssl_context) as response: + with open(archive_path, 'wb') as out_file: + out_file.write(response.read()) + except Exception as e2: + print(f"❌ Failed to download even with unverified SSL: {e2}") + return False + PORTABLEMC_BIN_DIR.mkdir(exist_ok=True) try: if url.endswith(".zip"): @@ -268,6 +324,11 @@ def launch_launcher(method): env["PYTHONPATH"] = "" env["PORTABLEMC_METHOD"] = method + cert_path = get_certifi_path() + if cert_path: + env["SSL_CERT_FILE"] = cert_path + env["REQUESTS_CA_BUNDLE"] = cert_path + cmd = [str(EMBEDDED_PYTHON), str(launcher_script)] print(f"🚀 Launching: {' '.join(cmd)}") try: @@ -285,6 +346,12 @@ def main(): sys.exit(1) if not fix_pth_file(): sys.exit(1) + if not test_embedded_python(): + print("❌ Embedded Python cannot be executed. Please check group policy or run without embedded isolation.") + print(" You may try to run portablemc.py directly with your system Python after installing dependencies:") + print(" pip install flask flask-socketio psutil ansi2html portablemc") + sys.exit(1) + # Check pip env_check = os.environ.copy() env_check["PYTHONNOUSERSITE"] = "1" @@ -297,11 +364,14 @@ def main(): sys.exit(1) else: print(f"✅ pip already installed: {pip_check.stdout.strip()}") + if not install_base_packages(): sys.exit(1) + method = ensure_portablemc() if not method: sys.exit(1) + print("✅ Setup complete. Launching portablemc.py...") launch_launcher(method)