Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jvmid): provide method for determining JVM's own hash ID without JMX connection #309

Merged
merged 3 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/main/java/io/cryostat/core/JvmIdentifier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright The Cryostat Authors.
*
* 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.
*/
package io.cryostat.core;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;

import io.cryostat.core.net.IDException;
import io.cryostat.core.net.RuntimeMetrics;

import org.apache.commons.codec.digest.DigestUtils;

public class JvmIdentifier {

private final String hash;

private JvmIdentifier(String hash) {
this.hash = hash;
}

public static JvmIdentifier getLocal() throws IDException {
return from(RuntimeMetrics.readLocalMetrics());
}

public static JvmIdentifier from(RuntimeMetrics metrics) throws IDException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
DataOutputStream dos = new DataOutputStream(baos)) {
dos.writeUTF(metrics.getClassPath());
dos.writeUTF(metrics.getName());
dos.writeUTF(stringifyArray(metrics.getInputArguments()));
dos.writeUTF(metrics.getLibraryPath());
dos.writeUTF(metrics.getVmVendor());
dos.writeUTF(metrics.getVmVersion());
dos.writeLong(metrics.getStartTime());
byte[] hash = DigestUtils.sha256(baos.toByteArray());
return new JvmIdentifier(
new String(Base64.getUrlEncoder().encode(hash), StandardCharsets.UTF_8).trim());
} catch (IOException e) {
throw new IDException(e);
}
}

public String getHash() {
return hash;
}

private static String stringifyArray(Object arrayObject) {
String stringified;
String componentType = arrayObject.getClass().getComponentType().toString();
switch (componentType) {
case "boolean":
stringified = Arrays.toString((boolean[]) arrayObject);
break;

case "byte":
stringified = Arrays.toString((byte[]) arrayObject);
break;

case "char":
stringified = Arrays.toString((char[]) arrayObject);
break;

case "short":
stringified = Arrays.toString((short[]) arrayObject);
break;

case "int":
stringified = Arrays.toString((int[]) arrayObject);
break;

case "long":
stringified = Arrays.toString((long[]) arrayObject);
break;

case "float":
stringified = Arrays.toString((float[]) arrayObject);
break;

case "double":
stringified = Arrays.toString((double[]) arrayObject);
break;

default:
stringified = Arrays.toString((Object[]) arrayObject);
}
return stringified;
}

@Override
public String toString() {
return hash;
}

@Override
public int hashCode() {
return Objects.hash(hash);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
JvmIdentifier other = (JvmIdentifier) obj;
return Objects.equals(hash, other.hash);
}
}
8 changes: 7 additions & 1 deletion src/main/java/io/cryostat/core/net/JFRConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.openjdk.jmc.rjmx.IConnectionHandle;
import org.openjdk.jmc.rjmx.ServiceNotAvailableException;

import io.cryostat.core.JvmIdentifier;
import io.cryostat.core.sys.Clock;
import io.cryostat.core.templates.TemplateService;

Expand All @@ -46,7 +47,12 @@ public CryostatFlightRecorderService getService()

public int getPort();

public String getJvmId() throws IDException, IOException;
@Deprecated
public default String getJvmId() throws IDException, IOException {
return getJvmIdentifier().getHash();
}

public JvmIdentifier getJvmIdentifier() throws IDException, IOException;

public MBeanMetrics getMBeanMetrics()
throws ConnectionException, IOException, InstanceNotFoundException,
Expand Down
118 changes: 23 additions & 95 deletions src/main/java/io/cryostat/core/net/JFRJMXConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,21 @@
*/
package io.cryostat.core.net;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.management.MemoryUsage;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
Expand All @@ -53,15 +51,14 @@
import org.openjdk.jmc.rjmx.subscription.MRI;
import org.openjdk.jmc.rjmx.subscription.MRI.Type;

import io.cryostat.core.JvmIdentifier;
import io.cryostat.core.sys.Clock;
import io.cryostat.core.sys.Environment;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.core.templates.MergedTemplateService;
import io.cryostat.core.templates.TemplateService;
import io.cryostat.core.tui.ClientWriter;

import org.apache.commons.codec.digest.DigestUtils;

public class JFRJMXConnection implements JFRConnection {

public static final int DEFAULT_PORT = 9091;
Expand Down Expand Up @@ -146,27 +143,8 @@ public synchronized int getPort() {
}
}

public synchronized String getJvmId(RuntimeMetrics metrics) throws IOException {
if (!isConnected()) {
connect();
}
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
DataOutputStream dos = new DataOutputStream(baos)) {
dos.writeUTF(metrics.getClassPath());
dos.writeUTF(metrics.getName());
dos.writeUTF(stringifyArray(metrics.getInputArguments()));
dos.writeUTF(metrics.getLibraryPath());
dos.writeUTF(metrics.getVmVendor());
dos.writeUTF(metrics.getVmVersion());
dos.writeLong(metrics.getStartTime());
byte[] hash = DigestUtils.sha256(baos.toByteArray());
return new String(Base64.getUrlEncoder().encode(hash), StandardCharsets.UTF_8).trim();
} catch (IOException e) {
throw new IDException(e);
}
}

