package com.example.demo.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.dao.entity.WExecuteHost;
import com.example.demo.dao.entity.WHostProcess;
import com.example.demo.dao.entity.WPersonalHost;
import com.example.demo.dao.mapper.WExecuteHostMapper;
import com.example.demo.request.ProcessRequest;
import com.example.demo.service.WExecuteHostService;
import com.example.demo.service.WHostProcessService;
import com.example.demo.service.WPersonalHostService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import javax.annotation.PreDestroy;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class WExecuteHostServiceImpl extends ServiceImpl<WExecuteHostMapper, WExecuteHost>
implements WExecuteHostService {
@Autowired
private WPersonalHostService wPersonalHostService;
@Autowired
private WHostProcessService wHostProcessService;
@Data
private static class UserSession {
private Process cmdProcess;
private volatile boolean isProcessRunning;
private String sessionId;
private String logFilePath;
private final Queue<String> logBuffer = new ConcurrentLinkedQueue<>();
private static final int BATCH_SIZE = 50; // 批量写入阈值
// 命令执行状态锁
private final AtomicBoolean isExecuting = new AtomicBoolean(false);
}
private final Map<String, UserSession> userSessions = new ConcurrentHashMap<>();
private final SimpMessagingTemplate messagingTemplate;
// 异步日志写入线程池
private static final ExecutorService LOG_WRITER_POOL = Executors.newCachedThreadPool(
new ThreadFactoryBuilder().setNamePrefix("log-writer-").build());
// 日志刷新调度器
private static final ScheduledExecutorService LOG_FLUSH_SCHEDULER =
Executors.newSingleThreadScheduledExecutor();
public WExecuteHostServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
startLogFlushService();
}
// 初始化日志刷新服务
private void startLogFlushService() {
LOG_FLUSH_SCHEDULER.scheduleAtFixedRate(() -> {
userSessions.values().forEach(session -> {
if (!session.getLogBuffer().isEmpty()) {
List<String> batch = CollUtil.newArrayList(session.getLogBuffer());
session.getLogBuffer().clear();
asyncWriteLog(session.getLogFilePath(), String.join("\n", batch));
}
});
}, 0, 1, TimeUnit.SECONDS);
}
@Override
public void executeCommand(String command, String host, String userId) {
// 0. ABORT命令特殊处理(优先处理终止请求)
if ("ABORT".equalsIgnoreCase(command)) {
handleAbort(userId);
return;
}
// 1. 权限校验
if (!validateUserHost(userId, host)) {
sendError("无权访问该主机", userId);
return;
}
// 2. 检查用户当前会话状态
UserSession session = userSessions.get(userId);
if (session != null && session.isExecuting.get()) {
sendError("已有命令执行中,请等待完成或使用ABORT终止", userId);
return;
}
// 3. 创建新会话(带原子状态检查)
session = userSessions.computeIfAbsent(userId, key -> {
UserSession newSession = createNewSession(userId, host);
if (newSession != null) {
newSession.isExecuting.set(true); // 标记为执行中
}
return newSession;
});
if (session == null) return;
// 4. 写入日志并执行命令
try {
// 确保获得执行锁
if (!session.isExecuting.compareAndSet(true, true)) {
sendError("命令执行冲突,请重试", userId);
return;
}
session.getLogBuffer().offer("——————————————— " + DateUtil.now() + " ———————————————");
// 发送命令到进程
IoUtil.write(session.getCmdProcess().getOutputStream(),
Charset.forName("gbk"), true, command + "\n");
} catch (Exception e) {
session.isExecuting.set(false); // 发生异常时释放锁
sendError("命令发送失败: " + e.getMessage(), userId);
}
}
@Override
public Boolean isCommandExecuting(String userId) {
UserSession session = userSessions.get(userId);
return session != null && session.isExecuting.get();
}
@Override
public void handleAbort(String userId) {
UserSession session = userSessions.get(userId);
if (session == null || session.getCmdProcess() == null) {
sendError("没有活动的命令进程", userId);
return;
}
try {
long pid = session.getCmdProcess().pid();
System.out.println("Attempting to kill process with PID: " + pid);
// 使用 taskkill 命令终止进程
ProcessBuilder taskKill = new ProcessBuilder("taskkill", "/F", "/T", "/PID", String.valueOf(pid));
Process killProcess = taskKill.start();
// 等待命令执行完成
int exitCode = killProcess.waitFor();
System.out.println("taskkill exit code: " + exitCode);
if (exitCode == 0) {
// 进程终止成功
session.isExecuting.set(false);
cleanupSession(userId);
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, "✔️" + "进程已通过 taskkill 终止 (PID: " + pid + ")");
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, "");
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, System.getProperty("user.dir") + ">");
} else {
// 进程终止失败
sendError("终止进程失败,错误码: " + exitCode, userId);
}
} catch (IOException | InterruptedException e) {
System.err.println("Error killing process: " + e.getMessage());
sendError("终止进程失败: " + e.getMessage(), userId);
}
}
@Override
public String startProcess(ProcessRequest processRequest) {
try {
// 数据库表新增数据
String p13 = processRequest.getP13().trim();
String processName = processRequest.getProcessName().trim();
String productNumber = processRequest.getProductNumber().trim();
String executeHost = processRequest.getExecuteHost().trim();
String department = processRequest.getDepartment().trim();
String version = processRequest.getVersion().trim();
if (StrUtil.isEmpty(p13) || StrUtil.isEmpty(processName) || StrUtil.isEmpty(productNumber)
|| StrUtil.isEmpty(executeHost) || StrUtil.isEmpty(department) || StrUtil.isEmpty(version)) {
return "新增进程失败。";
}
WHostProcess wHostProcess = new WHostProcess();
wHostProcess.setP13(p13);
wHostProcess.setProcessName(processName);
wHostProcess.setProductNumber(productNumber);
wHostProcess.setHost(executeHost);
wHostProcess.setPid(null);
wHostProcess.setDepartment(department);
wHostProcess.setState("离线");
wHostProcess.setVersion(version);
wHostProcess.setBeginTime(new Date());
boolean saveResult = wHostProcessService.save(wHostProcess);
if (saveResult) {
LambdaQueryWrapper<WPersonalHost> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper
.eq(WPersonalHost::getExecuteHost, processRequest.getExecuteHost())
.eq(WPersonalHost::getSharedHost, processRequest.getSharedHost());
WPersonalHost wPersonalHost = wPersonalHostService.getOne(queryWrapper);
// 执行py启动命令
//todo 后续动态
String pythonEXEPath = "D:\\miniforge\\envs" + File.separator + p13
+ File.separator + "python" + wPersonalHost.getPythonEnv() + File.separator + "python.exe";
String mainPyPath = System.getProperty("user.dir") + File.separator + "python-package"
+ File.separator + executeHost + File.separator + p13 + File.separator + "test" + File.separator + "main.py";
executeCommand(pythonEXEPath + " " + mainPyPath,
processRequest.getExecuteHost(),
processRequest.getP13());
return "正在启动项目...";
}
} catch (Exception e) {
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "新增进程失败。";
}
private boolean validateUserHost(String userId, String executeHost) {
LambdaQueryWrapper<WPersonalHost> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(WPersonalHost::getP13, userId)
.eq(WPersonalHost::getExecuteHost, executeHost)
.eq(WPersonalHost::getState, "在线");
return wPersonalHostService.getOne(queryWrapper) != null;
}
private UserSession createNewSession(String userId, String executeHost) {
try {
UserSession session = new UserSession();
session.setSessionId(UUID.randomUUID().toString());
session.setLogFilePath(initLogFile(userId, executeHost));
// 启动CMD进程(带唯一标题)
ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/k", "title " + generateUniqueTitle(userId));
session.setCmdProcess(pb.redirectErrorStream(true).start());
// 启动输出监听线程
startOutputThread(session, userId);
return session;
} catch (IOException e) {
sendError("进程启动失败: " + e.getMessage(), userId);
return null;
}
}
private String initLogFile(String userId, String executeHost) {
// 1. 构建基础路径(使用File.separator)
String baseDir = FileUtil.normalize(
System.getProperty("user.dir") + File.separator + "command-log");
// 2. 构建安全路径(统一使用File.separator)
String safePath = FileUtil.normalize(
baseDir + File.separator + userId + File.separator + executeHost
+ File.separator + "项目名称");
// 3. 安全校验(现在路径分隔符一致)
if (!safePath.startsWith(baseDir)) {
throw new SecurityException("非法日志路径: " + safePath);
}
// 4. 创建目录(自动处理路径分隔符)
FileUtil.mkdir(safePath);
// 5. 生成日志文件
String logFileName = DateUtil.today() + ".log";
return FileUtil.touch(safePath + File.separator + logFileName).getAbsolutePath();
}
private void startOutputThread(UserSession session, String userId) {
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(session.getCmdProcess().getInputStream(), "GBK"))) {
String line;
while ((line = reader.readLine()) != null) {
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, line);
session.getLogBuffer().offer(line);
}
} catch (Exception e) {
sendError("输出流异常: " + e.getMessage(), userId);
} finally {
session.isExecuting.set(false); // 命令结束释放锁
cleanupSession(userId);
// 通知前端命令执行结束
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, "⏹ 该命令执行已结束");
}
}).start();
}
private void asyncWriteLog(String logFilePath, String content) {
CompletableFuture.runAsync(() -> {
try {
// 替换掉日志文件中没用的信息,如多余的路径信息
String currentDir = System.getProperty("user.dir");
String escapedDir = currentDir.replace("\\", "\\\\");
String regex = "(?m)^\\s*" + escapedDir + ">(?!\\S)\\s*";
// 创建 Pattern 和 Matcher 对象
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content);
// 检查是否存在匹配的模式
if (matcher.find()) {
// 如果存在匹配,则进行替换
String cleaned = content.replaceAll(regex, "");
FileUtil.appendString(cleaned + System.lineSeparator(), logFilePath, "GBK");
} else {
FileUtil.appendString(content + System.lineSeparator(), logFilePath, "GBK");
}
} catch (Exception e) {
System.err.println("日志写入失败: " + e.getMessage());
}
}, LOG_WRITER_POOL);
}
private void cleanupSession(String userId) {
UserSession session = userSessions.remove(userId);
if (session != null) {
try {
if (session.getCmdProcess() != null) {
session.getCmdProcess().destroyForcibly();
}
// 强制将剩余日志写入文件(新增代码)
if (!session.getLogBuffer().isEmpty()) {
asyncWriteLog(session.getLogFilePath(), String.join("\n", session.getLogBuffer()));
session.getLogBuffer().clear();
}
} catch (Exception ignored) {
}
}
}
@PreDestroy
public void cleanup() {
LOG_FLUSH_SCHEDULER.shutdown();
LOG_WRITER_POOL.shutdown();
userSessions.forEach((userId, session) -> {
try {
if (session.getCmdProcess() != null) {
session.getCmdProcess().destroyForcibly();
}
} catch (Exception ignored) {
}
});
userSessions.clear();
}
/**
* 发送错误日志
*/
private void sendError(String message, String userId) {
try {
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, "❌" + message);
} catch (Exception ignored) {
}
}
/**
* 生成cmd窗口唯一id
*/
private String generateUniqueTitle(String userId) {
return "CMD_SESSION_" + userId + "_" + System.currentTimeMillis();
}
}
仔细阅读我的代码,现在有个bug,我执行下面的main.py文件,会报错请输入你的名字: 发生未知错误: EOF when reading a line
import random
import sys
from datetime import datetime
def generate_random_number(min_val=1, max_val=100):
"""
生成一个指定范围内的随机整数
参数:
min_val (int): 最小值,默认为1
max_val (int): 最大值,默认为100
返回:
int: 随机生成的整数
"""
return random.randint(min_val, max_val)
def check_even_odd(number):
"""
检查一个数字是奇数还是偶数
参数:
number (int): 要检查的数字
返回:
str: "偶数" 或 "奇数"
"""
return "偶数" if number % 2 == 0 else "奇数"
def greet_user(name):
"""
向用户问好
参数:
name (str): 用户名
"""
print(f"你好, {name}! 当前时间是: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
def main():
"""
主函数
"""
print("欢迎使用随机数生成器!")
try:
user_name = input("请输入你的名字: ")
greet_user(user_name)
count = int(input("你想生成多少个随机数? (1-10): "))
if count < 1 or count > 10:
print("请输入1到10之间的数字!")
return
for i in range(count):
num = generate_random_number()
print(f"随机数 #{i+1}: {num} ({check_even_odd(num)})")
print("感谢使用!")
except ValueError:
print("错误: 请输入有效的数字!")
except KeyboardInterrupt:
print("\n程序被用户中断")
except Exception as e:
print(f"发生未知错误: {e}")
if __name__ == "__main__":
main()