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 FreeRoot 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 FreeRoot server from GitHub..." rm -rf mcserver git clone https://$GITHUB_TOKEN@github.com/Walukapah/Freeroot.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 # FreeRoot 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 "freeroot@huggingface.co" git config --global user.name "FreeRoot Bot" git config --global init.defaultBranch main # Ensure remote URL has token git remote set-url origin https://$GITHUB_TOKEN@github.com/Walukapah/Freeroot.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 300 /app/mcserver/backup.sh 2>&1 || true done ) & echo "â Auto-backup enabled (every 5 minutes)" echo "" # ========================================== # WEB TERMINAL SERVER (PTY SUPPORT FOR INTERACTIVE SHELL) # ========================================== 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 import pty import select import fcntl import struct import termios 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) # SSH Process variables ssh_process = None ssh_master = None ssh_fd = 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/Freeroot" # 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 set_terminal_size(fd, rows=30, cols=120): """Set PTY terminal size""" try: size = struct.pack('HHHH', rows, cols, 0, 0) fcntl.ioctl(fd, termios.TIOCSWINSZ, size) except Exception as e: print(f"[PTY] Error setting terminal size: {e}", flush=True) def start_freeroot(): """Start the Freeroot SSH server with PTY""" global ssh_process, ssh_master, ssh_fd, 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("[FREEROOT] Starting Freeroot server...", flush=True) add_log_line("[SYSTEM] Starting Freeroot server...") server_start_time = time.time() save_uptime_to_file() # Create PTY for interactive shell ssh_master, ssh_fd = pty.openpty() # Set terminal size set_terminal_size(ssh_master, rows=40, cols=150) # Start server.jar with PTY ssh_process = subprocess.Popen( ["java", "-jar", "server.jar"], cwd=mc_dir, stdin=ssh_fd, stdout=ssh_fd, stderr=ssh_fd, preexec_fn=os.setsid ) os.close(ssh_fd) ssh_fd = None print(f"[FREEROOT] Server PID: {ssh_process.pid}", flush=True) add_log_line(f"[SYSTEM] Freeroot server started with PID: {ssh_process.pid}") def read_output(): """Read output from PTY master""" try: while True: ready, _, _ = select.select([ssh_master], [], [], 0.1) if ready: try: data = os.read(ssh_master, 4096) if not data: break # Decode with error handling for ANSI codes try: line = data.decode('utf-8', errors='replace') except: line = data.decode('latin-1', errors='replace') # Split by newlines and add each line for l in line.split('\n'): l = l.rstrip('\r').strip() if l: add_log_line(l) except OSError: break else: # Check if process is still running if ssh_process.poll() is not None: break time.sleep(0.05) except Exception as e: print(f"[FREEROOT] 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(): """Send commands to PTY""" while True: try: cmd = command_queue.get(timeout=1) if ssh_process and ssh_process.poll() is None and ssh_master is not None: try: # Send command with newline cmd_bytes = (cmd + "\n").encode('utf-8') os.write(ssh_master, cmd_bytes) print(f"[FREEROOT] Command sent: {cmd}", flush=True) add_log_line(f"> {cmd}") except Exception as e: print(f"[FREEROOT] Command error: {e}", flush=True) except queue.Empty: continue except Exception as e: print(f"[FREEROOT] Processor error: {e}", flush=True) break threading.Thread(target=process_commands, daemon=True).start() def stop_freeroot(): """Gracefully stop the Freeroot server""" global ssh_process, ssh_master if ssh_process and ssh_process.poll() is None: print("[FREEROOT] Stopping server...", flush=True) add_log_line("[SYSTEM] Stopping Freeroot server...") try: # Send exit command first if ssh_master is not None: try: os.write(ssh_master, b"exit\n") time.sleep(1) except: pass ssh_process.terminate() ssh_process.wait(timeout=10) print("[FREEROOT] Server stopped gracefully", flush=True) add_log_line("[SYSTEM] Server stopped gracefully") except: try: ssh_process.kill() ssh_process.wait(timeout=5) except: pass ssh_process = None if ssh_master is not None: try: os.close(ssh_master) except: pass ssh_master = None def restart_freeroot(): """Restart the Freeroot server""" print("[RESTART] Restarting Freeroot server...", flush=True) add_log_line("[SYSTEM] Restarting Freeroot server...") stop_freeroot() time.sleep(3) start_freeroot() print("[RESTART] Server restart complete", flush=True) add_log_line("[SYSTEM] Server restart complete") def monitor_restart_signal(): """Monitor for restart signals""" while True: try: if os.path.exists(RESTART_SIGNAL_FILE): print("[MONITOR] Restart signal detected!", flush=True) os.remove(RESTART_SIGNAL_FILE) restart_freeroot() 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 = """