Spaces:
Running
Running
import urllib.request | |
from pathlib import Path | |
import os | |
import subprocess | |
import sys | |
import shutil | |
PAPER_JAR_URL = "https://fill-data.papermc.io/v1/objects/234a9b32098100c6fc116664d64e36ccdb58b5b649af0f80bcccb08b0255eaea/paper-1.20.1-196.jar" | |
NGROK_URL = "https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz" | |
DATA_DIR = Path("/data") | |
JAVA_DIR = DATA_DIR / "java" | |
NGROK_DIR = DATA_DIR / "ngrok" | |
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 check_ngrok(): | |
"""Check if ngrok is available and return the path""" | |
# 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: {ngrok_path}") | |
print(f"Version: {result.stdout.strip()}") | |
return ngrok_path | |
except: | |
pass | |
# 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: | |
pass | |
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""" | |
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 | |
# Get ngrok path | |
ngrok_path = check_ngrok() | |
if not ngrok_path: | |
print("ngrok not found. Installing...") | |
if not install_ngrok(): | |
print("Failed to install ngrok") | |
return False | |
ngrok_path = check_ngrok() | |
if not ngrok_path: | |
print("ngrok installation failed") | |
return False | |
# Configure ngrok with auth token | |
print("Configuring ngrok with auth token...") | |
try: | |
result = subprocess.run([ | |
ngrok_path, "config", "add-authtoken", ngrok_token | |
], capture_output=True, text=True) | |
if result.returncode == 0: | |
print("β ngrok configured successfully") | |
return True | |
else: | |
print(f"Failed to configure ngrok: {result.stderr}") | |
return False | |
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""" | |
ngrok_path = check_ngrok() | |
if not ngrok_path: | |
print("ngrok not found. Run setup first.") | |
return None | |
print(f"Starting ngrok tunnel for port {port}...") | |
try: | |
# Start ngrok in background | |
process = subprocess.Popen([ | |
ngrok_path, "tcp", str(port), "--log", "stdout" | |
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) | |
# Give ngrok a moment to start | |
import time | |
time.sleep(3) | |
# Try to get the tunnel URL from ngrok API | |
try: | |
import json | |
import urllib.request | |
# ngrok exposes a local API on port 4040 | |
api_url = "http://localhost:4040/api/tunnels" | |
with urllib.request.urlopen(api_url) as response: | |
data = json.loads(response.read().decode()) | |
if data.get('tunnels'): | |
tunnel_url = data['tunnels'][0]['public_url'] | |
print(f"β ngrok tunnel active: {tunnel_url}") | |
print(f"Players can connect to: {tunnel_url.replace('tcp://', '')}") | |
return process, tunnel_url | |
else: | |
print("No active tunnels found") | |
return process, None | |
except Exception as e: | |
print(f"Could not retrieve tunnel URL: {e}") | |
print("Check ngrok dashboard at http://localhost:4040") | |
return process, None | |
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]) # Java version info is in stderr | |
return java_path | |
except: | |
pass | |
# Check our custom Java installation | |
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]) # Java version info is in stderr | |
return str(custom_java) | |
except: | |
pass | |
return None | |
"""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 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 | |
# Download Java | |
java_archive = DATA_DIR / "java.tar.gz" | |
if not download_file(java_url, java_archive): | |
return False | |
# Extract Java | |
print("Extracting Java...") | |
try: | |
import tarfile | |
with tarfile.open(java_archive, 'r:gz') as tar: | |
# Extract to temporary directory first | |
temp_dir = DATA_DIR / "java_temp" | |
temp_dir.mkdir(exist_ok=True) | |
tar.extractall(temp_dir) | |
# Find the extracted JDK directory (usually has a version number) | |
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] | |
# Move to final location | |
if JAVA_DIR.exists(): | |
shutil.rmtree(JAVA_DIR) | |
shutil.move(str(jdk_dir), str(JAVA_DIR)) | |
# Clean up | |
shutil.rmtree(temp_dir) | |
java_archive.unlink() | |
print(f"β Java installed to: {JAVA_DIR}") | |
# Make java executable | |
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(): | |
"""Set up the Minecraft server in persistent storage""" | |
# Ensure the data directory exists | |
DATA_DIR.mkdir(parents=True, exist_ok=True) | |
# Check and install Java if needed | |
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 | |
# Download the server JAR if it doesn't exist | |
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}") | |
# Create server.properties file if it doesn't exist | |
server_properties_path = DATA_DIR / "server.properties" | |
if not server_properties_path.exists(): | |
print("Creating server.properties...") | |
server_properties = """# Minecraft server properties | |
server-port=25565 | |
gamemode=survival | |
difficulty=easy | |
spawn-protection=16 | |
max-players=20 | |
online-mode=false | |
white-list=false | |
motd=Hugging Face Minecraft Server | |
""" | |
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 | |
) | |
# Print server output in real-time | |
for line in process.stdout: | |
print(line.strip()) | |
except Exception as e: | |
print(f"Failed to start server: {e}") | |
return False | |
if __name__ == "__main__": | |
if setup_minecraft_server(): | |
start_server() | |
else: | |
print("Setup failed. Cannot start server.") |