diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/HeapDumpSupport.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/HeapDumpSupport.java index 444974571abc..7bb974b018f7 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/HeapDumpSupport.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/HeapDumpSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -40,8 +40,14 @@ */ package org.graalvm.nativeimage.impl; +import java.io.IOException; + public interface HeapDumpSupport { - void dumpHeap(String outputFile, boolean live) throws java.io.IOException; + /** Overwrites the file if it already exists. */ + default void dumpHeap(String outputFile, boolean live) throws IOException { + dumpHeap(outputFile, live, true); + } + void dumpHeap(String outputFile, boolean live, boolean overwrite) throws IOException; } diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 7cd24cfb122f..5e776525ea71 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -9,6 +9,7 @@ At runtime, premain runtime options are set along with main class' arguments in The warning is planned to be replaced by an error in GraalVM for JDK 25. * (GR-48384) Added a GDB Python script (`gdb-debughelpers.py`) to improve the Native Image debugging experience. * (GR-49517) Add support for emitting Windows x64 unwind info. This enables stack walking in native tooling such as debuggers and profilers. +* (GR-56601) Together with Red Hat, we added experimental support for `jcmd` on Linux and macOS. * (GR-57384) Preserve the origin of a resource included in a native image. The information is included in the report produced by -H:+GenerateEmbeddedResourcesFile. * (GR-58000) Support for `GetStringUTFLengthAsLong` added in JNI_VERSION_24 ([JDK-8328877](https://bugs.openjdk.org/browse/JDK-8328877)) * (GR-58383) The length of the printed stack trace when using `-XX:MissingRegistrationReportingMode=Warn` can now be set with `-XX:MissingRegistrationWarnContextLines=` and its default length is now 8. diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixLogHandler.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixLogHandler.java index 344d2b6e868f..538464f404ed 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixLogHandler.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixLogHandler.java @@ -52,7 +52,7 @@ public void log(CCharPointer bytes, UnsignedWord length) { /* Save and restore errno around calls that would otherwise change errno. */ final int savedErrno = LibC.errno(); try { - if (!PosixUtils.writeBytes(getOutputFile(), bytes, length)) { + if (!PosixUtils.write(getOutputFile(), bytes, length)) { /* * We are in a low-level log routine and output failed, so there is little we can * do. diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixRawFileOperationSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixRawFileOperationSupport.java index af5da227d5fd..19648de2d7c6 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixRawFileOperationSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixRawFileOperationSupport.java @@ -40,11 +40,9 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; -import com.oracle.svm.core.headers.LibC; import com.oracle.svm.core.memory.UntrackedNullableNativeMemory; import com.oracle.svm.core.os.AbstractRawFileOperationSupport; import com.oracle.svm.core.os.AbstractRawFileOperationSupport.RawFileOperationSupportHolder; -import com.oracle.svm.core.posix.headers.Errno; import com.oracle.svm.core.posix.headers.Fcntl; import com.oracle.svm.core.posix.headers.Unistd; import com.oracle.svm.core.util.VMError; @@ -152,35 +150,14 @@ public boolean seek(RawFileDescriptor fd, long position) { @Override public boolean write(RawFileDescriptor fd, Pointer data, UnsignedWord size) { int posixFd = getPosixFileDescriptor(fd); - - Pointer position = data; - UnsignedWord remaining = size; - while (remaining.aboveThan(0)) { - SignedWord writtenBytes = Unistd.NoTransitions.write(posixFd, position, remaining); - if (writtenBytes.equal(-1)) { - if (LibC.errno() == Errno.EINTR()) { - // Retry the write if it was interrupted before any bytes were written. - continue; - } - return false; - } - position = position.add((UnsignedWord) writtenBytes); - remaining = remaining.subtract((UnsignedWord) writtenBytes); - } - return true; + return PosixUtils.writeUninterruptibly(posixFd, data, size); } @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) @Override public long read(RawFileDescriptor fd, Pointer buffer, UnsignedWord bufferSize) { int posixFd = getPosixFileDescriptor(fd); - - SignedWord readBytes; - do { - readBytes = Unistd.NoTransitions.read(posixFd, buffer, bufferSize); - } while (readBytes.equal(-1) && LibC.errno() == Errno.EINTR()); - - return readBytes.rawValue(); + return PosixUtils.readUninterruptibly(posixFd, buffer, bufferSize); } @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixStat.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixStat.java index e2f82f099ecb..e23bcad0ae1a 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixStat.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixStat.java @@ -42,6 +42,8 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; +import com.oracle.svm.core.headers.LibC; +import com.oracle.svm.core.posix.headers.Errno; import com.oracle.svm.core.posix.headers.PosixDirectives; import com.oracle.svm.core.posix.headers.darwin.DarwinStat; import com.oracle.svm.core.posix.headers.linux.LinuxStat; @@ -167,6 +169,26 @@ public interface stat extends PointerBase { UnsignedWord st_nlink(); } + @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") + public static int restartableFstat(int fd, PosixStat.stat buf) { + int result; + do { + result = PosixStat.NoTransitions.fstat(fd, buf); + } while (result == -1 && LibC.errno() == Errno.EINTR()); + + return result; + } + + @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") + public static int restartableLstat(CCharPointer path, PosixStat.stat buf) { + int result; + do { + result = PosixStat.NoTransitions.lstat(path, buf); + } while (result == -1 && LibC.errno() == Errno.EINTR()); + + return result; + } + @Platforms(Platform.HOSTED_ONLY.class) private PosixStat() { } diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixUtils.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixUtils.java index 95cac8f3b712..45433f961d33 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixUtils.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixUtils.java @@ -24,10 +24,10 @@ */ package com.oracle.svm.core.posix; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; import static com.oracle.svm.core.posix.headers.Unistd._SC_GETPW_R_SIZE_MAX; import java.io.FileDescriptor; -import java.io.IOException; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.StackValue; @@ -35,6 +35,7 @@ import org.graalvm.nativeimage.c.type.CIntPointer; import org.graalvm.nativeimage.c.type.CTypeConversion; import org.graalvm.nativeimage.c.type.CTypeConversion.CCharPointerHolder; +import org.graalvm.word.Pointer; import org.graalvm.word.PointerBase; import org.graalvm.word.SignedWord; import org.graalvm.word.UnsignedWord; @@ -48,6 +49,8 @@ import com.oracle.svm.core.c.libc.LibCBase; import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; import com.oracle.svm.core.headers.LibC; +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.hub.LayoutEncoding; import com.oracle.svm.core.memory.NullableNativeMemory; import com.oracle.svm.core.nmt.NmtCategory; import com.oracle.svm.core.posix.headers.Dlfcn; @@ -61,9 +64,12 @@ import com.oracle.svm.core.posix.headers.Wait; import com.oracle.svm.core.posix.headers.darwin.DarwinTime; import com.oracle.svm.core.posix.headers.linux.LinuxTime; +import com.oracle.svm.core.snippets.KnownIntrinsics; import com.oracle.svm.core.util.BasedOnJDKFile; import com.oracle.svm.core.util.VMError; +import jdk.graal.compiler.word.Word; + public class PosixUtils { static String setLocale(String category, String locale) { int intCategory = getCategory(category); @@ -140,10 +146,6 @@ public static String lastErrorString(String defaultMsg) { return errorString(errno, defaultMsg); } - public static IOException newIOExceptionWithLastError(String defaultMsg) { - return new IOException(lastErrorString(defaultMsg)); - } - /** Return the error string for the given error number, or a default message. */ public static String errorString(int errno, String defaultMsg) { String result = ""; @@ -194,25 +196,52 @@ public static int waitForProcessExit(int ppid) { * Low-level output of bytes already in native memory. This method is allocation free, so that * it can be used, e.g., in low-level logging routines. */ - public static boolean writeBytes(FileDescriptor descriptor, CCharPointer bytes, UnsignedWord length) { - CCharPointer curBuf = bytes; - UnsignedWord curLen = length; - while (curLen.notEqual(0)) { + public static boolean write(FileDescriptor descriptor, CCharPointer data, UnsignedWord size) { + CCharPointer position = data; + UnsignedWord remaining = size; + while (remaining.notEqual(0)) { int fd = getFD(descriptor); if (fd == -1) { return false; } - SignedWord n = Unistd.write(fd, curBuf, curLen); - if (n.equal(-1)) { + SignedWord writtenBytes = Unistd.write(fd, position, remaining); + if (writtenBytes.equal(-1)) { if (LibC.errno() == Errno.EINTR()) { // Retry the write if it was interrupted before any bytes were written. continue; } return false; } - curBuf = curBuf.addressOf(n); - curLen = curLen.subtract((UnsignedWord) n); + position = position.addressOf(writtenBytes); + remaining = remaining.subtract((UnsignedWord) writtenBytes); + } + return true; + } + + @Uninterruptible(reason = "Array must not move.") + public static boolean writeUninterruptibly(int fd, byte[] data) { + DynamicHub hub = KnownIntrinsics.readHub(data); + UnsignedWord baseOffset = LayoutEncoding.getArrayBaseOffset(hub.getLayoutEncoding()); + Pointer dataPtr = Word.objectToUntrackedPointer(data).add(baseOffset); + return writeUninterruptibly(fd, dataPtr, WordFactory.unsigned(data.length)); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public static boolean writeUninterruptibly(int fd, Pointer data, UnsignedWord size) { + Pointer position = data; + UnsignedWord remaining = size; + while (remaining.notEqual(0)) { + SignedWord writtenBytes = Unistd.NoTransitions.write(fd, position, remaining); + if (writtenBytes.equal(-1)) { + if (LibC.errno() == Errno.EINTR()) { + // Retry the write if it was interrupted before any bytes were written. + continue; + } + return false; + } + position = position.add((UnsignedWord) writtenBytes); + remaining = remaining.subtract((UnsignedWord) writtenBytes); } return true; } @@ -249,17 +278,35 @@ public static void checkStatusIs0(int status, String message) { VMError.guarantee(status == 0, message); } - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public static int readBytes(int fd, CCharPointer buffer, int bufferLen, int readOffset) { + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public static int readUninterruptibly(int fd, Pointer buffer, int bufferLen, int bufferOffset) { int readBytes = -1; - if (readOffset < bufferLen) { + if (bufferOffset < bufferLen) { do { - readBytes = (int) Unistd.NoTransitions.read(fd, buffer.addressOf(readOffset), WordFactory.unsigned(bufferLen - readOffset)).rawValue(); + readBytes = (int) Unistd.NoTransitions.read(fd, buffer.add(bufferOffset), WordFactory.unsigned(bufferLen - bufferOffset)).rawValue(); } while (readBytes == -1 && LibC.errno() == Errno.EINTR()); } return readBytes; } + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public static int readUninterruptibly(int fd, Pointer buffer, int bufferSize) { + VMError.guarantee(bufferSize >= 0); + long readBytes = readUninterruptibly(fd, buffer, WordFactory.unsigned(bufferSize)); + assert (int) readBytes == readBytes; + return (int) readBytes; + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public static long readUninterruptibly(int fd, Pointer buffer, UnsignedWord bufferSize) { + SignedWord readBytes; + do { + readBytes = Unistd.NoTransitions.read(fd, buffer, bufferSize); + } while (readBytes.equal(-1) && LibC.errno() == Errno.EINTR()); + + return readBytes.rawValue(); + } + // Checkstyle: stop @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public static int clock_gettime(int clock_id, Time.timespec ts) { diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/AttachHelper.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/AttachHelper.java new file mode 100644 index 000000000000..0bd032784b88 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/AttachHelper.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.posix.attach; + +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.function.CLibrary; +import org.graalvm.nativeimage.c.type.CCharPointer; + +/** C methods that are used to support the attach API. */ +@CLibrary(value = "libchelper", requireStatic = true) +public class AttachHelper { + @CFunction(value = "svm_attach_startup") + public static native void startup(CCharPointer path); + + @CFunction(value = "svm_attach_listener_cleanup") + public static native void listenerCleanup(int listenerSocket, CCharPointer path); + + /** Returns true if the socket file is valid. */ + @CFunction(value = "svm_attach_check_socket_file") + public static native boolean checkSocketFile(CCharPointer path); + + @CFunction(value = "svm_attach_is_init_trigger") + public static native boolean isInitTrigger(CCharPointer path); + + @CFunction(value = "svm_attach_create_listener") + public static native int createListener(CCharPointer path); + + @CFunction(value = "svm_attach_wait_for_request") + public static native int waitForRequest(int listenerSocket); + + @CFunction(value = "svm_attach_shutdown_socket") + public static native void shutdownSocket(int socket); +} diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/PosixAttachApiSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/PosixAttachApiSupport.java new file mode 100644 index 000000000000..749cae8306a0 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/PosixAttachApiSupport.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.posix.attach; + +import java.nio.file.Paths; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.nativeimage.c.type.CTypeConversion.CCharPointerHolder; + +import com.oracle.svm.core.attach.AttachApiFeature; +import com.oracle.svm.core.attach.AttachApiSupport; +import com.oracle.svm.core.attach.AttachListenerThread; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.jdk.management.Target_jdk_internal_vm_VMSupport; +import com.oracle.svm.core.util.BasedOnJDKFile; +import com.oracle.svm.core.util.VMError; + +import jdk.graal.compiler.api.replacements.Fold; + +/** The attach mechanism on Linux and macOS uses a UNIX domain socket for communication. */ +public class PosixAttachApiSupport implements AttachApiSupport { + private final ReentrantLock lock = new ReentrantLock(); + private final AtomicBoolean shutdownRequested = new AtomicBoolean(); + private State state; + private PosixAttachListenerThread attachListenerThread; + private String cachedSocketFilePath; + private int listener; + + @Platforms(Platform.HOSTED_ONLY.class) + public PosixAttachApiSupport() { + state = State.Uninitialized; + listener = -1; + } + + @Fold + public static PosixAttachApiSupport singleton() { + return ImageSingletons.lookup(PosixAttachApiSupport.class); + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L474-L490") + public void startup() { + String path = getSocketFilePath(); + try (CCharPointerHolder f = CTypeConversion.toCString(path)) { + AttachHelper.startup(f.get()); + } + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L537-L568") + public boolean isInitTrigger() { + String filename = ".attach_pid" + ProcessHandle.current().pid(); + if (isInitTrigger0(filename)) { + return true; + } + + String fallbackPath = Target_jdk_internal_vm_VMSupport.getVMTemporaryDirectory() + "/" + filename; + return isInitTrigger0(fallbackPath); + } + + private static boolean isInitTrigger0(String path) { + try (CCharPointerHolder f = CTypeConversion.toCString(path)) { + return AttachHelper.isInitTrigger(f.get()); + } + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L501-L523") + public void initialize() { + lock.lock(); + try { + if (state == State.Destroyed) { + /* Attaching isn't possible anymore. */ + return; + } + + if (state == State.Initialized) { + if (isSocketFileValid()) { + /* Nothing to do, already fully initialized. */ + return; + } + shutdown(false); + } + + assert state == State.Uninitialized; + if (createListener()) { + attachListenerThread = new PosixAttachListenerThread(listener); + attachListenerThread.start(); + state = State.Initialized; + } + } finally { + lock.unlock(); + } + } + + private boolean isSocketFileValid() { + assert lock.isHeldByCurrentThread(); + try (CCharPointerHolder f = CTypeConversion.toCString(getSocketFilePath())) { + return AttachHelper.checkSocketFile(f.get()); + } + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L170-L181") + public void shutdown(boolean inTeardownHook) { + if (!shutdownRequested.compareAndSet(false, true) && Thread.currentThread() instanceof AttachListenerThread) { + /* + * Another thread already does the shutdown, so return right away (the attach listener + * thread must not try to acquire the lock in that case because the thread that does the + * shutdown will block until the attach listener thread exited). + */ + return; + } + + shutdown0(inTeardownHook); + } + + private void shutdown0(boolean inTeardownHook) { + lock.lock(); + try { + if (state != State.Initialized) { + assert attachListenerThread == null; + shutdownRequested.set(false); + return; + } + + /* Shutdown the listener. This will also wake up the attach listener thread. */ + try (CCharPointerHolder f = CTypeConversion.toCString(getSocketFilePath())) { + AttachHelper.listenerCleanup(listener, f.get()); + listener = -1; + } + + if (attachListenerThread != Thread.currentThread()) { + /* Wait until the attach listener thread exits. */ + try { + attachListenerThread.join(); + } catch (InterruptedException e) { + throw VMError.shouldNotReachHere(e); + } + } + + attachListenerThread = null; + state = inTeardownHook ? State.Destroyed : State.Uninitialized; + shutdownRequested.set(false); + } finally { + lock.unlock(); + } + } + + private String getSocketFilePath() { + /* No need for synchronization - all threads will compute the same result. */ + if (cachedSocketFilePath == null) { + long pid = ProcessHandle.current().pid(); + String tempDir = Target_jdk_internal_vm_VMSupport.getVMTemporaryDirectory(); + cachedSocketFilePath = Paths.get(tempDir, ".java_pid" + pid).toString(); + } + return cachedSocketFilePath; + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L186-L250") + private boolean createListener() { + assert lock.isHeldByCurrentThread(); + + String path = getSocketFilePath(); + try (CCharPointerHolder p = CTypeConversion.toCString(path)) { + listener = AttachHelper.createListener(p.get()); + return listener != -1; + } + } + + private enum State { + Uninitialized, + Initialized, + Destroyed + } +} + +@AutomaticallyRegisteredFeature +class PosixAttachApiFeature extends AttachApiFeature { + @Override + public void afterRegistration(AfterRegistrationAccess access) { + PosixAttachApiSupport support = new PosixAttachApiSupport(); + ImageSingletons.add(AttachApiSupport.class, support); + ImageSingletons.add(PosixAttachApiSupport.class, support); + } +} diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/PosixAttachListenerThread.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/PosixAttachListenerThread.java new file mode 100644 index 000000000000..55b930a77782 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/PosixAttachListenerThread.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.posix.attach; + +import java.nio.charset.StandardCharsets; + +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.Pointer; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.attach.AttachListenerThread; +import com.oracle.svm.core.posix.PosixUtils; +import com.oracle.svm.core.posix.headers.Unistd; +import com.oracle.svm.core.util.BasedOnJDKFile; + +public final class PosixAttachListenerThread extends AttachListenerThread { + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L84") // + private static final String PROTOCOL_VERSION = "1"; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L259") // + private static final int VERSION_SIZE = 8; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L87") // + private static final int ATTACH_ERROR_BAD_VERSION = 101; + + /** + * Each attach request consists of a fixed number of zero-terminated UTF-8 strings. + * {@code } + */ + private static final int EXPECTED_STRING_COUNT = 2 + ARG_COUNT_MAX; + private static final int MAX_REQUEST_LEN = (VERSION_SIZE + 1) + (NAME_LENGTH_MAX + 1) + (ARG_COUNT_MAX * (ARG_LENGTH_MAX + 1)); + + private final int listener; + + public PosixAttachListenerThread(int listener) { + this.listener = listener; + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L354-L411") + protected PosixAttachOperation dequeue() { + while (true) { + int socket = AttachHelper.waitForRequest(listener); + if (socket == -1) { + return null; + } + + PosixAttachOperation op = readRequest(socket); + if (op == null) { + /* Close the socket and try again. */ + Unistd.NoTransitions.close(socket); + } else { + return op; + } + } + } + + /** This method reads and processes a single request from the socket. */ + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L258-L347") + private static PosixAttachOperation readRequest(int socket) { + int strCount = 0; + int[] stringEnds = new int[EXPECTED_STRING_COUNT]; + Pointer buf = StackValue.get(MAX_REQUEST_LEN); + + /* Read until all expected strings have been read, the buffer is full, or EOF. */ + int offset = 0; + do { + int n = PosixUtils.readUninterruptibly(socket, buf, MAX_REQUEST_LEN, offset); + if (n == -1) { + return null; + } else if (n == 0) { + break; + } + + int end = offset + n; + while (offset < end) { + if (buf.readByte(offset) == 0) { + /* End-of-string found. */ + stringEnds[strCount] = offset; + strCount++; + } + offset++; + } + } while (offset < MAX_REQUEST_LEN && strCount < EXPECTED_STRING_COUNT); + + if (strCount != EXPECTED_STRING_COUNT) { + /* Incomplete or invalid request. */ + return null; + } + + String version = decodeString(buf, stringEnds, 0); + if (!PROTOCOL_VERSION.equals(version)) { + complete(socket, ATTACH_ERROR_BAD_VERSION, null); + return null; + } + + String name = decodeString(buf, stringEnds, 1); + if (name.length() > NAME_LENGTH_MAX) { + return null; + } + + String arg0 = decodeString(buf, stringEnds, 2); + if (arg0.length() > ARG_LENGTH_MAX) { + return null; + } + + String arg1 = decodeString(buf, stringEnds, 3); + if (arg1.length() > ARG_LENGTH_MAX) { + return null; + } + + String arg2 = decodeString(buf, stringEnds, 4); + if (arg2.length() > ARG_LENGTH_MAX) { + return null; + } + + return new PosixAttachOperation(name, arg0, arg1, arg2, socket); + } + + private static String decodeString(Pointer buf, int[] stringEnds, int index) { + int start = index == 0 ? 0 : stringEnds[index - 1] + 1; + int length = stringEnds[index] - start; + assert length >= 0; + return CTypeConversion.toJavaString((CCharPointer) buf.add(start), WordFactory.unsigned(length), StandardCharsets.UTF_8); + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/os/posix/attachListener_posix.cpp#L436-L455") + private static void complete(int socket, int code, String response) { + /* Send the return code. */ + byte[] returnCodeData = Integer.toString(code).getBytes(StandardCharsets.UTF_8); + sendData(socket, returnCodeData); + + byte[] lineBreak = System.lineSeparator().getBytes(StandardCharsets.UTF_8); + sendData(socket, lineBreak); + + /* Send the actual response message. */ + if (response != null && !response.isEmpty()) { + byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); + sendData(socket, responseBytes); + sendData(socket, lineBreak); + } + + AttachHelper.shutdownSocket(socket); + Unistd.NoTransitions.close(socket); + } + + private static void sendData(int socket, byte[] data) { + PosixUtils.writeUninterruptibly(socket, data); + } + + private static class PosixAttachOperation extends AttachOperation { + private final int socket; + + PosixAttachOperation(String name, String arg0, String arg1, String arg2, int socket) { + super(name, arg0, arg1, arg2); + this.socket = socket; + } + + @Override + public void complete(int code, String response) { + PosixAttachListenerThread.complete(socket, code, response); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdParseException.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/package-info.java similarity index 78% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdParseException.java rename to substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/package-info.java index 4735dce5dde7..959261dd786e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdParseException.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/package-info.java @@ -1,6 +1,5 @@ /* * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,13 +23,8 @@ * questions. */ -package com.oracle.svm.core.dcmd; +@Platforms({Platform.LINUX.class, Platform.DARWIN.class}) +package com.oracle.svm.core.posix.attach; -/** For validation of input arguments. */ -public class DcmdParseException extends Exception { - public DcmdParseException(String message) { - super(message); - } - - private static final long serialVersionUID = 1L; -} +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/PosixPerfMemoryProvider.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/PosixPerfMemoryProvider.java index a844dcae9ddf..b05734d4e4fd 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/PosixPerfMemoryProvider.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/PosixPerfMemoryProvider.java @@ -52,12 +52,11 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.VMInspectionOptions; -import com.oracle.svm.core.annotate.Alias; -import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.headers.LibC; import com.oracle.svm.core.jdk.DirectByteBufferUtil; +import com.oracle.svm.core.jdk.management.Target_jdk_internal_vm_VMSupport; import com.oracle.svm.core.jvmstat.PerfManager; import com.oracle.svm.core.jvmstat.PerfMemoryPrologue; import com.oracle.svm.core.jvmstat.PerfMemoryProvider; @@ -378,7 +377,7 @@ private static SecureDirectory openDirectorySecure(CCharPointer directory) { @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) private static boolean isDirectorySecure(CCharPointer directory) { PosixStat.stat buf = StackValue.get(PosixStat.sizeOfStatStruct()); - int result = restartableLstat(directory, buf); + int result = PosixStat.restartableLstat(directory, buf); if (result == -1) { return false; } @@ -393,7 +392,7 @@ private static boolean isDirectorySecure(CCharPointer directory) { @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+13/src/hotspot/os/posix/perfMemory_posix.cpp#L249-L260") private static boolean isDirFdSecure(int dirFd) { PosixStat.stat buf = StackValue.get(PosixStat.sizeOfStatStruct()); - int result = restartableFstat(dirFd, buf); + int result = PosixStat.restartableFstat(dirFd, buf); if (result == -1) { return false; } @@ -403,7 +402,7 @@ private static boolean isDirFdSecure(int dirFd) { @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+13/src/hotspot/os/posix/perfMemory_posix.cpp#L408-L429") private static boolean isFileSecure(int fd) { PosixStat.stat buf = StackValue.get(PosixStat.sizeOfStatStruct()); - int result = restartableFstat(fd, buf); + int result = PosixStat.restartableFstat(fd, buf); if (result == -1) { return false; } @@ -513,26 +512,6 @@ private static int restartableFtruncate(int fd, int size) { return result; } - @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") - private static int restartableFstat(int fd, PosixStat.stat buf) { - int result; - do { - result = PosixStat.NoTransitions.fstat(fd, buf); - } while (result == -1 && LibC.errno() == Errno.EINTR()); - - return result; - } - - @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") - private static int restartableLstat(CCharPointer directory, PosixStat.stat buf) { - int result; - do { - result = PosixStat.NoTransitions.lstat(directory, buf); - } while (result == -1 && LibC.errno() == Errno.EINTR()); - - return result; - } - @Override @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+13/src/hotspot/os/posix/perfMemory_posix.cpp#L1112-L1128") public void teardown() { @@ -560,7 +539,6 @@ public void close() { } @AutomaticallyRegisteredFeature -@Platforms({LINUX.class, Platform.DARWIN.class}) class PosixPerfMemoryFeature implements InternalFeature { @Override public boolean isInConfiguration(IsInConfigurationAccess access) { @@ -572,10 +550,3 @@ public void afterRegistration(AfterRegistrationAccess access) { ImageSingletons.add(PerfMemoryProvider.class, new PosixPerfMemoryProvider()); } } - -@TargetClass(className = "jdk.internal.vm.VMSupport") -final class Target_jdk_internal_vm_VMSupport { - - @Alias - public static native String getVMTemporaryDirectory(); -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/Dcmd.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/package-info.java similarity index 69% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/Dcmd.java rename to substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/package-info.java index 7088125e75ff..0ff26cec9e22 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/Dcmd.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/package-info.java @@ -1,6 +1,5 @@ /* * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,24 +23,8 @@ * questions. */ -package com.oracle.svm.core.dcmd; +@Platforms({Platform.LINUX.class, Platform.DARWIN.class}) +package com.oracle.svm.core.posix.jvmstat; -public interface Dcmd { - /** - * Parse arguments and set internal fields to be used during execution. Returns a string to - * later be sent through the server socket as a response. - */ - String parseAndExecute(String[] arguments) throws DcmdParseException; - - String getName(); - - String getDescription(); - - String getImpact(); - - String[] getExample(); - - DcmdOption[] getOptions(); - - String printHelp(); -} +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/linux/LinuxImageHeapProvider.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/linux/LinuxImageHeapProvider.java index 5e37e73f6a2d..76687edc4684 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/linux/LinuxImageHeapProvider.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/linux/LinuxImageHeapProvider.java @@ -560,7 +560,7 @@ private static boolean checkImageFileMagic(int mapfd, int imagefd, CCharPointer return false; } - if (PosixUtils.readBytes(imagefd, buffer, wordSize, 0) != wordSize) { + if (PosixUtils.readUninterruptibly(imagefd, (Pointer) buffer, wordSize) != wordSize) { return false; } Word fileMagic = ((WordPointer) buffer).read(); diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/linux/ProcFSSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/linux/ProcFSSupport.java index fd0824bc948b..f9e68a418260 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/linux/ProcFSSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/linux/ProcFSSupport.java @@ -26,6 +26,7 @@ import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.WordPointer; +import org.graalvm.word.Pointer; import org.graalvm.word.UnsignedWord; import org.graalvm.word.WordFactory; @@ -76,7 +77,7 @@ static boolean findMapping(int fd, CCharPointer buffer, int bufferLen, UnsignedW UnsignedWord fileOffset = WordFactory.zero(); OUT: for (;;) { while (position == endOffset) { // fill buffer - int readBytes = PosixUtils.readBytes(fd, buffer, bufferLen, readOffset); + int readBytes = PosixUtils.readUninterruptibly(fd, (Pointer) buffer, bufferLen, readOffset); if (readBytes <= 0) { return false; // read failure or 0 == EOF -> not matched } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationOnSignalFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationOnSignalFeature.java index bebcea073967..cfaf48ba7b87 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationOnSignalFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationOnSignalFeature.java @@ -27,9 +27,7 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platform.WINDOWS; -import org.graalvm.nativeimage.ImageSingletons; -import com.oracle.svm.core.dcmd.DcmdSupport; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.graal.RuntimeCompilation; @@ -39,7 +37,6 @@ @AutomaticallyRegisteredFeature public class DumpRuntimeCompilationOnSignalFeature implements InternalFeature { - @Override public boolean isInConfiguration(IsInConfigurationAccess access) { return VMInspectionOptions.DumpRuntimeCompilationOnSignal.getValue() && !Platform.includedIn(WINDOWS.class) && RuntimeCompilation.isEnabled(); @@ -47,11 +44,7 @@ public boolean isInConfiguration(IsInConfigurationAccess access) { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { - if (VMInspectionOptions.hasAttachSupport()) { - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new DumpRuntimeCompilationDcmd()); - } else { - RuntimeSupport.getRuntimeSupport().addStartupHook(new DumpRuntimeCompilationStartupHook()); - } + RuntimeSupport.getRuntimeSupport().addStartupHook(new DumpRuntimeCompilationStartupHook()); } } @@ -73,5 +66,4 @@ static void install() { public void handle(Signal arg0) { DumpRuntimeCompilationSupport.dump(); } - } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksOnSignalFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SigQuitFeature.java similarity index 62% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksOnSignalFeature.java rename to substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SigQuitFeature.java index a189cec871ff..973a58ca4ffc 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksOnSignalFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SigQuitFeature.java @@ -27,50 +27,51 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platform.WINDOWS; -import org.graalvm.nativeimage.ImageSingletons; -import com.oracle.svm.core.dcmd.DcmdSupport; -import com.oracle.svm.core.thread.ThreadDumpStacksDcmd; +import com.oracle.svm.core.attach.AttachApiSupport; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.RuntimeSupport; +import com.oracle.svm.core.util.BasedOnJDKFile; import jdk.internal.misc.Signal; @AutomaticallyRegisteredFeature -public class DumpThreadStacksOnSignalFeature implements InternalFeature { - +public class SigQuitFeature implements InternalFeature { @Override public boolean isInConfiguration(IsInConfigurationAccess access) { - return VMInspectionOptions.hasThreadDumpSupport(); + return VMInspectionOptions.hasThreadDumpSupport() || VMInspectionOptions.hasJCmdSupport(); } @Override public void beforeAnalysis(BeforeAnalysisAccess access) { - if (Platform.includedIn(WINDOWS.class) || !VMInspectionOptions.hasAttachSupport()) { - RuntimeSupport.getRuntimeSupport().addStartupHook(new DumpThreadStacksOnSignalStartupHook()); - } else { - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new ThreadDumpStacksDcmd()); - } + RuntimeSupport.getRuntimeSupport().addStartupHook(new RegisterSigQuitHandlerStartupHook()); } } -final class DumpThreadStacksOnSignalStartupHook implements RuntimeSupport.Hook { +final class RegisterSigQuitHandlerStartupHook implements RuntimeSupport.Hook { @Override public void execute(boolean isFirstIsolate) { + if (AttachApiSupport.isPresent()) { + /* Must be executed before we register the signal handler. */ + AttachApiSupport.singleton().startup(); + } + if (isFirstIsolate) { - DumpAllStacks.install(); + String signal = Platform.includedIn(WINDOWS.class) ? "BREAK" : "QUIT"; + Signal.handle(new Signal(signal), new SigQuitHandler()); } } } -class DumpAllStacks implements Signal.Handler { - static void install() { - Signal.handle(Platform.includedIn(WINDOWS.class) ? new Signal("BREAK") : new Signal("QUIT"), new DumpAllStacks()); - } - +class SigQuitHandler implements Signal.Handler { @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/runtime/os.cpp#L388-L433") public void handle(Signal arg0) { - DumpThreadStacksSupport.dump(); + if (VMInspectionOptions.hasJCmdSupport() && AttachApiSupport.singleton().isInitTrigger()) { + AttachApiSupport.singleton().initialize(); + } else if (VMInspectionOptions.hasThreadDumpSupport()) { + DumpThreadStacksSupport.dump(); + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateDiagnostics.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateDiagnostics.java index 02ddfa788b82..ccd3b795dce6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateDiagnostics.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateDiagnostics.java @@ -850,7 +850,7 @@ public void printDiagnostics(Log log, ErrorContext context, int maxDiagnosticLev log.string("Build time information:").indent(true); VM vm = ImageSingletons.lookup(VM.class); - log.string("Version: ").string(vm.version).string(", ").string(vm.info).newline(); + log.string("Version: ").string(vm.vendorVersion).string(" (").string(vm.info).string("), JDK ").string(vm.version).newline(); Platform platform = ImageSingletons.lookup(Platform.class); log.string("Platform: ").string(platform.getOS()).string("/").string(platform.getArchitecture()).newline(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java index bd2aac9ca118..114c811bafa1 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java @@ -60,15 +60,23 @@ public final class VMInspectionOptions { private static final String MONITORING_JMXSERVER_NAME = "jmxserver"; private static final String MONITORING_THREADDUMP_NAME = "threaddump"; private static final String MONITORING_NMT_NAME = "nmt"; - private static final String MONITORING_ATTACH_NAME = "attach"; - - private static final List MONITORING_ALL_VALUES = List.of(MONITORING_HEAPDUMP_NAME, MONITORING_JFR_NAME, MONITORING_JVMSTAT_NAME, MONITORING_JMXCLIENT_NAME, MONITORING_JMXSERVER_NAME, - MONITORING_THREADDUMP_NAME, MONITORING_NMT_NAME, MONITORING_ATTACH_NAME, MONITORING_ALL_NAME, MONITORING_DEFAULT_NAME); - private static final List NOT_SUPPORTED_ON_WINDOWS = List.of(MONITORING_HEAPDUMP_NAME, MONITORING_JFR_NAME, MONITORING_JVMSTAT_NAME, MONITORING_JMXCLIENT_NAME, MONITORING_JMXSERVER_NAME); - private static final String MONITORING_ALLOWED_VALUES_TEXT = "'" + MONITORING_HEAPDUMP_NAME + "', '" + MONITORING_JFR_NAME + "', '" + MONITORING_JVMSTAT_NAME + "', '" + MONITORING_JMXSERVER_NAME + - "' (experimental), '" + MONITORING_JMXCLIENT_NAME + "' (experimental), '" + MONITORING_THREADDUMP_NAME + "', '" + MONITORING_NMT_NAME + "', '" + MONITORING_ATTACH_NAME + - "' (experimental), or '" + - MONITORING_ALL_NAME + "' (deprecated behavior: defaults to '" + MONITORING_ALL_NAME + "' if no argument is provided)"; + private static final String MONITORING_JCMD_NAME = "jcmd"; + + private static final Set MONITORING_ALL_VALUES = Set.of(MONITORING_HEAPDUMP_NAME, MONITORING_JFR_NAME, MONITORING_JVMSTAT_NAME, MONITORING_JMXCLIENT_NAME, MONITORING_JMXSERVER_NAME, + MONITORING_THREADDUMP_NAME, MONITORING_NMT_NAME, MONITORING_JCMD_NAME, MONITORING_ALL_NAME, MONITORING_DEFAULT_NAME); + private static final List NOT_SUPPORTED_ON_WINDOWS = List.of(MONITORING_HEAPDUMP_NAME, MONITORING_JFR_NAME, MONITORING_JVMSTAT_NAME, MONITORING_JMXCLIENT_NAME, MONITORING_JMXSERVER_NAME, + MONITORING_JCMD_NAME); + + private static final String MONITORING_ALLOWED_VALUES_TEXT = "" + + "'" + MONITORING_HEAPDUMP_NAME + "'" + + ", '" + MONITORING_JFR_NAME + "'" + + ", '" + MONITORING_JVMSTAT_NAME + "'" + + ", '" + MONITORING_JMXSERVER_NAME + "' (experimental)" + + ", '" + MONITORING_JMXCLIENT_NAME + "' (experimental)" + + ", '" + MONITORING_THREADDUMP_NAME + "'" + + ", '" + MONITORING_NMT_NAME + "' (experimental)" + + ", '" + MONITORING_JCMD_NAME + "' (experimental)" + + ", or '" + MONITORING_ALL_NAME + "' (deprecated behavior: defaults to '" + MONITORING_ALL_NAME + "' if no argument is provided)"; static { assert MONITORING_ALL_VALUES.stream().allMatch(v -> MONITORING_DEFAULT_NAME.equals(v) || MONITORING_ALLOWED_VALUES_TEXT.contains(v)) : "A value is missing in the user-facing help text"; @@ -82,7 +90,15 @@ public final class VMInspectionOptions { VMInspectionOptions::validateEnableMonitoringFeatures); @Option(help = "Dumps all runtime compiled methods on SIGUSR2.", type = OptionType.User) // - public static final HostedOptionKey DumpRuntimeCompilationOnSignal = new HostedOptionKey<>(false); + public static final HostedOptionKey DumpRuntimeCompilationOnSignal = new HostedOptionKey<>(false, VMInspectionOptions::notSupportedOnWindows); + + @Platforms(Platform.HOSTED_ONLY.class) + private static void notSupportedOnWindows(HostedOptionKey optionKey) { + if (Platform.includedIn(WINDOWS.class) && optionKey.getValue()) { + String msg = String.format("the option '%s' is not supported on Windows and will be ignored.", optionKey.getName()); + LogUtils.warning(msg); + } + } @Option(help = "Print native memory tracking statistics on shutdown if native memory tracking is enabled.", type = OptionType.User) // public static final RuntimeOptionKey PrintNMTStatistics = new RuntimeOptionKey<>(false); @@ -107,9 +123,9 @@ public static void validateEnableMonitoringFeatures(@SuppressWarnings("unused") Set notSupported = getEnabledMonitoringFeatures(); notSupported.retainAll(NOT_SUPPORTED_ON_WINDOWS); if (!notSupported.isEmpty()) { - String warning = String.format("the option '%s' contains value(s) that are not supported on Windows: %s. Those values will be ignored.", getDefaultMonitoringCommandArgument(), + String msg = String.format("the option '%s' contains value(s) that are not supported on Windows: %s. Those values will be ignored.", getDefaultMonitoringCommandArgument(), String.join(", ", notSupported)); - LogUtils.warning(warning); + LogUtils.warning(msg); } } } @@ -193,8 +209,8 @@ public static boolean hasNativeMemoryTrackingSupport() { } @Fold - public static boolean hasAttachSupport() { - return hasAllOrKeywordMonitoringSupport(MONITORING_ATTACH_NAME) && !Platform.includedIn(WINDOWS.class); + public static boolean hasJCmdSupport() { + return hasAllOrKeywordMonitoringSupport(MONITORING_JCMD_NAME) && !Platform.includedIn(WINDOWS.class); } static class DeprecatedOptions { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java index e82ff86c1bd3..95cd3ddf8787 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java @@ -23,55 +23,45 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ - package com.oracle.svm.core.attach; -import org.graalvm.nativeimage.ImageSingletons; +import java.util.Collections; +import java.util.List; + +import org.graalvm.nativeimage.hosted.Feature; +import com.oracle.svm.core.SigQuitFeature; import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.RuntimeSupport; -import jdk.internal.misc.Signal; +import com.oracle.svm.core.jdk.RuntimeSupportFeature; +/** + * The attach API mechanism uses platform-specific implementation (see {@link AttachApiSupport}) and + * a {@code SIGQUIT/SIGBREAK} signal handler (see {@link SigQuitFeature}). + */ @AutomaticallyRegisteredFeature public class AttachApiFeature implements InternalFeature { @Override public boolean isInConfiguration(IsInConfigurationAccess access) { - return VMInspectionOptions.hasAttachSupport(); + return VMInspectionOptions.hasJCmdSupport(); } @Override - public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeSupport.getRuntimeSupport().addStartupHook(new AttachApiStartupHook()); - RuntimeSupport.getRuntimeSupport().addShutdownHook(new AttachApiShutdownHook()); - ImageSingletons.add(AttachApiSupport.class, new AttachApiSupport()); + public List> getRequiredFeatures() { + return Collections.singletonList(RuntimeSupportFeature.class); } -} -final class AttachApiStartupHook implements RuntimeSupport.Hook { @Override - public void execute(boolean isFirstIsolate) { - if (isFirstIsolate) { - SigquitHandler.install(); - } + public void duringSetup(DuringSetupAccess access) { + RuntimeSupport.getRuntimeSupport().addShutdownHook(new AttachApiTeardownHook()); } } -final class AttachApiShutdownHook implements RuntimeSupport.Hook { +final class AttachApiTeardownHook implements RuntimeSupport.Hook { @Override public void execute(boolean isFirstIsolate) { - AttachApiSupport.singleton().teardown(); - } -} - -class SigquitHandler implements Signal.Handler { - static void install() { - Signal.handle(new Signal("QUIT"), new SigquitHandler()); - } - - @Override - public void handle(Signal arg0) { - AttachApiSupport.singleton().maybeInitialize(); + AttachApiSupport.singleton().shutdown(true); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java index cc9fcf5529bf..1c0a31b3211c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java @@ -23,155 +23,29 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ - package com.oracle.svm.core.attach; import org.graalvm.nativeimage.ImageSingletons; -import java.io.File; -import java.io.IOException; -import java.net.UnixDomainSocketAddress; -import java.nio.channels.ServerSocketChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.Set; -import com.oracle.svm.core.log.Log; - import jdk.graal.compiler.api.replacements.Fold; -import static java.net.StandardProtocolFamily.UNIX; - -/** - * This class is responsible for initialization/shutdown of the Attach-API. This includes performing - * the initialization handshake and setting up the UNIX domain sockets. Similar to Hotspot, once - * initialized, it will dispatch a dedicated thread to handle new connections. - */ -public class AttachApiSupport { - private boolean initialized; - private ServerSocketChannel serverChannel; - AttachListenerThread attachListenerThread; - private Path socketFile; - +/** Interface for the attach API mechanism. */ +public interface AttachApiSupport { @Fold - public static AttachApiSupport singleton() { - return ImageSingletons.lookup(AttachApiSupport.class); - } - - synchronized void maybeInitialize() { - if (isInitTrigger()) { - if (!initialized) { - init(); - } else if (!Files.exists(getSocketFilePath())) { - // If socket file is missing, but we're already initialized, restart. - teardown(); - init(); - } - } - } - - private synchronized void init() { - assert (!initialized); - // Set up Attach API unix domain socket - serverChannel = createServerSocket(); - if (serverChannel != null) { - attachListenerThread = new AttachListenerThread(serverChannel); - attachListenerThread.start(); - initialized = true; - } - } - - /** Stop dedicated thread. Close socket. Uninitialized. Can be initialized again. */ - public synchronized void teardown() { - if (initialized) { - try { - attachListenerThread.shutdown(); - attachListenerThread.join(); - serverChannel.close(); - socketFile = null; - initialized = false; - // .close() does not delete the file. - Files.deleteIfExists(getSocketFilePath()); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } + static boolean isPresent() { + return ImageSingletons.contains(AttachApiSupport.class); } - /** - * This method determines whether the SIGQUIT we've received is actually a signal to start the - * Attach API handshake. It is loosely based on AttachListener::is_init_trigger() in - * attachListener.cpp in jdk-24+2. - */ - private static boolean isInitTrigger() { - // Determine whether an attempt to use the Attach API is being made. - File attachFile = new File(".attach_pid" + ProcessHandle.current().pid()); - - if (!attachFile.exists()) { - // Check the alternate location. - String tempDir = System.getProperty("java.io.tmpdir"); - attachFile = new File(tempDir + "/.attach_pid" + ProcessHandle.current().pid()); - if (!attachFile.exists()) { - Log.log().string("Attach-API could not find .attach_pid file").newline(); - return false; - } - } - return true; + @Fold + static AttachApiSupport singleton() { + return ImageSingletons.lookup(AttachApiSupport.class); } - private Path getSocketFilePath() { - if (socketFile == null) { - socketFile = Paths.get(getSocketPathString()); - } - return socketFile; - } + void startup(); - private static String getSocketPathString() { - long pid = ProcessHandle.current().pid(); - String tempDir = System.getProperty("java.io.tmpdir"); - if (tempDir == null) { - tempDir = "/tmp"; - } - if (!Files.isDirectory(Paths.get(tempDir))) { - throw new RuntimeException("Could not find temporary directory."); - } - return tempDir + "/.java_pid" + pid; - } + boolean isInitTrigger(); - /** - * This method creates the server socket channel that will be handed over to the dedicated - * attach API listener thread. We must set specific permissions on the socket file, otherwise - * the client will not accept it. It is important that the permissions are set before the - * filename is set to the correct name the client is polling for. - */ - private ServerSocketChannel createServerSocket() { - String socketPathString = getSocketPathString(); - Path initialPath = Paths.get(socketPathString + "_tmp"); - Path finalPath = getSocketFilePath(); - var address = UnixDomainSocketAddress.of(initialPath); - ServerSocketChannel sc = null; - try { - sc = ServerSocketChannel.open(UNIX); - // Create the socket file - sc.bind(address); - // Change the file permissions - Set permissions = PosixFilePermissions.fromString("rw-------"); - Files.setPosixFilePermissions(initialPath, permissions); + void initialize(); - // Rename socket file so it can begin being used. - Files.move(initialPath, finalPath); - Files.deleteIfExists(initialPath); - } catch (IOException e) { - try { - Files.deleteIfExists(initialPath); - Files.deleteIfExists(finalPath); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - Log.log().string("Unable to create server socket. " + e).newline(); - } - return sc; - } + void shutdown(boolean inTeardownHook); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java index 9d0448675c59..718357e81510 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java @@ -26,241 +26,131 @@ package com.oracle.svm.core.attach; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ClosedByInterruptException; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.nio.charset.StandardCharsets; - -import org.graalvm.nativeimage.ImageSingletons; -import com.oracle.svm.core.dcmd.DcmdSupport; -import com.oracle.svm.core.dcmd.DcmdParseException; -import com.oracle.svm.core.log.Log; +import java.io.PrintWriter; +import java.io.StringWriter; + +import com.oracle.svm.core.dcmd.DCmd; +import com.oracle.svm.core.dcmd.DCmdSupport; +import com.oracle.svm.core.jni.headers.JNIErrors; +import com.oracle.svm.core.option.RuntimeOptionKey; +import com.oracle.svm.core.thread.PlatformThreads; import com.oracle.svm.core.util.BasedOnJDKFile; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.util.StringUtil; + +import jdk.graal.compiler.options.Option; /** - * This class is responsible for receiving connections and dispatching to the appropriate tool (jcmd - * etc). + * A dedicated listener thread that accepts client connections and that handles diagnostic command + * requests. At the moment, only jcmd is supported. */ -public final class AttachListenerThread extends Thread { - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/share/services/attachListener.hpp#L143") // - private static final int ARG_LENGTH_MAX = 1024; - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/share/services/attachListener.hpp#L144") // - private static final int ARG_COUNT_MAX = 3; - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/os/posix/attachListener_posix.cpp#L84") // - private static final char VERSION = '1'; - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/os/posix/attachListener_posix.cpp#L259") // - private static final int VERSION_SIZE = 8; - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/os/posix/attachListener_posix.cpp#L87") // - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java#L401") // - private static final String ATTACH_ERROR_BAD_VERSION = "101"; - private static final String RESPONSE_CODE_OK = "0"; - private static final String RESPONSE_CODE_BAD = "1"; +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/attachListener.cpp#L453-L467") +public abstract class AttachListenerThread extends Thread { private static final String JCMD_COMMAND_STRING = "jcmd"; - - private ServerSocketChannel serverSocketChannel; - private volatile boolean shutdown = false; - - public AttachListenerThread(ServerSocketChannel serverSocketChannel) { - super("AttachListener"); - this.serverSocketChannel = serverSocketChannel; - setDaemon(true); + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/attachListener.hpp#L142") // + protected static final int NAME_LENGTH_MAX = 16; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/attachListener.hpp#L143") // + protected static final int ARG_LENGTH_MAX = 1024; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/attachListener.hpp#L144") // + protected static final int ARG_COUNT_MAX = 3; + + @SuppressWarnings("this-escape") + // This constructor should be annotated with @BasedOnJDK instead of the class, see GR-59171. + public AttachListenerThread() { + super(PlatformThreads.singleton().systemGroup, "Attach Listener"); + this.setDaemon(true); } - /** - * The method is the main loop where the dedicated listener thread accepts client connections - * and handles commands. It is loosely based on AttachListenerThread::thread_entry in - * attachListener.cpp in jdk-24+2. - */ @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/attachListener.cpp#L377-L436") public void run() { - AttachRequest request = null; - while (true) { - try { - // Dequeue a connection from socket. May block inside here waiting for connections. - request = dequeue(serverSocketChannel); - - // Check if this thread been signalled to finish executing. - if (shutdown) { + try { + while (true) { + AttachOperation op = dequeue(); + if (op == null) { + /* Dequeue failed or shutdown. */ + AttachApiSupport.singleton().shutdown(false); return; } - // Find the correct handler to dispatch too. - if (request.name.equals(JCMD_COMMAND_STRING)) { - String response = jcmd(request.arguments); - sendResponse(request.clientChannel, response, RESPONSE_CODE_OK); + if (JCMD_COMMAND_STRING.equals(op.name)) { + handleJcmd(op); } else { - sendResponse(request.clientChannel, "Invalid Operation. Only JCMD is supported currently.", RESPONSE_CODE_BAD); + op.complete(JNIErrors.JNI_ERR(), "Invalid Operation. Only jcmd is supported currently."); } - - request.clientChannel.close(); - - } catch (IOException e) { - request.closeConnection(); - AttachApiSupport.singleton().teardown(); - } catch (DcmdParseException e) { - sendResponse(request.clientChannel, e.getMessage(), RESPONSE_CODE_BAD); - request.closeConnection(); } + } catch (Throwable e) { + VMError.shouldNotReachHere("Exception in attach listener thread", e); } } - /** - * This method will loop or block until a valid actionable request is received. It is loosely - * based on PosixAttachListener::dequeue() in attachListener_posix.cpp in jdk-24+2. - */ - private AttachRequest dequeue(ServerSocketChannel serverChannel) throws IOException { - AttachRequest request = new AttachRequest(); - while (true) { - - if (shutdown) { - return null; - } - - try { - // Block waiting for a connection - request.clientChannel = serverChannel.accept(); - } catch (ClosedByInterruptException e) { - // Allow unblocking if a teardown has been signalled. - return null; - } - - readRequest(request); - if (request.error != null) { - sendResponse(request.clientChannel, null, request.error); - request.reset(); - } else if (request.name == null || request.arguments == null) { - // Didn't get any usable request data. Try again. - request.reset(); - } else { - return request; - } + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/attachListener.cpp#L205-L217") + private static void handleJcmd(AttachOperation op) { + try { + /* jcmd only uses the first argument. */ + String response = parseAndExecute(op.arg0); + op.complete(JNIErrors.JNI_OK(), response); + } catch (Throwable e) { + handleException(op, e); } } - /** - * This method reads and processes a single request from the socket. It is loosely based on - * PosixAttachListener::read_request in attachListener_posix.cpp in jdk-24+2. - */ - private static void readRequest(AttachRequest request) throws IOException { - int expectedStringCount = 2 + ARG_COUNT_MAX; - int maxLen = (VERSION_SIZE + 1) + (ARG_LENGTH_MAX + 1) + (ARG_COUNT_MAX * (ARG_LENGTH_MAX + 1)); - int strCount = 0; - long left = maxLen; - ByteBuffer buf = ByteBuffer.allocate(maxLen); - - // The current position to inspect. - int bufIdx = 0; - // The start of the arguments. - int argIdx = 0; - // The start of the command type name. - int nameIdx = 0; - - // Start reading messages - while (strCount < expectedStringCount && left > 0) { - - // Do a single read. - int bytesRead = request.clientChannel.read(buf); - - // Check if finished or error. - if (bytesRead < 0) { - break; - } - - // Process data from a single read. - for (int i = 0; i < bytesRead; i++) { - if (buf.get(bufIdx) == 0) { - if (strCount == 0) { - // The first string should be the version identifier. - if ((char) buf.get(bufIdx - 1) == VERSION) { - nameIdx = bufIdx + 1; - } else { - /* - * Version is no good. Drain reads to avoid "Connection reset by peer" - * before sending error code and starting again. - */ - request.error = ATTACH_ERROR_BAD_VERSION; - } - } else if (strCount == 1) { - // The second string specifies the command type. - argIdx = bufIdx + 1; - request.name = StandardCharsets.UTF_8.decode(buf.slice(nameIdx, bufIdx - nameIdx)).toString(); - } - strCount++; - } - bufIdx++; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticFramework.cpp#L383-L420") + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticFramework.cpp#L422-L439") + private static String parseAndExecute(String input) throws Throwable { + String[] args = StringUtil.split(input, " "); + String cmdName = args[0]; + + /* Redirect to the help command if there is a corresponding argument in the input. */ + for (int i = 1; i < args.length; i++) { + String v = args[i]; + if ("-h".equals(v) || "--help".equals(v) || "-help".equals(v)) { + DCmd cmd = DCmdSupport.singleton().getCommand("help"); + return cmd.parseAndExecute("help " + cmdName); } - left -= bytesRead; } - // Only set arguments if we read real data. - if (argIdx > 0 && bufIdx > 0) { - // Remove non-printable characters from the result. - request.arguments = StandardCharsets.UTF_8.decode(buf.slice(argIdx, bufIdx - 1 - argIdx)).toString().replaceAll("\\P{Print}", ""); + /* Pass the input to the diagnostic command. */ + DCmd cmd = DCmdSupport.singleton().getCommand(cmdName); + if (cmd == null) { + throw new IllegalArgumentException("Unknown diagnostic command '" + cmdName + "'"); } + return cmd.parseAndExecute(input); } - private static String jcmd(String arguments) throws DcmdParseException { - return ImageSingletons.lookup(DcmdSupport.class).parseAndExecute(arguments); - } + private static void handleException(AttachOperation op, Throwable e) { + if (!Options.JCmdExceptionStackTrace.getValue()) { + op.complete(JNIErrors.JNI_ERR(), e.toString()); + return; + } - /** - * This method sends response data, or error data back to the client. It is loosely based on - * PosixAttachOperation::complete in in attachListener_posix.cpp in jdk-24+2. - */ - private static void sendResponse(SocketChannel clientChannel, String response, String code) { - try { - // Send result - ByteBuffer buffer = ByteBuffer.allocate(32); - buffer.clear(); - buffer.put((code + "\n").getBytes(StandardCharsets.UTF_8)); - buffer.flip(); - clientChannel.write(buffer); + StringWriter s = new StringWriter(); + e.printStackTrace(new PrintWriter(s)); - if (response != null && !response.isEmpty()) { - // Send data - byte[] responseBytes = response.getBytes(); - buffer = ByteBuffer.allocate(responseBytes.length); - buffer.clear(); - buffer.put(responseBytes); - buffer.flip(); - clientChannel.write(buffer); - } - } catch (IOException e) { - Log.log().string("Unable to send Attach API response: " + e).newline(); - } + /* jcmd swallows line breaks if JNI_ERR() is used, so use JNI_OK() instead. */ + op.complete(JNIErrors.JNI_OK(), s.toString()); } - /** This method is called to notify the listener thread that it should finish. */ - void shutdown() { - shutdown = true; - this.interrupt(); - } + protected abstract AttachOperation dequeue(); - /** This represents one individual connection/command request. */ - static class AttachRequest { - public String name; - public String arguments; - public SocketChannel clientChannel; - public String error; + public abstract static class AttachOperation { + private final String name; + private final String arg0; + @SuppressWarnings("unused") private final String arg1; + @SuppressWarnings("unused") private final String arg2; - public void reset() { - closeConnection(); - clientChannel = null; - error = null; - name = null; - arguments = null; + public AttachOperation(String name, String arg0, String arg1, String arg2) { + this.name = name; + this.arg0 = arg0; + this.arg1 = arg1; + this.arg2 = arg2; } - public void closeConnection() { - if (clientChannel != null && clientChannel.isConnected()) { - try { - clientChannel.close(); - } catch (IOException e) { - // Do nothing. - } - } - } + public abstract void complete(int code, String response); + } + + static class Options { + @Option(help = "Determines if stack traces are shown if exceptions occur in diagnostic commands that were triggered via jcmd.")// + public static final RuntimeOptionKey JCmdExceptionStackTrace = new RuntimeOptionKey<>(false); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDCmd.java new file mode 100644 index 000000000000..5c4c68b19a80 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDCmd.java @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.BasedOnJDKFile; +import com.oracle.svm.core.util.VMError; + +/* Abstract base class for diagnostic commands. */ +public abstract class AbstractDCmd implements DCmd { + private final String name; + private final String description; + private final Impact impact; + private final DCmdOption[] arguments; + private final DCmdOption[] options; + private final String[] examples; + + @Platforms(Platform.HOSTED_ONLY.class) + public AbstractDCmd(String name, String description, Impact impact) { + this(name, description, impact, new DCmdOption[0], new DCmdOption[0], new String[0]); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public AbstractDCmd(String name, String description, Impact impact, DCmdOption[] arguments, DCmdOption[] options) { + this(name, description, impact, arguments, options, new String[0]); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public AbstractDCmd(String name, String description, Impact impact, DCmdOption[] arguments, DCmdOption[] options, String[] examples) { + this.name = name; + this.description = description; + this.impact = impact; + this.arguments = arguments; + this.options = options; + this.examples = examples; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String parseAndExecute(String input) throws Throwable { + DCmdArguments args = parse(input); + return execute(args); + } + + protected abstract String execute(DCmdArguments args) throws Throwable; + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticFramework.cpp#L189-L220") + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticFramework.cpp#L234-L253") + private DCmdArguments parse(String input) { + DCmdArguments result = new DCmdArguments(); + DCmdArgCursor cursor = new DCmdArgCursor(input, ' '); + + /* Skip the first value in the input because it is the command-name. */ + boolean isCommandNamePresent = cursor.advance(); + assert isCommandNamePresent; + assert name.equals(cursor.getKey()); + assert cursor.getValue() == null; + + /* Iterate and parse the remaining input. */ + while (cursor.advance()) { + parseOption(cursor.getKey(), cursor.getValue(), result); + } + + /* Check that all mandatory arguments have been set. */ + for (DCmdOption arg : arguments) { + if (arg.isRequired() && !result.hasBeenSet(arg)) { + throw new IllegalArgumentException("The argument '" + arg.getName() + "' is mandatory."); + } + } + + /* Check that all mandatory options have been set. */ + for (DCmdOption option : options) { + if (option.isRequired() && !result.hasBeenSet(option)) { + throw new IllegalArgumentException("The option '" + option.getName() + "' is mandatory."); + } + } + + return result; + } + + private void parseOption(String left, String right, DCmdArguments result) { + DCmdOption matchingOption = findOption(left); + if (matchingOption != null) { + /* Found a matching option, so use the specified value. */ + Object value = parseValue(matchingOption, right); + result.set(matchingOption, value); + return; + } + + /* + * String doesn't match any option, so use the left part as the value for the next available + * argument (and completely ignore the right part). + */ + for (DCmdOption option : arguments) { + if (!result.hasBeenSet(option)) { + Object value = parseValue(option, left); + result.set(option, value); + return; + } + } + + throw new IllegalArgumentException("Unknown argument '" + left + "' in diagnostic command"); + } + + private DCmdOption findOption(String optionName) { + for (DCmdOption option : options) { + if (option.getName().equals(optionName)) { + return option; + } + } + return null; + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticArgument.cpp#L140-L166") + private static Object parseValue(DCmdOption option, String valueString) { + Class type = option.getType(); + if (type == Boolean.class) { + if (valueString == null || valueString.isEmpty() || "true".equals(valueString)) { + return Boolean.TRUE; + } else if ("false".equals(valueString)) { + return Boolean.FALSE; + } else { + throw new IllegalArgumentException("Boolean parsing error in command argument '" + option.getName() + "'. Could not parse: " + valueString + "."); + } + } else if (type == String.class) { + return valueString; + } else { + throw VMError.shouldNotReachHere("Unexpected option type: " + type); + } + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticFramework.cpp#L255-L299") + public String getHelp() { + String lineBreak = System.lineSeparator(); + StringBuilder sb = new StringBuilder(); + sb.append(getName()).append(lineBreak); + if (description != null) { + sb.append(description).append(lineBreak); + } + sb.append(lineBreak); + sb.append("Impact: ").append(impact.name()).append(lineBreak); + sb.append(lineBreak); + + String value = getSyntaxAndExamples(); + sb.append(value); + + return sb.toString(); + } + + protected String getSyntaxAndExamples() { + String lineBreak = System.lineSeparator(); + StringBuilder sb = new StringBuilder(); + sb.append("Syntax : ").append(getName()); + + if (options.length > 0) { + sb.append(" [options]"); + } + + for (DCmdOption option : arguments) { + sb.append(" "); + if (!option.isRequired()) { + sb.append("["); + } + sb.append("<").append(option.getName()).append(">"); + if (!option.isRequired()) { + sb.append("]"); + } + } + + if (arguments.length > 0) { + sb.append(lineBreak).append(lineBreak); + sb.append("Arguments:"); + for (DCmdOption arg : arguments) { + sb.append(lineBreak); + appendOption(sb, arg); + } + } + + if (options.length > 0) { + sb.append(lineBreak).append(lineBreak); + sb.append("Options: (options must be specified using the or = syntax)"); + for (DCmdOption option : options) { + sb.append(lineBreak); + appendOption(sb, option); + } + } + + if (examples.length > 0) { + sb.append(lineBreak).append(lineBreak); + sb.append("Example usage:"); + for (String example : examples) { + sb.append(lineBreak); + sb.append("\t").append(example); + } + } + + return sb.toString(); + } + + private static void appendOption(StringBuilder sb, DCmdOption option) { + sb.append("\t").append(option.getName()).append(" : "); + if (!option.isRequired()) { + sb.append("[optional] "); + } + sb.append(option.getDescription()); + sb.append(" (").append(typeToString(option)).append(", "); + if (option.getDefaultValue() != null) { + sb.append(option.getDefaultValue()); + } else { + sb.append("no default value"); + } + sb.append(")"); + } + + private static String typeToString(DCmdOption option) { + Class type = option.getType(); + if (type == Boolean.class) { + return "BOOLEAN"; + } else if (type == String.class) { + return "STRING"; + } else { + throw VMError.shouldNotReachHere("Unexpected option type: " + type); + } + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticFramework.hpp#L102-L121") + private static class DCmdArgCursor { + private final String input; + private final int length; + private final char delimiter; + + private int cursor; + private int keyPos; + private int keyLength; + private int valuePos; + private int valueLength; + + DCmdArgCursor(String input, char delimiter) { + this.input = input; + this.length = input.length(); + this.delimiter = delimiter; + } + + String getKey() { + if (keyLength == 0) { + return null; + } + return input.substring(keyPos, keyPos + keyLength); + } + + String getValue() { + if (valueLength == 0) { + return null; + } + return input.substring(valuePos, valuePos + valueLength); + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticFramework.cpp#L67-L145") + private boolean advance() { + /* Skip delimiters. */ + while (cursor < length - 1 && input.charAt(cursor) == delimiter) { + cursor++; + } + + /* Handle end of input. */ + if (cursor == length - 1 && input.charAt(cursor) == delimiter) { + keyPos = cursor; + keyLength = 0; + valuePos = cursor; + valueLength = 0; + return false; + } + + /* Extract first item (argument or option name). */ + keyPos = cursor; + boolean argHadQuotes = false; + while (cursor <= length - 1 && input.charAt(cursor) != '=' && input.charAt(cursor) != delimiter) { + /* Argument can be surrounded by single or double quotes. */ + if (input.charAt(cursor) == '\"' || input.charAt(cursor) == '\'') { + keyPos++; + char quote = input.charAt(cursor); + argHadQuotes = true; + while (cursor < length - 1) { + cursor++; + if (input.charAt(cursor) == quote && input.charAt(cursor - 1) != '\\') { + break; + } + } + if (input.charAt(cursor) != quote) { + throw new IllegalArgumentException("Format error in diagnostic command arguments"); + } + break; + } + cursor++; + } + + keyLength = cursor - keyPos; + if (argHadQuotes) { + /* If the argument was quoted, we need to step past the last quote here. */ + cursor++; + } + + /* Check if the argument has the = format. */ + if (cursor <= length - 1 && input.charAt(cursor) == '=') { + cursor++; + valuePos = cursor; + boolean valueHadQuotes = false; + /* Extract the value. */ + while (cursor <= length - 1 && input.charAt(cursor) != delimiter) { + /* Value can be surrounded by simple or double quotes. */ + if (input.charAt(cursor) == '\"' || input.charAt(cursor) == '\'') { + valuePos++; + char quote = input.charAt(cursor); + valueHadQuotes = true; + while (cursor < length - 1) { + cursor++; + if (input.charAt(cursor) == quote && input.charAt(cursor - 1) != '\\') { + break; + } + } + if (input.charAt(cursor) != quote) { + throw new IllegalArgumentException("Format error in diagnostic command arguments"); + } + break; + } + cursor++; + } + valueLength = cursor - valuePos; + if (valueHadQuotes) { + /* If the value was quoted, we need to step past the last quote here. */ + cursor++; + } + } else { + valuePos = 0; + valueLength = 0; + } + return keyLength != 0; + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDcmd.java deleted file mode 100644 index 154d8fb06944..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDcmd.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.dcmd; - -public abstract class AbstractDcmd implements Dcmd { - protected DcmdOption[] options; - protected String[] examples; - protected String name; - protected String description; - protected String impact; - - @Override - public DcmdOption[] getOptions() { - return options; - } - - @Override - public String[] getExample() { - return examples; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getDescription() { - return description; - } - - @Override - public String getImpact() { - return impact; - } - - @Override - public String printHelp() { - StringBuilder sb = new StringBuilder(); - sb.append(getName()).append("\n"); - if (getDescription() != null) { - sb.append(getDescription()).append("\n"); - } - sb.append("Impact: ").append(this.getImpact()).append("\n"); - sb.append("Syntax: ").append(getName()); - - if (getOptions() != null) { - sb.append(" [options]\n"); - sb.append("Options:\n"); - for (DcmdOption option : this.getOptions()) { - sb.append("\t").append(option.getName()).append(": "); - if (option.isRequired()) { - sb.append("[Required] "); - } else { - sb.append("[Optional] "); - } - sb.append(option.getDescription()); - if (option.getDefaultValue() != null) { - sb.append(" Default value: ").append(option.getDefaultValue()); - } - sb.append("\n"); - } - } - sb.append("\n"); - - if (getExample() != null) { - sb.append("Examples:\n"); - for (String example : this.getExample()) { - sb.append("\t").append(example).append("\n"); - } - sb.append("\n"); - } - return sb.toString(); - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractJfrDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractJfrDCmd.java new file mode 100644 index 000000000000..cd7a260037cc --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractJfrDCmd.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import com.oracle.svm.core.jfr.Target_jdk_jfr_internal_dcmd_AbstractDCmd; +import com.oracle.svm.core.util.VMError; + +import jdk.graal.compiler.serviceprovider.JavaVersionUtil; + +/** + * Base class for JFR-related diagnostic commands. Note that the JDK already implements those + * diagnostic commands, so we wrap and reuse the JDK implementations. + */ +public abstract class AbstractJfrDCmd extends AbstractDCmd { + public AbstractJfrDCmd(String name, String description, Impact impact) { + super(name, description, impact); + } + + @Override + public String parseAndExecute(String input) { + assert input.startsWith(getName()); + + Target_jdk_jfr_internal_dcmd_AbstractDCmd cmd = createDCmd(); + String args = input.substring(getName().length()); + String[] result = cmd.execute("attach", args, ' '); + return String.join(System.lineSeparator(), result); + } + + @Override + protected String execute(DCmdArguments args) throws Throwable { + throw VMError.shouldNotReachHereAtRuntime(); + } + + @Override + protected String getSyntaxAndExamples() { + Target_jdk_jfr_internal_dcmd_AbstractDCmd cmd = createDCmd(); + String[] lines = getHelp(cmd); + return String.join(System.lineSeparator(), lines); + } + + private static String[] getHelp(Target_jdk_jfr_internal_dcmd_AbstractDCmd cmd) { + if (JavaVersionUtil.JAVA_SPEC <= 21) { + return cmd.printHelp(); + } + return cmd.getHelp(); + } + + protected abstract Target_jdk_jfr_internal_dcmd_AbstractDCmd createDCmd(); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/CompilerDumpCodeCacheDCmd.java similarity index 73% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationDcmd.java rename to substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/CompilerDumpCodeCacheDCmd.java index b754c3c4fcd1..2c52de3d2132 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationDcmd.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/CompilerDumpCodeCacheDCmd.java @@ -23,24 +23,21 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ -package com.oracle.svm.core; +package com.oracle.svm.core.dcmd; -import com.oracle.svm.core.dcmd.AbstractDcmd; -import com.oracle.svm.core.dcmd.DcmdParseException; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; -public class DumpRuntimeCompilationDcmd extends AbstractDcmd { +import com.oracle.svm.core.DumpRuntimeCompilationSupport; - public DumpRuntimeCompilationDcmd() { - this.name = "VM.runtime_compilation"; - this.impact = "low"; +public class CompilerDumpCodeCacheDCmd extends AbstractDCmd { + @Platforms(Platform.HOSTED_ONLY.class) + public CompilerDumpCodeCacheDCmd() { + super("Compiler.dump_code_cache", "Print information about all compiled methods in the code cache.", Impact.Medium); } @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - if (arguments.length > 1) { - throw new DcmdParseException("Too many arguments specified"); - } - + public String execute(DCmdArguments args) throws Throwable { DumpRuntimeCompilationSupport.dump(); return "Dump created."; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmd.java new file mode 100644 index 000000000000..3c7d2d8e44e3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmd.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +/** Interface for diagnostic commands. */ +public interface DCmd { + String parseAndExecute(String args) throws Throwable; + + String getName(); + + String getDescription(); + + String getHelp(); + + /** + * Describes the intrusiveness of the diagnostic command on the Java Virtual Machine behavior. + * Some diagnostic commands can seriously disrupt the behavior of the Java Virtual Machine while + * other diagnostic commands have no serious impact on the JVM. + */ + enum Impact { + /** No safepoint needed. */ + Low, + + /** + * VM needs to reach a safepoint but the time spent in the safepoint is short (usually less + * than 10 ms). + */ + Medium, + + /** + * VM needs to reach a safepoint and the time spent in the safepoint is long (usually more + * than 10 ms). + */ + High + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdArguments.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdArguments.java new file mode 100644 index 000000000000..8719d0ba6d53 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdArguments.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.dcmd; + +import org.graalvm.collections.EconomicMap; + +import com.oracle.svm.core.util.BasedOnJDKFile; + +public class DCmdArguments { + private final EconomicMap, Object> values; + + public DCmdArguments() { + values = EconomicMap.create(); + } + + public boolean hasBeenSet(DCmdOption option) { + Object value = values.get(option); + return value != null; + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticArgument.cpp#L54-L68") + public void set(DCmdOption option, Object value) { + if (hasBeenSet(option)) { + throw new IllegalArgumentException("Duplicates in diagnostic command arguments"); + } + + assert value == null || option.getType().isAssignableFrom(value.getClass()); + values.put(option, value); + } + + @SuppressWarnings("unchecked") + public T get(DCmdOption option) { + Object value = values.get(option); + if (value == null) { + return option.getDefaultValue(); + } + return (T) value; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdFeature.java new file mode 100644 index 000000000000..e6db58ef1da4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdFeature.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.svm.core.JavaMainWrapper.JavaMainSupport; +import com.oracle.svm.core.VMInspectionOptions; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.graal.RuntimeCompilation; + +/** Registers the infrastructure for diagnostic commands. */ +@AutomaticallyRegisteredFeature +public class DCmdFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return VMInspectionOptions.hasJCmdSupport(); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + DCmdSupport dcmdSupport = new DCmdSupport(); + ImageSingletons.add(DCmdSupport.class, dcmdSupport); + + if (VMInspectionOptions.hasHeapDumpSupport()) { + dcmdSupport.registerCommand(new GCHeapDumpDCmd()); + } + + dcmdSupport.registerCommand(new GCRunDCmd()); + + if (VMInspectionOptions.hasJfrSupport()) { + dcmdSupport.registerCommand(new JfrStartDCmd()); + dcmdSupport.registerCommand(new JfrStopDCmd()); + dcmdSupport.registerCommand(new JfrCheckDCmd()); + dcmdSupport.registerCommand(new JfrDumpDCmd()); + } + + dcmdSupport.registerCommand(new ThreadDumpToFileDCmd()); + dcmdSupport.registerCommand(new ThreadPrintDCmd()); + + if (ImageSingletons.contains(JavaMainSupport.class)) { + dcmdSupport.registerCommand(new VMCommandLineDCmd()); + } + + if (VMInspectionOptions.hasNativeMemoryTrackingSupport()) { + dcmdSupport.registerCommand(new VMNativeMemoryDCmd()); + } + + if (RuntimeCompilation.isEnabled()) { + dcmdSupport.registerCommand(new CompilerDumpCodeCacheDCmd()); + } + + dcmdSupport.registerCommand(new VMSystemPropertiesDCmd()); + dcmdSupport.registerCommand(new VMUptimeDmd()); + dcmdSupport.registerCommand(new VMVersionDmd()); + + dcmdSupport.registerCommand(new HelpDCmd()); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdOption.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdOption.java similarity index 79% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdOption.java rename to substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdOption.java index 025a2c31c3e4..861aaaae69d6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdOption.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdOption.java @@ -26,19 +26,25 @@ package com.oracle.svm.core.dcmd; -public class DcmdOption { - private String name; - private String description; - private boolean required; - private String defaultValue; +public class DCmdOption { + private final Class type; + private final String name; + private final String description; + private final boolean required; + private final T defaultValue; - public DcmdOption(String name, String description, boolean required, String defaultValue) { + public DCmdOption(Class type, String name, String description, boolean required, T defaultValue) { + this.type = type; this.name = name; this.description = description; this.required = required; this.defaultValue = defaultValue; } + public Class getType() { + return type; + } + String getName() { return name; } @@ -51,7 +57,7 @@ boolean isRequired() { return required; } - String getDefaultValue() { + T getDefaultValue() { return defaultValue; } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdSupport.java new file mode 100644 index 000000000000..2ef1560a0a84 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdSupport.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import java.util.List; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform.HOSTED_ONLY; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.ImageHeapList; + +import jdk.graal.compiler.api.replacements.Fold; + +/** + * Diagnostic commands can only be registered at image build-time and are effectively singletons + * managed by this class. + */ +public class DCmdSupport { + private final List commands = ImageHeapList.create(DCmd.class); + + @Platforms(HOSTED_ONLY.class) + public DCmdSupport() { + } + + @Fold + public static DCmdSupport singleton() { + return ImageSingletons.lookup(DCmdSupport.class); + } + + @Platforms(HOSTED_ONLY.class) + public void registerCommand(DCmd cmd) { + commands.add(cmd); + } + + public DCmd getCommand(String name) { + for (DCmd cmd : commands) { + if (cmd.getName().equals(name)) { + return cmd; + } + } + return null; + } + + public List getCommands() { + return commands; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdSupport.java deleted file mode 100644 index 6742ff8c8a46..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdSupport.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.dcmd; - -import org.graalvm.nativeimage.Platforms; -import org.graalvm.nativeimage.Platform.HOSTED_ONLY; - -import java.util.ArrayList; -import java.util.List; - -/** - * Diagnostic commands should only be registered at build time and are effectively singletons - * managed by this class. The Attach-API uses this class directly, which then dispatches the - * appropriate diagnostic command to handle the request. - */ -public class DcmdSupport { - private List dcmds; - - @Platforms(HOSTED_ONLY.class) - public DcmdSupport() { - dcmds = new ArrayList<>(); - } - - Dcmd getDcmd(String cmdName) { - for (Dcmd dcmd : dcmds) { - if (dcmd.getName().equals(cmdName)) { - return dcmd; - } - } - return null; - } - - /** Should be called by relevant features that want to be accessed via diagnostic commands. */ - @Platforms(HOSTED_ONLY.class) - public void registerDcmd(Dcmd dcmd) { - dcmds.add(dcmd); - } - - String[] getRegisteredCommands() { - String[] commands = new String[dcmds.size()]; - for (int i = 0; i < dcmds.size(); i++) { - commands[i] = dcmds.get(i).getName(); - } - return commands; - } - - /** - * This method is to be used at runtime by the Attach-API. It connects the Attach-API with the - * DCMD infrastructure. - */ - public String parseAndExecute(String arguments) throws DcmdParseException { - String[] argumentsSplit = arguments.split(" "); - assert argumentsSplit.length > 0; - String cmdName = argumentsSplit[0]; - - Dcmd dcmd = getDcmd(cmdName); - - if (dcmd == null) { - return "The requested command is not supported."; - } - - return dcmd.parseAndExecute(argumentsSplit); - } - -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/GCHeapDumpDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/GCHeapDumpDCmd.java new file mode 100644 index 000000000000..359c940ab228 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/GCHeapDumpDCmd.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.heap.dump.HeapDumping; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L317-L343") +public class GCHeapDumpDCmd extends AbstractDCmd { + private static final DCmdOption FILENAME = new DCmdOption<>(String.class, "filename", "File path of where to put the heap dump", true, null); + private static final DCmdOption DUMP_ALL = new DCmdOption<>(Boolean.class, "-all", "Dump all objects, including unreachable objects", false, false); + private static final DCmdOption OVERWRITE = new DCmdOption<>(Boolean.class, "-overwrite", "If specified, the dump file will be overwritten if it exists", false, false); + + @Platforms(Platform.HOSTED_ONLY.class) + public GCHeapDumpDCmd() { + super("GC.heap_dump", "Generate a HPROF format dump of the Java heap.", Impact.High, new DCmdOption[]{FILENAME}, new DCmdOption[]{DUMP_ALL, OVERWRITE}); + } + + @Override + public String execute(DCmdArguments args) throws Throwable { + String path = args.get(FILENAME); + boolean gcBefore = !args.get(DUMP_ALL); + boolean overwrite = args.get(OVERWRITE); + + HeapDumping.singleton().dumpHeap(path, gcBefore, overwrite); + return "Dumped to: " + path; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/GCRunDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/GCRunDCmd.java new file mode 100644 index 000000000000..9e2973988c0b --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/GCRunDCmd.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.heap.GCCause; +import com.oracle.svm.core.heap.Heap; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L251-L262") +public class GCRunDCmd extends AbstractDCmd { + @Platforms(Platform.HOSTED_ONLY.class) + public GCRunDCmd() { + super("GC.run", "Call java.lang.System.gc().", Impact.High); + } + + @Override + public String execute(DCmdArguments args) throws Throwable { + Heap.getHeap().getGC().collect(GCCause.DiagnosticCommand); + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDCmd.java new file mode 100644 index 000000000000..44d28e3cc9ef --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDCmd.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L42-L57") +public class HelpDCmd extends AbstractDCmd { + private static final DCmdOption COMMAND_NAME = new DCmdOption<>(String.class, "command name", "The name of the command for which we want help", false, null); + private static final DCmdOption PRINT_ALL = new DCmdOption<>(Boolean.class, "-all", "Show help for all commands", false, false); + + @Platforms(Platform.HOSTED_ONLY.class) + public HelpDCmd() { + super("help", "For more information about a specific command use 'help '. With no argument this will show a list of available commands. 'help -all' will show help for all commands.", + Impact.Low, new DCmdOption[]{COMMAND_NAME}, new DCmdOption[]{PRINT_ALL}, + new String[]{ + "$ jcmd help Thread.dump_to_file" + }); + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.cpp#L188-L232") + public String execute(DCmdArguments args) throws Throwable { + if (args.get(PRINT_ALL)) { + String lineBreak = System.lineSeparator(); + StringBuilder response = new StringBuilder(); + for (DCmd cmd : DCmdSupport.singleton().getCommands()) { + response.append(cmd.getName()).append(lineBreak); + response.append("\t").append(cmd.getDescription()).append(lineBreak); + response.append(lineBreak); + } + return response.toString(); + } + + String commandName = args.get(COMMAND_NAME); + if (commandName == null) { + String lineBreak = System.lineSeparator(); + StringBuilder response = new StringBuilder("The following commands are available:").append(lineBreak); + for (DCmd cmd : DCmdSupport.singleton().getCommands()) { + response.append(cmd.getName()).append(lineBreak); + } + response.append(lineBreak); + response.append("For more information about a specific command use 'help '."); + return response.toString(); + } + + DCmd cmd = DCmdSupport.singleton().getCommand(commandName); + if (cmd == null) { + throw new IllegalArgumentException("Help unavailable : '" + commandName + "' : No such command"); + } + return cmd.getHelp(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDcmd.java deleted file mode 100644 index 8ba26d3b33a5..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDcmd.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.dcmd; - -import org.graalvm.nativeimage.ImageSingletons; - -public class HelpDcmd extends AbstractDcmd { - - public HelpDcmd() { - this.options = new DcmdOption[]{new DcmdOption("command name", "The name of the command for which we want help", false, null)}; - this.examples = new String[]{ - "$ jcmd help JFR.stop", - "$ jcmd help VM.native_memory" - }; - this.name = "help"; - this.description = "For more information about a specific command use 'help '. With no argument this will show a list of available commands."; - this.impact = "low"; - } - - @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - String commandName = null; - if (arguments.length > 1) { - commandName = arguments[1]; - } - if (arguments.length > 2) { - throw new DcmdParseException("Too many arguments specified"); - } - - if (commandName == null) { - String[] commands = ImageSingletons.lookup(DcmdSupport.class).getRegisteredCommands(); - StringBuilder sb = new StringBuilder(); - for (String command : commands) { - sb.append(command).append("\n"); - } - sb.append(getName()).append("\n"); - return sb.toString(); - } else { - Dcmd dcmd = ImageSingletons.lookup(DcmdSupport.class).getDcmd(commandName); - if (dcmd == null) { - throw new DcmdParseException("Specified command was not found: " + commandName); - } - return dcmd.printHelp(); - } - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrCheckDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrCheckDCmd.java new file mode 100644 index 000000000000..f3ea302ca5a2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrCheckDCmd.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jfr.Target_jdk_jfr_internal_dcmd_AbstractDCmd; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/jfr/dcmd/jfrDcmds.hpp#L106-L108") +public class JfrCheckDCmd extends AbstractJfrDCmd { + // This constructor should be annotated with @BasedOnJDK instead of the class, see GR-59171. + @Platforms(Platform.HOSTED_ONLY.class) + public JfrCheckDCmd() { + super("JFR.check", "Checks running JFR recording(s)", Impact.Low); + } + + @Override + protected Target_jdk_jfr_internal_dcmd_AbstractDCmd createDCmd() { + return SubstrateUtil.cast(new Target_jdk_jfr_internal_dcmd_DCmdCheck(), Target_jdk_jfr_internal_dcmd_AbstractDCmd.class); + } +} + +@TargetClass(className = "jdk.jfr.internal.dcmd.DCmdCheck") +final class Target_jdk_jfr_internal_dcmd_DCmdCheck { + @Alias + Target_jdk_jfr_internal_dcmd_DCmdCheck() { + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrDumpDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrDumpDCmd.java new file mode 100644 index 000000000000..94cf4b6421c1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrDumpDCmd.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jfr.Target_jdk_jfr_internal_dcmd_AbstractDCmd; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/jfr/dcmd/jfrDcmds.hpp#L81-L83") +public class JfrDumpDCmd extends AbstractJfrDCmd { + // This constructor should be annotated with @BasedOnJDK instead of the class, see GR-59171. + @Platforms(Platform.HOSTED_ONLY.class) + public JfrDumpDCmd() { + super("JFR.dump", "Copies contents of a JFR recording to file. Either the name or the recording id must be specified.", Impact.Medium); + } + + @Override + protected Target_jdk_jfr_internal_dcmd_AbstractDCmd createDCmd() { + return SubstrateUtil.cast(new Target_jdk_jfr_internal_dcmd_DCmdDump(), Target_jdk_jfr_internal_dcmd_AbstractDCmd.class); + } +} + +@TargetClass(className = "jdk.jfr.internal.dcmd.DCmdDump") +final class Target_jdk_jfr_internal_dcmd_DCmdDump { + @Alias + Target_jdk_jfr_internal_dcmd_DCmdDump() { + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrStartDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrStartDCmd.java new file mode 100644 index 000000000000..5ba6b45cd741 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrStartDCmd.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.jfr.Target_jdk_jfr_internal_dcmd_AbstractDCmd; +import com.oracle.svm.core.jfr.Target_jdk_jfr_internal_dcmd_DCmdStart; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/jfr/dcmd/jfrDcmds.hpp#L56-L58") +public class JfrStartDCmd extends AbstractJfrDCmd { + // This constructor should be annotated with @BasedOnJDK instead of the class, see GR-59171. + @Platforms(Platform.HOSTED_ONLY.class) + public JfrStartDCmd() { + super("JFR.start", "Starts a new JFR recording.", Impact.Medium); + } + + @Override + protected Target_jdk_jfr_internal_dcmd_AbstractDCmd createDCmd() { + return SubstrateUtil.cast(new Target_jdk_jfr_internal_dcmd_DCmdStart(), Target_jdk_jfr_internal_dcmd_AbstractDCmd.class); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrStopDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrStopDCmd.java new file mode 100644 index 000000000000..e8bd70b76ba2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/JfrStopDCmd.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jfr.Target_jdk_jfr_internal_dcmd_AbstractDCmd; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/jfr/dcmd/jfrDcmds.hpp#L131-L133") +public class JfrStopDCmd extends AbstractJfrDCmd { + // This constructor should be annotated with @BasedOnJDK instead of the class, see GR-59171. + @Platforms(Platform.HOSTED_ONLY.class) + public JfrStopDCmd() { + super("JFR.stop", "Stops a JFR recording.", Impact.Low); + } + + @Override + protected Target_jdk_jfr_internal_dcmd_AbstractDCmd createDCmd() { + return SubstrateUtil.cast(new Target_jdk_jfr_internal_dcmd_DCmdStop(), Target_jdk_jfr_internal_dcmd_AbstractDCmd.class); + } +} + +@TargetClass(className = "jdk.jfr.internal.dcmd.DCmdStop") +final class Target_jdk_jfr_internal_dcmd_DCmdStop { + @Alias + Target_jdk_jfr_internal_dcmd_DCmdStop() { + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/ThreadDumpToFileDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/ThreadDumpToFileDCmd.java new file mode 100644 index 000000000000..32edb6cc5a2f --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/ThreadDumpToFileDCmd.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.dcmd; + +import java.nio.charset.StandardCharsets; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.BasedOnJDKFile; + +import jdk.internal.vm.ThreadDumper; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L934-L958") +public class ThreadDumpToFileDCmd extends AbstractDCmd { + private static final DCmdOption FILEPATH = new DCmdOption<>(String.class, "filepath", "The file path to the output file", true, null); + private static final DCmdOption OVERWRITE = new DCmdOption<>(Boolean.class, "-overwrite", "May overwrite existing file", false, false); + private static final DCmdOption FORMAT = new DCmdOption<>(String.class, "-format", "Output format (\"plain\" or \"json\")", false, "plain"); + + @Platforms(Platform.HOSTED_ONLY.class) + public ThreadDumpToFileDCmd() { + super("Thread.dump_to_file", "Dump threads, with stack traces, to a file in plain text or JSON format.", Impact.Medium, + new DCmdOption[]{FILEPATH}, + new DCmdOption[]{OVERWRITE, FORMAT}, + new String[]{ + "$ jcmd Thread.dump_to_file /some/path/my_file.txt", + "$ jcmd Thread.dump_to_file -format=json -overwrite=true /some/path/my_file.json" + }); + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.cpp#L1110-L1161") + public String execute(DCmdArguments args) throws Throwable { + String path = args.get(FILEPATH); + boolean overwrite = args.get(OVERWRITE); + boolean useJson = "json".equals(args.get(FORMAT)); + byte[] reply = dumpThreads(useJson, path, overwrite); + return new String(reply, StandardCharsets.UTF_8); + } + + private static byte[] dumpThreads(boolean useJson, String path, boolean overwrite) { + if (useJson) { + return ThreadDumper.dumpThreadsToJson(path, overwrite); + } + return ThreadDumper.dumpThreads(path, overwrite); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpStacksDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/ThreadPrintDCmd.java similarity index 71% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpStacksDcmd.java rename to substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/ThreadPrintDCmd.java index c564c7ccb20c..ac8e09b49c35 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpStacksDcmd.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/ThreadPrintDCmd.java @@ -24,26 +24,23 @@ * questions. */ -package com.oracle.svm.core.thread; +package com.oracle.svm.core.dcmd; -import com.oracle.svm.core.DumpThreadStacksSupport; -import com.oracle.svm.core.dcmd.AbstractDcmd; - -import com.oracle.svm.core.dcmd.DcmdParseException; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; -public class ThreadDumpStacksDcmd extends AbstractDcmd { +import com.oracle.svm.core.DumpThreadStacksSupport; +import com.oracle.svm.core.util.BasedOnJDKFile; - public ThreadDumpStacksDcmd() { - this.name = "Thread.dump_stacks"; - this.description = "Dumps stacks of platform threads to log output."; - this.impact = "Medium"; +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L425-L445") +public class ThreadPrintDCmd extends AbstractDCmd { + @Platforms(Platform.HOSTED_ONLY.class) + public ThreadPrintDCmd() { + super("Thread.print", "Print all threads with stacktraces.", Impact.Medium); } @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - if (arguments.length > 1) { - throw new DcmdParseException("Too many arguments specified"); - } + public String execute(DCmdArguments args) throws Throwable { DumpThreadStacksSupport.dump(); return "Threads dumped."; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMCommandLineDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMCommandLineDCmd.java new file mode 100644 index 000000000000..9a945e363147 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMCommandLineDCmd.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.JavaMainWrapper.JavaMainSupport; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L75-L91") +public class VMCommandLineDCmd extends AbstractDCmd { + @Platforms(Platform.HOSTED_ONLY.class) + public VMCommandLineDCmd() { + super("VM.command_line", "Print the command line used to start this VM instance.", Impact.Low); + } + + @Override + public String execute(DCmdArguments args) throws Throwable { + String lineBreak = System.lineSeparator(); + StringBuilder result = new StringBuilder("VM Arguments:"); + + String[] mainArgs = ImageSingletons.lookup(JavaMainSupport.class).mainArgs; + if (mainArgs != null) { + result.append(lineBreak); + result.append("java_command: "); + for (String arg : mainArgs) { + result.append(arg).append(" "); + } + } + return result.toString(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMNativeMemoryDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMNativeMemoryDCmd.java new file mode 100644 index 000000000000..51d1c20c1632 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMNativeMemoryDCmd.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.nmt.NativeMemoryTracking; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/nmt/nmtDCmd.hpp#L49-L52") +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/nmt/nmtDCmd.cpp#L34-L64") +public class VMNativeMemoryDCmd extends AbstractDCmd { + private static final DCmdOption SUMMARY = new DCmdOption<>(Boolean.class, "summary", + "Request runtime to report current memory summary, which includes total reserved and committed memory, along with memory usage summary by each subsystem.", + false, false); + + // This constructor should be annotated with @BasedOnJDK instead of the class, see GR-59171. + @Platforms(Platform.HOSTED_ONLY.class) + public VMNativeMemoryDCmd() { + super("VM.native_memory", "Print native memory usage", Impact.Low, new DCmdOption[0], new DCmdOption[]{SUMMARY}, + new String[]{"$ jcmd VM.native_memory summary"}); + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/nmt/nmtDCmd.cpp#L72-L149") + public String execute(DCmdArguments args) throws Throwable { + boolean summary = args.get(SUMMARY); + if (args.hasBeenSet(SUMMARY) && !summary) { + return "No command to execute."; + } + return NativeMemoryTracking.singleton().generateReportString(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMSystemPropertiesDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMSystemPropertiesDCmd.java new file mode 100644 index 000000000000..e04677398879 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMSystemPropertiesDCmd.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import java.nio.charset.StandardCharsets; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.BasedOnJDKFile; + +import jdk.internal.vm.VMSupport; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L94-L110") +public class VMSystemPropertiesDCmd extends AbstractDCmd { + @Platforms(Platform.HOSTED_ONLY.class) + public VMSystemPropertiesDCmd() { + super("VM.system_properties", "Print system properties.", Impact.Low); + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/attachListener.cpp#L67-L109") + public String execute(DCmdArguments args) throws Throwable { + /* serializePropertiesToByteArray() explicitly returns a ISO_8859_1 encoded byte array. */ + byte[] bytes = VMSupport.serializePropertiesToByteArray(); + return new String(bytes, StandardCharsets.ISO_8859_1); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMUptimeDmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMUptimeDmd.java new file mode 100644 index 000000000000..db165d53e072 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMUptimeDmd.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.Isolates; +import com.oracle.svm.core.log.StringBuilderLog; +import com.oracle.svm.core.util.BasedOnJDKFile; +import com.oracle.svm.core.util.TimeUtils; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L219-L233") +public class VMUptimeDmd extends AbstractDCmd { + @Platforms(Platform.HOSTED_ONLY.class) + public VMUptimeDmd() { + super("VM.uptime", "Print VM uptime.", Impact.Low); + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.cpp#L393-L400") + public String execute(DCmdArguments args) throws Throwable { + StringBuilderLog log = new StringBuilderLog(); + log.rational(Isolates.getCurrentUptimeMillis(), TimeUtils.millisPerSecond, 3).string(" s").newline(); + return log.getResult(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMVersionDmd.java similarity index 61% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdFeature.java rename to substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMVersionDmd.java index fe1290e1c869..b6e1f92022c0 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMVersionDmd.java @@ -1,6 +1,5 @@ /* * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,25 +26,23 @@ package com.oracle.svm.core.dcmd; import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; -import com.oracle.svm.core.VMInspectionOptions; -import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; -import com.oracle.svm.core.feature.InternalFeature; -import com.oracle.svm.core.thread.ThreadDumpToFileDcmd; +import com.oracle.svm.core.VM; +import com.oracle.svm.core.util.BasedOnJDKFile; -@AutomaticallyRegisteredFeature -public class DcmdFeature implements InternalFeature { - - @Override - public boolean isInConfiguration(IsInConfigurationAccess access) { - return VMInspectionOptions.hasAttachSupport(); +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.hpp#L59-L73") +public class VMVersionDmd extends AbstractDCmd { + @Platforms(Platform.HOSTED_ONLY.class) + public VMVersionDmd() { + super("VM.version", "Print JVM version information.", Impact.Low); } @Override - public void afterRegistration(AfterRegistrationAccess access) { - ImageSingletons.add(DcmdSupport.class, new DcmdSupport()); - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new HelpDcmd()); - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new ThreadDumpToFileDcmd()); + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/diagnosticCommand.cpp#L234-L246") + public String execute(DCmdArguments args) throws Throwable { + VM vm = ImageSingletons.lookup(VM.class); + return vm.vendorVersion + " (" + vm.info + ")" + System.lineSeparator() + "JDK " + vm.version; } - } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java index 21645d729a49..13edc3851000 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java @@ -49,7 +49,8 @@ public class GCCause { @DuplicatedInNativeCode public static final GCCause TestGCInDeoptimizer = new GCCause("Test GC in deoptimizer", 2); @DuplicatedInNativeCode public static final GCCause HintedGC = new GCCause("Hinted GC", 3); @DuplicatedInNativeCode public static final GCCause JvmtiForceGC = new GCCause("JvmtiEnv ForceGarbageCollection", 4); - @DuplicatedInNativeCode public static final GCCause HeapDump = new GCCause("Heap Dump Initiated GC ", 5); + @DuplicatedInNativeCode public static final GCCause HeapDump = new GCCause("Heap Dump Initiated GC", 5); + @DuplicatedInNativeCode public static final GCCause DiagnosticCommand = new GCCause("Diagnostic Command", 6); private final int id; private final String name; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpDcmd.java deleted file mode 100644 index 9a6f6966f2cd..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpDcmd.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.heap.dump; - -import com.oracle.svm.core.dcmd.AbstractDcmd; -import com.oracle.svm.core.dcmd.DcmdOption; - -import java.io.IOException; -import com.oracle.svm.core.dcmd.DcmdParseException; - -public class HeapDumpDcmd extends AbstractDcmd { - - public HeapDumpDcmd() { - this.options = new DcmdOption[]{ - new DcmdOption("filename", "File path of where to put the heap dump.", true, null) - }; - - this.name = "GC.heap_dump"; - this.description = "Generate a HPROF format dump of the heap."; - this.impact = "medium"; - } - - @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - String path = null; - if (arguments.length != 2) { - throw new DcmdParseException("Must specify file to dump to."); - } - if (arguments[1].contains("filename=")) { - String[] pathArgumentSplit = arguments[1].split("="); - if (pathArgumentSplit.length != 2) { - throw new DcmdParseException("Must specify file to dump to."); - } - path = pathArgumentSplit[1]; - } - - if (path == null) { - return "The argument 'filename' is mandatory."; - } - try { - HeapDumping.singleton().dumpHeap(path, true); - } catch (IOException e) { - return "Could not dump heap: " + e; - } - return "Dumped to: " + path; - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpSupportImpl.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpSupportImpl.java index 365136729221..4e86832cd10a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpSupportImpl.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpSupportImpl.java @@ -132,8 +132,9 @@ private void dumpHeapOnOutOfMemoryError0() { } @Override - public void dumpHeap(String filename, boolean gcBefore) throws IOException { - RawFileDescriptor fd = getFileSupport().create(filename, FileCreationMode.CREATE_OR_REPLACE, RawFileOperationSupport.FileAccessMode.READ_WRITE); + public void dumpHeap(String filename, boolean gcBefore, boolean overwrite) throws IOException { + FileCreationMode creationMode = overwrite ? FileCreationMode.CREATE_OR_REPLACE : FileCreationMode.CREATE; + RawFileDescriptor fd = getFileSupport().create(filename, creationMode, RawFileOperationSupport.FileAccessMode.READ_WRITE); if (!getFileSupport().isValid(fd)) { throw new IOException("Could not create the heap dump file: " + filename); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jcmd/JCmdFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jcmd/JCmdFeature.java new file mode 100644 index 000000000000..74744eb23da5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jcmd/JCmdFeature.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jcmd; + +import java.util.List; + +import org.graalvm.nativeimage.hosted.Feature; + +import com.oracle.svm.core.SigQuitFeature; +import com.oracle.svm.core.VMInspectionOptions; +import com.oracle.svm.core.attach.AttachApiFeature; +import com.oracle.svm.core.attach.AttachApiSupport; +import com.oracle.svm.core.attach.AttachListenerThread; +import com.oracle.svm.core.dcmd.DCmd; +import com.oracle.svm.core.dcmd.DCmdFeature; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; + +/** + * jcmd can be used to send diagnostic command requests to a running JVM. + * + * Below is a rough high-level overview of how the interaction between jcmd and Native Image works: + *
    + *
  • In a Native Image startup hook (see {@link SigQuitFeature}), we check if an old socket file + * exists and delete it if necessary. Then, we register a {@code SIGQUIT/SIGBREAK} signal + * handler.
  • + *
  • jcmd creates a {@code .attach_pid} file in a well-known directory and raises a + * {@code SIGQUIT} signal.
  • + *
  • Native Image handles the signal in a Java thread and initializes the {@link AttachApiSupport} + * if the {@code .attach_pid} file is detected: + *
      + *
    • A domain socket is created and bound to the file {@code .java_pid}.
    • + *
    • A dedicated {@link AttachListenerThread listener thread} is started that acts as a + * single-threaded server. It waits for a client to connect, reads a request, executes it, and + * returns the response to the client via the socket connection.
    • + *
    + *
  • Once jcmd detects the file for the domain socket, it can connect to the same socket and + * communicate with Native Image. It may then request the execution of {@link DCmd diagnostic + * commands}.
  • + *
