import gradio as gr
import os
import subprocess
import threading
import shutil
import spaces
import asyncio
import json
import time
import re
import base64
try:
import psutil
except ImportError:
# هذا احتياطي فقط — يجب أن تكون psutil مثبّتة عبر requirements.txt
subprocess.run(["pip", "install", "psutil"], capture_output=True)
try:
import psutil
except ImportError:
psutil = None # لا نوقف التطبيق بالكامل بسبب فشل تثبيت مكتبة مساعدة
try:
import huggingface_hub
except ImportError:
# هذا احتياطي فقط — يجب أن تكون huggingface_hub مثبّتة عبر requirements.txt
subprocess.run(["pip", "install", "huggingface_hub>=0.20.0"], capture_output=True)
try:
import huggingface_hub
except ImportError:
huggingface_hub = None # ميزة المزامنة السحابية ستُعطَّل بدلاً من تعطيل كل التطبيق
WORKDIR = "/home/user/app/server"
os.makedirs(WORKDIR, exist_ok=True)
log_output = []
mc_process = None
playit_process = None
# ─── Playit Agent الكامل (يدعم كل أنواع التنلات: Java TCP + Bedrock UDP + أي UDP آخر) ───
# بلجن playit-minecraft-plugin.jar بيعمل Agent مصغّر بيدعم تنل الجافا فقط (تلقائي).
# عشان ندعم Bedrock (UDP) كمان، لازم Agent كامل مستقل — وده اللي بيوفره هنا.
PLAYIT_DIR = os.path.join(WORKDIR, "playit")
PLAYIT_BIN = os.path.join(PLAYIT_DIR, "playit-agent")
PLAYIT_SECRET_PATH = os.path.join(PLAYIT_DIR, "playit.toml")
PLAYIT_VERSION = "1.0.10"
PLAYIT_DOWNLOAD_URL = f"https://github.com/playit-cloud/playit-agent/releases/download/v{PLAYIT_VERSION}/playit-linux-amd64"
# ─── نظام الصلاحيات والمستخدمين ───
USERS_FILE = os.path.join(WORKDIR, "users.json")
PERMISSIONS = {
"admin": ["console", "start", "stop", "restart", "files_read", "files_write", "files_delete", "users_manage", "cloud_manage"],
"editor": ["console", "files_read", "files_write", "cloud_manage"],
"viewer": ["console", "files_read"],
}
def load_users():
try:
if os.path.exists(USERS_FILE):
with open(USERS_FILE) as f:
return json.load(f)
except: pass
return {}
def save_users(users):
try:
with open(USERS_FILE, "w") as f:
json.dump(users, f, indent=2)
except: pass
def get_user_role(username):
if not username:
return None
users = load_users()
return users.get(username, None)
# ⚠️ ملاحظة أمنية مهمة:
# هذا التطبيق لا يحتوي على نظام تسجيل دخول حقيقي — لا يوجد أي مكان في الواجهة
# يُحدَّث فيه current_user باسم مستخدم فعلي (يبقى دائماً None). لذلك عندما تكون
# LOCAL_DEV_MODE = False، كل الأزرار (Start/Stop/Files/...) سترفض التنفيذ بصمت
# لأن has_perm(None, ...) ستُرجع False دائماً.
# لهذا السبب أعدناها إلى True افتراضياً ليعمل التطبيق كما صُمم (مستخدم واحد يديره
# صاحب الـ Space). إذا كان الـ Space عاماً ويزوره أشخاص آخرون، الحل الصحيح هو
# حماية الوصول عبر demo.launch(auth=("username","password")) أو عبر جعل الـ
# Space خاصاً (Private) من إعدادات الريبو، وليس فقط عبر متغير LOCAL_DEV_MODE.
LOCAL_DEV_MODE = os.environ.get("LOCAL_DEV_MODE", "true").strip().lower() in ("1", "true", "yes")
def has_perm(username, perm):
if username is None:
return LOCAL_DEV_MODE
role = get_user_role(username)
if role is None:
return False
return perm in PERMISSIONS.get(role, [])
@spaces.GPU
def _zerogpu_probe():
"""
دالة صورية فقط لتلبية شرط منصة Hugging Face على عتاد ZeroGPU، والتي تتطلب
وجود دالة واحدة على الأقل معرّفة بـ @spaces.GPU وقت الإقلاع، وإلا فشل الـ Space
بالكامل بخطأ "No @spaces.GPU function detected during startup".
عمداً لا تُستدعى هذه الدالة من أي مكان في التطبيق (لا حاجة فعلية لـ GPU هنا)،
لتفادي استهلاك حصة GPU المجانية أو التسبب بأخطاء متكررة مع كل زيارة.
إذا تم تغيير عتاد الـ Space إلى CPU، يمكن حذف هذه الدالة وسطر `import spaces` بأمان.
"""
return "[SYSTEM] GPU probe (not used)."
def log(message):
log_output.append(message)
if len(log_output) > 400:
log_output.pop(0)
def run_thread(target, *args, name=None, **kwargs):
"""
يشغّل target داخل خيط منفصل مع التقاط أي استثناء غير متوقع وتسجيله في اللوج،
بدلاً من أن يموت الخيط بصمت (وهو ما كان يسبب ظهور تعطّل غير مفسَّر في الواجهة).
"""
def _wrapper():
try:
target(*args, **kwargs)
except Exception as e:
log(f"[THREAD ERROR] فشل غير متوقع في المهمة الخلفية [{name or target.__name__}]: {e}")
t = threading.Thread(target=_wrapper, daemon=True)
t.start()
return t
# قفل لمنع تزامن عمليتي رفع (push_to_dataset) في نفس الوقت — يمنع تعارض/فساد الرفع
_push_lock = threading.Lock()
start_time = time.time()
_last_auto_sync_time = time.time()
# ─── محرك التزامن المباشر مع Hugging Face Dataset (Server/) ───
HF_CONFIG_FILE = os.path.join(WORKDIR, "hf_sync.json")
def load_hf_config():
try:
if os.path.exists(HF_CONFIG_FILE):
with open(HF_CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except: pass
return {
"hf_token": os.environ.get("HF_TOKEN", "").strip(),
"hf_dataset_repo": os.environ.get("HF_DATASET_REPO", "").strip(),
"auto_sync": True,
"sync_interval": 10
}
def save_hf_config(config_dict):
try:
with open(HF_CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config_dict, f, indent=2, ensure_ascii=False)
return True, "✅ تم حفظ إعدادات التزامن المباشر مع الـ Dataset بنجاح!"
except Exception as e:
return False, f"❌ فشل حفظ الإعدادات: {e}"
def get_full_repo_id(repo_id, token):
"""
دالة لتصحيح اسم المستودع تلقائياً إذا تم إدخاله بدون اسم المستخدم (مثل: SAW).
"""
if not repo_id:
return None
if "/" not in repo_id and token:
try:
from huggingface_hub import HfApi
api = HfApi(token=token)
user_info = api.whoami()
username = user_info.get("name")
if username:
resolved = f"{username}/{repo_id}"
log(f"[DATASET SYNC] 🔄 تم تصحيح اسم المستودع تلقائياً إلى: {resolved}")
return resolved
except Exception as e:
log(f"[DATASET SYNC WARNING] لم نتمكن من التعرف على اسم المستخدم لتصحيح المستودع تلقائياً: {e}")
return repo_id
def init_dataset_and_pull():
"""
1. التحقق من وجود الـ Dataset على Hugging Face أو إنشاؤه فوراً باسم المستخدم الصحيح.
2. سحب وتنزيل محتويات مجلد Server/ إلى WORKDIR.
"""
if huggingface_hub is None:
return False, "⚠️ مكتبة huggingface_hub غير متاحة — سيتم العمل على المجلد المحلي فقط."
config = load_hf_config()
token = config.get("hf_token") or os.environ.get("HF_TOKEN", "").strip()
repo_id = config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO", "").strip()
if not token or not repo_id:
msg = "⚠️ التزامن المباشر: لم يتم ضبط HF_TOKEN أو HF_DATASET_REPO. سيتم العمل على المجلد المحلي فقط."
log(f"[DATASET SYNC] {msg}")
return False, msg
resolved_repo_id = get_full_repo_id(repo_id, token)
if not resolved_repo_id:
return False, "❌ اسم مستودع الـ Dataset غير صالح."
try:
from huggingface_hub import HfApi, snapshot_download
api = HfApi(token=token)
# 1. إنشاء الـ Dataset أو التحقق منه
try:
log(f"[DATASET SYNC] 🛡️ التحقق من وجود الـ Dataset [{resolved_repo_id}] أو إنشاؤه الآن...")
api.create_repo(repo_id=resolved_repo_id, repo_type="dataset", exist_ok=True, private=True, token=token)
log(f"[DATASET SYNC] ✅ الـ Dataset [{resolved_repo_id}] جاهز ومربوط.")
except Exception as create_err:
err_str = str(create_err)
if "403" in err_str or "401" in err_str:
msg = f"❌ خطأ صلاحيات (Forbidden/Unauthorized) عند التحقق من الـ Dataset [{resolved_repo_id}]: تأكد من صلاحية Write للتوكن الخاص بك."
log(f"[DATASET SYNC ERROR] {msg}")
return False, msg
# في حال وجود خطأ آخر حرج نوقف العملية ولا نحاول التحميل أو الرفع
msg = f"❌ فشل تهيئة مستودع الـ Dataset [{resolved_repo_id}]: {create_err}"
log(f"[DATASET SYNC ERROR] {msg}")
return False, msg
# 2. تنزيل وسحب الملفات
local_tmp = os.path.join(WORKDIR, "../_cloud_restore_tmp")
if os.path.exists(local_tmp): shutil.rmtree(local_tmp, ignore_errors=True)
os.makedirs(local_tmp, exist_ok=True)
try:
log(f"[DATASET SYNC] 📥 جاري تنزيل ملفات السيرفر من المجلد Server/ في الـ Dataset [{resolved_repo_id}] ...")
snapshot_download(
repo_id=resolved_repo_id,
repo_type="dataset",
allow_patterns=["Server/*"],
local_dir=local_tmp,
token=token
)
server_cloud_dir = os.path.join(local_tmp, "Server")
if os.path.exists(server_cloud_dir) and os.listdir(server_cloud_dir):
file_count = 0
for root, dirs, files in os.walk(server_cloud_dir):
rel_path = os.path.relpath(root, server_cloud_dir)
dest_dir = os.path.join(WORKDIR, rel_path) if rel_path != "." else WORKDIR
os.makedirs(dest_dir, exist_ok=True)
for f in files:
shutil.copy2(os.path.join(root, f), os.path.join(dest_dir, f))
file_count += 1
shutil.rmtree(local_tmp, ignore_errors=True)
msg = f"✅ تم تنزيل ({file_count}) ملف من المجلد Server/ داخل الـ Dataset [{resolved_repo_id}] بنجاح!"
log(f"[DATASET SYNC] {msg}")
return True, msg
else:
shutil.rmtree(local_tmp, ignore_errors=True)
log(f"[DATASET SYNC] ℹ️ المجلد Server/ غير موجود بعد في الـ Dataset [{resolved_repo_id}]. سيتم إنشاؤه ورفع الملفات الآن...")
push_to_dataset()
return True, f"✅ تم إنشاء المجلد Server/ داخل الـ Dataset [{resolved_repo_id}] ومزامنة الملفات."
except Exception as inner_e:
shutil.rmtree(local_tmp, ignore_errors=True)
log(f"[DATASET SYNC] المجلد Server غير متوفر بعد، جاري التأسيس والرفع الأولي: {inner_e}")
push_to_dataset()
return True, f"✅ تم تهيئة المجلد Server/ في الـ Dataset [{resolved_repo_id}]."
except Exception as e:
err_msg = f"❌ خطأ في الاتصال بالـ Dataset [{resolved_repo_id}]: {e}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
def push_to_dataset():
"""
مزامنة ورفع كل الملفات مباشرة في المجلد Server/ داخل الـ Dataset.
"""
if huggingface_hub is None:
return False, "⚠️ مكتبة huggingface_hub غير متاحة — تعذّر تفعيل المزامنة السحابية."
if not _push_lock.acquire(blocking=False):
log("[DATASET SYNC] ⏳ عملية رفع أخرى قيد التنفيذ بالفعل — تم تجاهل هذا الطلب لتفادي التعارض.")
return False, "⏳ عملية مزامنة أخرى قيد التنفيذ بالفعل، حاول لاحقاً."
try:
return _push_to_dataset_locked()
finally:
_push_lock.release()
def _push_to_dataset_locked():
config = load_hf_config()
token = config.get("hf_token") or os.environ.get("HF_TOKEN", "").strip()
repo_id = config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO", "").strip()
if not token or not repo_id:
return False, "⚠️ أدخل HF Token واسم الـ Dataset أولاً في تبويب '☁️ التزامن المباشر'."
resolved_repo_id = get_full_repo_id(repo_id, token)
if not resolved_repo_id:
return False, "❌ اسم مستودع الـ Dataset غير صالح."
try:
log(f"[DATASET SYNC] 📤 جاري مزامنة ورفع ملفات السيرفر وإعدادات الـ IP إلى [{resolved_repo_id}]/Server ...")
from huggingface_hub import HfApi
api = HfApi(token=token)
# التحقق من وجود المستودع قبل محاولة الرفع لتلافي أخطاء الـ 404
try:
api.create_repo(repo_id=resolved_repo_id, repo_type="dataset", exist_ok=True, private=True, token=token)
except Exception as ce:
err_str = str(ce)
if "403" in err_str or "401" in err_str or "404" in err_str:
err_msg = f"❌ خطأ أذونات أو عدم وجود المستودع ({ce}) عند التحقق من الـ Dataset [{resolved_repo_id}]."
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
# في حال وجود مشكلة تأسيسية أخرى نلغي الرفع لمنع تكرار الأخطاء السابقة
err_msg = f"❌ فشل تأسيس المستودع [{resolved_repo_id}]: {ce}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
ignore_list = [
"jdk/*", "jdk.tar.gz", "logs/*", "*.lock", "*.tmp", ".cache/*", "_cloud_restore_tmp/*"
]
api.upload_folder(
folder_path=WORKDIR,
path_in_repo="Server",
repo_id=resolved_repo_id,
repo_type="dataset",
token=token,
ignore_patterns=ignore_list,
commit_message=f"Live sync server files & Playit IP -> Server/ ({time.strftime('%Y-%m-%d %H:%M:%S')})"
)
msg = f"✅ تم مزامنة ورفع جميع ملفات السيرفر مباشرة إلى المجلد Server/ في الـ Dataset [{resolved_repo_id}]!"
log(f"[DATASET SYNC] {msg}")
return True, msg
except Exception as e:
err_msg = f"❌ خطأ أثناء الرفع إلى المجلد Server داخل الـ Dataset [{resolved_repo_id}]: {e}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
def dataset_sync_worker():
global _last_auto_sync_time
while True:
time.sleep(30)
try:
config = load_hf_config()
if config.get("auto_sync", True) and (config.get("hf_token") or os.environ.get("HF_TOKEN")) and (config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO")):
interval_mins = int(config.get("sync_interval", 10))
if interval_mins < 1: interval_mins = 10
if time.time() - _last_auto_sync_time >= interval_mins * 60:
_last_auto_sync_time = time.time()
if mc_process and mc_process.poll() is None:
log(f"[DATASET SYNC] 🕒 التزامن الدوري التلقائي مع المجلد Server في الـ Dataset (كل {interval_mins} دقيقة)...")
push_to_dataset()
except Exception as e:
log(f"[DATASET SYNC] Worker error: {e}")
run_thread(dataset_sync_worker, name="dataset_sync_worker")
def get_system_resources():
try:
cpu = psutil.cpu_percent(interval=0.1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
uptime_sec = int(time.time() - start_time)
days = uptime_sec // 86400
hours = (uptime_sec % 86400) // 3600
mins = (uptime_sec % 3600) // 60
uptime_str = f"{days}d {hours}h {mins}m" if days else f"{hours}h {mins}m"
return {
"cpu": round(cpu, 1),
"ram_used": round(mem.used / (1024**3), 1),
"ram_total": round(mem.total / (1024**3), 1),
"ram_pct": round(mem.percent, 1),
"disk_used": round(disk.used / (1024**3), 1),
"disk_total": round(disk.total / (1024**3), 1),
"disk_pct": round(disk.percent, 1),
"uptime": uptime_str,
}
except:
return {"cpu": 0, "ram_used": 0, "ram_total": 4, "ram_pct": 0, "disk_used": 0, "disk_total": 20, "disk_pct": 0, "uptime": "0m"}
def get_online_players():
try:
joined = set()
left = set()
lines = log_output[-300:]
for line in lines:
m = re.search(r"(\S+)\s+joined the game", line)
if m: joined.add(m.group(1))
m = re.search(r"(\S+)\s+left the game", line)
if m: left.add(m.group(1))
return list(joined - left)
except: return []
def send_console_command(cmd, user=None):
if not has_perm(user, "console"):
return "⛔ You don't have `console` permission"
if not cmd or not cmd.strip():
return "⚠️ Enter a command"
if not (mc_process and mc_process.poll() is None):
return "⚠️ Server is not running"
try:
mc_process.stdin.write(cmd.strip() + "\n")
mc_process.stdin.flush()
log(f"[CONSOLE] > {cmd.strip()}")
return f"✅ Command sent: `{cmd.strip()}`"
except Exception as e:
return f"❌ Failed to send command: {e}"
def safe_download(url, dest_path, label, timeout=180):
try:
result = subprocess.run(["wget", "-q", "-O", dest_path, url], capture_output=True, text=True, timeout=timeout)
if result.returncode != 0 or not os.path.exists(dest_path) or os.path.getsize(dest_path) == 0:
log(f"[ERROR] فشل تحميل {label}: {result.stderr.strip()[:200] if result.stderr else 'رابط غير صالح'}")
if os.path.exists(dest_path): os.remove(dest_path)
return False
log(f"[SYSTEM] تم تحميل {label} بنجاح.")
return True
except subprocess.TimeoutExpired:
log(f"[ERROR] انتهت مهلة تحميل {label} (timeout).")
if os.path.exists(dest_path): os.remove(dest_path)
return False
except Exception as e:
log(f"[ERROR] خطأ أثناء تحميل {label}: {e}")
if os.path.exists(dest_path): os.remove(dest_path)
return False
def download_dependencies():
if not os.path.exists(os.path.join(WORKDIR, "jdk")):
jdk_tar = os.path.join(WORKDIR, "jdk.tar.gz")
jdk_url = "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.4%2B7/OpenJDK21U-jre_x64_linux_hotspot_21.0.4_7.tar.gz"
if safe_download(jdk_url, jdk_tar, "Java 21 (JDK)"):
try:
subprocess.run(["tar", "-xzf", jdk_tar, "-C", WORKDIR], check=True, capture_output=True, text=True)
for extracted in os.listdir(WORKDIR):
if extracted.startswith("jdk-21"):
os.rename(os.path.join(WORKDIR, extracted), os.path.join(WORKDIR, "jdk"))
os.remove(jdk_tar)
except Exception as e:
log(f"[ERROR] فشل استخراج JDK: {e}")
if not os.path.exists(os.path.join(WORKDIR, "paper.jar")):
safe_download("https://api.purpurmc.org/v2/purpur/1.21.1/latest/download", os.path.join(WORKDIR, "paper.jar"), "Purpur Server (1.21.1)")
plugins_dir = os.path.join(WORKDIR, "plugins")
os.makedirs(plugins_dir, exist_ok=True)
if not os.path.exists(os.path.join(plugins_dir, "playit.jar")):
safe_download("https://github.com/playit-cloud/playit-minecraft-plugin/releases/latest/download/playit-minecraft-plugin.jar", os.path.join(plugins_dir, "playit.jar"), "Playit Plugin")
if not os.path.exists(os.path.join(plugins_dir, "ViaVersion.jar")):
safe_download("https://cdn.modrinth.com/data/P1OZGk5p/versions/ruzmiBqe/ViaVersion-5.10.0.jar", os.path.join(plugins_dir, "ViaVersion.jar"), "ViaVersion Plugin")
if not os.path.exists(os.path.join(plugins_dir, "ViaBackwards.jar")):
safe_download("https://cdn.modrinth.com/data/NpvuJQoq/versions/YjpKsm6j/ViaBackwards-5.10.0.jar", os.path.join(plugins_dir, "ViaBackwards.jar"), "ViaBackwards Plugin")
if not os.path.exists(os.path.join(plugins_dir, "Geyser-Spigot.jar")):
safe_download("https://cdn.modrinth.com/data/wKkoqHrH/versions/RHInMdHJ/Geyser-Spigot.jar", os.path.join(plugins_dir, "Geyser-Spigot.jar"), "Geyser-Spigot Plugin")
if not os.path.exists(os.path.join(plugins_dir, "floodgate.jar")):
safe_download("https://download.geysermc.org/v2/projects/floodgate/versions/latest/builds/latest/downloads/spigot", os.path.join(plugins_dir, "floodgate.jar"), "Floodgate Plugin")
eula_p = os.path.join(WORKDIR, "eula.txt")
if not os.path.exists(eula_p):
with open(eula_p, "w") as f: f.write("eula=true\n")
props_p = os.path.join(WORKDIR, "server.properties")
if not os.path.exists(props_p):
log("[SYSTEM] Configuring server.properties for cracked mode...")
with open(props_p, "w") as f: f.write("online-mode=false\nspawn-protection=0\n")
def _detect_container_memory_limit_gb():
"""
يحاول قراءة حد الذاكرة الفعلي المفروض على الحاوية نفسها (cgroup)، لأن psutil
أحياناً (خصوصاً داخل حاويات مثل HF Spaces) يُرجع ذاكرة الجهاز المضيف الكاملة
بدل حد الحاوية الفعلي — وهو ما تسبب سابقاً في قيمة خيالية مثل -Xmx999G وكاد
يتسبب في مشاكل خطيرة.
"""
for path in ("/sys/fs/cgroup/memory.max", "/sys/fs/cgroup/memory/memory.limit_in_bytes"):
try:
with open(path) as f:
raw = f.read().strip()
if raw and raw != "max":
val_gb = int(raw) / (1024**3)
if 0 < val_gb < 1024: # قيمة معقولة (أقل من تيرابايت) وإلا نتجاهلها
return val_gb
except Exception:
continue
return None
def get_safe_jvm_heap():
"""
يحدد قيم -Xms/-Xmx بأمان بناءً على الذاكرة الفعلية المتاحة على عتاد الـ Space،
بدلاً من قيمة ثابتة (4GB) قد تتجاوز حد الحاوية وتسبب OOM Kill مفاجئ للـ Space.
يمكن التحكم بها يدوياً عبر متغيرات البيئة MC_XMS / MC_XMX عند الحاجة.
"""
env_xms, env_xmx = os.environ.get("MC_XMS"), os.environ.get("MC_XMX")
if env_xms and env_xmx:
return env_xms, env_xmx
cgroup_gb = _detect_container_memory_limit_gb()
try:
psutil_gb = psutil.virtual_memory().total / (1024**3) if psutil else 4
except Exception:
psutil_gb = 4
# نفضّل حد الـ cgroup (حد الحاوية الفعلي) لو متاح ومنطقي، بدل رقم الجهاز المضيف الكامل
total_gb = cgroup_gb if cgroup_gb else psutil_gb
# نترك هامشاً كافياً لنظام التشغيل وGradio وعمليات المزامنة، ولا نستهلك كل الذاكرة على الـ heap
safe_max_gb = max(1, int(total_gb * 0.5))
# ⚠️ سقف أمان صارم غير قابل للتجاوز مهما كانت نتيجة الاكتشاف أعلاه — هذا يمنع تكرار
# مشكلة -Xmx999G في حال أعطت أي طريقة اكتشاف قيمة غير موثوقة على أي منصة مستقبلاً.
HARD_CAP_GB = 8
safe_max_gb = min(safe_max_gb, HARD_CAP_GB)
xmx = f"{safe_max_gb}G"
xms = f"{max(1, safe_max_gb // 2)}G"
return xms, xmx
def download_playit_agent():
"""
ينزّل الوكيل الكامل (playit-agent) مرة واحدة فقط ويعطيه صلاحية تنفيذ.
"""
os.makedirs(PLAYIT_DIR, exist_ok=True)
if os.path.isfile(PLAYIT_BIN) and os.access(PLAYIT_BIN, os.X_OK):
return True
ok = safe_download(PLAYIT_DOWNLOAD_URL, PLAYIT_BIN, f"Playit Full Agent v{PLAYIT_VERSION}")
if ok:
try:
os.chmod(PLAYIT_BIN, 0o755)
except Exception as e:
log(f"[PLAYIT ERROR] فشل ضبط صلاحيات تشغيل playit agent: {e}")
return False
return ok
def start_playit_agent():
"""
يشغّل playit agent الكامل (منفصل تماماً عن بلجن playit-minecraft-plugin.jar).
أول مرة: هيطبع رابط claim في اللوج — افتحه في المتصفح وسجّل دخول لربط الـ Agent بحسابك.
بعدها من https://playit.gg/tunnels اعمل تنلين على نفس الـ Agent الجديد ده:
1) Minecraft Java (TCP) -> local port 25565
2) Minecraft Bedrock/UDP -> local port 19132
المرات الجاية: السر (secret) هيتحفظ تلقائياً في playit/playit.toml (بيتزامن مع الـ
Dataset زي باقي ملفات السيرفر)، فمش هيحتاج claim تاني بعد إعادة تشغيل الـ Space.
"""
global playit_process
if playit_process and playit_process.poll() is None:
log("[PLAYIT] الوكيل الكامل شغال بالفعل.")
return
if not download_playit_agent():
log("[PLAYIT ERROR] ❌ فشل تحميل playit agent الكامل — تنل Bedrock لن يعمل تلقائياً. أعد المحاولة بالضغط على Start.")
return
try:
log("[PLAYIT] 🚀 جاري تشغيل playit agent الكامل (يدعم Java + Bedrock + أي UDP)...")
playit_process = subprocess.Popen(
[PLAYIT_BIN, "--secret-path", PLAYIT_SECRET_PATH],
cwd=PLAYIT_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
)
for line in iter(playit_process.stdout.readline, ''):
if not line:
continue
clean = line.strip()
log(f"[PLAYIT] {clean}")
if "playit.gg/claim" in clean:
log("[PLAYIT] ⚠️ افتح الرابط اللي فوق في المتصفح وسجّل دخول لتفعيل الـ Agent. بعدين من https://playit.gg/tunnels اعمل تنل Java (TCP 25565) وتنل Bedrock (UDP 19132) على نفس الـ Agent ده.")
playit_process.stdout.close()
exit_code = playit_process.poll()
log(f"[PLAYIT] توقف وكيل playit الكامل (exit code: {exit_code}).")
except Exception as e:
log(f"[PLAYIT ERROR] فشل تشغيل playit agent: {e}")
finally:
playit_process = None
def start_server_backend():
global mc_process
if mc_process and mc_process.poll() is None:
log("[SYSTEM] Server is already running!")
return
try:
# 1. تنزيل وسحب محتويات المجلد Server/ من الـ Dataset
init_dataset_and_pull()
# 2. تنزيل التبعيات المطلوبة في حال عدم وجودها
download_dependencies()
# 3. مزامنة فورية لتأكيد وجود المجلد Server/ والملفات داخل الـ Dataset قبل بدء ماين كرافت
push_to_dataset()
# 4. تشغيل الوكيل الكامل لـ playit بخيط منفصل (يدعم Bedrock/UDP بجانب تنل الجافا التلقائي)
run_thread(start_playit_agent, name="start_playit_agent")
java_path = os.path.join(WORKDIR, "jdk/bin/java")
paper_path = os.path.join(WORKDIR, "paper.jar")
# فحص واضح قبل التشغيل بدل السماح لـ FileNotFoundError بإسقاط الخيط بصمت
if not os.path.isfile(java_path):
log("[ERROR] ❌ تعذّر إيجاد Java (jdk/bin/java) — فشل تنزيل JDK على الأرجح. تحقق من اتصال الإنترنت وأعد المحاولة.")
return
if not os.path.isfile(paper_path):
log("[ERROR] ❌ تعذّر إيجاد paper.jar — فشل تنزيل سيرفر Purpur. تحقق من رابط التنزيل وأعد المحاولة.")
return
xms, xmx = get_safe_jvm_heap()
log(f"[SYSTEM] Starting Minecraft Server (Purpur 1.21.1) with -Xms{xms} -Xmx{xmx} ...")
mc_process = subprocess.Popen(
[java_path, f"-Xms{xms}", f"-Xmx{xmx}", "-jar", "paper.jar", "nogui"],
cwd=WORKDIR, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
)
for line in iter(mc_process.stdout.readline, ''):
if line: log(f"[MINECRAFT] {line.strip()}")
mc_process.stdout.close()
exit_code = mc_process.poll()
log(f"[SYSTEM] ⚠️ توقفت عملية Minecraft (exit code: {exit_code}). سيتم تحديث الحالة إلى 'متوقف'.")
except Exception as e:
log(f"[ERROR] ❌ فشل غير متوقع أثناء تشغيل السيرفر: {e}")
finally:
# مهم: بدون هذا كانت الواجهة تستمر بعرض "Running" رغم توقف العملية فعلياً
mc_process = None
run_thread(push_to_dataset, name="push_to_dataset_after_stop")
def control_server(action, user):
perm = action.lower()
if not has_perm(user, perm):
return f"⛔ You don't have `{perm}` permission"
global mc_process
if action == "START":
if mc_process and mc_process.poll() is None:
return "❌ السيرفر يعمل بالفعل الآن!"
run_thread(start_server_backend, name="start_server_backend")
return "⚡ جاري تشغيل خادم ماين كرافت الآن (مع تنزيل ورفع الملفات مباشرة من الـ Dataset)..."
elif action == "STOP":
if mc_process and mc_process.poll() is None:
try:
mc_process.stdin.write("/stop\n")
mc_process.stdin.flush()
log("[SYSTEM] Sent /stop command — waiting for graceful shutdown...")
except: pass
deadline = time.time() + 8
while time.time() < deadline:
if mc_process.poll() is not None: break
time.sleep(0.5)
if mc_process and mc_process.poll() is None:
mc_process.terminate()
time.sleep(1)
mc_process = None
log("[SYSTEM] Server stopped by user.")
# مزامنة محتويات السيرفر فوراً إلى مجلد Server داخل الـ Dataset عند الإيقاف
log("[SYSTEM] Syncing directly to Server/ inside Dataset upon stop...")
run_thread(push_to_dataset, name="push_to_dataset")
return "🛑 Server stopped gracefully — Synced directly to Dataset/Server."
return "⚠️ Server is already stopped."
elif action == "RESTART":
if mc_process and mc_process.poll() is None:
mc_process.terminate()
mc_process = None
log("[SYSTEM] Restarting server...")
run_thread(start_server_backend, name="start_server_backend")
return "🔄 جاري إعادة تشغيل السيرفر..."
async def get_logs():
return "\n".join(log_output)
async def get_server_status():
status = "🟢 **Running**" if (mc_process and mc_process.poll() is None) else "🔴 **Stopped**"
return f"## {status}\n\n**Purpur 1.21.1** — Java 21 — 2-4GB RAM"
BINARY_EXTENSIONS = {
".jar", ".zip", ".gz", ".tar", ".rar", ".7z", ".class",
".dat", ".mca", ".mcr", ".png", ".jpg", ".jpeg", ".gif",
".ico", ".exe", ".bin", ".db", ".so", ".dll"
}
MAX_EDITABLE_SIZE = 200 * 1024
PAGE_SIZE = 200
def get_safe_path(rel_path):
if rel_path is None: rel_path = ""
abs_path = os.path.abspath(os.path.join(WORKDIR, rel_path))
if abs_path.startswith(os.path.abspath(WORKDIR)): return abs_path
return WORKDIR
async def scan_directory(current_rel):
target_dir = get_safe_path(current_rel)
folders = []
text_files = []
binary_count = 0
try:
entries = await asyncio.wait_for(asyncio.to_thread(lambda: list(os.listdir(target_dir))), timeout=5.0)
for item in entries:
full_path = os.path.join(target_dir, item)
try:
if await asyncio.to_thread(os.path.isdir, full_path): folders.append(item)
else:
ext = os.path.splitext(item)[1].lower()
if ext in BINARY_EXTENSIONS: binary_count += 1
else: text_files.append(item)
except: text_files.append(item)
except Exception as e:
log(f"[FILE MGR ERROR] {e}")
folders.sort()
text_files.sort()
folder_choices = [".. (المجلد الأعلى)"] + folders if current_rel else folders
file_choices = text_files if text_files else ["لا توجد ملفات نصية في هذا المسار"]
path_display = f"📁 المسار الحالي المفتوح: /root/{current_rel}" if current_rel else "📁 المسار الحالي المفتوح: /root (المجلد الرئيسي)"
if binary_count > 0: path_display += f" \n📎 {binary_count} ملف ثنائي مخفي (jar/zip/png...)"
return path_display, gr.Dropdown(choices=folder_choices, value=None), gr.Dropdown(choices=file_choices, value=None)
async def enter_folder(selected_folder, current_rel):
if not selected_folder: return current_rel, *await scan_directory(current_rel)
if selected_folder == ".. (المجلد الأعلى)":
if not current_rel: return "", *await scan_directory("")
parent_rel = "/".join(current_rel.strip("/").split("/")[:-1])
return parent_rel, *await scan_directory(parent_rel)
else:
new_rel = os.path.join(current_rel, selected_folder).strip("/")
return new_rel, *await scan_directory(new_rel)
def is_probably_binary(file_path, chunk_size=1024):
try:
with open(file_path, "rb") as f: return b"\x00" in f.read(chunk_size)
except: return True
async def load_file_content(selected_file, current_rel):
empty, title_reset = gr.update(value=""), gr.update(value="### 📝 تعديل الملف: —")
if not selected_file or "لا توجد ملفات" in selected_file:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value="👈 اختر ملفاً نصياً من القائمة ليظهر صندوق التعديل هنا."), "", 0, gr.update(visible=False)
file_path = os.path.join(get_safe_path(current_rel), selected_file)
if not os.path.isfile(file_path):
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"⚠️ [{selected_file}] مجلد وليس ملفاً."), "", 0, gr.update(visible=False)
ext = os.path.splitext(selected_file)[1].lower()
if ext in BINARY_EXTENSIONS:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"🚫 الملف [{selected_file}] ثنائي (Binary) ولا يمكن تحريره نصياً."), "", 0, gr.update(visible=False)
try: size = await asyncio.to_thread(os.path.getsize, file_path)
except Exception as e: return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"خطأ أثناء قراءة الحجم: {e}"), "", 0, gr.update(visible=False)
if size > MAX_EDITABLE_SIZE:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"🚫 الملف [{selected_file}] حجمه كبير جداً ({size/1024/1024:.2f} MB)."), "", 0, gr.update(visible=False)
if await asyncio.to_thread(is_probably_binary, file_path):
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"🚫 الملف [{selected_file}] يحتوي على بيانات ثنائية."), "", 0, gr.update(visible=False)
try:
content = await asyncio.to_thread(lambda: open(file_path, "r", encoding="utf-8", errors="ignore").read())
lines = content.splitlines(keepends=True)
total_lines, display_lines = len(lines), lines[:PAGE_SIZE]
has_more = total_lines > PAGE_SIZE
info = f"### 📝 تعديل الملف: `{selected_file}`" + (f" — (أول {PAGE_SIZE} من {total_lines} سطر)" if has_more else "")
return gr.update(value="".join(display_lines)), gr.update(value=info), gr.update(visible=True), gr.update(visible=False), file_path, PAGE_SIZE, gr.update(visible=has_more)
except Exception as e:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"خطأ أثناء القراءة: {e}"), "", 0, gr.update(visible=False)
async def load_more_content(file_path, load_offset):
if not file_path or not os.path.isfile(file_path): return gr.update(), 0, gr.update(visible=False)
content = await asyncio.to_thread(lambda: open(file_path, "r", encoding="utf-8", errors="ignore").read())
lines = content.splitlines(keepends=True)
total, new_offset = len(lines), min(load_offset + PAGE_SIZE, len(lines))
return gr.update(value="".join(lines[:new_offset])), new_offset, gr.update(visible=new_offset < total)
async def save_file_content(selected_file, content, current_rel, user):
if not has_perm(user, "files_write"): return "⛔ You don't have `files_write` permission"
if not selected_file or "لا توجد ملفات" in selected_file: return "❌ لم يتم تحديد ملف صالح لحفظه."
file_path = os.path.join(get_safe_path(current_rel), selected_file)
try:
await asyncio.to_thread(lambda: open(file_path, "w", encoding="utf-8").write(content))
run_thread(push_to_dataset, name="push_to_dataset")
return f"✅ تم حفظ الملف [{selected_file}] ومزامنتُه مع الـ Dataset!"
except Exception as e: return f"❌ فشل الحفظ: {e}"
async def create_item(name, item_type, current_rel, user):
if not has_perm(user, "files_write"): return "⛔ You don't have `files_write` permission", *await scan_directory(current_rel)
if not name: return "⚠️ الرجاء إدخال اسم المجلد أو الملف أولاً.", *await scan_directory(current_rel)
target_path = os.path.join(get_safe_path(current_rel), name)
try:
if "Folder" in item_type or "مجلد" in item_type:
await asyncio.to_thread(os.makedirs, target_path, exist_ok=True)
msg = f"📁 Created folder [{name}]"
else:
await asyncio.to_thread(lambda: open(target_path, "w", encoding="utf-8").write(""))
msg = f"📄 Created file [{name}]"
run_thread(push_to_dataset, name="push_to_dataset")
return msg, *await scan_directory(current_rel)
except Exception as e: return f"❌ فشل الإنشاء: {e}", *await scan_directory(current_rel)
async def rename_item(old_name, new_name, current_rel, user):
if not has_perm(user, "files_write"): return "⛔ You don't have `files_write` permission", *await scan_directory(current_rel)
if not old_name or not new_name: return "⚠️ حدد العنصر واكتب الاسم الجديد.", *await scan_directory(current_rel)
try:
await asyncio.to_thread(os.rename, os.path.join(get_safe_path(current_rel), old_name), os.path.join(get_safe_path(current_rel), new_name))
run_thread(push_to_dataset, name="push_to_dataset")
return f"✏️ تم إعادة التسمية بنجاح إلى [{new_name}].", *await scan_directory(current_rel)
except Exception as e: return f"❌ خطأ أثناء التسمية: {e}", *await scan_directory(current_rel)
async def delete_item(item_name, is_folder, current_rel, user):
if not has_perm(user, "files_delete"): return "⛔ You don't have `files_delete` permission", *await scan_directory(current_rel)
if not item_name or "لا توجد ملفات" in item_name: return "⚠️ لم يتم تحديد عنصر لحذفه.", *await scan_directory(current_rel)
target_p = os.path.join(get_safe_path(current_rel), item_name)
try:
if is_folder:
await asyncio.to_thread(shutil.rmtree, target_p)
msg = f"🗑️ تم حذف المجلد ومحتوياته [{item_name}] بالكامل."
else:
await asyncio.to_thread(os.remove, target_p)
msg = f"🗑️ تم حذف الملف [{item_name}]."
run_thread(push_to_dataset, name="push_to_dataset")
return msg, *await scan_directory(current_rel)
except Exception as e: return f"❌ فشل الحذف: {e}", *await scan_directory(current_rel)
async def upload_to_current_dir(file_objs, current_rel, user):
if not has_perm(user, "files_write"): return "⛔ You don't have `files_write` permission", *await scan_directory(current_rel)
if not file_objs: return "❌ لم يتم اختيار ملفات للرفع.", *await scan_directory(current_rel)
target_dir, uploaded_list = get_safe_path(current_rel), []
if not isinstance(file_objs, list): file_objs = [file_objs]
for f_obj in file_objs:
orig_name = getattr(f_obj, 'orig_name', os.path.basename(f_obj.name))
await asyncio.to_thread(shutil.copy, f_obj.name, os.path.join(target_dir, orig_name))
uploaded_list.append(orig_name)
run_thread(push_to_dataset, name="push_to_dataset")
return f"✅ تم رفع {len(uploaded_list)} ملفات ومزامنتُها بنجاح!", *await scan_directory(current_rel)
# السيرفر سيعمل فقط عند ضغطك يدوياً على زر "▶ Start Server" بعد تجهيز الـ Dataset
def on_space_boot():
log("[SYSTEM] Space booted. Initializing Hugging Face Dataset connection (`Server/` directory)...")
init_dataset_and_pull()
log("[SYSTEM] Ready! Click '▶ Start Server' from the Console tab to boot Minecraft manually.")
run_thread(on_space_boot, name="on_space_boot")
# ─── واجهة PTERODACTYL UI DASHBOARD ───
DASHBOARD_HTML = r"""
⚡ PTERODACTYL MC
Purpur 1.21.1
Server Stopped
Management
Console & Overview
Real-time resource utilization and terminal logs
Memory Usage🧠
0.0 / 4.0 GB
0% allocated
CPU Utilization⚡
0%
Purpur Main Thread
Online Players🎮
0
No players connected
System Uptime⏱️
0m
Continuous execution
~/server/logs/latest.log● LIVE STREAM
[SYSTEM] Panel initialized. Ready to launch Minecraft Server...
File Explorer
📁 /root
Directory Contents
Loading files...
Select a file to edit
➕ Create Item
🗑️ Delete / Rename
☁️ Dataset Live Sync (Server/ Folder)
يتم إنشاء الـ Dataset تلقائياً عند أول تشغيل، وتنزيل ورفع كل محتويات ومجلدات السيرفر مباشرة داخل المجلد Server/
☁️ Hugging Face Dataset Connection
HF Token (Write access)
Dataset Repository ID
Auto Direct Sync (Server/)
Sync Interval (Mins)
🔄 المزامنة الفورية مع الـ Dataset
⚡ التزامن المباشر والإنشاء التلقائي:
1. عند أول تشغيل، يتحقق النظام من وجود الـ Dataset على حسابك، وإذا لم يكن موجوداً يقوم بإنشائه تلقائياً كأرشيف مغلق (Private).
2. يتم تنزيل كل الملفات الموجودة في [repo]/Server وتشغيل الخادم عليها مباشرة.
3. أي ملف أو مجلد جديد يُنشئه السيرفر يتم دفعه ومزامنتُه فوراً داخل Server/ ليكون المستودع بمثابة وحدة تخزين سحابية مستمرة للسيرفر.
Server Configuration
Modify core server.properties parameters directly
🎮 Gameplay Mechanics
Difficulty
Default Gamemode
Max Players
View Distance
🔒 Network & Security
Online Mode (Cracked)
Player vs Player (PvP)
Spawn Monsters
Enable Whitelist
Access Control (RBAC)
Manage panel collaborators and granular permissions
➕ Assign Collaborator
📋 Authorized Users List
No users configured.
"""
DASHBOARD_JS = r"""
function showToast(msg, isErr=false) {
var box = document.getElementById('toast-box');
if(!box) return;
var t = document.createElement('div');
t.className = 'toast' + (isErr ? ' err' : '');
t.innerHTML = (isErr ? '❌ ' : '✅ ') + msg;
box.appendChild(t);
setTimeout(function(){ t.remove() }, 3500);
}
function switchTab(name, btn) {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
if(btn) btn.classList.add('active');
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
var pane = document.getElementById('tab-' + name);
if(pane) pane.classList.add('active');
if(name === 'files') bridgeClick('gb-refresh-dir');
if(name === 'cloud') bridgeClick('gb-load-cloud-config');
if(name === 'settings') bridgeClick('gb-load-settings');
if(name === 'users') bridgeClick('gb-refresh-users');
}
function bridgeClick(id) {
var el = document.getElementById(id);
if(el){
var btn = el.querySelector('button') || el;
btn.click();
}
}
function setGradioInput(id, val) {
var root = document.getElementById(id);
if(!root) return;
var inp = root.querySelector('textarea, input');
if(!inp) return;
var proto = inp instanceof HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(inp, val);
inp.dispatchEvent(new Event('input', { bubbles: true }));
}
function getGradioText(id) {
var root = document.getElementById(id);
if(!root) return '';
var ta = root.querySelector('textarea, input');
return ta ? ta.value : (root.innerText || '');
}
var _selectedFile = '';
var _selectedFolder = '';
function selectFolderAction(fName) {
_selectedFolder = fName;
setGradioInput('gv-folder', fName);
bridgeClick('gb-enter-folder');
}
function selectFileAction(fName) {
_selectedFile = fName;
document.getElementById('editor-filename').textContent = 'Editing: ' + fName;
var ren = document.getElementById('rename-new-inp');
if(ren) ren.value = fName;
setGradioInput('gv-file', fName);
bridgeClick('gb-select-file');
}
function saveCurrentFile() {
if(!_selectedFile) return showToast('No file selected!', true);
var val = document.getElementById('editor-area').value;
setGradioInput('gv-content', val);
bridgeClick('gb-save-file');
}
function createItemAction() {
var name = document.getElementById('create-name-inp').value;
var type = document.getElementById('create-type-sel').value;
if(!name) return showToast('Enter name first!', true);
setGradioInput('gv-name', name);
setGradioInput('gv-type', type);
bridgeClick('gb-create');
}
function renameItemAction() {
var newN = document.getElementById('rename-new-inp').value;
if(!_selectedFile || !newN) return showToast('Select a file and enter new name!', true);
setGradioInput('gv-old', _selectedFile);
setGradioInput('gv-name', newN);
bridgeClick('gb-rename');
}
function deleteItemAction() {
if(!_selectedFile) return showToast('Select a file to delete!', true);
setGradioInput('gv-old', _selectedFile);
bridgeClick('gb-delete');
_selectedFile = '';
}
function triggerUpload(inp) {
if(!inp.files.length) return;
var dt = new DataTransfer();
for(var i=0; i= 0;
var pill = document.getElementById('nav-status');
if(pill) {
pill.className = 'status-pill ' + (isRun ? 'running' : 'stopped');
document.getElementById('nav-dot').className = 'status-dot ' + (isRun ? 'running' : 'stopped');
document.getElementById('nav-status-text').textContent = isRun ? 'Server Online' : 'Server Stopped';
}
var con = getGradioText('g-console');
if(con && con !== _lastConsole) {
_lastConsole = con;
var term = document.getElementById('term-logs');
if(term) { term.textContent = con; term.scrollTop = term.scrollHeight; }
}
var resTxt = getGradioText('g-resources');
if(resTxt){
try {
var r = JSON.parse(resTxt);
document.getElementById('stat-ram').textContent = r.ram_used + ' / ' + r.ram_total + ' GB';
document.getElementById('stat-ram-pct').textContent = r.ram_pct + '%';
document.getElementById('bar-ram').style.width = r.ram_pct + '%';
document.getElementById('stat-cpu').textContent = r.cpu + '%';
document.getElementById('bar-cpu').style.width = Math.min(r.cpu, 100) + '%';
document.getElementById('stat-uptime').textContent = r.uptime;
} catch(e){}
}
var pl = getGradioText('g-players');
if(pl && pl !== 'No players online') {
var arr = pl.split(',').filter(Boolean);
document.getElementById('stat-players').textContent = arr.length;
document.getElementById('stat-player-names').textContent = pl;
} else {
document.getElementById('stat-players').textContent = '0';
document.getElementById('stat-player-names').textContent = 'No players connected';
}
var op = getGradioText('g-op');
if(op && op !== _lastOp) {
_lastOp = op;
showToast(op, op.indexOf('❌') >= 0 || op.indexOf('⛔') >= 0);
}
var fld = getGradioText('g-folders');
var fil = getGradioText('g-files');
if(fld || fil) {
try {
var fldArr = fld ? JSON.parse(fld) : [];
var filArr = fil ? JSON.parse(fil) : [];
var html = '';
fldArr.forEach(function(f){ html += '
📁 '+f+'
'; });
filArr.forEach(function(f){ html += '
📄 '+f+'
'; });
var box = document.getElementById('file-list-box');
if(box) box.innerHTML = html || '
Empty directory
';
} catch(e){}
}
var edContent = getGradioText('g-editor');
if(edContent && document.activeElement !== document.getElementById('editor-area')) {
document.getElementById('editor-area').value = edContent;
}
var pDisplay = getGradioText('g-path');
if(pDisplay) document.getElementById('path-display').textContent = pDisplay;
var usrList = getGradioText('g-users');
if(usrList) document.getElementById('users-list-area').innerHTML = usrList.replace(/\n/g, ' ');
var cCfg = getGradioText('g-cloud-config');
if(cCfg && document.activeElement !== document.getElementById('cloud-token') && document.activeElement !== document.getElementById('cloud-repo')) {
try {
var cfg = JSON.parse(cCfg);
if(cfg.hf_token) document.getElementById('cloud-token').value = cfg.hf_token;
if(cfg.hf_dataset_repo) document.getElementById('cloud-repo').value = cfg.hf_dataset_repo;
document.getElementById('cloud-auto-sync').checked = cfg.auto_sync !== false;
if(cfg.sync_interval) document.getElementById('cloud-interval').value = cfg.sync_interval;
} catch(e){}
}
bridgeClick('gb-refresh-con');
if(_pollCount % 2 === 0) bridgeClick('gb-periodic-status');
if(_pollCount % 3 === 0) bridgeClick('gb-refresh-res');
if(_pollCount % 4 === 0) bridgeClick('gb-refresh-players');
}
setTimeout(function(){
setInterval(pollSystem, 1500);
}, 500);
"""
with gr.Blocks(title="Pterodactyl MC Panel", css="#bridge-wrap{display:none!important}", head="") as demo:
current_path_state, file_path_state, load_offset_state, current_user = gr.State(""), gr.State(""), gr.State(0), gr.State(None)
gr.HTML(value=DASHBOARD_HTML)
with gr.Column(elem_id="bridge-wrap"):
_console = gr.Textbox(elem_id="g-console")
_status = gr.Textbox(elem_id="g-status")
_path = gr.Textbox(elem_id="g-path")
_folders = gr.Textbox(elem_id="g-folders")
_files = gr.Textbox(elem_id="g-files")
_op = gr.Textbox(elem_id="g-op")
_editor = gr.Textbox(elem_id="g-editor")
_editor_title = gr.Textbox(elem_id="g-editor-title")
_editor_fpath = gr.Textbox(elem_id="g-editor-fpath")
_editor_offset = gr.Textbox(elem_id="g-editor-offset")
_editor_has_more = gr.Textbox(elem_id="g-editor-has-more")
_users = gr.Textbox(elem_id="g-users")
_resources = gr.Textbox(elem_id="g-resources")
_players = gr.Textbox(elem_id="g-players")
_cmd_result = gr.Textbox(elem_id="g-cmd-result")
_download_data = gr.Textbox(elem_id="g-download")
_settings = gr.Textbox(elem_id="g-settings")
_cloud_config_json = gr.Textbox(elem_id="g-cloud-config")
_b_start = gr.Button(elem_id="gb-start")
_b_stop = gr.Button(elem_id="gb-stop")
_b_restart = gr.Button(elem_id="gb-restart")
_b_refresh_con = gr.Button(elem_id="gb-refresh-con")
_b_refresh_dir = gr.Button(elem_id="gb-refresh-dir")
_b_enter_folder = gr.Button(elem_id="gb-enter-folder")
_b_select_file = gr.Button(elem_id="gb-select-file")
_b_save_file = gr.Button(elem_id="gb-save-file")
_b_create = gr.Button(elem_id="gb-create")
_b_rename = gr.Button(elem_id="gb-rename")
_b_delete = gr.Button(elem_id="gb-delete")
_b_upload = gr.Button(elem_id="gb-upload")
_b_add_user = gr.Button(elem_id="gb-add-user")
_b_refresh_users = gr.Button(elem_id="gb-refresh-users")
_b_periodic_status = gr.Button(elem_id="gb-periodic-status")
_b_send_cmd = gr.Button(elem_id="gb-send-cmd")
_b_refresh_res = gr.Button(elem_id="gb-refresh-res")
_b_refresh_players = gr.Button(elem_id="gb-refresh-players")
_b_download_file = gr.Button(elem_id="gb-download-file")
_b_load_settings = gr.Button(elem_id="gb-load-settings")
_b_save_settings = gr.Button(elem_id="gb-save-settings")
_b_cloud_restore = gr.Button(elem_id="gb-cloud-restore")
_b_cloud_backup = gr.Button(elem_id="gb-cloud-backup")
_b_load_cloud_config = gr.Button(elem_id="gb-load-cloud-config")
_b_save_cloud_config = gr.Button(elem_id="gb-save-cloud-config")
_v_folder = gr.Textbox(elem_id="gv-folder")
_v_file = gr.Textbox(elem_id="gv-file")
_v_content = gr.Textbox(elem_id="gv-content")
_v_name = gr.Textbox(elem_id="gv-name")
_v_type = gr.Textbox(elem_id="gv-type", value="File")
_v_old = gr.Textbox(elem_id="gv-old")
_v_del_folder = gr.Checkbox(elem_id="gv-del-folder", value=False)
_v_uname = gr.Textbox(elem_id="gv-uname")
_v_role = gr.Textbox(elem_id="gv-role", value="editor")
_v_upload = gr.File(elem_id="gv-upload", file_count="multiple")
_v_cmd = gr.Textbox(elem_id="gv-cmd")
_v_download_path = gr.Textbox(elem_id="gv-download-path")
_v_settings_data = gr.Textbox(elem_id="gv-settings-data")
_v_cloud_config_data = gr.Textbox(elem_id="gv-cloud-config-data")
async def w_scan_directory(rel):
target = get_safe_path(rel)
folders, files = [], []
try:
entries = await asyncio.wait_for(asyncio.to_thread(lambda: list(os.listdir(target))), timeout=5.0)
for item in entries:
fp = os.path.join(target, item)
try:
if await asyncio.to_thread(os.path.isdir, fp): folders.append(item)
elif os.path.splitext(item)[1].lower() not in BINARY_EXTENSIONS: files.append(item)
except: files.append(item)
except: pass
folders.sort(); files.sort()
return f"📁 /root/{rel}" if rel else "📁 /root", json.dumps(folders), json.dumps(files)
async def w_enter_folder(folder, rel):
if rel is None: rel = ""
if not folder: return rel, *await w_scan_directory(rel)
if folder == ".. (المجلد الأعلى)":
parent = "/".join(rel.strip("/").split("/")[:-1]) if rel else ""
return parent, *await w_scan_directory(parent)
new_rel = os.path.join(rel, folder).strip("/")
return new_rel, *await w_scan_directory(new_rel)
async def w_load_file(file, rel):
r = await load_file_content(file, rel)
content = r[0].get("value", "") if isinstance(r[0], dict) else r[0]
title = r[1].get("value", "") if isinstance(r[1], dict) else r[1]
has_more = r[6].get("visible", False) if isinstance(r[6], dict) else False
return content, title, str(r[4]), str(r[5]), str(has_more)
async def w_create(name, typ, rel, user):
r = await create_item(name, typ, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_rename(old, new, rel, user):
r = await rename_item(old, new, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_delete(name, is_folder, rel, user):
r = await delete_item(name, is_folder, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_upload(files, rel, user):
r = await upload_to_current_dir(files, rel, user)
return r[0], *await w_scan_directory(rel)
def w_list_users():
users = load_users()
if not users: return "No users configured yet."
lines = []
for u, r in sorted(users.items()):
icon = {"admin": "🛡️", "editor": "✏️", "viewer": "👁️"}.get(r, "❓")
lines.append(f"{icon} {u} — {r.upper()}")
return " ".join(lines)
def w_add_user(name, role, user):
if not has_perm(user, "users_manage"): return "⛔ Only admins can manage users"
if not name: return "⚠️ Enter a username"
users = load_users(); users[name.strip()] = role; save_users(users)
return f"✅ Added {name.strip()} as {role}"
def w_send_cmd(cmd, user): return send_console_command(cmd, user)
def w_get_resources(): return json.dumps(get_system_resources())
def w_get_players(): return ", ".join(get_online_players()) or "No players online"
def w_download_file(filepath, rel, user):
if not has_perm(user, "files_read"): return "ERROR:Permission denied"
if not filepath: return ""
target = get_safe_path(os.path.join(rel, filepath) if rel else filepath)
if not os.path.isfile(target): return ""
try:
with open(target, "rb") as f:
data = base64.b64encode(f.read()).decode()
return f"{filepath}|{data}"
except: return ""
def w_load_settings():
props_path = os.path.join(WORKDIR, "server.properties")
if not os.path.exists(props_path): return "{}"
try:
props = {}
with open(props_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1); props[k.strip()] = v.strip()
return json.dumps(props)
except: return "{}"
def w_save_settings(data_str, user):
if not has_perm(user, "files_write"): return "⛔ Permission denied"
props_path = os.path.join(WORKDIR, "server.properties")
try:
props = json.loads(data_str)
with open(props_path, "w") as f:
for k, v in sorted(props.items()): f.write(f"{k}={v}\n")
return "✅ Settings saved successfully!"
except Exception as e: return f"❌ Error saving: {e}"
def w_load_cloud_config():
return json.dumps(load_hf_config())
def w_save_cloud_config(data_str, user):
if not has_perm(user, "cloud_manage"): return "⛔ You don't have permission to manage cloud sync"
try:
cfg = json.loads(data_str)
success, msg = save_hf_config(cfg)
return msg
except Exception as e: return f"❌ Error saving cloud config: {e}"
def w_cloud_restore(user):
if not has_perm(user, "cloud_manage"): return "⛔ You don't have permission to perform cloud restore"
success, msg = init_dataset_and_pull()
return msg
def w_cloud_backup(user):
if not has_perm(user, "cloud_manage"): return "⛔ You don't have permission to perform cloud backup"
success, msg = push_to_dataset()
return msg
_b_start.click(fn=control_server, inputs=[gr.State("START"), current_user], outputs=_status)
_b_stop.click(fn=control_server, inputs=[gr.State("STOP"), current_user], outputs=_status)
_b_restart.click(fn=control_server, inputs=[gr.State("RESTART"), current_user], outputs=_status)
_b_refresh_con.click(fn=get_logs, outputs=_console)
_b_refresh_dir.click(fn=w_scan_directory, inputs=current_path_state, outputs=[_path, _folders, _files])
_b_enter_folder.click(fn=w_enter_folder, inputs=[_v_folder, current_path_state], outputs=[current_path_state, _path, _folders, _files])
_b_select_file.click(fn=w_load_file, inputs=[_v_file, current_path_state], outputs=[_editor, _editor_title, _editor_fpath, _editor_offset, _editor_has_more])
_b_save_file.click(fn=save_file_content, inputs=[_v_file, _v_content, current_path_state, current_user], outputs=_op)
_b_create.click(fn=w_create, inputs=[_v_name, _v_type, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_rename.click(fn=w_rename, inputs=[_v_old, _v_name, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_delete.click(fn=w_delete, inputs=[_v_old, _v_del_folder, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_upload.click(fn=w_upload, inputs=[_v_upload, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_add_user.click(fn=w_add_user, inputs=[_v_uname, _v_role, current_user], outputs=_op)
_b_refresh_users.click(fn=w_list_users, outputs=_users)
_b_periodic_status.click(fn=get_server_status, outputs=_status)
_b_send_cmd.click(fn=w_send_cmd, inputs=[_v_cmd, current_user], outputs=_cmd_result)
_b_refresh_res.click(fn=w_get_resources, outputs=_resources)
_b_refresh_players.click(fn=w_get_players, outputs=_players)
_b_download_file.click(fn=w_download_file, inputs=[_v_download_path, current_path_state, current_user], outputs=_download_data)
_b_load_settings.click(fn=w_load_settings, outputs=_settings)
_b_save_settings.click(fn=w_save_settings, inputs=[_v_settings_data, current_user], outputs=_cmd_result)
_b_cloud_restore.click(fn=w_cloud_restore, inputs=[current_user], outputs=_op)
_b_cloud_backup.click(fn=w_cloud_backup, inputs=[current_user], outputs=_op)
_b_load_cloud_config.click(fn=w_load_cloud_config, outputs=_cloud_config_json)
_b_save_cloud_config.click(fn=w_save_cloud_config, inputs=[_v_cloud_config_data, current_user], outputs=_op)
demo.load(fn=get_logs, outputs=_console)
demo.load(fn=get_server_status, outputs=_status)
demo.load(fn=w_scan_directory, inputs=current_path_state, outputs=[_path, _folders, _files])
demo.load(fn=w_list_users, outputs=_users)
demo.load(fn=w_load_cloud_config, outputs=_cloud_config_json)
demo.queue(default_concurrency_limit=3)
log("[SYSTEM] Pterodactyl MC Panel initialized with Live Dataset Sync.")
demo.launch()