/*
 * 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.geode.management.internal.cli.commands;

import java.io.File;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import javax.management.MalformedObjectNameException;
import javax.net.ssl.SSLException;

import org.apache.commons.lang.ArrayUtils;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;

import org.apache.geode.distributed.AbstractLauncher;
import org.apache.geode.distributed.ConfigurationProperties;
import org.apache.geode.distributed.LocatorLauncher;
import org.apache.geode.distributed.ServerLauncher;
import org.apache.geode.internal.OSProcess;
import org.apache.geode.internal.lang.StringUtils;
import org.apache.geode.internal.lang.SystemUtils;
import org.apache.geode.internal.process.ProcessStreamReader;
import org.apache.geode.internal.process.ProcessType;
import org.apache.geode.internal.util.IOUtils;
import org.apache.geode.management.cli.CliMetaData;
import org.apache.geode.management.cli.ConverterHint;
import org.apache.geode.management.cli.Result;
import org.apache.geode.management.internal.cli.GfshParser;
import org.apache.geode.management.internal.cli.domain.ConnectToLocatorResult;
import org.apache.geode.management.internal.cli.i18n.CliStrings;
import org.apache.geode.management.internal.cli.result.InfoResultData;
import org.apache.geode.management.internal.cli.result.ResultBuilder;
import org.apache.geode.management.internal.cli.shell.Gfsh;
import org.apache.geode.management.internal.cli.shell.JmxOperationInvoker;
import org.apache.geode.management.internal.cli.util.CauseFinder;
import org.apache.geode.management.internal.cli.util.CommandStringBuilder;
import org.apache.geode.management.internal.cli.util.ConnectionEndpoint;
import org.apache.geode.management.internal.cli.util.HostUtils;
import org.apache.geode.management.internal.configuration.utils.ClusterConfigurationStatusRetriever;
import org.apache.geode.security.AuthenticationFailedException;

public class StartLocatorCommand extends InternalGfshCommand {
  @CliCommand(value = CliStrings.START_LOCATOR, help = CliStrings.START_LOCATOR__HELP)
  @CliMetaData(shellOnly = true,
      relatedTopic = {CliStrings.TOPIC_GEODE_LOCATOR, CliStrings.TOPIC_GEODE_LIFECYCLE})
  public Result startLocator(
      @CliOption(key = CliStrings.START_LOCATOR__MEMBER_NAME,
          help = CliStrings.START_LOCATOR__MEMBER_NAME__HELP) String memberName,
      @CliOption(key = CliStrings.START_LOCATOR__BIND_ADDRESS,
          help = CliStrings.START_LOCATOR__BIND_ADDRESS__HELP) final String bindAddress,
      @CliOption(key = CliStrings.START_LOCATOR__CLASSPATH,
          help = CliStrings.START_LOCATOR__CLASSPATH__HELP) final String classpath,
      @CliOption(key = CliStrings.START_LOCATOR__FORCE, unspecifiedDefaultValue = "false",
          specifiedDefaultValue = "true",
          help = CliStrings.START_LOCATOR__FORCE__HELP) final Boolean force,
      @CliOption(key = {CliStrings.GROUP, CliStrings.GROUPS},
          optionContext = ConverterHint.MEMBERGROUP,
          help = CliStrings.START_LOCATOR__GROUP__HELP) final String group,
      @CliOption(key = CliStrings.START_LOCATOR__HOSTNAME_FOR_CLIENTS,
          help = CliStrings.START_LOCATOR__HOSTNAME_FOR_CLIENTS__HELP) final String hostnameForClients,
      @CliOption(key = ConfigurationProperties.JMX_MANAGER_HOSTNAME_FOR_CLIENTS,
          help = CliStrings.START_LOCATOR__JMX_MANAGER_HOSTNAME_FOR_CLIENTS__HELP) final String jmxManagerHostnameForClients,
      @CliOption(key = CliStrings.START_LOCATOR__INCLUDE_SYSTEM_CLASSPATH,
          specifiedDefaultValue = "true", unspecifiedDefaultValue = "false",
          help = CliStrings.START_LOCATOR__INCLUDE_SYSTEM_CLASSPATH__HELP) final Boolean includeSystemClasspath,
      @CliOption(key = CliStrings.START_LOCATOR__LOCATORS,
          optionContext = ConverterHint.LOCATOR_DISCOVERY_CONFIG,
          help = CliStrings.START_LOCATOR__LOCATORS__HELP) final String locators,
      @CliOption(key = CliStrings.START_LOCATOR__LOG_LEVEL, optionContext = ConverterHint.LOG_LEVEL,
          help = CliStrings.START_LOCATOR__LOG_LEVEL__HELP) final String logLevel,
      @CliOption(key = CliStrings.START_LOCATOR__MCAST_ADDRESS,
          help = CliStrings.START_LOCATOR__MCAST_ADDRESS__HELP) final String mcastBindAddress,
      @CliOption(key = CliStrings.START_LOCATOR__MCAST_PORT,
          help = CliStrings.START_LOCATOR__MCAST_PORT__HELP) final Integer mcastPort,
      @CliOption(key = CliStrings.START_LOCATOR__PORT,
          help = CliStrings.START_LOCATOR__PORT__HELP) final Integer port,
      @CliOption(key = CliStrings.START_LOCATOR__DIR,
          help = CliStrings.START_LOCATOR__DIR__HELP) String workingDirectory,
      @CliOption(key = CliStrings.START_LOCATOR__PROPERTIES, optionContext = ConverterHint.FILE,
          help = CliStrings.START_LOCATOR__PROPERTIES__HELP) File gemfirePropertiesFile,
      @CliOption(key = CliStrings.START_LOCATOR__SECURITY_PROPERTIES,
          optionContext = ConverterHint.FILE,
          help = CliStrings.START_LOCATOR__SECURITY_PROPERTIES__HELP) File gemfireSecurityPropertiesFile,
      @CliOption(key = CliStrings.START_LOCATOR__INITIALHEAP,
          help = CliStrings.START_LOCATOR__INITIALHEAP__HELP) final String initialHeap,
      @CliOption(key = CliStrings.START_LOCATOR__MAXHEAP,
          help = CliStrings.START_LOCATOR__MAXHEAP__HELP) final String maxHeap,
      @CliOption(key = CliStrings.START_LOCATOR__J, optionContext = GfshParser.J_OPTION_CONTEXT,
          help = CliStrings.START_LOCATOR__J__HELP) final String[] jvmArgsOpts,
      @CliOption(key = CliStrings.START_LOCATOR__CONNECT, unspecifiedDefaultValue = "true",
          specifiedDefaultValue = "true",
          help = CliStrings.START_LOCATOR__CONNECT__HELP) final boolean connect,
      @CliOption(key = CliStrings.START_LOCATOR__ENABLE__SHARED__CONFIGURATION,
          unspecifiedDefaultValue = "true", specifiedDefaultValue = "true",
          help = CliStrings.START_LOCATOR__ENABLE__SHARED__CONFIGURATION__HELP) final boolean enableSharedConfiguration,
      @CliOption(key = CliStrings.START_LOCATOR__LOAD__SHARED_CONFIGURATION__FROM__FILESYSTEM,
          unspecifiedDefaultValue = "false",
          help = CliStrings.START_LOCATOR__LOAD__SHARED_CONFIGURATION__FROM__FILESYSTEM__HELP) final boolean loadSharedConfigurationFromDirectory,
      @CliOption(key = CliStrings.START_LOCATOR__CLUSTER__CONFIG__DIR, unspecifiedDefaultValue = "",
          help = CliStrings.START_LOCATOR__CLUSTER__CONFIG__DIR__HELP) final String clusterConfigDir,
      @CliOption(key = CliStrings.START_LOCATOR__HTTP_SERVICE_PORT,
          help = CliStrings.START_LOCATOR__HTTP_SERVICE_PORT__HELP) final Integer httpServicePort,
      @CliOption(key = CliStrings.START_LOCATOR__HTTP_SERVICE_BIND_ADDRESS,
          help = CliStrings.START_LOCATOR__HTTP_SERVICE_BIND_ADDRESS__HELP) final String httpServiceBindAddress,
      @CliOption(key = CliStrings.START_LOCATOR__REDIRECT_OUTPUT, unspecifiedDefaultValue = "false",
          specifiedDefaultValue = "true",
          help = CliStrings.START_LOCATOR__REDIRECT_OUTPUT__HELP) final Boolean redirectOutput)
      throws Exception {
    if (StringUtils.isBlank(memberName)) {
      // when the user doesn't give us a name, we make one up!
      memberName = StartMemberUtils.getNameGenerator().generate('-');
    }

    workingDirectory = StartMemberUtils.resolveWorkingDir(workingDirectory, memberName);

    if (gemfirePropertiesFile != null && !gemfirePropertiesFile.exists()) {
      return ResultBuilder.createUserErrorResult(
          CliStrings.format(CliStrings.GEODE_0_PROPERTIES_1_NOT_FOUND_MESSAGE, StringUtils.EMPTY,
              gemfirePropertiesFile.getAbsolutePath()));
    }

    if (gemfireSecurityPropertiesFile != null && !gemfireSecurityPropertiesFile.exists()) {
      return ResultBuilder.createUserErrorResult(
          CliStrings.format(CliStrings.GEODE_0_PROPERTIES_1_NOT_FOUND_MESSAGE, "Security ",
              gemfireSecurityPropertiesFile.getAbsolutePath()));
    }

    File locatorPidFile = new File(workingDirectory, ProcessType.LOCATOR.getPidFileName());

    final int oldPid = StartMemberUtils.readPid(locatorPidFile);

    Properties gemfireProperties = new Properties();

    StartMemberUtils.setPropertyIfNotNull(gemfireProperties, ConfigurationProperties.GROUPS, group);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties, ConfigurationProperties.LOCATORS,
        locators);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties, ConfigurationProperties.LOG_LEVEL,
        logLevel);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties, ConfigurationProperties.MCAST_ADDRESS,
        mcastBindAddress);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties, ConfigurationProperties.MCAST_PORT,
        mcastPort);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties,
        ConfigurationProperties.ENABLE_CLUSTER_CONFIGURATION, enableSharedConfiguration);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties,
        ConfigurationProperties.LOAD_CLUSTER_CONFIGURATION_FROM_DIR,
        loadSharedConfigurationFromDirectory);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties,
        ConfigurationProperties.CLUSTER_CONFIGURATION_DIR, clusterConfigDir);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties,
        ConfigurationProperties.HTTP_SERVICE_PORT, httpServicePort);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties,
        ConfigurationProperties.HTTP_SERVICE_BIND_ADDRESS, httpServiceBindAddress);
    StartMemberUtils.setPropertyIfNotNull(gemfireProperties,
        ConfigurationProperties.JMX_MANAGER_HOSTNAME_FOR_CLIENTS, jmxManagerHostnameForClients);

    LocatorLauncher.Builder locatorLauncherBuilder =
        new LocatorLauncher.Builder().setBindAddress(bindAddress).setForce(force).setPort(port)
            .setRedirectOutput(redirectOutput).setWorkingDirectory(workingDirectory);
    if (hostnameForClients != null) {
      locatorLauncherBuilder.setHostnameForClients(hostnameForClients);
    }
    if (memberName != null) {
      locatorLauncherBuilder.setMemberName(memberName);
    }
    LocatorLauncher locatorLauncher = locatorLauncherBuilder.build();

    String[] locatorCommandLine = createStartLocatorCommandLine(locatorLauncher,
        gemfirePropertiesFile, gemfireSecurityPropertiesFile, gemfireProperties, classpath,
        includeSystemClasspath, jvmArgsOpts, initialHeap, maxHeap);

    final Process locatorProcess = new ProcessBuilder(locatorCommandLine)
        .directory(new File(locatorLauncher.getWorkingDirectory())).start();

    locatorProcess.getInputStream().close();
    locatorProcess.getOutputStream().close();

    // fix TRAC bug #51967 by using NON_BLOCKING on Windows
    final ProcessStreamReader.ReadingMode readingMode = SystemUtils.isWindows()
        ? ProcessStreamReader.ReadingMode.NON_BLOCKING : ProcessStreamReader.ReadingMode.BLOCKING;

    final StringBuffer message = new StringBuffer(); // need thread-safe StringBuffer
    ProcessStreamReader.InputListener inputListener = line -> {
      message.append(line);
      if (readingMode == ProcessStreamReader.ReadingMode.BLOCKING) {
        message.append(StringUtils.LINE_SEPARATOR);
      }
    };

    ProcessStreamReader stderrReader = new ProcessStreamReader.Builder(locatorProcess)
        .inputStream(locatorProcess.getErrorStream()).inputListener(inputListener)
        .readingMode(readingMode).continueReadingMillis(2 * 1000).build().start();

    LocatorLauncher.LocatorState locatorState;

    String previousLocatorStatusMessage = null;

    LauncherSignalListener locatorSignalListener = new LauncherSignalListener();

    final boolean registeredLocatorSignalListener =
        getGfsh().getSignalHandler().registerListener(locatorSignalListener);

    try {
      getGfsh().logInfo(String.format(CliStrings.START_LOCATOR__RUN_MESSAGE, IOUtils
          .tryGetCanonicalPathElseGetAbsolutePath(new File(locatorLauncher.getWorkingDirectory()))),
          null);

      do {
        if (locatorProcess.isAlive()) {
          Gfsh.print(".");

          synchronized (this) {
            TimeUnit.MILLISECONDS.timedWait(this, 500);
          }

          locatorState = LocatorLauncher.LocatorState.fromDirectory(workingDirectory, memberName);

          String currentLocatorStatusMessage = locatorState.getStatusMessage();

          if (locatorState.isStartingOrNotResponding()
              && !(StringUtils.isBlank(currentLocatorStatusMessage)
                  || currentLocatorStatusMessage.equalsIgnoreCase(previousLocatorStatusMessage)
                  || currentLocatorStatusMessage.trim().toLowerCase().equals("null"))) {
            Gfsh.println();
            Gfsh.println(currentLocatorStatusMessage);
            previousLocatorStatusMessage = currentLocatorStatusMessage;
          }
        } else {
          final int exitValue = locatorProcess.exitValue();

          return ResultBuilder.createShellClientErrorResult(
              String.format(CliStrings.START_LOCATOR__PROCESS_TERMINATED_ABNORMALLY_ERROR_MESSAGE,
                  exitValue, locatorLauncher.getWorkingDirectory(), message.toString()));
        }
      } while (!(registeredLocatorSignalListener && locatorSignalListener.isSignaled())
          && locatorState.isStartingOrNotResponding());
    } finally {
      // stop will close
      stderrReader.stopAsync(StartMemberUtils.PROCESS_STREAM_READER_ASYNC_STOP_TIMEOUT_MILLIS);

      // ErrorStream
      getGfsh().getSignalHandler().unregisterListener(locatorSignalListener);
    }

    Gfsh.println();

    final boolean asyncStart =
        (registeredLocatorSignalListener && locatorSignalListener.isSignaled()
            && ServerLauncher.ServerState.isStartingNotRespondingOrNull(locatorState));

    InfoResultData infoResultData = ResultBuilder.createInfoResultData();

    if (loadSharedConfigurationFromDirectory) {
      infoResultData.addLine("Warning: Option --load-cluster-config-from-dir is deprecated, use '"
          + CliStrings.IMPORT_SHARED_CONFIG
          + "' command instead to import any existing configuration.\n");
    }

    if (asyncStart) {
      infoResultData.addLine(
          String.format(CliStrings.ASYNC_PROCESS_LAUNCH_MESSAGE, CliStrings.LOCATOR_TERM_NAME));
      return ResultBuilder.buildResult(infoResultData);
    }

    infoResultData.addLine(locatorState.toString());
    String locatorHostName;
    InetAddress bindAddr = locatorLauncher.getBindAddress();
    if (bindAddr != null) {
      locatorHostName = bindAddr.getCanonicalHostName();
    } else {
      locatorHostName = StringUtils.defaultIfBlank(locatorLauncher.getHostnameForClients(),
          HostUtils.getLocalHost());
    }

    int locatorPort = Integer.parseInt(locatorState.getPort());


    ConnectCommand connectCommand = new ConnectCommand();
    Properties configProperties = connectCommand.resolveSslProperties(getGfsh(), false,
        gemfirePropertiesFile, gemfireSecurityPropertiesFile);

    // AUTO-CONNECT
    // If the connect succeeds add the connected message to the result,
    // Else, ask the user to use the "connect" command to connect to the Locator.
    if (shouldAutoConnect(connect)) {
      boolean connected =
          doAutoConnect(locatorHostName, locatorPort, configProperties, infoResultData);

      // Report on the state of the Shared Configuration service if enabled...
      if (enableSharedConfiguration && connected) {
        infoResultData.addLine(ClusterConfigurationStatusRetriever.fromLocator(locatorHostName,
            locatorPort, configProperties));
      }
    }

    return ResultBuilder.buildResult(infoResultData);

  }

  // TODO should we connect implicitly when in non-interactive, headless mode (e.g. gfsh -e "start
  // locator ...")?
  // With execute option (-e), there could be multiple commands which might presume that a prior
  // "start locator" has formed the connection.
  private boolean shouldAutoConnect(final boolean connect) {
    return (connect && !isConnectedAndReady());
  }

  private boolean doAutoConnect(final String locatorHostname, final int locatorPort,
      final Properties configurationProperties, final InfoResultData infoResultData) {
    boolean connectSuccess = false;
    boolean jmxManagerAuthEnabled = false;
    boolean jmxManagerSslEnabled = false;

    String responseFailureMessage = null;

    for (int attempts = 0; (attempts < 10 && !connectSuccess); attempts++) {
      try {
        ConnectToLocatorResult connectToLocatorResult =
            ConnectCommand.connectToLocator(locatorHostname, locatorPort,
                ConnectCommand.CONNECT_LOCATOR_TIMEOUT_MS / 4, configurationProperties);

        ConnectionEndpoint memberEndpoint = connectToLocatorResult.getMemberEndpoint();

        jmxManagerSslEnabled = connectToLocatorResult.isJmxManagerSslEnabled();

        getGfsh().setOperationInvoker(new JmxOperationInvoker(memberEndpoint.getHost(),
            memberEndpoint.getPort(), configurationProperties));

        String shellAndLogMessage = CliStrings.format(CliStrings.CONNECT__MSG__SUCCESS,
            "JMX Manager " + memberEndpoint.toString(false));

        infoResultData.addLine("\n");
        infoResultData.addLine(shellAndLogMessage);
        getGfsh().logToFile(shellAndLogMessage, null);

        connectSuccess = true;
        responseFailureMessage = null;
      } catch (IllegalStateException unexpected) {
        if (CauseFinder.indexOfCause(unexpected, ClassCastException.class, false) != -1) {
          responseFailureMessage = "The Locator might require SSL Configuration.";
        }
      } catch (SecurityException ignore) {
        getGfsh().logToFile(ignore.getMessage(), ignore);
        jmxManagerAuthEnabled = true;
        break; // no need to continue after SecurityException
      } catch (AuthenticationFailedException ignore) {
        getGfsh().logToFile(ignore.getMessage(), ignore);
        jmxManagerAuthEnabled = true;
        break; // no need to continue after AuthenticationFailedException
      } catch (SSLException ignore) {
        // another type of SSL error occurred (possibly a configuration issue); pass the buck...
        getGfsh().logToFile(ignore.getMessage(), ignore);
        responseFailureMessage = "Check your SSL configuration and try again.";
      } catch (Exception ignore) {
        getGfsh().logToFile(ignore.getMessage(), ignore);
        responseFailureMessage = "Failed to connect; unknown cause: " + ignore.getMessage();
      }
    }

    if (!connectSuccess) {
      doOnConnectionFailure(locatorHostname, locatorPort, jmxManagerAuthEnabled,
          jmxManagerSslEnabled, infoResultData);
    }

    if (StringUtils.isNotBlank(responseFailureMessage)) {
      infoResultData.addLine("\n");
      infoResultData.addLine(responseFailureMessage);
    }
    return connectSuccess;
  }

  private void doOnConnectionFailure(final String locatorHostName, final int locatorPort,
      final boolean jmxManagerAuthEnabled, final boolean jmxManagerSslEnabled,
      final InfoResultData infoResultData) {
    infoResultData.addLine("\n");
    CommandStringBuilder commandUsage = new CommandStringBuilder(CliStrings.CONNECT)
        .addOption(CliStrings.CONNECT__LOCATOR, locatorHostName + "[" + locatorPort + "]");

    StringBuilder message = new StringBuilder();

    if (jmxManagerAuthEnabled) {
      commandUsage.addOption(CliStrings.CONNECT__USERNAME).addOption(CliStrings.CONNECT__PASSWORD);
      message.append("Authentication");
    }
    if (jmxManagerSslEnabled) {
      message.append(jmxManagerAuthEnabled ? " and " : StringUtils.EMPTY)
          .append("SSL configuration");
    }
    infoResultData.addLine(CliStrings.format(
        CliStrings.START_LOCATOR__USE__0__TO__CONNECT_WITH_SECURITY, commandUsage.toString()));
    if (jmxManagerAuthEnabled || jmxManagerSslEnabled) {
      message.append(" required to connect to the Manager.");
      infoResultData.addLine("\n");
      infoResultData.addLine(message.toString());
    }
  }

  String[] createStartLocatorCommandLine(final LocatorLauncher launcher,
      final File gemfirePropertiesFile, final File gemfireSecurityPropertiesFile,
      final Properties gemfireProperties, final String userClasspath,
      final Boolean includeSystemClasspath, final String[] jvmArgsOpts, final String initialHeap,
      final String maxHeap) throws MalformedObjectNameException {
    List<String> commandLine = new ArrayList<>();

    commandLine.add(StartMemberUtils.getJavaPath());
    commandLine.add("-server");
    commandLine.add("-classpath");
    commandLine
        .add(getLocatorClasspath(Boolean.TRUE.equals(includeSystemClasspath), userClasspath));

    StartMemberUtils.addCurrentLocators(this, commandLine, gemfireProperties);
    StartMemberUtils.addGemFirePropertyFile(commandLine, gemfirePropertiesFile);
    StartMemberUtils.addGemFireSecurityPropertyFile(commandLine, gemfireSecurityPropertiesFile);
    StartMemberUtils.addGemFireSystemProperties(commandLine, gemfireProperties);
    StartMemberUtils.addJvmArgumentsAndOptions(commandLine, jvmArgsOpts);
    StartMemberUtils.addInitialHeap(commandLine, initialHeap);
    StartMemberUtils.addMaxHeap(commandLine, maxHeap);

    commandLine.add(
        "-D".concat(AbstractLauncher.SIGNAL_HANDLER_REGISTRATION_SYSTEM_PROPERTY.concat("=true")));
    commandLine.add("-Djava.awt.headless=true");
    commandLine.add(
        "-Dsun.rmi.dgc.server.gcInterval".concat("=").concat(Long.toString(Long.MAX_VALUE - 1)));
    if (launcher.isRedirectingOutput()) {
      commandLine
          .add("-D".concat(OSProcess.DISABLE_REDIRECTION_CONFIGURATION_PROPERTY).concat("=true"));
    }
    commandLine.add(LocatorLauncher.class.getName());
    commandLine.add(LocatorLauncher.Command.START.getName());

    if (StringUtils.isNotBlank(launcher.getMemberName())) {
      commandLine.add(launcher.getMemberName());
    }

    if (launcher.getBindAddress() != null) {
      commandLine.add("--bind-address=" + launcher.getBindAddress().getCanonicalHostName());
    }

    if (launcher.isDebugging() || isDebugging()) {
      commandLine.add("--debug");
    }

    if (launcher.isForcing()) {
      commandLine.add("--force");
    }

    if (StringUtils.isNotBlank(launcher.getHostnameForClients())) {
      commandLine.add("--hostname-for-clients=" + launcher.getHostnameForClients());
    }

    if (launcher.getPort() != null) {
      commandLine.add("--port=" + launcher.getPort());
    }

    if (launcher.isRedirectingOutput()) {
      commandLine.add("--redirect-output");
    }

    return commandLine.toArray(new String[commandLine.size()]);
  }

  String getLocatorClasspath(final boolean includeSystemClasspath, final String userClasspath) {
    List<String> jarFilePathnames = new ArrayList<>();
    jarFilePathnames.add(StartMemberUtils.CORE_DEPENDENCIES_JAR_PATHNAME);
    // include all extension dependencies on the CLASSPATH...
    for (String extensionsJarPathname : getExtensionsJars()) {
      if (org.apache.commons.lang.StringUtils.isNotBlank(extensionsJarPathname)) {
        jarFilePathnames.add(extensionsJarPathname);
      }
    }

    return StartMemberUtils.toClasspath(includeSystemClasspath,
        jarFilePathnames.toArray(new String[jarFilePathnames.size()]), userClasspath);
  }

  private String[] getExtensionsJars() {
    File extensionsDirectory = new File(StartMemberUtils.EXTENSIONS_PATHNAME);
    File[] extensionsJars = extensionsDirectory.listFiles();

    if (extensionsJars != null) {
      // assume `extensions` directory does not contain any subdirectories. It only contains jars.
      return Arrays.stream(extensionsJars).filter(file -> file.isFile()).map(
          file -> IOUtils.appendToPath(StartMemberUtils.GEODE_HOME, "extensions", file.getName()))
          .toArray(String[]::new);
    } else {
      return ArrayUtils.EMPTY_STRING_ARRAY;
    }
  }
}