+ */ +@AutomaticallyRegisteredFeature +public class JCmdFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return VMInspectionOptions.hasJCmdSupport(); + } + + @Override + public List> getRequiredFeatures() { + return List.of(SigQuitFeature.class, AttachApiFeature.class, DCmdFeature.class); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/Target_jdk_internal_vm_VMSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/Target_jdk_internal_vm_VMSupport.java index f9ca0dc1d1b4..db6ac96e4525 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/Target_jdk_internal_vm_VMSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/Target_jdk_internal_vm_VMSupport.java @@ -26,16 +26,20 @@ package com.oracle.svm.core.jdk.management; +import java.util.Properties; + +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.svm.core.JavaMainWrapper.JavaMainSupport; +import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.annotate.TargetElement; -import java.util.Properties; -import com.oracle.svm.core.JavaMainWrapper.JavaMainSupport; -import org.graalvm.nativeimage.ImageSingletons; - @TargetClass(jdk.internal.vm.VMSupport.class) -final class Target_jdk_internal_vm_VMSupport { +public final class Target_jdk_internal_vm_VMSupport { + @Alias + public static native String getVMTemporaryDirectory(); @Substitute @TargetElement(onlyWith = JmxServerIncluded.class) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java index a81c28eb5e9b..2cd2a792909a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java @@ -26,36 +26,34 @@ package com.oracle.svm.core.jfr; -import com.oracle.svm.core.option.RuntimeOptionKey; - import java.io.Serial; - -import java.time.Duration; import java.util.HashMap; import java.util.Map; +import com.oracle.svm.core.option.RuntimeOptionKey; +import com.oracle.svm.util.StringUtil; + import jdk.graal.compiler.core.common.SuppressFBWarnings; public class JfrArgumentParser { - private static final String DEFAULT_JFC_NAME = "default"; - public static Map parseJfrOptions(RuntimeOptionKey runtimeOptionKey, JfrArgument[] possibleArguments) throws JfrArgumentParsingFailed { String userInput = runtimeOptionKey.getValue(); if (!userInput.isEmpty()) { - String[] options = userInput.split(","); + String[] options = StringUtil.split(userInput, ","); return parseJfrOptions(options, possibleArguments); } return new HashMap<>(); } - public static Map parseJfrOptions(String[] options, JfrArgument[] possibleArguments) throws JfrArgumentParsingFailed { + private static Map parseJfrOptions(String[] options, JfrArgument[] possibleArguments) throws JfrArgumentParsingFailed { Map optionsMap = new HashMap<>(); for (String option : options) { - String[] keyVal = option.split("="); + String[] keyVal = StringUtil.split(option, "="); if (keyVal.length != 2) { - throw new JfrArgumentParsingFailed("Invalid command structure."); + throw new JfrArgumentParsingFailed("Invalid argument '" + keyVal[0] + "' in JFR options"); } + JfrArgument arg = findArgument(possibleArguments, keyVal[0]); if (arg == null) { throw new JfrArgumentParsingFailed("Unknown argument '" + keyVal[0] + "' in JFR options"); @@ -66,17 +64,6 @@ public static Map parseJfrOptions(String[] options, JfrArgu return optionsMap; } - public static String[] parseSettings(Map args) { - String settings = args.get(JfrStartArgument.Settings); - if (settings == null) { - return new String[]{DEFAULT_JFC_NAME}; - } else if (settings.equals("none")) { - return new String[0]; - } else { - return settings.split(","); - } - } - @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null allowed as return value") public static Boolean parseBoolean(Map args, JfrArgument key) throws JfrArgumentParsingFailed { String value = args.get(key); @@ -91,44 +78,6 @@ public static Boolean parseBoolean(Map args, JfrArgument ke } } - public static Long parseDuration(Map args, JfrArgument key) throws JfrArgumentParsingFailed { - String value = args.get(key); - if (value != null) { - try { - int idx = indexOfFirstNonDigitCharacter(value); - long time; - try { - time = Long.parseLong(value.substring(0, idx)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Expected a number."); - } - - if (idx == value.length()) { - // only accept missing unit if the value is 0 - if (time != 0) { - throw new IllegalArgumentException("Unit is required."); - } - return 0L; - } - - String unit = value.substring(idx); - return switch (unit) { - case "ns" -> Duration.ofNanos(time).toNanos(); - case "us" -> Duration.ofNanos(time * 1000).toNanos(); - case "ms" -> Duration.ofMillis(time).toNanos(); - case "s" -> Duration.ofSeconds(time).toNanos(); - case "m" -> Duration.ofMinutes(time).toNanos(); - case "h" -> Duration.ofHours(time).toNanos(); - case "d" -> Duration.ofDays(time).toNanos(); - default -> throw new IllegalArgumentException("Unit is invalid."); - }; - } catch (IllegalArgumentException e) { - throw new JfrArgumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); - } - } - return null; - } - public static Integer parseInteger(Map args, JfrArgument key) throws JfrArgumentParsingFailed { String value = args.get(key); if (value != null) { @@ -161,7 +110,7 @@ public static Long parseMaxSize(Map args, JfrArgument key) return number; } - final char unit = value.substring(idx).charAt(0); + char unit = value.substring(idx).charAt(0); return switch (unit) { case 'k', 'K' -> number * 1024; case 'm', 'M' -> number * 1024 * 1024; @@ -194,34 +143,6 @@ public interface JfrArgument { String getCmdLineKey(); } - /** - * Options available with the JFR.start diagnostic command or when starting JFR upon launching - * the application. - */ - public enum JfrStartArgument implements JfrArgument { - Name("name"), - Settings("settings"), - Delay("delay"), - Duration("duration"), - Filename("filename"), - Disk("disk"), - MaxAge("maxage"), - MaxSize("maxsize"), - DumpOnExit("dumponexit"), - PathToGCRoots("path-to-gc-roots"); - - private final String cmdLineKey; - - JfrStartArgument(String key) { - this.cmdLineKey = key; - } - - @Override - public String getCmdLineKey() { - return cmdLineKey; - } - } - public enum FlightRecorderOptionsArgument implements JfrArgument { GlobalBufferSize("globalbuffersize"), MaxChunkSize("maxchunksize"), @@ -244,46 +165,7 @@ public String getCmdLineKey() { } } - /** Options available with the JFR.dump diagnostic command. */ - public enum DumpArgument implements JfrArgument { - Begin("begin"), - End("end"), - Filename("filename"), - MaxAge("maxage"), - MaxSize("maxsize"), - Name("name"), - PathToGCRoots("path-to-gc-roots"); - - private final String cmdLineKey; - - DumpArgument(String key) { - this.cmdLineKey = key; - } - - @Override - public String getCmdLineKey() { - return cmdLineKey; - } - } - - /** Options available with the JFR.stop diagnostic command. */ - public enum StopArgument implements JfrArgument { - Filename("filename"), - Name("name"); - - private final String cmdLineKey; - - StopArgument(String key) { - this.cmdLineKey = key; - } - - @Override - public String getCmdLineKey() { - return cmdLineKey; - } - } - - public static class JfrArgumentParsingFailed extends Exception { + public static class JfrArgumentParsingFailed extends RuntimeException { @Serial private static final long serialVersionUID = -1050173145647068124L; JfrArgumentParsingFailed(String message, Throwable cause) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java index 7ed5f4bb30bc..121e5b336955 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java @@ -32,17 +32,12 @@ import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport; -import com.oracle.svm.core.dcmd.DcmdSupport; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.RuntimeSupport; -import com.oracle.svm.core.jfr.dcmd.JfrStartDcmd; -import com.oracle.svm.core.jfr.dcmd.JfrStopDcmd; -import com.oracle.svm.core.jfr.dcmd.JfrCheckDcmd; -import com.oracle.svm.core.jfr.dcmd.JfrDumpDcmd; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.jfr.traceid.JfrTraceIdMap; import com.oracle.svm.core.sampler.SamplerJfrStackTraceSerializer; @@ -209,11 +204,5 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { runtime.addInitializationHook(JfrManager.initializationHook()); runtime.addStartupHook(JfrManager.startupHook()); runtime.addShutdownHook(JfrManager.shutdownHook()); - if (VMInspectionOptions.hasAttachSupport()) { - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new JfrStartDcmd()); - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new JfrStopDcmd()); - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new JfrCheckDcmd()); - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new JfrDumpDcmd()); - } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrJdkCompatibility.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrJdkCompatibility.java index b53b3f7a3236..3d82ea404b65 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrJdkCompatibility.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrJdkCompatibility.java @@ -27,12 +27,15 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.Duration; +import java.time.LocalDateTime; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.ProcessProperties; import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.annotate.TargetElement; import com.oracle.svm.core.jdk.JDK21OrEarlier; @@ -53,7 +56,6 @@ * go away. */ @SuppressWarnings("unused") - public final class JfrJdkCompatibility { private JfrJdkCompatibility() { } @@ -126,8 +128,10 @@ public static void setDumpDirectory(PlatformRecording platformRecording, Securit @TargetClass(className = "jdk.jfr.internal.Utils", onlyWith = {JDK21OrEarlier.class, HasJfrSupport.class}) final class Target_jdk_jfr_internal_Utils { - @Alias - public static native String makeFilename(Recording recording); + @Substitute + public static String makeFilename(Recording recording) { + return JfrFilenameUtil.makeFilename(recording); + } @Alias public static native String formatTimespan(Duration dValue, String separation); @@ -135,14 +139,19 @@ final class Target_jdk_jfr_internal_Utils { @TargetClass(className = "jdk.jfr.internal.JVMSupport", onlyWith = {JDKLatest.class, HasJfrSupport.class}) final class Target_jdk_jfr_internal_JVMSupport { - @Alias - public static native String makeFilename(Recording recording); + @Substitute + public static String makeFilename(Recording recording) { + return JfrFilenameUtil.makeFilename(recording); + } } @TargetClass(className = "jdk.jfr.internal.util.ValueFormatter", onlyWith = {JDKLatest.class, HasJfrSupport.class}) final class Target_jdk_jfr_internal_util_ValueFormatter { @Alias public static native String formatTimespan(Duration dValue, String separation); + + @Alias + public static native String formatDateTime(LocalDateTime time); } @TargetClass(className = "jdk.jfr.internal.PlatformRecording") @@ -155,3 +164,12 @@ final class Target_jdk_jfr_internal_PlatformRecording { @TargetElement(onlyWith = JDK21OrEarlier.class) public native void setDumpOnExitDirectory(SecuritySupport.SafePath directory); } + +final class JfrFilenameUtil { + public static String makeFilename(Recording recording) { + long pid = ProcessProperties.getProcessID(); + String date = Target_jdk_jfr_internal_util_ValueFormatter.formatDateTime(LocalDateTime.now()); + String idText = recording == null ? "" : "-id-" + recording.getId(); + return "graalvm-pid-" + pid + idText + "-" + date + ".jfr"; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java index 6c06f187cbfc..359c869c7955 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java @@ -25,52 +25,35 @@ */ package com.oracle.svm.core.jfr; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.text.ParseException; -import java.time.Duration; -import java.util.Arrays; -import java.util.HashMap; +import static com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgumentParsingFailed; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseBoolean; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseInteger; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseJfrOptions; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseMaxSize; + import java.util.Map; -import com.oracle.svm.core.dcmd.DcmdParseException; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.jdk.RuntimeSupport; -import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgument; -import com.oracle.svm.core.jfr.JfrArgumentParser.JfrStartArgument; import com.oracle.svm.core.jfr.JfrArgumentParser.FlightRecorderOptionsArgument; +import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgument; import com.oracle.svm.core.jfr.events.EndChunkNativePeriodicEvents; import com.oracle.svm.core.jfr.events.EveryChunkNativePeriodicEvents; +import com.oracle.svm.core.util.BasedOnJDKFile; import jdk.graal.compiler.api.replacements.Fold; -import jdk.graal.compiler.serviceprovider.JavaVersionUtil; import jdk.jfr.FlightRecorder; -import jdk.jfr.Recording; import jdk.jfr.internal.LogLevel; import jdk.jfr.internal.LogTag; import jdk.jfr.internal.Logger; -import jdk.jfr.internal.OldObjectSample; import jdk.jfr.internal.Options; -import jdk.jfr.internal.PrivateAccess; import jdk.jfr.internal.Repository; import jdk.jfr.internal.SecuritySupport; -import jdk.jfr.internal.jfc.JFC; - -import static com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgumentParsingFailed; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseBoolean; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseDuration; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseInteger; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseMaxSize; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseSettings; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseJfrOptions; /** * Called during VM startup and teardown. Also triggers the JFR argument parsing. @@ -94,11 +77,7 @@ public static RuntimeSupport.Hook initializationHook() { /* Parse arguments early on so that we can tear down the isolate more easily if it fails. */ return isFirstIsolate -> { parseFlightRecorderLogging(); - try { - parseFlightRecorderOptions(); - } catch (JfrArgumentParsingFailed e) { - throw new RuntimeException(e); - } + parseFlightRecorderOptions(); }; } @@ -189,205 +168,12 @@ private static void periodicEventSetup() throws SecurityException { FlightRecorder.addPeriodicEvent(EndChunkNativePeriodicEvents.class, EndChunkNativePeriodicEvents::emit); } + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp#L219-L247") + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp#L146-L180") + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp#L130-L144") private static void initRecording() { - try { - Map startArgs = JfrArgumentParser.parseJfrOptions(SubstrateOptions.StartFlightRecording, JfrStartArgument.values()); - initRecording(startArgs); - } catch (JfrArgumentParsingFailed e) { - throw new RuntimeException(e); - } - } - - public static String initRecording(String[] options) throws DcmdParseException { - try { - Map startArgs = JfrArgumentParser.parseJfrOptions(options, JfrStartArgument.values()); - - return initRecording(startArgs); - } catch (JfrArgumentParsingFailed e) { - throw new DcmdParseException(e.getMessage()); - } - } - - private static String initRecording(Map startArgs) throws JfrArgumentParsingFailed { - String name = startArgs.get(JfrStartArgument.Name); - String[] settings = parseSettings(startArgs); - Long delay = parseDuration(startArgs, JfrStartArgument.Delay); - Long duration = parseDuration(startArgs, JfrStartArgument.Duration); - Boolean disk = parseBoolean(startArgs, JfrStartArgument.Disk); - String path = startArgs.get(JfrStartArgument.Filename); - Long maxAge = parseDuration(startArgs, JfrStartArgument.MaxAge); - Long maxSize = parseMaxSize(startArgs, JfrStartArgument.MaxSize); - Boolean dumpOnExit = parseBoolean(startArgs, JfrStartArgument.DumpOnExit); - Boolean pathToGcRoots = parseBoolean(startArgs, JfrStartArgument.PathToGCRoots); - - if (Logger.shouldLog(LogTag.JFR_DCMD, LogLevel.DEBUG)) { - Logger.log(LogTag.JFR_DCMD, LogLevel.DEBUG, "Executing DCmdStart: name=" + name + - ", settings=" + Arrays.asList(settings) + - ", delay=" + delay + - ", duration=" + duration + - ", disk=" + disk + - ", filename=" + path + - ", maxage=" + maxAge + - ", maxsize=" + maxSize + - ", dumponexit =" + dumpOnExit + - ", path-to-gc-roots=" + pathToGcRoots); - } - if (name != null) { - try { - Integer.parseInt(name); - throw new JfrArgumentParsingFailed("Name of recording can't be numeric"); - } catch (NumberFormatException nfe) { - // ok, can't be mixed up with name - } - } - - if (duration == null && Boolean.FALSE.equals(dumpOnExit) && path != null) { - throw new JfrArgumentParsingFailed("Filename can only be set for a time bound recording or if dumponexit=true. Set duration/dumponexit or omit filename."); - } - if (settings.length == 1 && settings[0].length() == 0) { - throw new JfrArgumentParsingFailed("No settings specified. Use settings=none to start without any settings"); - } - Map s = new HashMap<>(); - for (String configName : settings) { - try { - s.putAll(JFC.createKnown(configName).getSettings()); - } catch (FileNotFoundException e) { - throw new JfrArgumentParsingFailed("Could not find settings file'" + configName + "'", e); - } catch (IOException | ParseException e) { - throw new JfrArgumentParsingFailed("Could not parse settings file '" + settings[0] + "'", e); - } - } - - OldObjectSample.updateSettingPathToGcRoots(s, pathToGcRoots); - - if (duration != null) { - if (duration < 1000L * 1000L * 1000L) { - // to avoid typo, duration below 1s makes no sense - throw new JfrArgumentParsingFailed("Could not start recording, duration must be at least 1 second."); - } - } - - if (delay != null) { - if (delay < 1000L * 1000L * 1000) { - // to avoid typo, delay shorter than 1s makes no sense. - throw new JfrArgumentParsingFailed("Could not start recording, delay must be at least 1 second."); - } - } - - Recording recording = new Recording(); - if (name != null) { - recording.setName(name); - } - - if (disk != null) { - recording.setToDisk(disk); - } - recording.setSettings(s); - SecuritySupport.SafePath safePath = null; - - if (path != null) { - try { - if (dumpOnExit == null) { - // default to dumponexit=true if user specified filename - dumpOnExit = Boolean.TRUE; - } - Path p = Paths.get(path); - if (Files.isDirectory(p) && (JavaVersionUtil.JAVA_SPEC >= 23 || Boolean.TRUE.equals(dumpOnExit))) { - // Decide destination filename at dump time - // Purposely avoid generating filename in Recording#setDestination due to - // security concerns - JfrJdkCompatibility.setDumpDirectory(PrivateAccess.getInstance().getPlatformRecording(recording), new SecuritySupport.SafePath(p)); - } else { - safePath = resolvePath(recording, path); - recording.setDestination(safePath.toPath()); - } - } catch (IOException | InvalidPathException e) { - recording.close(); - throw new RuntimeException("Could not start recording, not able to write to file: " + path, e); - } - } - - if (maxAge != null) { - recording.setMaxAge(Duration.ofNanos(maxAge)); - } - - if (maxSize != null) { - recording.setMaxSize(maxSize); - } - - if (duration != null) { - recording.setDuration(Duration.ofNanos(duration)); - } - - if (dumpOnExit != null) { - recording.setDumpOnExit(dumpOnExit); - } - - StringBuilder msg = new StringBuilder(); - - if (delay != null) { - Duration dDelay = Duration.ofNanos(delay); - recording.scheduleStart(dDelay); - - msg.append("Recording "); - msg.append(recording.getId()); - msg.append(" scheduled to start in "); - msg.append(JfrJdkCompatibility.formatTimespan(dDelay, " ")); - msg.append("."); - } else { - recording.start(); - msg.append("Started recording "); - msg.append(recording.getId()); - msg.append("."); - } - - if (recording.isToDisk() && duration == null && maxAge == null && maxSize == null) { - msg.append(" No limit specified, using maxsize=250MB as default."); - recording.setMaxSize(250 * 1024L * 1024L); - } - - if (safePath != null && duration != null) { - msg.append(" The result will be written to:"); - msg.append(System.getProperty("line.separator")); - msg.append(getPath(safePath)); - msg.append(System.getProperty("line.separator")); - } - Logger.log(LogTag.JFR_SYSTEM, LogLevel.INFO, msg.toString()); - return recording.getName(); - } - - public static SecuritySupport.SafePath resolvePath(Recording recording, String filename) throws InvalidPathException { - if (filename == null) { - return makeGenerated(recording, Paths.get(".")); - } - Path path = Paths.get(filename); - if (Files.isDirectory(path)) { - return makeGenerated(recording, path); - } - return new SecuritySupport.SafePath(path.toAbsolutePath().normalize()); - } - - private static SecuritySupport.SafePath makeGenerated(Recording recording, Path directory) { - return new SecuritySupport.SafePath(directory.toAbsolutePath().resolve(JfrJdkCompatibility.makeFilename(recording)).normalize()); - } - - private static String getPath(SecuritySupport.SafePath path) { - if (path == null) { - return "N/A"; - } - try { - return getPath(SecuritySupport.getAbsolutePath(path).toPath()); - } catch (IOException ioe) { - return getPath(path.toPath()); - } - } - - private static String getPath(Path path) { - try { - return path.toAbsolutePath().toString(); - } catch (SecurityException e) { - // fall back on filename - return path.toString(); - } + Target_jdk_jfr_internal_dcmd_DCmdStart cmd = new Target_jdk_jfr_internal_dcmd_DCmdStart(); + String[] result = SubstrateUtil.cast(cmd, Target_jdk_jfr_internal_dcmd_AbstractDCmd.class).execute("internal", SubstrateOptions.StartFlightRecording.getValue(), ','); + Logger.log(LogTag.JFR_SYSTEM, LogLevel.INFO, String.join(System.lineSeparator(), result)); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM.java index 065820dd959b..67a96a5e6af9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM.java @@ -47,9 +47,7 @@ import jdk.graal.compiler.api.replacements.Fold; import jdk.jfr.internal.JVM; -import jdk.jfr.internal.LogLevel; import jdk.jfr.internal.LogTag; -import jdk.jfr.internal.Logger; @SuppressWarnings({"static-method", "unused"}) @TargetClass(value = jdk.jfr.internal.JVM.class, onlyWith = HasJfrSupport.class) @@ -174,13 +172,12 @@ public static void unregisterStackFilter(long stackFilterId) { /** * As of 22+27, This method is both used to set cutoff tick values for leak profiling and - * for @Deprecated events. + * for @Deprecated events. Note that this method is called during JFR startup. */ @Substitute @TargetElement(onlyWith = JDKLatest.class) public static void setMiscellaneous(long eventTypeId, long value) { - Logger.log(LogTag.JFR_SETTING, LogLevel.WARN, "@Deprecated JFR events, and leak profiling are not yet supported."); - /* Explicitly don't throw an exception (would result in an unspecific warning). */ + /* Ignore the call and don't throw an exception (would result in an unspecific warning). */ } /** See {@link JVM#getThreadId}. */ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_dcmd_AbstractDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_dcmd_AbstractDCmd.java new file mode 100644 index 000000000000..253b20af9473 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_dcmd_AbstractDCmd.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jfr; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.annotate.TargetElement; +import com.oracle.svm.core.jdk.JDK21OrEarlier; +import com.oracle.svm.core.jdk.JDKLatest; + +@TargetClass(className = "jdk.jfr.internal.dcmd.AbstractDCmd") +public final class Target_jdk_jfr_internal_dcmd_AbstractDCmd { + @Alias + public native String[] execute(String source, String arg, char delimiter); + + @Alias + @TargetElement(onlyWith = JDKLatest.class) + public native String[] getHelp(); + + @Alias + @TargetElement(onlyWith = JDK21OrEarlier.class) + public native String[] printHelp(); + + @Alias + native void logWarning(String message); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_dcmd_DCmdStart.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_dcmd_DCmdStart.java new file mode 100644 index 000000000000..fe7ee7efa692 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_dcmd_DCmdStart.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jfr; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "jdk.jfr.internal.dcmd.DCmdStart") +public final class Target_jdk_jfr_internal_dcmd_DCmdStart { + @Alias + public Target_jdk_jfr_internal_dcmd_DCmdStart() { + } + + @Alias + private native LinkedHashMap configureStandard(String[] settings); + + /** + * We don't support extended JFR configurations at the moment. However, this method is also + * called if the user specified an unknown/incorrect JFR option. To avoid confusing error + * messages, we substitute this method so that it calls {@link #configureStandard} and prints a + * warning for each unknown option. + */ + @Substitute + @SuppressWarnings("unused") + private LinkedHashMap configureExtended(String[] settings, Target_jdk_jfr_internal_dcmd_ArgumentParser parser) { + LinkedHashMap result = configureStandard(settings); + for (String optionName : parser.getExtendedOptions().keySet()) { + SubstrateUtil.cast(this, Target_jdk_jfr_internal_dcmd_AbstractDCmd.class).logWarning("The .jfc option/setting '" + optionName + "' doesn't exist or is not supported."); + } + return result; + } + + @Substitute + @SuppressWarnings("unused") + private static String jfcOptions() { + /* Not needed at the moment, as we do not support extended JFR configurations. */ + return ""; + } +} + +@TargetClass(className = "jdk.jfr.internal.dcmd.ArgumentParser") +final class Target_jdk_jfr_internal_dcmd_ArgumentParser { + @Alias + native Map getExtendedOptions(); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_jfc_JFC.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_jfc_JFC.java index 46b99e4200c7..561d5e93597f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_jfc_JFC.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_jfc_JFC.java @@ -41,6 +41,7 @@ import jdk.jfr.internal.jfc.JFC; @TargetClass(value = jdk.jfr.internal.jfc.JFC.class, onlyWith = HasJfrSupport.class) +@SuppressWarnings("unused") public final class Target_jdk_jfr_internal_jfc_JFC { @Substitute public static List getConfigurations() { @@ -49,23 +50,23 @@ public static List getConfigurations() { @Substitute public static Configuration createKnown(String name) throws IOException, ParseException { + Path localPath = Paths.get(name); + String jfcName = JFC.nameFromPath(localPath); + // Check if this is a pre-parsed known configuration. for (Configuration config : SubstrateJVM.getKnownConfigurations()) { - if (config.getName().equals(name)) { + if (config.getName().equals(jfcName)) { return config; } } - // Assume path included in name - Path localPath = Paths.get(name); - String jfcName = JFC.nameFromPath(localPath); + // Try to read the configuration from a file. try (Reader r = Files.newBufferedReader(localPath)) { return Target_jdk_jfr_internal_jfc_JFCParser.createConfiguration(jfcName, r); } } @Substitute - @SuppressWarnings("unused") public static Configuration getPredefined(String name) throws IOException, ParseException { for (Configuration config : SubstrateJVM.getKnownConfigurations()) { if (config.getName().equals(name)) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrCheckDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrCheckDcmd.java deleted file mode 100644 index 5cc9f991231a..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrCheckDcmd.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.jfr.dcmd; - -import com.oracle.svm.core.dcmd.AbstractDcmd; -import com.oracle.svm.core.dcmd.DcmdParseException; -import jdk.jfr.FlightRecorder; -import jdk.jfr.Recording; - -import java.util.List; - -public class JfrCheckDcmd extends AbstractDcmd { - - public JfrCheckDcmd() { - this.name = "JFR.check"; - this.description = "Checks running JFR recording(s)"; - this.impact = "low"; - } - - @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - if (arguments.length > 1) { - throw new DcmdParseException("Too many arguments specified"); - } - StringBuilder sb = new StringBuilder(); - List recordings = FlightRecorder.getFlightRecorder().getRecordings(); - - if (recordings.isEmpty()) { - return "No recordings."; - } - - for (Recording recording : recordings) { - sb.append("Recording \"").append(recording.getId()).append("\": name=").append(recording.getName()); - sb.append(" maxsize=").append(recording.getMaxSize()).append("B"); - sb.append(" (").append(recording.getState().toString()).append(")\n"); - } - return sb.toString(); - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrDumpDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrDumpDcmd.java deleted file mode 100644 index 579627480223..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrDumpDcmd.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.jfr.dcmd; - -import com.oracle.svm.core.dcmd.AbstractDcmd; -import com.oracle.svm.core.dcmd.DcmdOption; -import com.oracle.svm.core.dcmd.DcmdParseException; -import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgument; -import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgumentParsingFailed; -import com.oracle.svm.core.jfr.JfrManager; -import com.oracle.svm.core.util.BasedOnJDKFile; - -import jdk.jfr.FlightRecorder; -import jdk.jfr.Recording; -import jdk.jfr.internal.PlatformRecorder; -import jdk.jfr.internal.PlatformRecording; -import jdk.jfr.internal.SecuritySupport; -import jdk.jfr.internal.WriteableUserPath; -import jdk.jfr.internal.PrivateAccess; - -import java.io.IOException; -import java.nio.file.InvalidPathException; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeParseException; -import java.util.Arrays; -import java.util.Map; - -import static com.oracle.svm.core.jfr.JfrArgumentParser.DumpArgument; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseBoolean; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseDuration; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseJfrOptions; -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseMaxSize; -import static java.util.concurrent.TimeUnit.DAYS; -import static java.util.concurrent.TimeUnit.HOURS; -import static java.util.concurrent.TimeUnit.MICROSECONDS; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.MINUTES; -import static java.util.concurrent.TimeUnit.SECONDS; - -public class JfrDumpDcmd extends AbstractDcmd { - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L197-L277") // - public JfrDumpDcmd() { - this.options = new DcmdOption[]{ - new DcmdOption("filename", "Name of the file to which the flight recording data is\n" + - " dumped. If no filename is given, a filename is generated from the PID\n" + - " and the current date. The filename may also be a directory in which\n" + - " case, the filename is generated from the PID and the current date in\n" + - " the specified directory.", false, null), - new DcmdOption("name", "Name of the recording. If no name is given, data from all\n" + - " recordings is dumped.", false, null), - new DcmdOption("maxage", "Length of time for dumping the flight recording data to a\n" + - " file. (INTEGER followed by 's' for seconds 'm' for minutes or 'h' for\n" + - " hours)", false, null), - new DcmdOption("maxsize", "Maximum size for the amount of data to dump from a flight\n" + - " recording in bytes if one of the following suffixes is not used:\n" + - " 'm' or 'M' for megabytes OR 'g' or 'G' for gigabytes.", false, null), - new DcmdOption("path-to-gc-roots", " Flag for saving the path to garbage collection (GC) roots\n" + - " at the time the recording data is dumped. The path information is\n" + - " useful for finding memory leaks but collecting it can cause the\n" + - " application to pause for a short period of time. Turn on this flag\n" + - " only when you have an application that you suspect has a memory\n" + - " leak. (BOOLEAN)", false, null), - new DcmdOption("begin", "Specify the time from which recording data will be included\n" + - " in the dump file. The format is specified as local time.\n" + - " (STRING)", false, null), - new DcmdOption("end", "Specify the time to which recording data will be included\n" + - " in the dump file. The format is specified as local time.\n" + - " (STRING)", false, null) - }; - this.examples = new String[]{ - "$ jcmd JFR.dump", - "$ jcmd JFR.dump filename=recording.jfr", - "$ jcmd JFR.dump name=1 filename=/recordings/recording.jfr", - "$ jcmd JFR.dump maxage=1h maxsize=50M", - "$ jcmd JFR.dump begin=-1h", - "$ jcmd JFR.dump begin=-15m end=-5m", - "$ jcmd JFR.dump begin=13:15 end=21:30:00" - }; - this.name = "JFR.dump"; - this.description = "Copies contents of a JFR recording to file. Either the name or the recording id must be specified."; - this.impact = "low"; - } - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L55-L118") // - @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - String recordingName; - String filename; - Long maxAge; - Long maxSize; - Boolean pathToGcRoots; - String begin; - String end; - try { - Map dumpArgs = parseJfrOptions(Arrays.copyOfRange(arguments, 1, arguments.length), DumpArgument.values()); - recordingName = dumpArgs.get(DumpArgument.Name); - filename = dumpArgs.get(DumpArgument.Filename); - maxAge = parseDuration(dumpArgs, DumpArgument.MaxAge); - maxSize = parseMaxSize(dumpArgs, DumpArgument.MaxSize); - pathToGcRoots = parseBoolean(dumpArgs, DumpArgument.PathToGCRoots); - begin = dumpArgs.get(DumpArgument.Begin); - end = dumpArgs.get(DumpArgument.End); - } catch (JfrArgumentParsingFailed e) { - throw new DcmdParseException(e.getMessage()); - } - - if (FlightRecorder.getFlightRecorder().getRecordings().isEmpty()) { - throw new DcmdParseException("No recordings to dump from. Use JFR.start to start a recording."); - } - - if (maxAge != null) { - if (maxAge < 0) { - throw new DcmdParseException("Dump failed, maxage can't be negative."); - } - if (maxAge == 0) { - maxAge = Long.MAX_VALUE / 2; // a high value that won't overflow - } - } - - if (maxSize != null) { - if (maxSize < 0) { - throw new DcmdParseException("Dump failed, maxsize can't be negative."); - } - if (maxSize == 0) { - maxSize = Long.MAX_VALUE / 2; // a high value that won't overflow - } - } - - Instant beginTime = parseTime(begin, "begin"); - Instant endTime = parseTime(end, "end"); - - if (beginTime != null && endTime != null) { - if (endTime.isBefore(beginTime)) { - throw new DcmdParseException("Dump failed, begin must precede end."); - } - } - - Duration duration; - if (maxAge != null) { - duration = Duration.ofNanos(maxAge); - beginTime = Instant.now().minus(duration); - } - - Recording recording = null; - if (recordingName != null) { - for (Recording rec : FlightRecorder.getFlightRecorder().getRecordings()) { - if (rec.getName().equals(recordingName)) { - recording = rec; - break; - } - } - if (recording == null) { - throw new DcmdParseException("Could not find specified recording with name: " + recordingName); - } - } - PlatformRecorder recorder = PrivateAccess.getInstance().getPlatformRecorder(); - try { - synchronized (recorder) { - dump(recorder, recording, filename, maxSize, pathToGcRoots, beginTime, endTime); - } - } catch (IOException | InvalidPathException e) { - throw new DcmdParseException("Dump failed. Could not copy recording data."); - } - return "Dump created."; - } - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L120-L141") // - /* Mostly identical to Hotspot. */ - private static void dump(PlatformRecorder recorder, Recording recording, String filename, Long maxSize, Boolean pathToGcRoots, Instant beginTime, Instant endTime) - throws DcmdParseException, IOException { - try (PlatformRecording r = newSnapShot(recorder, recording, pathToGcRoots)) { - r.filter(beginTime, endTime, maxSize); - if (r.getChunks().isEmpty()) { - throw new DcmdParseException("Dump failed. No data found in the specified interval."); - } - /* - * If a filename exists, use it. If a filename doesn't exist, use the destination set - * earlier. If destination doesn't exist, generate a filename - */ - WriteableUserPath wup = null; - if (recording != null) { - PlatformRecording pRecording = PrivateAccess.getInstance().getPlatformRecording(recording); - wup = pRecording.getDestination(); - } - if (filename != null || wup == null) { - SecuritySupport.SafePath safe = JfrManager.resolvePath(recording, filename); - wup = new WriteableUserPath(safe.toPath()); - } - r.dumpStopped(wup); - } - } - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L143-L182") // - /* Mostly identical to Hotspot. */ - private static Instant parseTime(String time, String parameter) throws DcmdParseException { - if (time == null) { - return null; - } - try { - return Instant.parse(time); - } catch (DateTimeParseException dtp) { - // fall through - } - try { - LocalDateTime ldt = LocalDateTime.parse(time); - return ZonedDateTime.of(ldt, ZoneId.systemDefault()).toInstant(); - } catch (DateTimeParseException dtp) { - // fall through - } - try { - LocalTime lt = LocalTime.parse(time); - LocalDate ld = LocalDate.now(); - Instant instant = ZonedDateTime.of(ld, lt, ZoneId.systemDefault()).toInstant(); - Instant now = Instant.now(); - if (instant.isAfter(now) && !instant.isBefore(now.plusSeconds(3600))) { - // User must have meant previous day - ld = ld.minusDays(1); - } - return ZonedDateTime.of(ld, lt, ZoneId.systemDefault()).toInstant(); - } catch (DateTimeParseException dtp) { - // fall through - } - - if (time.startsWith("-")) { - try { - long durationNanos = parseTimespan(time.substring(1)); - Duration duration = Duration.ofNanos(durationNanos); - return Instant.now().minus(duration); - } catch (NumberFormatException nfe) { - // fall through - } - } - throw new DcmdParseException("Dump failed, not a valid time: " + parameter); - } - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L184-L194") // - /* Mostly identical to Hotspot. */ - private static PlatformRecording newSnapShot(PlatformRecorder recorder, Recording recording, Boolean pathToGcRoots) throws IOException { - if (recording == null) { - // Operate on all recordings - PlatformRecording snapshot = recorder.newTemporaryRecording(); - recorder.fillWithRecordedData(snapshot, pathToGcRoots); - return snapshot; - } - - PlatformRecording pr = PrivateAccess.getInstance().getPlatformRecording(recording); - return pr.newSnapshotClone("Dumped by user", pathToGcRoots); - } - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/util/ValueParser.java#L44-L74") // - /* Mostly identical to Hotspot. */ - private static long parseTimespan(String s) { - if (s.endsWith("ns")) { - return Long.parseLong(s.substring(0, s.length() - 2).trim()); - } - if (s.endsWith("us")) { - return MICROSECONDS.toNanos(Long.parseLong(s.substring(0, s.length() - 2).trim())); - } - if (s.endsWith("ms")) { - return MILLISECONDS.toNanos(Long.parseLong(s.substring(0, s.length() - 2).trim())); - } - if (s.endsWith("s")) { - return SECONDS.toNanos(Long.parseLong(s.substring(0, s.length() - 1).trim())); - } - if (s.endsWith("m")) { - return MINUTES.toNanos(Long.parseLong(s.substring(0, s.length() - 1).trim())); - } - if (s.endsWith("h")) { - return HOURS.toNanos(Long.parseLong(s.substring(0, s.length() - 1).trim())); - } - if (s.endsWith("d")) { - return DAYS.toNanos(Long.parseLong(s.substring(0, s.length() - 1).trim())); - } - - try { - Long.parseLong(s); - } catch (NumberFormatException nfe) { - throw new NumberFormatException("'" + s + "' is not a valid timespan. Should be numeric value followed by a unit, i.e. 20 ms. Valid units are ns, us, s, m, h and d."); - } - // Only accept values with units - throw new NumberFormatException("Timespan + '" + s + "' is missing unit. Valid units are ns, us, s, m, h and d."); - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStartDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStartDcmd.java deleted file mode 100644 index 1178ecfcecd5..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStartDcmd.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.jfr.dcmd; - -import com.oracle.svm.core.dcmd.DcmdOption; -import com.oracle.svm.core.dcmd.AbstractDcmd; -import com.oracle.svm.core.dcmd.DcmdParseException; -import java.util.Arrays; -import com.oracle.svm.core.util.BasedOnJDKFile; - -import com.oracle.svm.core.jfr.JfrManager; - -public class JfrStartDcmd extends AbstractDcmd { - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStart.java#L348-L461") // - public JfrStartDcmd() { - this.options = new DcmdOption[]{ - new DcmdOption("delay", "Length of time to wait before starting to record\n" + - " (INTEGER followed by 's' for seconds 'm' for minutes or h' for\n" + - " hours, 0s)", false, "0s"), - new DcmdOption("disk", "Flag for also writing the data to disk while recording\n" + - " (BOOLEAN)", false, "true"), - new DcmdOption("dumponexit", "Flag for writing the recording to disk when the Java\n" + - " Virtual Machine (JVM) shuts down. If set to 'true' and no value\n" + - " is given for filename, the recording is written to a file in the\n" + - " directory where the process was started. The file name is a\n" + - " system-generated name that contains the process ID, the recording\n" + - " ID and the current time stamp. (For example:\n" + - " id-1-2021_09_14_09_00.jfr) (BOOLEAN)", false, "false"), - new DcmdOption("duration", "Length of time to record. Note that 0s means forever\n" + - " (INTEGER followed by 's' for seconds 'm' for minutes or 'h' for\n" + - " hours)", false, "0s"), - new DcmdOption("filename", "Name of the file to which the flight recording data is\n" + - " written when the recording is stopped. If no filename is given, a\n" + - " filename is generated from the PID and the current date and is\n" + - " placed in the directory where the process was started. The\n" + - " filename may also be a directory in which case, the filename is\n" + - " generated from the PID and the current date in the specified\n" + - " directory.", false, null), - new DcmdOption("maxage", "Maximum time to keep the recorded data on disk. This\n" + - " parameter is valid only when the disk parameter is set to true.\n" + - " Note 0s means forever. (INTEGER followed by 's' for seconds 'm'\n" + - " for minutes or 'h' for hours, 0s)", false, "No max age."), - new DcmdOption("maxsize", "Maximum size of the data to keep on disk in bytes if\n" + - " one of the following suffixes is not used: 'm' or 'M' for\n" + - " megabytes OR 'g' or 'G' for gigabytes. This parameter is valid\n" + - " only when the disk parameter is set to 'true'. The value must not\n" + - " be less than the value for the maxchunksize parameter set with\n" + - " the JFR.configure command.", false, "No max size"), - new DcmdOption("name", "Name of the recording. If no name is provided, a name\n" + - " is generated. Make note of the generated name that is shown in\n" + - " the response to the command so that you can use it with other\n" + - " commands.", false, "System-generated default name"), - new DcmdOption("path-to-gc-root", "Flag for saving the path to garbage collection (GC)\n" + - " roots at the end of a recording. The path information is useful\n" + - " for finding memory leaks but collecting it is time consuming.\n" + - " Turn on this flag only when you have an application that you\n" + - " suspect has a memory leak. If the settings parameter is set to\n" + - " 'profile', then the information collected includes the stack\n" + - " trace from where the potential leaking object wasallocated. (BOOLEAN)", false, "false"), - new DcmdOption("settings", " Name of the settings file that identifies which events\n" + - " to record. To specify more than one file, use the settings\n" + - " parameter repeatedly. Include the path if the file is not in\n" + - " JAVA-HOME/lib/jfr. The following profiles are included with the\n" + - " JDK in the JAVA-HOME/lib/jfr directory: 'default.jfc': collects a\n" + - " predefined set of information with low overhead, so it has minimal\n" + - " impact on performance and can be used with recordings that run\n" + - " continuously; 'profile.jfc': Provides more data than the\n" + - " 'default.jfc' profile, but with more overhead and impact on\n" + - " performance. Use this configuration for short periods of time\n" + - " when more information is needed. Use none to start a recording\n" + - " without a predefined configuration file. (STRING)", false, "JAVA-HOME/lib/jfr/default.jfc") - }; - this.examples = new String[]{ - "$ jcmd JFR.start", - "$ jcmd JFR.start filename=dump.jfr", - "$ jcmd JFR.start filename=/directory/recordings", - "$ jcmd JFR.start maxage=1h maxsize=1000M", - "$ jcmd JFR.start delay=5m settings=my.jfc" - }; - this.name = "JFR.start"; - this.description = "Starts a new JFR recording."; - this.impact = "medium"; - } - - @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - String recordingName = JfrManager.initRecording(Arrays.copyOfRange(arguments, 1, arguments.length)); - return "Started recording " + recordingName + "\n"; - } - - @Override - public String getName() { - return "JFR.start"; - } - - @Override - public String getImpact() { - return "Medium"; - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStopDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStopDcmd.java deleted file mode 100644 index 1098bebd5a0a..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStopDcmd.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.jfr.dcmd; - -import com.oracle.svm.core.dcmd.DcmdOption; -import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgument; -import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgumentParsingFailed; -import com.oracle.svm.core.dcmd.AbstractDcmd; -import com.oracle.svm.core.dcmd.DcmdParseException; -import com.oracle.svm.core.util.BasedOnJDKFile; -import jdk.jfr.FlightRecorder; -import jdk.jfr.Recording; -import jdk.jfr.internal.SecuritySupport; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Map; - -import static com.oracle.svm.core.jfr.JfrArgumentParser.parseJfrOptions; -import static com.oracle.svm.core.jfr.JfrArgumentParser.StopArgument; -import static com.oracle.svm.core.jfr.JfrManager.resolvePath; - -public class JfrStopDcmd extends AbstractDcmd { - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStop.java#L74-L100") // - public JfrStopDcmd() { - this.options = new DcmdOption[]{ - new DcmdOption("filename", "Name of the file to which the recording is written when the\n" + - " recording is stopped. If no path is provided here or when the recording was started,\n" + - " the data from the recording is discarded.", false, null), - new DcmdOption("name", "Name of the recording to stop.", true, null) - }; - this.examples = new String[]{ - "$ jcmd JFR.stop name=1", - "$ jcmd JFR.stop name=benchmark filename=/directory/recordings", - "$ jcmd JFR.stop name=5 filename=recording.jfr" - }; - this.name = "JFR.stop"; - this.description = "Stops a JFR recording."; - this.impact = "low"; - } - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStop.java#L44-L71") // - @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - String response; - if (arguments.length > 3) { - throw new DcmdParseException("Too many arguments specified"); - } - - try { - Map stopArgs = parseJfrOptions(Arrays.copyOfRange(arguments, 1, arguments.length), StopArgument.values()); - String recordingName = stopArgs.get(StopArgument.Name); - String filename = stopArgs.get(StopArgument.Filename); - Recording target = null; - - if (recordingName == null) { - throw new DcmdParseException("The name of the recording to stop is required but was not specified."); - } - - for (Recording recording : FlightRecorder.getFlightRecorder().getRecordings()) { - if (recording.getName().equals(recordingName)) { - target = recording; - break; - } - } - if (target == null) { - throw new DcmdParseException("Could not find specified recording with name: " + recordingName); - } - - if (filename != null) { - SecuritySupport.SafePath safePath = resolvePath(target, filename); - target.setDestination(safePath.toPath()); - } - - target.stop(); - target.close(); - response = "Stopped recording: " + target.getName(); - - } catch (JfrArgumentParsingFailed e) { - throw new DcmdParseException("Invalid arguments provided: " + e.getMessage()); - } catch (NumberFormatException e) { - throw new DcmdParseException("Invalid recording name specified: " + e.getMessage()); - } catch (IOException e) { - throw new DcmdParseException("Invalid dump path specified: " + e.getMessage()); - } - return response + "\n"; - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java index 80293bc533aa..45d3805b9e6f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java @@ -28,15 +28,16 @@ import java.lang.management.ThreadMXBean; import java.util.concurrent.TimeUnit; -import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.thread.VMOperation; -import com.oracle.svm.core.thread.VMOperationListener; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.JavaMainWrapper; +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.attach.AttachApiSupport; import com.oracle.svm.core.heap.Heap; +import com.oracle.svm.core.thread.VMOperation; +import com.oracle.svm.core.thread.VMOperationListener; import com.oracle.svm.core.util.BasedOnJDKFile; import com.sun.management.OperatingSystemMXBean; @@ -45,8 +46,6 @@ * are specified at image build time). */ class SystemCounters implements PerfDataHolder, VMOperationListener { - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/master/src/hotspot/share/services/runtimeService.cpp#L72") // - private static final String ATTACH_SUPPORTED = "1"; // Constants. private final PerfLongConstant initDoneTime; private final PerfStringConstant javaCommand; @@ -137,7 +136,7 @@ public void allocate() { osName.allocate(getSystemProperty("os.name")); userDir.allocate(getSystemProperty("user.dir")); userName.allocate(getSystemProperty("user.name")); - jvmCapabilities.allocate(ATTACH_SUPPORTED); + jvmCapabilities.allocate(getJvmCapabilities()); gcInProgress.allocate(); @@ -160,6 +159,18 @@ private static String getSystemProperty(String s) { } } + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/runtimeService.cpp#L68-L77") // + private static String getJvmCapabilities() { + /* + * The capabilities are encoded as a string with 64 characters, where each character + * represent one specific capability. The first character is the attach API support. + */ + String attachApiSupport = AttachApiSupport.isPresent() ? "1" : "0"; + String result = attachApiSupport + "000000000000000000000000000000000000000000000000000000000000000"; + assert result.length() == 64; + return result; + } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public void vmOperationChanged(VMOperation operation) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtDcmd.java deleted file mode 100644 index ce7854c9c43b..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtDcmd.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.core.nmt; - -import com.oracle.svm.core.dcmd.AbstractDcmd; -import com.oracle.svm.core.dcmd.DcmdOption; -import com.oracle.svm.core.dcmd.DcmdParseException; -import com.oracle.svm.core.util.BasedOnJDKFile; - -public class NmtDcmd extends AbstractDcmd { - - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24%2B2/src/hotspot/share/nmt/nmtDCmd.cpp#L34-L64") // - public NmtDcmd() { - this.options = new DcmdOption[]{new DcmdOption("summary", - "request runtime to report current memory summary," + " which includes total reserved and committed memory," + " along with memory usage summary by each subsystem. BOOLEAN.", - false, "false")}; - this.examples = new String[]{"$ jcmd VM.native_memory summary"}; - this.name = "VM.native_memory"; - this.description = "Print native memory usage"; - this.impact = "low"; - } - - @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - if (arguments.length != 2) { - throw new DcmdParseException("Exactly 1 argument should be specified. See $ jcmd help VM.native_memory"); - } - if (arguments[1].equals("detailed")) { - return "Detailed NMT mode is not supported yet."; - } else if (arguments[1].equals("summary")) { - return NativeMemoryTracking.singleton().generateReportString(); - } - return null; - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtFeature.java index f21b31b67ab5..120317ca8292 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtFeature.java @@ -28,7 +28,6 @@ import org.graalvm.nativeimage.ImageSingletons; -import com.oracle.svm.core.dcmd.DcmdSupport; import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; @@ -50,8 +49,5 @@ public void afterRegistration(AfterRegistrationAccess access) { public void beforeAnalysis(BeforeAnalysisAccess access) { RuntimeSupport.getRuntimeSupport().addInitializationHook(NativeMemoryTracking.initializationHook()); RuntimeSupport.getRuntimeSupport().addShutdownHook(NativeMemoryTracking.shutdownHook()); - if (VMInspectionOptions.hasAttachSupport()) { - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new NmtDcmd()); - } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpToFileDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpToFileDcmd.java deleted file mode 100644 index 679f40ad39a4..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpToFileDcmd.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package com.oracle.svm.core.thread; - -import com.oracle.svm.core.dcmd.AbstractDcmd; -import com.oracle.svm.core.dcmd.DcmdParseException; -import com.oracle.svm.core.dcmd.DcmdOption; -import jdk.internal.vm.ThreadDumper; - -public class ThreadDumpToFileDcmd extends AbstractDcmd { - - public ThreadDumpToFileDcmd() { - this.options = new DcmdOption[]{ - new DcmdOption("filepath", "The file path to the output file. (STRING)", true, null), - new DcmdOption("overwrite", "May overwrite existing file. (BOOLEAN)", false, "false"), - new DcmdOption("format", "Output format (\"plain\" or \"json\") (STRING)", false, "plain") - - }; - this.name = "Thread.dump_to_file"; - this.description = "Dumps thread stacks (including virtual) to a specified file."; - this.impact = "High"; - this.examples = new String[]{ - "$ jcmd Thread.dump_to_file filepath=/some/path/my_file.txt", - "$ jcmd Thread.dump_to_file format=json overwrite=true filepath=/some/path/my_file.json"}; - } - - @Override - public String parseAndExecute(String[] arguments) throws DcmdParseException { - - String pathString = null; - boolean overwrite = false; - boolean useJson = false; - for (String argument : arguments) { - if (argument.contains("filepath=")) { - pathString = extractArgument(argument); - } else if (argument.contains("overwrite=")) { - overwrite = Boolean.parseBoolean(extractArgument(argument)); - } else if (argument.contains("format=")) { - String result = extractArgument(argument); - if (result.equals("json")) { - useJson = true; - } else if (!result.equals("plain")) { - throw new DcmdParseException("Format must be either json or plain, but provided: " + result); - } - } - } - - if (pathString == null) { - return "The argument 'filepath' is mandatory."; - } - - if (useJson) { - ThreadDumper.dumpThreadsToJson(pathString, overwrite); - } else { - ThreadDumper.dumpThreads(pathString, overwrite); - } - return "Created " + pathString; - } - - private static String extractArgument(String input) throws DcmdParseException { - String[] pathArgumentSplit = input.split("="); - if (pathArgumentSplit.length != 2) { - throw new DcmdParseException("Invalid command structure."); - } - return pathArgumentSplit[1]; - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/ImageHeapList.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/ImageHeapList.java index 3a038dc29da0..2500eafc09b9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/ImageHeapList.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/ImageHeapList.java @@ -52,6 +52,11 @@ @Platforms(Platform.HOSTED_ONLY.class) // public final class ImageHeapList { + @Platforms(Platform.HOSTED_ONLY.class) // + public static List create(Class elementClass) { + return create(elementClass, null); + } + @Platforms(Platform.HOSTED_ONLY.class) // public static List create(Class elementClass, Comparator comparator) { VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished(), "Trying to create an ImageHeapList after analysis."); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpFeature.java index bb7ebeb4bb10..02e3e51a8637 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpFeature.java @@ -36,15 +36,14 @@ import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.impl.HeapDumpSupport; -import com.oracle.svm.core.dcmd.DcmdSupport; import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.heap.dump.HProfType; -import com.oracle.svm.core.heap.dump.HeapDumpDcmd; import com.oracle.svm.core.heap.dump.HeapDumpMetadata; import com.oracle.svm.core.heap.dump.HeapDumpShutdownHook; import com.oracle.svm.core.heap.dump.HeapDumpStartupHook; +import com.oracle.svm.core.heap.dump.HeapDumpSupportImpl; import com.oracle.svm.core.heap.dump.HeapDumpWriter; import com.oracle.svm.core.heap.dump.HeapDumping; import com.oracle.svm.core.jdk.RuntimeSupport; @@ -62,8 +61,8 @@ * Heap dumping on Native Image needs some extra metadata about all the classes and fields that are * present in the image. The necessary information is encoded as binary data at image build time * (see {@link #encodeMetadata}}). When the heap dumping is triggered at run-time, the metadata is - * decoded on the fly (see {@link com.oracle.svm.core.heap.dump.HeapDumpMetadata}) and used for - * writing the heap dump (see {@link HeapDumpWriter}). + * decoded on the fly (see {@link HeapDumpMetadata}) and used for writing the heap dump (see + * {@link HeapDumpWriter}). */ @AutomaticallyRegisteredFeature public class HeapDumpFeature implements InternalFeature { @@ -81,7 +80,7 @@ public boolean isInConfiguration(IsInConfigurationAccess access) { @Override public void duringSetup(DuringSetupAccess access) { HeapDumpMetadata metadata = new HeapDumpMetadata(); - HeapDumping heapDumpSupport = new com.oracle.svm.core.heap.dump.HeapDumpSupportImpl(metadata); + HeapDumping heapDumpSupport = new HeapDumpSupportImpl(metadata); ImageSingletons.add(HeapDumpSupport.class, heapDumpSupport); ImageSingletons.add(HeapDumping.class, heapDumpSupport); @@ -94,9 +93,6 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { if (VMInspectionOptions.hasHeapDumpSupport()) { RuntimeSupport.getRuntimeSupport().addStartupHook(new HeapDumpStartupHook()); RuntimeSupport.getRuntimeSupport().addShutdownHook(new HeapDumpShutdownHook()); - if (VMInspectionOptions.hasAttachSupport()) { - ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new HeapDumpDcmd()); - } } } @@ -110,7 +106,7 @@ public void afterCompilation(Feature.AfterCompilationAccess access) { /** * This method writes the metadata that is needed for heap dumping into one large byte[] (see - * {@link com.oracle.svm.core.heap.dump.HeapDumpMetadata} for more details). + * {@link HeapDumpMetadata} for more details). */ private static byte[] encodeMetadata(Collection types) { int maxTypeId = types.stream().mapToInt(t -> t.getHub().getTypeID()).max().orElse(0); diff --git a/substratevm/src/com.oracle.svm.native.libchelper/src/attachHelper.c b/substratevm/src/com.oracle.svm.native.libchelper/src/attachHelper.c new file mode 100644 index 000000000000..7085d3e03f67 --- /dev/null +++ b/substratevm/src/com.oracle.svm.native.libchelper/src/attachHelper.c @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef _WIN64 + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * This file is based on HotSpot C++ code. As we need a mix of Java and C code to replicate the + * HotSpot logic, only the callers on the Java-side are annotated with @BasedOnJDKFile. + */ + +#ifndef UNIX_PATH_MAX +#define UNIX_PATH_MAX sizeof(((struct sockaddr_un *)0)->sun_path) +#endif + +#define ROOT_UID 0 + +#define RESTARTABLE(_cmd, _result) do { \ + _result = _cmd; \ + } while(((int)_result == -1) && (errno == EINTR)) + +bool svm_is_root(uid_t uid){ + return ROOT_UID == uid; +} + +bool svm_matches_effective_uid_or_root(uid_t uid) { + return svm_is_root(uid) || geteuid() == uid; +} + +bool svm_matches_effective_uid_and_gid_or_root(uid_t uid, gid_t gid) { + return svm_is_root(uid) || (geteuid() == uid && getegid() == gid); +} + +void svm_attach_startup(char* fn) { + struct stat st; + int ret; + RESTARTABLE(stat(fn, &st), ret); + if (ret == 0) { + unlink(fn); + } +} + +void svm_attach_listener_cleanup(int s, char* path) { + if (s != -1) { + shutdown(s, SHUT_RDWR); + close(s); + } + if (path != NULL) { + unlink(path); + } +} + +/* Returns true if the socket file is valid. */ +bool svm_attach_check_socket_file(char* path) { + int ret; + struct stat st; + ret = stat(path, &st); + if (ret == -1) { // need to restart attach listener. + return false; + } + return true; +} + +bool svm_attach_is_init_trigger(char* fn) { + int ret; + struct stat st; + RESTARTABLE(stat(fn, &st), ret); + if (ret == 0) { + // simple check to avoid starting the attach mechanism when + // a bogus non-root user creates the file + if (svm_matches_effective_uid_or_root(st.st_uid)) { + return true; + } + } + return false; +} + +int svm_attach_create_listener(char* path) { + char initial_path[UNIX_PATH_MAX]; // socket file during setup + int listener; // listener socket (file descriptor) + + int n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path); + if (n >= (int)UNIX_PATH_MAX) { + return -1; + } + + // create the listener socket + listener = socket(PF_UNIX, SOCK_STREAM, 0); + if (listener == -1) { + return -1; + } + + // bind socket + struct sockaddr_un addr; + memset((void *)&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strcpy(addr.sun_path, initial_path); + unlink(initial_path); + int res = bind(listener, (struct sockaddr*)&addr, sizeof(addr)); + if (res == -1) { + close(listener); + return -1; + } + + // put in listen mode, set permissions, and rename into place + res = listen(listener, 5); + if (res == 0) { + RESTARTABLE(chmod(initial_path, S_IREAD|S_IWRITE), res); + if (res == 0) { + // make sure the file is owned by the effective user and effective group + // e.g. the group could be inherited from the directory in case the s bit + // is set. The default behavior on mac is that new files inherit the group + // of the directory that they are created in. + RESTARTABLE(chown(initial_path, geteuid(), getegid()), res); + if (res == 0) { + res = rename(initial_path, path); + } + } + } + if (res == -1) { + close(listener); + unlink(initial_path); + return -1; + } + + return listener; +} + +int svm_attach_wait_for_request(int listener) { + for (;;) { + int s; + + // wait for client to connect + struct sockaddr addr; + socklen_t len = sizeof(addr); + RESTARTABLE(accept(listener, &addr, &len), s); + if (s == -1) { + return -1; // log a warning? + } + + // get the credentials of the peer and check the effective uid/guid +#ifdef LINUX + struct ucred cred_info; + socklen_t optlen = sizeof(cred_info); + if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, (void *)&cred_info, &optlen) == + -1) { + close(s); + continue; + } + + if (!svm_matches_effective_uid_and_gid_or_root(cred_info.uid, + cred_info.gid)) { + close(s); + continue; + } +#endif +#ifdef BSD + uid_t puid; + gid_t pgid; + if (getpeereid(s, &puid, &pgid) != 0) { + close(s); + continue; + } + + if (!svm_matches_effective_uid_and_gid_or_root(puid, pgid)) { + close(s); + continue; + } +#endif + + return s; + } +} + +void svm_attach_shutdown_socket(int s) { + shutdown(s, SHUT_RDWR); +} + +#endif // !_WIN64 + diff --git a/substratevm/src/com.oracle.svm.test/src/META-INF/native-image/com.oracle.svm.test/native-image.properties b/substratevm/src/com.oracle.svm.test/src/META-INF/native-image/com.oracle.svm.test/native-image.properties index 83e390273833..53830eac03f2 100644 --- a/substratevm/src/com.oracle.svm.test/src/META-INF/native-image/com.oracle.svm.test/native-image.properties +++ b/substratevm/src/com.oracle.svm.test/src/META-INF/native-image/com.oracle.svm.test/native-image.properties @@ -8,5 +8,5 @@ Args= \ --features=com.oracle.svm.test.jfr.JfrTestFeature \ --add-opens=java.base/java.lang=ALL-UNNAMED \ --add-exports=org.graalvm.nativeimage.base/com.oracle.svm.util=ALL-UNNAMED \ - --enable-monitoring=all \ - -J--enable-preview \ No newline at end of file + --enable-monitoring=heapdump,jcmd,jfr,jmxclient,jmxserver,jvmstat,nmt,threaddump \ + -J--enable-preview diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/attach/AttachTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/attach/AttachTest.java deleted file mode 100644 index b6f9e21d11ce..000000000000 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/attach/AttachTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.test.attach; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import com.oracle.svm.core.attach.AttachApiSupport; -import com.oracle.svm.core.dcmd.HelpDcmd; -import com.oracle.svm.core.nmt.NmtDcmd; -import com.oracle.svm.core.jfr.dcmd.JfrCheckDcmd; -import com.oracle.svm.core.jfr.dcmd.JfrStopDcmd; -import com.oracle.svm.core.jfr.dcmd.JfrStartDcmd; -import com.oracle.svm.core.jfr.dcmd.JfrDumpDcmd; -import com.oracle.svm.core.heap.dump.HeapDumpDcmd; -import com.oracle.svm.core.thread.ThreadDumpStacksDcmd; - -import org.junit.Test; -import org.graalvm.nativeimage.ImageSingletons; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.List; - -public class AttachTest { - /** - * This test ensure attaching to the VM works and that all known DCMDs get registered correctly. - */ - @Test - public void testAttachAndDcmdRegistration() throws IOException, InterruptedException { - long pid = ProcessHandle.current().pid(); - List command = new ArrayList<>(); - command.add(System.getenv("JAVA_HOME") + "/bin/jcmd"); - command.add(String.valueOf(pid)); - command.add("help"); - // If the process crashes, this test will fail, not hang. - ProcessBuilder pb = new ProcessBuilder(command); - Process jcmdProc = pb.start(); - - InputStream stdout = jcmdProc.getInputStream(); - BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdout)); - - List expectedStrings = new ArrayList<>(List.of( - new HelpDcmd().getName(), - new NmtDcmd().getName(), - new JfrCheckDcmd().getName(), - new JfrStopDcmd().getName(), - new JfrDumpDcmd().getName(), - new JfrStartDcmd().getName(), - new HeapDumpDcmd().getName(), - new ThreadDumpStacksDcmd().getName())); - - String line; - while ((line = stdoutReader.readLine()) != null) { - expectedStrings.remove(line); - } - assertTrue("Not all DCMDs were registered correctly. ", expectedStrings.isEmpty()); - - int exitCode = jcmdProc.waitFor(); - assertEquals(0, exitCode); - } - - /** This test verifies an edge case. It checks the teardown/restart process. */ - @Test - public void testBadSocketFile() throws IOException, InterruptedException { - checkJcmd(); - // Abruptly delete socket file. - String tempDir = System.getProperty("java.io.tmpdir"); - File attachFile = new File(tempDir + "/.java_pid" + ProcessHandle.current().pid()); - boolean deletedSocketFile = Files.deleteIfExists(attachFile.toPath()); - - assertTrue(deletedSocketFile); - - // Issue command again and verify response is still correct. - checkJcmd(); - } - - @Test - public void testConcurrentAttach() throws InterruptedException { - int threadCount = 100; - Thread[] threads = new Thread[threadCount]; - - ImageSingletons.lookup(AttachApiSupport.class).teardown(); - - for (int i = 0; i < threadCount; i++) { - threads[i] = new Thread(() -> { - try { - checkJcmd(); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - }); - threads[i].start(); - } - - for (int i = 0; i < threadCount; i++) { - threads[i].join(); - } - } - - /** This is a helper method that uses JCMD to attach to the VM and request help info. */ - static void checkJcmd() throws IOException, InterruptedException { - long pid = ProcessHandle.current().pid(); - List command = new ArrayList<>(); - command.add(System.getenv("JAVA_HOME") + "/bin/jcmd"); - command.add(String.valueOf(pid)); - command.add("help"); - ProcessBuilder pb = new ProcessBuilder(command); - Process jcmdProc; - try { - jcmdProc = pb.start(); - } catch (IOException e) { - // Couldn't start process. - return; - } - - InputStream stdout = jcmdProc.getInputStream(); - BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdout)); - - boolean foundExpectedResponse = false; - String line; - while (true) { - line = stdoutReader.readLine(); - if (line == null) { - break; - } else if (line.contains("help")) { - foundExpectedResponse = true; - } - } - assertTrue(foundExpectedResponse); - - int exitCode = jcmdProc.waitFor(); - assertEquals(0, exitCode); - } -} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jcmd/JCmdTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jcmd/JCmdTest.java new file mode 100644 index 000000000000..378490ce3a58 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jcmd/JCmdTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.test.jcmd; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.oracle.svm.core.VMInspectionOptions; + +public class JCmdTest { + @BeforeClass + public static void checkForJFR() { + assumeTrue("skipping JCmd tests", VMInspectionOptions.hasJCmdSupport()); + } + + @Test + public void testHelp() throws IOException, InterruptedException { + Process jcmd = runJCmd("help"); + String[] commands = new String[]{"GC.heap_dump", "GC.run", "JFR.check", "JFR.dump", "JFR.start", "JFR.stop", "Thread.dump_to_file", "Thread.print", "VM.command_line", + "VM.native_memory", "VM.system_properties", "VM.uptime", "VM.version", "help"}; + assertOutputContainsLines(jcmd, commands); + + for (String command : commands) { + jcmd = runJCmd("help", command); + assertOutputContainsStrings(jcmd, "Impact: ", "Syntax : " + command); + } + + for (String command : commands) { + jcmd = runJCmd(command, "-h"); + assertOutputContainsStrings(jcmd, "Impact: ", "Syntax : " + command); + } + } + + @Test + public void testBadSocketFile() throws IOException, InterruptedException { + checkJCmdConnection(); + + /* Delete the socket file. */ + String tempDir = System.getProperty("java.io.tmpdir"); + Path attachFile = Paths.get(tempDir, ".java_pid" + ProcessHandle.current().pid()); + boolean deletedSocketFile = Files.deleteIfExists(attachFile); + assertTrue(deletedSocketFile); + + checkJCmdConnection(); + } + + @Test + public void testConcurrentAttach() throws Throwable { + int threadCount = 32; + + AtomicReference exception = new AtomicReference<>(); + Runnable runnable = () -> { + try { + checkJCmdConnection(); + } catch (Throwable e) { + exception.set(e); + } + }; + + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(runnable); + threads[i].start(); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + + if (exception.get() != null) { + throw exception.get(); + } + } + + @Test + public void testJfr() throws IOException, InterruptedException { + Process jcmd = runJCmd("JFR.start", "name=JCmdTest"); + assertOutputContainsStrings(jcmd, "Started recording ", "JCmdTest"); + + jcmd = runJCmd("JFR.check"); + assertOutputContainsStrings(jcmd, "Recording ", "name=JCmdTest", "running"); + + jcmd = runJCmd("JFR.dump"); + assertOutputContainsStrings(jcmd, "Dumped recording", "graalvm-pid-"); + + jcmd = runJCmd("JFR.stop", "name=JCmdTest"); + assertOutputContainsLines(jcmd, "Stopped recording \"JCmdTest\"."); + } + + private static void checkJCmdConnection() throws IOException, InterruptedException { + Process jcmd = runJCmd("help"); + assertOutputContainsLines(jcmd, "help"); + } + + private static Process runJCmd(String... args) throws IOException { + long pid = ProcessHandle.current().pid(); + List process = new ArrayList<>(); + process.add(Paths.get(System.getenv("JAVA_HOME"), "bin", "jcmd").toString()); + process.add(String.valueOf(pid)); + process.addAll(List.of(args)); + return new ProcessBuilder(process).redirectErrorStream(true).start(); + } + + private static void assertOutputContainsLines(Process process, String... expectedLines) throws InterruptedException { + List remaining = new ArrayList<>(Arrays.asList(expectedLines)); + for (String line : getOutput(process)) { + remaining.remove(line); + } + + if (!remaining.isEmpty()) { + fail("The following lines were not found in the output: '" + String.join("', '", remaining) + "'"); + } + } + + private static void assertOutputContainsStrings(Process process, String... expected) throws InterruptedException { + List remaining = new ArrayList<>(Arrays.asList(expected)); + for (String line : getOutput(process)) { + remaining.removeIf(line::contains); + } + + if (!remaining.isEmpty()) { + fail("The following strings were not found in the output: '" + String.join("', '", remaining) + "'."); + } + } + + private static String[] getOutput(Process process) throws InterruptedException { + int exitCode = process.waitFor(); + + BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream())); + String[] lines = stdout.lines().toArray(String[]::new); + if (exitCode != 0) { + String lineBreak = System.lineSeparator(); + fail("jcmd returned with exit code " + exitCode + ": " + lineBreak + String.join(lineBreak, lines)); + } + return lines; + } +}