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

import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
import static org.apache.geode.distributed.ConfigurationProperties.START_LOCATOR;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;

import org.apache.commons.lang3.StringUtils;

import org.apache.geode.GemFireException;
import org.apache.geode.GemFireIOException;
import org.apache.geode.InternalGemFireException;
import org.apache.geode.NoSystemException;
import org.apache.geode.SystemFailure;
import org.apache.geode.UncreatedSystemException;
import org.apache.geode.UnstartedSystemException;
import org.apache.geode.admin.AdminException;
import org.apache.geode.admin.internal.AdminDistributedSystemImpl;
import org.apache.geode.annotations.Immutable;
import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.cache.persistence.PersistentID;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.DistributionConfigImpl;
import org.apache.geode.distributed.internal.HighPriorityAckedMessage;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.distributed.internal.tcpserver.HostAndPort;
import org.apache.geode.distributed.internal.tcpserver.TcpClient;
import org.apache.geode.distributed.internal.tcpserver.TcpSocketFactory;
import org.apache.geode.internal.admin.remote.TailLogResponse;
import org.apache.geode.internal.cache.DiskStoreImpl;
import org.apache.geode.internal.cache.backup.BackupOperation;
import org.apache.geode.internal.logging.DateFormatter;
import org.apache.geode.internal.logging.MergeLogFiles;
import org.apache.geode.internal.net.SocketCreator;
import org.apache.geode.internal.net.SocketCreatorFactory;
import org.apache.geode.internal.security.SecurableCommunicationChannel;
import org.apache.geode.internal.statistics.StatArchiveReader;
import org.apache.geode.internal.statistics.StatArchiveReader.ResourceInst;
import org.apache.geode.internal.statistics.StatArchiveReader.StatValue;
import org.apache.geode.internal.util.JavaCommandBuilder;
import org.apache.geode.internal.util.PluckStacks;
import org.apache.geode.internal.util.PluckStacks.ThreadStack;
import org.apache.geode.management.BackupStatus;
import org.apache.geode.util.internal.GeodeGlossary;

/**
 * Provides static methods for various system administation tasks.
 */
public class SystemAdmin {

  /**
   * Finds the gemfire jar path element in the given classpath and returns the directory that jar is
   * in.
   */
  public static File findGemFireLibDir() {
    URL jarURL = GemFireVersion.getJarURL();
    if (jarURL == null) {
      return null;
    }
    String path = jarURL.getPath();
    // Decode URL to get rid of escaped characters. See bug 32465.
    path = URLDecoder.decode(path);
    File f = new File(path);
    if (f.isDirectory()) {
      return f;
    }
    return f.getParentFile();
  }

  // ------------------- begin: Locator methods ------------------------------

  public void locatorStart(File directory, String portOption, String addressOption,
      String gemfirePropertiesFileOption, boolean peerOption, boolean serverOption,
      String hostnameForClientsOption) throws InterruptedException {
    // if (Thread.interrupted()) throw new InterruptedException(); not necessary checked in
    // locatorStart
    locatorStart(directory, portOption, addressOption, gemfirePropertiesFileOption, null, null,
        peerOption, serverOption, hostnameForClientsOption);
  }

