This is an automated email from the ASF dual-hosted git repository.

tanxinyu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 38c1d591acf Feat/updated cli (#13194)
38c1d591acf is described below

commit 38c1d591acfabdae4bbf542100090652416d2027
Author: Christofer Dutz <[email protected]>
AuthorDate: Tue Aug 27 05:11:31 2024 +0200

    Feat/updated cli (#13194)
---
 .idea/icon.png                                     | Bin 0 -> 6736 bytes
 .../iotdb/confignode/service/ConfigNode.java       |  71 ++++-
 .../confignode/service/ConfigNodeCommandLine.java  | 128 --------
 .../procedure/impl/CreateCQProcedureTest.java      |   2 +-
 .../assembly/resources/sbin/remove-datanode.bat    |   2 -
 .../src/assembly/resources/sbin/remove-datanode.sh |   2 -
 .../java/org/apache/iotdb/db/service/DataNode.java |  76 ++++-
 .../db/service/DataNodeServerCommandLine.java      | 224 --------------
 .../apache/iotdb/db/service/IoTDBShutdownHook.java |   8 +-
 .../org/apache/iotdb/db/service/DaemonTest.java    |   8 +-
 .../db/service/DataNodeServerCommandLineTest.java  | 218 -------------
 iotdb-core/node-commons/pom.xml                    |   4 +
 .../apache/iotdb/commons/ServerCommandLine.java    | 115 ++++---
 .../iotdb/commons/ServerCommandLineTest.java       | 337 +++++++++++++++++++++
 14 files changed, 568 insertions(+), 627 deletions(-)

diff --git a/.idea/icon.png b/.idea/icon.png
new file mode 100644
index 00000000000..493aca9320e
Binary files /dev/null and b/.idea/icon.png differ
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/ConfigNode.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/ConfigNode.java
index 3d537cdec61..a7f9d3d69a0 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/ConfigNode.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/ConfigNode.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.confignode.service;
 import org.apache.iotdb.common.rpc.thrift.TConfigNodeLocation;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.ServerCommandLine;
 import org.apache.iotdb.commons.client.ClientManagerMetrics;
 import org.apache.iotdb.commons.concurrent.ThreadModule;
 import org.apache.iotdb.commons.concurrent.ThreadName;
@@ -29,7 +30,10 @@ import org.apache.iotdb.commons.concurrent.ThreadPoolMetrics;
 import org.apache.iotdb.commons.conf.CommonConfig;
 import org.apache.iotdb.commons.conf.CommonDescriptor;
 import org.apache.iotdb.commons.conf.IoTDBConstant;
+import org.apache.iotdb.commons.exception.BadNodeUrlException;
+import org.apache.iotdb.commons.exception.ConfigurationException;
 import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.exception.IoTDBException;
 import org.apache.iotdb.commons.exception.StartupException;
 import org.apache.iotdb.commons.service.JMXService;
 import org.apache.iotdb.commons.service.RegisterManager;
@@ -43,6 +47,8 @@ import 
org.apache.iotdb.confignode.client.sync.SyncConfigNodeClientPool;
 import org.apache.iotdb.confignode.conf.ConfigNodeConfig;
 import org.apache.iotdb.confignode.conf.ConfigNodeConstant;
 import org.apache.iotdb.confignode.conf.ConfigNodeDescriptor;
+import org.apache.iotdb.confignode.conf.ConfigNodeRemoveCheck;
+import org.apache.iotdb.confignode.conf.ConfigNodeStartupCheck;
 import org.apache.iotdb.confignode.conf.SystemPropertiesUtils;
 import org.apache.iotdb.confignode.manager.ConfigManager;
 import org.apache.iotdb.confignode.manager.consensus.ConsensusManager;
@@ -74,7 +80,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
-public class ConfigNode implements ConfigNodeMBean {
+public class ConfigNode extends ServerCommandLine implements ConfigNodeMBean {
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(ConfigNode.class);
 
@@ -101,11 +107,13 @@ public class ConfigNode implements ConfigNodeMBean {
 
   protected ConfigManager configManager;
 
-  protected ConfigNode() {
+  public ConfigNode() {
+    super("ConfigNode");
     // We do not init anything here, so that we can re-initialize the instance 
in IT.
+    ConfigNodeHolder.instance = this;
   }
 
-  public static void main(String[] args) {
+  public static void main(String[] args) throws Exception {
     LOGGER.info(
         "{} environment variables: {}",
         ConfigNodeConstant.GLOBAL_NAME,
@@ -114,7 +122,60 @@ public class ConfigNode implements ConfigNodeMBean {
         "{} default charset is: {}",
         ConfigNodeConstant.GLOBAL_NAME,
         Charset.defaultCharset().displayName());
-    new ConfigNodeCommandLine().doMain(args);
+    ConfigNode configNode = new ConfigNode();
+    int returnCode = configNode.run(args);
+    if (returnCode != 0) {
+      System.exit(returnCode);
+    }
+  }
+
+  @Override
+  protected void start() throws IoTDBException {
+    try {
+      // Do ConfigNode startup checks
+      LOGGER.info("Starting IoTDB {}", IoTDBConstant.VERSION_WITH_BUILD);
+      ConfigNodeStartupCheck checks = new 
ConfigNodeStartupCheck(IoTDBConstant.CN_ROLE);
+      checks.startUpCheck();
+    } catch (StartupException | ConfigurationException | IOException e) {
+      LOGGER.error("Meet error when doing start checking", e);
+      throw new IoTDBException("Error starting", -1);
+    }
+    active();
+  }
+
+  @Override
+  protected void remove(Long nodeId) throws IoTDBException {
+    // If the nodeId was null, this is a shorthand for removing the current 
dataNode.
+    // In this case we need to find our nodeId.
+    if (nodeId == null) {
+      nodeId = (long) CONF.getConfigNodeId();
+    }
+
+    try {
+      LOGGER.info("Starting to remove ConfigNode with node-id {}", nodeId);
+
+      try {
+        TConfigNodeLocation removeConfigNodeLocation =
+            
ConfigNodeRemoveCheck.getInstance().removeCheck(Long.toString(nodeId));
+        if (removeConfigNodeLocation == null) {
+          LOGGER.error(
+              "The ConfigNode to be removed is not in the cluster, or the 
input format is incorrect.");
+          return;
+        }
+
+        
ConfigNodeRemoveCheck.getInstance().removeConfigNode(removeConfigNodeLocation);
+      } catch (BadNodeUrlException e) {
+        LOGGER.warn("No ConfigNodes need to be removed.", e);
+        return;
+      }
+
+      LOGGER.info(
+          "ConfigNode: {} is removed. If the confignode data directory is no 
longer needed, you can delete it manually.",
+          nodeId);
+    } catch (IOException e) {
+      LOGGER.error("Meet error when doing remove ConfigNode", e);
+      throw new IoTDBException("Error removing", -1);
+    }
   }
 
   public void active() {
@@ -465,7 +526,7 @@ public class ConfigNode implements ConfigNodeMBean {
 
   private static class ConfigNodeHolder {
 
-    private static ConfigNode instance = new ConfigNode();
+    private static ConfigNode instance;
 
     private ConfigNodeHolder() {
       // Empty constructor
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/ConfigNodeCommandLine.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/ConfigNodeCommandLine.java
deleted file mode 100644
index 21ccbab3421..00000000000
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/ConfigNodeCommandLine.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 org.apache.iotdb.confignode.service;
-
-import org.apache.iotdb.common.rpc.thrift.TConfigNodeLocation;
-import org.apache.iotdb.commons.ServerCommandLine;
-import org.apache.iotdb.commons.conf.IoTDBConstant;
-import org.apache.iotdb.commons.exception.BadNodeUrlException;
-import org.apache.iotdb.commons.exception.ConfigurationException;
-import org.apache.iotdb.commons.exception.StartupException;
-import org.apache.iotdb.confignode.conf.ConfigNodeRemoveCheck;
-import org.apache.iotdb.confignode.conf.ConfigNodeStartupCheck;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-import static 
org.apache.iotdb.confignode.conf.ConfigNodeConstant.REMOVE_CONFIGNODE_USAGE;
-
-public class ConfigNodeCommandLine extends ServerCommandLine {
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(ConfigNodeCommandLine.class);
-
-  // Start ConfigNode
-  private static final String MODE_START = "-s";
-  // Remove ConfigNode
-  private static final String MODE_REMOVE = "-r";
-
-  private static final String USAGE =
-      "Usage: <-s|-r> "
-          + "[-D{} <configure folder>] \n"
-          + "-s: Start the ConfigNode and join to the cluster\n"
-          + "-r: Remove the ConfigNode out of the cluster\n";
-
-  @Override
-  protected String getUsage() {
-    return USAGE;
-  }
-
-  @Override
-  protected int run(String[] args) {
-    String mode;
-    if (args.length < 1) {
-      mode = MODE_START;
-      LOGGER.warn(
-          "ConfigNode does not specify a startup mode. The default startup 
mode {} will be used",
-          MODE_START);
-    } else {
-      mode = args[0];
-    }
-
-    LOGGER.info("Running mode {}", mode);
-    if (MODE_START.equals(mode)) {
-      try {
-        // Do ConfigNode startup checks
-        LOGGER.info("Starting IoTDB {}", IoTDBConstant.VERSION_WITH_BUILD);
-        ConfigNodeStartupCheck checks = new 
ConfigNodeStartupCheck(IoTDBConstant.CN_ROLE);
-        checks.startUpCheck();
-      } catch (StartupException | ConfigurationException | IOException e) {
-        LOGGER.error("Meet error when doing start checking", e);
-        return -1;
-      }
-      activeConfigNodeInstance();
-    } else if (MODE_REMOVE.equals(mode)) {
-      // remove ConfigNode
-      try {
-        doRemoveConfigNode(args);
-      } catch (IOException e) {
-        LOGGER.error("Meet error when doing remove ConfigNode", e);
-        return -1;
-      }
-    } else {
-      LOGGER.error("Unsupported startup mode: {}", mode);
-      return -1;
-    }
-
-    return 0;
-  }
-
-  protected void activeConfigNodeInstance() {
-    ConfigNode.getInstance().active();
-  }
-
-  protected void doRemoveConfigNode(String[] args) throws IOException {
-
-    if (args.length != 2) {
-      LOGGER.info(REMOVE_CONFIGNODE_USAGE);
-      return;
-    }
-
-    LOGGER.info("Starting to remove ConfigNode, parameter: {}, {}", args[0], 
args[1]);
-
-    try {
-      TConfigNodeLocation removeConfigNodeLocation =
-          ConfigNodeRemoveCheck.getInstance().removeCheck(args[1]);
-      if (removeConfigNodeLocation == null) {
-        LOGGER.error(
-            "The ConfigNode to be removed is not in the cluster, or the input 
format is incorrect.");
-        return;
-      }
-
-      
ConfigNodeRemoveCheck.getInstance().removeConfigNode(removeConfigNodeLocation);
-    } catch (BadNodeUrlException e) {
-      LOGGER.warn("No ConfigNodes need to be removed.", e);
-      return;
-    }
-
-    LOGGER.info(
-        "ConfigNode: {} is removed. If the confignode data directory is no 
longer needed, you can delete it manually.",
-        args[1]);
-  }
-}
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/CreateCQProcedureTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/CreateCQProcedureTest.java
index 9915b7217f1..d0e92b32816 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/CreateCQProcedureTest.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/CreateCQProcedureTest.java
@@ -67,7 +67,7 @@ public class CreateCQProcedureTest {
     Mockito.when(cqManager.getExecutor()).thenReturn(executor);
     ConfigManager configManager = Mockito.mock(ConfigManager.class);
     Mockito.when(configManager.getCQManager()).thenReturn(cqManager);
-    ConfigNode configNode = ConfigNode.getInstance();
+    ConfigNode configNode = new ConfigNode();
     configNode.setConfigManager(configManager);
 
     try {
diff --git 
a/iotdb-core/datanode/src/assembly/resources/sbin/remove-datanode.bat 
b/iotdb-core/datanode/src/assembly/resources/sbin/remove-datanode.bat
index 1903e224d8e..f9dc167b956 100644
--- a/iotdb-core/datanode/src/assembly/resources/sbin/remove-datanode.bat
+++ b/iotdb-core/datanode/src/assembly/resources/sbin/remove-datanode.bat
@@ -25,8 +25,6 @@ IF "%~1"=="--help" (
     echo Usage:
     echo Remove the DataNode with datanode_id
     echo ./sbin/remove-datanode.bat [datanode_id]
-    echo Remove the DataNode with address:port
-    echo ./sbin/remove-datanode.bat [dn_rpc_address:dn_rpc_port]
     EXIT /B 0
 )
 
diff --git a/iotdb-core/datanode/src/assembly/resources/sbin/remove-datanode.sh 
b/iotdb-core/datanode/src/assembly/resources/sbin/remove-datanode.sh
index ba27784ca3b..8fb2ef5eebf 100755
--- a/iotdb-core/datanode/src/assembly/resources/sbin/remove-datanode.sh
+++ b/iotdb-core/datanode/src/assembly/resources/sbin/remove-datanode.sh
@@ -24,8 +24,6 @@ if [ "$#" -eq 1 ] && [ "$1" == "--help" ]; then
     echo "Usage:"
     echo "Remove the DataNode with datanode_id"
     echo "./sbin/remove-datanode.sh [datanode_id]"
-    echo "Remove the DataNode with address:port"
-    echo "./sbin/remove-datanode.sh [dn_rpc_address:dn_rpc_port]"
     exit 0
 fi
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNode.java 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNode.java
index 5e9be69b6d5..b715a7c6ca7 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNode.java
@@ -26,12 +26,14 @@ import 
org.apache.iotdb.common.rpc.thrift.TDataNodeConfiguration;
 import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 import org.apache.iotdb.common.rpc.thrift.TNodeResource;
+import org.apache.iotdb.commons.ServerCommandLine;
 import org.apache.iotdb.commons.client.exception.ClientManagerException;
 import org.apache.iotdb.commons.concurrent.IoTDBDefaultThreadExceptionHandler;
 import org.apache.iotdb.commons.conf.CommonDescriptor;
 import org.apache.iotdb.commons.conf.IoTDBConstant;
 import org.apache.iotdb.commons.consensus.ConsensusGroupId;
 import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.exception.IoTDBException;
 import org.apache.iotdb.commons.exception.StartupException;
 import org.apache.iotdb.commons.pipe.config.PipeConfig;
 import org.apache.iotdb.commons.pipe.plugin.meta.PipePluginMeta;
@@ -50,6 +52,8 @@ import org.apache.iotdb.commons.utils.FileUtils;
 import org.apache.iotdb.commons.utils.PathUtils;
 import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRegisterReq;
 import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRegisterResp;
+import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRemoveReq;
+import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRemoveResp;
 import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRestartReq;
 import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRestartResp;
 import org.apache.iotdb.confignode.rpc.thrift.TGetJarInListReq;
@@ -120,13 +124,14 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import static org.apache.iotdb.commons.conf.IoTDBConstant.DEFAULT_CLUSTER_NAME;
 import static org.apache.iotdb.db.conf.IoTDBStartCheck.PROPERTIES_FILE_NAME;
 
-public class DataNode implements DataNodeMBean {
+public class DataNode extends ServerCommandLine implements DataNodeMBean {
 
   private static final Logger logger = LoggerFactory.getLogger(DataNode.class);
   private static final IoTDBConfig config = 
IoTDBDescriptor.getInstance().getConfig();
@@ -162,8 +167,10 @@ public class DataNode implements DataNodeMBean {
   private boolean schemaRegionConsensusStarted = false;
   private boolean dataRegionConsensusStarted = false;
 
-  private DataNode() {
+  public DataNode() {
+    super("DataNode");
     // We do not init anything here, so that we can re-initialize the instance 
in IT.
+    DataNodeHolder.INSTANCE = this;
   }
 
   // TODO: This needs removal of statics ...
@@ -185,11 +192,16 @@ public class DataNode implements DataNodeMBean {
   public static void main(String[] args) {
     logger.info("IoTDB-DataNode environment variables: {}", 
IoTDBConfig.getEnvironmentVariables());
     logger.info("IoTDB-DataNode default charset is: {}", 
Charset.defaultCharset().displayName());
-    new DataNodeServerCommandLine().doMain(args);
+    DataNode dataNode = new DataNode();
+    int returnCode = dataNode.run(args);
+    if (returnCode != 0) {
+      System.exit(returnCode);
+    }
   }
 
-  protected void doAddNode() {
-    boolean isFirstStart = false;
+  @Override
+  protected void start() {
+    boolean isFirstStart;
     try {
       // Check if this DataNode is start for the first time and do other 
pre-checks
       isFirstStart = prepareDataNode();
@@ -242,6 +254,56 @@ public class DataNode implements DataNodeMBean {
     }
   }
 
+  @Override
+  protected void remove(Long nodeId) throws IoTDBException {
+    // If the nodeId was null, this is a shorthand for removing the current 
dataNode.
+    // In this case we need to find our nodeId.
+    if (nodeId == null) {
+      nodeId = (long) config.getDataNodeId();
+    }
+
+    logger.info("Starting to remove DataNode with node-id {} from cluster", 
nodeId);
+
+    // Load ConfigNodeList from system.properties file
+    ConfigNodeInfo.getInstance().loadConfigNodeList();
+
+    int removeNodeId = nodeId.intValue();
+    try (ConfigNodeClient configNodeClient =
+        
ConfigNodeClientManager.getInstance().borrowClient(ConfigNodeInfo.CONFIG_REGION_ID))
 {
+      // Find a datanode location with the given node id.
+      Optional<TDataNodeLocation> dataNodeLocationOpt =
+          configNodeClient
+              .getDataNodeConfiguration(-1)
+              .getDataNodeConfigurationMap()
+              .values()
+              .stream()
+              .map(TDataNodeConfiguration::getLocation)
+              .filter(location -> location.getDataNodeId() == removeNodeId)
+              .findFirst();
+      if (!dataNodeLocationOpt.isPresent()) {
+        throw new IoTDBException("Invalid node-id", -1);
+      }
+      TDataNodeLocation dataNodeLocation = dataNodeLocationOpt.get();
+
+      logger.info("Start to remove datanode, removed datanode endpoint: {}", 
dataNodeLocation);
+      TDataNodeRemoveReq removeReq =
+          new TDataNodeRemoveReq(Collections.singletonList(dataNodeLocation));
+      TDataNodeRemoveResp removeResp = 
configNodeClient.removeDataNode(removeReq);
+      logger.info("Remove result {} ", removeResp);
+      if (removeResp.getStatus().getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+        throw new IoTDBException(
+            removeResp.getStatus().toString(), 
removeResp.getStatus().getCode());
+      }
+      logger.info(
+          "Submit remove-datanode request successfully, but the process may 
fail. "
+              + "more details are shown in the logs of confignode-leader and 
removed-datanode, "
+              + "and after the process of removing datanode ends successfully, 
"
+              + "you are supposed to delete directory and data of the 
removed-datanode manually");
+    } catch (TException | ClientManagerException e) {
+      throw new IoTDBException("Failed removing datanode", e, -1);
+    }
+  }
+
   /** Prepare cluster IoTDB-DataNode */
   private boolean prepareDataNode() throws StartupException, IOException {
     long startTime = System.currentTimeMillis();
@@ -632,7 +694,7 @@ public class DataNode implements DataNodeMBean {
     // Get resources for trigger,udf,pipe...
     prepareResources();
 
-    Runtime.getRuntime().addShutdownHook(new IoTDBShutdownHook());
+    Runtime.getRuntime().addShutdownHook(new 
IoTDBShutdownHook(generateDataNodeLocation()));
     setUncaughtExceptionHandler();
 
     logger.info("Recover the schema...");
@@ -1093,7 +1155,7 @@ public class DataNode implements DataNodeMBean {
 
   private static class DataNodeHolder {
 
-    private static final DataNode INSTANCE = new DataNode();
+    private static DataNode INSTANCE;
 
     private DataNodeHolder() {
       // Empty constructor
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeServerCommandLine.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeServerCommandLine.java
deleted file mode 100644
index 92f55888d83..00000000000
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeServerCommandLine.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 org.apache.iotdb.db.service;
-
-import org.apache.iotdb.common.rpc.thrift.TDataNodeConfiguration;
-import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
-import org.apache.iotdb.commons.ServerCommandLine;
-import org.apache.iotdb.commons.client.IClientManager;
-import org.apache.iotdb.commons.client.exception.ClientManagerException;
-import org.apache.iotdb.commons.consensus.ConfigRegionId;
-import org.apache.iotdb.commons.exception.BadNodeUrlException;
-import org.apache.iotdb.commons.exception.IoTDBException;
-import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRemoveReq;
-import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRemoveResp;
-import org.apache.iotdb.db.protocol.client.ConfigNodeClient;
-import org.apache.iotdb.db.protocol.client.ConfigNodeClientManager;
-import org.apache.iotdb.db.protocol.client.ConfigNodeInfo;
-import org.apache.iotdb.rpc.TSStatusCode;
-
-import org.apache.commons.lang3.math.NumberUtils;
-import org.apache.thrift.TException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-public class DataNodeServerCommandLine extends ServerCommandLine {
-
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(DataNodeServerCommandLine.class);
-
-  // join an established cluster
-  public static final String MODE_START = "-s";
-  // send a request to remove a node, more arguments: ip-of-removed-node
-  // metaport-of-removed-node
-  public static final String MODE_REMOVE = "-r";
-
-  private final ConfigNodeInfo configNodeInfo;
-  private final IClientManager<ConfigRegionId, ConfigNodeClient> 
configNodeClientManager;
-  private final DataNode dataNode;
-
-  private static final String USAGE =
-      "Usage: <-s|-r> "
-          + "[-D{} <configure folder>] \n"
-          + "-s: start the node to the cluster\n"
-          + "-r: remove the node out of the cluster\n";
-
-  /** Default constructor using the singletons for initializing the 
relationship. */
-  public DataNodeServerCommandLine() {
-    configNodeInfo = ConfigNodeInfo.getInstance();
-    configNodeClientManager = ConfigNodeClientManager.getInstance();
-    dataNode = DataNode.getInstance();
-  }
-
-  /**
-   * Additional constructor allowing injection of custom instances (mainly for 
testing)
-   *
-   * @param configNodeInfo config node info
-   * @param configNodeClientManager config node client manager
-   * @param dataNode data node
-   */
-  public DataNodeServerCommandLine(
-      ConfigNodeInfo configNodeInfo,
-      IClientManager<ConfigRegionId, ConfigNodeClient> configNodeClientManager,
-      DataNode dataNode) {
-    this.configNodeInfo = configNodeInfo;
-    this.configNodeClientManager = configNodeClientManager;
-    this.dataNode = dataNode;
-  }
-
-  @Override
-  protected String getUsage() {
-    return USAGE;
-  }
-
-  @Override
-  protected int run(String[] args) throws Exception {
-    if (args.length < 1) {
-      usage(null);
-      return -1;
-    }
-
-    String mode = args[0];
-    LOGGER.info("Running mode {}", mode);
-
-    // Start IoTDB kernel first, then start the cluster module
-    if (MODE_START.equals(mode)) {
-      dataNode.doAddNode();
-    } else if (MODE_REMOVE.equals(mode)) {
-      return doRemoveDataNode(args);
-    } else {
-      LOGGER.error("Unrecognized mode {}", mode);
-    }
-    return 0;
-  }
-
-  /**
-   * remove data-nodes from cluster
-   *
-   * @param args id or ip:rpc_port for removed datanode
-   */
-  private int doRemoveDataNode(String[] args)
-      throws BadNodeUrlException, TException, IoTDBException, 
ClientManagerException {
-
-    if (args.length != 2) {
-      LOGGER.info("Usage: <node-id>/<ip>:<rpc-port>");
-      return -1;
-    }
-
-    // REMARK: Don't need null or empty-checks for args[0] or args[1], as if 
they were
-    // empty, the JVM would have not received them.
-
-    LOGGER.info("Starting to remove DataNode from cluster, parameter: {}, {}", 
args[0], args[1]);
-
-    // Load ConfigNodeList from system.properties file
-    configNodeInfo.loadConfigNodeList();
-
-    List<TDataNodeLocation> dataNodeLocations = 
buildDataNodeLocations(args[1]);
-    if (dataNodeLocations.isEmpty()) {
-      throw new BadNodeUrlException("No DataNode to remove");
-    }
-    LOGGER.info("Start to remove datanode, removed datanode endpoints: {}", 
dataNodeLocations);
-    TDataNodeRemoveReq removeReq = new TDataNodeRemoveReq(dataNodeLocations);
-    try (ConfigNodeClient configNodeClient =
-        configNodeClientManager.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID)) 
{
-      TDataNodeRemoveResp removeResp = 
configNodeClient.removeDataNode(removeReq);
-      LOGGER.info("Remove result {} ", removeResp);
-      if (removeResp.getStatus().getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
-        throw new IoTDBException(
-            removeResp.getStatus().toString(), 
removeResp.getStatus().getCode());
-      }
-      LOGGER.info(
-          "Submit remove-datanode request successfully, but the process may 
fail. "
-              + "more details are shown in the logs of confignode-leader and 
removed-datanode, "
-              + "and after the process of removing datanode ends successfully, 
"
-              + "you are supposed to delete directory and data of the 
removed-datanode manually");
-    }
-    return 0;
-  }
-
-  /**
-   * fetch all datanode info from ConfigNode, then compare with input 'args'
-   *
-   * @param args datanode id or ip:rpc_port
-   * @return TDataNodeLocation list
-   */
-  private List<TDataNodeLocation> buildDataNodeLocations(String args) {
-    List<TDataNodeLocation> dataNodeLocations = new ArrayList<>();
-
-    // Now support only single datanode deletion
-    if (args.split(",").length > 1) {
-      throw new IllegalArgumentException("Currently only removing single nodes 
is supported.");
-    }
-
-    // Below supports multiple datanode deletion, split by ',', and is 
reserved for extension
-    List<NodeCoordinate> nodeCoordinates = parseCoordinates(args);
-    try (ConfigNodeClient client =
-        configNodeClientManager.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID)) 
{
-      dataNodeLocations =
-          
client.getDataNodeConfiguration(-1).getDataNodeConfigurationMap().values().stream()
-              .map(TDataNodeConfiguration::getLocation)
-              .filter(
-                  location ->
-                      nodeCoordinates.stream()
-                          .anyMatch(nodeCoordinate -> 
nodeCoordinate.matches(location)))
-              .collect(Collectors.toList());
-    } catch (TException | ClientManagerException e) {
-      LOGGER.error("Get data node locations failed", e);
-    }
-
-    return dataNodeLocations;
-  }
-
-  protected List<NodeCoordinate> parseCoordinates(String coordinatesString) {
-    // Multiple nodeIds are separated by ","
-    String[] nodeIdStrings = coordinatesString.split(",");
-    List<NodeCoordinate> nodeIdCoordinates = new 
ArrayList<>(nodeIdStrings.length);
-    for (String nodeId : nodeIdStrings) {
-      // In the other case, we expect it to be a numeric value referring to 
the node-id
-      if (NumberUtils.isCreatable(nodeId)) {
-        nodeIdCoordinates.add(new 
NodeCoordinateNodeId(Integer.parseInt(nodeId)));
-      } else {
-        LOGGER.error("Invalid format. Expected a numeric node id, but got: 
{}", nodeId);
-      }
-    }
-    return nodeIdCoordinates;
-  }
-
-  protected interface NodeCoordinate {
-    // Returns true if the given location matches this coordinate
-    boolean matches(TDataNodeLocation location);
-  }
-
-  /** Implementation of a NodeCoordinate that uses the node id to match. */
-  protected static class NodeCoordinateNodeId implements NodeCoordinate {
-    private final int nodeId;
-
-    public NodeCoordinateNodeId(int nodeId) {
-      this.nodeId = nodeId;
-    }
-
-    @Override
-    public boolean matches(TDataNodeLocation location) {
-      return location.isSetDataNodeId() && location.dataNodeId == nodeId;
-    }
-  }
-}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/IoTDBShutdownHook.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/IoTDBShutdownHook.java
index 05c7b2c5637..8ef6bbdc430 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/IoTDBShutdownHook.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/IoTDBShutdownHook.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.service;
 
+import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
 import org.apache.iotdb.commons.client.exception.ClientManagerException;
 import org.apache.iotdb.commons.cluster.NodeStatus;
 import org.apache.iotdb.commons.concurrent.ThreadName;
@@ -49,8 +50,11 @@ public class IoTDBShutdownHook extends Thread {
 
   private static final Logger logger = 
LoggerFactory.getLogger(IoTDBShutdownHook.class);
 
-  public IoTDBShutdownHook() {
+  private final TDataNodeLocation nodeLocation;
+
+  public IoTDBShutdownHook(TDataNodeLocation nodeLocation) {
     super(ThreadName.IOTDB_SHUTDOWN_HOOK.getName());
+    this.nodeLocation = nodeLocation;
   }
 
   @Override
@@ -120,7 +124,7 @@ public class IoTDBShutdownHook extends Thread {
     try (ConfigNodeClient client =
         
ConfigNodeClientManager.getInstance().borrowClient(ConfigNodeInfo.CONFIG_REGION_ID))
 {
       isReportSuccess =
-          
client.reportDataNodeShutdown(DataNode.generateDataNodeLocation()).getCode()
+          client.reportDataNodeShutdown(nodeLocation).getCode()
               == TSStatusCode.SUCCESS_STATUS.getStatusCode();
 
       // Actually stop all services started by the DataNode.
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DaemonTest.java 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DaemonTest.java
index 8a1ba738a5c..301c801536a 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DaemonTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DaemonTest.java
@@ -24,16 +24,16 @@ public class DaemonTest {
 
   @Test
   public void testPid() {
-    DataNode ioTDB = DataNode.getInstance();
+    DataNode dataNode = new DataNode();
     // no pid set, so there is nothing happens
-    ioTDB.processPid();
+    dataNode.processPid();
   }
 
   @Test
   public void testSetPid() {
-    DataNode ioTDB = DataNode.getInstance();
+    DataNode dataNode = new DataNode();
     System.setProperty("iotdb-pidfile", "./iotdb.pid");
     // no pid set, so there is nothing happens
-    ioTDB.processPid();
+    dataNode.processPid();
   }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DataNodeServerCommandLineTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DataNodeServerCommandLineTest.java
deleted file mode 100644
index ab37e6bc64a..00000000000
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DataNodeServerCommandLineTest.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 org.apache.iotdb.db.service;
-
-import org.apache.iotdb.common.rpc.thrift.TDataNodeConfiguration;
-import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
-import org.apache.iotdb.common.rpc.thrift.TEndPoint;
-import org.apache.iotdb.common.rpc.thrift.TNodeResource;
-import org.apache.iotdb.common.rpc.thrift.TSStatus;
-import org.apache.iotdb.commons.client.IClientManager;
-import org.apache.iotdb.commons.consensus.ConfigRegionId;
-import org.apache.iotdb.commons.exception.BadNodeUrlException;
-import org.apache.iotdb.confignode.rpc.thrift.TDataNodeConfigurationResp;
-import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRemoveReq;
-import org.apache.iotdb.confignode.rpc.thrift.TDataNodeRemoveResp;
-import org.apache.iotdb.db.protocol.client.ConfigNodeClient;
-import org.apache.iotdb.db.protocol.client.ConfigNodeInfo;
-import org.apache.iotdb.rpc.TSStatusCode;
-
-import junit.framework.TestCase;
-import org.junit.Assert;
-import org.mockito.Mockito;
-
-import java.util.Arrays;
-import java.util.Collections;
-
-public class DataNodeServerCommandLineTest extends TestCase {
-
-  // List of well known locations for this test
-  protected static final TDataNodeLocation LOCATION_1 =
-      new TDataNodeLocation(1, new TEndPoint("1.2.3.4", 6667), null, null, 
null, null);
-  protected static final TDataNodeLocation LOCATION_2 =
-      new TDataNodeLocation(2, new TEndPoint("1.2.3.5", 6667), null, null, 
null, null);
-  protected static final TDataNodeLocation LOCATION_3 =
-      new TDataNodeLocation(3, new TEndPoint("1.2.3.6", 6667), null, null, 
null, null);
-  // An invalid location
-  protected static final TDataNodeLocation INVALID_LOCATION =
-      new TDataNodeLocation(23, new TEndPoint("4.3.2.1", 815), null, null, 
null, null);
-
-  /**
-   * In this test we pass an empty args list to the command. This is expected 
to fail.
-   *
-   * @throws Exception nothing should go wrong here.
-   */
-  public void testNoArgs() throws Exception {
-    // No need to initialize these mocks with anything sensible, as they 
should never be used.
-    ConfigNodeInfo configNodeInfo = null;
-    IClientManager<ConfigRegionId, ConfigNodeClient> configNodeClientManager = 
null;
-    DataNode dataNode = null;
-    DataNodeServerCommandLine sut =
-        new DataNodeServerCommandLine(configNodeInfo, configNodeClientManager, 
dataNode);
-
-    int returnCode = sut.run(new String[0]);
-
-    // We expect an error code of -1.
-    Assert.assertEquals(-1, returnCode);
-  }
-
-  /**
-   * In this test we pass too many arguments to the command. This should also 
fail with an error
-   * code.
-   *
-   * @throws Exception nothing should go wrong here.
-   */
-  public void testTooManyArgs() throws Exception {
-    // No need to initialize these mocks with anything sensible, as they 
should never be used.
-    ConfigNodeInfo configNodeInfo = null;
-    IClientManager<ConfigRegionId, ConfigNodeClient> configNodeClientManager = 
null;
-    DataNode dataNode = null;
-    DataNodeServerCommandLine sut =
-        new DataNodeServerCommandLine(configNodeInfo, configNodeClientManager, 
dataNode);
-
-    int returnCode = sut.run(new String[] {"-r", "2", "-s"});
-
-    // We expect an error code of -1.
-    Assert.assertEquals(-1, returnCode);
-  }
-
-  /**
-   * In this test case we provide the coordinates for the data-node that we 
want to delete by
-   * providing the node-id of that node.
-   *
-   * @throws Exception nothing should go wrong here.
-   */
-  public void testSingleDataNodeRemoveById() throws Exception {
-    ConfigNodeInfo configNodeInfo = Mockito.mock(ConfigNodeInfo.class);
-    IClientManager<ConfigRegionId, ConfigNodeClient> configNodeClientManager =
-        Mockito.mock(IClientManager.class);
-    ConfigNodeClient client = Mockito.mock(ConfigNodeClient.class);
-    
Mockito.when(configNodeClientManager.borrowClient(Mockito.any(ConfigRegionId.class)))
-        .thenReturn(client);
-    // This is the result of the getDataNodeConfiguration, which contains the 
list of known data
-    // nodes.
-    TDataNodeConfigurationResp tDataNodeConfigurationResp = new 
TDataNodeConfigurationResp();
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        1, new TDataNodeConfiguration(LOCATION_1, new TNodeResource()));
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        2, new TDataNodeConfiguration(LOCATION_2, new TNodeResource()));
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        3, new TDataNodeConfiguration(LOCATION_3, new TNodeResource()));
-    Mockito.when(client.getDataNodeConfiguration(Mockito.anyInt()))
-        .thenReturn(tDataNodeConfigurationResp);
-    // Only return something sensible, if exactly this location is asked to be 
deleted.
-    Mockito.when(
-            client.removeDataNode(new 
TDataNodeRemoveReq(Collections.singletonList(LOCATION_2))))
-        .thenReturn(
-            new TDataNodeRemoveResp(new 
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode())));
-    DataNode dataNode = Mockito.mock(DataNode.class);
-    DataNodeServerCommandLine sut =
-        new DataNodeServerCommandLine(configNodeInfo, configNodeClientManager, 
dataNode);
-
-    int returnCode = sut.run(new String[] {"-r", "2"});
-
-    // Check the overall return code was ok.
-    Assert.assertEquals(0, returnCode);
-    // Check that the config node client was actually called with a request to 
remove the
-    // node we want it to remove
-    Mockito.verify(client, Mockito.times(1))
-        .removeDataNode(new 
TDataNodeRemoveReq(Collections.singletonList(LOCATION_2)));
-  }
-
-  /**
-   * In this test case we provide the coordinates for the data-node that we 
want to delete by
-   * providing the node-id of that node. However, the coordinates are invalid 
and therefore the
-   * deletion fails with an error.
-   *
-   * @throws Exception nothing should go wrong here.
-   */
-  public void testSingleDataNodeRemoveByIdWithInvalidCoordinates() throws 
Exception {
-    ConfigNodeInfo configNodeInfo = Mockito.mock(ConfigNodeInfo.class);
-    IClientManager<ConfigRegionId, ConfigNodeClient> configNodeClientManager =
-        Mockito.mock(IClientManager.class);
-    ConfigNodeClient client = Mockito.mock(ConfigNodeClient.class);
-    
Mockito.when(configNodeClientManager.borrowClient(Mockito.any(ConfigRegionId.class)))
-        .thenReturn(client);
-    // This is the result of the getDataNodeConfiguration, which contains the 
list of known data
-    // nodes.
-    TDataNodeConfigurationResp tDataNodeConfigurationResp = new 
TDataNodeConfigurationResp();
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        1, new TDataNodeConfiguration(LOCATION_1, new TNodeResource()));
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        2, new TDataNodeConfiguration(LOCATION_2, new TNodeResource()));
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        3, new TDataNodeConfiguration(LOCATION_3, new TNodeResource()));
-    Mockito.when(client.getDataNodeConfiguration(Mockito.anyInt()))
-        .thenReturn(tDataNodeConfigurationResp);
-    DataNode dataNode = Mockito.mock(DataNode.class);
-    DataNodeServerCommandLine sut =
-        new DataNodeServerCommandLine(configNodeInfo, configNodeClientManager, 
dataNode);
-
-    try {
-      sut.run(new String[] {"-r", "23"});
-      Assert.fail("This call should have failed");
-    } catch (Exception e) {
-      // This is actually what we expected
-      Assert.assertTrue(e instanceof BadNodeUrlException);
-    }
-  }
-
-  /**
-   * In this test case we provide the coordinates for the data-node that we 
want to delete by
-   * providing the node-id of that node. NOTE: The test was prepared to test 
deletion of multiple
-   * nodes, however currently we don't support this.
-   *
-   * @throws Exception nothing should go wrong here.
-   */
-  public void testMultipleDataNodeRemoveById() throws Exception {
-    ConfigNodeInfo configNodeInfo = Mockito.mock(ConfigNodeInfo.class);
-    IClientManager<ConfigRegionId, ConfigNodeClient> configNodeClientManager =
-        Mockito.mock(IClientManager.class);
-    ConfigNodeClient client = Mockito.mock(ConfigNodeClient.class);
-    
Mockito.when(configNodeClientManager.borrowClient(Mockito.any(ConfigRegionId.class)))
-        .thenReturn(client);
-    // This is the result of the getDataNodeConfiguration, which contains the 
list of known data
-    // nodes.
-    TDataNodeConfigurationResp tDataNodeConfigurationResp = new 
TDataNodeConfigurationResp();
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        1, new TDataNodeConfiguration(LOCATION_1, new TNodeResource()));
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        2, new TDataNodeConfiguration(LOCATION_2, new TNodeResource()));
-    tDataNodeConfigurationResp.putToDataNodeConfigurationMap(
-        3, new TDataNodeConfiguration(LOCATION_3, new TNodeResource()));
-    Mockito.when(client.getDataNodeConfiguration(Mockito.anyInt()))
-        .thenReturn(tDataNodeConfigurationResp);
-    // Only return something sensible, if exactly the locations we want are 
asked to be deleted.
-    Mockito.when(
-            client.removeDataNode(new 
TDataNodeRemoveReq(Arrays.asList(LOCATION_1, LOCATION_2))))
-        .thenReturn(
-            new TDataNodeRemoveResp(new 
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode())));
-    DataNode dataNode = Mockito.mock(DataNode.class);
-    DataNodeServerCommandLine sut =
-        new DataNodeServerCommandLine(configNodeInfo, configNodeClientManager, 
dataNode);
-
-    try {
-      sut.run(new String[] {"-r", "1,2"});
-      Assert.fail("This call should have failed");
-    } catch (Exception e) {
-      // This is actually what we expected
-      Assert.assertTrue(e instanceof IllegalArgumentException);
-    }
-  }
-}
diff --git a/iotdb-core/node-commons/pom.xml b/iotdb-core/node-commons/pom.xml
index d34af0347f7..fb513e0e794 100644
--- a/iotdb-core/node-commons/pom.xml
+++ b/iotdb-core/node-commons/pom.xml
@@ -119,6 +119,10 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
         </dependency>
+        <dependency>
+            <groupId>commons-cli</groupId>
+            <artifactId>commons-cli</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/ServerCommandLine.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/ServerCommandLine.java
index 9bd4a4d3097..066e1893865 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/ServerCommandLine.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/ServerCommandLine.java
@@ -18,50 +18,97 @@
  */
 package org.apache.iotdb.commons;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.iotdb.commons.exception.IoTDBException;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionGroup;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+
+import java.io.PrintWriter;
 
 public abstract class ServerCommandLine {
-  private static final Logger LOG = 
LoggerFactory.getLogger(ServerCommandLine.class);
 
-  /**
-   * Implementing subclasses should return a usage string to print out
-   *
-   * @return usage
-   */
-  protected abstract String getUsage();
+  private static final Option OPTION_START =
+      Option.builder("s").longOpt("start").desc("start a new node").build();
+  private static final Option OPTION_REMOVE =
+      Option.builder("r")
+          .longOpt("remove")
+          .desc(
+              "remove a node (with the given nodeId or the node started on the 
current machine, if omitted)")
+          .hasArg()
+          .type(Number.class)
+          .argName("nodeId")
+          .optionalArg(true)
+          .build();
 
-  /**
-   * run command
-   *
-   * @param args system args
-   * @return return 0 if exec success
-   */
-  protected abstract int run(String[] args) throws Exception;
+  private final String cliName;
+  private final PrintWriter output;
+  private final Options options;
 
-  protected void usage(String message) {
-    if (message != null) {
-      System.err.println(message);
-      System.err.println();
-    }
+  public ServerCommandLine(String cliName) {
+    this(cliName, new PrintWriter(System.out));
+  }
 
-    System.err.println(getUsage());
+  public ServerCommandLine(String cliName, PrintWriter output) {
+    this.cliName = cliName;
+    this.output = output;
+    OptionGroup commands = new OptionGroup();
+    commands.addOption(OPTION_START);
+    commands.addOption(OPTION_REMOVE);
+    // Require one option of the group.
+    commands.setRequired(true);
+    options = new Options();
+    options.addOptionGroup(commands);
   }
 
-  /**
-   * Parse and run the given command line.
-   *
-   * @param args system args
-   */
-  public void doMain(String[] args) {
+  public int run(String[] args) {
+    CommandLineParser parser = new DefaultParser();
     try {
-      int result = run(args);
-      if (result != 0) {
-        System.exit(result);
+      CommandLine cmd = parser.parse(options, args);
+      // When starting there is no additional argument.
+      if (cmd.hasOption(OPTION_START)) {
+        start();
       }
-    } catch (Exception e) {
-      LOG.error("Failed to execute system command", e);
-      System.exit(-1);
+      // As we only support start and remove and one has to be selected,
+      // no need to check if OPTION_REMOVE is set.
+      else {
+        Number nodeId = (Number) cmd.getParsedOptionValue(OPTION_REMOVE);
+        if (nodeId != null) {
+          remove(nodeId.longValue());
+        } else {
+          remove(null);
+        }
+      }
+      // Make sure we exit with the 0 error code
+      return 0;
+    } catch (ParseException e) {
+      output.println(e.getMessage());
+      HelpFormatter formatter = new HelpFormatter();
+      formatter.printHelp(
+          output,
+          formatter.getWidth(),
+          cliName,
+          null,
+          options,
+          formatter.getLeftPadding(),
+          formatter.getDescPadding(),
+          null,
+          false);
+      // Forward a generic error code to the calling process
+      return 1;
+    } catch (IoTDBException e) {
+      output.println("An error occurred while running the command: " + 
e.getMessage());
+      // Forward the exit code from the exception to the calling process
+      return e.getErrorCode();
     }
   }
+
+  protected abstract void start() throws IoTDBException;
+
+  protected abstract void remove(Long nodeId) throws IoTDBException;
 }
diff --git 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/ServerCommandLineTest.java
 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/ServerCommandLineTest.java
new file mode 100644
index 00000000000..a4a42da26e4
--- /dev/null
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/ServerCommandLineTest.java
@@ -0,0 +1,337 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.iotdb.commons;
+
+import org.apache.iotdb.commons.exception.IoTDBException;
+
+import junit.framework.TestCase;
+import org.junit.Assert;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class ServerCommandLineTest extends TestCase {
+
+  /**
+   * In this test, the commandline is called without any args. In this case 
the usage should be
+   * output and nothing should be done.
+   */
+  public void testNoArgs() {
+    AtomicBoolean startCalled = new AtomicBoolean(false);
+    AtomicBoolean stopCalled = new AtomicBoolean(false);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() {
+            startCalled.set(true);
+          }
+
+          @Override
+          protected void remove(Long nodeId) {
+            stopCalled.set(true);
+          }
+        };
+    int returnCode = commandLine.run(new String[0]);
+
+    Assert.assertEquals(1, returnCode);
+    String consoleOutput = out.toString();
+    Assert.assertTrue(consoleOutput.contains("Missing required option"));
+    // No callbacks should have been called.
+    Assert.assertFalse(startCalled.get());
+    Assert.assertFalse(stopCalled.get());
+  }
+
+  /**
+   * In this test, the commandline is called with an invalid arg. In this case 
the usage should be
+   * output and nothing should be done.
+   */
+  public void testInvalidArgs() {
+    AtomicBoolean startCalled = new AtomicBoolean(false);
+    AtomicBoolean stopCalled = new AtomicBoolean(false);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() {
+            startCalled.set(true);
+          }
+
+          @Override
+          protected void remove(Long nodeId) {
+            stopCalled.set(true);
+          }
+        };
+    int returnCode = commandLine.run(new String[] {"-z"});
+
+    Assert.assertEquals(1, returnCode);
+    String consoleOutput = out.toString();
+    Assert.assertTrue(consoleOutput.contains("Unrecognized option"));
+    // No callbacks should have been called.
+    Assert.assertFalse(startCalled.get());
+    Assert.assertFalse(stopCalled.get());
+  }
+
+  /**
+   * In this test, the commandline is called with the start option. The start 
method should be
+   * called.
+   */
+  public void testStartArg() {
+    AtomicBoolean startCalled = new AtomicBoolean(false);
+    AtomicBoolean stopCalled = new AtomicBoolean(false);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() {
+            startCalled.set(true);
+          }
+
+          @Override
+          protected void remove(Long nodeId) {
+            stopCalled.set(true);
+          }
+        };
+    int returnCode = commandLine.run(new String[] {"-s"});
+
+    Assert.assertEquals(0, returnCode);
+    // Nothing should have been output on the console.
+    String consoleOutput = out.toString();
+    Assert.assertTrue(consoleOutput.isEmpty());
+    // Only the start method should have been called.
+    Assert.assertTrue(startCalled.get());
+    Assert.assertFalse(stopCalled.get());
+  }
+
+  /**
+   * In this test, the commandline is called with the remove option, but 
without an additional
+   * attribute for providing the node id. The stop method should be called and 
"null" should be
+   * provided as node id.
+   */
+  public void testRemoveArg() {
+    AtomicBoolean startCalled = new AtomicBoolean(false);
+    AtomicBoolean stopCalled = new AtomicBoolean(false);
+    AtomicLong stopNodeId = new AtomicLong(-1);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() {
+            startCalled.set(true);
+          }
+
+          @Override
+          protected void remove(Long nodeId) {
+            stopCalled.set(true);
+            if (nodeId != null) {
+              stopNodeId.set(nodeId);
+            }
+          }
+        };
+    int returnCode = commandLine.run(new String[] {"-r"});
+
+    Assert.assertEquals(0, returnCode);
+    // Nothing should have been output on the console.
+    String consoleOutput = out.toString();
+    Assert.assertTrue(consoleOutput.isEmpty());
+    // Only the start method should have been called.
+    Assert.assertFalse(startCalled.get());
+    Assert.assertTrue(stopCalled.get());
+    Assert.assertEquals(-1, stopNodeId.get());
+  }
+
+  /**
+   * In this test, the commandline is called with the remove option, with an 
additional attribute
+   * for providing the node id. The stop method should be called and the id 
should be passed to the
+   * remove callback.
+   */
+  public void testRemoveWithNodeIdArg() {
+    AtomicBoolean startCalled = new AtomicBoolean(false);
+    AtomicBoolean stopCalled = new AtomicBoolean(false);
+    AtomicLong stopNodeId = new AtomicLong(-1);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() {
+            startCalled.set(true);
+          }
+
+          @Override
+          protected void remove(Long nodeId) {
+            stopCalled.set(true);
+            if (nodeId != null) {
+              stopNodeId.set(nodeId);
+            }
+          }
+        };
+    int returnCode = commandLine.run(new String[] {"-r", "42"});
+
+    Assert.assertEquals(0, returnCode);
+    // Nothing should have been output on the console.
+    String consoleOutput = out.toString();
+    Assert.assertTrue(consoleOutput.isEmpty());
+    // Only the start method should have been called.
+    Assert.assertFalse(startCalled.get());
+    Assert.assertTrue(stopCalled.get());
+    Assert.assertEquals(42L, stopNodeId.get());
+  }
+
+  /**
+   * In this test, the commandline is called with the remove option, with an 
additional attribute
+   * for providing the node id. However, the attribute is not an integer 
value, therefore an error
+   * should be thrown.
+   */
+  public void testRemoveWithInvalidNodeIdArg() {
+    AtomicBoolean startCalled = new AtomicBoolean(false);
+    AtomicBoolean stopCalled = new AtomicBoolean(false);
+    AtomicLong stopNodeId = new AtomicLong(-1);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() {
+            startCalled.set(true);
+          }
+
+          @Override
+          protected void remove(Long nodeId) {
+            stopCalled.set(true);
+            if (nodeId != null) {
+              stopNodeId.set(nodeId);
+            }
+          }
+        };
+    int returnCode = commandLine.run(new String[] {"-r", "text"});
+
+    Assert.assertEquals(1, returnCode);
+    // Nothing should have been output on the console.
+    String consoleOutput = out.toString();
+    Assert.assertTrue(consoleOutput.contains("For input string"));
+    // No callbacks should have been called.
+    Assert.assertFalse(startCalled.get());
+    Assert.assertFalse(stopCalled.get());
+  }
+
+  /**
+   * In this test, the commandline is called with the both the start and stop 
option. This should
+   * result in an error report and no callback should be called.
+   */
+  public void testCallWithMultipleActions() {
+    AtomicBoolean startCalled = new AtomicBoolean(false);
+    AtomicBoolean stopCalled = new AtomicBoolean(false);
+    AtomicLong stopNodeId = new AtomicLong(-1);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() {
+            startCalled.set(true);
+          }
+
+          @Override
+          protected void remove(Long nodeId) {
+            stopCalled.set(true);
+            if (nodeId != null) {
+              stopNodeId.set(nodeId);
+            }
+          }
+        };
+    int returnCode = commandLine.run(new String[] {"-r", "42", "-s"});
+
+    Assert.assertEquals(1, returnCode);
+    // Nothing should have been output on the console.
+    String consoleOutput = out.toString();
+    Assert.assertTrue(
+        consoleOutput.contains("but an option from this group has already been 
selected"));
+    // No callbacks should have been called.
+    Assert.assertFalse(startCalled.get());
+    Assert.assertFalse(stopCalled.get());
+  }
+
+  /**
+   * In this test, the commandline is called with the start option. The start 
method should be
+   * called, however there an exception is thrown, which should be caught.
+   */
+  public void testStartWithErrorArg() {
+    AtomicBoolean stopCalled = new AtomicBoolean(false);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() throws IoTDBException {
+            throw new IoTDBException("Error", 23);
+          }
+
+          @Override
+          protected void remove(Long nodeId) {
+            stopCalled.set(true);
+          }
+        };
+    int returnCode = commandLine.run(new String[] {"-s"});
+
+    Assert.assertEquals(23, returnCode);
+    // Nothing should have been output on the console.
+    String consoleOutput = out.toString();
+    Assert.assertTrue(consoleOutput.contains("An error occurred while running 
the command"));
+    // Only the start method should have been called.
+    Assert.assertFalse(stopCalled.get());
+  }
+
+  /**
+   * In this test, the commandline is called with the remove option, with an 
additional attribute
+   * for providing the node id. The stop method should be called and the id 
should be passed to the
+   * remove callback, however there an exception is thrown, which should be 
caught.
+   */
+  public void testRemoveWithNodeIdWithErrorArg() {
+    AtomicBoolean startCalled = new AtomicBoolean(false);
+    StringWriter out = new StringWriter();
+    PrintWriter writer = new PrintWriter(out);
+    ServerCommandLine commandLine =
+        new ServerCommandLine("test-cli", writer) {
+          @Override
+          protected void start() {
+            startCalled.set(true);
+          }
+
+          @Override
+          protected void remove(Long nodeId) throws IoTDBException {
+            throw new IoTDBException("Error", 23);
+          }
+        };
+    int returnCode = commandLine.run(new String[] {"-r", "42"});
+
+    Assert.assertEquals(23, returnCode);
+    // Nothing should have been output on the console.
+    String consoleOutput = out.toString();
+    Assert.assertTrue(consoleOutput.contains("An error occurred while running 
the command"));
+    // Only the start method should have been called.
+    Assert.assertFalse(startCalled.get());
+  }
+}

Reply via email to