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 6fcfce6e8539..0714be289ea4 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. Add `--enable-monitoring=jcmd` to your build arguments to try it out. * (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.posix/src/com/oracle/svm/core/posix/attach/package-info.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/package-info.java new file mode 100644 index 000000000000..959261dd786e --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/attach/package-info.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +@Platforms({Platform.LINUX.class, Platform.DARWIN.class}) +package com.oracle.svm.core.posix.attach; + +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.posix/src/com/oracle/svm/core/posix/jvmstat/package-info.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/package-info.java new file mode 100644 index 000000000000..0ff26cec9e22 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/package-info.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +@Platforms({Platform.LINUX.class, Platform.DARWIN.class}) +package com.oracle.svm.core.posix.jvmstat; + +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 f5a522796491..88cb354d9fb5 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 @@ -599,7 +599,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 981fd5380bdd..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 @@ -1,5 +1,6 @@ /* - * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 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 @@ -30,16 +31,12 @@ import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.graal.RuntimeCompilation; -import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.jdk.RuntimeSupport; -import com.oracle.svm.core.log.Log; -import com.oracle.svm.core.thread.JavaVMOperation; import jdk.internal.misc.Signal; @AutomaticallyRegisteredFeature public class DumpRuntimeCompilationOnSignalFeature implements InternalFeature { - @Override public boolean isInConfiguration(IsInConfigurationAccess access) { return VMInspectionOptions.DumpRuntimeCompilationOnSignal.getValue() && !Platform.includedIn(WINDOWS.class) && RuntimeCompilation.isEnabled(); @@ -67,20 +64,6 @@ static void install() { @Override public void handle(Signal arg0) { - DumpRuntimeCompiledMethodsOperation vmOp = new DumpRuntimeCompiledMethodsOperation(); - vmOp.enqueue(); - } - - private static class DumpRuntimeCompiledMethodsOperation extends JavaVMOperation { - DumpRuntimeCompiledMethodsOperation() { - super(VMOperationInfos.get(DumpRuntimeCompiledMethodsOperation.class, "Dump runtime compiled methods", SystemEffect.SAFEPOINT)); - } - - @Override - protected void operate() { - Log log = Log.log(); - SubstrateDiagnostics.dumpRuntimeCompilation(log); - log.flush(); - } + DumpRuntimeCompilationSupport.dump(); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationSupport.java new file mode 100644 index 000000000000..1e74f988584c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationSupport.java @@ -0,0 +1,52 @@ +/* + * 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; + +import com.oracle.svm.core.heap.VMOperationInfos; +import com.oracle.svm.core.log.Log; +import com.oracle.svm.core.thread.JavaVMOperation; + +public class DumpRuntimeCompilationSupport { + + public static void dump() { + DumpRuntimeCompiledMethodsOperation vmOp = new DumpRuntimeCompiledMethodsOperation(); + vmOp.enqueue(); + } + + private static class DumpRuntimeCompiledMethodsOperation extends JavaVMOperation { + DumpRuntimeCompiledMethodsOperation() { + super(VMOperationInfos.get(DumpRuntimeCompiledMethodsOperation.class, "Dump runtime compiled methods", SystemEffect.SAFEPOINT)); + } + + @Override + protected void operate() { + Log log = Log.log(); + SubstrateDiagnostics.dumpRuntimeCompilation(log); + log.flush(); + } + } +} 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/DumpThreadStacksSupport.java similarity index 74% 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/DumpThreadStacksSupport.java index b4bd97ef37b2..d1f506c3c55c 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/DumpThreadStacksSupport.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -22,17 +23,13 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + package com.oracle.svm.core; import org.graalvm.nativeimage.CurrentIsolate; import org.graalvm.nativeimage.IsolateThread; -import org.graalvm.nativeimage.Platform; -import org.graalvm.nativeimage.Platform.WINDOWS; -import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; -import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.heap.VMOperationInfos; -import com.oracle.svm.core.jdk.RuntimeSupport; import com.oracle.svm.core.log.Log; import com.oracle.svm.core.stack.JavaStackWalker; import com.oracle.svm.core.stack.ThreadStackPrinter.StackFramePrintVisitor; @@ -41,38 +38,8 @@ import com.oracle.svm.core.thread.PlatformThreads; import com.oracle.svm.core.thread.VMThreads; -import jdk.internal.misc.Signal; - -@AutomaticallyRegisteredFeature -public class DumpThreadStacksOnSignalFeature implements InternalFeature { - - @Override - public boolean isInConfiguration(IsInConfigurationAccess access) { - return VMInspectionOptions.hasThreadDumpSupport(); - } - - @Override - public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeSupport.getRuntimeSupport().addStartupHook(new DumpThreadStacksOnSignalStartupHook()); - } -} - -final class DumpThreadStacksOnSignalStartupHook implements RuntimeSupport.Hook { - @Override - public void execute(boolean isFirstIsolate) { - if (isFirstIsolate) { - DumpAllStacks.install(); - } - } -} - -class DumpAllStacks implements Signal.Handler { - static void install() { - Signal.handle(Platform.includedIn(WINDOWS.class) ? new Signal("BREAK") : new Signal("QUIT"), new DumpAllStacks()); - } - - @Override - public void handle(Signal arg0) { +public class DumpThreadStacksSupport { + public static void dump() { DumpAllStacksOperation vmOp = new DumpAllStacksOperation(); vmOp.enqueue(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SigQuitFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SigQuitFeature.java new file mode 100644 index 000000000000..973a58ca4ffc --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SigQuitFeature.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2017, 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; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platform.WINDOWS; + +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 SigQuitFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return VMInspectionOptions.hasThreadDumpSupport() || VMInspectionOptions.hasJCmdSupport(); + } + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + RuntimeSupport.getRuntimeSupport().addStartupHook(new RegisterSigQuitHandlerStartupHook()); + } +} + +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) { + String signal = Platform.includedIn(WINDOWS.class) ? "BREAK" : "QUIT"; + Signal.handle(new Signal(signal), new SigQuitHandler()); + } + } +} + +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) { + 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 27e7fbd6ff29..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,13 +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 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_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 + "' (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"; @@ -80,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); @@ -105,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); } } } @@ -190,6 +208,11 @@ public static boolean hasNativeMemoryTrackingSupport() { return hasAllOrKeywordMonitoringSupport(MONITORING_NMT_NAME); } + @Fold + public static boolean hasJCmdSupport() { + return hasAllOrKeywordMonitoringSupport(MONITORING_JCMD_NAME) && !Platform.includedIn(WINDOWS.class); + } + static class DeprecatedOptions { @Option(help = "Enables features that allow the VM to be inspected during run time.", type = OptionType.User, // deprecated = true, deprecationMessage = "Please use '--" + ENABLE_MONITORING_OPTION + "'") // 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 new file mode 100644 index 000000000000..95cd3ddf8787 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java @@ -0,0 +1,67 @@ +/* + * 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.attach; + +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 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.hasJCmdSupport(); + } + + @Override + public List> getRequiredFeatures() { + return Collections.singletonList(RuntimeSupportFeature.class); + } + + @Override + public void duringSetup(DuringSetupAccess access) { + RuntimeSupport.getRuntimeSupport().addShutdownHook(new AttachApiTeardownHook()); + } +} + +final class AttachApiTeardownHook implements RuntimeSupport.Hook { + @Override + public void execute(boolean isFirstIsolate) { + 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 new file mode 100644 index 000000000000..1c0a31b3211c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java @@ -0,0 +1,51 @@ +/* + * 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.attach; + +import org.graalvm.nativeimage.ImageSingletons; + +import jdk.graal.compiler.api.replacements.Fold; + +/** Interface for the attach API mechanism. */ +public interface AttachApiSupport { + @Fold + static boolean isPresent() { + return ImageSingletons.contains(AttachApiSupport.class); + } + + @Fold + static AttachApiSupport singleton() { + return ImageSingletons.lookup(AttachApiSupport.class); + } + + void startup(); + + boolean isInitTrigger(); + + void initialize(); + + 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 new file mode 100644 index 000000000000..718357e81510 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java @@ -0,0 +1,156 @@ +/* + * 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.attach; + +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; + +/** + * A dedicated listener thread that accepts client connections and that handles diagnostic command + * requests. At the moment, only jcmd is supported. + */ +@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"; + @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); + } + + @Override + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+18/src/hotspot/share/services/attachListener.cpp#L377-L436") + public void run() { + try { + while (true) { + AttachOperation op = dequeue(); + if (op == null) { + /* Dequeue failed or shutdown. */ + AttachApiSupport.singleton().shutdown(false); + return; + } + + if (JCMD_COMMAND_STRING.equals(op.name)) { + handleJcmd(op); + } else { + op.complete(JNIErrors.JNI_ERR(), "Invalid Operation. Only jcmd is supported currently."); + } + } + } catch (Throwable e) { + VMError.shouldNotReachHere("Exception in attach listener thread", e); + } + } + + @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); + } + } + + @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); + } + } + + /* 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 void handleException(AttachOperation op, Throwable e) { + if (!Options.JCmdExceptionStackTrace.getValue()) { + op.complete(JNIErrors.JNI_ERR(), e.toString()); + return; + } + + StringWriter s = new StringWriter(); + e.printStackTrace(new PrintWriter(s)); + + /* jcmd swallows line breaks if JNI_ERR() is used, so use JNI_OK() instead. */ + op.complete(JNIErrors.JNI_OK(), s.toString()); + } + + protected abstract AttachOperation dequeue(); + + 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 AttachOperation(String name, String arg0, String arg1, String arg2) { + this.name = name; + this.arg0 = arg0; + this.arg1 = arg1; + this.arg2 = arg2; + } + + 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..4c835d8080e0 --- /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.required() && !result.hasBeenSet(arg)) { + throw new IllegalArgumentException("The argument '" + arg.name() + "' is mandatory."); + } + } + + /* Check that all mandatory options have been set. */ + for (DCmdOption option : options) { + if (option.required() && !result.hasBeenSet(option)) { + throw new IllegalArgumentException("The option '" + option.name() + "' 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.name().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.type(); + 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.name() + "'. 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.required()) { + sb.append("["); + } + sb.append("<").append(option.name()).append(">"); + if (!option.required()) { + 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.name()).append(" : "); + if (!option.required()) { + sb.append("[optional] "); + } + sb.append(option.description()); + sb.append(" (").append(typeToString(option)).append(", "); + if (option.defaultValue() != null) { + sb.append(option.defaultValue()); + } else { + sb.append("no default value"); + } + sb.append(")"); + } + + private static String typeToString(DCmdOption option) { + Class type = option.type(); + 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/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/dcmd/CompilerDumpCodeCacheDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/CompilerDumpCodeCacheDCmd.java new file mode 100644 index 000000000000..2c52de3d2132 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/CompilerDumpCodeCacheDCmd.java @@ -0,0 +1,44 @@ +/* + * 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.DumpRuntimeCompilationSupport; + +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 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..fb0787f9ebdf --- /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.type().isAssignableFrom(value.getClass()); + values.put(option, value); + } + + @SuppressWarnings("unchecked") + public T get(DCmdOption option) { + Object value = values.get(option); + if (value == null) { + return option.defaultValue(); + } + 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 new file mode 100644 index 000000000000..16d449320c31 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DCmdOption.java @@ -0,0 +1,30 @@ +/* + * 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 record DCmdOption(Class type, String name, String description, boolean required, T 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/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/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/dcmd/ThreadPrintDCmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/ThreadPrintDCmd.java new file mode 100644 index 000000000000..ac8e09b49c35 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/ThreadPrintDCmd.java @@ -0,0 +1,47 @@ +/* + * 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.DumpThreadStacksSupport; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@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 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/VMVersionDmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMVersionDmd.java new file mode 100644 index 000000000000..b6e1f92022c0 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/VMVersionDmd.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.dcmd; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.VM; +import com.oracle.svm.core.util.BasedOnJDKFile; + +@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 + @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/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 new file mode 100644 index 000000000000..2cd2a792909a --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java @@ -0,0 +1,179 @@ +/* + * 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; + +import java.io.Serial; +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 { + public static Map parseJfrOptions(RuntimeOptionKey runtimeOptionKey, JfrArgument[] possibleArguments) throws JfrArgumentParsingFailed { + String userInput = runtimeOptionKey.getValue(); + if (!userInput.isEmpty()) { + String[] options = StringUtil.split(userInput, ","); + return parseJfrOptions(options, possibleArguments); + } + return new HashMap<>(); + } + + private static Map parseJfrOptions(String[] options, JfrArgument[] possibleArguments) throws JfrArgumentParsingFailed { + Map optionsMap = new HashMap<>(); + + for (String option : options) { + String[] keyVal = StringUtil.split(option, "="); + if (keyVal.length != 2) { + 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"); + } + optionsMap.put(arg, keyVal[1]); + } + + return optionsMap; + } + + @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); + if (value == null) { + return null; + } else if ("true".equalsIgnoreCase(value)) { + return true; + } else if ("false".equalsIgnoreCase(value)) { + return false; + } else { + throw new JfrArgumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. Expected a boolean value."); + } + } + + public static Integer parseInteger(Map args, JfrArgument key) throws JfrArgumentParsingFailed { + String value = args.get(key); + if (value != null) { + try { + return Integer.valueOf(value); + } catch (Throwable e) { + throw new JfrArgumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); + } + } + return null; + } + + public static Long parseMaxSize(Map args, JfrArgument key) throws JfrArgumentParsingFailed { + String value = args.get(key); + if (value == null) { + return null; + } + + try { + int idx = indexOfFirstNonDigitCharacter(value); + long number; + try { + number = Long.parseLong(value.substring(0, idx)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Expected a positive number."); + } + + // Missing unit, number is plain bytes + if (idx == value.length()) { + return number; + } + + char unit = value.substring(idx).charAt(0); + return switch (unit) { + case 'k', 'K' -> number * 1024; + case 'm', 'M' -> number * 1024 * 1024; + case 'g', 'G' -> number * 1024 * 1024 * 1024; + default -> number; // Unknown unit, number is treated as plain bytes + }; + } catch (IllegalArgumentException e) { + throw new JfrArgumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); + } + } + + private static int indexOfFirstNonDigitCharacter(String durationText) { + int idx = 0; + while (idx < durationText.length() && Character.isDigit(durationText.charAt(idx))) { + idx++; + } + return idx; + } + + private static JfrArgument findArgument(JfrArgument[] possibleArguments, String value) { + for (JfrArgument arg : possibleArguments) { + if (arg.getCmdLineKey().equals(value)) { + return arg; + } + } + return null; + } + + public interface JfrArgument { + String getCmdLineKey(); + } + + public enum FlightRecorderOptionsArgument implements JfrArgument { + GlobalBufferSize("globalbuffersize"), + MaxChunkSize("maxchunksize"), + MemorySize("memorysize"), + OldObjectQueueSize("old-object-queue-size"), + RepositoryPath("repository"), + StackDepth("stackdepth"), + ThreadBufferSize("threadbuffersize"), + PreserveRepository("preserve-repository"); + + private final String cmdLineKey; + + FlightRecorderOptionsArgument(String key) { + this.cmdLineKey = key; + } + + @Override + public String getCmdLineKey() { + return cmdLineKey; + } + } + + public static class JfrArgumentParsingFailed extends RuntimeException { + @Serial private static final long serialVersionUID = -1050173145647068124L; + + JfrArgumentParsingFailed(String message, Throwable cause) { + super(message, cause); + } + + JfrArgumentParsingFailed(String message) { + super(message); + } + } +} 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..4e6dc4be511c 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,16 @@ 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.SubstrateOptions; 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 +57,6 @@ * go away. */ @SuppressWarnings("unused") - public final class JfrJdkCompatibility { private JfrJdkCompatibility() { } @@ -126,8 +129,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 +140,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 +165,13 @@ 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(); + String imageName = SubstrateOptions.Name.getValue(); + return imageName + "-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 045de0e9059d..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 @@ -1,5 +1,6 @@ /* - * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 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,17 +25,12 @@ */ package com.oracle.svm.core.jfr; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Serial; -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 org.graalvm.nativeimage.ImageSingletons; @@ -42,32 +38,27 @@ 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.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.option.RuntimeOptionKey; -import com.oracle.svm.core.util.UserError.UserException; +import com.oracle.svm.core.util.BasedOnJDKFile; import jdk.graal.compiler.api.replacements.Fold; -import jdk.graal.compiler.core.common.SuppressFBWarnings; -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; /** * Called during VM startup and teardown. Also triggers the JFR argument parsing. */ public class JfrManager { - private static final String DEFAULT_JFC_NAME = "default"; @Platforms(Platform.HOSTED_ONLY.class) // final boolean hostedEnabled; @@ -101,7 +92,7 @@ public static RuntimeSupport.Hook startupHook() { }; } - private static void parseFlightRecorderOptions() { + private static void parseFlightRecorderOptions() throws JfrArgumentParsingFailed { Map optionsArgs = parseJfrOptions(SubstrateOptions.FlightRecorderOptions, FlightRecorderOptionsArgument.values()); Long globalBufferSize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.GlobalBufferSize); Long maxChunkSize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.MaxChunkSize); @@ -128,7 +119,7 @@ private static void parseFlightRecorderOptions() { if (oldObjectQueueSize >= 0) { SubstrateJVM.getOldObjectProfiler().configure(oldObjectQueueSize); } else { - throw argumentParsingFailed(FlightRecorderOptionsArgument.OldObjectQueueSize.getCmdLineKey() + " must be greater or equal 0."); + throw new JfrArgumentParsingFailed(FlightRecorderOptionsArgument.OldObjectQueueSize.getCmdLineKey() + " must be greater or equal 0."); } } @@ -137,7 +128,7 @@ private static void parseFlightRecorderOptions() { SecuritySupport.SafePath repositorySafePath = new SecuritySupport.SafePath(repositoryPath); Repository.getRepository().setBasePath(repositorySafePath); } catch (Throwable e) { - throw argumentParsingFailed("Could not use " + repositoryPath + " as repository. " + e.getMessage(), e); + throw new JfrArgumentParsingFailed("Could not use " + repositoryPath + " as repository. " + e.getMessage(), e); } } @@ -177,397 +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() { - Map startArgs = parseJfrOptions(SubstrateOptions.StartFlightRecording, JfrStartArgument.values()); - 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 argumentParsingFailed("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 argumentParsingFailed("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 argumentParsingFailed("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 argumentParsingFailed("Could not find settings file'" + configName + "'", e); - } catch (IOException | ParseException e) { - throw argumentParsingFailed("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 argumentParsingFailed("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 argumentParsingFailed("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()); - } - - private 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(); - } - } - - private static Map parseJfrOptions(RuntimeOptionKey runtimeOptionKey, JfrArgument[] possibleArguments) { - Map optionsMap = new HashMap<>(); - String userInput = runtimeOptionKey.getValue(); - if (!userInput.isEmpty()) { - String[] options = userInput.split(","); - for (String option : options) { - String[] keyVal = option.split("="); - JfrArgument arg = findArgument(possibleArguments, keyVal[0]); - if (arg == null) { - throw argumentParsingFailed("Unknown argument '" + keyVal[0] + "' in " + runtimeOptionKey.getName()); - } - optionsMap.put(arg, keyVal[1]); - } - } - return optionsMap; - } - - private static String[] parseSettings(Map args) throws UserException { - 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") - private static Boolean parseBoolean(Map args, JfrArgument key) throws IllegalArgumentException { - String value = args.get(key); - if (value == null) { - return null; - } else if ("true".equalsIgnoreCase(value)) { - return true; - } else if ("false".equalsIgnoreCase(value)) { - return false; - } else { - throw argumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. Expected a boolean value."); - } - } - - private static Long parseDuration(Map args, JfrStartArgument key) { - 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 argumentParsingFailed("Could not parse JFR argument '" + key.cmdLineKey + "=" + value + "'. " + e.getMessage()); - } - } - return null; - } - - private static Integer parseInteger(Map args, JfrArgument key) { - String value = args.get(key); - if (value != null) { - try { - return Integer.valueOf(value); - } catch (Throwable e) { - throw argumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); - } - } - return null; - } - - private static Long parseMaxSize(Map args, JfrArgument key) { - String value = args.get(key); - if (value == null) { - return null; - } - - try { - int idx = indexOfFirstNonDigitCharacter(value); - long number; - try { - number = Long.parseLong(value.substring(0, idx)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Expected a positive number."); - } - - // Missing unit, number is plain bytes - if (idx == value.length()) { - return number; - } - - final char unit = value.substring(idx).charAt(0); - return switch (unit) { - case 'k', 'K' -> number * 1024; - case 'm', 'M' -> number * 1024 * 1024; - case 'g', 'G' -> number * 1024 * 1024 * 1024; - default -> number; // Unknown unit, number is treated as plain bytes - }; - } catch (IllegalArgumentException e) { - throw argumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); - } - } - - private static int indexOfFirstNonDigitCharacter(String durationText) { - int idx = 0; - while (idx < durationText.length() && Character.isDigit(durationText.charAt(idx))) { - idx++; - } - return idx; - } - - private static JfrArgument findArgument(JfrArgument[] possibleArguments, String value) { - for (JfrArgument arg : possibleArguments) { - if (arg.getCmdLineKey().equals(value)) { - return arg; - } - } - return null; - } - - private static RuntimeException argumentParsingFailed(String message) { - throw new JfrArgumentParsingFailed(message); - } - - private static RuntimeException argumentParsingFailed(String message, Throwable cause) { - throw new JfrArgumentParsingFailed(message, cause); - } - - private interface JfrArgument { - String getCmdLineKey(); - } - - private 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; - } - } - - private enum FlightRecorderOptionsArgument implements JfrArgument { - GlobalBufferSize("globalbuffersize"), - MaxChunkSize("maxchunksize"), - MemorySize("memorysize"), - OldObjectQueueSize("old-object-queue-size"), - RepositoryPath("repository"), - StackDepth("stackdepth"), - ThreadBufferSize("threadbuffersize"), - PreserveRepository("preserve-repository"); - - private final String cmdLineKey; - - FlightRecorderOptionsArgument(String key) { - this.cmdLineKey = key; - } - - @Override - public String getCmdLineKey() { - return cmdLineKey; - } - } - - private static class JfrArgumentParsingFailed extends RuntimeException { - @Serial private static final long serialVersionUID = -1050173145647068124L; - - JfrArgumentParsingFailed(String message, Throwable cause) { - super(message, cause); - } - - JfrArgumentParsingFailed(String message) { - super(message); - } + 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/jvmstat/SystemCounters.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java index 66ffbf021f4c..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,17 @@ 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; /** @@ -52,6 +54,7 @@ class SystemCounters implements PerfDataHolder, VMOperationListener { private final PerfLongConstant frequency; private final PerfLongConstant loadedClasses; private final PerfLongConstant processors; + private final PerfStringConstant jvmCapabilities; // Exported system properties. private final PerfStringConstant tempDir; @@ -98,6 +101,7 @@ class SystemCounters implements PerfDataHolder, VMOperationListener { osName = perfManager.createStringConstant("java.property.os.name"); userDir = perfManager.createStringConstant("java.property.user.dir"); userName = perfManager.createStringConstant("java.property.user.name"); + jvmCapabilities = perfManager.createStringConstant("sun.rt.jvmCapabilities"); gcInProgress = perfManager.createLongVariable("com.oracle.svm.gcInProgress", PerfUnit.NONE); @@ -132,6 +136,7 @@ public void allocate() { osName.allocate(getSystemProperty("os.name")); userDir.allocate(getSystemProperty("user.dir")); userName.allocate(getSystemProperty("user.name")); + jvmCapabilities.allocate(getJvmCapabilities()); gcInProgress.allocate(); @@ -154,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/NativeMemoryTracking.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java index 74d416a33aa8..c46a177f4d5e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java @@ -325,8 +325,9 @@ public static RuntimeSupport.Hook shutdownHook() { }; } - private void printStatistics() { + public void printStatistics() { if (VMInspectionOptions.PrintNMTStatistics.getValue()) { + System.out.println(); System.out.println(generateReportString()); } } @@ -335,7 +336,6 @@ public String generateReportString() { String lineBreak = System.lineSeparator(); StringBuilder result = new StringBuilder(3000); - result.append(lineBreak); result.append("Native memory tracking").append(lineBreak).append(lineBreak); result.append("Total").append(lineBreak); 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 c572af6f462a..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 @@ -43,6 +43,7 @@ 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; @@ -60,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 { @@ -79,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); @@ -105,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..247ee1eb6783 --- /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; + strncpy(addr.sun_path, initial_path, UNIX_PATH_MAX); + 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 e1470477f070..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=jvmstat,jfr,jmxserver,jmxclient,nmt \ - -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/jcmd/JCmdTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jcmd/JCmdTest.java new file mode 100644 index 000000000000..44322e0efd0c --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jcmd/JCmdTest.java @@ -0,0 +1,178 @@ +/* + * 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.graalvm.nativeimage.Platform; +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 { + assumeTrue("Fails transiently on MacOS", !Platform.includedIn(Platform.DARWIN.class)); + 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", "svmjunit-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; + } +}