  public void locatorStart(File directory, String portOption, String addressOption,
      String gemfirePropertiesFileOption, Properties propertyOptionArg, List xoptions,
      boolean peerOption, boolean serverOption, String hostnameForClientsOption)
      throws InterruptedException {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
    int port = DistributionLocator.parsePort(portOption);

    if (addressOption == null) {
      addressOption = "";
    }

    if (!addressOption.equals("")) {
      // make sure its a valid ip address
      if (!validLocalAddress(addressOption)) {
        throw new IllegalArgumentException(
            String.format("' %s ' is not a valid IP address for this machine",
                addressOption));
      }
    }
    // check to see if locator is already running
    File logFile = new File(directory, DistributionLocator.DEFAULT_STARTUP_LOG_FILE);
    try {
      // make sure file can be opened for writing
      (new FileOutputStream(logFile.getPath(), true)).close();
    } catch (IOException ex) {
      throw new GemFireIOException(
          String.format(
              "Logfile %s could not be opened for writing. Verify file permissions are correct and that another locator is not already running in the same directory.",
              logFile.getPath()),
          ex);
    }

    if (gemfirePropertiesFileOption != null) {
      Properties newPropOptions = new Properties();// see #43731
      newPropOptions.putAll(propertyOptionArg);
      newPropOptions.setProperty("gemfirePropertyFile", gemfirePropertiesFileOption);
      propertyOptionArg = newPropOptions;
    }

    // read ssl properties
    Map<String, String> env = new HashMap<>();
    SocketCreator.readSSLProperties(env);

    List cmdVec = JavaCommandBuilder.buildCommand(getDistributionLocatorPath(), null,
        propertyOptionArg, xoptions);

    cmdVec.add(String.valueOf(port));
    cmdVec.add(addressOption);
    cmdVec.add(Boolean.toString(peerOption));
    cmdVec.add(Boolean.toString(serverOption));
    if (hostnameForClientsOption == null) {
      hostnameForClientsOption = "";
    }
    cmdVec.add(hostnameForClientsOption);

    String[] cmd = (String[]) cmdVec.toArray(new String[0]);

    // start with a fresh log each time
    if (!logFile.delete() && logFile.exists()) {
      throw new GemFireIOException("Unable to delete " + logFile.getAbsolutePath());
    }
    boolean treatAsPure = true;
    /*
     * A counter used by PureJava to determine when its waited too long to start the locator
     * process. countDown * 250 = how many seconds to wait before giving up.
     */
    int countDown = 60;
    // NYI: wait around until we can attach
    while (!ManagerInfo.isLocatorStarted(directory)) {
      countDown--;
      Thread.sleep(250);
      if (countDown < 0) {
        try {
          String msg = tailFile(logFile, false);
          throw new GemFireIOException(
              String.format("Start of locator failed. The end of %s contained this message: %s.",
                  logFile, msg),
              null);
        } catch (IOException ignore) {
          throw new GemFireIOException(
              String.format("Start of locator failed. Check end of %s for reason.",
                  logFile),
              null);
        }
      }
      Thread.sleep(500);
    }
  }


  /** get the path to the distribution locator class */
  protected String getDistributionLocatorPath() {
    return "org.apache.geode.internal.DistributionLocator";
  }

  /**
   * enumerates all available local network addresses to find a match with the given address.
   * Returns false if the address is not usable on the current machine
   */
  public static boolean validLocalAddress(String bindAddress) {
    InetAddress addr = null;
    try {
      addr = InetAddress.getByName(bindAddress);
    } catch (UnknownHostException ex) {
      return false;
    }
    try {
      Enumeration en = NetworkInterface.getNetworkInterfaces();
      while (en.hasMoreElements()) {
        NetworkInterface ni = (NetworkInterface) en.nextElement();
        Enumeration en2 = ni.getInetAddresses();
        while (en2.hasMoreElements()) {
          InetAddress check = (InetAddress) en2.nextElement();
          if (check.equals(addr)) {
            return true;
          }
        }
      }
    } catch (SocketException sex) {
      return true; // can't query the interfaces - punt
    }
    return false;
  }

