Compare commits

..

8 Commits

Author SHA1 Message Date
Maxim Kartashev
bfa8e73dcf JBR-9016 Make screenshot JBR API work on Windows 2025-09-05 17:33:08 +04:00
Maxim Kartashev
5493f14d30 JBR-9016 Add API for making screenshots of some regions of the application without interacting with OS 2025-09-04 13:23:01 +04:00
Maxim Kartashev
09ecf47329 JBR-9228 KDE: jb/java/awt/Toolkit/DetectingOSThemeTest.java fails 2025-09-04 12:01:59 +04:00
Maxim Kartashev
b3becb25a2 JBR-9289 Wayland: re-enable window shadow by default 2025-09-03 13:19:50 +04:00
Maxim Kartashev
8bad559f91 JBR-9289 Wayland: an option to turn window shadow off
Use -Dsun.awt.wl.Shadow=false to turn all the window shadows off
2025-09-03 12:49:12 +04:00
Nikita Gubarkov
43cbf9a7db JBR-7334 Skip custom title bar reconfiguration if nothing changed 2025-09-01 15:17:52 +02:00
Vitaly Provodin
3c70bd7f50 fixup! JBR-9238 Introduce distinct test groups for Vulkan runs 2025-08-29 15:47:14 +04:00
Vitaly Provodin
dca326f640 JBR-9274 turn off streaming output for attach API by default 2025-08-29 15:47:14 +04:00
10 changed files with 263 additions and 211 deletions

View File

@@ -53,10 +53,14 @@ import java.awt.event.TextEvent;
import java.awt.im.InputContext;
import java.awt.im.InputMethodRequests;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.VolatileImage;
import java.awt.image.WritableRaster;
import java.awt.peer.ComponentPeer;
import java.awt.peer.ContainerPeer;
import java.awt.peer.LightweightPeer;
@@ -89,6 +93,7 @@ import javax.accessibility.AccessibleStateSet;
import javax.swing.JComponent;
import javax.swing.JRootPane;
import com.jetbrains.exported.JBRApi;
import sun.awt.AWTAccessor;
import sun.awt.AppContext;
import sun.awt.ComponentFactory;
@@ -99,6 +104,7 @@ import sun.awt.EmbeddedFrame;
import sun.awt.RequestFocusController;
import sun.awt.SubRegionShowable;
import sun.awt.SunToolkit;
import sun.awt.SurfacePixelGrabber;
import sun.awt.dnd.SunDropTargetEvent;
import sun.awt.im.CompositionArea;
import sun.awt.image.VSyncedBSManager;
@@ -10559,4 +10565,79 @@ public abstract class Component implements ImageObserver, MenuContainer,
}
return p.updateCustomTitleBarHitTest(allowNativeActions);
}
@JBRApi.Provides("Screenshoter#getWindowBackbufferArea")
private static BufferedImage getWindowBackbufferArea(Window window, int x, int y, int width, int height) {
Objects.requireNonNull(window);
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Negative coordinates are not allowed");
}
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("The size must be positive");
}
Image fullBackbuffer = window.getBackBuffer();
if (fullBackbuffer == null) {
return null;
}
var bufferWidth = fullBackbuffer.getWidth(null);
var bufferHeight = fullBackbuffer.getHeight(null);
if (x >= width) {
throw new IllegalArgumentException(String.format("x coordinate (%d) is out of bounds (%d)", x, bufferWidth));
}
if (y >= height) {
throw new IllegalArgumentException(String.format("y coordinate (%d) is out of bounds (%d)", y, bufferHeight));
}
if ((long) x + width > bufferWidth) {
width = bufferWidth - x;
}
if ((long) y + height > bufferHeight) {
height = bufferHeight - y;
}
if (fullBackbuffer instanceof BufferedImage bufferedImage) {
return bufferedImage.getSubimage(x, y, width, height);
} else {
ColorModel colorModel = window.getGraphicsConfiguration().getColorModel();
SampleModel sampleModel = colorModel.createCompatibleSampleModel(width, height);
WritableRaster raster = Raster.createWritableRaster(sampleModel, null);
BufferedImage image = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);
image.getGraphics().drawImage(fullBackbuffer,
0, 0, width, height,
x, y, x + width, y + height,
null);
return image;
}
}
@JBRApi.Provides("Screenshoter#getWindowSurfaceArea")
private static BufferedImage getWindowSurfaceArea(Window window, int x, int y, int width, int height) {
Objects.requireNonNull(window);
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Negative coordinates are not allowed");
}
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("The size must be positive");
}
ComponentPeer peer = window.peer;
if (peer == null || !window.isVisible()) {
return null;
}
if (peer instanceof SurfacePixelGrabber spg) {
// TODO: translate coordinates, maybe?
return spg.getClientAreaSnapshot(x, y, width, height);
}
return null;
}
}

