-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
8262731: [macOS] Exception from "Printable.print" is swallowed during "PrinterJob.print" #4036
Changes from 1 commit
db2ce06
4705151
746dc0c
056734b
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 |
---|---|---|
|
@@ -33,6 +33,7 @@ | |
import java.net.URI; | ||
import java.security.AccessController; | ||
import java.security.PrivilegedAction; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
import javax.print.*; | ||
import javax.print.attribute.PrintRequestAttributeSet; | ||
|
@@ -60,6 +61,7 @@ public final class CPrinterJob extends RasterPrinterJob { | |
private static String sShouldNotReachHere = "Should not reach here."; | ||
|
||
private volatile SecondaryLoop printingLoop; | ||
private AtomicReference<Exception> lastPrintExRef = new AtomicReference<>(); | ||
|
||
private boolean noDefaultPrinter = false; | ||
|
||
|
@@ -256,6 +258,14 @@ private void completePrintLoop() { | |
} | ||
} | ||
|
||
private Exception setLastPrintEx(Exception newEx, boolean printOldEx) { | ||
Exception oldEx = lastPrintExRef.getAndSet(newEx); | ||
if (printOldEx && (oldEx != null)) { | ||
oldEx.printStackTrace(); | ||
} | ||
return oldEx; | ||
} | ||
|
||
boolean isPrintToFile = false; | ||
private void setPrintToFile(boolean printToFile) { | ||
isPrintToFile = printToFile; | ||
|
@@ -322,6 +332,7 @@ public void print(PrintRequestAttributeSet attributes) throws PrinterException { | |
performingPrinting = true; | ||
userCancelled = false; | ||
} | ||
setLastPrintEx(null, false); | ||
|
||
//Add support for PageRange | ||
PageRanges pr = (attributes == null) ? null | ||
|
@@ -380,6 +391,19 @@ public SecondaryLoop run() { | |
if (printingLoop != null) { | ||
printingLoop.exit(); | ||
} | ||
|
||
Exception lastPrintEx = setLastPrintEx(null, false); | ||
if (lastPrintEx != null) { | ||
if (lastPrintEx instanceof PrinterException) { | ||
throw (PrinterException) lastPrintEx; | ||
} else if (lastPrintEx instanceof RuntimeException) { | ||
throw (RuntimeException) lastPrintEx; | ||
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. I don't see you testing this case 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. In the 2nd version of the fix I added testing of the scenario with "RuntimeException". Yes, this code is executed only after the execution of the "printLoop" is fully finished for each of all page ranges. If printing of one page fails within one page range, then printing of other pages does not occur. "CPrinterJob.print(PrintRequestAttributeSet)" is blocking until printing is finished or fails, and my code is executed in the very end of the method. |
||
} else { | ||
PrinterException pe = new PrinterException(); | ||
pe.initCause(lastPrintEx); | ||
throw pe; | ||
} | ||
} | ||
} | ||
|
||
// Normalize the collated, # copies, numPages, first/last pages. Need to | ||
|
@@ -785,22 +809,37 @@ private Object[] getPageformatPrintablePeekgraphics(final int pageIndex) { | |
private Rectangle2D printAndGetPageFormatArea(final Printable printable, final Graphics graphics, final PageFormat pageFormat, final int pageIndex) { | ||
final Rectangle2D[] ret = new Rectangle2D[1]; | ||
|
||
Runnable r = new Runnable() { public void run() { synchronized(ret) { | ||
try { | ||
int pageResult = printable.print(graphics, pageFormat, pageIndex); | ||
if (pageResult != Printable.NO_SUCH_PAGE) { | ||
ret[0] = getPageFormatArea(pageFormat); | ||
Runnable r = new Runnable() { | ||
@Override | ||
public void run() { | ||
synchronized (ret) { | ||
try { | ||
int pageResult = printable.print( | ||
graphics, pageFormat, pageIndex); | ||
if (pageResult != Printable.NO_SUCH_PAGE) { | ||
ret[0] = getPageFormatArea(pageFormat); | ||
} | ||
} catch (Exception e) { | ||
// Original code bailed on any exception | ||
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. Throwable ? says I take that to mean we should not carry on printing .. but it looks like we just note the exception and carry on. We do want to grab the exception so it doesn't mess up interverning code but don't we want this to reach the caller of PrinterJob.print - in the form of a PrinterExcepton ? 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. In the 2nd version of the fix I substituted "Exception" for "Throwable" in this catch block and in second "catch" block several lines below. Also in the 2nd fix version I removed this comment about bailing. My opinion is the same, the code before the fix did not work according to the specification and was ignoring any caught exceptions. Perhaps, the reason of this is involvement of more than 1 thread in the code flow standing behind "PrinterJob.print" method execution and execution of the JDK's code through JNI in this code flow. Thus propagating the exception is impossible across threads and it is not easy to stop printing done by macOS at the moment, when the exception occurs in Java code in JDK. This pattern is used in the file "CPrinterJob.java" in other places, even there is the same comment in the method "CPrinterJob.getPageformatPrintablePeekgraphics". At the same time propagating "RuntimeException" from "PrinterJob.print" on Windows, Linux and now on macOS after this fix also is not according to the specification but still consistency in JDK behavior on these OS versions should be good. As I understand, the method "CPrinterJob.printAndGetPageFormatArea" returns "null" as the "Rectangle2D", when the exception is caught, and this "null" value should automatically lead to returning of "NSZeroRect" from the Objective-C method "(NSRect)rectForPage:(NSInteger)pageNumber" in the file "src/java.desktop/macosx/native/libawt_lwawt/awt/PrinterView.m", from which "CPrinterJob.printAndGetPageFormatArea" Java method was invoked. And this "NSZeroRect" return value is indication of the error for macOS, which further leads to stopping of the printing process. |
||
setLastPrintEx(e, true); | ||
} | ||
} | ||
} catch (Exception e) {} // Original code bailed on any exception | ||
}}}; | ||
} | ||
}; | ||
|
||
if (onEventThread) { | ||
try { EventQueue.invokeAndWait(r); } catch (Exception e) { e.printStackTrace(); } | ||
try { | ||
EventQueue.invokeAndWait(r); | ||
} catch (Exception e) { | ||
setLastPrintEx(e, true); | ||
} | ||
} else { | ||
r.run(); | ||
} | ||
|
||
synchronized(ret) { return ret[0]; } | ||
synchronized (ret) { | ||
return ret[0]; | ||
} | ||
} | ||
|
||
// upcall from native | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* Copyright (c) 2021, 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 8262731 | ||
@key headful printer | ||
@summary Verify that "PrinterJob.print" throws the exception, if | ||
"Printable.print" throws "PrinterException". | ||
@run main/manual ExceptionFromPrintableIsIgnoredTest MAIN | ||
@run main/manual ExceptionFromPrintableIsIgnoredTest EDT | ||
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. See comment below |
||
*/ | ||
|
||
import java.awt.Graphics; | ||
import java.awt.print.PageFormat; | ||
import java.awt.print.Printable; | ||
import java.awt.print.PrinterException; | ||
import java.awt.print.PrinterJob; | ||
import java.lang.reflect.InvocationTargetException; | ||
import javax.swing.SwingUtilities; | ||
|
||
public class ExceptionFromPrintableIsIgnoredTest { | ||
private enum TestThreadType {MAIN, EDT} | ||
|
||
public static void main(String[] args) { | ||
if (args.length < 1) { | ||
throw new RuntimeException("Test thread type is not specified."); | ||
} | ||
|
||
TestThreadType threadType = TestThreadType.valueOf(args[0]); | ||
new ExceptionFromPrintableIsIgnoredTest(threadType); | ||
} | ||
|
||
public ExceptionFromPrintableIsIgnoredTest(TestThreadType threadType) { | ||
System.out.println(String.format( | ||
"Test started. threadType='%s'", threadType)); | ||
|
||
if (threadType == TestThreadType.MAIN) { | ||
runTest(); | ||
} else if (threadType == TestThreadType.EDT) { | ||
try { | ||
SwingUtilities.invokeAndWait(new Runnable() { | ||
@Override | ||
public void run() { | ||
runTest(); | ||
} | ||
}); | ||
} catch (InterruptedException | InvocationTargetException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
System.out.println("Test passed."); | ||
} | ||
|
||
private void runTest() { | ||
PrinterJob job = PrinterJob.getPrinterJob(); | ||
job.setPrintable(new Printable() { | ||
@Override | ||
public int print(Graphics graphics, PageFormat pageFormat, | ||
int pageIndex) throws PrinterException { | ||
if (pageIndex > 1) { | ||
return NO_SUCH_PAGE; | ||
} | ||
throw new PrinterException("Exception from Printable.print"); | ||
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. Don't you want to also test this with some kind of RuntimeException ? 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. In the 1st version of the fix I did not test the scenario with "RuntimeException", because despite of the fact that I had known that JDK 17 will pass on Windows and Linux the scenario with "RuntimeException" I still was not sure if the behavior of older JDK release families is the same with JDK 17 on Windows and Linux, and just did not want to run the risk of failure of this test during potential porting of the fix to older JDK release families. But yesterday I did testing with my standalone test case attached to the bug record in JBS and now I can certainly say that JDK 8u291-b10, JDK JDK 8-b132 do not fail in the test scenarios with "RuntimeException" on Windows and Linux. Therefore it is safe to include test scenario with "RuntimeException" to the regression test. In the 2nd version of the fix now I am testing also "RuntimeException" both on EDT thread and on main thread. |
||
} | ||
}); | ||
if (job.printDialog()) { | ||
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. why do we need a dialog ? If you remove this it can be an automated test. 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. Hello Phil. Thank you very much for review of this fix. First I am answering your question, which is about why the test passes on Windows and which is your first and separate comment not bound to the source code. The test passes on Windows, because in JDK 17 for both Windows and Linux JDK works similarly stably and reliably, JDK 17 on Window and Linux successfully propagates "PrinterException" and "RuntimeException", if it is thrown from "Printable.print" method. I had verified this using my standalone manual test case "ExceptionFromPrintableIsIgnored.java" attached to the bug record in JBS, before sending the 1st version of the fix for review. I did not explore, why it is so, because it works on Windows and Linux and this bug is related to macOS, but I think that it works on Windows and Linux, because in JDK implementations for Windows and Linux printing is consistently done by JDK on the same thread on which "PrinterJob.print" was called and no portions of the involved code are executed asynchronously on other thread, like EDT in case of macOS. I agree there is no need in the dialog in the regression test. In the 1st version of the fix I did it deliberately, to make it more manual, because the test initiates printing and there is no guarantee what type of printer would be set up on the test host, if some virtual printer is used on the host, that printer can show its own native dialog asking to specify location of a file, in which the printed document should be saved. That is why I think that this test cannot be fully automatic. In the 2nd version of the fix which I am submitting right now I removed this unnecessary code showing JDK print dialog. |
||
Exception printEx = null; | ||
try { | ||
job.print(); | ||
} catch (PrinterException pe) { | ||
printEx = pe; | ||
} | ||
|
||
if (printEx != null) { | ||
System.out.println("'PrinterJob.print' threw the exception:"); | ||
printEx.printStackTrace(System.out); | ||
} else { | ||
throw new RuntimeException( | ||
"'PrinterJob.print' did not throw any exception."); | ||
} | ||
} else { | ||
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. When running on the EDT do you really want to do this ? 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. Yes, I do really want to do this, when running on EDT, the whole test relies on this approach. When the test is run on EDT this is done via "SwingUtilities.invokeAndWait" method in the constructor of the test "ExceptionFromPrintableIsIgnoredTest" and in that constructor there is "try/catch" block. If some exception occurs on EDT and is not caught on EDT, then it will be wrapped by JDK in "InvocationTargetException" instance which will be thrown from "SwingUtilities.invokeAndWait" method and will be caught by that "try/catch" block in the constructor of the test. I checked that this code works before sending the code for review and today additionally. |
||
throw new RuntimeException("User canceled the print dialog."); | ||
} | ||
} | ||
} |
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.
Why not Throwable ?
I suggest to not do this. ie don't print and don't replace Instead swallow the new exception
and let the original problem that started it get propagated. That seems more likely to be useful.
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.
Thank you for this remark. I agree using "Throwable" is more solid approach. In the 2nd version of the fix I use "Throwable" in this "AtomicReference" object and I removed completely this method "setLastPrintEx".