  @SuppressWarnings("hiding")
  public void locatorStop(File directory, String portOption, String addressOption,
      Properties propertyOption) throws InterruptedException {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
    InetAddress addr = null; // fix for bug 30810
    if (addressOption == null) {
      addressOption = "";
    }
    addressOption = addressOption.trim();
    if (!addressOption.equals("")) {
      // make sure its a valid ip address
      try {
        addr = InetAddress.getByName(addressOption);
      } catch (UnknownHostException ex) {
        throw new IllegalArgumentException(
            String.format("-address value was not a known IP address: %s",
                ex));
      }
    }

    if (propertyOption != null) {
      for (final Object o : propertyOption.keySet()) {
        String key = (String) o;
        System.setProperty(key, propertyOption.getProperty(key));
      }
    }
    int port = DistributionLocator.parsePort(portOption);
    int pid = 0;
    try {
      ManagerInfo info = ManagerInfo.loadLocatorInfo(directory);
      pid = info.getManagerProcessId();
      if (portOption == null || portOption.trim().length() == 0) {
        port = info.getManagerPort();
      }
      if (addr == null) {
        addr = info.getManagerAddress();
      }

      try {
        new TcpClient(SocketCreatorFactory
            .getSocketCreatorForComponent(SecurableCommunicationChannel.LOCATOR),
            InternalDataSerializer.getDSFIDSerializer().getObjectSerializer(),
            InternalDataSerializer.getDSFIDSerializer().getObjectDeserializer(),
            TcpSocketFactory.DEFAULT)
                .stop(new HostAndPort(addr.getHostName(), port));
      } catch (java.net.ConnectException ce) {
        System.out.println(
            "Unable to connect to Locator process. Possible causes are that an incorrect bind address/port combination was specified to the stop-locator command or the process is unresponsive.");
        return;
      }
      // wait for the locator process to go away
      // format and change message
      if (!quiet) {
        System.out.println(
            "Waiting 5 seconds for locator process to terminate...");
      }
      Thread.sleep(5000);
    } catch (UnstartedSystemException ex) {
      // fix for bug 28133
      throw new UnstartedSystemException(
          String.format("Locator in directory %s is not running.",
              directory));
    } catch (NoSystemException ex) {
      // before returning see if a stale lock file/shared memory can be cleaned up
      cleanupAfterKilledLocator(directory);
      throw ex;
    }
  }

  /**
   * Gets the status of a locator.
   *
   * @param directory the locator's directory
   * @return the status string. Will be one of the following: "running", "killed", "stopped",
   *         "stopping", or "starting".
   * @throws UncreatedSystemException if the locator <code>directory</code> does not exist or is not
   *         a directory.
   * @throws GemFireIOException if the manager info exists but could not be read. This probably
   *         means that the info file is corrupt.
   */
  public String locatorStatus(File directory) {
    return ManagerInfo.getLocatorStatusCodeString(directory);
  }

  /**
   * Gets information on the locator.
   *
   * @param directory the locator's directory
   * @return information string.
   * @throws UncreatedSystemException if the locator <code>directory</code> does not exist or is not
   *         a directory.
   * @throws GemFireIOException if the manager info exists but could not be read. This probably
   *         means that the info file is corrupt.
   */
  public String locatorInfo(File directory) {
    int statusCode = ManagerInfo.getLocatorStatusCode(directory);
    String statusString = ManagerInfo.statusToString(statusCode);
    try {
      ManagerInfo mi = ManagerInfo.loadLocatorInfo(directory);
      if (statusCode == ManagerInfo.KILLED_STATUS_CODE) {
        return String.format("Locator in %s was killed while it was %s. Locator process id was %s.",
            directory, ManagerInfo.statusToString(mi.getManagerStatus()),
            mi.getManagerProcessId());
      } else {
        return String.format("Locator in %s is %s. Locator process id is %s.",
            directory, statusString, mi.getManagerProcessId());
      }
    } catch (UnstartedSystemException ex) {
      return String.format("Locator in %s is stopped.", directory);
    } catch (GemFireIOException ex) {
      return String.format("Locator in %s is starting.", directory);
    }
  }

  /**
   * Cleans up any artifacts left by a killed locator. Namely the info file is deleted.
   */
  private static void cleanupAfterKilledLocator(File directory) {
    try {
      if (ManagerInfo.getLocatorStatusCode(directory) == ManagerInfo.KILLED_STATUS_CODE) {
        File infoFile = ManagerInfo.getLocatorInfoFile(directory);
        if (infoFile.exists()) {
          if (!infoFile.delete() && infoFile.exists()) {
            System.out.println("WARNING: unable to delete " + infoFile.getAbsolutePath());
          }
          if (!quiet) {
            System.out.println(
                "Cleaned up artifacts left by the previous killed locator.");
          }
        }
      }
    } catch (GemFireException ignore) {
    }
  }

  /**
   * Tails the end of the locator's log file
   *
   * @since GemFire 4.0
   */
  public String locatorTailLog(File directory) {
    File logFile = new File(directory, DistributionLocator.DEFAULT_LOG_FILE);
    if (!logFile.exists()) {
      return String.format("Log file %s does not exist.", logFile);
    }

    try {
      return TailLogResponse.tailSystemLog(logFile);
    } catch (IOException ex) {
      StringWriter sw = new StringWriter();
      PrintWriter pw = new PrintWriter(sw, true);
      sw.write(String.format("An IOException was thrown while tailing %s",
          logFile));
      ex.printStackTrace(pw);
      pw.flush();
      return sw.toString();
    }
  }