View File

@@ -3950,6 +3950,7 @@ public class Window extends Container implements Accessible {
private float getHeight() { return height; }
private void setHeight(float height) {
if (height <= 0.0f) throw new IllegalArgumentException("TitleBar height must be positive");
if (this.height == height) return;
this.height = height;
notifyUpdate();
}
@@ -3958,13 +3959,17 @@ public class Window extends Container implements Accessible {
}
private void putProperties(Map<String, ?> m) {
if (properties == null) properties = new HashMap<>();
properties.putAll(m);
notifyUpdate();
boolean needsUpdate = false;
for (Map.Entry<String, ?> e : m.entrySet()) {
Object old = properties.put(e.getKey(), e.getValue());
if (!needsUpdate && !Objects.equals(old, e.getValue())) needsUpdate = true;
}
if (needsUpdate) notifyUpdate();
}
private void putProperty(String key, Object value) {
if (properties == null) properties = new HashMap<>();
properties.put(key, value);
notifyUpdate();
Object old = properties.put(key, value);
if (!Objects.equals(old, value)) notifyUpdate();
}
private float getLeftInset() { return insets[0]; }
private float getRightInset() { return insets[1]; }

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 JetBrains s.r.o.
* 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 sun.awt;
import java.awt.image.BufferedImage;
public interface SurfacePixelGrabber {
BufferedImage getClientAreaSnapshot(int x, int y, int width, int height);
}

View File

@@ -137,7 +137,7 @@ public abstract class VKSurfaceData extends SurfaceData
}
}
protected BufferedImage getSnapshot(int x, int y, int width, int height) {
public BufferedImage getSnapshot(int x, int y, int width, int height) {
BufferedImage image = getFormat().createCompatibleImage(width, height, getTransparency());
SurfaceData sd = SurfaceData.getPrimarySurfaceData(image);
Blit blit = Blit.getFromCache(getSurfaceType(), CompositeType.SrcNoEa, sd.getSurfaceType());

View File

@@ -30,12 +30,14 @@ import sun.awt.AWTAccessor;
import sun.awt.AWTAccessor.ComponentAccessor;
import sun.awt.PaintEventDispatcher;
import sun.awt.SunToolkit;
import sun.awt.SurfacePixelGrabber;
import sun.awt.event.IgnorePaintEvent;
import sun.awt.image.SunVolatileImage;
import sun.java2d.SunGraphics2D;
import sun.java2d.SunGraphicsEnvironment;
import sun.java2d.SurfaceData;
import sun.java2d.pipe.Region;
import sun.java2d.vulkan.VKSurfaceData;
import sun.java2d.wl.WLSurfaceDataExt;
import sun.java2d.wl.WLSurfaceSizeListener;
import sun.util.logging.PlatformLogger;
@@ -135,7 +137,12 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
log.fine("WLComponentPeer: target=" + target + " with size=" + wlSize);
}
shadow = new Shadow(targetIsWlPopup() ? ShadowImage.POPUP_SHADOW_SIZE : ShadowImage.WINDOW_SHADOW_SIZE);
boolean shadowEnabled = Boolean.parseBoolean(System.getProperty("sun.awt.wl.Shadow", "true"));
if (shadowEnabled) {
shadow = new ShadowImpl(targetIsWlPopup() ? ShadowImage.POPUP_SHADOW_SIZE : ShadowImage.WINDOW_SHADOW_SIZE);
} else {
shadow = new NilShadow();
}
// TODO
// setup parent window for target
}
@@ -1611,6 +1618,14 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
return new Dimension(javaUnitsToSurfaceSize(d.width), javaUnitsToSurfaceSize(d.height));
}
int javaUnitsToBufferUnits(int value) {
return (int) Math.floor(value * effectiveScale);
}
int javaSizeToBufferSize(int value) {
return (int) Math.ceil(value * effectiveScale);
}
/**
* Converts a point in the device (screen) space into coordinates on this surface
*/
@@ -1812,6 +1827,20 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
return result;
}
private interface Shadow {
int getSize();
void updateSurfaceSize();
void resizeToParentWindow();
void createSurface();
void commitSurface();
void dispose();
void hide();
void updateSurfaceData();
void paint();
void commitSurfaceData();
void notifyConfigured(boolean active, boolean maximized, boolean fullscreen);
}
private static class ShadowImage {
private static final Color activeColor = new Color(0, 0, 0, 0xA0);
private static final Color inactiveColor = new Color(0, 0, 0, 0x40);
@@ -1916,7 +1945,7 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
}
}
private class Shadow implements WLSurfaceSizeListener {
private class ShadowImpl implements WLSurfaceSizeListener, Shadow {
private WLSubSurface shadowSurface; // protected by AWT lock
private SurfaceData shadowSurfaceData; // protected by AWT lock
private boolean needsRepaint = true; // protected by AWT lock
@@ -1924,7 +1953,7 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
private final WLSize shadowWlSize = new WLSize(); // protected by stateLock
private boolean isActive; // protected by AWT lock
public Shadow(int shadowSize) {
public ShadowImpl(int shadowSize) {
this.shadowSize = shadowSize;
shadowWlSize.deriveFromJavaSize(wlSize.getJavaWidth() + shadowSize * 2, wlSize.getJavaHeight() + shadowSize * 2);
shadowSurfaceData = ((WLGraphicsConfig) getGraphicsConfiguration()).createSurfaceData(this, shadowWlSize.getPixelWidth(), shadowWlSize.getPixelHeight());
@@ -2039,6 +2068,20 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
}
}
private static class NilShadow implements Shadow {
@Override public int getSize() { return 0; }
@Override public void updateSurfaceSize() { }
@Override public void resizeToParentWindow() { }
@Override public void createSurface() { }
@Override public void commitSurface() { }
@Override public void dispose() { }
@Override public void hide() { }
@Override public void updateSurfaceData() { }
@Override public void paint() { }
@Override public void commitSurfaceData() { }
@Override public void notifyConfigured(boolean active, boolean maximized, boolean fullscreen) { }
}
private class WLSize {
/**
* Represents the full size of the component in "client" units as returned by Component.getSize().

View File

@@ -25,7 +25,10 @@
package sun.awt.wl;
import sun.awt.AWTAccessor;
import sun.awt.SurfacePixelGrabber;
import sun.java2d.SunGraphics2D;
import sun.java2d.vulkan.VKSurfaceData;
import sun.java2d.wl.WLSMSurfaceData;
import javax.swing.JRootPane;
import javax.swing.RootPaneContainer;
@@ -36,16 +39,18 @@ import java.awt.Dialog;
import java.awt.Font;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.SystemColor;
import java.awt.Window;
import java.awt.event.WindowEvent;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.awt.peer.ComponentPeer;
import java.awt.peer.WindowPeer;
import java.lang.ref.WeakReference;
public class WLWindowPeer extends WLComponentPeer implements WindowPeer {
public class WLWindowPeer extends WLComponentPeer implements WindowPeer, SurfacePixelGrabber {
private static Font defaultFont;
private Dialog blocker;
@@ -236,6 +241,64 @@ public class WLWindowPeer extends WLComponentPeer implements WindowPeer {
synthFocusOwner = new WeakReference<>(c);
}
@Override
public BufferedImage getClientAreaSnapshot(int x, int y, int width, int height) {
// Move the coordinate system to the client area
Insets insets = getInsets();
x += insets.left;
y += insets.top;
if (width <= 0 || height <= 0) {
return null;
}
if (x < 0 || y < 0) {
// Shouldn't happen, but better avoid accessing surface data outside the range
throw new IllegalArgumentException("Negative coordinates are not allowed");
}
if (x >= getWidth()) {
throw new IllegalArgumentException(String.format("x coordinate (%d) is out of bounds (%d)", x, getWidth()));
}
if (y >= getHeight()) {
throw new IllegalArgumentException(String.format("y coordinate (%d) is out of bounds (%d)", y, getHeight()));
}
if ((long) x + width > getWidth()) {
width = getWidth() - x;
}
if ((long) y + height > getHeight()) {
height = getHeight() - y;
}
// At this point the coordinates and size are in Java units;
// need to convert them into pixels.
Rectangle bounds = new Rectangle(
javaUnitsToBufferUnits(x),
javaUnitsToBufferUnits(y),
javaSizeToBufferSize(width),
javaSizeToBufferSize(height)
);
Rectangle bufferBounds = getBufferBounds();
if (bounds.x >= bufferBounds.width) {
bounds.x = bufferBounds.width - 1;
}
if (bounds.y >= bufferBounds.height) {
bounds.y = bufferBounds.height - 1;
}
if (bounds.x + bounds.width > bufferBounds.width) {
bounds.width = bufferBounds.width - bounds.x;
}
if (bounds.y + bounds.height > bufferBounds.height) {
bounds.height = bufferBounds.height - bounds.y;
}
if (surfaceData instanceof VKSurfaceData vksd) {
return vksd.getSnapshot(bounds.x, bounds.y, bounds.width, bounds.height);
} else if (surfaceData instanceof WLSMSurfaceData smsd) {
return smsd.getSnapshot(bounds.x, bounds.y, bounds.width, bounds.height);
}
return null;
}
private boolean canPaintRoundedCorners() {
int roundedCornerSize = WLRoundedCornersManager.roundCornerRadiusFor(roundedCornerKind);
// Note: You would normally get a transparency-capable color model when using

View File

@@ -26,19 +26,25 @@
package sun.java2d.wl;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Window;
import java.awt.GraphicsConfiguration;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.Objects;
import sun.awt.AWTAccessor;
import sun.awt.wl.WLComponentPeer;
import sun.awt.wl.WLGraphicsConfig;
import sun.awt.wl.WLSMGraphicsConfig;
import sun.java2d.SurfaceData;
import sun.java2d.loops.Blit;
import sun.java2d.loops.CompositeType;
import sun.java2d.loops.SurfaceType;
import sun.util.logging.PlatformLogger;
@@ -124,16 +130,20 @@ public class WLSMSurfaceData extends SurfaceData implements WLSurfaceDataExt, WL
return gc;
}
public BufferedImage getSnapshot(int x, int y, int width, int height) {
ColorModel colorModel = getColorModel();
SampleModel sampleModel = colorModel.createCompatibleSampleModel(width, height);
WritableRaster raster = Raster.createWritableRaster(sampleModel, null);
BufferedImage image = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);
SurfaceData sd = SurfaceData.getPrimarySurfaceData(image);
Blit blit = Blit.getFromCache(getSurfaceType(), CompositeType.SrcNoEa, sd.getSurfaceType());
blit.Blit(this, sd, AlphaComposite.Src, null, x, y, 0, 0, width, height);
return image;
}
@Override
public Raster getRaster(int x, int y, int w, int h) {
// Can do something like the following:
// Raster r = getColorModel().createCompatibleWritableRaster(w, h);
// copy surface data to this raster
// save a reference to this raster
// return r;
// then in flush() check if raster was modified and take pixels from there
// This is obviously suboptimal and shouldn't be used in performance-critical situations.
throw new UnsupportedOperationException("Not implemented yet");
return getSnapshot(x, y, w, h).getRaster().createTranslatedChild(x, y);
}
@Override

View File

@@ -35,6 +35,7 @@ public class DetectingOSThemeTest {
private static final String LIGHT_THEME_NAME = "Light";
private static final String DARK_THEME_NAME = "Dark";
private static final String UNDEFINED_THEME_NAME = "Undefined";
private static boolean isKDE = false;
private static String currentTheme() {
Boolean val = (Boolean) Toolkit.getDefaultToolkit().getDesktopProperty("awt.os.theme.isDark");
@@ -44,8 +45,14 @@ public class DetectingOSThemeTest {
return (val) ? DARK_THEME_NAME : LIGHT_THEME_NAME;
}
private static void setOsDarkTheme(String val) {
try {
private static void setOsDarkTheme(String val) throws Exception {
if (isKDE) {
if (val.equals(DARK_THEME_NAME)) {
Runtime.getRuntime().exec("plasma-apply-colorscheme BreezeDark");
} else {
Runtime.getRuntime().exec("plasma-apply-colorscheme BreezeLight");
}
} else {
if (val.equals(DARK_THEME_NAME)) {
Runtime.getRuntime().exec("gsettings set org.gnome.desktop.interface gtk-theme 'Adwaita-dark'");
Runtime.getRuntime().exec("gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'");
@@ -53,14 +60,13 @@ public class DetectingOSThemeTest {
Runtime.getRuntime().exec("gsettings set org.gnome.desktop.interface gtk-theme 'Adwaita'");
Runtime.getRuntime().exec("gsettings set org.gnome.desktop.interface color-scheme 'default'");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static String currentTheme = null;
public static void main(String[] args) throws Exception {
isKDE = "KDE".equals(System.getenv("XDG_CURRENT_DESKTOP"));
currentTheme = currentTheme();
if (currentTheme.equals(UNDEFINED_THEME_NAME)) {
throw new RuntimeException("Test Failed! Cannot detect current OS theme");

View File

@@ -1,71 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o.
* 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.
*
* 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.
*/
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import java.util.concurrent.TimeUnit;
/**
* @test
* @bug 8354460
* @summary <code>JStackHangTest</code> verifies the streaming output of the attach API. It launches
* <code>JStackLauncher</code> in a child VM and ensures <code>jstack</code> does not hang.
* @library /test/lib
* @compile JStackLauncher.java
* @run main/othervm JStackHangTest
*/
public class JStackHangTest {
static final int JSTACK_WAIT_TIME = JStackLauncher.JSTACK_WAIT_TIME * 2;
public static final int CODE_NOT_RETURNED = 100;
public static void main(String[] args) throws Exception {
ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(JStackLauncher.class.getName());
Process process = ProcessTools.startProcess("Child", pb);
OutputAnalyzer outputAnalyzer = new OutputAnalyzer(process);
int returnCode;
if (!process.waitFor(JSTACK_WAIT_TIME, TimeUnit.MILLISECONDS)) {
System.out.println("jstack did not complete in " + JSTACK_WAIT_TIME + " ms.");
System.out.println("killing all the jstack processes");
ProcessTools.executeCommand("killall", "-9", "jstack");
returnCode = CODE_NOT_RETURNED;
} else {
returnCode = outputAnalyzer.getExitValue();
System.out.println("returnCode = " + returnCode);
}
if (returnCode == CODE_NOT_RETURNED)
throw new RuntimeException("jstack: failed to start or hanged");
else
System.out.println("jstack: completed successfully");
}
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o.
* 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.
*
* 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.
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
/**
* <code>JStackLauncher</code> is used to launch <code>jstack</code> in the test of streaming output for the attach API
*
*/
public class JStackLauncher {
public static final String JAVA_HOME = System.getProperty("java.home");
public static final String JAVA_BIN = JAVA_HOME + File.separator + "bin";
public static final String JSTACK_PATH = JAVA_BIN + File.separator + "jstack";
static final int JSTACK_WAIT_TIME = 5000;
static final int THREAD_COUNT = 10;
static final int STACK_DEPTH = 100;
static Process process;
static CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
public static void main(String[] args) throws InterruptedException {
// Create threads with deep stacks
List<Thread> threads = createManyThreadsWithDeepStacks();
// Run jstack against current process
System.out.println("Starting jstack...");
try {
process = new ProcessBuilder(JSTACK_PATH, String.valueOf(ProcessHandle.current().pid())).start();
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("Reading jstack output...");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
Stream<String> output = reader.lines();
output.forEach(System.out::println);
if (!process.waitFor(JSTACK_WAIT_TIME, TimeUnit.MILLISECONDS)) {
process.destroyForcibly();
throw new RuntimeException("jstack failed to complete in " + JSTACK_WAIT_TIME + " ms.");
}
threads.forEach(Thread::interrupt);
}
public static List<Thread> createManyThreadsWithDeepStacks() throws InterruptedException {
List<Thread> threads = new ArrayList<>();
for (int threadIndex = 0; threadIndex < THREAD_COUNT; threadIndex++) {
System.out.println("Creating thread " + threadIndex);
Thread t = new DeepStackThread(threadIndex);
t.start();
threads.add(t);
}
latch.await();
return threads;
}
static class DeepStackThread extends Thread {
DeepStackThread(int index) {
super.setName("DeepStackThread-" + index);
}
@Override
public synchronized void run() {
try {
createDeepStack(STACK_DEPTH);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread " + getName() + " exiting");
}
void createDeepStack(int depth) throws InterruptedException {
if (depth > 0) {
createDeepStack(depth - 1);
} else {
latch.countDown();
Thread.sleep(Long.MAX_VALUE);
}
}
}
}