View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  
21  package org.waarp.common.guid;
22  
23  import org.waarp.common.logging.SysErrLogger;
24  import org.waarp.common.utility.StringUtils;
25  import org.waarp.common.utility.SystemPropertyUtil;
26  
27  import java.lang.reflect.Method;
28  import java.net.InetAddress;
29  import java.net.NetworkInterface;
30  import java.net.SocketException;
31  import java.net.UnknownHostException;
32  import java.security.AccessController;
33  import java.security.PrivilegedAction;
34  import java.util.Arrays;
35  import java.util.Enumeration;
36  import java.util.LinkedHashMap;
37  import java.util.Map;
38  import java.util.Map.Entry;
39  import java.util.regex.Pattern;
40  
41  public final class JvmProcessId {
42    /**
43     * Definition for Machine Id replacing MAC address
44     */
45    private static final Pattern MACHINE_ID_PATTERN =
46        Pattern.compile("^(?:[0-9a-fA-F][:-]?){6,8}$");
47    private static final int MACHINE_ID_LEN = 6;
48    /**
49     * So MAX value on 3 bytes (64 system use 2^22 id)
50     */
51    private static final int MAX_PID = 4194304;
52    /**
53     * 2 bytes value maximum
54     */
55    static final int JVMPID;
56    private static final Object[] EMPTY_OBJECTS = new Object[0];
57    private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0];
58  
59    /**
60     * Try to get Mac Address but could be also changed dynamically
61     */
62    static byte[] mac;
63    static int macInt;
64    static byte jvmId;
65  
66    static {
67      JVMPID = jvmProcessId();
68      mac = macAddress();
69      macInt = macAddressAsInt();
70      jvmId = jvmInstanceId();
71    }
72  
73    private JvmProcessId() {
74    }
75  
76    /**
77     * Use both PID and MAC address but as 8 bytes hash
78     *
79     * @return one id as much as possible unique
80     */
81    public static byte jvmInstanceId() {
82      final long id = 31L * jvmProcessId() + macAddressAsInt();
83      return (byte) (Long.hashCode(id) & 0xFF);
84    }
85  
86    /**
87     * @return the JVM Process ID
88     */
89    public static int jvmProcessId() {
90      // Note: may fail in some JVM implementations
91      // something like '<pid>@<hostname>', at least in SUN / Oracle JVMs
92      try {
93        final ClassLoader loader = getSystemClassLoader();
94        String value;
95        value =
96            jvmProcessIdManagementFactory(loader, EMPTY_OBJECTS, EMPTY_CLASSES);
97        final int atIndex = value.indexOf('@');
98        if (atIndex >= 0) {
99          value = value.substring(0, atIndex);
100       }
101       int processId = -1;
102       processId = parseProcessId(processId, value);
103       if (processId < 0 || processId > MAX_PID) {
104         processId = StringUtils.RANDOM.nextInt(MAX_PID + 1);
105       }
106       return processId;
107     } catch (final Throwable e) {//NOSONAR
108       SysErrLogger.FAKE_LOGGER.syserr(e);
109       return StringUtils.RANDOM.nextInt(MAX_PID + 1);
110     }
111   }
112 
113   /**
114    * @return the mac address if possible, else random values
115    */
116   public static byte[] macAddress() {
117     try {
118       byte[] machineId = null;
119       final String customMachineId =
120           SystemPropertyUtil.get("org.waarp.machineId");
121       if (customMachineId != null &&
122           MACHINE_ID_PATTERN.matcher(customMachineId).matches()) {
123         machineId = parseMachineId(customMachineId);
124       }
125 
126       if (machineId == null) {
127         machineId = defaultMachineId();
128       }
129       return machineId;
130     } catch (final Throwable e) {//NOSONAR
131       return StringUtils.getRandom(MACHINE_ID_LEN);
132     }
133   }
134 
135   /**
136    * @return MAC address as int (truncated to 4 bytes instead of 6)
137    */
138   public static int macAddressAsInt() {
139     return (mac[3] & 0xFF) << 24 | (mac[2] & 0xFF) << 16 |
140            (mac[1] & 0xFF) << 8 | mac[0] & 0xFF;
141   }
142 
143   /**
144    * Up to the 6 first bytes will be used. If Null or less than 6 bytes, extra
145    * bytes will be randomly generated.
146    *
147    * @param mac the MAC address in byte format (up to the 6 first
148    *     bytes will
149    *     be used)
150    */
151   public static synchronized void setMac(final byte[] mac) {
152     if (mac == null) {
153       JvmProcessId.mac = StringUtils.getRandom(MACHINE_ID_LEN);
154     } else {
155       JvmProcessId.mac = Arrays.copyOf(mac, MACHINE_ID_LEN);
156       for (int i = mac.length; i < MACHINE_ID_LEN; i++) {
157         JvmProcessId.mac[i] = (byte) StringUtils.RANDOM.nextInt(256);
158       }
159     }
160     macInt = macAddressAsInt();
161   }
162 
163   /**
164    * @return positive - current is better, 0 - cannot tell from MAC addr,
165    *     negative - candidate is better.
166    */
167   private static int compareAddresses(final byte[] current,
168                                       final byte[] candidate) {
169     if (candidate == null) {
170       return 1;
171     }
172     // Must be EUI-48 or longer.
173     if (candidate.length < 6) {
174       return 1;
175     }
176     // Must not be filled with only 0 and 1.
177     boolean onlyZeroAndOne = true;
178     for (final byte b : candidate) {
179       if (b != 0 && b != 1) {
180         onlyZeroAndOne = false;
181         break;
182       }
183     }
184     if (onlyZeroAndOne) {
185       return 1;
186     }
187     // Must not be a multicast address
188     if ((candidate[0] & 1) != 0) {
189       return 1;
190     }
191     // Prefer globally unique address.
192     if ((current[0] & 2) == 0) {
193       if ((candidate[0] & 2) == 0) {
194         // Both current and candidate are globally unique addresses.
195         return 0;
196       } else {
197         // Only current is globally unique.
198         return 1;
199       }
200     } else {
201       if ((candidate[0] & 2) == 0) {
202         // Only candidate is globally unique.
203         return -1;
204       } else {
205         // Both current and candidate are non-unique.
206         return 0;
207       }
208     }
209   }
210 
211   /**
212    * @return positive - current is better, 0 - cannot tell, negative -
213    *     candidate
214    *     is better
215    */
216   private static int compareAddresses(final InetAddress current,
217                                       final InetAddress candidate) {
218     return scoreAddress(current) - scoreAddress(candidate);
219   }
220 
221   private static int scoreAddress(final InetAddress addr) {
222     if (addr.isAnyLocalAddress()) {
223       return 0;
224     }
225     if (addr.isMulticastAddress()) {
226       return 1;
227     }
228     if (addr.isLinkLocalAddress()) {
229       return 2;
230     }
231     if (addr.isSiteLocalAddress()) {
232       return 3;
233     }
234 
235     return 4;
236   }
237 
238   // pulled from http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-process-id
239   private static ClassLoader getSystemClassLoader() {
240     if (System.getSecurityManager() == null) {
241       return ClassLoader.getSystemClassLoader();
242     } else {
243       return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
244         @Override
245         public ClassLoader run() {
246           return ClassLoader.getSystemClassLoader();
247         }
248       });
249     }
250   }
251 
252   /**
253    * @param oldProcessId
254    * @param customProcessId
255    *
256    * @return the processId
257    */
258   private static int parseProcessId(final int oldProcessId,
259                                     final String customProcessId) {
260     int processId = oldProcessId;
261     try {
262       processId = Integer.parseInt(customProcessId);
263     } catch (final NumberFormatException e) {
264       // Malformed input.
265     }
266     if (processId < 0 || processId > MAX_PID) {
267       processId = -1;
268     }
269     return processId;
270   }
271 
272   /**
273    * @param loader
274    * @param emptyObjects
275    * @param emptyClasses
276    *
277    * @return the processId as String
278    */
279   private static String jvmProcessIdManagementFactory(final ClassLoader loader,
280                                                       final Object[] emptyObjects,
281                                                       final Class<?>[] emptyClasses) {
282     String value;
283     try {
284       // Invoke
285       // java.lang.management.ManagementFactory.getRuntimeMXBean().getName()
286       final Class<?> mgmtFactoryType =
287           Class.forName("java.lang.management.ManagementFactory", true, loader);
288       final Class<?> runtimeMxBeanType =
289           Class.forName("java.lang.management.RuntimeMXBean", true, loader);
290 
291       final Method getRuntimeMXBean =
292           mgmtFactoryType.getMethod("getRuntimeMXBean", emptyClasses);
293       final Object bean = getRuntimeMXBean.invoke(null, emptyObjects);
294       final Method getName =
295           runtimeMxBeanType.getDeclaredMethod("getName", emptyClasses);
296       value = (String) getName.invoke(bean, emptyObjects);
297     } catch (final Exception e) {
298       SysErrLogger.FAKE_LOGGER.syserr(
299           "Unable to get PID, try another way: " + e.getMessage());
300 
301       try {
302         // Invoke android.os.Process.myPid()
303         final Class<?> processType =
304             Class.forName("android.os.Process", true, loader);
305         final Method myPid = processType.getMethod("myPid", emptyClasses);
306         value = myPid.invoke(null, emptyObjects).toString();
307       } catch (final Exception e2) {
308         SysErrLogger.FAKE_LOGGER.syserr(
309             "Unable to get PID: " + e2.getMessage());
310         value = "";
311       }
312     }
313     return value;
314   }
315 
316   private static byte[] parseMachineId(String value) {
317     // Strip separators.
318     value = value.replaceAll("[:-]", "");
319 
320     final byte[] machineId = new byte[MACHINE_ID_LEN];
321     for (int i = 0; i < value.length() && i < MACHINE_ID_LEN; i += 2) {
322       machineId[i] = (byte) Integer.parseInt(value.substring(i, i + 2), 16);
323     }
324 
325     return machineId;
326   }
327 
328   private static byte[] defaultMachineId() {
329     // Find the best MAC address available.
330     final byte[] notFound = { -1 };
331     byte[] bestMacAddr = notFound;
332     InetAddress bestInetAddr;
333     try {
334       bestInetAddr = InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 });
335     } catch (final UnknownHostException e) {
336       // Never happens.
337       throw new IllegalArgumentException(e);
338     }
339 
340     // Retrieve the list of available network interfaces.
341     final Map<NetworkInterface, InetAddress> ifaces =
342         new LinkedHashMap<NetworkInterface, InetAddress>();
343     try {
344       for (final Enumeration<NetworkInterface> i =
345            NetworkInterface.getNetworkInterfaces(); i.hasMoreElements(); ) {
346         final NetworkInterface iface = i.nextElement();
347         // Use the interface with proper INET addresses only.
348         final Enumeration<InetAddress> addrs = iface.getInetAddresses();
349         if (addrs.hasMoreElements()) {
350           final InetAddress a = addrs.nextElement();
351           if (!a.isLoopbackAddress()) {
352             ifaces.put(iface, a);
353           }
354         }
355       }
356     } catch (final SocketException ignored) {
357       // nothing
358     }
359 
360     for (final Entry<NetworkInterface, InetAddress> entry : ifaces.entrySet()) {
361       final NetworkInterface iface = entry.getKey();
362       final InetAddress inetAddr = entry.getValue();
363       if (iface.isVirtual()) {
364         continue;
365       }
366 
367       final byte[] macAddr;
368       try {
369         macAddr = iface.getHardwareAddress();
370       } catch (final SocketException e) {
371         continue;
372       }
373 
374       boolean replace = false;
375       int res = compareAddresses(bestMacAddr, macAddr);
376       if (res < 0) {
377         // Found a better MAC address.
378         replace = true;
379       } else if (res == 0) {
380         // Two MAC addresses are of pretty much same quality.
381         res = compareAddresses(bestInetAddr, inetAddr);
382         if (res < 0) {
383           // Found a MAC address with better INET address.
384           replace = true;
385         } else if (res == 0) {
386           // Cannot tell the difference. Choose the longer one.
387           if (bestMacAddr.length < macAddr.length) {
388             replace = true;
389           }
390         }
391       }
392 
393       if (replace) {
394         bestMacAddr = macAddr;
395         bestInetAddr = inetAddr;
396       }
397     }
398 
399     if (bestMacAddr == notFound) {
400       bestMacAddr = StringUtils.getRandom(MACHINE_ID_LEN);
401     }
402     return bestMacAddr;
403   }
404 }