6

As the title says, I would like to refresh the printers that are registered in the settings of the computer while my Java application is running. Normally, I can use PrinterJob.lookupPrintServices() to get the printers. However, these are only refreshed when restarting the application. I've read something on that lookupPrintServices() should be done in a new thread in order to get the printers. This however did not work, the list of printers remains the same. The following link shows that this problem should have been fixed in Java 5.0, am I doing something wrong?

Any help is much appreciated!

EDIT Added MWE.

public class MTPrinterTest extends Thread {
    public static void main(String[] args) {
        MTPrinterTest t1 = new MTPrinterTest();
        t1.start();

        try {
            System.in.read();
        } catch (Exception e){}

        MTPrinterTest t2 = new MTPrinterTest();
        t2.start();
    }
    public void run() {
        PrinterJob printerJob;
        PrintService[] printServices;

        printerJob = PrinterJob.getPrinterJob();
        printServices = printerJob.lookupPrintServices();
        System.out.println("Number of servies found: " + printServices.length);
        for (int i =0; i< printServices.length; i++)
            System.out.println("--> Available Printer " + i + ": " + printServices[i]);
        printerJob.printDialog(); 
    } 
}
Robin Trietsch
  • 1,662
  • 2
  • 19
  • 31
  • Questions seeking debugging help ("why isn't this code working?") must include the desired behavior, a specific problem or error and the shortest code necessary to reproduce it in the question itself. Questions without a clear problem statement are not useful to other readers. See: [How to create a Minimal, Complete, and Verifiable example.](http://stackoverflow.com/help/mcve) – durron597 Feb 11 '15 at 14:02
  • Thanks for mentioning, I've added a MWE. – Robin Trietsch Feb 11 '15 at 14:07

5 Answers5

3

There is no need to restart the application in order to refresh the list of print services.

Here I found the solution:

/**
 * Printer list does not necessarily refresh if you change the list of 
 * printers within the O/S; you can run this to refresh if necessary.
 */
public static void refreshSystemPrinterList() {
    Class<?>[] classes = PrintServiceLookup.class.getDeclaredClasses();
    for (Class<?> clazz : classes) {
        if ("javax.print.PrintServiceLookup$Services".equals(clazz.getName())) {
            // sun.awt.AppContext.getAppContext().remove(clazz);
            // Use reflection to avoid "Access restriction" error message
            try {
                Class<?> acClass = Class.forName("sun.awt.AppContext");
                Object appContext = acClass.getMethod("getAppContext").invoke(null);
                acClass.getMethod("remove", Object.class).invoke(appContext, clazz);
            } catch (Exception e) {
            }
            break;
        }
    }
}

Basically, the static class PrintServiceLookup.Services maintains the list of print services. So, if you remove this class from the AppContext, you force PrintServiceLookup to create a new instance again. Thus, the list of print services gets refreshed.

IvanRF
  • 7,115
  • 5
  • 47
  • 71
  • 1
    `sun.awt.AppContext` receives a warning when used with OpenJDK11. `warning: AppContext is internal proprietary API and may be removed in a future release`. Is there a way to do this without compiler warnings? – tresf May 30 '19 at 04:46
  • I've recently discovered that this solution causes a serious regression performance dealing with CUPS. Specifically, a larger number of searches will bring the searching to a crawl. Details here: https://github.com/qzind/tray/issues/479 – tresf Aug 10 '19 at 15:40
  • @tresf I run your test code on Windows and there are no performance issues, so as you mentioned it seems to be an issue related to Java on MacOS. Let me know if you find a workaround. – IvanRF Aug 10 '19 at 21:31
  • It appears that the JVM leaves an orphaned thread with CUPS. A possible workaround is documented here https://gist.github.com/Vzor-/68f3e6759b4e45071a8bb36735062a8a. Note, this is ultimately an upstream bug and the awt workaround is not guaranteed to work in future Java versions. – tresf Aug 13 '19 at 13:16
  • 1
    CUPS seems to be happy with using a system property for this. `System.setProperty("sun.java2d.print.polling", "false");`. More information is here: https://github.com/AdoptOpenJDK/openjdk-build/issues/1212 – tresf Aug 14 '19 at 13:38
  • I can't seem to reproduce the original bug report on Windows at all any more. Do you know if this bug still exists? I'm testing with openjdk11 and Windows refreshes near instantaneously, it's really CUPS (Linux, MacOS) that suffer. – tresf Aug 15 '19 at 14:45
1

I have encountered the same problem before and after multiple tests, it seems like the printer list is snapshotted at the start of the Java application and can't be refreshed after that using java's lookupPrintServices().

What I did to solve that problem is call directly the Winspool API using JNA. If you intend to do so, the Winspool API is well documented by Microsoft : Winspool API documentation

Also, I described a part of my solution to a problem I had a few month ago in this question, it might help you understand JNA and the Winspool API.

Community
  • 1
  • 1
Padrus
  • 2,013
  • 1
  • 24
  • 37
  • Thanks for your reply. I'll just restart te application if the printers are changes in the OS settings. No other option, since Java makes a snapshot of the printers. – Robin Trietsch Feb 11 '15 at 17:10
1

A quick workaround is for CUPS-based systems is:

System.setProperty("sun.java2d.print.polling", "false");

Warning, this has the side-effect of gradually slowing down calls (e.g. 80ms) to PrintServiceLookup.lookupPrintServices(...).

Specifically:

Property sun.java2d.print.polling set to true:

  • <1ms to call PrintServiceLookup.lookupPrintServices(...)

Property sun.java2d.print.polling set to false:

  • 80ms to call PrintServiceLookup.lookupPrintServices(...)

Although 80ms is a very short amount of time for most UI, in a high-load scenario this can affect performance.

Furthemore, this 80ms gradually increases over time. For example, 100,000 calls to PrintServiceLookup.lookupPrintServices(...) gradually increases the delay from 80ms to 1,000ms. Long-running programs may experience a noticeable delay.

That said, this delay is still prefered over the AppContext workaround, especially on CUPS systems (Linux, Mac). The AppContext provided in other solutions introduces JVM threading issues that eventually cause 2,000ms delays, and at times up to 200,000ms delays (reference: https://github.com/qzind/tray/issues/479)

Detailed bug report: https://github.com/AdoptOpenJDK/openjdk-build/issues/1212

tresf
  • 7,103
  • 6
  • 40
  • 101
1

Based on this solution link proposed by tresf of the problem link and in a previous answer of IvanRF, I created a new source code which is the merge of both codes.

The purpose of this code in addition to solving the problem is to eliminate the warning:

AppContext is internal proprietary API and may be removed in a future release

public static boolean refreshSystemPrinters() {
    try {
        for (final Thread thread : Thread.getAllStackTraces().keySet()) {
            if (thread.getName().equals("PrinterListener")) {
                thread.interrupt();
            }
        }
        final Class<?> appContextClass = Class.forName("sun.awt.AppContext");
        final Object appContext = appContextClass.getMethod("getAppContext").invoke(null);
        appContextClass.getMethod("put", Object.class, Object.class).invoke(appContext, javax.print.PrintServiceLookup.class.getDeclaredClasses()[0], null);
        javax.print.PrintServiceLookup.lookupPrintServices(null, null);
        return true;
    } catch (final Exception e) {
        e.printStackTrace();
    }
    return false;
}

This code using Java reflection:

final Class<?> appContextClass = Class.forName("sun.awt.AppContext");
final Object appContext = appContextClass.getMethod("getAppContext").invoke(null);
appContextClass.getMethod("put", Object.class, Object.class).invoke(appContext, javax.print.PrintServiceLookup.class.getDeclaredClasses()[0], null);

is equivalent than this:

sun.awt.AppContext.getAppContext().put(javax.print.PrintServiceLookup.class.getDeclaredClasses()[0], null);
-3

I'm not sure what you were expecting. Here's the output for my Windows Vista laptop running Java 7.

Number of servies found: 3
--> Available Printer 0: Win32 Printer : PamFax
--> Available Printer 1: Win32 Printer : Microsoft XPS Document Writer
--> Available Printer 2: Win32 Printer : CutePDF Writer
x
Number of servies found: 3
--> Available Printer 0: Win32 Printer : PamFax
--> Available Printer 1: Win32 Printer : Microsoft XPS Document Writer
--> Available Printer 2: Win32 Printer : CutePDF Writer

Printer services don't change every 5 minutes, even on a shared network.

Updated based on the comment:

Your Java application will have to have to be stopped and started to get the updated list of printers.

I guess I don't understand your environment. In the places I've worked, all of the printers have been defined on the network long before we ever wrote any code. Printer changes were rarely (i mean once a year) made.

Gilbert Le Blanc
  • 50,182
  • 6
  • 67
  • 111
  • Thanks for your answer! What I mean by refreshing, is that when I add a printer while my application is running, I want to be able to view all printers that exist in the settings (control panel for Windows, system settings for Mac, etc). Currently, Java does not show the newly added printer, only the existing printers. – Robin Trietsch Feb 11 '15 at 16:36
  • Thanks for the update. The application can be restarted, but I wanted to know why Java is unable to refresh the printers. If printer changes are made, I'll just restart the application. As Padrus mentions, Java makes a snapshot of the printers and cannot be refreshed once the application is running. – Robin Trietsch Feb 11 '15 at 17:09