/*
 * 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.distributed;

import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.lowerCase;
import static org.apache.geode.distributed.ConfigurationProperties.LOG_FILE;
import static org.apache.geode.distributed.ConfigurationProperties.NAME;
import static org.apache.geode.internal.lang.StringUtils.wrap;
import static org.apache.geode.internal.lang.SystemUtils.CURRENT_DIRECTORY;
import static org.apache.geode.internal.util.IOUtils.tryGetCanonicalPathElseGetAbsolutePath;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.Logger;

import org.apache.geode.annotations.Immutable;
import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.client.internal.locator.LocatorStatusRequest;
import org.apache.geode.cache.client.internal.locator.LocatorStatusResponse;
import org.apache.geode.distributed.internal.DistributionConfigImpl;
import org.apache.geode.distributed.internal.InternalLocator;
import org.apache.geode.distributed.internal.tcpserver.HostAddress;
import org.apache.geode.distributed.internal.tcpserver.HostAndPort;
import org.apache.geode.distributed.internal.tcpserver.TcpClient;
import org.apache.geode.distributed.internal.tcpserver.TcpSocketCreator;
import org.apache.geode.distributed.internal.tcpserver.TcpSocketFactory;
import org.apache.geode.internal.DistributedSerializableObjectConfig;
import org.apache.geode.internal.DistributionLocator;
import org.apache.geode.internal.GemFireVersion;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.inet.LocalHostUtil;
import org.apache.geode.internal.lang.ObjectUtils;
import org.apache.geode.internal.net.SSLConfig;
import org.apache.geode.internal.net.SSLConfigurationFactory;
import org.apache.geode.internal.net.SocketCreator;
import org.apache.geode.internal.process.ConnectionFailedException;
import org.apache.geode.internal.process.ControlNotificationHandler;
import org.apache.geode.internal.process.ControllableProcess;
import org.apache.geode.internal.process.FileAlreadyExistsException;
import org.apache.geode.internal.process.FileControllableProcess;
import org.apache.geode.internal.process.MBeanInvocationFailedException;
import org.apache.geode.internal.process.PidFile;
import org.apache.geode.internal.process.PidUnavailableException;
import org.apache.geode.internal.process.ProcessController;
import org.apache.geode.internal.process.ProcessControllerFactory;
import org.apache.geode.internal.process.ProcessControllerParameters;
import org.apache.geode.internal.process.ProcessLauncherContext;
import org.apache.geode.internal.process.ProcessType;
import org.apache.geode.internal.process.ProcessUtils;
import org.apache.geode.internal.process.UnableToControlProcessException;
import org.apache.geode.internal.security.SecurableCommunicationChannel;
import org.apache.geode.internal.serialization.filter.SystemPropertyGlobalSerialFilterConfigurationFactory;
import org.apache.geode.internal.serialization.filter.UnableToSetSerialFilterException;
import org.apache.geode.lang.AttachAPINotFoundException;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.management.internal.util.HostUtils;
import org.apache.geode.management.internal.util.JsonUtil;
import org.apache.geode.util.internal.GeodeGlossary;

/**
 * The LocatorLauncher class is a launcher for a Geode Locator.
 *
 * @see org.apache.geode.distributed.AbstractLauncher
 * @see org.apache.geode.distributed.ServerLauncher
 * @since GemFire 7.0
 */
@SuppressWarnings({"unused"})
public class LocatorLauncher extends AbstractLauncher<String> {

  private static final Logger log = LogService.getLogger();

  @Immutable
  private static final Boolean DEFAULT_LOAD_SHARED_CONFIG_FROM_DIR = Boolean.FALSE;

  @Immutable
  private static final Map<String, String> helpMap;

