8153732: Windows remote printer changes do not reflect in lookupPrintServices()
authorsveerabhadra
Mon, 25 Jun 2018 14:32:46 +0530
changeset 50838 732a3b600098
parent 50837 86897f8a6598
child 50839 8e326bd343bb
child 50840 1db5917dfe1c
8153732: Windows remote printer changes do not reflect in lookupPrintServices() Reviewed-by: prr, psadhukhan
src/java.desktop/windows/classes/sun/print/PrintServiceLookupProvider.java
src/java.desktop/windows/native/libawt/windows/WPrinterJob.cpp
test/jdk/java/awt/print/RemotePrinterStatusRefresh/RemotePrinterStatusRefresh.java
--- a/src/java.desktop/windows/classes/sun/print/PrintServiceLookupProvider.java	Mon Jun 25 12:50:25 2018 +0530
+++ b/src/java.desktop/windows/classes/sun/print/PrintServiceLookupProvider.java	Mon Jun 25 14:32:46 2018 +0530
@@ -53,8 +53,42 @@
     private PrintService defaultPrintService;
     private String[] printers; /* excludes the default printer */
     private PrintService[] printServices; /* includes the default printer */
+    private static boolean pollServices = true;
+    private static final int DEFAULT_MINREFRESH = 240;  // 4 minutes
+    private static int minRefreshTime = DEFAULT_MINREFRESH;
 
     static {
+        /* The system property "sun.java2d.print.polling"
+         * can be used to force the printing code to poll or not poll
+         * for PrintServices.
+         */
+        String pollStr = java.security.AccessController.doPrivileged(
+            new sun.security.action.GetPropertyAction("sun.java2d.print.polling"));
+
+        if (pollStr != null) {
+            if (pollStr.equalsIgnoreCase("false")) {
+                pollServices = false;
+            }
+        }
+
+        /* The system property "sun.java2d.print.minRefreshTime"
+         * can be used to specify minimum refresh time (in seconds)
+         * for polling PrintServices.  The default is 240.
+         */
+        String refreshTimeStr = java.security.AccessController.doPrivileged(
+            new sun.security.action.GetPropertyAction(
+                "sun.java2d.print.minRefreshTime"));
+
+        if (refreshTimeStr != null) {
+            try {
+                minRefreshTime = (Integer.valueOf(refreshTimeStr)).intValue();
+            } catch (NumberFormatException e) {
+            }
+            if (minRefreshTime < DEFAULT_MINREFRESH) {
+                minRefreshTime = DEFAULT_MINREFRESH;
+            }
+        }
+
         java.security.AccessController.doPrivileged(
             new java.security.PrivilegedAction<Void>() {
                 public Void run() {
@@ -96,11 +130,19 @@
             if (osName != null && osName.startsWith("Windows 98")) {
                 return;
             }
-            // start the printer listener thread
+            // start the local printer listener thread
             Thread thr = new Thread(null, new PrinterChangeListener(),
                                     "PrinterListener", 0, false);
             thr.setDaemon(true);
             thr.start();
+
+            if (pollServices) {
+                // start the remote printer listener thread
+                Thread remThr = new Thread(null, new RemotePrinterChangeListener(),
+                                        "RemotePrinterListener", 0, false);
+                remThr.setDaemon(true);
+                remThr.start();
+            }
         } /* else condition ought to never happen! */
     }
 
@@ -316,7 +358,6 @@
         }
         return defaultPrintService;
     }
-
     class PrinterChangeListener implements Runnable {
         long chgObj;
         PrinterChangeListener() {
@@ -343,9 +384,74 @@
         }
     }
 
