Spaces:
Build error
Build error
Download app.py from smodusermc/MC: direct link, hf CLI and curl.
- Browser
- Download file 19.9 kB
-
https://huggingface.co/spaces/smodusermc/MC/resolve/main/app.py
- Command line
-
hf download hf://spaces/smodusermc/MC/app.py
-
curl -L -o app.py https://huggingface.co/spaces/smodusermc/MC/resolve/main/app.py
19.9 kB
| import eventlet | |
| eventlet.monkey_patch() | |
| import os | |
| import sys | |
| import json | |
| import threading | |
| import time | |
| from datetime import datetime | |
| from functools import wraps | |
| from pathlib import Path | |
| import subprocess | |
| import requests | |
| from flask import Flask, render_template, request, jsonify, redirect, url_for, session, send_file, Response | |
| from flask_socketio import SocketIO, emit | |
| from werkzeug.security import check_password_hash, generate_password_hash | |
| from config import config | |
| from server_manager import init_server_manager, get_server_manager | |
| # --- IMPORTING DEDICATED MANAGER FILES --- | |
| from console_handler import ConsoleHandler | |
| from player_tracker import PlayerTracker | |
| from mod_manager import ModManager | |
| from file_manager import FileManager | |
| from health_ping import health_pinger | |
| # ----------------------------------------- | |
| # Initialize Flask app | |
| app = Flask(__name__) | |
| app.secret_key = config.SECRET_KEY | |
| app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 | |
| # Initialize SocketIO | |
| socketio = SocketIO(app, cors_allowed_origins="*") | |
| # Initialize managers | |
| server_manager = init_server_manager(socketio) | |
| console_handler = ConsoleHandler(server_manager) | |
| player_tracker = PlayerTracker(server_manager) | |
| mod_manager = ModManager() | |
| file_manager = FileManager() | |
| # Admin password hash | |
| ADMIN_PASSWORD_HASH = generate_password_hash(config.ADMIN_PASSWORD) | |
| # --- GLOBAL PLAYIT VARIABLES --- | |
| PLAYIT_PROCESS = None | |
| PLAYIT_ERROR_LOG = Path(os.getenv('MINECRAFT_SERVER_DIR', '/app/minecraft_server')) / "playit_error.log" | |
| # --- Authentication Decorator --- | |
| def login_required(f): | |
| def decorated_function(*args, **kwargs): | |
| if 'logged_in' not in session or not session['logged_in']: | |
| if request.is_json: | |
| return jsonify({'success': False, 'message': 'Authentication required'}), 401 | |
| return redirect(url_for('login')) | |
| return f(*args, **kwargs) | |
| return decorated_function | |
| # ==================================================================== | |
| # === PLAYIT.GG AGENT FUNCTIONS (Debugging with File Redirection) === | |
| # ==================================================================== | |
| def run_playit_claim_mode(): | |
| """ | |
| Executes 'playit claim' and extracts the claim code. | |
| """ | |
| try: | |
| # Use the simplest command which works with the official static binary. | |
| result = subprocess.run( | |
| ['playit', 'claim'], | |
| capture_output=True, | |
| text=True, | |
| timeout=10 | |
| ) | |
| if result.returncode == 0: | |
| claim_output = result.stdout.strip() | |
| claim_code = 'CODE NOT FOUND' | |
| for line in claim_output.splitlines(): | |
| if 'playit.gg/claim/' in line: | |
| claim_code = line.split('/')[-1].strip() | |
| break | |
| if claim_code == 'CODE NOT FOUND' and claim_output: | |
| claim_code = claim_output.split()[-1] | |
| if claim_code == 'CODE NOT FOUND': | |
| raise ValueError("Could not find a valid claim code in playit output.") | |
| server_manager.log(f"PlayIt Claim code generated: {claim_code}", "success") | |
| start_tunneling(claim_code=claim_code) | |
| else: | |
| error_details = result.stderr.strip() or "No detailed error output." | |
| print("=" * 50) | |
| print(f"PlayIt Claim FAILED. Details:") | |
| print(error_details) | |
| print("=" * 50) | |
| server_manager.log(f"PlayIt claim failed: {error_details}", "error") | |
| start_tunneling(error_message="PlayIt 'claim' failed (See logs for details).") | |
| except Exception as e: | |
| server_manager.log(f"Error during PlayIt claim: {e}", "error") | |
| start_tunneling(error_message=f"PlayIt claim encountered an unexpected error: {type(e).__name__}.") | |
| def check_playit_initial_status(): | |
| """Checks if the agent dies within the first 5 seconds after launch.""" | |
| time.sleep(5) | |
| # Check if the process handle exists and if it has terminated | |
| if PLAYIT_PROCESS and PLAYIT_PROCESS.poll() is not None: | |
| # Agent has crashed | |
| server_manager.log(f"PlayIt Agent process crashed. Exit Code: {PLAYIT_PROCESS.poll()}", "error") | |
| start_tunneling(error_message=f"PlayIt Agent died. Check {PLAYIT_ERROR_LOG.name} in File Manager.") | |
| else: | |
| # Agent seems to be running | |
| server_manager.log("PlayIt Agent appears to be stable. Connection info will be in the log file.", "info") | |
| # Notify the user to check the log file | |
| start_tunneling(secret_mode=True) | |
| def run_playit_secret_mode(): | |
| """ | |
| Launches the 'playit run' agent, redirecting all output to a log file | |
| for debugging purposes. This replaces the problematic monitoring thread. | |
| """ | |
| global PLAYIT_PROCESS | |
| try: | |
| # 1. Open the log file for writing (will create or truncate it) | |
| with open(PLAYIT_ERROR_LOG, 'w') as log_file: | |
| # 2. Launch the agent, redirecting STDOUT and STDERR to the log file | |
| # This captures both success and crash messages. | |
| PLAYIT_PROCESS = subprocess.Popen( | |
| ['playit', 'run', '--api-address', '127.0.0.1:9000'], | |
| stdout=log_file, | |
| stderr=log_file, | |
| bufsize=1 # Line buffering | |
| ) | |
| server_manager.log(f"PlayIt.gg agent launched. Output redirected to: {PLAYIT_ERROR_LOG}", "info") | |
| # 3. Start a monitor thread to check for immediate crashes | |
| threading.Thread(target=check_playit_initial_status, daemon=True).start() | |
| except Exception as e: | |
| server_manager.log(f"Error launching PlayIt.gg agent: {e}", "error") | |
| start_tunneling(error_message="PlayIt 'run' failed (Check logs for details).") | |
| def start_playit_agent(): | |
| """ | |
| Determines which PlayIt.gg mode to run based on the environment variable. | |
| """ | |
| # Delete the old log file on start for a clean run | |
| if PLAYIT_ERROR_LOG.exists(): | |
| PLAYIT_ERROR_LOG.unlink() | |
| if os.environ.get('PLAYIT_SECRET_KEY'): | |
| print("=" * 50) | |
| print("🚀 STARTING PLAYIT.GG AGENT (Secret Key Mode) 🚀") | |
| print("==================================================") | |
| run_playit_secret_mode() | |
| else: | |
| print("=" * 50) | |
| print("🔑 STARTING PLAYIT.GG AGENT (Claim Code Mode) 🔑") | |
| print("Action Required: Go to playit.gg/claim and enter the code below.") | |
| print("==================================================") | |
| run_playit_claim_mode() | |
| def start_tunneling(claim_code=None, secret_mode=False, error_message=None): | |
| """ | |
| Displays the connection information based on the current PlayIt mode. | |
| """ | |
| connect_address = f"{config.SPACE_URL}:{config.SERVER_PORT}" | |
| print("=" * 50) | |
| print("🌐 MINECRAFT CONNECTION INFO 🌐") | |
| print(f"Web Panel Address: {connect_address}") | |
| print("------------------------------------------") | |
| if error_message: | |
| print(f"MC Tunnel: **ERROR: {error_message}**") | |
| socketio.emit('server_connect_info', {'address': error_message, 'type': 'error'}) | |
| elif claim_code: | |
| print(f"MC Tunnel: CLAIM CODE") | |
| print(f"Claim Code: **{claim_code}** (Use at playit.gg/claim)") | |
| socketio.emit('server_connect_info', {'address': f'CLAIM: {claim_code}', 'type': 'claim'}) | |
| elif secret_mode: | |
| # This is the WAITING path for Secret Key mode, pointing the user to the log file. | |
| print(f"MC Tunnel: **CHECK FILE MANAGER**") | |
| print(f"Agent running. Check {PLAYIT_ERROR_LOG.name} in the minecraft_server directory for the public address or crash reason.") | |
| socketio.emit('server_connect_info', {'address': f'Agent Running (Check {PLAYIT_ERROR_LOG.name})', 'type': 'playit'}) | |
| print("------------------------------------------") | |
| print("=" * 50) | |
| server_manager.log(f"Connect Address: {connect_address}", "info") | |
| # ==================================================================== | |
| # === END PLAYIT.GG AGENT FUNCTIONS === | |
| # ==================================================================== | |
| # --- Automated Save/Cleanup Thread --- | |
| def start_auto_save_cleanup_thread(): | |
| CLEANUP_INTERVAL = 6 * 60 * 60 # Save and cleanup every 6 hours | |
| while True: | |
| time.sleep(CLEANUP_INTERVAL) | |
| print("Starting scheduled Supabase config save and local cleanup...") | |
| server_manager.save_and_cleanup() | |
| # --- Authentication Routes --- | |
| def login(): | |
| if request.method == 'POST': | |
| username = request.form.get('username', '') | |
| password = request.form.get('password', '') | |
| if username == config.ADMIN_USERNAME and check_password_hash(ADMIN_PASSWORD_HASH, password): | |
| session['logged_in'] = True | |
| session['username'] = username | |
| return redirect(url_for('index')) | |
| return render_template('login.html', error='Invalid credentials') | |
| return render_template('login.html') | |
| def logout(): | |
| session.clear() | |
| return redirect(url_for('login')) | |
| # --- Main Routes --- | |
| def index(): | |
| return render_template('index.html', | |
| status=server_manager.get_status(), | |
| mods_count=len(mod_manager.get_installed_mods())) | |
| def console(): | |
| return render_template('console.html', | |
| history=server_manager.get_console_history(200)) | |
| def players(): | |
| return render_template('players.html', | |
| online=server_manager.players_online, | |
| all_players=player_tracker.get_all_players(), | |
| whitelist=player_tracker.get_whitelist(), | |
| ops=player_tracker.get_ops(), | |
| banned=player_tracker.get_banned()) | |
| def mods(): | |
| return render_template('mods.html', | |
| mods=mod_manager.get_installed_mods(), | |
| total_size=mod_manager.get_total_size()) | |
| def files(filepath=''): | |
| dir_content = file_manager.list_directory(filepath) | |
| return render_template('files.html', | |
| content=dir_content, | |
| current_path=filepath) | |
| def import_page(): | |
| return render_template('import.html', | |
| backups=file_manager.list_backups()) | |
| def settings(): | |
| props = file_manager.get_server_properties() | |
| return render_template('settings.html', | |
| properties=props, | |
| pinger_status=health_pinger.get_status()) | |
| # --- API Routes --- | |
| def health(): | |
| return jsonify({ | |
| 'status': 'ok', | |
| 'timestamp': datetime.now().isoformat(), | |
| 'server_running': server_manager.running | |
| }) | |
| def api_status(): | |
| return jsonify(server_manager.get_status()) | |
| def api_start(): | |
| result = server_manager.start() | |
| return jsonify(result) | |
| def api_stop(): | |
| force = request.json.get('force', False) if request.is_json else False | |
| result = server_manager.stop(force=force) | |
| return jsonify(result) | |
| def api_restart(): | |
| result = server_manager.restart() | |
| return jsonify(result) | |
| def api_save_cleanup(): | |
| threading.Thread(target=server_manager.save_and_cleanup, daemon=True).start() | |
| return jsonify({"success": True, "message": "Save and cleanup process started in background."}) | |
| def api_send_command(): | |
| data = request.get_json() | |
| command = data.get('command', '') | |
| if not command: | |
| return jsonify({'success': False, 'message': 'No command provided'}) | |
| result = console_handler.execute(command) | |
| return jsonify(result) | |
| def api_console_history(): | |
| limit = request.args.get('limit', 100, type=int) | |
| return jsonify(server_manager.get_console_history(limit)) | |
| # Player API routes | |
| def api_online_players(): | |
| return jsonify(server_manager.players_online) | |
| def api_all_players(): | |
| return jsonify(player_tracker.get_all_players()) | |
| def api_kick_player(): | |
| data = request.get_json() | |
| result = player_tracker.kick_player(data.get('player'), data.get('reason', '')) | |
| return jsonify(result) | |
| def api_ban_player(): | |
| data = request.get_json() | |
| result = player_tracker.ban_player(data.get('player'), data.get('reason', '')) | |
| return jsonify(result) | |
| def api_pardon_player(): | |
| data = request.get_json() | |
| result = player_tracker.pardon_player(data.get('player')) | |
| return jsonify(result) | |
| def api_op_player(): | |
| data = request.get_json() | |
| result = player_tracker.op_player(data.get('player')) | |
| return jsonify(result) | |
| def api_deop_player(): | |
| data = request.get_json() | |
| result = player_tracker.deop_player(data.get('player')) | |
| return jsonify(result) | |
| # Mod API routes | |
| def api_list_mods(): | |
| return jsonify(mod_manager.get_installed_mods()) | |
| def api_upload_mod(): | |
| if 'file' not in request.files: | |
| return jsonify({'success': False, 'message': 'No file provided'}) | |
| result = mod_manager.upload_mod(request.files['file']) | |
| return jsonify(result) | |
| def api_delete_mod(): | |
| data = request.get_json() | |
| result = mod_manager.delete_mod(data.get('filename')) | |
| return jsonify(result) | |
| def api_toggle_mod(): | |
| data = request.get_json() | |
| result = mod_manager.toggle_mod(data.get('filename')) | |
| return jsonify(result) | |
| # File API routes | |
| def api_list_files(): | |
| path = request.args.get('path', '') | |
| return jsonify(file_manager.list_directory(path)) | |
| def api_read_file(): | |
| path = request.args.get('path', '') | |
| return jsonify(file_manager.read_file(path)) | |
| def api_write_file(): | |
| data = request.get_json() | |
| result = file_manager.write_file(data.get('path'), data.get('content')) | |
| return jsonify(result) | |
| def api_create_file(): | |
| data = request.get_json() | |
| result = file_manager.create_file(data.get('path', ''), data.get('name')) | |
| return jsonify(result) | |
| def api_create_dir(): | |
| data = request.get_json() | |
| result = file_manager.create_directory(data.get('path', ''), data.get('name')) | |
| return jsonify(result) | |
| def api_delete_file(): | |
| data = request.get_json() | |
| result = file_manager.delete_item(data.get('path')) | |
| return jsonify(result) | |
| def api_rename_file(): | |
| data = request.get_json() | |
| result = file_manager.rename_item(data.get('path'), data.get('new_name')) | |
| return jsonify(result) | |
| # Backup API routes | |
| def api_create_backup(): | |
| data = request.get_json() or {} | |
| result = file_manager.create_backup(data.get('name')) | |
| return jsonify(result) | |
| def api_list_backups(): | |
| return jsonify(file_manager.list_backups()) | |
| def api_restore_backup(): | |
| data = request.get_json() | |
| result = file_manager.restore_backup(data.get('name')) | |
| return jsonify(result) | |
| def api_delete_backup(): | |
| data = request.get_json() | |
| result = file_manager.delete_backup(data.get('name')) | |
| return jsonify(result) | |
| def api_download_backup(name): | |
| path = file_manager.download_backup(name) | |
| if path: | |
| return send_file(path, as_attachment=True) | |
| return jsonify({'success': False, 'message': 'Backup not found'}), 404 | |
| # Import API routes | |
| def api_import_world(): | |
| if 'file' not in request.files: | |
| return jsonify({'success': False, 'message': 'No file provided'}) | |
| result = file_manager.import_world(request.files['file']) | |
| return jsonify(result) | |
| # Settings API routes | |
| def api_get_properties(): | |
| return jsonify(file_manager.get_server_properties()) | |
| def api_update_properties(): | |
| data = request.get_json() | |
| result = file_manager.update_server_properties(data) | |
| return jsonify(result) | |
| def api_pinger_status(): | |
| return jsonify(health_pinger.get_status()) | |
| # --- SocketIO Events --- | |
| def handle_connect(): | |
| if session.get('logged_in'): | |
| emit('status_update', server_manager.get_status()) | |
| emit('initial_console', server_manager.get_console_history(100)) | |
| def handle_command(data): | |
| if session.get('logged_in'): | |
| command = data.get('command', '') | |
| server_manager.send_command(command) | |
| def handle_status_request(): | |
| if session.get('logged_in'): | |
| emit('status_update', server_manager.get_status()) | |
| # ============== Main Entry Point ============== | |
| def main(): | |
| # Start health pinger | |
| health_pinger.start() | |
| # Ensure required directories exist | |
| config.MODS_DIR.mkdir(parents=True, exist_ok=True) | |
| config.WORLD_DIR.mkdir(parents=True, exist_ok=True) | |
| config.LOGS_DIR.mkdir(parents=True, exist_ok=True) | |
| config.BACKUPS_DIR.mkdir(parents=True, exist_ok=True) | |
| config.UPLOADS_DIR.mkdir(parents=True, exist_ok=True) | |
| print("=" * 50) | |
| print("Minecraft Server Panel Starting") | |
| print(f"Web Panel: http://0.0.0.0:{config.WEB_PORT}") | |
| print(f"MC Server: Port {config.SERVER_PORT}") | |
| print("=" * 50) | |
| # 1. Start the PlayIt.gg agent | |
| threading.Thread(target=start_playit_agent, daemon=True).start() | |
| # 2. Start the Auto-Save and Cleanup Thread | |
| threading.Thread(target=start_auto_save_cleanup_thread, daemon=True).start() | |
| # 3. Run Flask app with SocketIO (CRITICAL HF PORT: 7860) | |
| socketio.run( | |
| app, | |
| host='0.0.0.0', | |
| port=config.WEB_PORT, | |
| debug=False, | |
| use_reloader=False | |
| ) | |
| if __name__ == '__main__': | |
| main() |