  // -------------------- end: Locator methods -------------------------------

  public static void compactDiskStore(String... args) {
    compactDiskStore(Arrays.asList(args));
  }

  public static void compactDiskStore(List args) {
    String diskStoreName = (String) args.get(0);
    List dirList = args.subList(1, args.size());
    File[] dirs = new File[dirList.size()];
    Iterator it = dirList.iterator();
    int idx = 0;
    while (it.hasNext()) {
      dirs[idx] = new File((String) it.next());
      idx++;
    }
    try {
      DiskStoreImpl.offlineCompact(diskStoreName, dirs, false, maxOplogSize);
    } catch (Exception ex) {
      throw new GemFireIOException(" disk-store=" + diskStoreName + ": " + ex, ex);
    }
  }

  public static void upgradeDiskStore(List args) {
    String diskStoreName = (String) args.get(0);
    List dirList = args.subList(1, args.size());
    File[] dirs = new File[dirList.size()];
    Iterator it = dirList.iterator();
    int idx = 0;
    while (it.hasNext()) {
      dirs[idx] = new File((String) it.next());
      idx++;
    }
    try {
      DiskStoreImpl.offlineCompact(diskStoreName, dirs, true, maxOplogSize);
    } catch (Exception ex) {
      throw new GemFireIOException(" disk-store=" + diskStoreName + ": " + ex, ex);
    }
  }

  public static void compactAllDiskStores(List args) throws AdminException {
    InternalDistributedSystem ads = getAdminCnx();
    Map<DistributedMember, Set<PersistentID>> status =
        AdminDistributedSystemImpl.compactAllDiskStores(ads.getDistributionManager());

    System.out.println("Compaction complete.");
    System.out.println("The following disk stores compacted some files:");
    for (Set<PersistentID> memberStores : status.values()) {
      for (PersistentID store : memberStores) {
        System.out.println("\t" + store);
      }
    }
  }

  public static void validateDiskStore(String... args) {
    validateDiskStore(Arrays.asList(args));
  }

  public static void validateDiskStore(List args) {
    String diskStoreName = (String) args.get(0);
    List dirList = args.subList(1, args.size());
    File[] dirs = new File[dirList.size()];
    Iterator it = dirList.iterator();
    int idx = 0;
    while (it.hasNext()) {
      dirs[idx] = new File((String) it.next());
      idx++;
    }
    try {
      DiskStoreImpl.validate(diskStoreName, dirs);
    } catch (Exception ex) {
      throw new GemFireIOException(" disk-store=" + diskStoreName + ": " + ex, ex);
    }
  }

  private static InternalDistributedSystem getAdminCnx() {
    InternalDistributedSystem.setCommandLineAdmin(true);
    Properties props = propertyOption;
    props.setProperty(LOG_LEVEL, "warning");
    DistributionConfigImpl dsc = new DistributionConfigImpl(props);
    System.out.print("Connecting to distributed system:");
    if (!"".equals(dsc.getLocators())) {
      System.out.println(" locators=" + dsc.getLocators());
    } else {
      System.out.println(" mcast=" + dsc.getMcastAddress() + ":" + dsc.getMcastPort());
    }
    InternalDistributedSystem ds =
        (InternalDistributedSystem) InternalDistributedSystem.connectForAdmin(props);
    Set existingMembers = ds.getDistributionManager().getDistributionManagerIds();
    if (existingMembers.isEmpty()) {
      throw new RuntimeException("There are no members in the distributed system");
    }
    return ds;
  }