  static {
    final Map<String, String> help = new HashMap<>();
    help.put("launcher",
        "A Geode launcher used to start, stop and determine a Locator's status.");
    help.put(Command.START.getName(), String.format(
        "Starts a Locator running in the current working directory listening on the default port (%s) bound to all IP addresses available to the localhost.  The Locator must be given a member name in the Geode cluster.  The default bind-address and port may be overridden using the corresponding command-line options.",
        getDefaultLocatorPort()));
    help.put(Command.STATUS.getName(),
        "Displays the status of a Locator given any combination of the bind-address[port], member name/ID, PID, or the directory in which the Locator is running.");
    help.put(Command.STOP.getName(),
        "Stops a running Locator given a member name/ID, PID, or the directory in which the Locator is running.");
    help.put(Command.VERSION.getName(),
        "Displays Geode product version information.");
    help.put("bind-address",
        "Specifies the IP address on which to bind, or on which the Locator is bound, listening for client requests.  Defaults to all IP addresses available to the localhost.");
    help.put("debug", "Displays verbose information during the invocation of the launcher.");
    help.put("delete-pid-file-on-stop",
        "Specifies that this Locator's PID file should be deleted on stop.  The default is to not delete this Locator's PID file until JVM exit if --delete-pid-file-on-stop is not specified.");
    help.put("dir",
        "Specifies the working directory where the Locator is running.  Defaults to the current working directory.");
    help.put("force",
        "Enables any existing Locator PID file to be overwritten on start.  The default is to throw an error if a PID file already exists and --force is not specified.");
    help.put("help",
        "Causes Geode to print out information instead of performing the command. This option is supported by all commands.");
    help.put("hostname-for-clients",
        "An option to specify the hostname or IP address to send to clients so they can connect to this Locator. The default is to use the IP address to which the Locator is bound.");
    help.put("member", "Identifies the Locator by member name or ID in the Geode cluster.");
    help.put("pid", "Indicates the OS process ID of the running Locator.");
    help.put("port", String.format(
        "Specifies the port on which the Locator is listening for client requests. Defaults to %s.",
        getDefaultLocatorPort()));
    help.put("redirect-output",
        "An option to cause the Locator to redirect standard out and standard error to the Geode log file.");

    helpMap = Collections.unmodifiableMap(help);
  }

  @Immutable
  private static final Map<Command, String> usageMap;

  static {
    Map<Command, String> usage = new TreeMap<>();
    usage.put(Command.START,
        "start <member-name> [--bind-address=<IP-address>] [--hostname-for-clients=<IP-address>] [--port=<port>] [--dir=<Locator-working-directory>] [--force] [--debug] [--help]");
    usage.put(Command.STATUS,
        "status [--bind-address=<IP-address>] [--port=<port>] [--member=<member-ID/Name>] [--pid=<process-ID>] [--dir=<Locator-working-directory>] [--debug] [--help]");
    usage.put(Command.STOP,
        "stop [--member=<member-ID/Name>] [--pid=<process-ID>] [--dir=<Locator-working-directory>] [--debug] [--help]");
    usage.put(Command.VERSION, "version");
    usageMap = usage;
  }

  private static final String DEFAULT_LOCATOR_LOG_EXT = ".log";
  private static final String DEFAULT_LOCATOR_LOG_NAME = "locator";
  private static final String LOCATOR_SERVICE_NAME = "Locator";

  @MakeNotStatic
  private static final AtomicReference<LocatorLauncher> INSTANCE = new AtomicReference<>();

  // private transient volatile boolean debug;

  private final transient ControlNotificationHandler controlHandler;

  private final AtomicBoolean starting = new AtomicBoolean(false);

  private final boolean deletePidFileOnStop;
  private final boolean force;
  private final boolean help;
  private final boolean redirectOutput;

  private final Command command;

  private final boolean bindAddressSpecified;
  private final boolean portSpecified;
  private final boolean workingDirectorySpecified;

  private final HostAddress bindAddress;

  private final Integer pid;
  private final Integer port;

  private transient volatile InternalLocator locator;

  private final Properties distributedSystemProperties;

  private final String hostnameForClients;
  private final String memberName;
  private final String workingDirectory;

  // NOTE in addition to debug and locator, the other shared, mutable state
  private transient volatile String statusMessage;

  private transient volatile ControllableProcess process;

  private final transient LocatorControllerParameters controllerParameters;

