mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-09-07 10:07:39 +02:00
Update to v8.21.0
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user