All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gemstone.gemfire.distributed.ServerLauncher Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2010-2015 Pivotal Software, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License. You
 * may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License. See accompanying
 * LICENSE file.
 */

package com.gemstone.gemfire.distributed;

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.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.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import com.gemstone.gemfire.SystemFailure;
import com.gemstone.gemfire.cache.Cache;
import com.gemstone.gemfire.cache.CacheFactory;
import com.gemstone.gemfire.cache.partition.PartitionRegionHelper;
import com.gemstone.gemfire.cache.server.CacheServer;
import com.gemstone.gemfire.distributed.AbstractLauncher.Status;
import com.gemstone.gemfire.distributed.internal.DistributionConfig;
import com.gemstone.gemfire.distributed.internal.InternalDistributedSystem;
import com.gemstone.gemfire.internal.GemFireVersion;
import com.gemstone.gemfire.internal.ManagerLogWriter;
import com.gemstone.gemfire.internal.SharedLibrary;
import com.gemstone.gemfire.internal.SocketCreator;
import com.gemstone.gemfire.internal.cache.AbstractBridgeServer;
import com.gemstone.gemfire.internal.cache.CacheConfig;
import com.gemstone.gemfire.internal.cache.CacheServerLauncher;
import com.gemstone.gemfire.internal.cache.GemFireCacheImpl;
import com.gemstone.gemfire.internal.cache.PartitionedRegion;
import com.gemstone.gemfire.internal.i18n.LocalizedStrings;
import com.gemstone.gemfire.internal.lang.ObjectUtils;
import com.gemstone.gemfire.internal.lang.StringUtils;
import com.gemstone.gemfire.internal.lang.SystemUtils;
import com.gemstone.gemfire.internal.process.ConnectionFailedException;
import com.gemstone.gemfire.internal.process.ControlNotificationHandler;
import com.gemstone.gemfire.internal.process.ControllableProcess;
import com.gemstone.gemfire.internal.process.FileAlreadyExistsException;
import com.gemstone.gemfire.internal.process.LocalProcessLauncher;
import com.gemstone.gemfire.internal.process.MBeanInvocationFailedException;
import com.gemstone.gemfire.internal.process.PidUnavailableException;
import com.gemstone.gemfire.internal.process.ProcessController;
import com.gemstone.gemfire.internal.process.ProcessControllerFactory;
import com.gemstone.gemfire.internal.process.ProcessControllerParameters;
import com.gemstone.gemfire.internal.process.ProcessLauncherContext;
import com.gemstone.gemfire.internal.process.ProcessType;
import com.gemstone.gemfire.internal.process.ProcessUtils;
import com.gemstone.gemfire.internal.process.StartupStatusListener;
import com.gemstone.gemfire.internal.process.UnableToControlProcessException;
import com.gemstone.gemfire.internal.util.CollectionUtils;
import com.gemstone.gemfire.internal.util.IOUtils;
import com.gemstone.gemfire.lang.AttachAPINotFoundException;
import com.gemstone.gemfire.management.internal.cli.json.GfJsonArray;
import com.gemstone.gemfire.management.internal.cli.json.GfJsonException;
import com.gemstone.gemfire.management.internal.cli.json.GfJsonObject;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;

/**
 * The ServerLauncher class is a launcher class with main method to start a GemFire Server (implying a GemFire Cache
 * Server process).
 * 
 * @author John Blum
 * @author Kirk Lund
 * @see com.gemstone.gemfire.distributed.AbstractLauncher
 * @see com.gemstone.gemfire.distributed.LocatorLauncher
 * @since 7.0
 */
@SuppressWarnings({ "unused" })
public final class ServerLauncher extends AbstractLauncher {

  /**
   * @deprecated This is specific to the internal implementation and may go away in a future release.
   */
  protected static final Integer DEFAULT_SERVER_PORT = getDefaultServerPort();
  private static final Integer getDefaultServerPort() {
    return Integer.getInteger(AbstractBridgeServer.TEST_OVERRIDE_DEFAULT_PORT_PROPERTY, CacheServer.DEFAULT_PORT);
  }

  private static final Map helpMap = new HashMap();

  static {
    helpMap.put("launcher", LocalizedStrings.ServerLauncher_SERVER_LAUNCHER_HELP.toLocalizedString());
    helpMap.put(Command.START.getName(), LocalizedStrings.ServerLauncher_START_SERVER_HELP.toLocalizedString(String.valueOf(getDefaultServerPort())));
    helpMap.put(Command.STATUS.getName(), LocalizedStrings.ServerLauncher_STATUS_SERVER_HELP.toLocalizedString());
    helpMap.put(Command.STOP.getName(),LocalizedStrings.ServerLauncher_STOP_SERVER_HELP.toLocalizedString());
    helpMap.put(Command.VERSION.getName(), LocalizedStrings.ServerLauncher_VERSION_SERVER_HELP.toLocalizedString());
    helpMap.put("assign-buckets",LocalizedStrings.ServerLauncher_SERVER_ASSIGN_BUCKETS_HELP.toLocalizedString());
    helpMap.put("debug", LocalizedStrings.ServerLauncher_SERVER_DEBUG_HELP.toLocalizedString());
    helpMap.put("dir",LocalizedStrings.ServerLauncher_SERVER_DIR_HELP.toLocalizedString());
    helpMap.put("disable-default-server", LocalizedStrings.ServerLauncher_SERVER_DISABLE_DEFAULT_SERVER_HELP.toLocalizedString());
    helpMap.put("force", LocalizedStrings.ServerLauncher_SERVER_FORCE_HELP.toLocalizedString());
    helpMap.put("help", LocalizedStrings.SystemAdmin_CAUSES_GEMFIRE_TO_PRINT_OUT_INFORMATION_INSTEAD_OF_PERFORMING_THE_COMMAND_THIS_OPTION_IS_SUPPORTED_BY_ALL_COMMANDS.toLocalizedString());
    helpMap.put("member", LocalizedStrings.ServerLauncher_SERVER_MEMBER_HELP.toLocalizedString());
    helpMap.put("pid", LocalizedStrings.ServerLauncher_SERVER_PID_HELP.toLocalizedString());
    helpMap.put("server-bind-address", LocalizedStrings.ServerLauncher_SERVER_BIND_ADDRESS_HELP.toLocalizedString());
    helpMap.put("server-port", LocalizedStrings.ServerLauncher_SERVER_PORT_HELP.toLocalizedString(String.valueOf(getDefaultServerPort())));
    helpMap.put("rebalance", LocalizedStrings.ServerLauncher_SERVER_REBALANCE_HELP.toLocalizedString());
    helpMap.put("redirect-output", LocalizedStrings.ServerLauncher_SERVER_REDIRECT_OUTPUT_HELP.toLocalizedString());
  }

  private static final Map usageMap = new TreeMap();

  static {
    usageMap.put(Command.START, "start  [--assign-buckets] [--disable-default-server] [--rebalance] [--server-bind-address=] [--server-port=] [--force] [--debug] [--help]");
    usageMap.put(Command.STATUS, "status [--member=] [--pid=] [--dir=] [--debug] [--help]");
    usageMap.put(Command.STOP, "stop [--member=] [--pid=] [--dir=] [--debug] [--help]");
    usageMap.put(Command.VERSION, "version");
  }

  /**
   * @deprecated This is specific to the internal implementation and may go away in a future release.
   */
  public static final String DEFAULT_SERVER_PID_FILE = "vf.gf.server.pid";

  private static final String DEFAULT_SERVER_LOG_EXT = ".log";
  private static final String DEFAULT_SERVER_LOG_NAME = "gemfire";
  private static final String SERVER_SERVICE_NAME = "Server";

  private static final AtomicReference INSTANCE = new AtomicReference();

  private volatile transient boolean debug;

  private final transient ControlNotificationHandler controlHandler;
  
  private final AtomicBoolean starting = new AtomicBoolean(false);

  private final boolean assignBuckets;
  private final boolean disableDefaultServer;
  private final boolean force;
  private final boolean help;
  private final boolean rebalance;
  private final boolean redirectOutput;

  private volatile transient Cache cache;

  private final Command command;

  private final InetAddress serverBindAddress;

  private final Integer pid;
  private final Integer serverPort;

  private final Properties distributedSystemProperties;

