Add SSM deployment and reliable Maven setup
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user