Skip to content
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

8263311: Watch registry changes for remote printers update instead of polling #2915

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2000, 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
@@ -26,8 +26,6 @@
package sun.print;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

import javax.print.DocFlavor;
import javax.print.MultiDocPrintService;
@@ -50,31 +48,7 @@ public class PrintServiceLookupProvider extends PrintServiceLookup {
private String[] printers; /* excludes the default printer */
private PrintService[] printServices; /* includes the default printer */

private static final int DEFAULT_REFRESH_TIME = 240; // 4 minutes
private static final int MINIMUM_REFRESH_TIME = 120; // 2 minutes
private static final boolean pollServices;
private static final int refreshTime;

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"));
pollServices = !("false".equalsIgnoreCase(pollStr));

/* 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"));
refreshTime = (refreshTimeStr != null)
? getRefreshTime(refreshTimeStr)
: DEFAULT_REFRESH_TIME;

java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
@@ -84,17 +58,6 @@ public Void run() {
});
}

private static int getRefreshTime(final String refreshTimeStr) {
try {
int minRefreshTime = Integer.parseInt(refreshTimeStr);
return (minRefreshTime < MINIMUM_REFRESH_TIME)
? MINIMUM_REFRESH_TIME
: minRefreshTime;
} catch (NumberFormatException e) {
return DEFAULT_REFRESH_TIME;
}
}

/* The singleton win32 print lookup service.
* Code that is aware of this field and wants to use it must first
* see if its null, and if so instantiate it by calling a method such as
@@ -126,13 +89,11 @@ public PrintServiceLookupProvider() {
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();
}
// 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! */
}

@@ -356,70 +317,15 @@ public void run() {
}
}

