mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-08-17 21:25:47 +02:00
224 lines
9.4 KiB
Python
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
|