Spaces:
Running
Running
import urllib.request | |
from pathlib import Path | |
import os | |
import subprocess | |
import sys | |
import shutil | |
import gradio as gr | |
import threading | |
import time | |
import signal | |
try: | |
from pyngrok import ngrok, conf | |
PYNGROK_AVAILABLE = True | |
except ImportError: | |
print("pyngrok not found. Installing...") | |
try: | |
subprocess.check_call([sys.executable, "-m", "pip", "install", "pyngrok"]) | |
from pyngrok import ngrok, conf | |
PYNGROK_AVAILABLE = True | |
print("β pyngrok installed successfully") | |
except Exception as e: | |
print(f"Failed to install pyngrok: {e}") | |
PYNGROK_AVAILABLE = False | |
subprocess.check_call([sys.executable, "-m", "pip", "install", "mcstatus"]) | |
from mcstatus import JavaServer | |
PAPER_JAR_URL = "https://fill-data.papermc.io/v1/objects/234a9b32098100c6fc116664d64e36ccdb58b5b649af0f80bcccb08b0255eaea/paper-1.20.1-196.jar" | |
DATA_DIR = Path("/data") | |
JAVA_DIR = DATA_DIR / "java" | |
JAR_NAME = "paper-1.20.1-196.jar" | |
JAR_PATH = DATA_DIR / JAR_NAME | |
# Java download URLs (OpenJDK 17) | |
JAVA_URLS = { | |
"linux_x64": "https://download.java.net/java/GA/jdk17.0.2/dfd4a8d0985749f896bed50d7138ee7f/8/GPL/openjdk-17.0.2_linux-x64_bin.tar.gz", | |
"linux_aarch64": "https://download.java.net/java/GA/jdk17.0.2/dfd4a8d0985749f896bed50d7138ee7f/8/GPL/openjdk-17.0.2_linux-aarch64_bin.tar.gz" | |
} | |
def load_env_vars(): | |
"""Load environment variables from .env file if it exists""" | |
env_vars = {} | |
env_file = Path(".env") | |
if env_file.exists(): | |
try: | |
with open(env_file, 'r') as f: | |
for line in f: | |
line = line.strip() | |
if line and not line.startswith('#') and '=' in line: | |
key, value = line.split('=', 1) | |
env_vars[key.strip()] = value.strip().strip('"\'') | |
print(f"Loaded {len(env_vars)} environment variables from .env") | |
except Exception as e: | |
print(f"Warning: Could not read .env file: {e}") | |
# Also check system environment variables | |
ngrok_token = env_vars.get('NGROK_TOKEN') or os.getenv('NGROK_TOKEN') | |
return { | |
'NGROK_TOKEN': ngrok_token | |
} | |
def get_platform(): | |
"""Detect the platform architecture""" | |
import platform | |
machine = platform.machine().lower() | |
if 'x86_64' in machine or 'amd64' in machine: | |
return "linux_x64" | |
elif 'aarch64' in machine or 'arm64' in machine: | |
return "linux_aarch64" | |
else: | |
print(f"Unsupported architecture: {machine}") | |
return "linux_x64" # Default fallback | |
def check_ngrok(): | |
"""Check if ngrok is available and return the path""" | |
print("Checking for ngrok...") | |
# Check system PATH first | |
ngrok_path = shutil.which("ngrok") | |
if ngrok_path: | |
try: | |
result = subprocess.run([ngrok_path, "version"], capture_output=True, text=True) | |
print(f"Found ngrok in PATH: {ngrok_path}") | |
print(f"Version: {result.stdout.strip()}") | |
return ngrok_path | |
except Exception as e: | |
print(f"Error checking system ngrok: {e}") | |
# Check our custom ngrok installation | |
custom_ngrok = NGROK_DIR / "ngrok" | |
if custom_ngrok.exists(): | |
try: | |
result = subprocess.run([str(custom_ngrok), "version"], capture_output=True, text=True) | |
print(f"Found custom ngrok: {custom_ngrok}") | |
print(f"Version: {result.stdout.strip()}") | |
return str(custom_ngrok) | |
except Exception as e: | |
print(f"Error checking custom ngrok: {e}") | |
print("ngrok not found") | |
return None | |
def install_ngrok(): | |
"""Download and install ngrok""" | |
print("Installing ngrok...") | |
# Create ngrok directory | |
NGROK_DIR.mkdir(parents=True, exist_ok=True) | |
# Download ngrok | |
ngrok_archive = DATA_DIR / "ngrok.tgz" | |
if not download_file(NGROK_URL, ngrok_archive): | |
return False | |
# Extract ngrok | |
print("Extracting ngrok...") | |
try: | |
import tarfile | |
with tarfile.open(ngrok_archive, 'r:gz') as tar: | |
tar.extractall(NGROK_DIR) | |
# Make ngrok executable | |
ngrok_bin = NGROK_DIR / "ngrok" | |
ngrok_bin.chmod(0o755) | |
# Clean up | |
ngrok_archive.unlink() | |
print(f"β ngrok installed to: {ngrok_bin}") | |
return True | |
except Exception as e: | |
print(f"Failed to extract ngrok: {e}") | |
return False | |
def setup_ngrok(): | |
"""Setup ngrok with authentication token using pyngrok""" | |
print("Setting up ngrok...") | |
if not PYNGROK_AVAILABLE: | |
print("pyngrok is not available. Cannot setup ngrok.") | |
return False | |
env_vars = load_env_vars() | |
ngrok_token = env_vars.get('NGROK_TOKEN') | |
if not ngrok_token: | |
print("Warning: NGROK_TOKEN not found in environment variables or .env file") | |
print("Please set NGROK_TOKEN in your .env file or Hugging Face Space secrets") | |
return False | |
try: | |
# Set the auth token | |
ngrok.set_auth_token(ngrok_token) | |
print("β ngrok configured successfully with pyngrok") | |
return True | |
except Exception as e: | |
print(f"Failed to configure ngrok: {e}") | |
return False | |
def start_ngrok_tunnel(port=25565): | |
"""Start ngrok tunnel for the Minecraft server using pyngrok""" | |
print(f"Starting ngrok tunnel for port {port}...") | |
if not PYNGROK_AVAILABLE: | |
print("pyngrok is not available. Cannot start tunnel.") | |
return None, None | |
try: | |
# Create a TCP tunnel | |
print("Creating ngrok tunnel...") | |
tunnel = ngrok.connect(port, "tcp") | |
tunnel_url = tunnel.public_url | |
print(f"β ngrok tunnel active: {tunnel_url}") | |
print(f"Players can connect to: {tunnel_url.replace('tcp://', '')}") | |
# Get all active tunnels for reference | |
tunnels = ngrok.get_tunnels() | |
print(f"Active tunnels: {len(tunnels)}") | |
return tunnel, tunnel_url | |
except Exception as e: | |
print(f"Failed to start ngrok tunnel: {e}") | |
return None, None | |
def check_java(): | |
"""Check if Java is available and return the path""" | |
# Check if java is already in PATH | |
java_path = shutil.which("java") | |
if java_path: | |
try: | |
result = subprocess.run([java_path, "-version"], capture_output=True, text=True) | |
print(f"Found Java: {java_path}") | |
print(result.stderr.split('\n')[0]) | |
return java_path | |
except: | |
pass | |
custom_java = JAVA_DIR / "bin" / "java" | |
if custom_java.exists(): | |
try: | |
result = subprocess.run([str(custom_java), "-version"], capture_output=True, text=True) | |
print(f"Found custom Java: {custom_java}") | |
print(result.stderr.split('\n')[0]) | |
return str(custom_java) | |
except: | |
pass | |
return None | |
def download_file(url, destination): | |
print(f"Downloading {url}...") | |
try: | |
destination.parent.mkdir(parents=True, exist_ok=True) | |
headers = { | |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' | |
} | |
request = urllib.request.Request(url, headers=headers) | |
with urllib.request.urlopen(request) as response: | |
with open(destination, 'wb') as f: | |
total_size = int(response.headers.get('content-length', 0)) | |
block_size = 8192 | |
downloaded = 0 | |
while True: | |
buffer = response.read(block_size) | |
if not buffer: | |
break | |
downloaded += len(buffer) | |
f.write(buffer) | |
if total_size: | |
percent = min(100, (downloaded * 100) // total_size) | |
print(f"\rProgress: {percent}% ({downloaded}/{total_size} bytes)", end="") | |
print(f"\nβ Downloaded: {destination}") | |
return True | |
except Exception as e: | |
print(f"\nβ Failed to download {url}: {e}") | |
return False | |
def install_java(): | |
"""Download and install Java if not available""" | |
print("Installing Java...") | |
platform_key = get_platform() | |
java_url = JAVA_URLS.get(platform_key) | |
if not java_url: | |
print(f"No Java download available for platform: {platform_key}") | |
return False | |
java_archive = DATA_DIR / "java.tar.gz" | |
if not download_file(java_url, java_archive): | |
return False | |
print("Extracting Java...") | |
try: | |
import tarfile | |
with tarfile.open(java_archive, 'r:gz') as tar: | |
temp_dir = DATA_DIR / "java_temp" | |
temp_dir.mkdir(exist_ok=True) | |
tar.extractall(temp_dir) | |
extracted_dirs = [d for d in temp_dir.iterdir() if d.is_dir() and d.name.startswith('jdk')] | |
if not extracted_dirs: | |
print("Could not find extracted JDK directory") | |
return False | |
jdk_dir = extracted_dirs[0] | |
if JAVA_DIR.exists(): | |
shutil.rmtree(JAVA_DIR) | |
shutil.move(str(jdk_dir), str(JAVA_DIR)) | |
shutil.rmtree(temp_dir) | |
java_archive.unlink() | |
print(f"β Java installed to: {JAVA_DIR}") | |
java_bin = JAVA_DIR / "bin" / "java" | |
java_bin.chmod(0o755) | |
return True | |
except Exception as e: | |
print(f"Failed to extract Java: {e}") | |
return False | |
def setup_minecraft_server(): | |
DATA_DIR.mkdir(parents=True, exist_ok=True) | |
java_path = check_java() | |
if not java_path: | |
print("Java not found. Installing Java...") | |
if not install_java(): | |
print("Failed to install Java. Cannot proceed.") | |
return False | |
java_path = check_java() | |
if not java_path: | |
print("Java installation failed or not working.") | |
return False | |
if not JAR_PATH.exists(): | |
print(f"Downloading Minecraft server to {JAR_PATH}") | |
if not download_file(PAPER_JAR_URL, JAR_PATH): | |
return False | |
else: | |
print(f"Server JAR already exists at {JAR_PATH}") | |
server_properties_path = DATA_DIR / "server.properties" | |
print("Creating server.properties...") | |
server_properties = """# Minecraft server properties | |
server-port=25565 | |
gamemode=survival | |
difficulty=easy | |
spawn-protection=0 | |
max-players=64 | |
online-mode=true | |
white-list=false | |
motd=Hugging Face Spaces Minecraft Server | |
enable-query=true | |
query.port=25565 | |
""" | |
with open(server_properties_path, 'w') as f: | |
f.write(server_properties) | |
# Create eula.txt (required for server to start) | |
eula_path = DATA_DIR / "eula.txt" | |
if not eula_path.exists(): | |
print("Creating eula.txt...") | |
with open(eula_path, 'w') as f: | |
f.write("eula=true\n") | |
print("β Minecraft server setup complete!") | |
print(f"Server files are stored in: {DATA_DIR}") | |
print(f"Java path: {java_path}") | |
return True | |
def start_server(): | |
"""Start the Minecraft server""" | |
if not JAR_PATH.exists(): | |
print("Server JAR not found. Run setup first.") | |
return False | |
# Get Java path | |
java_path = check_java() | |
if not java_path: | |
print("Java not found. Run setup first.") | |
return False | |
# Change to the data directory | |
os.chdir(DATA_DIR) | |
# Start the server | |
cmd = [ | |
java_path, | |
"-Xmx2G", # Max heap size | |
"-Xms1G", # Initial heap size | |
"-jar", str(JAR_PATH), | |
"--nogui" | |
] | |
print(f"Starting Minecraft server with command: {' '.join(cmd)}") | |
print(f"Working directory: {DATA_DIR}") | |
try: | |
process = subprocess.Popen( | |
cmd, | |
stdout=subprocess.PIPE, | |
stderr=subprocess.STDOUT, | |
universal_newlines=True, | |
bufsize=1 | |
) | |
for line in process.stdout: | |
print(line.strip()) | |
except Exception as e: | |
print(f"Failed to start server: {e}") | |
return False | |
def start_server_background(): | |
"""Start the Minecraft server in the background""" | |
global server_process | |
if not JAR_PATH.exists(): | |
print("Server JAR not found. Run setup first.") | |
return False | |
# Get Java path | |
java_path = check_java() | |
if not java_path: | |
print("Java not found. Run setup first.") | |
return False | |
# Change to the data directory | |
original_cwd = os.getcwd() | |
os.chdir(DATA_DIR) | |
# Start the server | |
cmd = [ | |
java_path, | |
"-Xmx2G", # Max heap size | |
"-Xms1G", # Initial heap size | |
"-jar", str(JAR_PATH), | |
"--nogui" | |
] | |
print(f"Starting Minecraft server in background with command: {' '.join(cmd)}") | |
print(f"Working directory: {DATA_DIR}") | |
try: | |
server_process = subprocess.Popen( | |
cmd, | |
stdout=subprocess.PIPE, | |
stderr=subprocess.STDOUT, | |
universal_newlines=True, | |
bufsize=1, | |
cwd=DATA_DIR | |
) | |
# Start a thread to monitor server output | |
def monitor_server(): | |
for line in server_process.stdout: | |
print(f"[SERVER] {line.strip()}") | |
monitor_thread = threading.Thread(target=monitor_server, daemon=True) | |
monitor_thread.start() | |
# Wait a bit for server to start | |
time.sleep(5) | |
os.chdir(original_cwd) | |
print("β Minecraft server started in background") | |
return True | |
except Exception as e: | |
print(f"Failed to start server: {e}") | |
os.chdir(original_cwd) | |
return False | |
def create_gradio_interface(): | |
"""Create and return the Gradio interface""" | |
global server_ip | |
server = JavaServer.lookup(tunnel_url) | |
status = server.status() | |
latency = server.ping() | |
with gr.Blocks() as demo: | |
def get_server_status(): | |
server = JavaServer.lookup(tunnel_url) | |
status = server.status() | |
latency = server.ping() | |
return f"**Server Status:** {status.players.online} player(s) online, latency: {round(latency)} ms", f"**Server Version:** {status.version.name} ({status.version.protocol})", f"**Server Description:** {status.description}", f"**Players:** {', '.join(player.name for player in status.players.sample)}" if status.players.sample else "No players online" | |
gr.Markdown("# Minecraft Server Status") | |
gr.Markdown(f"**Server IP:** {tunnel_url}") | |
status_markdown = gr.Markdown(f"**Server Status:** {status.players.online} player(s) online, latency: {round(latency)} ms") | |
version_markdown = gr.Markdown(f"**Server Version:** {status.version.name} ({status.version.protocol})") | |
description_markdown = gr.Markdown(f"**Server Description:** {status.description}") | |
players_markdown = gr.Markdown(f"**Players:** {', '.join(player.name for player in status.players.sample)}" if status.players.sample else "No players online") | |
btn = gr.Button("Refresh Status") | |
btn.click(get_server_status, outputs=[status_markdown, version_markdown, description_markdown, players_markdown]) | |
return demo | |
def stop_server(): | |
"""Stop the Minecraft server""" | |
global server_process | |
if server_process: | |
print("Stopping Minecraft server...") | |
server_process.terminate() | |
try: | |
server_process.wait(timeout=10) | |
except subprocess.TimeoutExpired: | |
server_process.kill() | |
server_process = None | |
print("β Minecraft server stopped") | |
def cleanup_ngrok(): | |
"""Clean up ngrok tunnels on exit""" | |
if PYNGROK_AVAILABLE: | |
try: | |
ngrok.kill() | |
print("β ngrok tunnels cleaned up") | |
except: | |
pass | |
def cleanup_all(): | |
"""Clean up all resources""" | |
stop_server() | |
cleanup_ngrok() | |
def signal_handler(signum, frame): | |
"""Handle shutdown signals""" | |
print("\nReceived shutdown signal. Cleaning up...") | |
cleanup_all() | |
sys.exit(0) | |
if __name__ == "__main__": | |
# Register signal handlers | |
signal.signal(signal.SIGINT, signal_handler) | |
signal.signal(signal.SIGTERM, signal_handler) | |
print("=== Minecraft Server Setup ===") | |
try: | |
if not setup_minecraft_server(): | |
print("Setup failed. Cannot start server.") | |
sys.exit(1) | |
print("\n=== Setting up ngrok ===") | |
if not setup_ngrok(): | |
print("ngrok setup failed. Server will start but won't be accessible externally.") | |
tunnel = None | |
else: | |
print("\n=== Starting ngrok tunnel ===") | |
tunnel, tunnel_url = start_ngrok_tunnel() | |
if tunnel: | |
ngrok_tunnel = tunnel | |
# Extract IP from tunnel URL (format: tcp://x.tcp.ngrok.io:port) | |
server_ip = tunnel_url.replace('tcp://', '') | |
print(f"Server will be accessible at: {server_ip}") | |
else: | |
print("Failed to start ngrok tunnel. Server will start but won't be accessible externally.") | |
print("\n=== Starting Minecraft Server ===") | |
if not start_server_background(): | |
print("Failed to start server.") | |
cleanup_all() | |
sys.exit(1) | |
# Wait a bit for server to fully start | |
print("Waiting for server to start...") | |
time.sleep(10) | |
print("\n=== Starting Gradio Interface ===") | |
demo = create_gradio_interface() | |
# Launch Gradio interface | |
demo.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False, | |
show_error=True | |
) | |
except KeyboardInterrupt: | |
print("\n\nShutting down...") | |
cleanup_all() | |
sys.exit(0) | |
except Exception as e: | |
print(f"Unexpected error: {e}") | |
cleanup_all() | |
sys.exit(1) |