/* Windows provides *PrinterChangeNotification* functions that provides
information about printer status changes of the local printers but not
network printers.
Alternatively, Windows provides a way through 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 through 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 chosen 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 Comparator<String>, Runnable {

RemotePrinterChangeListener() {
}

@Override
public int compare(String o1, String o2) {
return ((o1 == null)
? ((o2 == null) ? 0 : 1)
: ((o2 == null) ? -1 : o1.compareTo(o2)));
}

private final class RemotePrinterChangeListener implements Runnable {
@Override
public void run() {
// Init the list of remote printers
String[] prevRemotePrinters = getRemotePrintersNames();
if (prevRemotePrinters != null) {
Arrays.sort(prevRemotePrinters, this);
}

while (true) {
try {
Thread.sleep(refreshTime * 1000);
} catch (InterruptedException e) {
break;
}

String[] currentRemotePrinters = getRemotePrintersNames();
if (currentRemotePrinters != null) {
Arrays.sort(currentRemotePrinters, this);
}
if (!Arrays.equals(prevRemotePrinters, currentRemotePrinters)) {
// The list of remote printers got updated,
// so update the cached list printers which
// includes both local and network printers
refreshServices();

// store the current data for next comparison
prevRemotePrinters = currentRemotePrinters;
}
}
notifyRemotePrinterChange(); // busy loop in the native code
}
}

private native String getDefaultPrinterName();
private native String[] getAllPrinterNames();
private native void notifyLocalPrinterChange();
private native String[] getRemotePrintersNames();
private native void notifyRemotePrinterChange();
}
@@ -191,12 +191,6 @@ Java_sun_print_PrintServiceLookupProvider_getAllPrinterNames(JNIEnv *env,
return getPrinterNames(env, PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS);
}

JNIEXPORT jobjectArray JNICALL
Java_sun_print_PrintServiceLookupProvider_getRemotePrintersNames(JNIEnv *env,
jobject peer)
{
return getPrinterNames(env, PRINTER_ENUM_CONNECTIONS);
}

JNIEXPORT void JNICALL
Java_sun_print_PrintServiceLookupProvider_notifyLocalPrinterChange(JNIEnv *env,
@@ -239,6 +233,37 @@ Java_sun_print_PrintServiceLookupProvider_notifyLocalPrinterChange(JNIEnv *env,
::ClosePrinter(hPrinter);
}

JNIEXPORT void JNICALL
Java_sun_print_PrintServiceLookupProvider_notifyRemotePrinterChange(JNIEnv *env,
jobject peer)
{
jclass cls = env->GetObjectClass(peer);
CHECK_NULL(cls);
jmethodID refresh = env->GetMethodID(cls, "refreshServices", "()V");
CHECK_NULL(refresh);

HKEY hKey;
if (ERROR_SUCCESS != RegOpenKeyEx(HKEY_CURRENT_USER,
_T("Printers\\Connections"),
0, KEY_NOTIFY, &hKey)) {
return;
}

BOOL keepMonitoring;
do {
keepMonitoring =
ERROR_SUCCESS == RegNotifyChangeKeyValue(hKey, TRUE,
REG_NOTIFY_CHANGE_NAME,
NULL,
FALSE);
Copy link
Member Author

@aivanov-jdk aivanov-jdk Mar 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RegNotifyChangeKeyValue notifies the caller about changes to the attributes or contents of a specified registry key.

hKey: A handle to HKEY_CURRENT_USER\Printers\Connections key which is opened above.
bWatchSubtree = TRUE: The function reports changes in the specified key and its subkeys.
dwNotifyFilter = REG_NOTIFY_CHANGE_NAME: Notify the caller if a subkey is added or deleted.
hEvent = NULL: If fAsynchronous is FALSE, hEvent is ignored.
fAsynchronous = FALSE: The function does not return until a change has occurred.

When a new remote printer is added, a new key is created under HKCU\Printers\Connections; when an existing remote printer is removed, the key below Connections is removed; no values are added or removed in Connections key, thus REG_NOTIFY_CHANGE_LAST_SET filter is not needed.

Copy link
Contributor

@prsadhuk prsadhuk Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only about addition/removal? What about printer name change? Shouldn't we get notified in that case as trying to print on printer with old name will not find the printer!!
If yes, in that regard I guess REG_NOTIFY_CHANGE_LAST_SET is the one to go for as it states
"Notify the caller of changes to a value of the key. This can include adding or deleting a value, or changing an existing value. "

Copy link
Member Author

@aivanov-jdk aivanov-jdk Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only about addition/removal? What about printer name change?

You cannot change the name of a remote printer.

Opening Printer Properties dialog from the printer context menu on the local host and editing its name changes the name of the printer on the remote host which shares it. Windows warns, “This is a shared printer. If you rename a shared printer, existing connections to this printer from other computers will break and will have to be created again.”

Nothing has been updated on the local system after renaming the printer. As the warning said, the printer does not work any more because it refers to a printer that does not exist.

To fix this, one has to add a new remote printer which refers to the new name of the printer on the remote host.

Shouldn't we get notified in that case as trying to print on printer with old name will not find the printer!!

I'm afraid there's nothing Java can do to mitigate renaming the printer.

If yes, in that regard I guess REG_NOTIFY_CHANGE_LAST_SET is the one to go for as it states
"Notify the caller of changes to a value of the key. This can include adding or deleting a value, or changing an existing value. "

Since no values are created, removed, or changed when a remote printer is added or removed under Connections key, adding REG_NOTIFY_CHANGE_LAST_SET to dwNotifyFilter is not required. There are some values stored in the printer key but Java does not use them directly; if any value is changed there, getting notifications about such an event only creates noise.

So, as far as Java is concerned, getting notifications about new keys being created or removed under Connections is enough. This is what REG_NOTIFY_CHANGE_NAME filter does.

Copy link
Contributor

@prsadhuk prsadhuk Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can understand that as a user, you cannot /shouldn't change the name of a remote network printer but a network admin can change the name, so shouldn't we get notification on that name change when this method gets called after the name change? or would it be notified as a new printer in that case? Then what will happen to the old stale printer in the registry?

Copy link
Member Author

@aivanov-jdk aivanov-jdk Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't. Try it yourself. The printer connection is per-user, after all it's stored under HKCU.

Windows displays the name of remote printers as “Generic / Text Only on 192.168.1.18”: the name of the printer and the remote host.

If you open the Printer Properties dialog of a remote printer and edit its name, you change the name of the printer on the remote host which shares it. It changes absolutely nothing on the local system.

shouldn't we get notification on that name change when this method gets called after the name change?

Yes, we should. But we cannot.

For Windows, nothing has changed on the local system, it's the remote system that has been changed.

or would it be notified as a new printer in that case?

No, it wouldn't because nothing has changed on the local system.

Then what will happen to the old stale printer in the registry?

Nothing, again. It just stays there but Windows reports an error if you try to open its Printer Properties, Windows reports an error if you try to print to it. I tried printing with Java and Notepad: the behaviour is the same. The difference is that Notepad displays the error message whereas Java throws an exception (it depends on the Java app, I guess).

The behaviour aligns to the one seen when the printer is unreachable because of, let's say, network issues.

Copy link
Contributor

@prsadhuk prsadhuk Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK.

if (keepMonitoring) {
aivanov-jdk marked this conversation as resolved.
Show resolved Hide resolved
env->CallVoidMethod(peer, refresh);
}
} while (keepMonitoring && !env->ExceptionCheck());

RegCloseKey(hKey);
}


JNIEXPORT jfloatArray JNICALL
Java_sun_print_Win32PrintService_getMediaPrintableArea(JNIEnv *env,
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 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
@@ -23,10 +23,10 @@

/*
* @test
* @bug 8153732 8212202 8221263 8221412 8222108
* @bug 8153732 8212202 8221263 8221412 8222108 8263311
* @requires (os.family == "Windows")
* @summary Windows remote printer changes do not reflect in lookupPrintServices()
* @run main/manual/othervm -Dsun.java2d.print.minRefreshTime=120 RemotePrinterStatusRefresh
* @run main/manual RemotePrinterStatusRefresh
*/