  private final String memberName;
  private final String workingDirectory;

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

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

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

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

  /**
   * Private constructor used to properly construct an immutable instance of the ServerLauncher using a Builder.
   * The Builder is used to configure a ServerLauncher instance.  The Builder can process user input from the
   * command-line or be used programmatically to properly construct an instance of the ServerLauncher using the API.
   * 
   * @param builder an instance of ServerLauncher.Builder for configuring and constructing an instance of the
   * ServerLauncher.
   * @see com.gemstone.gemfire.distributed.ServerLauncher.Builder
   */
  private ServerLauncher(final Builder builder) {
    this.cache = builder.getCache(); // testing
    this.command = builder.getCommand();
    this.assignBuckets = Boolean.TRUE.equals(builder.getAssignBuckets());
    setDebug(Boolean.TRUE.equals(builder.getDebug()));
    this.disableDefaultServer = Boolean.TRUE.equals(builder.getDisableDefaultServer());
    CacheServerLauncher.disableDefaultServer.set(this.disableDefaultServer);
    this.distributedSystemProperties = builder.getDistributedSystemProperties();
    this.force = Boolean.TRUE.equals(builder.getForce());
    this.help = Boolean.TRUE.equals(builder.getHelp());
    this.rebalance = Boolean.TRUE.equals(builder.getRebalance());
    this.memberName = builder.getMemberName();
    this.pid = builder.getPid();
    this.redirectOutput = Boolean.TRUE.equals(builder.getRedirectOutput());
    this.serverBindAddress = builder.getServerBindAddress();
    if (builder.isServerBindAddressSetByUser() && this.serverBindAddress != null) {
      CacheServerLauncher.serverBindAddress.set(this.serverBindAddress.getHostAddress());
    }
    this.serverPort = builder.getServerPort();
    if (builder.isServerPortSetByUser() && this.serverPort != null) {
      CacheServerLauncher.serverPort.set(this.serverPort);
    }
    this.workingDirectory = builder.getWorkingDirectory();
    this.controllerParameters = new ServerControllerParameters();
    this.controlHandler = new ControlNotificationHandler() {
      @Override
      public void handleStop() {
        if (isStoppable()) {
          stopInProcess();
        }
      }
      @Override
      public ServiceState handleStatus() {
        return statusInProcess();
      }
    };
  }

  /**
   * Gets a reference to the Cache that was created when the GemFire Server was started.
   * 
   * @return a reference to the Cache created by the GemFire Server start operation.
   * @see com.gemstone.gemfire.cache.Cache
   */
  final Cache getCache() {
    return this.cache;
  }

  /**
   * Gets an identifier that uniquely identifies and represents the Server associated with this launcher.
   * 
   * @return a String value identifier to uniquely identify the Server and it's launcher.
   * @see #getServerBindAddressAsString()
   * @see #getServerPortAsString()
   */
  public final String getId() {
    final StringBuilder buffer = new StringBuilder(ServerState.getServerBindAddressAsString(this));
    final String serverPort = ServerState.getServerPortAsString(this);

    if (!StringUtils.isBlank(serverPort)) {
      buffer.append("[").append(serverPort).append("]");
    }

    return buffer.toString();
  }

  /**
   * Get the Server launcher command used to invoke the Server.
   * 
   * @return the Server launcher command used to invoke the Server.
   * @see com.gemstone.gemfire.distributed.ServerLauncher.Command
   */
  public Command getCommand() {
    return this.command;
  }

  /**
   * Determines whether buckets should be assigned to partitioned regions in the cache upon Server start.
   * 
   * @return a boolean indicating if buckets should be assigned upon Server start.
   */
  public boolean isAssignBuckets() {
    return this.assignBuckets;
  }

  /**
   * Determines whether a default cache server will be added when the GemFire Server comes online.
   * 
   * @return a boolean value indicating whether to add a default cache server.
   */
  public boolean isDisableDefaultServer() {
    return this.disableDefaultServer;
  }

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

  /**
   * Determines whether this launcher will be used to display help information.  If so, then none of the standard
   * Server launcher commands will be used to affect the state of the Server.  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 com.gemstone.gemfire.distributed.ServerLauncher.Command
   */
  public boolean isHelping() {
    return this.help;
  }

  /**
   * Determines whether a rebalance operation on the cache will occur upon starting the GemFire server using this
   * launcher.
   * 
   * @return a boolean indicating if the cache will be rebalance when the GemFire server starts.
   */
  public boolean isRebalancing() {
    return this.rebalance;
  }

  /**
   * 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 this.redirectOutput;
  }

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

  /**
   * Gets the name of this member (this Server) in the GemFire distributed system as determined by the 'name' GemFire
   * property.
   * 
   * @return a String indicating the name of the member (this Server) in the GemFire distributed system.
   * @see AbstractLauncher#getMemberName()
   */
  public String getMemberName() {
    return StringUtils.defaultIfBlank(this.memberName, super.getMemberName());
  }

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

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

  /**
   * Gets the IP address to which the Server is bound listening for and accepting cache client connections.  This
   * property should not be confused with 'bindAddress' ServerLauncher property, which is the port for binding the
   * Server's ServerSocket used in distribution and messaging between the peers of the GemFire distributed system.
   * 
   * @return an InetAddress indicating the IP address that the Server is bound to listening for and accepting cache
   * client connections in a client/server topology.
   */
  public InetAddress getServerBindAddress() {
    return this.serverBindAddress;
  }

  /**
   * Gets the host, as either hostname or IP address, on which the Server was bound and running.  An attempt is made
   * to get the canonical hostname for IP address to which the Server was bound for accepting client requests.  If
   * the server 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 Server, 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 #getServerBindAddress()
   */
  public String getServerBindAddressAsString() {
    try {
      if (getServerBindAddress() != null) {
        return getServerBindAddress().getCanonicalHostName();
      }

      final InetAddress localhost = SocketCreator.getLocalHost();

      return localhost.getCanonicalHostName();
    }
    catch (UnknownHostException ignore) {
      // TODO determine a better value for the host on which the Server is running to return here...
      // NOTE returning localhost/127.0.0.1 implies the serverBindAddress was null and no IP address for localhost
      // could be found
      return "localhost/127.0.0.1";
    }
  }

  /**
   * Gets the port on which the Server is listening for cache client connections.  This property should not be confused
   * with the 'port' ServerLauncher property, which is used by the Server to set the 'tcp-port' distribution config
   * property and is used by the ServerSocket for peer distribution and messaging.
   * 
   * @return an Integer value indicating the port the Server is listening on for cache client connections in the
   * client/server topology.
   */
  public Integer getServerPort() {
    return this.serverPort;
  }

  /**
   * Gets the server port on which the Server is listening for client requests represented as a String value.
   * 
   * @return a String representing the server port on which the Server is listening for client requests.
   * @see #getServerPort()
   */
  public String getServerPortAsString() {
    return ObjectUtils.defaultIfNull(getServerPort(), getDefaultServerPort()).toString();
  }

  /**
   * Gets the name for a GemFire Server.
   * 
   * @return a String indicating the name for a GemFire Server.
   */
  public String getServiceName() {
    return SERVER_SERVICE_NAME;
  }

  /**
   * Gets the working directory pathname in which the Server will be run.
   * 
   * @return a String value indicating the pathname of the Server's working directory.
   */
  @Override
  public String getWorkingDirectory() {
    return this.workingDirectory;
  }
  
