Add SSM deployment and reliable Maven setup

This commit is contained in:
王鹏
2026-10-08 17:58:09 +08:00
parent 2d7b6216ba
commit d1695eba74
13 changed files with 626 additions and 46 deletions

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import json
import os
import re
import socket
import shutil
import subprocess
@@ -52,17 +53,21 @@ ProcessRunner = Callable[[list[str], Path, OutputHandler, Path | None, bool], in
def build_deployment_plan(scan: ScanResult, config: DeploymentConfig) -> list[DeploymentStep]:
steps: list[DeploymentStep] = []
output_dir = config.output_dir or scan.root / "deploy"
backend_is_ssm = bool(
scan.backend and (scan.backend.kind == "SSM" or scan.backend.metadata.get("backend_type") == "ssm")
)
if scan.backend:
backend_dir = scan.backend.path
existing_jar = _find_existing_backend_jar(backend_dir)
if existing_jar:
existing_artifact = _find_existing_backend_war(backend_dir) if backend_is_ssm else _find_existing_backend_jar(backend_dir)
if existing_artifact:
artifact_label = "WAR" if backend_is_ssm else "jar"
steps.append(
DeploymentStep(
"跳过后端构建",
[],
backend_dir,
f"检测到已有后端 jar,跳过构建: {existing_jar}",
f"检测到已有后端 {artifact_label},跳过构建: {existing_artifact}",
kind="backend_build_skip",
)
)
@@ -71,14 +76,15 @@ def build_deployment_plan(scan: ScanResult, config: DeploymentConfig) -> list[De
if "pom.xml" in build_file_names:
command = [_tool_command(config, "Maven", "mvn"), "clean", "package", "-DskipTests"]
elif "gradlew.bat" in build_file_names:
command = ["gradlew.bat", "bootJar"]
command = ["gradlew.bat", "war" if backend_is_ssm else "bootJar"]
elif "gradlew" in build_file_names:
command = ["./gradlew", "bootJar"]
command = ["./gradlew", "war" if backend_is_ssm else "bootJar"]
elif backend_dir.exists():
command = [_tool_command(config, "Gradle", "gradle"), "bootJar"]
command = [_tool_command(config, "Gradle", "gradle"), "war" if backend_is_ssm else "bootJar"]
else:
command = [_tool_command(config, "Maven", "mvn"), "clean", "package", "-DskipTests"]
steps.append(DeploymentStep("构建后端", command, backend_dir, "打包 Spring Boot 应用", kind="backend_build"))
description = "构建 SSM WAR 应用" if backend_is_ssm else "打包 Spring Boot 应用"
steps.append(DeploymentStep("构建后端", command, backend_dir, description, kind="backend_build"))
frontends = scan.detected_frontends
for frontend in frontends:
@@ -116,20 +122,45 @@ def build_deployment_plan(scan: ScanResult, config: DeploymentConfig) -> list[De
if scan.backend:
backend_context = scan.backend.metadata.get("context_path", "")
backend_url = f"http://localhost:{config.backend_port}{backend_context}"
steps.append(
DeploymentStep(
"启动后端",
[_tool_command(config, "JDK", "java"), f"-Dserver.port={config.backend_port}", "-jar", "latest-built-jar"],
output_dir,
"启动 Spring Boot 服务",
kind="backend_start",
source_path=scan.backend.path,
background=True,
service_name="后端",
port=_validate_port(config.backend_port, "后端端口"),
url=backend_url,
if backend_is_ssm:
if _validate_port(config.backend_port, "后端端口") != 8080:
raise ValueError("SSM 外置 Tomcat 当前使用默认端口 8080,请将后端端口设置为 8080")
tomcat_command = _required_tool_command(config, "Tomcat", "catalina.bat")
tomcat_home = Path(tomcat_command).parent.parent
context_name = backend_context.strip("/") or "ROOT"
if not re.fullmatch(r"[A-Za-z0-9._-]+", context_name):
raise ValueError(f"SSM context path 不能用于 WAR 文件名: {backend_context}")
deployed_war = tomcat_home / "webapps" / f"{context_name}.war"
steps.append(
DeploymentStep(
"发布并启动 SSM 后端",
[tomcat_command, "run"],
output_dir,
f"发布 WAR 到 Tomcat: {deployed_war}",
kind="ssm_start",
source_path=scan.backend.path,
output_path=deployed_war,
background=True,
service_name="SSM 后端",
port=8080,
url=backend_url,
)
)
else:
steps.append(
DeploymentStep(
"启动后端",
[_tool_command(config, "JDK", "java"), f"-Dserver.port={config.backend_port}", "-jar", "latest-built-jar"],
output_dir,
"启动 Spring Boot 服务",
kind="backend_start",
source_path=scan.backend.path,
background=True,
service_name="后端",
port=_validate_port(config.backend_port, "后端端口"),
url=backend_url,
)
)
)
frontend_urls: list[tuple[str, str]] = []
allocated_ports: dict[int, str] = {}
@@ -193,6 +224,17 @@ def _tool_command(config: DeploymentConfig, tool_name: str, fallback: str) -> st
return str(discovered) if discovered else fallback
def _required_tool_command(config: DeploymentConfig, tool_name: str, fallback: str) -> str:
command = _tool_command(config, tool_name, fallback)
path = Path(command)
if path.exists():
return str(path.resolve())
resolved = shutil.which(command)
if resolved:
return resolved
raise FileNotFoundError(f"部署 SSM 项目需要 {tool_name},请先安装或选择其可执行文件")
def _default_npm_command() -> str:
return "npm.cmd" if os.name == "nt" else "npm"
@@ -371,10 +413,12 @@ def _restart_backend(
cwd: Path,
on_output: OutputHandler,
*,
port: int | None = None,
require_service_pid: bool = True,
replace_listeners=None,
popen=None,
) -> int:
port = _extract_server_port(command)
port = port if port is not None else _extract_server_port(command)
if port is None:
raise ValueError("后端启动命令缺少 -Dserver.port 配置")
replace_listeners = replace_listeners or _replace_port_listeners
@@ -433,7 +477,8 @@ def _restart_backend(
on_output(f"后端进程 PID {process.pid} 在端口就绪前退出,退出码: {return_code},日志: {log_path}")
return return_code or 1
service_pid = _read_service_pid(pid_path) or process.pid
if service_pid in _find_listening_pids(port) and _is_port_open(port):
listening_pids = _find_listening_pids(port)
if _is_port_open(port) and (not require_service_pid or service_pid in listening_pids):
on_output(f"后端服务启动成功,PID: {service_pid},端口: {port},日志: {log_path}")
return 0
time.sleep(0.2)
@@ -443,6 +488,40 @@ def _restart_backend(
return 1
def _restart_ssm_backend(
command: list[str],
cwd: Path,
on_output: OutputHandler,
*,
port: int,
source_war: Path,
deployed_war: Path,
replace_listeners=None,
) -> int:
replace_listeners = replace_listeners or _replace_port_listeners
if not replace_listeners(port, on_output):
on_output(f"端口 {port} 未能在限定时间内释放,SSM 后端未启动。")
return 1
_deploy_ssm_war(source_war, deployed_war, on_output)
return _restart_backend(
command,
cwd,
on_output,
port=port,
require_service_pid=False,
replace_listeners=lambda _port, _output: True,
)
def _deploy_ssm_war(source_war: Path, deployed_war: Path, on_output: OutputHandler) -> None:
deployed_war.parent.mkdir(parents=True, exist_ok=True)
exploded_dir = deployed_war.with_suffix("")
if exploded_dir.is_dir():
shutil.rmtree(exploded_dir)
shutil.copy2(source_war, deployed_war)
on_output(f"SSM WAR 已发布: {source_war} -> {deployed_war}")
def _restart_frontend(
command: list[str],
cwd: Path,
@@ -545,6 +624,22 @@ def run_steps(
service_name=step.service_name or "前端",
log_path=step.output_path,
)
elif step.kind == "ssm_start":
if step.port is None or step.source_path is None or step.output_path is None:
raise ValueError(f"{step.label}缺少端口、项目目录或 Tomcat WAR 路径配置")
source_war = _find_latest_backend_war(step.source_path)
if process_runner is None:
return_code = _restart_ssm_backend(
resolve_process_command(command),
step.cwd,
on_output,
port=step.port,
source_war=source_war,
deployed_war=step.output_path,
)
else:
_deploy_ssm_war(source_war, step.output_path, on_output)
return_code = runner(command, step.cwd, on_output, step.input_path, step.background)
else:
return_code = runner(command, step.cwd, on_output, step.input_path, step.background)
except Exception as exc:
@@ -589,6 +684,23 @@ def _find_existing_backend_jar(backend_dir: Path) -> Path | None:
return None
def _find_latest_backend_war(backend_dir: Path) -> Path:
candidates: list[Path] = []
for folder in (backend_dir / "target", backend_dir / "build" / "libs"):
if folder.is_dir():
candidates.extend(path for path in folder.glob("*.war") if not path.name.lower().endswith("-sources.war"))
if not candidates:
raise FileNotFoundError(f"未找到 SSM WAR 产物: {backend_dir / 'target'} 或 {backend_dir / 'build' / 'libs'}")
return max(candidates, key=lambda path: (path.stat().st_mtime, path.name))
def _find_existing_backend_war(backend_dir: Path) -> Path | None:
try:
return _find_latest_backend_war(backend_dir)
except FileNotFoundError:
return None
def _is_runnable_jar(path: Path) -> bool:
name = path.name.lower()
excluded_suffixes = ("-sources.jar", "-javadoc.jar", "-plain.jar")