Bläddra i källkod

【代码优化】IoT: 插件管理

安浩浩 7 månader sedan
förälder
incheckning
0e20ca342f

+ 1 - 0
plugins/enabled.txt

@@ -0,0 +1 @@
+http-plugin@0.0.1

BIN
plugins/yudao-module-iot-http-plugin-2.2.0-snapshot.jar


+ 5 - 13
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/PluginInfoController.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.plugininfo;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoImportReqVO;
 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoRespVO;
 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
@@ -16,11 +17,8 @@ import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
 
 @Tag(name = "管理后台 - IoT 插件信息")
 @RestController
@@ -72,16 +70,10 @@ public class PluginInfoController {
         return success(BeanUtils.toBean(pageResult, PluginInfoRespVO.class));
     }
 
-    @RequestMapping(value = "/update-jar",
-            method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题
-    @Operation(summary = "上传Jar包")
-    public CommonResult<Boolean> uploadJar(
-            @RequestParam("id") Long id,
-            @RequestParam("jar") MultipartFile file) throws Exception {
-        if (file.isEmpty()) {
-            throw exception(FILE_IS_EMPTY);
-        }
-        pluginInfoService.uploadJar(id, file);
+    @PostMapping("/upload-file")
+    @Operation(summary = "上传插件文件")
+    public CommonResult<Boolean> uploadFile(@Valid PluginInfoImportReqVO reqVO) {
+        pluginInfoService.uploadFile(reqVO.getId(), reqVO.getFile());
         return success(true);
     }
 

+ 19 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/vo/PluginInfoImportReqVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springframework.web.multipart.MultipartFile;
+
+@Schema(description = "管理后台 - IoT 插件上传 Request VO")
+@Data
+public class PluginInfoImportReqVO {
+
+    @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
+    private Long id;
+
+    @Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "插件文件不能为空")
+    private MultipartFile file;
+
+}

+ 0 - 17
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/SpringConfiguration.java

@@ -1,17 +0,0 @@
-package cn.iocoder.yudao.module.iot.framework.plugin;
-
-import org.pf4j.spring.SpringPluginManager;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.DependsOn;
-
-@Configuration
-public class SpringConfiguration {
-
-    @Bean
-    @DependsOn("serviceRegistryInitializedMarker")
-    public SpringPluginManager pluginManager() {
-        return new SpringPluginManager();
-    }
-
-}

+ 14 - 13
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/ServiceRegistryConfiguration.java → yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/UnifiedConfiguration.java

@@ -1,35 +1,36 @@
 package cn.iocoder.yudao.module.iot.framework.plugin;
 
-import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
 import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
+import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
 import lombok.extern.slf4j.Slf4j;
+import org.pf4j.spring.SpringPluginManager;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
 
-import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
 
 @Slf4j
 @Configuration
-public class ServiceRegistryConfiguration {
+public class UnifiedConfiguration {
+
+    private static final String SERVICE_REGISTRY_INITIALIZED_MARKER = "serviceRegistryInitializedMarker";
 
     @Resource
     private DeviceDataApi deviceDataApi;
 
-    @PostConstruct
-    public void init() {
-        // 将主程序中的 DeviceDataApi 实例注册到 ServiceRegistry
+    @Bean(SERVICE_REGISTRY_INITIALIZED_MARKER)
+    public Object serviceRegistryInitializedMarker() {
         ServiceRegistry.registerService(DeviceDataApi.class, deviceDataApi);
         log.info("[init][将 DeviceDataApi 实例注册到 ServiceRegistry 中]");
+        return new Object();
     }
 
-    /**
-     * 定义一个标记用的 Bean,用于表示 ServiceRegistry 已初始化完成
-     */
-    @Bean("serviceRegistryInitializedMarker") // TODO @haohao:1)这个名字,可以搞个 public static final 常量;2)是不是 conditionBefore 啥
-    public Object serviceRegistryInitializedMarker() {
-        // 返回任意对象即可,这里返回 null 都可以,但最好返回个实际对象
-        return new Object();
+    @Bean
+    @DependsOn(SERVICE_REGISTRY_INITIALIZED_MARKER)
+    public SpringPluginManager pluginManager() {
+        log.info("[init][实例化 SpringPluginManager]");
+        return new SpringPluginManager();
     }
 
 }

+ 10 - 1
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininfo/PluginInfoService.java

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
 import jakarta.validation.Valid;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.List;
+
 /**
  * IoT 插件信息 Service 接口
  *
@@ -58,7 +60,7 @@ public interface PluginInfoService {
      * @param id   插件id
      * @param file 文件
      */
-    void uploadJar(Long id, MultipartFile file);
+    void uploadFile(Long id, MultipartFile file);
 
     /**
      * 更新插件的状态
@@ -67,4 +69,11 @@ public interface PluginInfoService {
      * @param status 状态
      */
     void updatePluginStatus(Long id, Integer status);
+
+    /**
+     * 获得启用的插件列表
+     *
+     * @return 插件列表-插件id
+     */
+    List<String> getEnabledPlugins();
 }

+ 110 - 77
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininfo/PluginInfoServiceImpl.java

@@ -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();
+    }
 
 }

+ 91 - 0
yudao-module-iot/yudao-module-iot-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/WelcomePlugin.java

@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package cn.iocoder.yudao.module.iot.plugin;
+
+import cn.iocoder.yudao.module.iot.api.Greeting;
+import org.apache.commons.lang.StringUtils;
+import org.pf4j.Extension;
+import org.pf4j.Plugin;
+import org.pf4j.PluginWrapper;
+import org.pf4j.RuntimeMode;
+import com.sun.net.httpserver.HttpServer;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+
+/**
+ * 打招呼 测试用例
+ */
+public class WelcomePlugin extends Plugin {
+
+    private HttpServer server;
+
+    public WelcomePlugin(PluginWrapper wrapper) {
+        super(wrapper);
+    }
+
+    @Override
+    public void start() {
+        System.out.println("WelcomePlugin.start()");
+        // for testing the development mode
+        if (RuntimeMode.DEVELOPMENT.equals(wrapper.getRuntimeMode())) {
+            System.out.println(StringUtils.upperCase("WelcomePlugin"));
+        }
+        startHttpServer();
+    }
+
+    @Override
+    public void stop() {
+        System.out.println("WelcomePlugin.stop()");
+        stopHttpServer();
+    }
+
+    private void startHttpServer() {
+        try {
+            server = HttpServer.create(new InetSocketAddress(9081), 0);
+            server.createContext("/", exchange -> {
+                String response = "Welcome to PF4J HTTP Server";
+                exchange.sendResponseHeaders(200, response.getBytes().length);
+                OutputStream os = exchange.getResponseBody();
+                os.write(response.getBytes());
+                os.close();
+            });
+            server.setExecutor(null);
+            server.start();
+            System.out.println("HTTP server started on port 9081");
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void stopHttpServer() {
+        if (server != null) {
+            server.stop(0);
+            System.out.println("HTTP server stopped");
+        }
+    }
+
+    @Extension
+    public static class WelcomeGreeting implements Greeting {
+
+        @Override
+        public String getGreeting() {
+            return "Welcome to PF4J";
+        }
+
+    }
+
+}

+ 2 - 0
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/pom.xml

@@ -24,6 +24,7 @@
         <plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpPlugin</plugin.class>
         <plugin.version>0.0.1</plugin.version>
         <plugin.provider>ahh</plugin.provider>
+        <plugin.description>http-plugin-0.0.1</plugin.description>
         <plugin.dependencies/>
     </properties>
 
@@ -104,6 +105,7 @@
                             <Plugin-Class>${plugin.class}</Plugin-Class>
                             <Plugin-Version>${plugin.version}</Plugin-Version>
                             <Plugin-Provider>${plugin.provider}</Plugin-Provider>
+                            <Plugin-Description>${plugin.description}</Plugin-Description>
                             <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
                         </manifestEntries>
                     </archive>