+    /* Windows provides *PrinterChangeNotification* functions that provides
+       information about printer status changes of the local printers but not
+       network printers.
+       Alternatively, Windows provides a way thro' which one can get the
+       network printer status changes by using WMI, RegistryKeyChange combination,
+       which is a slightly complex mechanism.
+       The Windows WMI offers an async and sync method to read thro' registry
+       via the WQL query. The async method is considered dangerous as it leaves
+       open a channel until we close it. But the async method has the advantage of
+       being notified of a change in registry by calling callback without polling for it.
+       The sync method uses the polling mechanism to notify.
+       RegistryValueChange cannot be used in combination with WMI to get registry
+       value change notification because of an error that may be generated because the
+       scope of the query would be too big to handle(at times).
+       Hence an alternative mechanism is choosen via the EnumPrinters by polling for the
+       count of printer status changes(add\remove) and based on it update the printers
+       list.
+    */
+    class RemotePrinterChangeListener implements Runnable {
+        private String[] prevRemotePrinters;
+
+        RemotePrinterChangeListener() {
+            prevRemotePrinters = getRemotePrintersNames();
+        }
+
+        boolean doCompare(String[] str1, String[] str2) {
+            if (str1.length != str2.length) {
+                return true;
+            } else {
+                for (int i = 0;i < str1.length;i++) {
+                    for (int j = 0;j < str2.length;j++) {
+                        if (!str1[i].equals(str2[j])) {
+                            return true;
+                        }
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        @Override
+        public void run() {
+            while (true) {
+                String[] currentRemotePrinters = getRemotePrintersNames();
+                if (doCompare(prevRemotePrinters, currentRemotePrinters)) {
+
+                    // updated the printers data
+                    // printers list now contains both local and network printer data
+                    refreshServices();
+
+                    // store the current data for next comparison
+                    prevRemotePrinters = currentRemotePrinters;
+                }
+
+                try {
+                    Thread.sleep(minRefreshTime * 1000);
+                } catch (InterruptedException e) {
+                    break;
+                }
+            }
+        }
+    }
+
     private native String getDefaultPrinterName();
     private native String[] getAllPrinterNames();
     private native long notifyFirstPrinterChange(String printer);
     private native void notifyClosePrinterChange(long chgObj);
     private native int notifyPrinterChange(long chgObj);
+    private native String[] getRemotePrintersNames();
 }
--- a/src/java.desktop/windows/native/libawt/windows/WPrinterJob.cpp	Mon Jun 25 12:50:25 2018 +0530
+++ b/src/java.desktop/windows/native/libawt/windows/WPrinterJob.cpp	Mon Jun 25 14:32:46 2018 +0530
@@ -232,6 +232,76 @@
     }
 }
 
+JNIEXPORT jobjectArray JNICALL
+Java_sun_print_PrintServiceLookupProvider_getRemotePrintersNames(JNIEnv *env,
+                                                           jobject peer)
+{
+    TRY;
+
+    int remotePrintersCount = 0;
+    DWORD cbNeeded = 0;
+    DWORD cReturned = 0;
+    LPBYTE pPrinterEnum = NULL;
+    LPBYTE pNetworkPrinterLoc = NULL;
+
+    jstring utf_str;
+    jclass clazz = env->FindClass("java/lang/String");
+    if (clazz == NULL) {
+        return NULL;
+    }
+    jobjectArray nameArray;
+
+    try {
+        ::EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS,
+                       NULL, 4, NULL, 0, &cbNeeded, &cReturned);
+        pPrinterEnum = new BYTE[cbNeeded];
+        pNetworkPrinterLoc = new BYTE[cbNeeded/sizeof(PRINTER_INFO_4)];
+        ::EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS,
+                       NULL, 4, pPrinterEnum, cbNeeded, &cbNeeded,
+                       &cReturned);
+
+        if (cReturned > 0) {
+            for (DWORD i = 0; i < cReturned; i++) {
+                PRINTER_INFO_4 *info4 = (PRINTER_INFO_4 *) (pPrinterEnum + i * sizeof(PRINTER_INFO_4));
+
+                // Store the network printers indexes
+                if (info4->Attributes & PRINTER_ATTRIBUTE_NETWORK) {
+                    pNetworkPrinterLoc[remotePrintersCount++] = i;
+                }
+            }
+
+            // Allocate space only for the network type printers
+            nameArray = env->NewObjectArray(remotePrintersCount, clazz, NULL);
+            if (nameArray == NULL) {
+                throw std::bad_alloc();
+            }
+        } else {
+            nameArray = NULL;
+        }
+
+        // Loop thro' network printers list only
+        for (int i = 0; i < remotePrintersCount; i++) {
+            PRINTER_INFO_4 *info4 = (PRINTER_INFO_4 *)
+                (pPrinterEnum + pNetworkPrinterLoc[i] * sizeof(PRINTER_INFO_4));
+            utf_str = JNU_NewStringPlatform(env, info4->pPrinterName);
+            if (utf_str == NULL) {
+                throw std::bad_alloc();
+            }
+            env->SetObjectArrayElement(nameArray, i, utf_str);
+            env->DeleteLocalRef(utf_str);
+        }
+    } catch (std::bad_alloc&) {
+        delete [] pPrinterEnum;
+        delete [] pNetworkPrinterLoc;
+        throw;
+    }
+
+    delete [] pPrinterEnum;
+    delete [] pNetworkPrinterLoc;
+    return nameArray;
+
+    CATCH_BAD_ALLOC_RET(NULL);
+}
 
 JNIEXPORT jfloatArray JNICALL
 Java_sun_print_Win32PrintService_getMediaPrintableArea(JNIEnv *env,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/awt/print/RemotePrinterStatusRefresh/RemotePrinterStatusRefresh.java	Mon Jun 25 14:32:46 2018 +0530
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2018, 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 8153732
+ * @requires (os.family == "Windows")
+ * @summary Windows remote printer changes do not reflect in lookupPrintServices()
+ * @ignore Requires a new network printer installation\removal
+ * @run main/manual RemotePrinterStatusRefresh
+ */
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.print.PageFormat;
+import java.awt.print.Paper;
+import java.awt.print.PrinterException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextArea;
+import javax.swing.SwingUtilities;
+import java.awt.print.PrinterJob;
+import javax.print.PrintService;
+
+public class RemotePrinterStatusRefresh
+{
+    private static TestUI test = null;
+    public static void main(String args[]) throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        // Test UI creation
+        test = new TestUI(latch);
+
+        SwingUtilities.invokeAndWait(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    test.createUI();
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        });
+
+        // RemotePrinterStatusRefresh creation
+        RemotePrinterStatusRefresh RemotePrinterStatusRefresh = new RemotePrinterStatusRefresh();
+        SwingUtilities.invokeAndWait(() -> {
+            collectPrintersList(test.resultsTextArea, true);
+        });
+
+        // 8 min = 480000 msec
+        if(waitForFlag(480000)) {
+            SwingUtilities.invokeAndWait(() -> {
+                collectPrintersList(test.resultsTextArea, false);
+            });
+        } else {
+            dispose();
+            throw new RuntimeException("No new network printer got added/removed!! Test timed out!!");
+        }
+
+        boolean status = latch.await(1, TimeUnit.MINUTES);
+        if (!status) {
+            dispose();
+            throw new RuntimeException("Test timed out.");
+        }
+
+        if (test.testResult == false) {
+            dispose();
+            throw new RuntimeException("Test Failed.");
+        }
+
+        dispose();
+    }
+
+    public static void dispose() throws Exception {
+        SwingUtilities.invokeAndWait(() -> {
+            test.disposeUI();
+        });
+    }
+
+    public static boolean waitForFlag (long maxTimeoutInMsec) throws Exception {
+        while(!test.isAdded && maxTimeoutInMsec > 0) {
+            maxTimeoutInMsec -= 100;
+            Thread.sleep(100);
+        }
+
+        if(maxTimeoutInMsec <= 0) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private static void collectPrintersList(JTextArea textArea, boolean before) {
+        if(before) {
+            System.out.println("List of printers(before): ");
+            textArea.setText("List of printers(before): \n");
+            for (PrintService printServiceBefore : PrinterJob.lookupPrintServices()) {
+                System.out.println(printServiceBefore);
+                textArea.append(printServiceBefore.toString());
+                textArea.append("\n");
+            }
+        } else {
+            textArea.append("\n");
+            System.out.println("List of printers(after): ");
+            textArea.append("List of printers(after): \n");
+            for (PrintService printServiceAfter : PrinterJob.lookupPrintServices()) {
+                System.out.println(printServiceAfter);
+                textArea.append(printServiceAfter.toString());
+                textArea.append("\n");
+            }
+        }
+    }
+}
+
+class TestUI {
+    private static JFrame mainFrame;
+    private static JPanel mainControlPanel;
+
+    private static JTextArea instructionTextArea;
+
+    private static JPanel resultButtonPanel;
+    private static JButton passButton;
+    private static JButton failButton;
+    private static JButton addedButton;
+
+    private static JPanel testPanel;
+    private static JButton testButton;
+    private static JLabel buttonPressCountLabel;
+
+    private static GridBagLayout layout;
+    private final CountDownLatch latch;
+    public boolean testResult = false;
+    public volatile Boolean isAdded = false;
+    public static JTextArea resultsTextArea;
+
+    public TestUI(CountDownLatch latch) throws Exception {
+        this.latch = latch;
+    }
+
+    public final void createUI() {
+        mainFrame = new JFrame("RemotePrinterStatusRefresh");
+        layout = new GridBagLayout();
+        mainControlPanel = new JPanel(layout);
+        resultButtonPanel = new JPanel(layout);
+        testPanel = new JPanel(layout);
+        GridBagConstraints gbc = new GridBagConstraints();
+
+        // Create Test instructions
+        String instructions
+                = "This test displays the current list of printers(before) attached to \n"
+                + "this computer in the results panel.\n\n"
+                + "Please follow the below steps for this manual test\n"
+                + "--------------------------------------------------------------------\n"
+                + "Step 1: Add/Remove a new network printer and Wait for 4 minutes after adding/removing\n"
+                + "Step 2: Then click on 'Printer Added/Removed' button\n"
+                + "Step 2: Once the new network printer is added/removed, see if it is \n"
+                + "        the same as displayed/not displayed in the results panel.\n"
+                + "Step 3: If displayed/not displayed, then click 'Pass' else click on 'Fail' button";
+
+        instructionTextArea = new JTextArea();
+        instructionTextArea.setText(instructions);
+        instructionTextArea.setEditable(false);
+        instructionTextArea.setBorder(BorderFactory.
+                createTitledBorder("Test Instructions"));
+
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        mainControlPanel.add(instructionTextArea, gbc);
+
+        gbc.gridx = 0;
+        gbc.gridy = 1;
+        testPanel.add(Box.createVerticalStrut(50));
+        mainControlPanel.add(testPanel);
+
+        addedButton = new JButton("Printer Added/Removed");
+        addedButton.setActionCommand("Added");
+        addedButton.addActionListener((ActionEvent e) -> {
+            System.out.println("Added Button pressed!");
+            isAdded = true;
+        });
+
+        // Create resultButtonPanel with Pass, Fail buttons
+        passButton = new JButton("Pass");
+        passButton.setActionCommand("Pass");
+        passButton.addActionListener((ActionEvent e) -> {
+            System.out.println("Pass Button pressed!");
+            testResult = true;
+            latch.countDown();
+            disposeUI();
+        });
+
+        failButton = new JButton("Fail");
+        failButton.setActionCommand("Fail");
+        failButton.addActionListener((ActionEvent e) -> {
+            System.out.println("Fail Button pressed!");
+            testResult = false;
+            latch.countDown();
+            disposeUI();
+        });
+
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        resultButtonPanel.add(addedButton, gbc);
+
+        gbc.gridx = 1;
+        gbc.gridy = 0;
+        resultButtonPanel.add(passButton, gbc);
+
+        gbc.gridx = 2;
+        gbc.gridy = 0;
+        resultButtonPanel.add(failButton, gbc);
+
+        resultsTextArea = new JTextArea();
+        resultsTextArea.setEditable(false);
+        resultsTextArea.setBorder(BorderFactory.
+                createTitledBorder("Results"));
+
+        gbc.gridx = 0;
+        gbc.gridy = 1;
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        mainControlPanel.add(resultsTextArea, gbc);
+
+        gbc.gridx = 0;
+        gbc.gridy = 2;
+        mainControlPanel.add(resultButtonPanel, gbc);
+
+        mainFrame.add(mainControlPanel);
+        mainFrame.pack();
+        mainFrame.setVisible(true);
+    }
+
+    public void disposeUI() {
+        mainFrame.dispose();
+    }
+}