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

import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notEmpty;
import static org.apache.commons.lang3.Validate.notNull;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.management.ManagementFactory;
import java.nio.file.Path;

import org.apache.geode.annotations.Immutable;

/**
 * Utility operations for processes such as identifying the process id (pid).
 *
 * @since GemFire 7.0
 */
public class ProcessUtils {

  @Immutable
  private static final ProcessUtilsProvider internal = initializeInternalProcessUtils();

  private ProcessUtils() {
    // nothing
  }

  /**
   * Returns the pid for this process.
   *
   * @throws PidUnavailableException if parsing the pid from the name of the RuntimeMXBean fails
   *
   * @see java.lang.management.RuntimeMXBean#getName()
   */
  public static int identifyPid() throws PidUnavailableException {
    return identifyPid(ManagementFactory.getRuntimeMXBean().getName());
  }

  /**
   * Returns the pid for this process without throwing a checked exception.
   *
   * @throws UncheckedPidUnavailableException if parsing the pid from the name of the RuntimeMXBean
   *         fails
   *
   * @see java.lang.management.RuntimeMXBean#getName()
   */
  public static int identifyPidAsUnchecked() throws UncheckedPidUnavailableException {
    try {
      return identifyPid();
    } catch (PidUnavailableException e) {
      throw new UncheckedPidUnavailableException(e);
    }
  }

  /**
   * Returns the pid for this process using the specified name from RuntimeMXBean.
   *
   * @throws PidUnavailableException if parsing the pid from the RuntimeMXBean name fails
   */
  public static int identifyPid(final String name) throws PidUnavailableException {
    notEmpty(name, "Invalid name '" + name + "' specified");

    try {
      final int index = name.indexOf('@');
      if (index < 0) {
        throw new PidUnavailableException("Unable to parse pid from " + name);
      }
      return Integer.parseInt(name.substring(0, index));
    } catch (NumberFormatException e) {
      throw new PidUnavailableException("Unable to parse pid from " + name, e);
    }
  }

  /**
   * Returns true if a process identified by the process id is currently running on this host
   * machine.
   *
   * @param pid process id to check for
   * @return true if the pid matches a currently running process
   */
  public static boolean isProcessAlive(final int pid) {
    isTrue(pid > 0, "Invalid pid '" + pid + "' specified");

    return internal.isProcessAlive(pid);
  }

  /**
   * Returns true if a process identified by the specified Process is currently running on this host
   * machine.
   *
   * @param process the Process to check
   * @return true if the Process is a currently running process
   */
  public static boolean isProcessAlive(final Process process) {
    notNull(process, "Invalid process '" + process + "' specified");

    return process.isAlive();
  }

  /**
   * Returns true if a process identified by the process id was running on this host machine and has
   * been terminated by this operation.
   *
   * @param pid process id
   * @return true if the process was terminated by this operation
   */
  public static boolean killProcess(final int pid) {
    isTrue(pid > 0, "Invalid pid '" + pid + "' specified");

    return internal.killProcess(pid);
  }

  public static int readPid(final Path pidFile) {
    return readPid(pidFile.toFile());
  }

  public static int readPid(final File pidFile) {
    notNull(pidFile, "Invalid pidFile '" + pidFile + "' specified");
    isTrue(pidFile.exists(), "Nonexistent pidFile '" + pidFile + "' specified");

    try (BufferedReader reader = new BufferedReader(new FileReader(pidFile))) {
      return Integer.parseInt(reader.readLine());
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  /**
   * Returns true if Attach API or JNA NativeCalls is available for killing process or checking if
   * it is alive.
   */
  public static boolean isAvailable() {
    return internal.isAvailable();
  }

  /**
   * Returns true if Attach API is available for checking status.
   */
  public static boolean isAttachApiAvailable() {
    return internal.isAttachApiAvailable();
  }

  private static ProcessUtilsProvider initializeInternalProcessUtils() {
    // 1) prefer Attach because it filters out non-JVM processes
    try {
      Class.forName("com.sun.tools.attach.VirtualMachine");
      Class.forName("com.sun.tools.attach.VirtualMachineDescriptor");
      return new AttachProcessUtils();
    } catch (ClassNotFoundException | LinkageError ignored) {
      // fall through
    }

    // 2) try NativeCalls but make sure it doesn't throw UnsupportedOperationException
    try {
      // consider getting rid of Class.forName usage if NativeCalls always safely loads
      Class.forName("org.apache.geode.internal.shared.NativeCalls");
      NativeProcessUtils nativeProcessUtils = new NativeProcessUtils();
      boolean result = nativeProcessUtils.isProcessAlive(identifyPid());
      if (result) {
        return nativeProcessUtils;
      }
    } catch (ClassNotFoundException | LinkageError | PidUnavailableException
        | UnsupportedOperationException ignored) {
      // fall through
    }

    // 3) consider logging warning and then proceed with no-op
    return new ProcessUtilsProvider() {
      @Override
      public boolean isProcessAlive(final int pid) {
        return false;
      }

      @Override
      public boolean killProcess(final int pid) {
        return false;
      }

      @Override
      public boolean isAvailable() {
        return false;
      }

      @Override
      public boolean isAttachApiAvailable() {
        return false;
      }
    };
  }

}
