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")

View File

@@ -14,6 +14,7 @@ CommandRunner = Callable[[list[str]], subprocess.CompletedProcess[str]]
DEFAULT_CHECKS: dict[str, list[str]] = {
"JDK": ["java", "-version"],
"Maven": ["mvn", "-version"],
"Tomcat": ["catalina.bat", "version"],
"Gradle": ["gradle", "-version"],
"Node.js": ["node", "--version"],
"npm": ["npm", "--version"],
@@ -25,6 +26,7 @@ DEFAULT_CHECKS: dict[str, list[str]] = {
EXECUTABLE_NAMES: dict[str, list[str]] = {
"JDK": ["java.exe", "java"],
"Maven": ["mvn.cmd", "mvn.bat", "mvn"],
"Tomcat": ["catalina.bat", "catalina.sh"],
"Gradle": ["gradle.bat", "gradle"],
"Node.js": ["node.exe", "node"],
"npm": ["npm.cmd", "npm"],
@@ -151,7 +153,11 @@ def _try_command(name: str, command: list[str], source: str, runner: CommandRunn
def _run_command(command: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(command, capture_output=True, text=True, timeout=6, shell=False)
environment = os.environ.copy()
executable = Path(command[0])
if executable.name.lower() in {"catalina.bat", "catalina.sh"} and executable.exists():
environment["CATALINA_HOME"] = str(executable.parent.parent)
return subprocess.run(command, capture_output=True, text=True, timeout=6, shell=False, env=environment)
def _combine_output(completed: subprocess.CompletedProcess[str]) -> str:
@@ -175,6 +181,8 @@ def _direct_candidates(name: str, root: Path) -> list[Path]:
return [root / "nodejs" / "npm.cmd", root / "Programs" / "nodejs" / "npm.cmd"]
if name == "Maven":
return _glob(root, "apache-maven*/bin/mvn.cmd")
if name == "Tomcat":
return _glob(root, "apache-tomcat*/bin/catalina.bat")
if name == "Gradle":
return _glob(root, "gradle*/bin/gradle.bat")
if name == "Git":

View File

@@ -8,13 +8,21 @@ from tkinter import filedialog, messagebox, ttk
from .deployer import DeploymentConfig, build_deployment_plan, run_steps
from .environment import EnvironmentCheck, check_environment, load_tool_paths, save_tool_paths
from .installer import build_install_command, recommend_jdk_version, run_install_command
from .installer import (
MAVEN_VERSION,
TOMCAT_VERSION,
build_install_command,
install_maven,
install_tomcat,
recommend_jdk_version,
run_install_command,
)
from .path_manager import apply_user_environment, build_environment_update
from .scanner import ScanResult, scan_project
TEXT = {
"title": "\u0053\u0070\u0072\u0069\u006e\u0067\u0020\u0042\u006f\u006f\u0074\u0020\u002b\u0020\u0056\u0075\u0065\u0020\u81ea\u52a8\u5316\u90e8\u7f72\u5de5\u5177",
"title": "Spring Boot / SSM + Vue 自动化部署工具",
"workbench": "\u90e8\u7f72\u5de5\u4f5c\u53f0",
"select_dir": "\u9009\u62e9\u76ee\u5f55",
"scan": "\u626b\u63cf\u9879\u76ee",
@@ -54,7 +62,7 @@ class DeployWorkbench(tk.Tk):
self.tool_paths = load_tool_paths()
self.tool_path_vars: dict[str, tk.StringVar] = {
name: tk.StringVar(value=str(self.tool_paths.get(name, "")))
for name in ("MySQL", "JDK", "Maven", "Gradle", "Node.js", "npm", "Git")
for name in ("MySQL", "JDK", "Maven", "Tomcat", "Gradle", "Node.js", "npm", "Git")
}
self.scan_result: ScanResult | None = None
@@ -124,7 +132,7 @@ class DeployWorkbench(tk.Tk):
self._build_logs(content)
def _build_header(self, parent: ttk.Frame) -> None:
ttk.Label(parent, text="Spring Boot + Vue 自动化部署", style="Title.TLabel").pack(anchor=tk.W)
ttk.Label(parent, text="Spring Boot / SSM + Vue 自动化部署", style="Title.TLabel").pack(anchor=tk.W)
action_bar = ttk.Frame(parent)
action_bar.pack(fill=tk.X, pady=(14, 16))
self.project_dir_entry = ttk.Entry(action_bar, textvariable=self.project_dir)
@@ -237,7 +245,7 @@ class DeployWorkbench(tk.Tk):
ttk.Button(jdk_row, text="\u52a0\u5165 PATH", command=lambda: self.add_tool_to_path("JDK")).pack(side=tk.LEFT, padx=(8, 0))
ttk.Label(jdk_row, textvariable=self.jdk_reason, style="Muted.TLabel").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 0))
for name in ("Maven", "Gradle", "Node.js", "Git", "MySQL"):
for name in ("Maven", "Tomcat", "Gradle", "Node.js", "Git", "MySQL"):
row = ttk.Frame(panel, style="Panel.TFrame")
row.pack(fill=tk.X, pady=5)
ttk.Label(row, text=name, width=12, style="CardTitle.TLabel").pack(side=tk.LEFT)
@@ -347,6 +355,12 @@ class DeployWorkbench(tk.Tk):
threading.Thread(target=lambda: self.ui_queue.put(("install_done", ("JDK", run_install_command(command, self._thread_log)))), daemon=True).start()
def install_tool_async(self, tool_name: str) -> None:
if tool_name == "Maven":
self.install_maven_async()
return
if tool_name == "Tomcat":
self.install_tomcat_async()
return
try:
command = build_install_command(tool_name)
except ValueError as exc:
@@ -365,6 +379,42 @@ class DeployWorkbench(tk.Tk):
daemon=True,
).start()
def install_maven_async(self) -> None:
if not messagebox.askyesno(
"确认安装 Maven",
f"将从 Apache 官方地址下载 Maven {MAVEN_VERSION},校验安装包后解压到当前用户目录。\n\n"
"安装完成后会自动保存 mvn.cmd 路径。是否继续?",
):
return
self._append_log(f"开始安装 Maven {MAVEN_VERSION}...")
threading.Thread(target=self._install_maven_worker, daemon=True).start()
def _install_maven_worker(self) -> None:
try:
executable = install_maven(self._thread_log)
except Exception as exc:
self.ui_queue.put(("maven_install_done", (None, str(exc))))
return
self.ui_queue.put(("maven_install_done", (executable, "")))
def install_tomcat_async(self) -> None:
if not messagebox.askyesno(
"确认安装 Tomcat",
f"将从 Apache 官方地址下载 Tomcat {TOMCAT_VERSION},校验安装包后解压到当前用户目录。\n\n"
"Tomcat 9 适用于常见的 Java 8 / javax.servlet SSM 项目。是否继续?",
):
return
self._append_log(f"开始安装 Tomcat {TOMCAT_VERSION}...")
threading.Thread(target=self._install_tomcat_worker, daemon=True).start()
def _install_tomcat_worker(self) -> None:
try:
executable = install_tomcat(self._thread_log)
except Exception as exc:
self.ui_queue.put(("tomcat_install_done", (None, str(exc))))
return
self.ui_queue.put(("tomcat_install_done", (executable, "")))
def add_tool_to_path(self, tool_name: str) -> None:
result = self._find_environment_result(tool_name)
executable = Path(result.command[0]) if result and Path(result.command[0]).exists() else None
@@ -539,6 +589,28 @@ class DeployWorkbench(tk.Tk):
self._append_log(f"{tool_name} 安装命令结束,退出码: {exit_code}")
self.check_environment_async()
self.after(1800, lambda name=tool_name: self._prompt_add_tool_after_install(name))
elif event == "maven_install_done":
executable, error = payload # type: ignore[misc]
if error:
self._append_log(f"Maven 安装失败: {error}")
messagebox.showerror("Maven 安装失败", str(error))
continue
self.tool_path_vars["Maven"].set(str(executable))
self.save_tool_path_settings(silent=True)
self._append_log(f"Maven 路径已保存: {executable}")
self.check_environment_async()
self.after(1800, lambda: self._prompt_add_tool_after_install("Maven"))
elif event == "tomcat_install_done":
executable, error = payload # type: ignore[misc]
if error:
self._append_log(f"Tomcat 安装失败: {error}")
messagebox.showerror("Tomcat 安装失败", str(error))
continue
self.tool_path_vars["Tomcat"].set(str(executable))
self.save_tool_path_settings(silent=True)
self._append_log(f"Tomcat 路径已保存: {executable}")
self.check_environment_async()
self.after(1800, lambda: self._prompt_add_tool_after_install("Tomcat"))
self.after(120, self._drain_queue)
def _prompt_add_tool_after_install(self, tool_name: str) -> None:

View File

@@ -1,8 +1,16 @@
from __future__ import annotations
import re
import hashlib
import os
import shutil
import subprocess
import tempfile
import urllib.request
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import BinaryIO, Callable
from .scanner import ScanResult
@@ -15,13 +23,19 @@ JDK_PACKAGES = {
}
WINGET_PACKAGES = {
"Maven": "Apache.Maven",
"Gradle": "Gradle.Gradle",
"Node.js": "OpenJS.NodeJS.LTS",
"Git": "Git.Git",
"MySQL": "Oracle.MySQL",
}
MAVEN_VERSION = "3.10.0"
MAVEN_ARCHIVE_NAME = f"apache-maven-{MAVEN_VERSION}-bin.zip"
MAVEN_DOWNLOAD_BASE = f"https://dlcdn.apache.org/maven/maven-3/{MAVEN_VERSION}/binaries"
TOMCAT_VERSION = "9.0.122"
TOMCAT_ARCHIVE_NAME = f"apache-tomcat-{TOMCAT_VERSION}-windows-x64.zip"
TOMCAT_DOWNLOAD_BASE = f"https://dlcdn.apache.org/tomcat/tomcat-9/v{TOMCAT_VERSION}/bin"
ANSI_PATTERN = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
PERCENT_PATTERN = re.compile(r"(\d{1,3})\s*%")
PROGRESS_CHARS = "█▓▒░▏▎▍▌▋▊▉■□▪▫▬─━═|/-\\"
@@ -65,6 +79,153 @@ def build_install_command(tool_name: str, version: str | None = None) -> list[st
return ["winget", "install", "-e", "--id", package_id]
def default_maven_install_root() -> Path:
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
base = Path(local_app_data) if local_app_data else Path.home() / ".autodeploy"
return base / "AutoDeploy" / "tools"
def install_maven(
on_output: Callable[[str], None],
install_root: Path | None = None,
opener: Callable[..., BinaryIO] | None = None,
) -> Path:
root = Path(install_root) if install_root is not None else default_maven_install_root()
archive_url = f"{MAVEN_DOWNLOAD_BASE}/{MAVEN_ARCHIVE_NAME}"
return _install_verified_apache_archive(
product_name="Maven",
version=MAVEN_VERSION,
archive_name=MAVEN_ARCHIVE_NAME,
archive_url=archive_url,
extracted_dir_name=f"apache-maven-{MAVEN_VERSION}",
executable_relative=Path("bin") / "mvn.cmd",
on_output=on_output,
install_root=root,
opener=opener,
)
def install_tomcat(
on_output: Callable[[str], None],
install_root: Path | None = None,
opener: Callable[..., BinaryIO] | None = None,
) -> Path:
root = Path(install_root) if install_root is not None else default_maven_install_root()
archive_url = f"{TOMCAT_DOWNLOAD_BASE}/{TOMCAT_ARCHIVE_NAME}"
return _install_verified_apache_archive(
product_name="Tomcat",
version=TOMCAT_VERSION,
archive_name=TOMCAT_ARCHIVE_NAME,
archive_url=archive_url,
extracted_dir_name=f"apache-tomcat-{TOMCAT_VERSION}",
executable_relative=Path("bin") / "catalina.bat",
on_output=on_output,
install_root=root,
opener=opener,
)
def _install_verified_apache_archive(
*,
product_name: str,
version: str,
archive_name: str,
archive_url: str,
extracted_dir_name: str,
executable_relative: Path,
on_output: Callable[[str], None],
install_root: Path,
opener: Callable[..., BinaryIO] | None,
) -> Path:
product_home = install_root / extracted_dir_name
executable = product_home / executable_relative
if executable.exists():
on_output(f"{product_name} {version} 已安装: {executable}")
return executable
open_url = opener or urllib.request.urlopen
checksum_url = f"{archive_url}.sha512"
install_root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=f"autodeploy-{product_name.lower()}-") as temporary:
temp_dir = Path(temporary)
archive_path = temp_dir / archive_name
on_output(f"正在从 Apache 官方地址下载 {product_name} {version}...")
_download_to_file(archive_url, archive_path, open_url, on_output)
expected_checksum = _download_checksum(checksum_url, open_url)
actual_checksum = _sha512(archive_path)
if actual_checksum.lower() != expected_checksum.lower():
raise RuntimeError(f"{product_name} 安装包 SHA-512 校验失败,已停止安装。")
on_output(f"{product_name} 安装包校验通过,正在解压...")
extract_root = temp_dir / "extracted"
_safe_extract_zip(archive_path, extract_root)
extracted_home = extract_root / extracted_dir_name
extracted_executable = extracted_home / executable_relative
if not extracted_executable.exists():
raise RuntimeError(f"{product_name} 安装包结构异常,未找到 {executable_relative}。")
shutil.copytree(extracted_home, product_home, dirs_exist_ok=True)
if not executable.exists():
raise RuntimeError(f"{product_name} 解压完成,但未找到 {executable_relative}。")
on_output(f"{product_name} {version} 安装完成: {executable}")
return executable
def _download_to_file(
url: str,
destination: Path,
opener: Callable[..., BinaryIO],
on_output: Callable[[str], None],
) -> None:
with opener(url, timeout=60) as response, destination.open("wb") as output:
headers = getattr(response, "headers", {})
total_text = headers.get("Content-Length", "") if hasattr(headers, "get") else ""
total = int(total_text) if str(total_text).isdigit() else 0
downloaded = 0
last_percent = -10
while True:
chunk = response.read(1024 * 256)
if not chunk:
break
output.write(chunk)
downloaded += len(chunk)
if total:
percent = min(downloaded * 100 // total, 100)
if percent >= last_percent + 10 or percent == 100:
on_output(f"下载进度: {percent}%")
last_percent = percent
def _download_checksum(url: str, opener: Callable[..., BinaryIO]) -> str:
with opener(url, timeout=30) as response:
text = response.read().decode("ascii", errors="strict").strip()
checksum = text.split()[0] if text else ""
if not re.fullmatch(r"[0-9a-fA-F]{128}", checksum):
raise RuntimeError("Apache Maven 校验文件格式无效。")
return checksum
def _sha512(path: Path) -> str:
digest = hashlib.sha512()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _safe_extract_zip(archive_path: Path, destination: Path) -> None:
destination.mkdir(parents=True, exist_ok=True)
destination_resolved = destination.resolve()
with zipfile.ZipFile(archive_path) as archive:
for member in archive.infolist():
target = (destination / member.filename).resolve()
if target != destination_resolved and destination_resolved not in target.parents:
raise RuntimeError("Maven 安装包包含不安全的文件路径。")
archive.extractall(destination)
def run_install_command(command: list[str], on_output) -> int:
output_lines: list[str] = []
process = subprocess.Popen(

View File

@@ -79,6 +79,8 @@ def build_environment_update(tool_name: str, executable: Path) -> EnvironmentUpd
env_vars["JAVA_HOME"] = str(bin_dir.parent)
elif tool_name == "Maven":
env_vars["MAVEN_HOME"] = str(bin_dir.parent)
elif tool_name == "Tomcat":
env_vars["CATALINA_HOME"] = str(bin_dir.parent)
elif tool_name == "Gradle":
env_vars["GRADLE_HOME"] = str(bin_dir.parent)

View File

@@ -50,7 +50,7 @@ def scan_project(root: str | Path) -> ScanResult:
def _find_backend(root: Path) -> ProjectComponent | None:
candidates: list[tuple[int, Path, str]] = []
candidates: list[tuple[int, Path, str, str, dict[str, str]]] = []
for build_file in _walk_project_files(root, names={"pom.xml", "build.gradle", "build.gradle.kts"}):
project_dir = build_file.parent
score = 1
@@ -58,9 +58,29 @@ def _find_backend(root: Path) -> ProjectComponent | None:
text = _read_text(build_file)
metadata = _extract_backend_metadata(text)
metadata.update(_extract_backend_runtime_metadata(project_dir))
if "spring-boot" in text or "org.springframework.boot" in text:
is_spring_boot = "spring-boot" in text or "org.springframework.boot" in text
web_xml = project_dir / "src" / "main" / "webapp" / "WEB-INF" / "web.xml"
packaging = metadata.get("packaging", "")
is_ssm = not is_spring_boot and (
packaging == "war"
or web_xml.exists()
or ("spring" in text and "mybatis" in text)
)
if not is_spring_boot and not is_ssm:
continue
if is_spring_boot:
score += 3
detail_parts.append("Spring Boot dependency")
kind = "Spring Boot"
metadata["backend_type"] = "spring_boot"
else:
score += 3
kind = "SSM"
metadata["backend_type"] = "ssm"
metadata["packaging"] = "war"
context_name = metadata.get("final_name") or metadata.get("artifact_id") or project_dir.name
metadata.setdefault("context_path", f"/{context_name}")
detail_parts.append("SSM WAR application")
if java_version := metadata.get("java_version"):
detail_parts.append(f"Java {java_version}")
if boot_version := metadata.get("spring_boot_version"):
@@ -68,14 +88,14 @@ def _find_backend(root: Path) -> ProjectComponent | None:
if (project_dir / "src" / "main" / "java").exists():
score += 2
detail_parts.append("src/main/java")
candidates.append((score, project_dir, ", ".join(detail_parts), metadata))
candidates.append((score, project_dir, kind, ", ".join(detail_parts), metadata))
if not candidates:
return None
score, path, detail, metadata = sorted(candidates, key=lambda item: (-item[0], len(item[1].parts)))[0]
score, path, kind, detail, metadata = sorted(candidates, key=lambda item: (-item[0], len(item[1].parts)))[0]
if score < 2:
return None
return ProjectComponent("Spring Boot", path, detail, metadata)
return ProjectComponent(kind, path, detail, metadata)
def _find_frontends(root: Path) -> list[ProjectComponent]:
@@ -188,6 +208,11 @@ def _load_json(path: Path) -> dict:
def _extract_backend_metadata(text: str) -> dict[str, str]:
metadata: dict[str, str] = {}
project_text = re.sub(r"<parent\b[^>]*>.*?</parent>", "", text, flags=re.IGNORECASE | re.DOTALL)
for key, tag in (("packaging", "packaging"), ("artifact_id", "artifactId"), ("final_name", "finalName")):
match = re.search(rf"<{tag}>\s*([^<\s]+)\s*</{tag}>", project_text, re.IGNORECASE)
if match:
metadata[key] = match.group(1).strip()
java_patterns = [
r"<maven\.compiler\.source>\s*([^<\s]+)\s*</maven\.compiler\.source>",
r"<maven\.compiler\.target>\s*([^<\s]+)\s*</maven\.compiler\.target>",

View File

@@ -35,6 +35,10 @@ def main() -> int:
pid_path.parent.mkdir(parents=True, exist_ok=True)
encoding = locale.getpreferredencoding(False) or "utf-8"
environment = os.environ.copy()
executable = Path(command[0])
if executable.name.lower() in {"catalina.bat", "catalina.sh"} and executable.exists():
environment["CATALINA_HOME"] = str(executable.parent.parent)
with log_path.open("a", encoding="utf-8") as log_file:
process = subprocess.Popen(
command,
@@ -46,6 +50,7 @@ def main() -> int:
errors="replace",
bufsize=1,
shell=False,
env=environment,
)
pid_path.write_text(str(process.pid), encoding="ascii")
assert process.stdout is not None