FROM openjdk:21-jdk-slim # Install required packages RUN apt-get update && apt-get install -y \ python3 \ python3-pip \ curl \ git \ procps \ psmisc \ tzdata \ inotify-tools \ && rm -rf /var/lib/apt/lists/* # Set Sri Lankan Timezone (Asia/Colombo, UTC+5:30) ENV TZ=Asia/Colombo RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # Install Python dependencies RUN pip3 install flask pytz flask-sock psutil requests --break-system-packages WORKDIR /app # Create startup script RUN cat > start.sh << 'EOF' #!/bin/bash set -e cd /app echo "==========================================" echo "đ Starting Minecraft Server with Web Terminal" echo "â° $(date)" echo "đ Timezone: $(cat /etc/timezone) | $(date +%Z)" echo "==========================================" # Kill any existing processes on port 7860 fuser -k 7860/tcp 2>/dev/null || true sleep 2 # Clone private repo (requires GITHUB_TOKEN) if [ -n "$GITHUB_TOKEN" ]; then echo "đĨ Cloning Minecraft server from GitHub..." rm -rf mcserver git clone https://$GITHUB_TOKEN@github.com/Walukapah/funmcserver.git mcserver cd mcserver echo "â Repository cloned successfully" else echo "â ERROR: GITHUB_TOKEN not set!" exit 1 fi # Accept EULA if not exists if [ ! -f eula.txt ]; then echo "eula=true" > eula.txt echo "â EULA accepted" fi # ========================================== # CREATE RESTART SCRIPT (FIXED!) # ========================================== echo "đ Creating restart.sh script..." cat > restart.sh << 'RESTARTEOF' #!/bin/bash # Minecraft Server Restart Script # This script tells the Python web terminal to restart the server echo "[RESTART] Restart signal sent at $(date)" # Create a signal file that Python monitors touch /tmp/mc_restart_requested # Also kill the old java process after a short delay to ensure clean shutdown sleep 2 pkill -f "server.jar" 2>/dev/null || true RESTARTEOF chmod +x restart.sh echo "â restart.sh created and made executable" # ========================================== # GIT CONFIGURATION (VERY IMPORTANT!) # ========================================== echo "đ§ Setting up Git configuration..." git config --global user.email "minecraft@huggingface.co" git config --global user.name "Minecraft Bot" git config --global init.defaultBranch main # Ensure remote URL has token git remote set-url origin https://$GITHUB_TOKEN@github.com/Walukapah/funmcserver.git # Create .gitignore to avoid unnecessary files cat > .gitignore << 'GITIGNORE' *.log logs/ cache/ tmp/ *.tmp restart.sh /tmp/ GITIGNORE echo "â Git configured successfully" # Create logs directory for persistent storage mkdir -p /app/logs # ========================================== # BACKUP SCRIPT (Fixed with Pull Before Push) # ========================================== cat > /app/mcserver/backup.sh << 'BACKUPSCRIPT' #!/bin/bash set -e echo "" echo "đž [BACKUP] Starting backup at $(date '+%Y-%m-%d %H:%M:%S')" echo "đ Current directory: $(pwd)" # Must be in mcserver directory cd /app/mcserver 2>/dev/null || { echo "â ERROR: Cannot find /app/mcserver directory!" exit 1 } # Check if git is initialized if [ ! -d .git ]; then echo "â ERROR: Not a git repository!" exit 1 fi # ========================================== # STEP 1: Fetch latest changes from GitHub # ========================================== echo "đ Fetching latest changes from GitHub..." git fetch origin main 2>&1 || git fetch origin master 2>&1 || { echo "â ī¸ Warning: Could not fetch from remote" } # ========================================== # STEP 2: Check for remote changes and pull if needed # ========================================== LOCAL=$(git rev-parse HEAD 2>/dev/null || echo "unknown") REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "unknown") if [ "$LOCAL" != "$REMOTE" ] && [ "$REMOTE" != "unknown" ]; then echo "đĨ Remote has new changes, pulling first..." # Stash local changes, pull, then apply stash git stash push -m "auto-backup-stash" 2>/dev/null || true git pull origin main --rebase 2>&1 || git pull origin master --rebase 2>&1 || { echo "â ī¸ Warning: Pull failed, trying merge..." git pull origin main 2>&1 || git pull origin master 2>&1 || true } # Try to pop stash git stash pop 2>/dev/null || true fi # ========================================== # STEP 3: Check for local changes # ========================================== if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then echo "âšī¸ No changes to backup" exit 0 fi echo "đ¤ Pushing changes to GitHub..." # Add all files including new ones git add -A # Commit with timestamp git commit -m "đŽ Auto backup: $(date '+%Y-%m-%d %H:%M:%S') | Server Data Backup" || { echo "âšī¸ Nothing new to commit" exit 0 } # ========================================== # STEP 4: Push with retry logic # ========================================== MAX_RETRIES=3 RETRY_COUNT=0 while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do # Fetch again to check if remote changed during commit git fetch origin main 2>/dev/null || git fetch origin master 2>/dev/null || true LOCAL=$(git rev-parse HEAD) REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "unknown") if [ "$LOCAL" != "$REMOTE" ] && [ "$REMOTE" != "unknown" ]; then echo "đ Remote changed during commit, pulling again..." git stash push -m "auto-backup-stash-$RETRY_COUNT" 2>/dev/null || true git pull origin main --rebase 2>&1 || git pull origin master --rebase 2>&1 || { git pull origin main 2>&1 || git pull origin master 2>&1 || true } git stash pop 2>/dev/null || true # Re-commit if needed git add -A git commit -m "đŽ Auto backup: $(date '+%Y-%m-%d %H:%M:%S') | Server Data Backup (retry $RETRY_COUNT)" || true fi # Try to push if git push origin main 2>&1 || git push origin master 2>&1; then echo "â [BACKUP] Successfully pushed to GitHub!" echo "" exit 0 fi RETRY_COUNT=$((RETRY_COUNT + 1)) echo "â ī¸ Push failed, retrying... ($RETRY_COUNT/$MAX_RETRIES)" sleep 2 done echo "â ERROR: Failed to push to GitHub after $MAX_RETRIES attempts!" echo "đ Local HEAD: $LOCAL" echo "đ Remote HEAD: $REMOTE" exit 1 BACKUPSCRIPT chmod +x /app/mcserver/backup.sh # ========================================== # BACKUP LOOP (Fixed - runs in correct directory) # ========================================== ( echo "â° Backup scheduler started (every 5 minutes)" while true; do sleep 5 /app/mcserver/backup.sh 2>&1 || true done ) & echo "â Auto-backup enabled (every 5 minutes)" echo "" # ========================================== # WEB TERMINAL SERVER (FIXED WITH AUTO-RESTART) # ========================================== cat > /app/web_terminal.py << 'PYTHON_SCRIPT' import os import sys import subprocess import json import threading import time import queue import signal import requests from flask import Flask, render_template_string from flask_sock import Sock import psutil from datetime import datetime, timezone, timedelta import pytz app = Flask(__name__) sock = Sock(app) mc_process = None output_queue = queue.Queue() command_queue = queue.Queue() server_ready = threading.Event() restart_lock = threading.Lock() # Persistent storage variables server_start_time = None logs_history = [] LOGS_FILE = "/app/logs/console_logs.txt" UPTIME_FILE = "/app/logs/server_start_time.txt" RESTART_SIGNAL_FILE = "/tmp/mc_restart_requested" GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN', '') GITHUB_REPO = "Walukapah/funmcserver" # Sri Lankan Timezone SL_TZ = pytz.timezone('Asia/Colombo') def get_sl_time(): """Get current time in Sri Lankan timezone""" return datetime.now(SL_TZ) def format_sl_time(dt=None, fmt='%Y-%m-%d %H:%M:%S'): """Format datetime in Sri Lankan timezone""" if dt is None: dt = get_sl_time() return dt.strftime(fmt) def load_logs_from_file(): """Load previous logs from local file""" global logs_history try: if os.path.exists(LOGS_FILE): with open(LOGS_FILE, 'r', encoding='utf-8') as f: logs_history = f.read().splitlines() logs_history = logs_history[-1000:] print(f"[LOGS] Loaded {len(logs_history)} lines from file", flush=True) except Exception as e: print(f"[LOGS] Error loading logs: {e}", flush=True) logs_history = [] def save_logs_to_file(): """Save logs to local file""" try: os.makedirs(os.path.dirname(LOGS_FILE), exist_ok=True) with open(LOGS_FILE, 'w', encoding='utf-8') as f: f.write('\n'.join(logs_history[-1000:])) except Exception as e: print(f"[LOGS] Error saving logs: {e}", flush=True) def push_logs_to_github(): """Push logs to GitHub for persistent storage""" try: if not GITHUB_TOKEN: return if not os.path.exists(LOGS_FILE): return with open(LOGS_FILE, 'r', encoding='utf-8') as f: content = f.read() api_url = f"https://api.github.com/repos/{GITHUB_REPO}/contents/logs/console_logs.txt" headers = { "Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json" } sha = None try: resp = requests.get(api_url, headers=headers, timeout=10) if resp.status_code == 200: sha = resp.json().get('sha') except: pass import base64 encoded_content = base64.b64encode(content.encode()).decode() data = { "message": f"Update logs - {format_sl_time()}", "content": encoded_content, "branch": "main" } if sha: data["sha"] = sha resp = requests.put(api_url, headers=headers, json=data, timeout=10) if resp.status_code in [200, 201]: print("[LOGS] Pushed to GitHub successfully", flush=True) except Exception as e: print(f"[LOGS] GitHub push error: {e}", flush=True) def load_uptime_from_file(): """Load server start time from file""" global server_start_time try: if os.path.exists(UPTIME_FILE): with open(UPTIME_FILE, 'r') as f: timestamp = f.read().strip() if timestamp: server_start_time = float(timestamp) print(f"[UPTIME] Loaded start time: {datetime.fromtimestamp(server_start_time, SL_TZ)}", flush=True) except Exception as e: print(f"[UPTIME] Error loading uptime: {e}", flush=True) server_start_time = None def save_uptime_to_file(): """Save server start time to file""" try: os.makedirs(os.path.dirname(UPTIME_FILE), exist_ok=True) with open(UPTIME_FILE, 'w') as f: f.write(str(server_start_time)) except Exception as e: print(f"[UPTIME] Error saving uptime: {e}", flush=True) def add_log_line(line): """Add a log line to history and save""" global logs_history timestamp = format_sl_time(fmt='%H:%M:%S') log_entry = f"[{timestamp}] {line}" logs_history.append(log_entry) if len(logs_history) > 1000: logs_history = logs_history[-1000:] output_queue.put(log_entry) if len(logs_history) % 10 == 0: save_logs_to_file() def create_restart_script(): """Create restart.sh if it doesn't exist""" mc_dir = "/app/mcserver" restart_script = os.path.join(mc_dir, "restart.sh") if not os.path.exists(restart_script): print("[MC] Creating restart.sh...", flush=True) with open(restart_script, 'w') as f: f.write('''#!/bin/bash echo "[RESTART] Restart signal sent at $(date)" touch /tmp/mc_restart_requested sleep 2 pkill -f "server.jar" 2>/dev/null || true ''') os.chmod(restart_script, 0o755) print("[MC] restart.sh created", flush=True) def start_minecraft(): global mc_process, server_start_time mc_dir = "/app/mcserver" with restart_lock: # Clear any old restart signal if os.path.exists(RESTART_SIGNAL_FILE): os.remove(RESTART_SIGNAL_FILE) print("[MC] Starting Minecraft server...", flush=True) add_log_line("[SYSTEM] Starting Minecraft server...") server_start_time = time.time() save_uptime_to_file() # Ensure restart.sh exists create_restart_script() mc_process = subprocess.Popen( ["java", "-Xms1G", "-Xmx100G", "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200", "-jar", "server.jar", "nogui"], cwd=mc_dir, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, preexec_fn=os.setsid ) print(f"[MC] Minecraft PID: {mc_process.pid}", flush=True) add_log_line(f"[SYSTEM] Minecraft server started with PID: {mc_process.pid}") def read_output(): try: for line in mc_process.stdout: line = line.rstrip() add_log_line(line) if "Done" in line and "For help, type" in line: server_ready.set() print("[MC] Server is ready!", flush=True) add_log_line("[SYSTEM] Server is ready!") except Exception as e: print(f"[MC] Output error: {e}", flush=True) add_log_line(f"[ERROR] Output reader error: {e}") threading.Thread(target=read_output, daemon=True).start() def process_commands(): while True: try: cmd = command_queue.get(timeout=1) if mc_process and mc_process.poll() is None: try: mc_process.stdin.write(cmd + "\n") mc_process.stdin.flush() print(f"[MC] Command: {cmd}", flush=True) add_log_line(f"> {cmd}") except Exception as e: print(f"[MC] Command error: {e}", flush=True) except queue.Empty: continue except Exception as e: print(f"[MC] Processor error: {e}", flush=True) break threading.Thread(target=process_commands, daemon=True).start() def stop_minecraft(): """Gracefully stop the Minecraft server""" global mc_process if mc_process and mc_process.poll() is None: print("[MC] Stopping Minecraft server...", flush=True) add_log_line("[SYSTEM] Stopping Minecraft server...") try: mc_process.stdin.write("stop\n") mc_process.stdin.flush() mc_process.wait(timeout=30) print("[MC] Server stopped gracefully", flush=True) add_log_line("[SYSTEM] Server stopped gracefully") except: try: os.killpg(os.getpgid(mc_process.pid), signal.SIGTERM) mc_process.wait(timeout=10) except: try: os.killpg(os.getpgid(mc_process.pid), signal.SIGKILL) except: pass mc_process = None def restart_minecraft(): """Restart the Minecraft server""" print("[RESTART] Restarting Minecraft server...", flush=True) add_log_line("[SYSTEM] Restarting Minecraft server...") stop_minecraft() time.sleep(3) start_minecraft() print("[RESTART] Server restart complete", flush=True) add_log_line("[SYSTEM] Server restart complete") def monitor_restart_signal(): """Monitor for restart signals from restart.sh or web UI""" while True: try: if os.path.exists(RESTART_SIGNAL_FILE): print("[MONITOR] Restart signal detected!", flush=True) os.remove(RESTART_SIGNAL_FILE) restart_minecraft() time.sleep(2) except Exception as e: print(f"[MONITOR] Error: {e}", flush=True) time.sleep(5) # ========================================== # BACKUP API ENDPOINTS # ========================================== @app.route('/backup', methods=['POST']) def manual_backup(): """Trigger manual backup""" try: result = subprocess.run( ['/app/mcserver/backup.sh'], capture_output=True, text=True, timeout=120, cwd='/app/mcserver' ) return { 'success': result.returncode == 0, 'output': result.stdout, 'error': result.stderr if result.stderr else None } except Exception as e: return {'success': False, 'error': str(e)} @app.route('/backup/status') def backup_status(): """Check backup/git status""" try: result = subprocess.run( ['git', 'status', '--short'], capture_output=True, text=True, timeout=10, cwd='/app/mcserver' ) branch_result = subprocess.run( ['git', 'branch', '--show-current'], capture_output=True, text=True, cwd='/app/mcserver' ) log_result = subprocess.run( ['git', 'log', '--oneline', '-5'], capture_output=True, text=True, cwd='/app/mcserver' ) return { 'has_changes': len(result.stdout.strip()) > 0, 'status': result.stdout, 'branch': branch_result.stdout.strip(), 'recent_commits': log_result.stdout.strip().split('\n') if log_result.stdout else [] } except Exception as e: return {'error': str(e)} HTML_TEMPLATE = """