Files
2026-08-13 18:24:50 +08:00

224 lines
9.4 KiB
Python

# coding: utf-8
import os
import signal
import subprocess
import sys
if "/www/server/panel" not in sys.path:
sys.path.insert(0, "/www/server/panel")
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
if "/www/server/panel/class_v2" not in sys.path:
sys.path.insert(0, "/www/server/panel/class_v2")
import public
from mod.project.project_import.core.api_utils import error_response, parse_json_field
from mod.project.project_import.core.constants import (
STEP_WAITING,
TASK_CANCELLED,
TASK_FAILED,
TASK_QUEUED,
TASK_RUNNING,
logs_dir,
)
from mod.project.project_import.core.exceptions import ProjectImportError
from mod.project.project_import.core.session_store import SessionStore
from mod.project.project_import.core.task_store import TaskStore
from mod.project.project_import.sources.git import normalize_git_config
ANALYSIS_STEPS = [
{"key": "validate_source", "title": "Validate project source", "status": STEP_WAITING},
{"key": "fetch_source", "title": "Prepare project files", "status": STEP_WAITING},
{"key": "detect_project", "title": "Analyze project type", "status": STEP_WAITING},
{"key": "finalize", "title": "Save analysis result", "status": STEP_WAITING},
]
IMPORT_STEPS = [
{"key": "preflight", "title": "Validate import settings", "status": STEP_WAITING},
{"key": "runtime", "title": "Prepare project runtime", "status": STEP_WAITING},
{"key": "commit_files", "title": "Commit project files", "status": STEP_WAITING},
{"key": "create_project", "title": "Create aaPanel project", "status": STEP_WAITING},
{"key": "database", "title": "Import database", "status": STEP_WAITING},
{"key": "ssl", "title": "Configure SSL", "status": STEP_WAITING},
{"key": "health", "title": "Check project health", "status": STEP_WAITING},
]
class main:
def __init__(self):
self.sessions = SessionStore()
self.tasks = TaskStore()
def start_analysis(self, get):
try:
session_id = str(get.get("session_id", "")).strip()
source_type = str(get.get("source_type", "")).strip().lower()
session = None
if session_id:
session = self.sessions.get(session_id)
if source_type and source_type != session.get("source_type"):
raise ProjectImportError(
"source_type does not match the import session",
"SOURCE_TYPE_MISMATCH",
)
source_type = str(session.get("source_type", "")).strip().lower()
else:
if not source_type:
raise ProjectImportError("source_type is required", "SOURCE_TYPE_REQUIRED")
if source_type == "archive":
raise ProjectImportError(
"Archive analysis must use the session_id returned by upload",
"UPLOAD_SESSION_REQUIRED",
)
source_config = parse_json_field(get.get("source_config", "{}"), "source_config")
if not isinstance(source_config, dict):
raise ProjectImportError("source_config must be a JSON object", "INVALID_SOURCE_CONFIG")
if source_type == "git":
source_config = normalize_git_config(source_config)
if session is None:
session = self.sessions.create(source_type)
session_id = session["session_id"]
self._ensure_task_inactive(session.get("analysis_task_id", ""))
task = self.tasks.create(
"analysis",
session_id,
ANALYSIS_STEPS,
payload={"source_type": session.get("source_type", "")},
secret={"source_config": source_config},
)
def update_session(data):
data["status"] = "analyzing"
data["analysis_task_id"] = task["task_id"]
data["analysis"] = {}
return data
self.sessions.update(session_id, update_session)
self._spawn(task["task_id"])
if source_type:
public.set_module_logs("project_import", "scan_project_{}".format(source_type))
# 每次启动分析顺带清理上传区中长期残留的孤儿压缩包(> 会话 TTL 未消费)
self.sessions.cleanup_upload_root()
return public.return_message(0, 0, {
"task_id": task["task_id"],
"session_id": session_id,
})
except Exception as exc:
return error_response(exc)
def start_import(self, get):
try:
session_id = str(get.get("session_id", ""))
session = self.sessions.get(session_id)
self._ensure_task_inactive(session.get("import_task_id", ""))
retryable_stale_import = (
session.get("status") == "importing"
and self._task_has_status(
session.get("import_task_id", ""),
(TASK_FAILED, TASK_CANCELLED),
)
)
if session.get("status") != "analyzed" and not retryable_stale_import:
raise ProjectImportError("Project analysis has not completed", "ANALYSIS_REQUIRED")
project_config = parse_json_field(get.get("project_config", "{}"), "project_config")
database_config = parse_json_field(get.get("database_config", "{}"), "database_config")
if not isinstance(project_config, dict) or not isinstance(database_config, dict):
raise ProjectImportError("Import configurations must be JSON objects", "INVALID_IMPORT_CONFIG")
task = self.tasks.create(
"import",
session_id,
IMPORT_STEPS,
payload={"project_type": project_config.get("project_type", "")},
secret={
"project_config": project_config,
"database_config": database_config,
},
)
def update_session(data):
data["status"] = "importing"
data["import_task_id"] = task["task_id"]
return data
self.sessions.update(session_id, update_session)
self._spawn(task["task_id"])
project_type = str(project_config.get("project_type", "")).strip().lower()
if project_type:
public.set_module_logs("project_import", "import_project_{}".format(project_type))
return public.return_message(0, 0, {
"task_id": task["task_id"],
"session_id": session_id,
})
except Exception as exc:
return error_response(exc)
def get_progress(self, get):
try:
return public.return_message(0, 0, self.tasks.public_progress(get.get("task_id", "")))
except Exception as exc:
return error_response(exc)
def cancel_task(self, get):
try:
task_id = str(get.get("task_id", ""))
task = self.tasks.request_cancel(task_id)
pid = int(task.get("pid", 0) or 0)
pgid = int(task.get("pgid", 0) or 0)
if pid > 0:
try:
if pgid > 0 and hasattr(os, "killpg"):
os.killpg(pgid, signal.SIGTERM)
else:
os.kill(pid, signal.SIGTERM)
except (ProcessLookupError, PermissionError, OSError):
pass
return public.return_message(0, 0, {
"task_id": task_id,
"cancel_requested": True,
})
except Exception as exc:
return error_response(exc)
def _spawn(self, task_id):
service_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "service.py")
python_bin = public.get_python_bin()
if not python_bin or not os.path.isfile(python_bin):
raise ProjectImportError("Panel Python interpreter was not found", "PYTHON_NOT_FOUND")
os.makedirs(logs_dir(), mode=0o700, exist_ok=True)
log_path = os.path.join(logs_dir(), task_id + ".log")
try:
with open(log_path, "ab", buffering=0) as output:
process = subprocess.Popen(
[python_bin, "-u", service_path, task_id],
stdin=subprocess.DEVNULL,
stdout=output,
stderr=subprocess.STDOUT,
close_fds=True,
start_new_session=True,
)
self.tasks.set_process(task_id, process.pid, process.pid)
except Exception as exc:
self.tasks.set_failed(task_id, "Failed to start worker: {}".format(exc), "WORKER_START_FAILED")
raise ProjectImportError("Failed to start import worker", "WORKER_START_FAILED")
def _ensure_task_inactive(self, task_id):
if not task_id:
return
try:
task = self.tasks.get(task_id)
except Exception:
return
if task.get("status") in (TASK_QUEUED, TASK_RUNNING):
raise ProjectImportError("Another task in this stage is still running", "TASK_ALREADY_RUNNING")
def _task_has_status(self, task_id, statuses):
if not task_id:
return False
try:
return self.tasks.get(task_id).get("status") in statuses
except Exception:
return False