import java.awt.BorderLayout;
@@ -63,12 +63,7 @@

public class RemotePrinterStatusRefresh extends WindowAdapter {

private static final long DEFAULT_REFRESH_TIME = 240L;
private static final long MINIMAL_REFRESH_TIME = 120L;

private static final long refreshTime = getRefreshTime();

private static final long TIMEOUT = refreshTime * 4 + 60;
private static final long TIMEOUT = 15L * 60;


private static final CountDownLatch latch = new CountDownLatch(1);
@@ -86,7 +81,6 @@ public class RemotePrinterStatusRefresh extends WindowAdapter {
private final ServiceItemListModel beforeList;
private final ServiceItemListModel afterList;

private JTextField nextRefresh;
private JTextField timeLeft;

private final Timer timer;
@@ -184,22 +178,18 @@ public Component getListCellRendererComponent(JList<?> list,
+ "configured printers.\n"
+ "Step 1: Add or Remove a network printer using "
+ "Windows Control Panel.\n"
+ "Step 2: Wait for 2\u20134 minutes after adding or removing.\n"
+ " \"Next printer refresh in\" gives you a "
+ "rough estimation on when update will happen.\n"
+ "Step 3: Click Refresh."
+ "Step 2: Click Refresh."
+ "\"After\" list is populated with updated list "
+ "of printers.\n"
+ "Step 4: Compare the list of printers in \"Before\" and "
+ "Step 3: Compare the list of printers in \"Before\" and "
+ "\"After\" lists.\n"
+ " Added printers are highlighted with "
+ "green color, removed ones \u2014 with "
+ "red color.\n"
+ "Step 5: Click Pass if the list of printers is correctly "
+ "Step 4: Click Pass if the list of printers is correctly "
+ "updated.\n"
+ "Step 6: If the list is not updated, wait for another "
+ "2\u20134 minutes, and then click Refresh again.\n"
+ "Step 7: If the list does not update, click Fail.\n"
+ "Step 5: If the list is not updated, click Refresh again.\n"
+ "Step 6: If the list does not update, click Fail.\n"
+ "\n"
+ "You have to click Refresh to enable Pass and Fail buttons. "
+ "If no button is pressed,\n"
@@ -216,18 +206,6 @@ public static void main(String[] args) throws Exception {
}
}

private static long getRefreshTime() {
String refreshTime =
System.getProperty("sun.java2d.print.minRefreshTime",
Long.toString(DEFAULT_REFRESH_TIME));
try {
long value = Long.parseLong(refreshTime);
return value < MINIMAL_REFRESH_TIME ? MINIMAL_REFRESH_TIME : value;
} catch (NumberFormatException e) {
return DEFAULT_REFRESH_TIME;
}
}

private static void createUI() {
test = new RemotePrinterStatusRefresh();
}
@@ -278,31 +256,6 @@ private JPanel createInfoPanel() {
javaVersion.setEditable(false);
javaLabel.setLabelFor(javaVersion);

JLabel refreshTimeLabel = new JLabel("Refresh interval:");
long minutes = refreshTime / 60;
long seconds = refreshTime % 60;
String interval = String.format("%1$d seconds%2$s",
refreshTime,
minutes > 0
? String.format(" (%1$d %2$s%3$s)",
minutes,
minutes > 1 ? "minutes" : "minute",
seconds > 0
? String.format(" %1$d %2$s",
seconds,
seconds > 1 ? "seconds" : "second")
: "")
: ""
);
JTextField refreshInterval = new JTextField(interval);
refreshInterval.setEditable(false);
refreshTimeLabel.setLabelFor(refreshInterval);

JLabel nextRefreshLabel = new JLabel("Next printer refresh in:");
nextRefresh = new JTextField();
nextRefresh.setEditable(false);
nextRefreshLabel.setLabelFor(nextRefresh);

JLabel timeoutLabel = new JLabel("Time left:");
timeLeft = new JTextField();
timeLeft.setEditable(false);
@@ -317,14 +270,10 @@ private JPanel createInfoPanel() {
layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(javaLabel)
.addComponent(refreshTimeLabel)
.addComponent(nextRefreshLabel)
.addComponent(timeoutLabel)
)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING, true)
.addComponent(javaVersion)
.addComponent(refreshInterval)
.addComponent(nextRefresh)
.addComponent(timeLeft)
)
);
@@ -334,12 +283,6 @@ private JPanel createInfoPanel() {
.addComponent(javaLabel)
.addComponent(javaVersion)
)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(refreshTimeLabel)
.addComponent(refreshInterval))
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(nextRefreshLabel)
.addComponent(nextRefresh))
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(timeoutLabel)
.addComponent(timeLeft))
@@ -493,7 +436,6 @@ private void updateTimeLeft(ActionEvent e) {
disposeUI();
}
timeLeft.setText(formatTime(left));
nextRefresh.setText(formatTime(refreshTime - (elapsed % refreshTime)));
}

private static String formatTime(final long seconds) {