  public static void shutDownAll(ArrayList<String> cmdLine) {
    try {
      long timeout = 0;
      if (cmdLine.size() > 0) {
        timeout = Long.parseLong(cmdLine.get(0));
      }
      InternalDistributedSystem ads = getAdminCnx();
      Set members =
          AdminDistributedSystemImpl.shutDownAllMembers(ads.getDistributionManager(), timeout);
      int count = members == null ? 0 : members.size();
      if (members == null) {
        System.err
            .println("Unable to shut down the distributed system in the specified amount of time.");
      } else if (count == 0) {
        System.err.println("The distributed system had no members to shut down.");
      } else if (count == 1) {
        System.out.println("Successfully shut down one member");
      } else {
        System.out.println("Successfully shut down " + count + " members");
      }
    } catch (Exception ex) {
      throw new GemFireIOException(ex.toString(), ex);
    }
  }

  /**
   * this is a test hook to allow us to drive SystemAdmin functions without invoking main(), which
   * can call System.exit().
   *
   */
  public static void setDistributedSystemProperties(Properties props) {
    propertyOption = props;
  }

  public static void printStacks(List<String> cmdLine, boolean allStacks) {
    try {
      InternalDistributedSystem ads = getAdminCnx();
      HighPriorityAckedMessage msg = new HighPriorityAckedMessage();
      OutputStream os;
      PrintWriter ps;
      File outputFile = null;

      if (cmdLine.size() > 0) {
        outputFile = new File(cmdLine.get(0));
        os = new FileOutputStream(outputFile);
        ps = new PrintWriter(os);
      } else {
        os = System.out;
        ps = new PrintWriter(System.out);
      }

      Map<InternalDistributedMember, byte[]> dumps =
          msg.dumpStacks(ads.getDistributionManager().getAllOtherMembers(), false, true);
      for (Map.Entry<InternalDistributedMember, byte[]> entry : dumps.entrySet()) {
        ps.append("--- dump of stack for member " + entry.getKey()
            + " ------------------------------------------------------------------------------\n");
        ps.flush();
        GZIPInputStream zipIn = new GZIPInputStream(new ByteArrayInputStream(entry.getValue()));
        if (allStacks) {
          BufferedInputStream bin = new BufferedInputStream(zipIn);
          byte[] buffer = new byte[10000];
          int count;
          while ((count = bin.read(buffer)) != -1) {
            os.write(buffer, 0, count);
          }
          ps.append('\n');
        } else {
          BufferedReader reader = new BufferedReader(new InputStreamReader(zipIn));
          List<ThreadStack> stacks = (new PluckStacks()).getStacks(reader);
          for (ThreadStack s : stacks) {
            s.writeTo(ps);
            ps.append('\n');
          }
          ps.append('\n');
        }
      }
      ps.flush();
      os.close();
      if (outputFile != null) {
        System.out.println(dumps.size() + " stack dumps written to " + outputFile.getName());
      }
    } catch (Exception ex) {
      throw new GemFireIOException(ex.toString(), ex);
    }
  }

  public static void backup(String targetDir) throws AdminException {
    InternalDistributedSystem ads = getAdminCnx();

    // Baseline directory should be null if it was not provided on the command line
    BackupStatus status =
        new BackupOperation(ads.getDistributionManager(), ads.getCache()).backupAllMembers(
            targetDir,
            SystemAdmin.baselineDir);

    boolean incomplete = !status.getOfflineDiskStores().isEmpty();

    System.out.println("The following disk stores were backed up:");
    for (Set<PersistentID> memberStores : status.getBackedUpDiskStores().values()) {
      for (PersistentID store : memberStores) {
        System.out.println("\t" + store);
      }
    }
    if (incomplete) {
      System.err.println("The backup may be incomplete. The following disk stores are not online:");
      for (PersistentID store : status.getOfflineDiskStores()) {
        System.err.println("\t" + store);
      }
    } else {
      System.out.println("Backup successful.");
    }
  }

  public static void listMissingDiskStores() throws AdminException {
    InternalDistributedSystem ads = getAdminCnx();
    Set s = AdminDistributedSystemImpl.getMissingPersistentMembers(ads.getDistributionManager());
    if (s.isEmpty()) {
      System.out.println("The distributed system did not have any missing disk stores");
    } else {
      for (Object o : s) {
        System.out.println(o);
      }
    }
  }

  private static File[] argsToFile(Collection<String> args) {
    File[] dirs = new File[args.size()];

    int i = 0;
    for (String dir : args) {
      dirs[i++] = new File(dir);
    }
    return dirs;
  }