  /**
   * Displays help for the specified Server launcher command to standard err.  If the Server launcher command
   * is unspecified, then usage information is displayed instead.
   * 
   * @param command the Server launcher command in which to display help information.
   * @see #usage()
   */
  public void help(final Command command) {
    if (Command.isUnspecified(command)) {
      usage();
    }
    else {
      info(StringUtils.wrap(helpMap.get(command.getName()), 80, ""));
      info("\n\nusage: \n\n");
      info(StringUtils.wrap("> java ... " + getClass().getName() + " " + usageMap.get(command), 80, "\t\t"));
      info("\n\noptions: \n\n");

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

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

  /**
   * Displays usage information on the proper invocation of the ServerLauncher from the command-line to standard err.
   * 
   * @see #help(com.gemstone.gemfire.distributed.ServerLauncher.Command)
   */
  public void usage() {
    info(StringUtils.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);
  }

  /**
   * A Runnable method used to invoke the GemFire server (cache server) with the specified command.  From run, a user
   * can invoke 'start', 'status', 'stop' and 'version'.  Note, that 'version' is also a command-line option, but can
   * be treated as a "command" as well.
   * 
   * @see java.lang.Runnable
   */
  @Override
  public void run() {
    if (!isHelping()) {
      switch (getCommand()) {
        case START:
          info(start());
          waitOnServer();
          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 Server.
   * 
   * @return a File reference to the path of the Server's PID file.
   */
  protected File getServerPidFile() {
    return new File(getWorkingDirectory(), ProcessType.SERVER.getPidFileName());
  }
  
  /**
   * Determines whether a GemFire Cache Server can be started with this instance of ServerLauncher.
   *
   * @return a boolean indicating whether a GemFire Cache Server can be started with this instance of ServerLauncher,
   * which is true if the ServerLauncher has not already started a Server or a Server is not already running.
   * @see #start()
   */
  private boolean isStartable() {
    return (!isRunning() && this.starting.compareAndSet(false, true));
  }

  /**
   * Invokes the 'start' command and operation to startup a GemFire server (a cache server).  Note, this method will
   * cause the JVM to block upon server start, providing the calling Thread is a non-daemon Thread.
   *
   * @see #run()
   */
  public ServerState start() {
    if (isStartable()) {
      INSTANCE.compareAndSet(null, this);

      try {
        this.process = new ControllableProcess(this.controlHandler, new File(getWorkingDirectory()), ProcessType.SERVER, isForcing());

        if (!isDisableDefaultServer()) {
          assertPortAvailable(getServerBindAddress(), getServerPort());
        }

        SystemFailure.setExitOK(true);

        ProcessLauncherContext.set(isRedirectingOutput(), getOverriddenDefaults(), new StartupStatusListener() {
          @Override
          public void setStatus(final String statusMessage) {
            debug("Callback setStatus(String) called with message (%1$s)...", statusMessage);
            ServerLauncher.this.statusMessage = statusMessage;
          }
        });

        try {
          final Properties gemfireProperties = getDistributedSystemProperties(getProperties());
          this.cache = new CacheFactory(gemfireProperties).create();
          this.cache.setIsServer(true);
          startCacheServer(this.cache);
          assignBuckets(this.cache);
          rebalance(this.cache);
        }
        finally {
          ProcessLauncherContext.remove();
        }
        
        debug("Running Server on (%1$s) in (%2$s) as (%2$s)...", getId(), getWorkingDirectory(), getMember());
        this.running.set(true);

        return new ServerState(this, Status.ONLINE);
      }
      catch (IOException e) {
        failOnStart(e);
        throw new RuntimeException(LocalizedStrings.Launcher_Command_START_IO_ERROR_MESSAGE.toLocalizedString(
          getServiceName(), getWorkingDirectory(), getId(), e.getMessage()), e);
      }
      catch (FileAlreadyExistsException e) {
        failOnStart(e);
        throw new RuntimeException(LocalizedStrings.Launcher_Command_START_PID_FILE_ALREADY_EXISTS_ERROR_MESSAGE
          .toLocalizedString(getServiceName(), getWorkingDirectory(), getId()), e);
      }
      catch (PidUnavailableException e) {
        failOnStart(e);
        throw new RuntimeException(LocalizedStrings.Launcher_Command_START_PID_UNAVAILABLE_ERROR_MESSAGE
          .toLocalizedString(getServiceName(), getId(), getWorkingDirectory(), e.getMessage()), e);
      }
      catch (Error e) {
        failOnStart(e);
        throw e;
      }
      catch (RuntimeException e) {
        failOnStart(e);
        throw e;
      }
      catch (Exception e) {
        failOnStart(e);
        throw new RuntimeException(e);
      }
      finally {
        this.starting.set(false);
      }
    }
    else {
      throw new IllegalStateException(LocalizedStrings.Launcher_Command_START_SERVICE_ALREADY_RUNNING_ERROR_MESSAGE
        .toLocalizedString(getServiceName(), getWorkingDirectory(), getId()));
    }
  }

  /**
   * A helper method to ensure the same sequence of actions are taken when the Server fails to start
   * caused by some exception.
   * 
   * @param cause the Throwable thrown during the startup operation on the Server.
   */
  private void failOnStart(final Throwable cause) {
    if (this.cache != null) {
      this.cache.close();
      this.cache = null;
    }
    if (this.process != null) {
      this.process.stop();
      this.process = null;
    }

    INSTANCE.compareAndSet(this, null);

    this.running.set(false);
  }

  /**
   * Determines whether the specified Cache has any CacheServers.
   * 
   * @param cache the Cache to check for existing CacheServers.
   * @return a boolean value indicating if any CacheServers were added to the Cache.
   */
  protected boolean isServing(final Cache cache) {
    return !cache.getCacheServers().isEmpty();
  }

  /**
   * Determines whether to continue waiting and keep the GemFire non-Server data member running.
   * 
   * @param cache the Cache associated with this GemFire (non-Server) data member.
   * @return a boolean value indicating whether the GemFire data member should continue running, as determined
   * by the running flag and a connection to the distributed system (GemFire cluster).
   */
  final boolean isWaiting(final Cache cache) {
    //return (isRunning() && !getCache().isClosed());
    return (isRunning() && cache.getDistributedSystem().isConnected());
  }

  /**
   * Causes the calling Thread to block until the GemFire Cache Server/Data Member stops.
   */
  public void waitOnServer() {
    assert getCache() != null : "The Cache Server must first be started with a call to start!";

    if (!isServing(getCache())) {
      Throwable cause = null;
      try {
        while (isWaiting(getCache())) {
          try {
            synchronized (this) {
              wait(500l);
            }
          }
          catch (InterruptedException ignore) {
          }
        }
      }
      catch (RuntimeException e) {
        cause = e;
        throw e;
      }
      finally {
        failOnStart(cause);
      }
    }
  }

  /**
   * Determines whether a default server (a cache server) should be created on startup as determined by the absence
   * of specifying the --disable-default-server command-line option (switch).  In addition, a default cache server
   * is started only if no cache servers have been added to the Cache by way of cache.xml.
   * 
   * @param cache the reference to the Cache to check for any existing cache servers.
   * @return a boolean indicating whether a default server should be added to the Cache.
   * @see #isDisableDefaultServer()
   */
  protected boolean isDefaultServerEnabled(final Cache cache) {
    return (cache.getCacheServers().isEmpty() && !isDisableDefaultServer());
  }

  /**
   * If the default server (cache server) has not been disabled and no prior cache servers were added to the cache,
   * then this method will add a cache server to the Cache and start the server Thread on the specified bind address
   * and port.
   * 
   * @param cache the Cache to which the server will be added.
   * @throws IOException if the Cache server fails to start due to IO error.
   */
  final void startCacheServer(final Cache cache) throws IOException {
    if (isDefaultServerEnabled(cache)) {
      final String serverBindAddress = (getServerBindAddress() == null ? null : getServerBindAddress().getHostAddress());
      final Integer serverPort = getServerPort();
      CacheServerLauncher.serverBindAddress.set(serverBindAddress);
      CacheServerLauncher.serverPort.set(serverPort);
      final CacheServer cacheServer = cache.addCacheServer();
      cacheServer.setBindAddress(serverBindAddress);
      cacheServer.setPort(serverPort);
      cacheServer.start();
    }
  }

  /**
   * Causes a rebalance operation to occur on the given Cache.
   * 
   * @param cache the reference to the Cache to rebalance.
   * @see com.gemstone.gemfire.cache.control.ResourceManager#createRebalanceFactory()
   */
  private final void rebalance(final Cache cache) {
    if (isRebalancing()) {
      cache.getResourceManager().createRebalanceFactory().start();
    }
  }

  /**
   * Determines whether the user indicated that buckets should be assigned on cache server start using the
   * --assign-buckets command-line option (switch) at the command-line as well as whether the option is technically
   * allowed.  The option is only allowed if the instance of the Cache is the internal GemFireCacheImpl at present.
   * 
   * @param cache the Cache reference to check for instance type.
   * @return a boolean indicating if bucket assignment is both enabled and allowed.
   * @see #isAssignBuckets()
   */
  protected boolean isAssignBucketsAllowed(final Cache cache) {
    return (isAssignBuckets() && (cache instanceof GemFireCacheImpl));
  }

  /**
   * Assigns buckets to individual Partitioned Regions of the Cache.
   * 
   * @param cache the Cache who's Partitioned Regions are accessed to assign buckets to.
   * @see PartitionRegionHelper#assignBucketsToPartitions(com.gemstone.gemfire.cache.Region)
   */
  final void assignBuckets(final Cache cache) {
    if (isAssignBucketsAllowed(cache)) {
      for (PartitionedRegion region : ((GemFireCacheImpl) cache).getPartitionedRegions()) {
        PartitionRegionHelper.assignBucketsToPartitions(region);
      }
    }
  }

  /**
   * Determines whether the Server is the process of starting or is already running.
   * 
   * @return a boolean indicating if the Server is starting or is already running.
   */
  protected boolean isStartingOrRunning() {
    return (this.starting.get() || isRunning());
  }

  /**
   * Invokes the 'status' command and operation to check the status of a GemFire server (a cache server).
   */
  public ServerState status() {
    final ServerLauncher launcher = getInstance();
    // if this instance is running then return local status
    if (isStartingOrRunning()) {
      debug("Getting status from the ServerLauncher instance that actually launched the GemFire Cache Server.%n");
      return new ServerState(this, (isRunning() ? Status.ONLINE : Status.STARTING));
    }
    else if (isPidInProcess() && launcher != null) {
      return launcher.statusInProcess();
    }
    else if (getPid() != null) {
      debug("Getting Server status using process ID (%1$s)%n", getPid());
      return statusWithPid();
    }
    // attempt to get status using workingDirectory
    else if (getWorkingDirectory() != null) {
      debug("Getting Server status using working directory (%1$s)%n", getWorkingDirectory());
      return statusWithWorkingDirectory();
    }

    debug("This ServerLauncher was not the instance used to launch the GemFire Cache Server, and neither PID "
      .concat("nor working directory were specified; the Server's state is unknown.%n"));

    return new ServerState(this, Status.NOT_RESPONDING);
  }
  
  private ServerState statusInProcess() {
    if (isStartingOrRunning()) {
      debug("Getting status from the ServerLauncher instance that actually launched the GemFire Cache Server.%n");
      return new ServerState(this, (isRunning() ? Status.ONLINE : Status.STARTING));
    } else {
      return new ServerState(this, Status.NOT_RESPONDING);
    }
  }
  
  private ServerState statusWithPid() {
    try {
      final ProcessController controller = new ProcessControllerFactory().createProcessController(this.controllerParameters, getPid());
      controller.checkPidSupport();
      final String statusJson = controller.status();
      return ServerState.fromJson(statusJson);
    }
    catch (ConnectionFailedException e) {
      // failed to attach to server JVM
      return createNoResponseState(e, "Failed to connect to server with process id " + getPid());
    } 
    catch (IOException e) {
      // failed to open or read file or dir
      return createNoResponseState(e, "Failed to communicate with server with process id " + getPid());
    } 
    catch (MBeanInvocationFailedException e) {
      // MBean either doesn't exist or method or attribute don't exist
      return createNoResponseState(e, "Failed to communicate with server with process id " + getPid());
    } 
    catch (UnableToControlProcessException e) {
      // TODO comment me
      return createNoResponseState(e, "Failed to communicate with server with process id " + getPid());
    } 
    catch (InterruptedException e) {
      // TODO comment me
      return createNoResponseState(e, "Failed to communicate with server with process id " + getPid());
    } 
    catch (TimeoutException e) {
      // TODO comment me
      return createNoResponseState(e, "Failed to communicate with server with process id " + getPid());
    }
  }

  private ServerState statusWithWorkingDirectory() {
    int parsedPid = 0;
    try {
      final ProcessController controller = new ProcessControllerFactory().createProcessController(this.controllerParameters, new File(getWorkingDirectory()), ProcessType.SERVER.getPidFileName());
      parsedPid = controller.getProcessId();
      
      // note: in-process request will go infinite loop unless we do the following
      if (parsedPid == identifyPid()) {
        final ServerLauncher runningLauncher = getInstance();
        if (runningLauncher != null) {
          return runningLauncher.statusInProcess();
        }
      }

      final String statusJson = controller.status();
      return ServerState.fromJson(statusJson);
    }
    catch (ConnectionFailedException e) {
      // failed to attach to server JVM
      return createNoResponseState(e, "Failed to connect to server with process id " + parsedPid);
    } 
    catch (FileNotFoundException e) {
      // could not find pid file
      return createNoResponseState(e, "Failed to find process file " + ProcessType.SERVER.getPidFileName() + " in " + getWorkingDirectory());
    } 
    catch (IOException e) {
      // failed to open or read file or dir
      return createNoResponseState(e, "Failed to communicate with server with process id " + parsedPid);
    } 
    catch (MBeanInvocationFailedException e) {
      // MBean either doesn't exist or method or attribute don't exist
      return createNoResponseState(e, "Failed to communicate with server with process id " + parsedPid);
    } 
    catch (PidUnavailableException e) {
      // couldn't determine pid from within server JVM
      return createNoResponseState(e, "Failed to find usable process id within file " + ProcessType.SERVER.getPidFileName() + " in " + getWorkingDirectory());
    } 
    catch (UnableToControlProcessException e) {
      // TODO comment me
      return createNoResponseState(e, "Failed to communicate with server with process id " + parsedPid);
    } 
    catch (InterruptedException e) {
      // TODO comment me
      return createNoResponseState(e, "Failed to communicate with server with process id " + parsedPid);
    } 
    catch (TimeoutException e) {
      // TODO comment me
      return createNoResponseState(e, "Failed to communicate with server with process id " + parsedPid);
    }
  }

  /**
   * Determines whether the Server can be stopped in-process, such as when a Server is embedded in an application
   * and the ServerLauncher API is being used.
   * 
   * @return a boolean indicating whether the Server can be stopped in-process (the application's process with
   * an embedded Server).
   */
  private boolean isStoppable() {
    return (isRunning() && getCache() != null);
  }

  /**
   * Invokes the 'stop' command and operation to stop a GemFire server (a cache server).
   */
  public ServerState stop() {
    final ServerLauncher launcher = getInstance();
    // if this instance is running then stop it
    if (isStoppable()) {
      return stopInProcess();
    }
    // if in-process but difference instance of ServerLauncher
    else if (isPidInProcess() && launcher != null) {
      return launcher.stopInProcess();
    }
    // attempt to stop using pid if provided
    else if (getPid() != null) {
      return stopWithPid();
    }
    // attempt to stop using workingDirectory
    else if (getWorkingDirectory() != null) {
      return stopWithWorkingDirectory();
    }

    // TODO give user detailed error message?
    return new ServerState(this, Status.NOT_RESPONDING);
  }
  
  private ServerState stopInProcess() {
    if (isStoppable()) {
      this.cache.close();
      this.cache = null;
      this.process.stop();
      this.process = null;
      INSTANCE.compareAndSet(this, null); // note: other thread may return Status.NOT_RESPONDING now
      this.running.set(false);
      return new ServerState(this, Status.STOPPED);
    } else {
      return new ServerState(this, Status.NOT_RESPONDING);
    }
  }

  private ServerState stopWithPid() {
    try {
      final ProcessController controller = new ProcessControllerFactory().createProcessController(this.controllerParameters, getPid());
      controller.checkPidSupport();
      controller.stop();
      return new ServerState(this, Status.STOPPED);
    }
    catch (ConnectionFailedException e) {
      // failed to attach to server JVM
      return createNoResponseState(e, "Failed to connect to server with process id " + getPid());
    } 
    catch (IOException e) {
      // failed to open or read file or dir
      return createNoResponseState(e, "Failed to communicate with server with process id " + getPid());
    } 
    catch (MBeanInvocationFailedException e) {
      // MBean either doesn't exist or method or attribute don't exist
      return createNoResponseState(e, "Failed to communicate with server with process id " + getPid());
    } 
    catch (UnableToControlProcessException e) {
      // TODO comment me
      return createNoResponseState(e, "Failed to communicate with server with process id " + getPid());
    }
  }

  private ServerState stopWithWorkingDirectory() {
    int parsedPid = 0;
    try {
      final ProcessController controller = new ProcessControllerFactory().createProcessController(this.controllerParameters, new File(getWorkingDirectory()), ProcessType.SERVER.getPidFileName());
      parsedPid = controller.getProcessId();
      
      // NOTE in-process request will go infinite loop unless we do the following
      if (parsedPid == identifyPid()) {
        final ServerLauncher runningLauncher = getInstance();
        if (runningLauncher != null) {
          return runningLauncher.stopInProcess();
        }
      }
      
      controller.stop();
      return new ServerState(this, Status.STOPPED);
    }
    catch (ConnectionFailedException e) {
      // failed to attach to server JVM
      return createNoResponseState(e, "Failed to connect to server with process id " + parsedPid);
    } 
    catch (FileNotFoundException e) {
      // could not find pid file
      return createNoResponseState(e, "Failed to find process file " + ProcessType.SERVER.getPidFileName() + " in " + getWorkingDirectory());
    } 
    catch (IOException e) {
      // failed to open or read file or dir
      return createNoResponseState(e, "Failed to communicate with server with process id " + parsedPid);
    } 
    catch (MBeanInvocationFailedException e) {
      // MBean either doesn't exist or method or attribute don't exist
      return createNoResponseState(e, "Failed to communicate with server with process id " + parsedPid);
    } 
    catch (PidUnavailableException e) {
      // couldn't determine pid from within server JVM
      return createNoResponseState(e, "Failed to find usable process id within file " + ProcessType.SERVER.getPidFileName() + " in " + getWorkingDirectory());
    } 
    catch (UnableToControlProcessException e) {
      // TODO comment me
      return createNoResponseState(e, "Failed to communicate with server with process id " + parsedPid);
    }
  }

  private ServerState createNoResponseState(final Exception cause, final String errorMessage) {
    debug(cause);
    //info(errorMessage);
    return new ServerState(this, Status.NOT_RESPONDING); // TODO: use errorMessage
  }

  private Properties getOverriddenDefaults() {
    final Properties overriddenDefaults = new Properties();
    
    overriddenDefaults.put(
      ProcessLauncherContext.OVERRIDDEN_DEFAULTS_PREFIX.concat(DistributionConfig.LOG_FILE_NAME), 
      getLogFileName());

    for (String key : System.getProperties().stringPropertyNames()) {
      if (key.startsWith(ProcessLauncherContext.OVERRIDDEN_DEFAULTS_PREFIX)) {
        overriddenDefaults.put(key, System.getProperty(key));
      }
    }

    return overriddenDefaults;
  }
  
  private class ServerControllerParameters implements ProcessControllerParameters {
    @Override
    public File getPidFile() {
      return getServerPidFile();
    }
  
    @Override
    public File getWorkingDirectory() {
      return new File(ServerLauncher.this.getWorkingDirectory());
    }
  
    @Override
    public int getProcessId() {
      return getPid();
    }
  
    @Override
    public ProcessType getProcessType() {
      return ProcessType.SERVER;
    }
  
    @Override
    public ObjectName getNamePattern() {
      try {
        return ObjectName.getInstance("GemFire:type=Member,*");
      } catch (MalformedObjectNameException e) {
        return null;
      } catch (NullPointerException e) {
        return null;
      }
    }
  
    @Override
    public String getPidAttribute() {
      return "ProcessId";
    }
  
    @Override
    public String getStopMethod() {
      return "shutDownMember";
    }
    
    @Override
    public String getStatusMethod() {
      return "status";
    }
  
    @Override
    public String[] getAttributes() {
      return new String[] {"Server"};
    }
  
    @Override
    public Object[] getValues() {
      return new Object[] {Boolean.TRUE};
    }
  }

  /**
   * The Builder class, modeled after the Builder creational design pattern, is used to construct a properly configured
   * and initialized instance of the ServerLauncher to control and run GemFire servers (in particular, cache servers).
   */
  public static class Builder {

    protected static final Command DEFAULT_COMMAND = Command.UNSPECIFIED;

    private boolean serverBindAddressSetByUser;
    private boolean serverPortSetByUser;

    private Boolean assignBuckets;
    private Boolean debug;
    private Boolean disableDefaultServer;
    private Boolean force;
    private Boolean help;
    private Boolean rebalance;
    private Boolean redirectOutput;

    private Cache cache;

    private Command command;

    private InetAddress serverBindAddress;

    private Integer pid;
    private Integer serverPort;

    private final Properties distributedSystemProperties = new Properties();

    private String memberName;
    private String workingDirectory;

    /**
     * Default constructor used to create an instance of the Builder class for programmatical access.
     */
    public Builder() {
    }

    /**
     * Constructor used to create and configure an instance of the Builder class with the specified arguments, passed in
     * from the command-line when launching an instance of this class from the command-line using the Java launcher.
     * 
     * @param args the array of arguments used to configure the Builder.
     * @see #parseArguments(String...)
     */
    public Builder(final String... args) {
      parseArguments(args != null ? args : new String[0]);
    }

    /**
     * Gets an instance of the JOptSimple OptionParser to parse the command-line arguments for Server.
     * 
     * @return an instance of the JOptSimple OptionParser configured with the command-line options used by the Server.
     */
    private OptionParser getParser() {
      final OptionParser parser = new OptionParser(true);

      parser.accepts("assign-buckets");
      parser.accepts("debug");
      parser.accepts("dir").withRequiredArg().ofType(String.class);
      parser.accepts("disable-default-server");
      parser.accepts("force");
      parser.accepts("help");
      parser.accepts("member").withRequiredArg().ofType(String.class);
      parser.accepts("pid").withRequiredArg().ofType(Integer.class);
      parser.accepts("rebalance");
      parser.accepts("redirect-output");
      parser.accepts("server-bind-address").withRequiredArg().ofType(String.class);
      parser.accepts("server-port").withRequiredArg().ofType(Integer.class);
      parser.accepts("version");

      return parser;
    }

    /**
     * Parses the list of arguments to configure this Builder with the intent of constructing a Server launcher to
     * invoke a Cache Server.  This method is called to parse the arguments specified by the user on the command-line.
     * 
     * @param args the array of arguments used to configure this Builder and create an instance of ServerLauncher.
     */
    protected void parseArguments(final String... args) {
      try {
        final OptionSet options = getParser().parse(args);

        parseCommand(args);
        parseMemberName(args);

        setAssignBuckets(options.has("assign-buckets"));
        setDebug(options.has("debug"));
        setDisableDefaultServer(options.has("disable-default-server"));
        setForce(options.has("force"));
        setHelp(options.has("help"));
        setRebalance(options.has("rebalance"));
        setRedirectOutput(options.has("redirect-output"));

        if (!isHelping()) {
          if (options.has("dir")) {
            setWorkingDirectory(ObjectUtils.toString(options.valueOf("dir")));
          }

          if (options.has("pid")) {
            setPid((Integer) options.valueOf("pid"));
          }

          if (options.has("server-bind-address")) {
            setServerBindAddress(ObjectUtils.toString(options.valueOf("server-bind-address")));
          }

          if (options.has("server-port")) {
            setServerPort((Integer) options.valueOf("server-port"));
          }

          if (options.has("version")) {
            setCommand(Command.VERSION);
          }
        }
      }
      catch (OptionException e) {
        throw new IllegalArgumentException(LocalizedStrings.Launcher_Builder_PARSE_COMMAND_LINE_ARGUMENT_ERROR_MESSAGE
          .toLocalizedString("Server", e.getMessage()), e);
      }
      catch (Exception e) {
        throw new RuntimeException(e.getMessage(), e);
      }
    }

    /**
     * Iterates the list of arguments in search of the target Server launcher command.
     * 
     * @param args an array of arguments from which to search for the Server launcher command.
     * @see com.gemstone.gemfire.distributed.ServerLauncher.Command#valueOfName(String)
     * @see #parseArguments(String...)
     */
    protected void parseCommand(final String... args) {
      if (args != null) {
        for (final String arg : args) {
          final Command command = Command.valueOfName(arg);
          if (command != null) {
            setCommand(command);
            break;
          }
        }
      }
    }

    /**
     * Iterates the list of arguments in search of the Server's GemFire member name.  If the argument does not
     * start with '-' or is not the name of a Server launcher command, then the value is presumed to be the member name
     * for the Server in GemFire.
     * 
     * @param args the array of arguments from which to search for the Server's member name in GemFire.
     * @see com.gemstone.gemfire.distributed.ServerLauncher.Command#isCommand(String)
     * @see #parseArguments(String...)
     */
    protected void parseMemberName(final String... args) {
      if (args != null) {
        for (final String arg : args) {
          if (!(arg.startsWith(OPTION_PREFIX) || Command.isCommand(arg))) {
            setMemberName(arg);
            break;
          }
        }
      }
    }

    /**
     * Gets the Server launcher command used during the invocation of the ServerLauncher.
     * 
     * @return the Server launcher command used to invoke (run) the ServerLauncher class.
     * @see #setCommand(com.gemstone.gemfire.distributed.ServerLauncher.Command)
     * @see com.gemstone.gemfire.distributed.ServerLauncher.Command
     */
    public Command getCommand() {
      return ObjectUtils.defaultIfNull(this.command, DEFAULT_COMMAND);
    }

    /**
     * Sets the Sever launcher command used during the invocation of the ServerLauncher
     * 
     * @param command the targeted Server launcher command used during the invocation (run) of ServerLauncher.
     * @return this Builder instance.
     * @see #getCommand()
     * @see com.gemstone.gemfire.distributed.ServerLauncher.Command
     */
    public Builder setCommand(final Command command) {
      this.command = command;
      return this;
    }

    /**
     * Determines whether buckets should be assigned to partitioned regions in the cache upon Server start.
     * 
     * @return a boolean indicating if buckets should be assigned upon Server start.
     * @see #setAssignBuckets(Boolean)
     */
    public Boolean getAssignBuckets() {
      return this.assignBuckets;
    }

    /**
     * Sets whether buckets should be assigned to partitioned regions in the cache upon Server start.
     * 
     * @param assignBuckets a boolean indicating if buckets should be assigned upon Server start.
     * @return this Builder instance.
     * @see #getAssignBuckets()
     */
    public Builder setAssignBuckets(final Boolean assignBuckets) {
      this.assignBuckets = assignBuckets;
      return this;
    }

    // For testing purposes only!
    Cache getCache() {
      return this.cache;
    }

    // For testing purposes only!
    Builder setCache(final Cache cache) {
      this.cache = cache;
      return this;
    }

    /**
     * Determines whether the new instance of the ServerLauncher will be set to debug mode.
     * 
     * @return a boolean value indicating whether debug mode is enabled or disabled.
     * @see #setDebug(Boolean)
     */
    public Boolean getDebug() {
      return this.debug;
    }

    /**
     * Sets whether the new instance of the ServerLauncher will be set to debug mode.
     * 
     * @param debug a boolean value indicating whether debug mode is to be enabled or disabled.
     * @return this Builder instance.
     * @see #getDebug()
     */
    public Builder setDebug(final Boolean debug) {
      this.debug = debug;
      return this;
    }

    /**
     * Determines whether a default cache server will be added when the GemFire Server comes online.
     * 
     * @return a boolean value indicating whether to add a default cache server.
     * @see #setDisableDefaultServer(Boolean)
     */
    public Boolean getDisableDefaultServer() {
      return this.disableDefaultServer;
    }

    /**
     * Sets a boolean value indicating whether to add a default cache when the GemFire Server comes online.
     * 
     * @param disableDefaultServer a boolean value indicating whether to add a default cache server.
     * @return this Builder instance.
     * @see #getDisableDefaultServer()
     */
    public Builder setDisableDefaultServer(final Boolean disableDefaultServer) {
      this.disableDefaultServer = disableDefaultServer;
      return this;
    }

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

    /**
     * Gets the boolean value used by the Server to determine if it should overwrite the PID file if it already exists.
     * 
     * @return the boolean value specifying whether or not to overwrite the PID file if it already exists.
     * @see com.gemstone.gemfire.internal.process.LocalProcessLauncher
     * @see #setForce(Boolean)
     */
    public Boolean getForce() {
      return ObjectUtils.defaultIfNull(this.force, DEFAULT_FORCE);
    }

    /**
     * Sets the boolean value used by the Server to determine if it should overwrite the PID file if it already exists.
     * 
     * @param force a boolean value indicating whether to overwrite the PID file when it already exists.
     * @return this Builder instance.
     * @see com.gemstone.gemfire.internal.process.LocalProcessLauncher
     * @see #getForce()
     */
    public Builder setForce(final Boolean force) {
      this.force = force;
      return this;
    }

    /**
     * Determines whether the new instance of the ServerLauncher will be used to output help information for either
     * a specific command, or for using ServerLauncher in general.
     * 
     * @return a boolean value indicating whether help will be output during the invocation of the ServerLauncher.
     * @see #setHelp(Boolean)
     */
    public Boolean getHelp() {
      return this.help;
    }

    /**
     * Determines whether help has been enabled.
     * 
     * @return a boolean indicating if help was enabled.
     */
    protected final boolean isHelping() {
      return Boolean.TRUE.equals(getHelp());
    }

    /**
     * Sets whether the new instance of ServerLauncher will be used to output help information for either a specific
     * command, or for using ServerLauncher in general.
     * 
     * @param help a boolean indicating whether help information is to be displayed during invocation of ServerLauncher.
     * @return this Builder instance.
     * @see #getHelp()
     */
    public Builder setHelp(final Boolean help) {
      this.help = help;
      return this;
    }

    /**
     * Determines whether a rebalance operation on the cache will occur upon starting the GemFire server.
     * 
     * @return a boolean indicating if the cache will be rebalance when the GemFire server starts.
     * @see #setRebalance(Boolean)
     */
    public Boolean getRebalance() {
      return this.rebalance;
    }

    /**
     * Set a boolean value indicating whether a rebalance operation on the cache should occur upon starting
     * the GemFire server.
     * 
     * @param rebalance a boolean indicating if the cache will be rebalanced when the GemFire server starts.
     * @return this Builder instance.
     * @see #getRebalance()
     */
    public Builder setRebalance(final Boolean rebalance) {
      this.rebalance = rebalance;
      return this;
    }

    /**
     * Gets the member name of this Server in GemFire.
     * 
     * @return a String indicating the member name of this Server in GemFire.
     * @see #setMemberName(String)
     */
    public String getMemberName() {
      return this.memberName;
    }

    /**
     * Sets the member name of the Server in GemFire.
     * 
     * @param memberName a String indicating the member name of this Server in GemFire.
     * @return this Builder instance.
     * @throws IllegalArgumentException if the member name is invalid.
     * @see #getMemberName()
     */
    public Builder setMemberName(final String memberName) {
      if (StringUtils.isEmpty(StringUtils.trim(memberName))) {
        throw new IllegalArgumentException(LocalizedStrings.Launcher_Builder_MEMBER_NAME_ERROR_MESSAGE
          .toLocalizedString("Server"));
      }
      this.memberName = memberName;
      return this;
    }

    /**
     * Gets the process ID (PID) of the running Server indicated by the user as an argument to the ServerLauncher.
     * This PID is used by the Server launcher to determine the Server's status, or invoke shutdown on the Server.
     * 
     * @return a user specified Integer value indicating the process ID of the running Server.
     * @see #setPid(Integer)
     */
    public Integer getPid() {
      return this.pid;
    }

    /**
     * Sets the process ID (PID) of the running Server indicated by the user as an argument to the ServerLauncher.
     * This PID will be used by the Server launcher to determine the Server's status, or invoke shutdown on the Server.
     * 
     * @param pid a user specified Integer value indicating the process ID of the running Server.
     * @return this Builder instance.
     * @throws IllegalArgumentException if the process ID (PID) is not valid (greater than zero if not null).
     * @see #getPid()
     */
    public Builder setPid(final Integer pid) {
      if (pid != null && pid < 0) {
        throw new IllegalArgumentException(LocalizedStrings.Launcher_Builder_PID_ERROR_MESSAGE.toLocalizedString());
      }
      this.pid = pid;
      return this;
    }

    /**
     * Determines whether the new instance of LocatorLauncher will redirect
     * output to system logs when starting a Locator.
     * 
     * @return a boolean value indicating if output will be redirected to system 
     * logs when starting a Locator
     * 
     * @see #setRedirectOutput(Boolean)
     */
    public Boolean getRedirectOutput() {
      return this.redirectOutput;
    }

    /**
     * Determines whether redirecting of output has been enabled.
     * 
     * @return a boolean indicating if redirecting of output was enabled.
     */
    private boolean isRedirectingOutput() {
      return Boolean.TRUE.equals(getRedirectOutput());
    }

    /**
     * Sets whether the new instance of LocatorLauncher will redirect output to system logs when starting a Locator.
     * 
     * @param redirectOutput a boolean value indicating if output will be redirected to system logs when starting
     * a Locator.
     * @return this Builder instance.
     * @see #getRedirectOutput()
     */
    public Builder setRedirectOutput(final Boolean redirectOutput) {
      this.redirectOutput = redirectOutput;
      return this;
    }

    /**
     * Gets the IP address to which the Server will be bound listening for and accepting cache client connections in
     * a client/server topology.
     * 
     * @return an InetAddress indicating the IP address that the Server is bound to listening for and accepting cache
     * client connections in a client/server topology.
     * @see #setServerBindAddress(String)
     */
    public InetAddress getServerBindAddress() {
      return this.serverBindAddress;
    }
    
    boolean isServerBindAddressSetByUser() {
      return this.serverBindAddressSetByUser;
    }

    /**
     * Sets the IP address to which the Server will be bound listening for and accepting cache client connections in
     * a client/server topology.
     * 
     * @param serverBindAddress a String specifying the IP address or hostname that the Server will be bound to listen
     * for and accept cache client connections in a client/server topology.
     * @return this Builder instance.
     * @throws IllegalArgumentException wrapping the UnknownHostException if the IP address or hostname for the
     * server bind address is unknown.
     * @see #getServerBindAddress()
     */
    public Builder setServerBindAddress(final String serverBindAddress) {
      if (StringUtils.isBlank(serverBindAddress)) {
        this.serverBindAddress = null;
        return this;
      }
      // NOTE only set the 'bind address' if the user specified a value
      else {
        try {
          this.serverBindAddress = InetAddress.getByName(serverBindAddress);
          this.serverBindAddressSetByUser = true;
          return this;
        }
        catch (UnknownHostException e) {
          throw new IllegalArgumentException(LocalizedStrings.Launcher_Builder_UNKNOWN_HOST_ERROR_MESSAGE
            .toLocalizedString("Server"), e);
        }
      }
    }

    /**
     * Gets the port on which the Server will listen for and accept cache client connections in a client/server topology.
     * 
     * @return an Integer value specifying the port the Server will listen on and accept cache client connections in
     * a client/server topology.
     * @see #setServerPort(Integer)
     */
    public Integer getServerPort() {
      return ObjectUtils.defaultIfNull(this.serverPort, getDefaultServerPort());
    }
    
    boolean isServerPortSetByUser() {
      return this.serverPortSetByUser;
    }

    /**
     * Sets the port on which the Server will listen for and accept cache client connections in a client/server topology.
     * 
     * @param serverPort an Integer value specifying the port the Server will listen on and accept cache client
     * connections in a client/server topology.
     * @return this Builder instance.
     * @throws IllegalArgumentException if the port number is not valid.
     * @see #getServerPort()
     */
    public Builder setServerPort(final Integer serverPort) {
      if (serverPort != null && (serverPort < 0 || serverPort > 65535)) {
        throw new IllegalArgumentException(LocalizedStrings.Launcher_Builder_INVALID_PORT_ERROR_MESSAGE
          .toLocalizedString("Server"));
      }
      this.serverPort = serverPort;
      this.serverPortSetByUser = true;
      return this;
    }

    /**
     * Gets the working directory pathname in which the Server will be ran.  If the directory is unspecified,
     * then working directory defaults to the current directory.
     * 
     * @return a String indicating the working directory pathname.
     * @see #setWorkingDirectory(String)
     */
    public String getWorkingDirectory() {
      return IOUtils.tryGetCanonicalPathElseGetAbsolutePath(
        new File(StringUtils.defaultIfBlank(this.workingDirectory, DEFAULT_WORKING_DIRECTORY)));
    }

    /**
     * Sets the working directory in which the Server will be ran.  This also the directory in which all Server files
     * (such as log and license files) will be written.  If the directory is unspecified, then the working directory
     * defaults to the current directory.
     * 
     * @param workingDirectory a String indicating the pathname of the directory in which the Server will be ran.
     * @return this Builder instance.
     * @throws IllegalArgumentException wrapping a FileNotFoundException if the working directory pathname cannot be
     * found.
     * @see #getWorkingDirectory()
     * @see java.io.FileNotFoundException
     */
    public Builder setWorkingDirectory(final String workingDirectory) {
      if (!(new File(StringUtils.defaultIfBlank(workingDirectory, DEFAULT_WORKING_DIRECTORY)).isDirectory())) {
        throw new IllegalArgumentException(
          LocalizedStrings.Launcher_Builder_WORKING_DIRECTORY_NOT_FOUND_ERROR_MESSAGE.toLocalizedString("Server"),
            new FileNotFoundException(workingDirectory));
      }
      this.workingDirectory = workingDirectory;
      return this;
    }

    /**
     * Sets a GemFire Distributed System Property.
     *
     * @param propertyName a String indicating the name of the GemFire Distributed System property.
     * @param propertyValue a String value for the GemFire Distributed System property.
     * @return this Builder instance.
     */
    public Builder set(final String propertyName, final String propertyValue) {
      this.distributedSystemProperties.setProperty(propertyName, propertyValue);
      return this;
    }

    /**
     * Validates the configuration settings and properties of this Builder, ensuring that all invariants have been met.
     * Currently, the only invariant constraining the Builder is that the user must specify the member name for the
     * Server in the GemFire distributed system as a command-line argument, or by setting the memberName property
     * programmatically using the corresponding setter method.
     * 
     * @throws IllegalStateException if the Builder is not properly configured.
     */
    protected void validate() {
      if (!isHelping()) {
        validateOnStart();
        validateOnStatus();
        validateOnStop();
      }
    }

    /**
     * Validates the arguments passed to the Builder when the 'start' command has been issued.
     * 
     * @see com.gemstone.gemfire.distributed.ServerLauncher.Command#START
     */
    protected void validateOnStart() {
      if (Command.START.equals(getCommand())) {
        if (StringUtils.isBlank(getMemberName())
          && !isSet(System.getProperties(), DistributionConfig.GEMFIRE_PREFIX + DistributionConfig.NAME_NAME)
          && !isSet(getDistributedSystemProperties(), DistributionConfig.NAME_NAME)
          && !isSet(loadGemFireProperties(DistributedSystem.getPropertyFileURL()), DistributionConfig.NAME_NAME))
        {
          throw new IllegalStateException(LocalizedStrings.Launcher_Builder_MEMBER_NAME_VALIDATION_ERROR_MESSAGE
            .toLocalizedString("Server"));
        }

        if (!SystemUtils.CURRENT_DIRECTORY.equals(getWorkingDirectory())) {
          throw new IllegalStateException(LocalizedStrings.Launcher_Builder_WORKING_DIRECTORY_OPTION_NOT_VALID_ERROR_MESSAGE
            .toLocalizedString("Server"));
        }
      }
    }

    /**
     * Validates the arguments passed to the Builder when the 'status' command has been issued.
     * 
     * @see com.gemstone.gemfire.distributed.ServerLauncher.Command#STATUS
     */
    protected void validateOnStatus() {
      if (Command.STATUS.equals(getCommand())) {
        // TODO implement (if required)
      }
    }

    /**
     * Validates the arguments passed to the Builder when the 'stop' command has been issued.
     * 
     * @see com.gemstone.gemfire.distributed.ServerLauncher.Command#STOP
     */
    protected void validateOnStop() {
      if (Command.STOP.equals(getCommand())) {
        // TODO implement (if required)
      }
    }

    /**
     * Validates the Builder configuration settings and then constructs an instance of the ServerLauncher class
     * to invoke operations on a GemFire Server.
     * 
     * @return a newly constructed instance of the ServerLauncher configured with this Builder.
     * @see #validate()
     * @see com.gemstone.gemfire.distributed.ServerLauncher
     */
    public ServerLauncher build() {
      validate();
      return new ServerLauncher(this);
    }
  }

  /**
   * An enumerated type representing valid commands to the Server launcher.
   */
  public static enum Command {
    START("start", "assign-buckets", "disable-default-server", "rebalance", "server-bind-address", "server-port", "force", "debug", "help"),
    STATUS("status", "member", "pid", "dir", "debug", "help"),
    STOP("stop", "member", "pid", "dir", "debug", "help"),
    UNSPECIFIED("unspecified"),
    VERSION("version");

    private final List options;

    private final String name;

    Command(final String name, final String... options) {
      assert !StringUtils.isBlank(name) : "The name of the command must be specified!";
      this.name = name;
      this.options = (options != null ? Collections.unmodifiableList(Arrays.asList(options))
        : Collections.emptyList());
    }

    /**
     * Determines whether the specified name refers to a valid Server launcher command, as defined by this
     * enumerated type.
     * 
     * @param name a String value indicating the potential name of a Server launcher command.
     * @return a boolean indicating whether the specified name for a Server launcher command is valid.
     */
    public static boolean isCommand(final String name) {
      return (valueOfName(name) != null);
    }

    /**
     * Determines whether the given Server launcher command has been properly specified.  The command is deemed
     * unspecified if the reference is null or the Command is UNSPECIFIED.
     * 
     * @param command the Server launcher command.
     * @return a boolean value indicating whether the Server launcher command is unspecified.
     * @see Command#UNSPECIFIED
     */
    public static boolean isUnspecified(final Command command) {
      return (command == null || command.isUnspecified());
    }

    /**
     * Looks up a Server launcher command by name.  The equality comparison on name is case-insensitive.
     * 
     * @param name a String value indicating the name of the Server launcher command.
     * @return an enumerated type representing the command name or null if the no such command with the specified name
     * exists.
     */
    public static Command valueOfName(final String name) {
      for (final Command command : values()) {
        if (command.getName().equalsIgnoreCase(name)) {
          return command;
        }
      }

      return null;
    }

    /**
     * Gets the name of the Server launcher command.
     * 
     * @return a String value indicating the name of the Server launcher command.
     */
    public String getName() {
      return this.name;
    }

    /**
     * Gets a set of valid options that can be used with the Locator launcher command when used from the command-line.
     * 
     * @return a Set of Strings indicating the names of the options available to the Server launcher command.
     */
    public List getOptions() {
      return this.options;
    }

    /**
     * Determines whether this Locator launcher command has the specified command-line option.
     * 
     * @param option a String indicating the name of the command-line option to this command.
     * @return a boolean value indicating whether this command has the specified named command-line option.
     */
    public boolean hasOption(final String option) {
      return getOptions().contains(StringUtils.toLowerCase(option));
    }

    /**
     * Convenience method for determining whether this is the UNSPECIFIED Server launcher command.
     * 
     * @return a boolean indicating if this command is UNSPECIFIED.
     * @see #UNSPECIFIED
     */
    public boolean isUnspecified() {
      return (this == UNSPECIFIED);
    }

    /**
     * Gets the String representation of this Server launcher command.
     * 
     * @return a String value representing this Server launcher command.
     */
    @Override
    public String toString() {
      return getName();
    }
  }

  /**
   * The ServerState is an immutable type representing the state of the specified Locator at any given moment in time.
   * The state of the Locator is assessed at the exact moment an instance of this class is constructed.
   * 
   * @see com.gemstone.gemfire.distributed.AbstractLauncher.ServiceState
   */
  public static final class ServerState extends ServiceState {

    /**
     * Unmarshals a ServerState instance from the JSON String.
     * 
     * @return a ServerState value unmarshalled from the JSON String.
     */
    public static ServerState fromJson(final String json) {
      try {
        final GfJsonObject gfJsonObject = new GfJsonObject(json);

        final Status status = Status.valueOfDescription(gfJsonObject.getString(JSON_STATUS));
        final List jvmArguments = Arrays.asList(GfJsonArray.toStringArray(gfJsonObject.getJSONArray(
          JSON_JVMARGUMENTS)));

        return new ServerState(status,
          gfJsonObject.getString(JSON_STATUSMESSAGE),
          gfJsonObject.getLong(JSON_TIMESTAMP),
          gfJsonObject.getString(JSON_LOCATION),
          gfJsonObject.getInt(JSON_PID),
          gfJsonObject.getLong(JSON_UPTIME),
          gfJsonObject.getString(JSON_WORKINGDIRECTORY),
          jvmArguments,
          gfJsonObject.getString(JSON_CLASSPATH),
          gfJsonObject.getString(JSON_GEMFIREVERSION),
          gfJsonObject.getString(JSON_JAVAVERSION),
          gfJsonObject.getString(JSON_LOGFILE),
          gfJsonObject.getString(JSON_HOST),
          gfJsonObject.getString(JSON_PORT),
          gfJsonObject.getString(JSON_MEMBERNAME));
      }
      catch (GfJsonException e) {
        // TODO: or should we return OFFLINE?
        throw new IllegalArgumentException("Unable to create ServerStatus from JSON: " + json);
      }
    }

    public ServerState(final ServerLauncher launcher, final Status status) {
      this(status,
        launcher.statusMessage,
        System.currentTimeMillis(),
        launcher.getId(),
        identifyPid(),
        ManagementFactory.getRuntimeMXBean().getUptime(),
        launcher.getWorkingDirectory(),
        ManagementFactory.getRuntimeMXBean().getInputArguments(),
        System.getProperty("java.class.path"),
        GemFireVersion.getGemFireVersion(),
        System.getProperty("java.version"),
        getServerLogFileCanonicalPath(launcher),
        getServerBindAddressAsString(launcher),
        getServerPortAsString(launcher),
        launcher.getMemberName());
    }

    protected ServerState(final Status status,
                          final String statusMessage,
                          final long timestamp,
                          final String serverLocation,
                          final Integer pid,
                          final Long uptime,
                          final String workingDirectory,
                          final List jvmArguments,
                          final String classpath,
                          final String gemfireVersion,
                          final String javaVersion,
                          final String logFile,
                          final String host,
                          final String port,
                          final String memberName)
    {
      super(status, statusMessage, timestamp, serverLocation, pid, uptime, workingDirectory, jvmArguments, classpath,
        gemfireVersion, javaVersion, logFile, host, port, memberName);
    }

    private static String getServerLogFileCanonicalPath(final ServerLauncher launcher) {
      final InternalDistributedSystem system = InternalDistributedSystem.getAnyInstance();

      if (system != null) {
        final File logFile = system.getConfig().getLogFile();
        if (logFile != null && logFile.isFile()) {
          final String logFileCanonicalPath = IOUtils.tryGetCanonicalPathElseGetAbsolutePath(logFile);
          if (!StringUtils.isBlank(logFileCanonicalPath)) { 
            return logFileCanonicalPath;
          }
        }
      }

      return launcher.getLogFileCanonicalPath();
    }

    @SuppressWarnings("unchecked")
    private static String getServerBindAddressAsString(final ServerLauncher launcher) {
      final GemFireCacheImpl gemfireCache = GemFireCacheImpl.getInstance();
      
      if (gemfireCache != null) {
        final List csList = gemfireCache.getCacheServers();
        if (csList != null && !csList.isEmpty()) {
          final CacheServer cs = csList.get(0);
          final String serverBindAddressAsString = cs.getBindAddress();
          if (!StringUtils.isBlank(serverBindAddressAsString)) {
            return serverBindAddressAsString;
          }
        }
      }

      return launcher.getServerBindAddressAsString();
    }

    @SuppressWarnings("unchecked")
    private static String getServerPortAsString(final ServerLauncher launcher) {
      final GemFireCacheImpl gemfireCache = GemFireCacheImpl.getInstance();

      if (gemfireCache != null) {
        final List csList = gemfireCache.getCacheServers();
        if (csList != null && !csList.isEmpty()) {
          final CacheServer cs = csList.get(0);
          final String portAsString = String.valueOf(cs.getPort());
          if (!StringUtils.isBlank(portAsString)) {
            return portAsString;
          }
        }
      }

      return (launcher.isDisableDefaultServer() ? StringUtils.EMPTY_STRING : launcher.getServerPortAsString());
    }

    @Override
    protected String getServiceName() {
      return SERVER_SERVICE_NAME;
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy