|
@@ -1,17 +1,13 @@
|
|
|
package cn.iocoder.yudao.module.iot.service.plugininfo;
|
|
|
|
|
|
-import cn.hutool.core.io.IoUtil;
|
|
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
|
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
|
|
-import cn.iocoder.yudao.module.infra.api.file.FileApi;
|
|
|
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
|
|
|
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
|
|
|
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
|
|
|
import cn.iocoder.yudao.module.iot.dal.mysql.plugininfo.PluginInfoMapper;
|
|
|
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
|
|
|
-import jakarta.annotation.PostConstruct;
|
|
|
import jakarta.annotation.Resource;
|
|
|
-import lombok.SneakyThrows;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.pf4j.PluginDescriptor;
|
|
|
import org.pf4j.PluginState;
|
|
@@ -22,11 +18,13 @@ import org.springframework.stereotype.Service;
|
|
|
import org.springframework.validation.annotation.Validated;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
-import java.nio.file.Path;
|
|
|
+import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.file.*;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
import java.util.concurrent.Executors;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
-import java.util.jar.JarEntry;
|
|
|
-import java.util.jar.JarFile;
|
|
|
|
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
|
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
|
|
@@ -47,9 +45,6 @@ public class PluginInfoServiceImpl implements PluginInfoService {
|
|
|
@Resource
|
|
|
private SpringPluginManager pluginManager;
|
|
|
|
|
|
- @Resource
|
|
|
- private FileApi fileApi;
|
|
|
-
|
|
|
@Value("${pf4j.pluginsDir}")
|
|
|
private String pluginsDir;
|
|
|
|
|
@@ -95,6 +90,19 @@ public class PluginInfoServiceImpl implements PluginInfoService {
|
|
|
|
|
|
// 删除
|
|
|
pluginInfoMapper.deleteById(id);
|
|
|
+ // 删除插件文件
|
|
|
+ Executors.newSingleThreadExecutor().submit(() -> {
|
|
|
+ try {
|
|
|
+ TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕
|
|
|
+ File file = new File(pluginsDir, pluginInfoDO.getFile());
|
|
|
+ if (file.exists() && !file.delete()) {
|
|
|
+ log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFile());
|
|
|
+ }
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFile(), e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
}
|
|
|
|
|
|
private PluginInfoDO validatePluginInfoExists(Long id) {
|
|
@@ -116,73 +124,100 @@ public class PluginInfoServiceImpl implements PluginInfoService {
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- public void uploadJar(Long id, MultipartFile file) {
|
|
|
- // 1. 校验存在
|
|
|
+ public void uploadFile(Long id, MultipartFile file) {
|
|
|
+ // 1. 校验插件信息是否存在
|
|
|
PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
|
|
|
|
|
|
- // 2. 判断文件名称与插件 ID 是否匹配
|
|
|
+ // 2. 获取插件 ID
|
|
|
String pluginId = pluginInfoDo.getPluginId();
|
|
|
|
|
|
- // 3. 停止卸载旧的插件
|
|
|
- // 3.1. 获取插件信息
|
|
|
+ // 3. 停止并卸载旧的插件
|
|
|
+ stopAndUnloadPlugin(pluginId);
|
|
|
+
|
|
|
+ // 4. 上传新的插件文件
|
|
|
+ String pluginIdNew = uploadAndLoadNewPlugin(file);
|
|
|
+
|
|
|
+ // 5. 更新插件启用状态文件
|
|
|
+ updatePluginStatusFile(pluginIdNew, false);
|
|
|
+
|
|
|
+ // 6. 更新插件信息
|
|
|
+ updatePluginInfo(pluginInfoDo, pluginIdNew, file);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 停止并卸载旧的插件
|
|
|
+ private void stopAndUnloadPlugin(String pluginId) {
|
|
|
PluginWrapper plugin = pluginManager.getPlugin(pluginId);
|
|
|
if (plugin != null) {
|
|
|
- // 3.2. 如果插件状态是启动的,停止插件
|
|
|
if (plugin.getPluginState().equals(PluginState.STARTED)) {
|
|
|
- pluginManager.stopPlugin(pluginId);
|
|
|
+ pluginManager.stopPlugin(pluginId); // 停止插件
|
|
|
}
|
|
|
- // 3.3. 卸载插件
|
|
|
- pluginManager.unloadPlugin(pluginId);
|
|
|
+ pluginManager.unloadPlugin(pluginId); // 卸载插件
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 4. 上传插件
|
|
|
- String pluginIdNew;
|
|
|
+ // 上传并加载新的插件文件
|
|
|
+ private String uploadAndLoadNewPlugin(MultipartFile file) {
|
|
|
+ Path pluginsPath = Paths.get(pluginsDir);
|
|
|
try {
|
|
|
- String path = fileApi.createFile(pluginsDir, IoUtil.readBytes(file.getInputStream()));
|
|
|
- Path pluginPath = Path.of(path);
|
|
|
- pluginIdNew = pluginManager.loadPlugin(pluginPath);
|
|
|
+ if (!Files.exists(pluginsPath)) {
|
|
|
+ Files.createDirectories(pluginsPath); // 创建插件目录
|
|
|
+ }
|
|
|
+ String filename = file.getOriginalFilename();
|
|
|
+ if (filename != null) {
|
|
|
+ Path jarPath = pluginsPath.resolve(filename);
|
|
|
+ Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
|
|
|
+ return pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
|
|
|
+ } else {
|
|
|
+ throw exception(PLUGIN_INSTALL_FAILED);
|
|
|
+ }
|
|
|
} catch (Exception e) {
|
|
|
throw exception(PLUGIN_INSTALL_FAILED);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginIdNew);
|
|
|
- if (pluginWrapper == null) {
|
|
|
- throw exception(PLUGIN_INSTALL_FAILED);
|
|
|
- }
|
|
|
+ // 更新插件状态文件
|
|
|
+ private void updatePluginStatusFile(String pluginIdNew, boolean isEnabled) {
|
|
|
+ Path enabledFilePath = Paths.get(pluginsDir, "enabled.txt");
|
|
|
+ Path disabledFilePath = Paths.get(pluginsDir, "disabled.txt");
|
|
|
+ Path targetFilePath = isEnabled ? enabledFilePath : disabledFilePath;
|
|
|
+ Path oppositeFilePath = isEnabled ? disabledFilePath : enabledFilePath;
|
|
|
|
|
|
- // 5. 读取配置文件和脚本
|
|
|
- String configJson = "";
|
|
|
- String script = "";
|
|
|
- try (JarFile jarFile = new JarFile(pluginWrapper.getPluginPath().toFile())) {
|
|
|
- // 5.1 获取config文件在jar包中的路径
|
|
|
- String configFile = "classes/config.json";
|
|
|
- JarEntry configEntry = jarFile.getJarEntry(configFile);
|
|
|
-
|
|
|
- if (configEntry != null) {
|
|
|
- // 5.2 读取配置文件
|
|
|
- configJson = IoUtil.readUtf8(jarFile.getInputStream(configEntry));
|
|
|
- log.info("configJson:{}", configJson);
|
|
|
+ try {
|
|
|
+ PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginIdNew);
|
|
|
+ if (pluginWrapper == null) {
|
|
|
+ throw exception(PLUGIN_INSTALL_FAILED);
|
|
|
+ }
|
|
|
+ String pluginInfo = pluginIdNew + "@" + pluginWrapper.getDescriptor().getVersion();
|
|
|
+ List<String> targetLines = Files.exists(targetFilePath) ? Files.readAllLines(targetFilePath)
|
|
|
+ : new ArrayList<>();
|
|
|
+ List<String> oppositeLines = Files.exists(oppositeFilePath) ? Files.readAllLines(oppositeFilePath)
|
|
|
+ : new ArrayList<>();
|
|
|
+
|
|
|
+ if (!targetLines.contains(pluginInfo)) {
|
|
|
+ targetLines.add(pluginInfo);
|
|
|
+ Files.write(targetFilePath, targetLines, StandardOpenOption.CREATE,
|
|
|
+ StandardOpenOption.TRUNCATE_EXISTING);
|
|
|
}
|
|
|
|
|
|
- // 5.3 读取script.js脚本
|
|
|
- String scriptFile = "classes/script.js";
|
|
|
- JarEntry scriptEntity = jarFile.getJarEntry(scriptFile);
|
|
|
- if (scriptEntity != null) {
|
|
|
- // 5.4 读取脚本文件
|
|
|
- script = IoUtil.readUtf8(jarFile.getInputStream(scriptEntity));
|
|
|
- log.info("script:{}", script);
|
|
|
+ if (oppositeLines.contains(pluginInfo)) {
|
|
|
+ oppositeLines.remove(pluginInfo);
|
|
|
+ Files.write(oppositeFilePath, oppositeLines, StandardOpenOption.CREATE,
|
|
|
+ StandardOpenOption.TRUNCATE_EXISTING);
|
|
|
}
|
|
|
- } catch (Exception e) {
|
|
|
+ } catch (IOException e) {
|
|
|
throw exception(PLUGIN_INSTALL_FAILED);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
+ // 更新插件信息
|
|
|
+ private void updatePluginInfo(PluginInfoDO pluginInfoDo, String pluginIdNew, MultipartFile file) {
|
|
|
pluginInfoDo.setPluginId(pluginIdNew);
|
|
|
pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus());
|
|
|
pluginInfoDo.setFile(file.getOriginalFilename());
|
|
|
- pluginInfoDo.setConfigSchema(configJson);
|
|
|
- pluginInfoDo.setScript(script);
|
|
|
+ pluginInfoDo.setScript("");
|
|
|
|
|
|
- PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
|
|
|
+ PluginDescriptor pluginDescriptor = pluginManager.getPlugin(pluginIdNew).getDescriptor();
|
|
|
+ pluginInfoDo.setConfigSchema(pluginDescriptor.getPluginDescription());
|
|
|
pluginInfoDo.setVersion(pluginDescriptor.getVersion());
|
|
|
pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription());
|
|
|
pluginInfoMapper.updateById(pluginInfoDo);
|
|
@@ -190,52 +225,50 @@ public class PluginInfoServiceImpl implements PluginInfoService {
|
|
|
|
|
|
@Override
|
|
|
public void updatePluginStatus(Long id, Integer status) {
|
|
|
- // 1. 校验存在
|
|
|
+ // 1. 校验插件信息是否存在
|
|
|
PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
|
|
|
|
|
|
- // 插件状态无效
|
|
|
+ // 2. 校验插件状态是否有效
|
|
|
if (!IotPluginStatusEnum.contains(status)) {
|
|
|
throw exception(PLUGIN_STATUS_INVALID);
|
|
|
}
|
|
|
|
|
|
+ // 3. 获取插件ID和插件实例
|
|
|
String pluginId = pluginInfoDo.getPluginId();
|
|
|
PluginWrapper plugin = pluginManager.getPlugin(pluginId);
|
|
|
+
|
|
|
+ // 4. 根据状态更新插件
|
|
|
if (plugin != null) {
|
|
|
- if (status.equals(IotPluginStatusEnum.RUNNING.getStatus()) && plugin.getPluginState() != PluginState.STARTED) {
|
|
|
- // 启动插件
|
|
|
+ // 4.1 如果目标状态是运行且插件未启动,则启动插件
|
|
|
+ if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
|
|
|
+ && plugin.getPluginState() != PluginState.STARTED) {
|
|
|
pluginManager.startPlugin(pluginId);
|
|
|
- } else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus()) && plugin.getPluginState() == PluginState.STARTED) {
|
|
|
- // 停止插件
|
|
|
+ updatePluginStatusFile(pluginId, true); // 更新插件状态文件为启用
|
|
|
+ }
|
|
|
+ // 4.2 如果目标状态是停止且插件已启动,则停止插件
|
|
|
+ else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
|
|
|
+ && plugin.getPluginState() == PluginState.STARTED) {
|
|
|
pluginManager.stopPlugin(pluginId);
|
|
|
+ updatePluginStatusFile(pluginId, false); // 更新插件状态文件为禁用
|
|
|
}
|
|
|
} else {
|
|
|
- // 已经停止,未获取到插件
|
|
|
+ // 5. 插件不存在且状态为停止,抛出异常
|
|
|
if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
|
|
|
throw exception(PLUGIN_STATUS_INVALID);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 6. 更新数据库中的插件状态
|
|
|
pluginInfoDo.setStatus(status);
|
|
|
pluginInfoMapper.updateById(pluginInfoDo);
|
|
|
}
|
|
|
|
|
|
-// @PostConstruct
|
|
|
-// public void init() {
|
|
|
-// Executors.newSingleThreadScheduledExecutor().schedule(this::startPlugins, 3, TimeUnit.SECONDS);
|
|
|
-// }
|
|
|
-//
|
|
|
-// @SneakyThrows
|
|
|
-// private void startPlugins() {
|
|
|
-// for (PluginInfoDO pluginInfoDO : pluginInfoMapper.selectList()) {
|
|
|
-// if (!IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
|
|
|
-// continue;
|
|
|
-// }
|
|
|
-// log.info("start plugin:{}", pluginInfoDO.getPluginId());
|
|
|
-// try {
|
|
|
-// pluginManager.startPlugin(pluginInfoDO.getPluginId());
|
|
|
-// } catch (Exception e) {
|
|
|
-// log.error("start plugin error", e);
|
|
|
-// }
|
|
|
-// }
|
|
|
-// }
|
|
|
+ @Override
|
|
|
+ public List<String> getEnabledPlugins() {
|
|
|
+ return pluginInfoMapper.selectList().stream()
|
|
|
+ .filter(pluginInfoDO -> IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus()))
|
|
|
+ .map(PluginInfoDO::getPluginId)
|
|
|
+ .toList();
|
|
|
+ }
|
|
|
|
|
|
}
|