JvmProcessId.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */

package org.waarp.common.guid;

import org.waarp.common.logging.SysErrLogger;
import org.waarp.common.utility.StringUtils;
import org.waarp.common.utility.SystemPropertyUtil;

import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

public final class JvmProcessId {
  /**
   * Definition for Machine Id replacing MAC address
   */
  private static final Pattern MACHINE_ID_PATTERN =
      Pattern.compile("^(?:[0-9a-fA-F][:-]?){6,8}$");
  private static final int MACHINE_ID_LEN = 6;
  /**
   * So MAX value on 3 bytes (64 system use 2^22 id)
   */
  private static final int MAX_PID = 4194304;
  /**
   * 2 bytes value maximum
   */
  static final int JVMPID;
  private static final Object[] EMPTY_OBJECTS = new Object[0];
  private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0];

  /**
   * Try to get Mac Address but could be also changed dynamically
   */
  static byte[] mac;
  static int macInt;
  static byte jvmId;

  static {
    JVMPID = jvmProcessId();
    mac = macAddress();
    macInt = macAddressAsInt();
    jvmId = jvmInstanceId();
  }

  private JvmProcessId() {
  }

  /**
   * Use both PID and MAC address but as 8 bytes hash
   *
   * @return one id as much as possible unique
   */
  public static byte jvmInstanceId() {
    final long id = 31L * jvmProcessId() + macAddressAsInt();
    return (byte) (Long.hashCode(id) & 0xFF);
  }

  /**
   * @return the JVM Process ID
   */
  public static int jvmProcessId() {
    // Note: may fail in some JVM implementations
    // something like '<pid>@<hostname>', at least in SUN / Oracle JVMs
    try {
      final ClassLoader loader = getSystemClassLoader();
      String value;
      value =
          jvmProcessIdManagementFactory(loader, EMPTY_OBJECTS, EMPTY_CLASSES);
      final int atIndex = value.indexOf('@');
      if (atIndex >= 0) {
        value = value.substring(0, atIndex);
      }
      int processId = -1;
      processId = parseProcessId(processId, value);
      if (processId < 0 || processId > MAX_PID) {
        processId = StringUtils.RANDOM.nextInt(MAX_PID + 1);
      }
      return processId;
    } catch (final Throwable e) {//NOSONAR
      SysErrLogger.FAKE_LOGGER.syserr(e);
      return StringUtils.RANDOM.nextInt(MAX_PID + 1);
    }
  }

  /**
   * @return the mac address if possible, else random values
   */
  public static byte[] macAddress() {
    try {
      byte[] machineId = null;
      final String customMachineId =
          SystemPropertyUtil.get("org.waarp.machineId");
      if (customMachineId != null &&
          MACHINE_ID_PATTERN.matcher(customMachineId).matches()) {
        machineId = parseMachineId(customMachineId);
      }

      if (machineId == null) {
        machineId = defaultMachineId();
      }
      return machineId;
    } catch (final Throwable e) {//NOSONAR
      return StringUtils.getRandom(MACHINE_ID_LEN);
    }
  }

  /**
   * @return MAC address as int (truncated to 4 bytes instead of 6)
   */
  public static int macAddressAsInt() {
    return (mac[3] & 0xFF) << 24 | (mac[2] & 0xFF) << 16 |
           (mac[1] & 0xFF) << 8 | mac[0] & 0xFF;
  }

  /**
   * Up to the 6 first bytes will be used. If Null or less than 6 bytes, extra
   * bytes will be randomly generated.
   *
   * @param mac the MAC address in byte format (up to the 6 first
   *     bytes will
   *     be used)
   */
  public static synchronized void setMac(final byte[] mac) {
    if (mac == null) {
      JvmProcessId.mac = StringUtils.getRandom(MACHINE_ID_LEN);
    } else {
      JvmProcessId.mac = Arrays.copyOf(mac, MACHINE_ID_LEN);
      for (int i = mac.length; i < MACHINE_ID_LEN; i++) {
        JvmProcessId.mac[i] = (byte) StringUtils.RANDOM.nextInt(256);
      }
    }
    macInt = macAddressAsInt();
  }

  /**
   * @return positive - current is better, 0 - cannot tell from MAC addr,
   *     negative - candidate is better.
   */
  private static int compareAddresses(final byte[] current,
                                      final byte[] candidate) {
    if (candidate == null) {
      return 1;
    }
    // Must be EUI-48 or longer.
    if (candidate.length < 6) {
      return 1;
    }
    // Must not be filled with only 0 and 1.
    boolean onlyZeroAndOne = true;
    for (final byte b : candidate) {
      if (b != 0 && b != 1) {
        onlyZeroAndOne = false;
        break;
      }
    }
    if (onlyZeroAndOne) {
      return 1;
    }
    // Must not be a multicast address
    if ((candidate[0] & 1) != 0) {
      return 1;
    }
    // Prefer globally unique address.
    if ((current[0] & 2) == 0) {
      if ((candidate[0] & 2) == 0) {
        // Both current and candidate are globally unique addresses.
        return 0;
      } else {
        // Only current is globally unique.
        return 1;
      }
    } else {
      if ((candidate[0] & 2) == 0) {
        // Only candidate is globally unique.
        return -1;
      } else {
        // Both current and candidate are non-unique.
        return 0;
      }
    }
  }

  /**
   * @return positive - current is better, 0 - cannot tell, negative -
   *     candidate
   *     is better
   */
  private static int compareAddresses(final InetAddress current,
                                      final InetAddress candidate) {
    return scoreAddress(current) - scoreAddress(candidate);
  }

  private static int scoreAddress(final InetAddress addr) {
    if (addr.isAnyLocalAddress()) {
      return 0;
    }
    if (addr.isMulticastAddress()) {
      return 1;
    }
    if (addr.isLinkLocalAddress()) {
      return 2;
    }
    if (addr.isSiteLocalAddress()) {
      return 3;
    }

    return 4;
  }

  // pulled from http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-process-id
  private static ClassLoader getSystemClassLoader() {
    if (System.getSecurityManager() == null) {
      return ClassLoader.getSystemClassLoader();
    } else {
      return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
        @Override
        public ClassLoader run() {
          return ClassLoader.getSystemClassLoader();
        }
      });
    }
  }

  /**
   * @param oldProcessId
   * @param customProcessId
   *
   * @return the processId
   */
  private static int parseProcessId(final int oldProcessId,
                                    final String customProcessId) {
    int processId = oldProcessId;
    try {
      processId = Integer.parseInt(customProcessId);
    } catch (final NumberFormatException e) {
      // Malformed input.
    }
    if (processId < 0 || processId > MAX_PID) {
      processId = -1;
    }
    return processId;
  }

  /**
   * @param loader
   * @param emptyObjects
   * @param emptyClasses
   *
   * @return the processId as String
   */
  private static String jvmProcessIdManagementFactory(final ClassLoader loader,
                                                      final Object[] emptyObjects,
                                                      final Class<?>[] emptyClasses) {
    String value;
    try {
      // Invoke
      // java.lang.management.ManagementFactory.getRuntimeMXBean().getName()
      final Class<?> mgmtFactoryType =
          Class.forName("java.lang.management.ManagementFactory", true, loader);
      final Class<?> runtimeMxBeanType =
          Class.forName("java.lang.management.RuntimeMXBean", true, loader);

      final Method getRuntimeMXBean =
          mgmtFactoryType.getMethod("getRuntimeMXBean", emptyClasses);
      final Object bean = getRuntimeMXBean.invoke(null, emptyObjects);
      final Method getName =
          runtimeMxBeanType.getDeclaredMethod("getName", emptyClasses);
      value = (String) getName.invoke(bean, emptyObjects);
    } catch (final Exception e) {
      SysErrLogger.FAKE_LOGGER.syserr(
          "Unable to get PID, try another way: " + e.getMessage());

      try {
        // Invoke android.os.Process.myPid()
        final Class<?> processType =
            Class.forName("android.os.Process", true, loader);
        final Method myPid = processType.getMethod("myPid", emptyClasses);
        value = myPid.invoke(null, emptyObjects).toString();
      } catch (final Exception e2) {
        SysErrLogger.FAKE_LOGGER.syserr(
            "Unable to get PID: " + e2.getMessage());
        value = "";
      }
    }
    return value;
  }

  private static byte[] parseMachineId(String value) {
    // Strip separators.
    value = value.replaceAll("[:-]", "");

    final byte[] machineId = new byte[MACHINE_ID_LEN];
    for (int i = 0; i < value.length() && i < MACHINE_ID_LEN; i += 2) {
      machineId[i] = (byte) Integer.parseInt(value.substring(i, i + 2), 16);
    }

    return machineId;
  }

  private static byte[] defaultMachineId() {
    // Find the best MAC address available.
    final byte[] notFound = { -1 };
    byte[] bestMacAddr = notFound;
    InetAddress bestInetAddr;
    try {
      bestInetAddr = InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 });
    } catch (final UnknownHostException e) {
      // Never happens.
      throw new IllegalArgumentException(e);
    }

    // Retrieve the list of available network interfaces.
    final Map<NetworkInterface, InetAddress> ifaces =
        new LinkedHashMap<NetworkInterface, InetAddress>();
    try {
      for (final Enumeration<NetworkInterface> i =
           NetworkInterface.getNetworkInterfaces(); i.hasMoreElements(); ) {
        final NetworkInterface iface = i.nextElement();
        // Use the interface with proper INET addresses only.
        final Enumeration<InetAddress> addrs = iface.getInetAddresses();
        if (addrs.hasMoreElements()) {
          final InetAddress a = addrs.nextElement();
          if (!a.isLoopbackAddress()) {
            ifaces.put(iface, a);
          }
        }
      }
    } catch (final SocketException ignored) {
      // nothing
    }

    for (final Entry<NetworkInterface, InetAddress> entry : ifaces.entrySet()) {
      final NetworkInterface iface = entry.getKey();
      final InetAddress inetAddr = entry.getValue();
      if (iface.isVirtual()) {
        continue;
      }

      final byte[] macAddr;
      try {
        macAddr = iface.getHardwareAddress();
      } catch (final SocketException e) {
        continue;
      }

      boolean replace = false;
      int res = compareAddresses(bestMacAddr, macAddr);
      if (res < 0) {
        // Found a better MAC address.
        replace = true;
      } else if (res == 0) {
        // Two MAC addresses are of pretty much same quality.
        res = compareAddresses(bestInetAddr, inetAddr);
        if (res < 0) {
          // Found a MAC address with better INET address.
          replace = true;
        } else if (res == 0) {
          // Cannot tell the difference. Choose the longer one.
          if (bestMacAddr.length < macAddr.length) {
            replace = true;
          }
        }
      }

      if (replace) {
        bestMacAddr = macAddr;
        bestInetAddr = inetAddr;
      }
    }

    if (bestMacAddr == notFound) {
      bestMacAddr = StringUtils.getRandom(MACHINE_ID_LEN);
    }
    return bestMacAddr;
  }
}