-
Notifications
You must be signed in to change notification settings - Fork 6k
8353950: Clipboard interaction on Windows is unstable #24614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e893b36
24c0d0a
4fa4ef9
e5ebf59
bfa99a7
50ccf76
b930745
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (c) 1996, 2014, Oracle and/or its affiliates. All rights reserved. | ||
* Copyright (c) 1996, 2025, 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 | ||
|
@@ -29,7 +29,9 @@ | |
import java.awt.datatransfer.Transferable; | ||
import java.awt.datatransfer.UnsupportedFlavorException; | ||
import java.io.IOException; | ||
import java.lang.System.Logger.Level; | ||
import java.util.Map; | ||
import java.util.concurrent.locks.ReentrantLock; | ||
|
||
import sun.awt.datatransfer.DataTransferer; | ||
import sun.awt.datatransfer.SunClipboard; | ||
|
@@ -51,8 +53,12 @@ final class WClipboard extends SunClipboard { | |
|
||
private boolean isClipboardViewerRegistered; | ||
|
||
private final ReentrantLock clipboardLocked = new ReentrantLock(); | ||
|
||
WClipboard() { | ||
super("System"); | ||
// Register java side of the clipboard with the native side | ||
registerClipboard(); | ||
} | ||
|
||
@Override | ||
|
@@ -104,18 +110,42 @@ protected void clearNativeContext() {} | |
|
||
/** | ||
* Call the Win32 OpenClipboard function. If newOwner is non-null, | ||
* we also call EmptyClipboard and take ownership. | ||
* we also call EmptyClipboard and take ownership. If this method call | ||
* succeeds, it must be followed by a call to {@link #closeClipboard()}. | ||
* | ||
* @throws IllegalStateException if the clipboard has not been opened | ||
*/ | ||
@Override | ||
public native void openClipboard(SunClipboard newOwner) throws IllegalStateException; | ||
public void openClipboard(SunClipboard newOwner) throws IllegalStateException { | ||
if (!clipboardLocked.tryLock()) { | ||
throw new IllegalStateException("Failed to acquire clipboard lock"); | ||
} | ||
try { | ||
openClipboard0(newOwner); | ||
} catch (IllegalStateException ex) { | ||
clipboardLocked.unlock(); | ||
throw ex; | ||
} | ||
} | ||
|
||
/** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. was this comment accidentally deleted? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restored the comment and adjusted the comment on |
||
* Call the Win32 CloseClipboard function if we have clipboard ownership, | ||
* does nothing if we have not ownership. | ||
*/ | ||
@Override | ||
public native void closeClipboard(); | ||
public void closeClipboard() { | ||
if (clipboardLocked.isLocked()) { | ||
try { | ||
closeClipboard0(); | ||
} finally { | ||
clipboardLocked.unlock(); | ||
} | ||
} | ||
} | ||
|
||
private native void openClipboard0(SunClipboard newOwner) throws IllegalStateException; | ||
private native void closeClipboard0(); | ||
|
||
/** | ||
* Call the Win32 SetClipboardData function. | ||
*/ | ||
|
@@ -157,16 +187,12 @@ private void handleContentsChanged() { | |
return; | ||
} | ||
|
||
long[] formats = null; | ||
try { | ||
openClipboard(null); | ||
formats = getClipboardFormats(); | ||
} catch (IllegalStateException exc) { | ||
// do nothing to handle the exception, call checkChange(null) | ||
} finally { | ||
closeClipboard(); | ||
long[] formats = getClipboardFormats(); | ||
checkChange(formats); | ||
} catch (Throwable ex) { | ||
System.getLogger(WClipboard.class.getName()).log(Level.DEBUG, "Failed to process handleContentsChanged", ex); | ||
} | ||
checkChange(formats); | ||
} | ||
|
||
/** | ||
|
@@ -214,4 +240,6 @@ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorExcepti | |
} | ||
}; | ||
} | ||
|
||
private native void registerClipboard(); | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test passed even without the fix on macOS. If yes, then I think you should restrict the test to run only on required platforms. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/* | ||
* Copyright (c) 2025, 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. | ||
* | ||
* 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. | ||
*/ | ||
|
||
/* | ||
@test | ||
@bug 8332271 | ||
@summary tests that concurrent access to the clipboard does not crash the JVM | ||
@key headful | ||
@requires (os.family == "windows") | ||
@run main ConcurrentClipboardAccessTest | ||
*/ | ||
import java.awt.Toolkit; | ||
import java.awt.datatransfer.Clipboard; | ||
import java.awt.datatransfer.DataFlavor; | ||
|
||
public class ConcurrentClipboardAccessTest { | ||
|
||
public static void main(String[] args) { | ||
Thread clipboardLoader1 = new Thread(new ClipboardLoader()); | ||
clipboardLoader1.setDaemon(true); | ||
clipboardLoader1.start(); | ||
Thread clipboardLoader2 = new Thread(new ClipboardLoader()); | ||
clipboardLoader2.setDaemon(true); | ||
clipboardLoader2.start(); | ||
long start = System.currentTimeMillis(); | ||
while (true) { | ||
try { | ||
Thread.sleep(1000); | ||
} catch (InterruptedException ignored) { | ||
} | ||
long now = System.currentTimeMillis(); | ||
if ((now - start) > (10L * 1000L)) { | ||
break; | ||
} | ||
} | ||
// Test is considered successful if the concurrent repeated reading | ||
// from clipboard succeeds for the allotted time and the JVM does not | ||
// crash. | ||
System.out.println("Shutdown normally"); | ||
} | ||
|
||
public static class ClipboardLoader implements Runnable { | ||
|
||
@Override | ||
public void run() { | ||
final Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); | ||
while (true) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will this cause the test to run until timeout if the thread isn't interrupted? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid point. Adjusted test to use daemon threads for the concurrent access tests. I reran the tests on the baseline JDK and after the changes to verify the findings from #24614 (comment). Did not notice this as the jtreg harness indeed seems to terminate the JVM and not report that as a failure, tested now also manually (without jtreg). |
||
try { | ||
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) { | ||
systemClipboard.getData(DataFlavor.stringFlavor); | ||
} | ||
} catch (Exception ignored) { | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you help me understand why this is better? Seems odd that the try/finally block has
closeClipboard
in the finally block but theopenClipboard
was moved out of the try.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before the change the call order was inconsistent:
ClipboardTransferable
: one call,openClipboard
is called before thetry
-blockWClipboard
: one call withopenClipboard
before thetry
-block, one call withopenClipboard
as first statement in thetry
-blockSunClipboard
: three calls withopenClipboard
as first statement inside thetry
-blockI unified these so that in all cases
openClipboard
is called as the last statement before the try-block. The assumption is, that only ifopenClipboard
succeeds (does not raise an exception), callingcloseClipboard
makes sense.