diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e1377e6..a8cfefc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,12 +12,11 @@ We recommend that developers following the rules [here](https://jeffkreeftmeijer
## Contributing
-- Fork the repository.
+- Fork the repository or create branches.
- Checkout your branch like a feature or bugfix.
- Make a pull request to the `develop` branch of this repository.
- Wait for review and PR merged.
---
-**Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://kcd.im/pull-request)
-
+**Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://kcd.im/pull-request)
\ No newline at end of file
diff --git a/README.md b/README.md
index 3929230..b629cd8 100644
--- a/README.md
+++ b/README.md
@@ -47,14 +47,14 @@ AntChain Bridge 插件服务(PluginServer, PS)用于管理异构链插件、
mvn clean package
```
-产生的安装包在`ps-bootstrap/target/plugin-server-0.1.2-SNAPSHOT.tar.gz`。
+产生的安装包在`ps-bootstrap/target/plugin-server-x.x.x.tar.gz`。
## 配置
-在获得安装包之后,执行解压缩操作,这里以`plugin-server-0.1.2-SNAPSHOT.tar.gz`为例。
+在获得安装包之后,执行解压缩操作,这里以`plugin-server-x.x.x.tar.gz`为例。
```
-tar -zxf plugin-server-0.1.2-SNAPSHOT.tar.gz
+tar -zxf plugin-server-x.x.x.tar.gz
```
进入解压后的目录,可以看到:
@@ -72,7 +72,7 @@ tree .
├── config
│ └── application.yml
└── lib
- └── ps-bootstrap-0.1.2-SNAPSHOT.jar
+ └── ps-bootstrap-x.x.x.jar
3 directories, 7 files
diff --git a/pom.xml b/pom.xml
index 3a29ea9..33913be 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,11 +14,12 @@
ps-service
ps-server
ps-pluginmanager
+ ps-cli
8
- 0.1.2-SNAPSHOT
+ 0.2.0
2.13.1.RELEASE
2.0.59.Final
3.19.1
@@ -26,12 +27,19 @@
1.42.2
1.3.5
4.13.2
- 0.1-SNAPSHOT
+ 0.1.1
+ 3.3.0
+ 3.0.17
+ 1.5.0
+ 5.8.10
+ 2.0.21
+ 3.8.0
+ 1.7.28
com.alipay.antchain.bridge
antchain-bridge-pluginserver
- 0.1.2-SNAPSHOT
+ 0.2.0
antchain-bridge-pluginserver
antchain-bridge-pluginserver
@@ -103,25 +111,48 @@
org.pf4j
pf4j
- 3.8.0
+ ${pf4j.version}
-
com.alipay.antchain.bridge
antchain-bridge-plugin-lib
${antchain-bridge.sdk.version}
-
org.slf4j
slf4j-api
- 1.7.28
+ ${slf4j.version}
-
org.slf4j
slf4j-simple
- 1.7.28
+ ${slf4j.version}
+
+
+ org.jline
+ jline
+ ${jline.version}
+
+
+ org.codehaus.groovy
+ groovy-all
+ ${groovy.version}
+ pom
+
+
+ commons-cli
+ commons-cli
+ ${commons-cli.version}
+
+
+ cn.hutool
+ hutool-all
+ ${hutool.version}
+
+
+ com.alibaba
+ fastjson
+ ${fastjson.version}
diff --git a/ps-bootstrap/pom.xml b/ps-bootstrap/pom.xml
index 6ec7d41..1f829cc 100644
--- a/ps-bootstrap/pom.xml
+++ b/ps-bootstrap/pom.xml
@@ -6,7 +6,7 @@
com.alipay.antchain.bridge
antchain-bridge-pluginserver
- 0.1.2-SNAPSHOT
+ 0.2.0
ps-bootstrap
diff --git a/ps-bootstrap/src/main/java/com/alipay/antchain/bridge/pluginserver/config/PluginManagerConfiguration.java b/ps-bootstrap/src/main/java/com/alipay/antchain/bridge/pluginserver/config/PluginManagerConfiguration.java
index 4660e37..76715f4 100644
--- a/ps-bootstrap/src/main/java/com/alipay/antchain/bridge/pluginserver/config/PluginManagerConfiguration.java
+++ b/ps-bootstrap/src/main/java/com/alipay/antchain/bridge/pluginserver/config/PluginManagerConfiguration.java
@@ -17,6 +17,7 @@
package com.alipay.antchain.bridge.pluginserver.config;
import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.StrUtil;
import com.alipay.antchain.bridge.pluginserver.pluginmanager.IPluginManagerWrapper;
import com.alipay.antchain.bridge.pluginserver.pluginmanager.PluginManagerWrapperImpl;
import com.alipay.antchain.bridge.pluginserver.server.PluginManagementServiceImpl;
@@ -24,11 +25,14 @@
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.ClassLoadingStrategy;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -43,6 +47,9 @@ public class PluginManagerConfiguration {
@Value("${pluginserver.plugin.policy.classloader.resource.ban-with-prefix.APPLICATION}")
private String[] resourceBannedPrefixOnAppLevel;
+ @Value("${pluginserver.managerserver.host}")
+ private String managementHost;
+
@Bean
public IPluginManagerWrapper pluginManagerWrapper() {
return new PluginManagerWrapperImpl(
@@ -66,10 +73,14 @@ private Map> convertPathPrefixBannedMap
private String pluginServerMgrPort;
@Bean
- public Server pluginMgrServer() throws IOException {
+ public Server pluginMgrServer(@Autowired PluginManagementServiceImpl pluginManagementService) throws IOException {
log.info("Starting plugin managing server on port " + pluginServerMgrPort);
- return NettyServerBuilder.forPort(Integer.parseInt(pluginServerMgrPort))
- .addService(new PluginManagementServiceImpl())
+ return NettyServerBuilder.forAddress(
+ new InetSocketAddress(
+ StrUtil.isEmpty(managementHost) ? InetAddress.getLoopbackAddress() : InetAddress.getByName(managementHost),
+ Integer.parseInt(pluginServerMgrPort)
+ )
+ ).addService(pluginManagementService)
.build()
.start();
}
diff --git a/ps-bootstrap/src/main/resources/application.yml b/ps-bootstrap/src/main/resources/application.yml
index b719be1..da0752c 100644
--- a/ps-bootstrap/src/main/resources/application.yml
+++ b/ps-bootstrap/src/main/resources/application.yml
@@ -28,9 +28,12 @@ pluginserver:
# where to load the hetero-chain plugins
repo: ./plugins
policy:
+ # limit actions of the plugin classloader
classloader:
resource:
ban-with-prefix:
+ # the plugin classloader will not read the resource file starting with the prefix below
APPLICATION: "META-INF/services/io.grpc."
managerserver:
+ host: localhost
port: 9091
\ No newline at end of file
diff --git a/ps-bootstrap/src/main/resources/scripts/start.sh b/ps-bootstrap/src/main/resources/scripts/start.sh
index 59ee7d0..f451440 100755
--- a/ps-bootstrap/src/main/resources/scripts/start.sh
+++ b/ps-bootstrap/src/main/resources/scripts/start.sh
@@ -7,7 +7,9 @@ print_title
log_info "start plugin-server now..."
-java -jar -Dlogging.file.path=${CURR_DIR}/../log ${CURR_DIR}/../lib/ps-bootstrap-0.1.2-SNAPSHOT.jar --spring.config.location=file:${CURR_DIR}/../config/application.yml > /dev/null 2>&1 &
+JAR_FILE=`ls ${CURR_DIR}/../lib/ | grep '.jar'`
+
+java -jar -Dlogging.file.path=${CURR_DIR}/../log ${CURR_DIR}/../lib/${JAR_FILE} --spring.config.location=file:${CURR_DIR}/../config/application.yml > /dev/null 2>&1 &
if [ $? -ne 0 ]; then
log_error "failed to start plugin-server"
exit 1
diff --git a/ps-bootstrap/src/test/java/com/alipay/antchain/bridge/pluginserver/AntChainBridgePluginManagementApplicationTests.java b/ps-bootstrap/src/test/java/com/alipay/antchain/bridge/pluginserver/AntChainBridgePluginManagementApplicationTests.java
index afe5385..044496b 100644
--- a/ps-bootstrap/src/test/java/com/alipay/antchain/bridge/pluginserver/AntChainBridgePluginManagementApplicationTests.java
+++ b/ps-bootstrap/src/test/java/com/alipay/antchain/bridge/pluginserver/AntChainBridgePluginManagementApplicationTests.java
@@ -22,6 +22,7 @@
import com.alipay.antchain.bridge.pluginserver.server.CrossChainServiceImpl;
import com.alipay.antchain.bridge.pluginserver.server.PluginManagementServiceImpl;
import com.alipay.antchain.bridge.pluginserver.server.ResponseBuilder;
+import com.alipay.antchain.bridge.pluginserver.server.exception.ServerErrorCodeEnum;
import com.alipay.antchain.bridge.pluginserver.service.CallBBCRequest;
import com.alipay.antchain.bridge.pluginserver.service.CallBBCResponse;
import com.alipay.antchain.bridge.pluginserver.service.Response;
@@ -83,7 +84,7 @@ public void testManageLoadAndStartPluginsReq() {
@Test
@DirtiesContext
- public void testManagePluginReq(){
+ public void testManagePluginReq() {
PluginManageRequest pluginManageRequest;
// 1. load plugin
@@ -163,7 +164,7 @@ public void testManagePluginReq(){
@Test
@DirtiesContext
- public void testPluginQueryReq(){
+ public void testPluginQueryReq() {
PluginManageRequest pluginManageRequest;
// 1. All plugins in the default path are automatically loaded when the service starts
@@ -174,7 +175,7 @@ public void testPluginQueryReq(){
mngResponseStreamObserver = Mockito.mock(StreamObserver.class);
pluginManagementService.hasPlugins(hasPluginsRequest, mngResponseStreamObserver);
Mockito.verify(mngResponseStreamObserver).onNext(ResponseBuilder.buildHasPluginsResp(
- HasPluginsResp.newBuilder().putAllResults(new HashMap(){{
+ HasPluginsResp.newBuilder().putAllResults(new HashMap() {{
put(DEFAULT_PRODUCT, true);
put(TEST_PRODUCT, false);
}})
@@ -214,13 +215,13 @@ public void testPluginQueryReq(){
mngResponseStreamObserver = Mockito.mock(StreamObserver.class);
pluginManagementService.hasDomains(hasDomainsRequest, mngResponseStreamObserver);
Mockito.verify(mngResponseStreamObserver).onNext(ResponseBuilder.buildHasDomainsResp(
- HasDomainsResp.newBuilder().putAllResults(new HashMap(){{
+ HasDomainsResp.newBuilder().putAllResults(new HashMap() {{
put(TEST_DOMAIN, false);
}})
));
Mockito.verify(mngResponseStreamObserver).onCompleted();
- // 6. all doamin
+ // 6. all domain
AllDomainsRequest allDomainsRequest = AllDomainsRequest.newBuilder().build();
mngResponseStreamObserver = Mockito.mock(StreamObserver.class);
pluginManagementService.allDomains(allDomainsRequest, mngResponseStreamObserver);
@@ -229,7 +230,7 @@ public void testPluginQueryReq(){
));
Mockito.verify(mngResponseStreamObserver).onCompleted();
- // 7. create service with test doamin
+ // 7. create service with test domain
AbstractBBCContext mockCtx = AntChainBridgePluginServerApplicationTests.mockInitCtx();
CallBBCRequest callBBCRequest = CallBBCRequest.newBuilder()
.setProduct(DEFAULT_PRODUCT)
@@ -240,7 +241,7 @@ public void testPluginQueryReq(){
Mockito.verify(responseStreamObserver).onNext(ResponseBuilder.buildBBCSuccessResp(CallBBCResponse.newBuilder()));
Mockito.verify(responseStreamObserver).onCompleted();
- // 8. all doamin
+ // 8. all domain
allDomainsRequest = AllDomainsRequest.newBuilder().build();
mngResponseStreamObserver = Mockito.mock(StreamObserver.class);
pluginManagementService.allDomains(allDomainsRequest, mngResponseStreamObserver);
@@ -250,4 +251,50 @@ public void testPluginQueryReq(){
Mockito.verify(mngResponseStreamObserver).onCompleted();
}
+ @Test
+ @DirtiesContext
+ public void testRestartBBC() {
+ // 1. failed to restart a domain or product not exist
+ RestartBBCRequest request = RestartBBCRequest.newBuilder()
+ .setDomain("domain")
+ .setProduct("product")
+ .build();
+ mngResponseStreamObserver = Mockito.mock(StreamObserver.class);
+ pluginManagementService.restartBBC(request, mngResponseStreamObserver);
+ Mockito.verify(mngResponseStreamObserver)
+ .onNext(ResponseBuilder.buildFailManageResp(ServerErrorCodeEnum.MANAGE_RESTART_BBC_ERROR, "product not found"));
+ Mockito.verify(mngResponseStreamObserver).onCompleted();
+
+ // 2. green case
+ PluginManageRequest pluginManageRequest = PluginManageRequest.newBuilder()
+ .setType(PluginManageRequest.Type.LOAD_PLUGIN)
+ .setPath(TEST_PLUGIN_PATH).build();
+ mngResponseStreamObserver = Mockito.mock(StreamObserver.class);
+ pluginManagementService.managePlugin(pluginManageRequest, mngResponseStreamObserver);
+ pluginManageRequest = PluginManageRequest.newBuilder()
+ .setType(PluginManageRequest.Type.START_PLUGIN)
+ .setPath(TEST_PLUGIN_PATH).build();
+ mngResponseStreamObserver = Mockito.mock(StreamObserver.class);
+ pluginManagementService.managePlugin(pluginManageRequest, mngResponseStreamObserver);
+
+ responseStreamObserver = Mockito.mock(StreamObserver.class);
+ crossChainService.bbcCall(
+ CallBBCRequest.newBuilder()
+ .setDomain("domain")
+ .setProduct("testchain")
+ .setStartUpReq(StartUpRequest.newBuilder().setRawContext(ByteString.copyFromUtf8("{\"raw_conf\": \"\"}")))
+ .build(),
+ responseStreamObserver
+ );
+
+ request = RestartBBCRequest.newBuilder()
+ .setDomain("domain")
+ .setProduct("testchain")
+ .build();
+ mngResponseStreamObserver = Mockito.mock(StreamObserver.class);
+ pluginManagementService.restartBBC(request, mngResponseStreamObserver);
+
+ Mockito.verify(mngResponseStreamObserver).onNext(ResponseBuilder.buildRestartBBCResp(RestartBBCResp.newBuilder()));
+ Mockito.verify(mngResponseStreamObserver).onCompleted();
+ }
}
diff --git a/ps-bootstrap/src/test/resources/application.yml b/ps-bootstrap/src/test/resources/application.yml
index 158fbd7..81393e9 100644
--- a/ps-bootstrap/src/test/resources/application.yml
+++ b/ps-bootstrap/src/test/resources/application.yml
@@ -32,4 +32,5 @@ pluginserver:
ban-with-prefix:
APPLICATION: "META-INF/services/io.grpc."
managerserver:
+ host: 0.0.0.0
port: 9091
\ No newline at end of file
diff --git a/ps-cli/.gitignore b/ps-cli/.gitignore
new file mode 100644
index 0000000..5ff6309
--- /dev/null
+++ b/ps-cli/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/ps-cli/README.md b/ps-cli/README.md
new file mode 100644
index 0000000..502c22a
--- /dev/null
+++ b/ps-cli/README.md
@@ -0,0 +1,319 @@
+
+
+
AntChain Bridge Plugin Server CLI
+
+
+## 介绍
+
+为了帮助管理插件服务,我们提供一个命令行工具(Command-Line Interface, CLI),帮助进行一些插件服务的运维和管理工作,比如更新插件的之后,可以通过CLI重新加载插件。
+
+## 快速开始
+
+### 编译
+
+在编译插件服务的时候,会同时编译CLI。在插件服务项目的根目录下,执行:
+
+```
+mvn clean package
+```
+
+编译之后,压缩包在路径`ps-cli/target/`之下。
+
+当然,也可以在[release](https://github.com/AntChainOpenLab/AntChainBridgePluginServer/releases)页面下载安装包。
+
+### 运行
+
+首先,解压安装包,这里以`plugin-server-cli-0.2.0.tar.gz`为例。
+
+```
+tar -zxf plugin-server-cli-0.2.0.tar.gz
+```
+
+然后,进入文件夹`plugin-server-cli`,文件结构如下:
+
+```
+.
+├── README.md
+├── bin
+│ └── start.sh
+└── lib
+ └── ps-cli-0.2.0.jar
+
+2 directories, 3 files
+```
+
+通过运行`bin/start.sh -h`可以打印帮助信息,查看脚本使用方式:
+
+```
+╰─± bin/start.sh -h
+
+ start.sh — Start the CLI tool to manage your plugin server
+
+ Usage:
+ start.sh
+
+ Examples:
+ 1. print help info:
+ start.sh -h
+ 2. identify the port number to start CLI
+ start.sh -p 9091
+ 3. identify the server IP to start CLI
+ start.sh -H 0.0.0.0
+
+ Options:
+ -h print help info
+ -p identify the port number to start CLI, default 9091
+ -H identify the server IP to start CLI, default 0.0.0.0
+
+```
+
+运行`bin/start.sh`以启动CLI,按下Tab键可以补全命令。
+
+### 使用
+
+#### 重新加载插件
+
+场景:让插件服务,重新加载某个类型的插件。如果你修改了插件逻辑并重新编译,将插件的Jar包拷贝并覆盖原有文件,运行命令,插件服务会重新加载新的插件文件。
+
+命令:`manage.reloadPlugin(String product)`
+
+参数:参数`product`为插件中设置的区块链类型,比如我们提供的插件[demo](https://github.com/AntChainOpenLab/AntChainBridgePluginSDK/blob/df28f973a6f0ebdf204b86c2e49aa1be8f8d0c0c/pluginset/ethereum/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum/EthereumBBCService.java#L57)中的`simple-ethereum`。
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps>
+ps> manage.reloadPlugin("simple-ethereum")
+```
+
+#### 在新路径重新加载插件
+
+场景:让插件服务,在一个新路径重新加载某个类型的插件。如果你修改了插件逻辑并重新编译,指定新的插件文件路径,运行命令,插件服务会重新加载新的插件文件。
+
+命令:`manage.reloadPluginInNewPath(String product, String path)`
+
+参数:参数`product`为插件中设置的区块链类型;参数`path`指定新的插件文件路径;
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps>
+ps> manage.reloadPluginInNewPath("simple-ethereum", "path/to/new/simple-ethereum-bbc-0.1-SNAPSHOT-plugin.jar")
+```
+
+#### 重启BBC实例
+
+场景:当中继请求插件服务为某条链启动BBC实例之后,插件服务会从插件中读取相关Class,创建BBC实例,而更新了插件代码,经过插件的reload之后,需要重启BBC实例,才可以使用更新之后的代码提供服务。
+
+命令:`manage.restartBBC(String product, String domain)`
+
+参数:参数`product`为插件中设置的区块链类型;参数`domain`是你想让插件服务重新创建的BBC实例;
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps>
+ps> manage.restartBBC("simple-ethereum", "domainA.eth.org")
+success
+```
+
+#### 加载新插件
+
+场景:让插件服务,加载一个新插件,插件的类型和ID必须没有被加载过。
+
+命令:`manage.loadPlugin(String path)`
+
+参数:参数`path`指定新的插件文件路径,路径是相对插件服务进程的,相对路径和绝对路径都会被判断为相等;
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps>
+ps> manage.loadPlugin("path/to/simple-ethereum-bbc-0.1-SNAPSHOT-plugin.jar")
+```
+
+#### 启动新插件
+
+场景:让插件服务,启动一个新加载的插件(重新加载的不需要),**只有启动之后的插件才可以用于创建BBC对象**。
+
+命令:`manage.startPlugin(String path)`
+
+参数:参数`path`指定该插件文件路径,路径是相对插件服务进程的,相对路径和绝对路径都会被判断为相等;
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps>
+ps> manage.startPlugin("path/to/simple-ethereum-bbc-0.1-SNAPSHOT-plugin.jar")
+```
+
+#### 停止插件
+
+场景:让插件服务,停止使用某个插件,只有启动之后的插件才可以停止。
+
+命令:`manage.stopPlugin(String product)`
+
+参数:参数`product`为插件中设置的区块链类型;
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps>
+ps> manage.stopPlugin("simple-ethereum")
+```
+
+#### 启动已停止的插件
+
+场景:让插件服务重新启动某个已经停止的插件,只有停止之后的插件才可以重新启动。
+
+命令:`manage.startPluginFromStop(String product)`
+
+参数:参数`product`为插件中设置的区块链类型;
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps>
+ps> manage.startPluginFromStop("simple-ethereum")
+```
+
+#### 查询当前所有已启动插件的类型列表
+
+场景:让插件服务返回当前所有已经启动的插件类型列表。
+
+命令:`manage.allPlugins()`
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps> manage.allPlugins()
+simple-ethereum, testchain
+```
+
+#### 查询当前所有服务中的区块链域名列表
+
+场景:让插件服务返回当前所有服务中的区块链域名列表
+
+命令:` manage.allDomains()`
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps> manage.allDomains()
+chaina, chainb
+```
+
+#### 查询某些区块链是否有启动的插件
+
+场景:查询插件服务某些指定的区块链类型是否有已经启动的插件。
+
+命令:` manage.hasPlugins(String[] products...)`
+
+参数:`products`是区块链插件类型的数组;
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps> manage.hasPlugins("simple-ethereum", "testchain", "notexist")
+{
+ "testchain":true,
+ "notexist":false, # false 代表不存在
+ "simple-ethereum":true # true 代表存在
+}
+```
+
+#### 查询某些域名是否正在服务
+
+场景:查询插件服务某些域名是否正在服务,即有正在运行的区块链BBC对象。
+
+命令:` manage.hasDomains(String[] domains...)`
+
+参数:`domains`是区块链插件类型的数组;
+
+```
+ ___ __ ______ __ _ ____ _ __
+ / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___
+ / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \
+ / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/
+/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/
+ /____/
+ PLUGIN SERVER CLI 0.2.0
+
+>>> type help to see all commands...
+ps> manage.hasDomains("chaina", "chainb")
+{
+ "chainb":true,
+ "chaina":false
+}
+```
+
diff --git a/ps-cli/desc_tar.xml b/ps-cli/desc_tar.xml
new file mode 100644
index 0000000..b6b2330
--- /dev/null
+++ b/ps-cli/desc_tar.xml
@@ -0,0 +1,32 @@
+
+ ${version}
+
+ tar.gz
+
+ true
+
+
+ ${project.basedir}/target/boot/
+ ${file.separator}lib
+
+ ${artifactId}-${version}.jar
+
+
+
+ ${project.basedir}/src/main/resources/scripts
+ ${file.separator}bin
+
+ *.sh
+
+
+
+ ${project.basedir}/
+ ${file.separator}
+
+ README.md
+
+
+
+
\ No newline at end of file
diff --git a/ps-cli/pom.xml b/ps-cli/pom.xml
new file mode 100644
index 0000000..34ae3b2
--- /dev/null
+++ b/ps-cli/pom.xml
@@ -0,0 +1,108 @@
+
+
+ 4.0.0
+
+ com.alipay.antchain.bridge
+ antchain-bridge-pluginserver
+ 0.2.0
+
+
+ ps-cli
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+ com.alipay.antchain.bridge
+ ps-service
+
+
+ org.jline
+ jline
+
+
+ org.codehaus.groovy
+ groovy-all
+ pom
+
+
+ commons-cli
+ commons-cli
+
+
+ cn.hutool
+ hutool-all
+
+
+ io.grpc
+ grpc-netty-shaded
+
+
+ com.alibaba
+ fastjson
+
+
+
+
+
+
+ src/main/resources
+
+ **/*.xml
+ **/VERSION
+
+
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+ ./target/boot
+
+
+
+
+ repackage
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.1.0
+
+
+
+ plugin-server-cli
+
+ desc_tar.xml
+
+
+ make-tar
+ package
+
+ single
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/ArgsConstraint.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/ArgsConstraint.java
new file mode 100644
index 0000000..1567b28
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/ArgsConstraint.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.command;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ArgsConstraint {
+
+ String name() default "";
+
+ String[] constraints() default "";
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/Command.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/Command.java
new file mode 100644
index 0000000..c2e7983
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/Command.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.command;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Command {
+
+ /**
+ * 命令名称
+ */
+ private String name;
+
+ /**
+ * 命令参数描述
+ */
+ private List args = new ArrayList<>();
+
+ /**
+ * 使用命令名称构造命令
+ *
+ * @param name
+ */
+ public Command(String name) {
+ this.name = name;
+ }
+
+ /**
+ * 添加参数
+ *
+ * @param argName 参数名称
+ * @param constraints 参数取值约束
+ */
+ public void addArgs(String argName, String type, List constraints) {
+
+ Arg item = new Arg();
+ item.name = argName;
+ item.type = type;
+ item.constraints = constraints;
+
+ this.args.add(item);
+ }
+
+ /**
+ * Getter method for property name..
+ *
+ * @return property value of name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Getter method for property args..
+ *
+ * @return property value of args.
+ */
+ public List getArgs() {
+ return args;
+ }
+
+ /**
+ * 参数描述类
+ */
+ public static class Arg {
+ private String name;
+ private String type;
+ private List constraints;
+
+ /**
+ * Getter method for property name..
+ *
+ * @return property value of name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Getter method for property constraints..
+ *
+ * @return property value of constraints.
+ */
+ public List getConstraints() {
+ return constraints;
+ }
+
+ /**
+ * Getter method for property type..
+ *
+ * @return property value of type.
+ */
+ public String getType() {
+ return type;
+ }
+ }
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandHandler.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandHandler.java
new file mode 100644
index 0000000..714d04e
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandHandler.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.command;
+
+@FunctionalInterface
+public interface CommandHandler {
+
+ /**
+ * 执行命令
+ *
+ * @param namespace 命令空间
+ * @param command 命令
+ * @param params 执行参数
+ * @return
+ */
+ Object execute(String namespace, String command, String... params);
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandNamespace.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandNamespace.java
new file mode 100644
index 0000000..343a0e1
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandNamespace.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.command;
+
+import java.util.Map;
+
+/**
+ * 命令命名空间
+ */
+public interface CommandNamespace {
+
+ /**
+ * 命名空间名称
+ *
+ * @return
+ */
+ public String name();
+
+ /**
+ * 获取命令命名空间下所有命令
+ *
+ * @return
+ */
+ public Map getCommands();
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandNamespaceImpl.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandNamespaceImpl.java
new file mode 100644
index 0000000..9061215
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/CommandNamespaceImpl.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.command;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CommandNamespaceImpl implements CommandNamespace {
+ /**
+ * 命令集合
+ */
+ private final Map commands = new HashMap<>();
+
+ /**
+ * 命名空间名称
+ *
+ * @return
+ */
+ @Override
+ public String name() {
+ return null;
+ }
+
+ /**
+ * 往命名空间添加一个命令
+ *
+ * @param cmd
+ */
+ public void addCommand(Command cmd) {
+ commands.put(cmd.getName(), cmd);
+ }
+
+ /**
+ * 获取命名空间下所有命令
+ *
+ * @return
+ */
+ public Map getCommands() {
+ return commands;
+ }
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/ManagementCommandNamespace.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/ManagementCommandNamespace.java
new file mode 100644
index 0000000..37a4396
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/ManagementCommandNamespace.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.command;
+
+import com.alipay.antchain.bridge.pluginserver.cli.shell.GroovyScriptCommandNamespace;
+
+public class ManagementCommandNamespace extends GroovyScriptCommandNamespace {
+ @Override
+ public String name() {
+ return "manage";
+ }
+
+ Object loadPlugins() {
+ return queryAPI("loadPlugins");
+ }
+
+ Object startPlugins() {
+ return queryAPI("startPlugins");
+ }
+
+ Object loadPlugin(
+ @ArgsConstraint(name = "path") String path
+ ) {
+ return queryAPI("loadPlugin", path);
+ }
+
+ Object startPlugin(
+ @ArgsConstraint(name = "path") String path
+ ) {
+ return queryAPI("startPlugin", path);
+ }
+
+ Object stopPlugin(
+ @ArgsConstraint(name = "product") String product
+ ) {
+ return queryAPI("stopPlugin", product);
+ }
+
+ Object startPluginFromStop(
+ @ArgsConstraint(name = "product") String product
+ ) {
+ return queryAPI("startPluginFromStop", product);
+ }
+
+ Object reloadPlugin(
+ @ArgsConstraint(name = "product") String product
+ ) {
+ return queryAPI("reloadPlugin", product);
+ }
+
+ Object reloadPluginInNewPath(
+ @ArgsConstraint(name = "product") String product,
+ @ArgsConstraint(name = "path") String path
+ ) {
+ return queryAPI("reloadPluginInNewPath", product, path);
+ }
+
+ Object hasPlugins(
+ @ArgsConstraint(name = "products...") String... products
+ ) {
+ return queryAPI("hasPlugins", products);
+ }
+
+ Object allPlugins() {
+ return queryAPI("allPlugins");
+ }
+
+ Object hasDomains(
+ @ArgsConstraint(name = "domains...") String... domains
+ ) {
+ return queryAPI("hasDomains", domains);
+ }
+
+ Object allDomains() {
+ return queryAPI("allDomains");
+ }
+
+ Object restartBBC(
+ @ArgsConstraint(name = "product") String product,
+ @ArgsConstraint(name = "domain") String domain
+ ) {
+ return queryAPI("restartBBC", product, domain);
+ }
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/NamespaceManager.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/NamespaceManager.java
new file mode 100644
index 0000000..750b2d0
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/NamespaceManager.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.command;
+
+import java.util.List;
+
+public interface NamespaceManager {
+
+ /**
+ * 添加namespace
+ *
+ * @param commandNamespace
+ */
+ void addNamespace(CommandNamespace commandNamespace);
+
+ /**
+ * 获取所有namespace
+ *
+ * @return
+ */
+ List getCommandNamespaces();
+
+ /**
+ * namespace快照
+ *
+ * @return
+ */
+ String dump();
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/NamespaceManagerImpl.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/NamespaceManagerImpl.java
new file mode 100644
index 0000000..083da20
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/command/NamespaceManagerImpl.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.command;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NamespaceManagerImpl implements NamespaceManager {
+
+ private final List commandNamespaces = new ArrayList<>();
+
+ public NamespaceManagerImpl() {
+ addNamespace(new ManagementCommandNamespace());
+ }
+
+ @Override
+ public void addNamespace(CommandNamespace commandNamespace) {
+ this.commandNamespaces.add(commandNamespace);
+ }
+
+ @Override
+ public List getCommandNamespaces() {
+ return commandNamespaces;
+ }
+
+ @Override
+ public String dump() {
+ StringBuilder builder = new StringBuilder();
+ commandNamespaces.forEach(
+ commandNamespace -> {
+ builder.append("\n").append(commandNamespace.name());
+ commandNamespace.getCommands().forEach(
+ (cmdName, cmd) -> {
+ builder.append("\n\t.").append(cmdName);
+ if (!cmd.getArgs().isEmpty()) {
+ builder.append("(");
+ cmd.getArgs().forEach(
+ arg -> {
+ builder.append(arg.getName()).append(",");
+ }
+ );
+ builder.deleteCharAt(builder.length() - 1);
+ builder.append(")");
+ } else {
+ builder.append("()");
+ }
+ }
+ );
+ }
+ );
+
+ return builder.append("\n\n").toString();
+ }
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/core/ManagementGrpcClient.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/core/ManagementGrpcClient.java
new file mode 100644
index 0000000..a6ac341
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/core/ManagementGrpcClient.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.core;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.fastjson.JSON;
+import com.alipay.antchain.bridge.pluginserver.managementservice.*;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+
+public class ManagementGrpcClient {
+
+ private final ManagedChannel channel;
+ private ManagementServiceGrpc.ManagementServiceBlockingStub blockingStub;
+
+ private final String host;
+
+ private final int port;
+
+ public ManagementGrpcClient(int port) {
+ this("127.0.0.1", port);
+ }
+
+ public ManagementGrpcClient(String host, int port) {
+ this.port = port;
+ this.host = host;
+ this.channel = ManagedChannelBuilder.forAddress(this.host, this.port)
+ .usePlaintext()
+ .build();
+ this.blockingStub = ManagementServiceGrpc.newBlockingStub(channel);
+ }
+
+ public boolean checkServerStatus() {
+ try {
+ Socket socket = new Socket(host, port);
+ socket.close();
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ public void shutdown() throws InterruptedException {
+ channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
+ }
+
+ public String managePlugin(PluginManageRequest.Type type, String product, String path) {
+ ManageResponse response = this.blockingStub.managePlugin(
+ PluginManageRequest.newBuilder()
+ .setType(type)
+ .setPath(path)
+ .setProduct(product)
+ .build()
+ );
+ return response.getCode() == 0 ? "success" : "failed with msg: " + response.getErrorMsg();
+ }
+
+ public String hasPlugins(List productList) {
+ ManageResponse response = this.blockingStub.hasPlugins(
+ HasPluginsRequest.newBuilder()
+ .addAllProducts(productList)
+ .build()
+ );
+ if (response.getCode() != 0) {
+ return "failed with msg: " + response.getErrorMsg();
+ }
+
+ return JSON.toJSONString(response.getHasPluginsResp().getResultsMap());
+ }
+
+ public String getAllPlugins() {
+ ManageResponse response = this.blockingStub.allPlugins(AllPluginsRequest.newBuilder().build());
+ if (response.getCode() != 0) {
+ return "failed with msg: " + response.getErrorMsg();
+ }
+
+ return CollUtil.join(response.getAllPluginsResp().getProductsList(), ", ");
+ }
+
+ public String hasDomains(List domains) {
+ ManageResponse response = this.blockingStub.hasDomains(
+ HasDomainsRequest.newBuilder()
+ .addAllDomains(domains)
+ .build()
+ );
+ if (response.getCode() != 0) {
+ return "failed with msg: " + response.getErrorMsg();
+ }
+
+ return JSON.toJSONString(response.getHasDomainsResp().getResultsMap());
+ }
+
+ public String getAllDomains() {
+ ManageResponse response = this.blockingStub.allDomains(AllDomainsRequest.newBuilder().build());
+ if (response.getCode() != 0) {
+ return "failed with msg: " + response.getErrorMsg();
+ }
+
+ return CollUtil.join(response.getAllDomainsResp().getDomainsList(), ", ");
+ }
+
+ public String restartBBC(String product, String domain) {
+ ManageResponse response = this.blockingStub.restartBBC(
+ RestartBBCRequest.newBuilder()
+ .setProduct(product)
+ .setDomain(domain)
+ .build()
+ );
+ if (response.getCode() != 0) {
+ return "failed with msg: " + response.getErrorMsg();
+ }
+ return "success";
+ }
+}
diff --git a/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/shell/GlLineReader.java b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/shell/GlLineReader.java
new file mode 100644
index 0000000..b718cd3
--- /dev/null
+++ b/ps-cli/src/main/java/com/alipay/antchain/bridge/pluginserver/cli/shell/GlLineReader.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2023 Ant Group
+ *
+ * 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 com.alipay.antchain.bridge.pluginserver.cli.shell;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.jline.reader.Binding;
+import org.jline.reader.Candidate;
+import org.jline.reader.ParsedLine;
+import org.jline.reader.Parser.ParseContext;
+import org.jline.reader.Reference;
+import org.jline.reader.impl.LineReaderImpl;
+import org.jline.reader.impl.ReaderUtils;
+import org.jline.terminal.Terminal;
+import org.jline.utils.AttributedString;
+import org.jline.utils.Levenshtein;
+import org.jline.utils.Log;
+
+public class GlLineReader extends LineReaderImpl {
+
+ public GlLineReader(Terminal terminal) throws IOException {
+ super(terminal);
+ }
+
+ public GlLineReader(Terminal terminal, String appName) throws IOException {
+ super(terminal, appName);
+ }
+
+ public GlLineReader(Terminal terminal, String appName, Map variables) {
+ super(terminal, appName, variables);
+ }
+
+ @Override
+ protected boolean insertTab() {
+ return isSet(Option.INSERT_TAB) && false;
+ }
+
+ @Override
+ protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix) {
+ // Try to expand history first
+ // If there is actually an expansion, bail out now
+ try {
+ if (expandHistory()) {
+ return true;
+ }
+ } catch (Exception e) {
+ Log.info("Error while expanding history", e);
+ return false;
+ }
+
+ // Parse the command line
+ ParsedLine line;
+ try {
+ line = parser.parse(buf.toString(), buf.cursor(), ParseContext.COMPLETE);
+ } catch (Exception e) {
+ Log.info("Error while parsing line", e);
+ return false;
+ }
+
+ // Find completion candidates
+ List candidates = new ArrayList<>();
+ try {
+ if (completer != null) {
+ completer.complete(this, line, candidates);
+ }
+ } catch (Exception e) {
+ Log.info("Error while finding completion candidates", e);
+ return false;
+ }
+
+ if (lst == CompletionType.ExpandComplete || lst == CompletionType.Expand) {
+ String w = expander.expandVar(line.word());
+ if (!line.word().equals(w)) {
+ if (prefix) {
+ buf.backspace(line.wordCursor());
+ } else {
+ buf.move(line.word().length() - line.wordCursor());
+ buf.backspace(line.word().length());
+ }
+ buf.write(w);
+ return true;
+ }
+ if (lst == CompletionType.Expand) {
+ return false;
+ } else {
+ lst = CompletionType.Complete;
+ }
+ }
+
+ boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE);
+ int errors = getInt(ERRORS, DEFAULT_ERRORS);
+
+ // Build a list of sorted candidates
+ NavigableMap> sortedCandidates =
+ new TreeMap<>(caseInsensitive ? String.CASE_INSENSITIVE_ORDER : null);
+ for (Candidate cand : candidates) {
+ sortedCandidates
+ .computeIfAbsent(AttributedString.fromAnsi(cand.value()).toString(), s -> new ArrayList<>())
+ .add(cand);
+ }
+
+ // Find matchers
+ // TODO: glob completion
+ List>,
+ Map>>> matchers;
+ Predicate exact;
+ if (prefix) {
+ String wp = line.word().substring(0, line.wordCursor());
+ matchers = Arrays.asList(
+ simpleMatcher(s -> s.startsWith(wp)),
+ simpleMatcher(s -> s.contains(wp)),
+ typoMatcher(wp, errors)
+ );
+ exact = s -> s.equals(wp);
+ } else if (isSet(Option.COMPLETE_IN_WORD)) {
+ String wd = line.word();
+ String wp = wd.substring(0, line.wordCursor());
+ String ws = wd.substring(line.wordCursor());
+ Pattern p1 = Pattern.compile(Pattern.quote(wp) + ".*" + Pattern.quote(ws) + ".*");
+ Pattern p2 = Pattern.compile(".*" + Pattern.quote(wp) + ".*" + Pattern.quote(ws) + ".*");
+ matchers = Arrays.asList(
+ simpleMatcher(s -> p1.matcher(s).matches()),
+ simpleMatcher(s -> p2.matcher(s).matches()),
+ typoMatcher(wd, errors)
+ );
+ exact = s -> s.equals(wd);
+ } else {
+ String wd = line.word();
+ matchers = Arrays.asList(
+ simpleMatcher(s -> s.startsWith(wd)),
+ simpleMatcher(s -> s.contains(wd)),
+ typoMatcher(wd, errors)
+ );
+ exact = s -> s.equals(wd);
+ }
+ // Find matching candidates
+ Map> matching = Collections.emptyMap();
+ for (Function