public synchronized String getJvmId() throws IDException, IOException {
@Override
public synchronized JvmIdentifier getJvmIdentifier() throws IDException, IOException {
if (!isConnected()) {
connect();
}
Expand All @@ -180,30 +158,13 @@ public synchronized String getJvmId() throws IDException, IOException {
"VmVendor",
"VmVersion",
"StartTime"));

try (ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
DataOutputStream dos = new DataOutputStream(baos)) {
for (String attr : attrNames) {
Object attrObject =
this.rjmxConnection.getAttributeValue(
new MRI(Type.ATTRIBUTE, ConnectionToolkit.RUNTIME_BEAN_NAME, attr));
if (attrObject != null) {
if (attrObject.getClass().isArray()) {
String stringified = stringifyArray(attrObject);
dos.writeUTF(stringified);
} else if (attrObject instanceof Long) {
dos.writeLong((Long) attrObject);
} else {
dos.writeUTF(attrObject.toString());
}
}
}
byte[] hash = DigestUtils.sha256(baos.toByteArray());
return new String(Base64.getUrlEncoder().encode(hash), StandardCharsets.UTF_8).trim();
} catch (AttributeNotFoundException
| InstanceNotFoundException
| MBeanException
| ReflectionException e) {
try {
return JvmIdentifier.from(
new RuntimeMetrics(
getAttributeMap(
ConnectionToolkit.RUNTIME_BEAN_NAME,
m -> attrNames.contains(m.getName()))));
} catch (ReflectionException | IntrospectionException | InstanceNotFoundException e) {
throw new IDException(e);
}
}
Expand Down Expand Up @@ -260,12 +221,21 @@ private Object parseObject(Object obj) {
private Map<String, Object> getAttributeMap(ObjectName beanName)
throws InstanceNotFoundException, IntrospectionException, ReflectionException,
IOException {
return getAttributeMap(beanName, m -> true);
}

private Map<String, Object> getAttributeMap(
ObjectName beanName, Predicate<MBeanAttributeInfo> attrPredicate)
throws InstanceNotFoundException, IntrospectionException, ReflectionException,
IOException {
Map<String, Object> attrMap = new HashMap<>();

var attrs = rjmxConnection.getMBeanInfo(beanName).getAttributes();

for (var attr : attrs) {
if (attr.isReadable() && !attr.getName().equals("ObjectName")) {
if (attr.isReadable()
&& !attr.getName().equals("ObjectName")
&& attrPredicate.test(attr)) {
try {
Object attrObject =
this.rjmxConnection.getAttributeValue(
Expand Down Expand Up @@ -304,49 +274,7 @@ public synchronized MBeanMetrics getMBeanMetrics()
new MemoryMetrics(memoryMap),
new ThreadMetrics(threadMap),
new OperatingSystemMetrics(osMap),
getJvmId(runtimeMetrics));
}

private String stringifyArray(Object arrayObject) {
String stringified;
String componentType = arrayObject.getClass().getComponentType().toString();
switch (componentType) {
case "boolean":
stringified = Arrays.toString((boolean[]) arrayObject);
break;

case "byte":
stringified = Arrays.toString((byte[]) arrayObject);
break;

case "char":
stringified = Arrays.toString((char[]) arrayObject);
break;

case "short":
stringified = Arrays.toString((short[]) arrayObject);
break;

case "int":
stringified = Arrays.toString((int[]) arrayObject);
break;

case "long":
stringified = Arrays.toString((long[]) arrayObject);
break;

case "float":
stringified = Arrays.toString((float[]) arrayObject);
break;

case "double":
stringified = Arrays.toString((double[]) arrayObject);
break;

default:
stringified = Arrays.toString((Object[]) arrayObject);
}
return stringified;
JvmIdentifier.from(runtimeMetrics).getHash());
}

public synchronized boolean isV1() throws ConnectionException, IOException {
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/io/cryostat/core/net/RuntimeMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/
package io.cryostat.core.net;

import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -61,6 +65,38 @@ public RuntimeMetrics(Map<String, Object> attributes) {
(boolean) attributes.getOrDefault("BootClassPathSupported", false);
}

public static RuntimeMetrics readLocalMetrics() {
RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
Map<String, Object> attrs = new HashMap<>();
store(attrs, "BootClassPath", bean::getBootClassPath);
store(attrs, "ClassPath", bean::getClassPath);
store(attrs, "InputArguments", () -> bean.getInputArguments().toArray(new String[0]));
store(attrs, "LibraryPath", bean::getLibraryPath);
store(attrs, "ManagementSpecVersion", bean::getManagementSpecVersion);
store(attrs, "Name", bean::getName);
store(attrs, "SpecName", bean::getSpecName);
store(attrs, "SpecVendor", bean::getSpecVendor);
store(attrs, "SpecVersion", bean::getSpecVersion);
store(attrs, "SystemProperties", bean::getSystemProperties);
store(attrs, "StartTime", bean::getStartTime);
store(attrs, "Uptime", bean::getUptime);
store(attrs, "VmName", bean::getVmName);
store(attrs, "VmVendor", bean::getVmVendor);
store(attrs, "VmVersion", bean::getVmVersion);
store(attrs, "BootClassPathSupported", bean::isBootClassPathSupported);
return new RuntimeMetrics(attrs);
}

private static void store(Map<String, Object> map, String key, Supplier<?> supplier) {
try {
Object s = supplier.get();
if (s != null) {
map.put(key, s);
}
} catch (UnsupportedOperationException __) {
}
}

public String getBootClassPath() {
return bootClassPath;
}
Expand Down
Loading