  /**
   * Launches a Geode Locator from the command-line configured with the given arguments.
   *
   * @param args the command-line arguments used to configure the Geode Locator at runtime.
   */
  public static void main(final String... args) {
    try {
      new Builder(args).build().run();
    } catch (AttachAPINotFoundException handled) {
      System.err.println(handled.getMessage());
    }
  }

  private static Integer getDefaultLocatorPort() {
    return Integer.getInteger(DistributionLocator.TEST_OVERRIDE_DEFAULT_PORT_PROPERTY,
        DistributionLocator.DEFAULT_LOCATOR_PORT);
  }

  /**
   * Gets the instance of the LocatorLauncher used to launch the Geode Locator, or null if this VM
   * does not have an instance of LocatorLauncher indicating no Geode Locator is running.
   *
   * @return the instance of LocatorLauncher used to launcher a Geode Locator in this VM.
   */
  public static LocatorLauncher getInstance() {
    return INSTANCE.get();
  }

  /**
   * Gets the LocatorState for this process or null if this process was not launched using this VM's
   * LocatorLauncher reference.
   *
   * @return the LocatorState for this process or null.
   */
  public static LocatorState getLocatorState() {
    return (getInstance() != null ? getInstance().status() : null);
  }

  /**
   * Private constructor used to properly construct an immutable instance of the LocatorLauncher
   * using a Builder. The Builder is used to configure a LocatorLauncher instance. The Builder can
   * process user input from the command-line or be used to properly construct an instance of the
   * LocatorLauncher programmatically using the API.
   *
   * @param builder an instance of LocatorLauncher.Builder for configuring and constructing an
   *        instance of the LocatorLauncher.
   * @see org.apache.geode.distributed.LocatorLauncher.Builder
   */
  private LocatorLauncher(final Builder builder) {
    command = builder.getCommand();
    help = Boolean.TRUE.equals(builder.getHelp());
    bindAddressSpecified = builder.isBindAddressSpecified();
    bindAddress = builder.getHostAddress();
    setDebug(Boolean.TRUE.equals(builder.getDebug()));
    deletePidFileOnStop = Boolean.TRUE.equals(builder.getDeletePidFileOnStop());
    distributedSystemProperties = builder.getDistributedSystemProperties();
    force = Boolean.TRUE.equals(builder.getForce());
    hostnameForClients = builder.getHostnameForClients();
    memberName = builder.getMemberName();
    pid = builder.getPid();
    portSpecified = builder.isPortSpecified();
    port = builder.getPort();
    redirectOutput = Boolean.TRUE.equals(builder.getRedirectOutput());
    workingDirectorySpecified = builder.isWorkingDirectorySpecified();
    workingDirectory = builder.getWorkingDirectory();
    controllerParameters = new LocatorControllerParameters();
    controlHandler = new ControlNotificationHandler() {
      @Override
      public void handleStop() {
        if (isStoppable()) {
          stopInProcess();
        }
      }

      @Override
      public ServiceState<?> handleStatus() {
        return statusInProcess();
      }
    };
  }

  /**
   * Returns the status of the locator on the given host and port
   *
   * @param port the port that the locator is listening on
   * @param bindAddress the IP address to which the locator's socket binds
   * @return a {@link LocatorStatusResponse} representing the status of the locator
   * @throws IOException if there is a problem interacting with the locator
   *
   * @deprecated in Geode 1.12. Use statusForLocator() instance method instead.
   *             This static method does not use the properties set via the Builder.
   */
  @Deprecated
  public static LocatorStatusResponse statusLocator(int port, InetAddress bindAddress)
      throws IOException {
    return statusLocator(port, bindAddress == null ? null : bindAddress.getCanonicalHostName(),
        new DistributionConfigImpl(new Properties()));
  }

