From e395f186030f465260e05e89b1918ddfe47ca600 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Sun, 30 Apr 2023 00:42:46 -0400 Subject: [PATCH 01/13] StatusMonitor retooling --- src/qz/printer/status/StatusMonitor.java | 125 +++++++++++++++-------- src/qz/ws/PrintSocketClient.java | 11 +- src/qz/ws/SocketConnection.java | 20 +--- 3 files changed, 87 insertions(+), 69 deletions(-) diff --git a/src/qz/printer/status/StatusMonitor.java b/src/qz/printer/status/StatusMonitor.java index 0b16e9375..81423c2d1 100644 --- a/src/qz/printer/status/StatusMonitor.java +++ b/src/qz/printer/status/StatusMonitor.java @@ -8,6 +8,7 @@ import org.eclipse.jetty.util.MultiMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.eclipse.jetty.websocket.api.Session; import qz.printer.PrintServiceMatcher; import qz.printer.info.NativePrinterMap; import qz.utils.PrintingUtilities; @@ -27,8 +28,11 @@ public class StatusMonitor { public static final String ALL_PRINTERS = ""; private static Thread printerConnectionsThread; + private static Thread statusEventDispatchThread; private static final HashMap notificationThreadCollection = new HashMap<>(); + private static final HashMap statusSessions = new HashMap<>(); private static final MultiMap clientPrinterConnections = new MultiMap<>(); + private static final LinkedList statusQueue = new LinkedList(); public synchronized static boolean launchNotificationThreads() { ArrayList printerNameList = new ArrayList<>(); @@ -74,45 +78,25 @@ public synchronized static void closeNotificationThreads() { } } - public synchronized static boolean startListening(SocketConnection connection, JSONObject params) throws JSONException { + public synchronized static boolean isListening(SocketConnection connection) { + return statusSessions.containsKey(connection); + } + + public synchronized static boolean startListening(SocketConnection connection, Session session, JSONObject params) throws JSONException { + statusSessions.putIfAbsent(connection, new StatusSession(session)); JSONArray printerNames = params.getJSONArray("printerNames"); - boolean jobData = params.optBoolean("jobData", false); - int maxJobData = params.optInt("maxJobData", -1); - PrintingUtilities.Flavor dataFlavor = PrintingUtilities.Flavor.parse(params, PrintingUtilities.Flavor.PLAIN); if (printerNames.isNull(0)) { //listen to all printers - if (jobData) { - connection.getStatusListener().enableJobDataOnPrinter(ALL_PRINTERS, maxJobData, dataFlavor); - } - if (!clientPrinterConnections.containsKey(ALL_PRINTERS)) { - clientPrinterConnections.add(ALL_PRINTERS, connection); - } else if (!clientPrinterConnections.getValues(ALL_PRINTERS).contains(connection)) { - clientPrinterConnections.add(ALL_PRINTERS, connection); - } + addClientPrinterConnection(ALL_PRINTERS, connection, params); } else { // listen to specific printer(s) for (int i = 0; i < printerNames.length(); i++) { String printerName = printerNames.getString(i); - if (SystemUtilities.isMac()) { - // Since 2.0: Mac printers use descriptions as printer names; Find CUPS ID by Description - printerName = NativePrinterMap.getInstance().lookupPrinterId(printerName); - // Handle edge-case where printer was recently renamed/added - if (printerName == null) { - // Call PrintServiceLookup.lookupPrintServices again - PrintServiceMatcher.getNativePrinterList(true); - printerName = NativePrinterMap.getInstance().lookupPrinterId(printerNames.getString(i)); - } - } + if (SystemUtilities.isMac()) printerName = macNameFix(printerName); + if (printerName == null || printerName.equals("")) { throw new IllegalArgumentException(); } - if(jobData) { - connection.getStatusListener().enableJobDataOnPrinter(printerName, maxJobData, dataFlavor); - } - if (!clientPrinterConnections.containsKey(printerName)) { - clientPrinterConnections.add(printerName, connection); - } else if (!clientPrinterConnections.getValues(printerName).contains(connection)) { - clientPrinterConnections.add(printerName, connection); - } + addClientPrinterConnection(printerName, connection, params); } } @@ -124,6 +108,26 @@ public synchronized static boolean startListening(SocketConnection connection, J } } + public synchronized static void stopListening(SocketConnection connection) { + statusSessions.remove(connection); + closeListener(connection); + } + + private synchronized static void addClientPrinterConnection(String printerName, SocketConnection connection, JSONObject params) { + boolean jobData = params.optBoolean("jobData", false); + int maxJobData = params.optInt("maxJobData", -1); + PrintingUtilities.Flavor dataFlavor = PrintingUtilities.Flavor.parse(params, PrintingUtilities.Flavor.PLAIN); + + if (jobData) { + statusSessions.get(connection).enableJobDataOnPrinter(printerName, maxJobData, dataFlavor); + } + if (!clientPrinterConnections.containsKey(printerName)) { + clientPrinterConnections.add(printerName, connection); + } else if (!clientPrinterConnections.getValues(printerName).contains(connection)) { + clientPrinterConnections.add(printerName, connection); + } + } + public synchronized static void sendStatuses(SocketConnection connection) { boolean sendForAllPrinters = false; ArrayList statuses = isWindows() ? WmiPrinterStatusThread.getAllStatuses(): CupsUtils.getAllStatuses(); @@ -135,11 +139,11 @@ public synchronized static void sendStatuses(SocketConnection connection) { for (Status status : statuses) { if (sendForAllPrinters) { - connection.getStatusListener().statusChanged(status); + statusSessions.get(connection).statusChanged(status); } else { connections = clientPrinterConnections.get(status.getPrinter()); if ((connections != null) && connections.contains(connection)) { - connection.getStatusListener().statusChanged(status); + statusSessions.get(connection).statusChanged(status); } } } @@ -160,18 +164,55 @@ public synchronized static void closeListener(SocketConnection connection) { } } + private synchronized static void launchStatusEventDispatchThread() { + if (statusEventDispatchThread != null && statusEventDispatchThread.isAlive()) return; + statusEventDispatchThread = new Thread(() -> { + while (!Thread.currentThread().isInterrupted() && dispatchStatusEvent()) { + Thread.yield(); + } + if (Thread.currentThread().isInterrupted()) log.warn("statusEventDispatchThread Interrupted"); + }, "statusEventDispatchThread"); + statusEventDispatchThread.start(); + } + public synchronized static void statusChanged(Status[] statuses) { - HashSet connections = new HashSet<>(); for (Status status : statuses) { - if (clientPrinterConnections.containsKey(status.getPrinter())) { - connections.addAll(clientPrinterConnections.get(status.getPrinter())); - } - if (clientPrinterConnections.containsKey(ALL_PRINTERS)) { - connections.addAll(clientPrinterConnections.get(ALL_PRINTERS)); - } - for (SocketConnection connection : connections) { - connection.getStatusListener().statusChanged(status); - } + statusQueue.add(status); + } + if (!statusQueue.isEmpty()) { + launchStatusEventDispatchThread(); + } + } + + private synchronized static boolean dispatchStatusEvent() { + if (statusQueue.isEmpty()) { + statusEventDispatchThread = null; + return false; + } + Status status = statusQueue.removeFirst(); + + HashSet listeningConnections = new HashSet<>(); + if (clientPrinterConnections.containsKey(status.getPrinter())) { + listeningConnections.addAll(clientPrinterConnections.get(status.getPrinter())); + } + if (clientPrinterConnections.containsKey(ALL_PRINTERS)) { + listeningConnections.addAll(clientPrinterConnections.get(ALL_PRINTERS)); + } + for (SocketConnection connection : listeningConnections) { + statusSessions.get(connection).statusChanged(status); + } + return true; + } + + private static String macNameFix(String printerName) { + // Since 2.0: Mac printers use descriptions as printer names; Find CUPS ID by Description + String returnString = NativePrinterMap.getInstance().lookupPrinterId(printerName); + // Handle edge-case where printer was recently renamed/added + if (returnString == null) { + // Call PrintServiceLookup.lookupPrintServices again + PrintServiceMatcher.getNativePrinterList(true); + returnString = NativePrinterMap.getInstance().lookupPrinterId(printerName); } + return returnString; } } diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index 4133f5cb5..c38afb47c 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -276,14 +276,11 @@ private void processMessage(Session session, JSONObject json, SocketConnection c sendResult(session, UID, PrintServiceMatcher.getPrintersJSON(true)); break; case PRINTERS_START_LISTENING: - if (!connection.hasStatusListener()) { - connection.startStatusListener(new StatusSession(session)); - } - StatusMonitor.startListening(connection, params); + StatusMonitor.startListening(connection, session, params); sendResult(session, UID, null); break; case PRINTERS_GET_STATUS: - if (connection.hasStatusListener()) { + if (StatusMonitor.isListening(connection)) { StatusMonitor.sendStatuses(connection); } else { sendError(session, UID, "No printer listeners started for this client."); @@ -291,9 +288,7 @@ private void processMessage(Session session, JSONObject json, SocketConnection c sendResult(session, UID, null); break; case PRINTERS_STOP_LISTENING: - if (connection.hasStatusListener()) { - connection.stopStatusListener(); - } + StatusMonitor.stopListening(connection); sendResult(session, UID, null); break; case PRINT: diff --git a/src/qz/ws/SocketConnection.java b/src/qz/ws/SocketConnection.java index dbadd164d..f85629057 100644 --- a/src/qz/ws/SocketConnection.java +++ b/src/qz/ws/SocketConnection.java @@ -89,24 +89,6 @@ public void stopDeviceListening() { deviceListener = null; } - public synchronized boolean hasStatusListener() { - return statusListener != null; - } - - public synchronized void startStatusListener(StatusSession listener) { - statusListener = listener; - } - - public synchronized void stopStatusListener() { - StatusMonitor.closeListener(this); - statusListener = null; - } - - public synchronized StatusSession getStatusListener() { - return statusListener; - } - - public void addFileListener(Path absolute, FileIO listener) { openFiles.put(absolute, listener); } @@ -169,7 +151,7 @@ public synchronized void disconnect() throws SerialPortException, DeviceExceptio removeAllFileListeners(); stopDeviceListening(); - stopStatusListener(); + StatusMonitor.stopListening(this); } } From 178a5c6872d4467d9ae60aab16520a991f211628 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Sun, 30 Apr 2023 02:40:20 -0400 Subject: [PATCH 02/13] rough comments. needs styling --- src/qz/printer/status/StatusMonitor.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/qz/printer/status/StatusMonitor.java b/src/qz/printer/status/StatusMonitor.java index 81423c2d1..35f5b10ec 100644 --- a/src/qz/printer/status/StatusMonitor.java +++ b/src/qz/printer/status/StatusMonitor.java @@ -165,9 +165,11 @@ public synchronized static void closeListener(SocketConnection connection) { } private synchronized static void launchStatusEventDispatchThread() { + // Null is our main test to see if the thread needs to restart. If the thread was suspended, it won't be null, so check to see if it is alive as well. if (statusEventDispatchThread != null && statusEventDispatchThread.isAlive()) return; statusEventDispatchThread = new Thread(() -> { while (!Thread.currentThread().isInterrupted() && dispatchStatusEvent()) { + // If we don't yield, this will constantly run dispatchStatusEvent and lock up the class, even though this thread isn't synchronized. Thread.yield(); } if (Thread.currentThread().isInterrupted()) log.warn("statusEventDispatchThread Interrupted"); @@ -177,15 +179,21 @@ private synchronized static void launchStatusEventDispatchThread() { public synchronized static void statusChanged(Status[] statuses) { for (Status status : statuses) { + // Add statuses to the queue, statusEventDispatchThread will resolve these one at a time until the queue is empty statusQueue.add(status); } if (!statusQueue.isEmpty()) { + // If statusEventDispatchThread isn't already running, launch it launchStatusEventDispatchThread(); } } + // This is the main body of the statusEventDispatchThread. + // Dispatch one status event to n clients connection, based on clientPrinterConnections + // Returns false when there are no more statuses in the queue private synchronized static boolean dispatchStatusEvent() { if (statusQueue.isEmpty()) { + // Returning false will kill statusEventDispatchThread, but we also want to null out the value while we are still in a synchronized method statusEventDispatchThread = null; return false; } @@ -193,9 +201,11 @@ private synchronized static boolean dispatchStatusEvent() { HashSet listeningConnections = new HashSet<>(); if (clientPrinterConnections.containsKey(status.getPrinter())) { + // Find every client that subscribed to this printer listeningConnections.addAll(clientPrinterConnections.get(status.getPrinter())); } if (clientPrinterConnections.containsKey(ALL_PRINTERS)) { + // And find every client that subscribed to all printers listeningConnections.addAll(clientPrinterConnections.get(ALL_PRINTERS)); } for (SocketConnection connection : listeningConnections) { From 343d52630821b85942a0dfdc27e3bd69ab301c77 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Mon, 1 May 2023 15:57:08 -0400 Subject: [PATCH 03/13] nativePrinter getname speedup --- src/qz/printer/info/NativePrinter.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/qz/printer/info/NativePrinter.java b/src/qz/printer/info/NativePrinter.java index 85b64f832..1fa61be0c 100644 --- a/src/qz/printer/info/NativePrinter.java +++ b/src/qz/printer/info/NativePrinter.java @@ -10,9 +10,11 @@ import javax.print.attribute.standard.PrinterResolution; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; public class NativePrinter { private static final Logger log = LogManager.getLogger(NativePrinter.class); + private static final long nameLifespan = TimeUnit.SECONDS.toNanos(10); /** * Simple object wrapper allowing lazy fetching of values * @param @@ -69,6 +71,8 @@ public boolean equals(Object o) { } private final String printerId; + private String name; + private long nameTimestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, set as min to guarantee first-run. private boolean outdated; private PrinterProperty description; private PrinterProperty printService; @@ -113,11 +117,18 @@ public PrinterProperty getDriver() { public String getName() { if (printService != null && printService.value() != null) { - return printService.value().getName(); + long timeStamp = System.nanoTime(); + // getName() reaches out to native and is expensive. This name is cached, and is refreshed if nameLifespan has elapsed. + if (nameTimestamp + nameLifespan <= timeStamp) { + name = printService.value().getName(); + nameTimestamp = timeStamp; + } + return name; } return null; } + public PrinterName getLegacyName() { if (printService != null && printService.value() != null) { return printService.value().getAttribute(PrinterName.class); From ba0b18d33ce5c3de92efb1cf2cf9acdb5c854ae9 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Tue, 6 Jun 2023 19:26:11 -0400 Subject: [PATCH 04/13] added defaultService cache --- src/qz/printer/PrintServiceMatcher.java | 25 +++++++++++++++++++++++-- src/qz/printer/info/NativePrinter.java | 6 ++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/qz/printer/PrintServiceMatcher.java b/src/qz/printer/PrintServiceMatcher.java index b474af2ae..48c02226a 100644 --- a/src/qz/printer/PrintServiceMatcher.java +++ b/src/qz/printer/PrintServiceMatcher.java @@ -27,9 +27,13 @@ import javax.print.attribute.standard.PrinterName; import javax.print.attribute.standard.PrinterResolution; import java.util.Locale; +import java.util.concurrent.TimeUnit; public class PrintServiceMatcher { private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class); + private static final long cachedDefaultServiceLifespan = TimeUnit.SECONDS.toNanos(10); //Set to 0 to disable, do not set to a negative number + private static PrintService cachedDefaultService; + private static long cachedDefaultServiceTimestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, set as min to guarantee first-run. public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) { NativePrinterMap printers = NativePrinterMap.getInstance(); @@ -48,7 +52,21 @@ public static NativePrinterMap getNativePrinterList() { } public static NativePrinter getDefaultPrinter() { - PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService(); + PrintService defaultService; + + //Todo: This is a temporary fix for slow lookupDefaultPrintService on linux + // An upstream fix is being pursued + if (SystemUtilities.isLinux()) { + long timeStamp = System.nanoTime(); + // lookupDefaultPrintService() is expensive on linux. The printService is cached, and is refreshed if the lifespan has elapsed + if (cachedDefaultServiceTimestamp + cachedDefaultServiceLifespan <= timeStamp) { + cachedDefaultService = PrintServiceLookup.lookupDefaultPrintService(); + cachedDefaultServiceTimestamp = timeStamp; + } + defaultService = cachedDefaultService; + } else { + defaultService = PrintServiceLookup.lookupDefaultPrintService(); + } if(defaultService == null) { return null; @@ -84,6 +102,8 @@ public static NativePrinter matchPrinter(String printerSearch, boolean silent) { if (!silent) { log.debug("Searching for PrintService matching {}", printerSearch); } + // Fix for https://github.com/qzind/tray/issues/931 + // This is more than an optimization, removal will lead to a regression NativePrinter defaultPrinter = getDefaultPrinter(); if (defaultPrinter != null && printerSearch.equals(defaultPrinter.getName())) { if (!silent) { log.debug("Matched default printer, skipping further search"); } @@ -149,7 +169,8 @@ public static NativePrinter matchPrinter(String printerSearch, boolean silent) { return use; } - public static NativePrinter matchPrinter(String printerSearch) { + public static NativePrinter + matchPrinter(String printerSearch) { return matchPrinter(printerSearch, false); } diff --git a/src/qz/printer/info/NativePrinter.java b/src/qz/printer/info/NativePrinter.java index 1fa61be0c..34718fdb0 100644 --- a/src/qz/printer/info/NativePrinter.java +++ b/src/qz/printer/info/NativePrinter.java @@ -14,7 +14,7 @@ public class NativePrinter { private static final Logger log = LogManager.getLogger(NativePrinter.class); - private static final long nameLifespan = TimeUnit.SECONDS.toNanos(10); + private static final long nameLifespan = TimeUnit.SECONDS.toNanos(10); //Set to 0 to disable, do not set to a negative number /** * Simple object wrapper allowing lazy fetching of values * @param @@ -117,8 +117,10 @@ public PrinterProperty getDriver() { public String getName() { if (printService != null && printService.value() != null) { + if (!SystemUtilities.isMac()) return printService.value().getName(); + long timeStamp = System.nanoTime(); - // getName() reaches out to native and is expensive. This name is cached, and is refreshed if nameLifespan has elapsed. + // getName() reaches out to native and is expensive on mac. This name is cached, and is refreshed if nameLifespan has elapsed if (nameTimestamp + nameLifespan <= timeStamp) { name = printService.value().getName(); nameTimestamp = timeStamp; From 1526592e3465a21720e3168011fb8c6435eb744f Mon Sep 17 00:00:00 2001 From: Vzor- Date: Wed, 7 Jun 2023 14:10:39 -0400 Subject: [PATCH 05/13] mac defaultService cache --- src/qz/printer/PrintServiceMatcher.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qz/printer/PrintServiceMatcher.java b/src/qz/printer/PrintServiceMatcher.java index 48c02226a..a51f3d7c2 100644 --- a/src/qz/printer/PrintServiceMatcher.java +++ b/src/qz/printer/PrintServiceMatcher.java @@ -54,11 +54,11 @@ public static NativePrinterMap getNativePrinterList() { public static NativePrinter getDefaultPrinter() { PrintService defaultService; - //Todo: This is a temporary fix for slow lookupDefaultPrintService on linux + //Todo: This is a temporary fix for slow lookupDefaultPrintService on mac/linux // An upstream fix is being pursued - if (SystemUtilities.isLinux()) { + if (!SystemUtilities.isWindows()) { long timeStamp = System.nanoTime(); - // lookupDefaultPrintService() is expensive on linux. The printService is cached, and is refreshed if the lifespan has elapsed + // lookupDefaultPrintService() is expensive on mac/linux. The printService is cached, and is refreshed if the lifespan has elapsed if (cachedDefaultServiceTimestamp + cachedDefaultServiceLifespan <= timeStamp) { cachedDefaultService = PrintServiceLookup.lookupDefaultPrintService(); cachedDefaultServiceTimestamp = timeStamp; From f7398ffc912cc74e28898436ab7603a31a5eb53f Mon Sep 17 00:00:00 2001 From: Vzor- Date: Thu, 8 Jun 2023 03:57:46 -0400 Subject: [PATCH 06/13] added generic caching object --- src/qz/common/CachedObject.java | 97 +++++++++++++++++++++++++ src/qz/printer/PrintServiceMatcher.java | 26 ++----- src/qz/printer/info/NativePrinter.java | 24 +++--- 3 files changed, 115 insertions(+), 32 deletions(-) create mode 100644 src/qz/common/CachedObject.java diff --git a/src/qz/common/CachedObject.java b/src/qz/common/CachedObject.java new file mode 100644 index 000000000..b16d3e316 --- /dev/null +++ b/src/qz/common/CachedObject.java @@ -0,0 +1,97 @@ +package qz.common; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +/** + * A generic class that encapsulates an object for caching. The cached object + * will be refreshed automatically when accessed after its lifespan has expired. + * + * @param The type of object to be cached. + */ +public class CachedObject { + public static final long DEFAULT_LIFESPAN = 5000; + T cachedObject; + Supplier supplier; + private long timestamp; + private long lifespan; + + /** + * Creates a new CachedObject with a default lifespan of 5000 milliseconds + * + * @param supplier The function to pull new values from + */ + public CachedObject(Supplier supplier) { + this(supplier, DEFAULT_LIFESPAN); + } + + /** + * Creates a new CachedObject + * + * @param supplier The function to pull new values from + * @param lifespan The lifespan of the cached object in milliseconds + */ + public CachedObject(Supplier supplier, long lifespan) { + this.supplier = supplier; + setLifespan(lifespan); + timestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, MIN_VALUE guarantees a first-run. + } + + /** + * Registers a new supplier for the CachedObject + * + * @param supplier The function to pull new values from + */ + public void registerSupplier(Supplier supplier) { + this.supplier = supplier; + } + + /** + * Sets the lifespan of the cached object + * + * @param milliseconds The lifespan of the cached object in milliseconds + */ + public void setLifespan(long milliseconds) { + // we never want lifespan to be negative, since timestamp initializes as long.MIN_VALUE + lifespan = Math.max(0, milliseconds); + } + + /** + * Retrieves the cached object. + * If the cached object's lifespan is expired, it gets refreshed before being returned. + * + * @return The cached object + */ + public T get() { + return get(false); + } + + /** + * Retrieves the cached object. + * If the cached object's lifespan is expired or forceRefresh is true, it gets refreshed before being returned. + * + * @param forceRefresh If true, the cached object will be refreshed before being returned regardless of its lifespan + * @return The cached object + */ + public T get(boolean forceRefresh) { + long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); + // check lifespan + if (forceRefresh || (timestamp + lifespan <= now)) { + timestamp = now; + cachedObject = supplier.get(); + } + return cachedObject; + } + + // Test + public static void main(String ... args) throws InterruptedException { + final AtomicInteger testInt = new AtomicInteger(0); + + CachedObject cachedString = new CachedObject<>(testInt::incrementAndGet); + for(int i = 0; i < 100; i++) { + Thread.sleep(1500); + System.out.println(cachedString.get()); + } + } +} diff --git a/src/qz/printer/PrintServiceMatcher.java b/src/qz/printer/PrintServiceMatcher.java index a51f3d7c2..b1bd251e5 100644 --- a/src/qz/printer/PrintServiceMatcher.java +++ b/src/qz/printer/PrintServiceMatcher.java @@ -15,6 +15,7 @@ import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import qz.common.CachedObject; import qz.printer.info.NativePrinter; import qz.printer.info.NativePrinterMap; import qz.utils.SystemUtilities; @@ -27,13 +28,14 @@ import javax.print.attribute.standard.PrinterName; import javax.print.attribute.standard.PrinterResolution; import java.util.Locale; -import java.util.concurrent.TimeUnit; public class PrintServiceMatcher { private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class); - private static final long cachedDefaultServiceLifespan = TimeUnit.SECONDS.toNanos(10); //Set to 0 to disable, do not set to a negative number - private static PrintService cachedDefaultService; - private static long cachedDefaultServiceTimestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, set as min to guarantee first-run. + + // lookupDefaultPrintService() reaches out to native and is expensive on mac/linux. This printService is cached, and refreshed if lifespan has elapsed + // todo Fix before merging: mention upsteam bug report + private static final long lifespan = SystemUtilities.isWindows() ? 0 : CachedObject.DEFAULT_LIFESPAN; + private static CachedObject cachedDefault = new CachedObject<>(PrintServiceLookup::lookupDefaultPrintService, lifespan); public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) { NativePrinterMap printers = NativePrinterMap.getInstance(); @@ -52,21 +54,7 @@ public static NativePrinterMap getNativePrinterList() { } public static NativePrinter getDefaultPrinter() { - PrintService defaultService; - - //Todo: This is a temporary fix for slow lookupDefaultPrintService on mac/linux - // An upstream fix is being pursued - if (!SystemUtilities.isWindows()) { - long timeStamp = System.nanoTime(); - // lookupDefaultPrintService() is expensive on mac/linux. The printService is cached, and is refreshed if the lifespan has elapsed - if (cachedDefaultServiceTimestamp + cachedDefaultServiceLifespan <= timeStamp) { - cachedDefaultService = PrintServiceLookup.lookupDefaultPrintService(); - cachedDefaultServiceTimestamp = timeStamp; - } - defaultService = cachedDefaultService; - } else { - defaultService = PrintServiceLookup.lookupDefaultPrintService(); - } + PrintService defaultService = cachedDefault.get(); if(defaultService == null) { return null; diff --git a/src/qz/printer/info/NativePrinter.java b/src/qz/printer/info/NativePrinter.java index 34718fdb0..dabbe3fd0 100644 --- a/src/qz/printer/info/NativePrinter.java +++ b/src/qz/printer/info/NativePrinter.java @@ -2,6 +2,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import qz.common.CachedObject; import qz.utils.SystemUtilities; import javax.print.PrintService; @@ -10,11 +11,9 @@ import javax.print.attribute.standard.PrinterResolution; import java.util.Arrays; import java.util.List; -import java.util.concurrent.TimeUnit; public class NativePrinter { private static final Logger log = LogManager.getLogger(NativePrinter.class); - private static final long nameLifespan = TimeUnit.SECONDS.toNanos(10); //Set to 0 to disable, do not set to a negative number /** * Simple object wrapper allowing lazy fetching of values * @param @@ -71,8 +70,11 @@ public boolean equals(Object o) { } private final String printerId; - private String name; - private long nameTimestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, set as min to guarantee first-run. + + // getName() reaches out to native and is expensive on mac. This name is cached, and is refreshed if lifespan has elapsed + // todo Fix before merging: mention upsteam bug report + private final long lifespan = SystemUtilities.isMac() ? CachedObject.DEFAULT_LIFESPAN : 0; + private CachedObject cachedName = new CachedObject<>(this::getNameNative, lifespan); private boolean outdated; private PrinterProperty description; private PrinterProperty printService; @@ -116,16 +118,12 @@ public PrinterProperty getDriver() { } public String getName() { - if (printService != null && printService.value() != null) { - if (!SystemUtilities.isMac()) return printService.value().getName(); + return cachedName.get(); + } - long timeStamp = System.nanoTime(); - // getName() reaches out to native and is expensive on mac. This name is cached, and is refreshed if nameLifespan has elapsed - if (nameTimestamp + nameLifespan <= timeStamp) { - name = printService.value().getName(); - nameTimestamp = timeStamp; - } - return name; + private String getNameNative() { + if (printService != null && printService.value() != null) { + return printService.value().getName(); } return null; } From f5a22223e64bbe32b51e0a212205facf6f535e99 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Thu, 8 Jun 2023 15:26:48 -0400 Subject: [PATCH 07/13] cleanup --- src/qz/printer/info/NativePrinter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qz/printer/info/NativePrinter.java b/src/qz/printer/info/NativePrinter.java index dabbe3fd0..80192b2e2 100644 --- a/src/qz/printer/info/NativePrinter.java +++ b/src/qz/printer/info/NativePrinter.java @@ -128,7 +128,6 @@ private String getNameNative() { return null; } - public PrinterName getLegacyName() { if (printService != null && printService.value() != null) { return printService.value().getAttribute(PrinterName.class); From 3dde730f8a8e54778c541d7386e725df369d8dab Mon Sep 17 00:00:00 2001 From: Vzor- Date: Wed, 13 Sep 2023 12:22:35 -0400 Subject: [PATCH 08/13] resolving pr comments --- src/qz/common/CachedObject.java | 10 +++++----- src/qz/printer/PrintServiceMatcher.java | 2 +- src/qz/printer/info/NativePrinter.java | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/qz/common/CachedObject.java b/src/qz/common/CachedObject.java index b16d3e316..b23432fed 100644 --- a/src/qz/common/CachedObject.java +++ b/src/qz/common/CachedObject.java @@ -11,8 +11,8 @@ * @param The type of object to be cached. */ public class CachedObject { - public static final long DEFAULT_LIFESPAN = 5000; - T cachedObject; + public static final long DEFAULT_LIFESPAN = 5000; // in milliseconds + T lastObject; Supplier supplier; private long timestamp; private long lifespan; @@ -59,7 +59,7 @@ public void setLifespan(long milliseconds) { /** * Retrieves the cached object. - * If the cached object's lifespan is expired, it gets refreshed before being returned. + * If the cached object's lifespan has expired, it gets refreshed before being returned. * * @return The cached object */ @@ -79,9 +79,9 @@ public T get(boolean forceRefresh) { // check lifespan if (forceRefresh || (timestamp + lifespan <= now)) { timestamp = now; - cachedObject = supplier.get(); + lastObject = supplier.get(); } - return cachedObject; + return lastObject; } // Test diff --git a/src/qz/printer/PrintServiceMatcher.java b/src/qz/printer/PrintServiceMatcher.java index b1bd251e5..83a3e6df1 100644 --- a/src/qz/printer/PrintServiceMatcher.java +++ b/src/qz/printer/PrintServiceMatcher.java @@ -32,7 +32,7 @@ public class PrintServiceMatcher { private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class); - // lookupDefaultPrintService() reaches out to native and is expensive on mac/linux. This printService is cached, and refreshed if lifespan has elapsed + // PrintServiceLookup.lookupDefaultPrintService() is slow, use a cache instead per JDK-XXXXXXX // todo Fix before merging: mention upsteam bug report private static final long lifespan = SystemUtilities.isWindows() ? 0 : CachedObject.DEFAULT_LIFESPAN; private static CachedObject cachedDefault = new CachedObject<>(PrintServiceLookup::lookupDefaultPrintService, lifespan); diff --git a/src/qz/printer/info/NativePrinter.java b/src/qz/printer/info/NativePrinter.java index 80192b2e2..80ac7d016 100644 --- a/src/qz/printer/info/NativePrinter.java +++ b/src/qz/printer/info/NativePrinter.java @@ -71,8 +71,8 @@ public boolean equals(Object o) { private final String printerId; - // getName() reaches out to native and is expensive on mac. This name is cached, and is refreshed if lifespan has elapsed - // todo Fix before merging: mention upsteam bug report + // PrintService.getName() is slow, use a cache instead per JDK-XXXXXXX + // TODO: Remove this comment when upstream bug report is filed private final long lifespan = SystemUtilities.isMac() ? CachedObject.DEFAULT_LIFESPAN : 0; private CachedObject cachedName = new CachedObject<>(this::getNameNative, lifespan); private boolean outdated; From a9dc5fab9607cde69885baec2379cec5b34a3379 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Wed, 13 Sep 2023 12:34:51 -0400 Subject: [PATCH 09/13] resolving pr comments 2 --- src/qz/common/CachedObject.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/qz/common/CachedObject.java b/src/qz/common/CachedObject.java index b23432fed..f9a5b3804 100644 --- a/src/qz/common/CachedObject.java +++ b/src/qz/common/CachedObject.java @@ -53,8 +53,7 @@ public void registerSupplier(Supplier supplier) { * @param milliseconds The lifespan of the cached object in milliseconds */ public void setLifespan(long milliseconds) { - // we never want lifespan to be negative, since timestamp initializes as long.MIN_VALUE - lifespan = Math.max(0, milliseconds); + lifespan = Math.max(0, milliseconds); // prevent overflow } /** From 6a9408da88308180e8d3e5fa8d6e8f5c6f74294b Mon Sep 17 00:00:00 2001 From: Vzor- Date: Sun, 24 Sep 2023 23:19:25 -0400 Subject: [PATCH 10/13] Removal of threading --- src/qz/printer/status/StatusMonitor.java | 137 +++++++---------------- src/qz/ws/PrintSocketClient.java | 13 ++- src/qz/ws/SocketConnection.java | 22 +++- 3 files changed, 72 insertions(+), 100 deletions(-) diff --git a/src/qz/printer/status/StatusMonitor.java b/src/qz/printer/status/StatusMonitor.java index 35f5b10ec..1f5512bcb 100644 --- a/src/qz/printer/status/StatusMonitor.java +++ b/src/qz/printer/status/StatusMonitor.java @@ -8,7 +8,6 @@ import org.eclipse.jetty.util.MultiMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.eclipse.jetty.websocket.api.Session; import qz.printer.PrintServiceMatcher; import qz.printer.info.NativePrinterMap; import qz.utils.PrintingUtilities; @@ -28,11 +27,8 @@ public class StatusMonitor { public static final String ALL_PRINTERS = ""; private static Thread printerConnectionsThread; - private static Thread statusEventDispatchThread; private static final HashMap notificationThreadCollection = new HashMap<>(); - private static final HashMap statusSessions = new HashMap<>(); private static final MultiMap clientPrinterConnections = new MultiMap<>(); - private static final LinkedList statusQueue = new LinkedList(); public synchronized static boolean launchNotificationThreads() { ArrayList printerNameList = new ArrayList<>(); @@ -78,25 +74,45 @@ public synchronized static void closeNotificationThreads() { } } - public synchronized static boolean isListening(SocketConnection connection) { - return statusSessions.containsKey(connection); - } - - public synchronized static boolean startListening(SocketConnection connection, Session session, JSONObject params) throws JSONException { - statusSessions.putIfAbsent(connection, new StatusSession(session)); + public synchronized static boolean startListening(SocketConnection connection, JSONObject params) throws JSONException { JSONArray printerNames = params.getJSONArray("printerNames"); + boolean jobData = params.optBoolean("jobData", false); + int maxJobData = params.optInt("maxJobData", -1); + PrintingUtilities.Flavor dataFlavor = PrintingUtilities.Flavor.parse(params, PrintingUtilities.Flavor.PLAIN); if (printerNames.isNull(0)) { //listen to all printers - addClientPrinterConnection(ALL_PRINTERS, connection, params); + if (jobData) { + connection.getStatusListener().enableJobDataOnPrinter(ALL_PRINTERS, maxJobData, dataFlavor); + } + if (!clientPrinterConnections.containsKey(ALL_PRINTERS)) { + clientPrinterConnections.add(ALL_PRINTERS, connection); + } else if (!clientPrinterConnections.getValues(ALL_PRINTERS).contains(connection)) { + clientPrinterConnections.add(ALL_PRINTERS, connection); + } } else { // listen to specific printer(s) for (int i = 0; i < printerNames.length(); i++) { String printerName = printerNames.getString(i); - if (SystemUtilities.isMac()) printerName = macNameFix(printerName); - + if (SystemUtilities.isMac()) { + // Since 2.0: Mac printers use descriptions as printer names; Find CUPS ID by Description + printerName = NativePrinterMap.getInstance().lookupPrinterId(printerName); + // Handle edge-case where printer was recently renamed/added + if (printerName == null) { + // Call PrintServiceLookup.lookupPrintServices again + PrintServiceMatcher.getNativePrinterList(true); + printerName = NativePrinterMap.getInstance().lookupPrinterId(printerNames.getString(i)); + } + } if (printerName == null || printerName.equals("")) { throw new IllegalArgumentException(); } - addClientPrinterConnection(printerName, connection, params); + if(jobData) { + connection.getStatusListener().enableJobDataOnPrinter(printerName, maxJobData, dataFlavor); + } + if (!clientPrinterConnections.containsKey(printerName)) { + clientPrinterConnections.add(printerName, connection); + } else if (!clientPrinterConnections.getValues(printerName).contains(connection)) { + clientPrinterConnections.add(printerName, connection); + } } } @@ -108,26 +124,6 @@ public synchronized static boolean startListening(SocketConnection connection, S } } - public synchronized static void stopListening(SocketConnection connection) { - statusSessions.remove(connection); - closeListener(connection); - } - - private synchronized static void addClientPrinterConnection(String printerName, SocketConnection connection, JSONObject params) { - boolean jobData = params.optBoolean("jobData", false); - int maxJobData = params.optInt("maxJobData", -1); - PrintingUtilities.Flavor dataFlavor = PrintingUtilities.Flavor.parse(params, PrintingUtilities.Flavor.PLAIN); - - if (jobData) { - statusSessions.get(connection).enableJobDataOnPrinter(printerName, maxJobData, dataFlavor); - } - if (!clientPrinterConnections.containsKey(printerName)) { - clientPrinterConnections.add(printerName, connection); - } else if (!clientPrinterConnections.getValues(printerName).contains(connection)) { - clientPrinterConnections.add(printerName, connection); - } - } - public synchronized static void sendStatuses(SocketConnection connection) { boolean sendForAllPrinters = false; ArrayList statuses = isWindows() ? WmiPrinterStatusThread.getAllStatuses(): CupsUtils.getAllStatuses(); @@ -139,11 +135,11 @@ public synchronized static void sendStatuses(SocketConnection connection) { for (Status status : statuses) { if (sendForAllPrinters) { - statusSessions.get(connection).statusChanged(status); + connection.getStatusListener().statusChanged(status); } else { connections = clientPrinterConnections.get(status.getPrinter()); if ((connections != null) && connections.contains(connection)) { - statusSessions.get(connection).statusChanged(status); + connection.getStatusListener().statusChanged(status); } } } @@ -164,65 +160,18 @@ public synchronized static void closeListener(SocketConnection connection) { } } - private synchronized static void launchStatusEventDispatchThread() { - // Null is our main test to see if the thread needs to restart. If the thread was suspended, it won't be null, so check to see if it is alive as well. - if (statusEventDispatchThread != null && statusEventDispatchThread.isAlive()) return; - statusEventDispatchThread = new Thread(() -> { - while (!Thread.currentThread().isInterrupted() && dispatchStatusEvent()) { - // If we don't yield, this will constantly run dispatchStatusEvent and lock up the class, even though this thread isn't synchronized. - Thread.yield(); - } - if (Thread.currentThread().isInterrupted()) log.warn("statusEventDispatchThread Interrupted"); - }, "statusEventDispatchThread"); - statusEventDispatchThread.start(); - } - public synchronized static void statusChanged(Status[] statuses) { + HashSet connections = new HashSet<>(); for (Status status : statuses) { - // Add statuses to the queue, statusEventDispatchThread will resolve these one at a time until the queue is empty - statusQueue.add(status); - } - if (!statusQueue.isEmpty()) { - // If statusEventDispatchThread isn't already running, launch it - launchStatusEventDispatchThread(); - } - } - - // This is the main body of the statusEventDispatchThread. - // Dispatch one status event to n clients connection, based on clientPrinterConnections - // Returns false when there are no more statuses in the queue - private synchronized static boolean dispatchStatusEvent() { - if (statusQueue.isEmpty()) { - // Returning false will kill statusEventDispatchThread, but we also want to null out the value while we are still in a synchronized method - statusEventDispatchThread = null; - return false; - } - Status status = statusQueue.removeFirst(); - - HashSet listeningConnections = new HashSet<>(); - if (clientPrinterConnections.containsKey(status.getPrinter())) { - // Find every client that subscribed to this printer - listeningConnections.addAll(clientPrinterConnections.get(status.getPrinter())); - } - if (clientPrinterConnections.containsKey(ALL_PRINTERS)) { - // And find every client that subscribed to all printers - listeningConnections.addAll(clientPrinterConnections.get(ALL_PRINTERS)); - } - for (SocketConnection connection : listeningConnections) { - statusSessions.get(connection).statusChanged(status); - } - return true; - } - - private static String macNameFix(String printerName) { - // Since 2.0: Mac printers use descriptions as printer names; Find CUPS ID by Description - String returnString = NativePrinterMap.getInstance().lookupPrinterId(printerName); - // Handle edge-case where printer was recently renamed/added - if (returnString == null) { - // Call PrintServiceLookup.lookupPrintServices again - PrintServiceMatcher.getNativePrinterList(true); - returnString = NativePrinterMap.getInstance().lookupPrinterId(printerName); + if (clientPrinterConnections.containsKey(status.getPrinter())) { + connections.addAll(clientPrinterConnections.get(status.getPrinter())); + } + if (clientPrinterConnections.containsKey(ALL_PRINTERS)) { + connections.addAll(clientPrinterConnections.get(ALL_PRINTERS)); + } + for (SocketConnection connection : connections) { + connection.getStatusListener().statusChanged(status); + } } - return returnString; } -} +} \ No newline at end of file diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index c38afb47c..9f9b996cd 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -276,11 +276,14 @@ private void processMessage(Session session, JSONObject json, SocketConnection c sendResult(session, UID, PrintServiceMatcher.getPrintersJSON(true)); break; case PRINTERS_START_LISTENING: - StatusMonitor.startListening(connection, session, params); + if (!connection.hasStatusListener()) { + connection.startStatusListener(new StatusSession(session)); + } + StatusMonitor.startListening(connection, params); sendResult(session, UID, null); break; case PRINTERS_GET_STATUS: - if (StatusMonitor.isListening(connection)) { + if (connection.hasStatusListener()) { StatusMonitor.sendStatuses(connection); } else { sendError(session, UID, "No printer listeners started for this client."); @@ -288,7 +291,9 @@ private void processMessage(Session session, JSONObject json, SocketConnection c sendResult(session, UID, null); break; case PRINTERS_STOP_LISTENING: - StatusMonitor.stopListening(connection); + if (connection.hasStatusListener()) { + connection.stopStatusListener(); + } sendResult(session, UID, null); break; case PRINT: @@ -775,4 +780,4 @@ private static synchronized void send(Session session, JSONObject reply) throws } } -} +} \ No newline at end of file diff --git a/src/qz/ws/SocketConnection.java b/src/qz/ws/SocketConnection.java index f85629057..a778e7ae2 100644 --- a/src/qz/ws/SocketConnection.java +++ b/src/qz/ws/SocketConnection.java @@ -89,6 +89,24 @@ public void stopDeviceListening() { deviceListener = null; } + public synchronized boolean hasStatusListener() { + return statusListener != null; + } + + public synchronized void startStatusListener(StatusSession listener) { + statusListener = listener; + } + + public synchronized void stopStatusListener() { + StatusMonitor.closeListener(this); + statusListener = null; + } + + public synchronized StatusSession getStatusListener() { + return statusListener; + } + + public void addFileListener(Path absolute, FileIO listener) { openFiles.put(absolute, listener); } @@ -151,7 +169,7 @@ public synchronized void disconnect() throws SerialPortException, DeviceExceptio removeAllFileListeners(); stopDeviceListening(); - StatusMonitor.stopListening(this); + stopStatusListener(); } -} +} \ No newline at end of file From cb0ae08c2e22595d78371950400d1ccf3f73b39d Mon Sep 17 00:00:00 2001 From: Vzor- Date: Sun, 24 Sep 2023 23:20:36 -0400 Subject: [PATCH 11/13] cleanup --- src/qz/printer/status/StatusMonitor.java | 2 +- src/qz/ws/PrintSocketClient.java | 2 +- src/qz/ws/SocketConnection.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qz/printer/status/StatusMonitor.java b/src/qz/printer/status/StatusMonitor.java index 1f5512bcb..0b16e9375 100644 --- a/src/qz/printer/status/StatusMonitor.java +++ b/src/qz/printer/status/StatusMonitor.java @@ -174,4 +174,4 @@ public synchronized static void statusChanged(Status[] statuses) { } } } -} \ No newline at end of file +} diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index 9f9b996cd..4133f5cb5 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -780,4 +780,4 @@ private static synchronized void send(Session session, JSONObject reply) throws } } -} \ No newline at end of file +} diff --git a/src/qz/ws/SocketConnection.java b/src/qz/ws/SocketConnection.java index a778e7ae2..dbadd164d 100644 --- a/src/qz/ws/SocketConnection.java +++ b/src/qz/ws/SocketConnection.java @@ -172,4 +172,4 @@ public synchronized void disconnect() throws SerialPortException, DeviceExceptio stopStatusListener(); } -} \ No newline at end of file +} From 04c5e803873444a2c5d8db45904e27873d4ab704 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Tue, 26 Sep 2023 06:02:01 -0400 Subject: [PATCH 12/13] Service wrapper --- src/qz/printer/PrintServiceMatcher.java | 15 +-- src/qz/printer/info/CachedPrintService.java | 132 ++++++++++++++++++++ src/qz/printer/info/NativePrinter.java | 16 +-- src/qz/printer/info/NativePrinterMap.java | 1 + 4 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 src/qz/printer/info/CachedPrintService.java diff --git a/src/qz/printer/PrintServiceMatcher.java b/src/qz/printer/PrintServiceMatcher.java index 83a3e6df1..1667f72bf 100644 --- a/src/qz/printer/PrintServiceMatcher.java +++ b/src/qz/printer/PrintServiceMatcher.java @@ -15,13 +15,12 @@ import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; -import qz.common.CachedObject; +import qz.printer.info.CachedPrintService; import qz.printer.info.NativePrinter; import qz.printer.info.NativePrinterMap; import qz.utils.SystemUtilities; import javax.print.PrintService; -import javax.print.PrintServiceLookup; import javax.print.attribute.ResolutionSyntax; import javax.print.attribute.standard.Media; import javax.print.attribute.standard.MediaTray; @@ -32,14 +31,9 @@ public class PrintServiceMatcher { private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class); - // PrintServiceLookup.lookupDefaultPrintService() is slow, use a cache instead per JDK-XXXXXXX - // todo Fix before merging: mention upsteam bug report - private static final long lifespan = SystemUtilities.isWindows() ? 0 : CachedObject.DEFAULT_LIFESPAN; - private static CachedObject cachedDefault = new CachedObject<>(PrintServiceLookup::lookupDefaultPrintService, lifespan); - public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) { NativePrinterMap printers = NativePrinterMap.getInstance(); - printers.putAll(PrintServiceLookup.lookupPrintServices(null, null)); + printers.putAll(CachedPrintService.lookupPrintServices()); if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); } if (!silent) { log.debug("Found {} printers", printers.size()); } return printers; @@ -54,7 +48,7 @@ public static NativePrinterMap getNativePrinterList() { } public static NativePrinter getDefaultPrinter() { - PrintService defaultService = cachedDefault.get(); + PrintService defaultService = CachedPrintService.lookupDefaultPrintService(); if(defaultService == null) { return null; @@ -62,6 +56,7 @@ public static NativePrinter getDefaultPrinter() { NativePrinterMap printers = NativePrinterMap.getInstance(); if (!printers.contains(defaultService)) { + //todo: is this working correctly? it seems to set the printers list = to [1] {defaultPrinter} printers.putAll(defaultService); } @@ -165,7 +160,7 @@ public static NativePrinter matchPrinter(String printerSearch, boolean silent) { public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException { JSONArray list = new JSONArray(); - PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService(); + PrintService defaultService = CachedPrintService.lookupDefaultPrintService(); boolean mediaTrayCrawled = false; diff --git a/src/qz/printer/info/CachedPrintService.java b/src/qz/printer/info/CachedPrintService.java new file mode 100644 index 000000000..b9cc719b2 --- /dev/null +++ b/src/qz/printer/info/CachedPrintService.java @@ -0,0 +1,132 @@ +package qz.printer.info; + +import qz.common.CachedObject; +import qz.utils.SystemUtilities; + +import javax.print.*; +import javax.print.attribute.Attribute; +import javax.print.attribute.AttributeSet; +import javax.print.attribute.PrintServiceAttribute; +import javax.print.attribute.PrintServiceAttributeSet; +import javax.print.event.PrintServiceAttributeListener; +import java.util.HashMap; +import java.util.function.Supplier; + +public class CachedPrintService implements PrintService { + public PrintService innerPrintService; + // PrintService.getName() is slow, use a cache instead per JDK-7001133 + // TODO: Remove this comment when upstream bug report is filed + private static final long lifespan = SystemUtilities.isMac() ? CachedObject.DEFAULT_LIFESPAN : 0; + private static final CachedObject cachedDefault = new CachedObject<>(CachedPrintService::innerLookupDefaultPrintService, lifespan); + private static final CachedObject cachedPrintServices = new CachedObject<>(CachedPrintService::innerLookupPrintServices, lifespan); + private final CachedObject cachedName; + private final CachedObject cachedAttributeSet; + private final HashMap, CachedObject> cachedAttributes = new HashMap<>(); + + public static PrintService lookupDefaultPrintService() { + return cachedDefault.get(); + } + + public static PrintService[] lookupPrintServices() { + return cachedPrintServices.get(); + } + + private static PrintService innerLookupDefaultPrintService() { + return new CachedPrintService(PrintServiceLookup.lookupDefaultPrintService()); + } + + private static PrintService[] innerLookupPrintServices() { + PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null); + for (int i = 0; i < printServices.length; i++) { + printServices[i] = new CachedPrintService(printServices[i]); + } + return printServices; + } + + public CachedPrintService(PrintService printService) { + innerPrintService = printService; + cachedName = new CachedObject<>(innerPrintService::getName, lifespan); + cachedAttributeSet = new CachedObject<>(innerPrintService::getAttributes, lifespan); + } + + @Override + public String getName() { + return cachedName.get(); + } + + @Override + public DocPrintJob createPrintJob() { + return innerPrintService.createPrintJob(); + } + + @Override + public void addPrintServiceAttributeListener(PrintServiceAttributeListener listener) { + innerPrintService.addPrintServiceAttributeListener(listener); + } + + @Override + public void removePrintServiceAttributeListener(PrintServiceAttributeListener listener) { + innerPrintService.removePrintServiceAttributeListener(listener); + } + + @Override + public PrintServiceAttributeSet getAttributes() { + return cachedAttributeSet.get(); + } + + @Override + public T getAttribute(Class category) { + if (lifespan <= 0) return innerPrintService.getAttribute(category); + if (!cachedAttributes.containsKey(category)) { + Supplier supplier = () -> innerPrintService.getAttribute(category); + CachedObject cachedObject = new CachedObject<>(supplier, lifespan); + cachedAttributes.put(category, cachedObject); + } + return (T)cachedAttributes.get(category).get(); + } + + @Override + public DocFlavor[] getSupportedDocFlavors() { + return innerPrintService.getSupportedDocFlavors(); + } + + @Override + public boolean isDocFlavorSupported(DocFlavor flavor) { + return innerPrintService.isDocFlavorSupported(flavor); + } + + @Override + public Class[] getSupportedAttributeCategories() { + return innerPrintService.getSupportedAttributeCategories(); + } + + @Override + public boolean isAttributeCategorySupported(Class category) { + return innerPrintService.isAttributeCategorySupported(category); + } + + @Override + public Object getDefaultAttributeValue(Class category) { + return innerPrintService.getDefaultAttributeValue(category); + } + + @Override + public Object getSupportedAttributeValues(Class category, DocFlavor flavor, AttributeSet attributes) { + return innerPrintService.getSupportedAttributeValues(category, flavor, attributes); + } + + @Override + public boolean isAttributeValueSupported(Attribute attrval, DocFlavor flavor, AttributeSet attributes) { + return innerPrintService.isAttributeValueSupported(attrval, flavor, attributes); + } + + @Override + public AttributeSet getUnsupportedAttributes(DocFlavor flavor, AttributeSet attributes) { + return innerPrintService.getUnsupportedAttributes(flavor, attributes); + } + + @Override + public ServiceUIFactory getServiceUIFactory() { + return innerPrintService.getServiceUIFactory(); + } +} diff --git a/src/qz/printer/info/NativePrinter.java b/src/qz/printer/info/NativePrinter.java index 80ac7d016..ab1966a1e 100644 --- a/src/qz/printer/info/NativePrinter.java +++ b/src/qz/printer/info/NativePrinter.java @@ -2,7 +2,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import qz.common.CachedObject; import qz.utils.SystemUtilities; import javax.print.PrintService; @@ -58,9 +57,10 @@ public boolean isNull() { @Override public boolean equals(Object o) { - // PrintService.equals(...) is very slow in CUPS; use the pointer - if (SystemUtilities.isUnix() && value instanceof PrintService) { - return o == value; + // PrintService.equals(...) is very slow in CUPS; use the pointer instead per JDK-7001133 + if (SystemUtilities.isUnix() && value instanceof PrintService ps) { + //todo this needs to be more than a name check. maybe use attribute set + return ps.getName().equals(getName()); } if (value != null) { return value.equals(o); @@ -71,10 +71,6 @@ public boolean equals(Object o) { private final String printerId; - // PrintService.getName() is slow, use a cache instead per JDK-XXXXXXX - // TODO: Remove this comment when upstream bug report is filed - private final long lifespan = SystemUtilities.isMac() ? CachedObject.DEFAULT_LIFESPAN : 0; - private CachedObject cachedName = new CachedObject<>(this::getNameNative, lifespan); private boolean outdated; private PrinterProperty description; private PrinterProperty printService; @@ -118,10 +114,6 @@ public PrinterProperty getDriver() { } public String getName() { - return cachedName.get(); - } - - private String getNameNative() { if (printService != null && printService.value() != null) { return printService.value().getName(); } diff --git a/src/qz/printer/info/NativePrinterMap.java b/src/qz/printer/info/NativePrinterMap.java index 15fb0db59..6c4089320 100644 --- a/src/qz/printer/info/NativePrinterMap.java +++ b/src/qz/printer/info/NativePrinterMap.java @@ -65,6 +65,7 @@ public ArrayList findMissing(PrintService[] services) { public boolean contains(PrintService service) { for (NativePrinter printer : values()) { + //todo, this is a bad if, just return if (printer.getPrintService().equals(service)) { return true; } From 7bc4679c14242a34ffc7d78b94f017a06747c800 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Tue, 26 Sep 2023 18:14:50 -0400 Subject: [PATCH 13/13] slight rework --- src/qz/printer/PrintServiceMatcher.java | 22 ++++++++++++++++----- src/qz/printer/info/CachedPrintService.java | 8 +++----- src/qz/printer/info/NativePrinter.java | 4 ++-- src/qz/printer/info/NativePrinterMap.java | 1 - 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/qz/printer/PrintServiceMatcher.java b/src/qz/printer/PrintServiceMatcher.java index 1667f72bf..4375f072b 100644 --- a/src/qz/printer/PrintServiceMatcher.java +++ b/src/qz/printer/PrintServiceMatcher.java @@ -21,6 +21,7 @@ import qz.utils.SystemUtilities; import javax.print.PrintService; +import javax.print.PrintServiceLookup; import javax.print.attribute.ResolutionSyntax; import javax.print.attribute.standard.Media; import javax.print.attribute.standard.MediaTray; @@ -30,15 +31,27 @@ public class PrintServiceMatcher { private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class); + //todo: include jdk version test for caching when JDK-7001133 is resolved + private static final boolean useCache = SystemUtilities.isMac(); // PrintService is slow, use a cache instead per JDK-7001133 public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) { NativePrinterMap printers = NativePrinterMap.getInstance(); - printers.putAll(CachedPrintService.lookupPrintServices()); + printers.putAll(lookupPrintServices()); if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); } if (!silent) { log.debug("Found {} printers", printers.size()); } return printers; } + private static PrintService[] lookupPrintServices() { + if (useCache) return CachedPrintService.lookupPrintServices(); + return PrintServiceLookup.lookupPrintServices(null, null); + } + + private static PrintService lookupDefaultPrintService() { + if (useCache) return CachedPrintService.lookupDefaultPrintService(); + return PrintServiceLookup.lookupDefaultPrintService(); + } + public static NativePrinterMap getNativePrinterList(boolean silent) { return getNativePrinterList(silent, false); } @@ -48,7 +61,7 @@ public static NativePrinterMap getNativePrinterList() { } public static NativePrinter getDefaultPrinter() { - PrintService defaultService = CachedPrintService.lookupDefaultPrintService(); + PrintService defaultService = lookupDefaultPrintService(); if(defaultService == null) { return null; @@ -152,15 +165,14 @@ public static NativePrinter matchPrinter(String printerSearch, boolean silent) { return use; } - public static NativePrinter - matchPrinter(String printerSearch) { + public static NativePrinter matchPrinter(String printerSearch) { return matchPrinter(printerSearch, false); } public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException { JSONArray list = new JSONArray(); - PrintService defaultService = CachedPrintService.lookupDefaultPrintService(); + PrintService defaultService = lookupDefaultPrintService(); boolean mediaTrayCrawled = false; diff --git a/src/qz/printer/info/CachedPrintService.java b/src/qz/printer/info/CachedPrintService.java index b9cc719b2..445435710 100644 --- a/src/qz/printer/info/CachedPrintService.java +++ b/src/qz/printer/info/CachedPrintService.java @@ -1,7 +1,6 @@ package qz.printer.info; import qz.common.CachedObject; -import qz.utils.SystemUtilities; import javax.print.*; import javax.print.attribute.Attribute; @@ -16,12 +15,12 @@ public class CachedPrintService implements PrintService { public PrintService innerPrintService; // PrintService.getName() is slow, use a cache instead per JDK-7001133 // TODO: Remove this comment when upstream bug report is filed - private static final long lifespan = SystemUtilities.isMac() ? CachedObject.DEFAULT_LIFESPAN : 0; + private static final long lifespan = CachedObject.DEFAULT_LIFESPAN; private static final CachedObject cachedDefault = new CachedObject<>(CachedPrintService::innerLookupDefaultPrintService, lifespan); private static final CachedObject cachedPrintServices = new CachedObject<>(CachedPrintService::innerLookupPrintServices, lifespan); private final CachedObject cachedName; private final CachedObject cachedAttributeSet; - private final HashMap, CachedObject> cachedAttributes = new HashMap<>(); + private final HashMap, CachedObject> cachedAttributes = new HashMap<>(); public static PrintService lookupDefaultPrintService() { return cachedDefault.get(); @@ -76,13 +75,12 @@ public PrintServiceAttributeSet getAttributes() { @Override public T getAttribute(Class category) { - if (lifespan <= 0) return innerPrintService.getAttribute(category); if (!cachedAttributes.containsKey(category)) { Supplier supplier = () -> innerPrintService.getAttribute(category); CachedObject cachedObject = new CachedObject<>(supplier, lifespan); cachedAttributes.put(category, cachedObject); } - return (T)cachedAttributes.get(category).get(); + return category.cast(cachedAttributes.get(category).get()); } @Override diff --git a/src/qz/printer/info/NativePrinter.java b/src/qz/printer/info/NativePrinter.java index ab1966a1e..e1d03f356 100644 --- a/src/qz/printer/info/NativePrinter.java +++ b/src/qz/printer/info/NativePrinter.java @@ -58,9 +58,9 @@ public boolean isNull() { @Override public boolean equals(Object o) { // PrintService.equals(...) is very slow in CUPS; use the pointer instead per JDK-7001133 - if (SystemUtilities.isUnix() && value instanceof PrintService ps) { + if (SystemUtilities.isUnix() && value instanceof PrintService) { //todo this needs to be more than a name check. maybe use attribute set - return ps.getName().equals(getName()); + return ((PrintService)value).getName().equals(getName()); } if (value != null) { return value.equals(o); diff --git a/src/qz/printer/info/NativePrinterMap.java b/src/qz/printer/info/NativePrinterMap.java index 6c4089320..15fb0db59 100644 --- a/src/qz/printer/info/NativePrinterMap.java +++ b/src/qz/printer/info/NativePrinterMap.java @@ -65,7 +65,6 @@ public ArrayList findMissing(PrintService[] services) { public boolean contains(PrintService service) { for (NativePrinter printer : values()) { - //todo, this is a bad if, just return if (printer.getPrintService().equals(service)) { return true; }