  public static void showDiskStoreMetadata(ArrayList<String> args) {
    String dsName = args.get(0);
    File[] dirs = argsToFile(args.subList(1, args.size()));

    try {
      DiskStoreImpl.dumpMetadata(dsName, dirs, showBuckets);
    } catch (Exception ex) {
      throw new GemFireIOException(" disk-store=" + dsName + ": " + ex, ex);
    }
  }

  public static void exportDiskStore(ArrayList<String> args, String outputDir) {
    File out = outputDir == null ? new File(".") : new File(outputDir);
    if (!out.exists()) {
      out.mkdirs();
    }

    String dsName = args.get(0);
    File[] dirs = argsToFile(args.subList(1, args.size()));

    try {
      DiskStoreImpl.exportOfflineSnapshot(dsName, dirs, out);
    } catch (Exception ex) {
      throw new GemFireIOException(" disk-store=" + dsName + ": " + ex, ex);
    }
  }

  public static void revokeMissingDiskStores(ArrayList<String> cmdLine)
      throws UnknownHostException, AdminException {
    String uuidString = cmdLine.get(0);
    UUID uuid = UUID.fromString(uuidString);
    InternalDistributedSystem ads = getAdminCnx();
    AdminDistributedSystemImpl.revokePersistentMember(ads.getDistributionManager(), uuid);
    Set<PersistentID> s =
        AdminDistributedSystemImpl.getMissingPersistentMembers(ads.getDistributionManager());


    // Fix for 42607 - wait to see if the revoked member goes way if it is still in the set of
    // missing members. It may take a moment to clear the missing member set after the revoke.
    long start = System.currentTimeMillis();
    while (containsRevokedMember(s, uuid)) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException ignore) {
      }
      s = AdminDistributedSystemImpl.getMissingPersistentMembers(ads.getDistributionManager());
      if (start + 30000 < System.currentTimeMillis()) {
        break;
      }
    }
    if (s.isEmpty()) {
      System.out.println("revocation was successful and no disk stores are now missing");
    } else {
      System.out.println("The following disk stores are still missing:");
      for (Object o : s) {
        System.out.println(o);
      }
    }
  }

  private static boolean containsRevokedMember(Set<PersistentID> missing, UUID revokedUUID) {
    for (PersistentID id : missing) {
      if (id.getUUID().equals(revokedUUID)) {
        return true;
      }
    }
    return false;
  }

  public static void modifyDiskStore(String... args) {
    modifyDiskStore(Arrays.asList(args));
  }

  public static void modifyDiskStore(List args) {
    String diskStoreName = (String) args.get(0);
    List dirList = args.subList(1, args.size());
    File[] dirs = new File[dirList.size()];
    Iterator it = dirList.iterator();
    int idx = 0;
    while (it.hasNext()) {
      dirs[idx] = new File((String) it.next());
      idx++;
    }
    try {
      if (lruOption != null || lruActionOption != null || lruLimitOption != null
          || concurrencyLevelOption != null || initialCapacityOption != null
          || loadFactorOption != null || compressorClassNameOption != null
          || statisticsEnabledOption != null) {
        if (regionOption == null) {
          throw new IllegalArgumentException("modify-disk-store requires -region=<regionName>");
        }
        if (remove) {
          throw new IllegalArgumentException(
              "the -remove option can not be used with the other modify options.");
        }
        DiskStoreImpl.modifyRegion(diskStoreName, dirs, regionOption, lruOption, lruActionOption,
            lruLimitOption, concurrencyLevelOption, initialCapacityOption, loadFactorOption,
            compressorClassNameOption, statisticsEnabledOption, null/* offHeap */, true);
        System.out.println("The region " + regionOption
            + " was successfully modified in the disk store " + diskStoreName);
      } else if (remove) {
        if (regionOption == null) {
          throw new IllegalArgumentException("modify-disk-store requires -region=<regionName>");
        }
        DiskStoreImpl.destroyRegion(diskStoreName, dirs, regionOption);
        System.out.println("The region " + regionOption
            + " was successfully removed from the disk store " + diskStoreName);
      } else {
        DiskStoreImpl.dumpInfo(System.out, diskStoreName, dirs, regionOption, null);
        if (regionOption == null) {
          System.out.println("Please specify -region=<regionName> and a modify option.");
        } else {
          System.out.println("Please specify a modify option.");
        }
      }
    } catch (Exception ex) {
      throw new GemFireIOException(" disk-store=" + diskStoreName + ": " + ex, ex);
    }
  }

  public void mergeLogs(String outOption, List<String> args) {
    Map<String, InputStream> inputs = new HashMap<>();

    PrintStream ps;
    if (outOption != null) {
      try {
        ps = new PrintStream(new FileOutputStream(outOption));
      } catch (FileNotFoundException ex) {
        throw new GemFireIOException(
            String.format("Could not create file %s for output because %s",
                outOption, getExceptionMessage(ex)));
      }
    } else {
      ps = System.out;
    }
    PrintWriter mergedFile = new PrintWriter(ps, true);

    List<String> normalizedFiles =
        args.stream().map(f -> Paths.get(f).toAbsolutePath().toString()).collect(
            Collectors.toList());
    int prefixLength =
        StringUtils.getCommonPrefix(normalizedFiles.toArray(new String[] {})).length();

    if (!quiet) {
      ps.println("Merging the following log files:");
    }

    for (String fileName : normalizedFiles) {
      try {
        String shortName = fileName.substring(prefixLength);
        inputs.put(shortName, new FileInputStream(fileName));
      } catch (FileNotFoundException ex) {
        throw new GemFireIOException(
            String.format("Could not open to %s for reading because %s",
                fileName, getExceptionMessage(ex)));
      }
      if (!quiet) {
        ps.println("  " + fileName);
      }
    }

    if (MergeLogFiles.mergeLogFiles(inputs, mergedFile)) {
      throw new GemFireIOException(
          "trouble merging log files.");
    }
    mergedFile.flush();
    if (outOption != null) {
      mergedFile.close();
    }
    if (!quiet) {
      System.out
          .println(String.format("Completed merge of %s logs to %s.",
              args.size(),
              ((outOption != null) ? outOption : "stdout")));
    }
  }

  /**
   * Returns the contents located at the end of the file as a string.
   *
   * @throws IOException if the file can not be opened or read
   */
  public String tailFile(File file, boolean problemsOnly) throws IOException {
    byte[] buffer = new byte[128000];
    int readSize = buffer.length;
    RandomAccessFile f = new RandomAccessFile(file, "r");
    long length = f.length();
    if (length < readSize) {
      readSize = (int) length;
    }
    long seekOffset = length - readSize;
    f.seek(seekOffset);
    if (readSize != f.read(buffer, 0, readSize)) {
      throw new EOFException(
          "Failed to read " + readSize + " bytes from " + file.getAbsolutePath());
    }
    f.close();
    // Now look for the last message header
    int msgStart = -1;
    int msgEnd = readSize;
    for (int i = readSize - 1; i >= 0; i--) {
      if (buffer[i] == '[' && (buffer[i + 1] == 's' || buffer[i + 1] == 'e' || buffer[i
          + 1] == 'w' /* ignore all messages except severe, error, and warning to fix bug 28968 */)
          && i > 0 && (buffer[i - 1] == '\n' || buffer[i - 1] == '\r')) {
        msgStart = i;
        break;
      }
    }
    if (msgStart == -1) {
      if (problemsOnly) {
        return null;
      }
      // Could not find message start. Show last line instead.
      for (int i = readSize - 3; i >= 0; i--) {
        if (buffer[i] == '\n' || buffer[i] == '\r') {
          msgStart = (buffer[i] == '\n') ? (i + 1) : (i + 2);
          break;
        }
      }
      if (msgStart == -1) {
        // Could not find line so show entire buffer
        msgStart = 0;
      }
    } else {
      // try to find the start of the next message and get rid of it
      for (int i = msgStart + 1; i < readSize; i++) {
        if (buffer[i] == '[' && (buffer[i - 1] == '\n' || buffer[i - 1] == '\r')) {
          msgEnd = i;
          break;
        }
      }
    }
    for (int i = msgStart; i < msgEnd; i++) {
      if (buffer[i] == '\n' || buffer[i] == '\r') {