  /**
   * Returns the status of the locator on the given host and port. If you have endpoint
   * identification enabled the preferred method is statusForLocator(int, String), which
   * lets you specify the locator's name that the locator has stored in its TLS certificate
   *
   * @param port the port that the locator is listening on
   * @param bindAddressArg the IP address to which the locator's socket binds
   * @return a {@link LocatorStatusResponse} representing the status of the locator
   * @throws IOException if there is a problem interacting with the locator
   */
  public LocatorStatusResponse statusForLocator(int port, InetAddress bindAddressArg)
      throws IOException {
    final String bindAddress =
        bindAddressArg == null ? null : bindAddressArg.getCanonicalHostName();
    final SSLConfig sslConfig = getSSLConfigurations(getLocator());
    return getLocatorStatusResponse(port, bindAddress, sslConfig);
  }

  private SSLConfig getSSLConfigurations(Locator locator) {
    final SSLConfig sslConfig;
    if (locator == null) {
      /*
       * Locator is null in the case when a LocatorLauncher is created
       * using its Builder but start is never called, hence no locator exists.
       * Instead, this LocatorLauncher is used to get the status of the
       * locator using PID, working dir etc.
       * This is done in StatusLocatorCommand in gfsh
       */
      sslConfig = SSLConfigurationFactory.getSSLConfigForComponent(
          getProperties(),
          SecurableCommunicationChannel.LOCATOR);
    } else {
      /*
       * If a locator is already present then the DistributedConfigImpl
       * is not constructed again but rather taken from the locator properties.
       * This will avoid reading the property files and possible old address
       * verification error as those may not be valid anymore.
       */
      sslConfig = SSLConfigurationFactory.getSSLConfigForComponent(
          ((InternalLocator) locator).getConfig(),
          SecurableCommunicationChannel.LOCATOR);
    }
    return sslConfig;
  }

  /**
   * Returns the status of the locator on the given host and port
   *
   * @param port the port that the locator is listening on
   * @param hostname the host name or ip address that the locator is listening on.
   * @return a {@link LocatorStatusResponse} representing the status of the locator
   * @throws IOException if there is a problem interacting with the locator
   */
  public LocatorStatusResponse statusForLocator(int port, String hostname)
      throws IOException {
    final SSLConfig sslConfig = getSSLConfigurations(getLocator());
    return getLocatorStatusResponse(port, hostname, sslConfig);
  }

  private static LocatorStatusResponse statusLocator(
      final int port, String bindAddress, DistributionConfigImpl distributionConfig)
      throws IOException {
    final SSLConfig sslConfig = SSLConfigurationFactory.getSSLConfigForComponent(
        distributionConfig,
        SecurableCommunicationChannel.LOCATOR);
    return getLocatorStatusResponse(port, bindAddress, sslConfig);
  }

  private static LocatorStatusResponse getLocatorStatusResponse(int port, String bindAddress,
      SSLConfig sslConfig)
      throws IOException {
    // final int timeout = (60 * 2 * 1000); // 2 minutes
    final int timeout = Integer.MAX_VALUE; // 2 minutes
    try {
      final TcpSocketCreator socketCreator = new SocketCreator(sslConfig);
      final TcpClient client = new TcpClient(socketCreator,
          InternalDataSerializer.getDSFIDSerializer().getObjectSerializer(),
          InternalDataSerializer.getDSFIDSerializer().getObjectDeserializer(),
          TcpSocketFactory.DEFAULT);
      return (LocatorStatusResponse) client.requestToServer(
          new HostAndPort(bindAddress == null ? HostUtils.getLocalHost() : bindAddress, port),
          new LocatorStatusRequest(), timeout, true);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Gets a reference to the {@code Cache} that was created by this {@code LocatorLauncher}.
   *
   * @return a reference to the Cache
   * @see Cache
   */
  public Cache getCache() {
    return getInternalLocator().getCache();
  }

  /**
   * Gets a reference to the {@code Locator} that was created by this {@code LocatorLauncher}.
   *
   * @return a reference to the Locator.
   * @see Locator
   */
  public Locator getLocator() {
    return locator;
  }

  /**
   * Gets a reference to the {@code Locator} as an {@code InternalLocator}. For internal use only.
   *
   * @return a reference to the Locator as an InternalLocator.
   */
  InternalLocator getInternalLocator() {
    return locator;
  }

  /**
   * Gets an identifier that uniquely identifies and represents the Locator associated with this
   * launcher.
   *
   * @return a String value identifier to uniquely identify the Locator and it's launcher.
   * @see #getBindAddressAsString()
   * @see #getPortAsString()
   */
  public String getId() {
    return LocatorState.getBindAddressAsString(this).concat("[")
        .concat(LocatorState.getPortAsString(this)).concat("]");
  }

  /**
   * Get the Locator launcher command used to invoke the Locator.
   *
   * @return the Locator launcher command used to invoke the Locator.
   * @see org.apache.geode.distributed.LocatorLauncher.Command
   */
  public Command getCommand() {
    return command;
  }

  /**
   * Determines whether the PID file is allowed to be overwritten when the Locator is started and a
   * PID file already exists in the Locator's specified working directory.
   *
   * @return boolean indicating if force has been enabled.
   */
  public boolean isForcing() {
    return force;
  }

  /**
   * Determines whether this launcher will be used to display help information. If so, then none of
   * the standard Locator launcher commands will be used to affect the state of the Locator. A
   * launcher is said to be 'helping' if the user entered the "--help" option (switch) on the
   * command-line.
   *
   * @return a boolean value indicating if this launcher is used for displaying help information.
   * @see org.apache.geode.distributed.LocatorLauncher.Command
   */
  public boolean isHelping() {
    return help;
  }

  /**
   * Determines whether this launcher will redirect output to system logs when starting a new
   * Locator process.
   *
   * @return a boolean value indicating if this launcher will redirect output to system logs when
   *         starting a new Locator process
   */
  public boolean isRedirectingOutput() {
    return redirectOutput;
  }

  /**
   * Gets the IP address of the NIC to which the Locator has bound itself listening for client
   * requests.
   *
   * @return an InetAddress object representing the configured bind address for the Locator.
   * @see java.net.InetAddress
   */
  public InetAddress getBindAddress() {
    return bindAddress == null ? null : bindAddress.getAddress();
  }

  /**
   * Gets the host, as either hostname or IP address, on which the Locator was bound and running. An
   * attempt is made to get the canonical hostname for IP address to which the Locator was bound for
   * accepting client requests. If the bind address is null or localhost is unknown, then a default
   * String value of "localhost/127.0.0.1" is returned.
   *
   * Note, this information is purely information and should not be used to re-construct state or
   * for other purposes.
   *
   * @return the hostname or IP address of the host running the Locator, based on the bind-address,
   *         or 'localhost/127.0.0.1' if the bind address is null and localhost is unknown.
   * @see java.net.InetAddress
   * @see #getBindAddress()
   */
  protected String getBindAddressAsString() {
    try {
      if (bindAddress != null) {
        return bindAddress.getHostName();
      }
      return LocalHostUtil.getCanonicalLocalHostName();
    } catch (UnknownHostException handled) {
      // Returning localhost/127.0.0.1 implies the bindAddress was null and no IP address for
      // localhost could be found
      return "localhost/127.0.0.1";
    }
  }

  /**
   * Gets the hostname that clients will use to lookup the running Locator.
   *
   * @return a String indicating the hostname used by clients to lookup the Locator.
   */
  public String getHostnameForClients() {
    return hostnameForClients;
  }

  /**
   * Gets the name of the log file used to log information about this Locator.
   *
   * @return a String value indicating the name of this Locator's log file.
   */
  @Override
  public String getLogFileName() {
    return defaultIfBlank(getMemberName(), DEFAULT_LOCATOR_LOG_NAME)
        .concat(DEFAULT_LOCATOR_LOG_EXT);
  }

  /**
   * Gets the name of this member (this Locator) in the Geode distributed system and determined by
   * the 'name' Geode property.
   *
   * @return a String indicating the name of the member (this Locator) in the Geode distributed
   *         system.
   */
  @Override
  public String getMemberName() {
    return defaultIfBlank(memberName, super.getMemberName());
  }

  /**
   * Gets the user-specified process ID (PID) of the running Locator that LocatorLauncher uses to
   * issue status and stop commands to the Locator.
   *
   * @return an Integer value indicating the process ID (PID) of the running Locator.
   */
  @Override
  public Integer getPid() {
    return pid;
  }

  /**
   * Gets the port number on which the Locator listens for client requests.
   *
   * @return an Integer value indicating the port number on which the Locator is listening for
   *         client requests.
   */
  public Integer getPort() {
    if (locator != null) {
      return locator.getPort();
    }

    return port;
  }

  /**
   * Gets the port number represented as a String value. If the port number is null, the the default
   * Locator port (10334) is returned;
   *
   * @return the port number as a String value.
   * @see #getPort()
   */
  public String getPortAsString() {
    Integer port = getPort();
    return (port != null ? port : getDefaultLocatorPort()).toString();
  }

  /**
   * Gets the Geode Distributed System (cluster) Properties.
   *
   * @return a Properties object containing the configuration settings for the Geode Distributed
   *         System (cluster).
   * @see java.util.Properties
   */
  public Properties getProperties() {
    return (Properties) distributedSystemProperties.clone();
  }

  /**
   * Gets the name for a Geode Locator.
   *
   * @return a String indicating the name for a Geode Locator.
   */
  @Override
  public String getServiceName() {
    return LOCATOR_SERVICE_NAME;
  }

  /**
   * Gets the working directory pathname in which the Locator will be run.
   *
   * @return a String value indicating the pathname of the Locator's working directory.
   */
  @Override
  public String getWorkingDirectory() {
    return workingDirectory;
  }

  /**
   * Displays help for the specified Locator launcher command to standard output. If the Locator
   * launcher command is unspecified, then usage information is displayed instead.
   *
   * @param command the Locator launcher command in which to display help information.
   * @see #usage()
   */
  public void help(final Command command) {
    if (Command.isUnspecified(command)) {
      usage();
    } else {
      info(wrap(helpMap.get(command.getName()), 80, ""));
      info("\n\nusage: \n\n");
      info(wrap("> java ... " + getClass().getName() + " " + usageMap.get(command), 80, "\t\t"));
      info("\n\noptions: \n\n");

      for (String option : command.getOptions()) {
        info(wrap("--" + option + ": " + helpMap.get(option) + "\n", 80, "\t"));
      }

      info("\n\n");
    }
  }

  /**
   * Displays usage information on the proper invocation of the LocatorLauncher from the
   * command-line to standard output.
   *
   * @see #help(org.apache.geode.distributed.LocatorLauncher.Command)
   */
  public void usage() {
    info(wrap(helpMap.get("launcher"), 80, "\t"));
    info("\n\nSTART\n\n");
    help(Command.START);
    info("STATUS\n\n");
    help(Command.STATUS);
    info("STOP\n\n");
    help(Command.STOP);
  }

  /**
   * The Runnable method used to launch the Locator with the specified command. If 'start' has been
   * issued, then run will block as expected for the Locator to stop. The 'start' command is
   * implemented with a call to start() followed by a call to waitOnLocator().
   *
   * @see java.lang.Runnable
   * @see LocatorLauncher.Command
   * @see LocatorLauncher#start()
   * @see LocatorLauncher#waitOnLocator()
   * @see LocatorLauncher#status()
   * @see LocatorLauncher#stop()
   * @see LocatorLauncher#version()
   * @see LocatorLauncher#help(org.apache.geode.distributed.LocatorLauncher.Command)
   * @see LocatorLauncher#usage()
   */
  @Override
  public void run() {
    if (!isHelping()) {
      switch (getCommand()) {
        case START:
          info(start());
          waitOnLocator();
          break;
        case STATUS:
          info(status());
          break;
        case STOP:
          info(stop());
          break;
        case VERSION:
          info(version());
          break;
        default:
          usage();
      }
    } else {
      help(getCommand());
    }
  }

  /**
   * Gets a File reference with the path to the PID file for the Locator.
   *
   * @return a File reference to the path of the Locator's PID file.
   */
  protected File getLocatorPidFile() {
    return new File(getWorkingDirectory(), ProcessType.LOCATOR.getPidFileName());
  }

  /**
   * Determines whether a Geode Locator can be started with this instance of LocatorLauncher.
   *
   * @return a boolean indicating whether a Geode Locator can be started with this instance of
   *         LocatorLauncher, which is true if the LocatorLauncher has not already started a Locator
   *         or a Locator is not already running.
   * @see #start()
   */
  private boolean isStartable() {
    return (!isRunning() && starting.compareAndSet(false, true));
  }

  /**
   * Starts a Locator running on the specified port and bind address, as determined by getPort and
   * getSocketAddress respectively, defaulting to 10334 and 'localhost' if not specified, with both
   * peer and server location enabled.
   *
   * 'start' is an asynchronous invocation of the Locator. As such, this method makes no guarantees
   * whether the Locator's location services (peer and server) are actually running before it
   * returns. The Locator's location-based services are initiated in separate, daemon Threads and
   * depends on the relative timing and scheduling of those Threads by the JVM. If the application
   * using this API wishes for the Locator to continue running after normal application processing
   * completes, then one must call <code>waitOnLocator</code>.
   *
   * Given the nature of start, the Locator's status will be in either 1 of 2 possible states. If
   * the 'request' to start the Locator proceeds without exception, the status will be 'STARTED'.
   * However, if any exception is encountered during the normal startup sequence, then a
   * RuntimeException is thrown and the status is set to 'STOPPED'.
   *
   * @return a LocatorState to reflect the state of the Locator after start.
   * @throws RuntimeException if the Locator failed to start for any reason.
   * @throws IllegalStateException if the Locator is already running.
   * @see #failOnStart(Throwable)
   * @see #getBindAddress()
   * @see #getDistributedSystemProperties()
   * @see #isForcing()
   * @see #getLogFile()
   * @see #getLocatorPidFile
   * @see #getPort()
   * @see #status()
   * @see #stop()
   * @see #waitOnLocator()
   * @see #waitOnStatusResponse(long, long, java.util.concurrent.TimeUnit)
   * @see org.apache.geode.distributed.LocatorLauncher.LocatorState
   * @see org.apache.geode.distributed.AbstractLauncher.Status#NOT_RESPONDING
   * @see org.apache.geode.distributed.AbstractLauncher.Status#ONLINE
   * @see org.apache.geode.distributed.AbstractLauncher.Status#STARTING
   */
  public LocatorState start() {
    if (isStartable()) {
      INSTANCE.compareAndSet(null, this);

      boolean serializationFilterConfigured = configureGlobalSerialFilterIfEnabled();

      try {
        process =
            new FileControllableProcess(controlHandler, new File(getWorkingDirectory()),
                ProcessType.LOCATOR, isForcing());

        assertPortAvailable(getBindAddress(), getPort());

        ProcessLauncherContext.set(isRedirectingOutput(), getOverriddenDefaults(),
            statusMessage -> this.statusMessage = statusMessage);

        try {
          locator = InternalLocator.startLocator(getPort(), getLogFile(), null, null,
              bindAddress, true, getDistributedSystemProperties(),
              getHostnameForClients(),
              Paths.get(workingDirectory));

          if (serializationFilterConfigured) {
            log.info("Global serial filter is now configured.");
          }

        } finally {
          ProcessLauncherContext.remove();
        }

        debug("Running Locator on (%1$s) in (%2$s) as (%3$s)...", getId(), getWorkingDirectory(),
            getMember());
        log.debug("Locator is online");
        running.set(true);

        return new LocatorState(this, Status.ONLINE);
      } catch (IOException e) {
        failOnStart(e);
        throw new RuntimeException(
            String.format("An IO error occurred while starting a %s in %s on %s: %s",
                getServiceName(), getWorkingDirectory(), getId(), e.getMessage()),
            e);
      } catch (FileAlreadyExistsException e) {
        failOnStart(e);
        throw new RuntimeException(
            String.format("A PID file already exists and a %s may be running in %s on %s.",
                getServiceName(), getWorkingDirectory(), getId()),
            e);
      } catch (PidUnavailableException e) {
        failOnStart(e);
        throw new RuntimeException(
