diff --git a/js/qz-tray.js b/js/qz-tray.js index 19aa9aa9e..1514d8a09 100644 --- a/js/qz-tray.js +++ b/js/qz-tray.js @@ -1398,6 +1398,27 @@ var qz = (function() { return _qz.websocket.dataPromise('printers.startListening', params); }, + /** + * Clear the queue of a specified printer or printers. Does not delete retained jobs. + * + * @param {string|Object} [options] Name of printer to clear + * @param {string} [options.printerName] Name of printer to clear + * @param {number} [options.jobId] Cancel a job of a specific JobId instead of canceling all. Must include a printerName. + * + * @returns {Promise} + * @since 2.2.4 + * + * @memberof qz.printers + */ + clearQueue: function(options) { + if (typeof options !== 'object') { + options = { + printerName: options + }; + } + return _qz.websocket.dataPromise('printers.clearQueue', options); + }, + /** * Stop listening for printer status actions. * diff --git a/sample.html b/sample.html index 9beff71b1..4a08fa0d4 100755 --- a/sample.html +++ b/sample.html @@ -123,6 +123,7 @@

Printer

+ @@ -2082,6 +2083,9 @@

Options

qz.print(config, printData).catch(displayError); } + function clearQueue(printer) { + qz.printers.clearQueue(printer).catch(displayError); + } /// Pixel Printers /// function printHTML() { diff --git a/src/qz/communication/WinspoolEx.java b/src/qz/communication/WinspoolEx.java new file mode 100644 index 000000000..1966da083 --- /dev/null +++ b/src/qz/communication/WinspoolEx.java @@ -0,0 +1,28 @@ +package qz.communication; + +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.platform.win32.Winspool; +import com.sun.jna.win32.W32APIOptions; + +/** + * TODO: Remove when JNA 5.14.0+ is bundled + */ +@SuppressWarnings("unused") +public interface WinspoolEx extends Winspool { + WinspoolEx INSTANCE = Native.load("Winspool.drv", WinspoolEx.class, W32APIOptions.DEFAULT_OPTIONS); + + int JOB_CONTROL_NONE = 0x00000000; // Perform no additional action. + int JOB_CONTROL_PAUSE = 0x00000001; // Pause the print job. + int JOB_CONTROL_RESUME = 0x00000002; // Resume a paused print job. + int JOB_CONTROL_CANCEL = 0x00000003; // Delete a print job. + int JOB_CONTROL_RESTART = 0x00000004; // Restart a print job. + int JOB_CONTROL_DELETE = 0x00000005; // Delete a print job. + int JOB_CONTROL_SENT_TO_PRINTER = 0x00000006; // Used by port monitors to signal that a print job has been sent to the printer. This value SHOULD NOT be used remotely. + int JOB_CONTROL_LAST_PAGE_EJECTED = 0x00000007; // Used by language monitors to signal that the last page of a print job has been ejected from the printer. This value SHOULD NOT be used remotely. + int JOB_CONTROL_RETAIN = 0x00000008; // Keep the print job in the print queue after it prints. + int JOB_CONTROL_RELEASE = 0x00000009; // Release the print job, undoing the effect of a JOB_CONTROL_RETAIN action. + + boolean SetJob(WinNT.HANDLE hPrinter, int JobId, int Level, Pointer pJob, int Command); +} \ No newline at end of file diff --git a/src/qz/printer/status/Cups.java b/src/qz/printer/status/Cups.java index 0ed3d6aee..c9d8fde10 100644 --- a/src/qz/printer/status/Cups.java +++ b/src/qz/printer/status/Cups.java @@ -5,6 +5,7 @@ /** * Created by kyle on 3/14/17. */ +@SuppressWarnings("unused") public interface Cups extends Library { Cups INSTANCE = Native.load("cups", Cups.class); @@ -31,6 +32,8 @@ class IPP { public static int CREATE_PRINTER_SUBSCRIPTION = INSTANCE.ippOpValue("Create-Printer-Subscription"); public static int CREATE_JOB_SUBSCRIPTION = INSTANCE.ippOpValue("Create-Job-Subscription"); public static int CANCEL_SUBSCRIPTION = INSTANCE.ippOpValue("Cancel-Subscription"); + public static int GET_JOBS = INSTANCE.ippOpValue("Get-Jobs"); + public static int CANCEL_JOB = INSTANCE.ippOpValue("Cancel-Job"); public static final int OP_PRINT_JOB = 0x02; public static final int INT_ERROR = 0; diff --git a/src/qz/printer/status/CupsUtils.java b/src/qz/printer/status/CupsUtils.java index 00280d178..c19893574 100644 --- a/src/qz/printer/status/CupsUtils.java +++ b/src/qz/printer/status/CupsUtils.java @@ -167,7 +167,7 @@ public static boolean clearSubscriptions() { } static void startSubscription(int rssPort) { - Runtime.getRuntime().addShutdownHook(new Thread(() -> freeIppObjs())); + Runtime.getRuntime().addShutdownHook(new Thread(CupsUtils::freeIppObjs)); String[] subscriptions = {"job-state-changed", "printer-state-changed"}; Pointer request = cups.ippNewRequest(IPP.CREATE_JOB_SUBSCRIPTION); @@ -205,6 +205,29 @@ static void endSubscription(int id) { cups.ippDelete(response); } + public static ArrayList listJobs(String printerName) { + Pointer request = cups.ippNewRequest(IPP.GET_JOBS); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER); + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, + URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers/" + printerName)); + + Pointer response = doRequest(request, "/"); + ArrayList ret = parseJobIds(response); + cups.ippDelete(response); + return ret; + } + + public static void cancelJob(int jobId) { + Pointer request = cups.ippNewRequest(IPP.CANCEL_JOB); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, + URIUtil.encodePath("ipp://localhost:" + IPP.PORT)); + cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "job-id", jobId); + Pointer response = doRequest(request, "/"); + cups.ippDelete(response); + } + public synchronized static void freeIppObjs() { if (http != null) { endSubscription(subscriptionID); @@ -214,36 +237,53 @@ public synchronized static void freeIppObjs() { } } - @SuppressWarnings("unused") - static void parseResponse(Pointer response) { - Pointer attr = Cups.INSTANCE.ippFirstAttribute(response); - while (true) { - if (attr == Pointer.NULL) { - break; + static ArrayList parseJobIds(Pointer response) { + ArrayList attributes = getAttributes(response); + ArrayList ret = new ArrayList<>(); + for (Pointer attribute : attributes) { + if (cups.ippGetName(attribute) != null && cups.ippGetName(attribute).equals("job-id")) { + ret.add(cups.ippGetInteger(attribute, 0)); } - System.out.println(parseAttr(attr)); + } + return ret; + } + + static ArrayList getAttributes(Pointer response) { + ArrayList attributes = new ArrayList<>(); + Pointer attr = Cups.INSTANCE.ippFirstAttribute(response); + while(attr != Pointer.NULL) { + attributes.add(attr); attr = Cups.INSTANCE.ippNextAttribute(response); } + return attributes; + } + + @SuppressWarnings("unused") + static void parseResponse(Pointer response) { + ArrayList attributes = getAttributes(response); + for (Pointer attribute : attributes) { + System.out.println(parseAttr(attribute)); + } System.out.println("------------------------"); } static String parseAttr(Pointer attr){ int valueTag = Cups.INSTANCE.ippGetValueTag(attr); int attrCount = Cups.INSTANCE.ippGetCount(attr); - String data = ""; + StringBuilder data = new StringBuilder(); String attrName = Cups.INSTANCE.ippGetName(attr); for (int i = 0; i < attrCount; i++) { if (valueTag == Cups.INSTANCE.ippTagValue("Integer")) { - data += Cups.INSTANCE.ippGetInteger(attr, i); + data.append(Cups.INSTANCE.ippGetInteger(attr, i)); } else if (valueTag == Cups.INSTANCE.ippTagValue("Boolean")) { - data += (Cups.INSTANCE.ippGetInteger(attr, i) == 1); + data.append(Cups.INSTANCE.ippGetInteger(attr, i) == 1); } else if (valueTag == Cups.INSTANCE.ippTagValue("Enum")) { - data += Cups.INSTANCE.ippEnumString(attrName, Cups.INSTANCE.ippGetInteger(attr, i)); + data.append(Cups.INSTANCE.ippEnumString(attrName, Cups.INSTANCE.ippGetInteger(attr, i))); } else { - data += Cups.INSTANCE.ippGetString(attr, i, ""); + data.append(Cups.INSTANCE.ippGetString(attr, i, "")); } if (i + 1 < attrCount) { - data += ", "; + data.append(", "); } } diff --git a/src/qz/utils/PrintingUtilities.java b/src/qz/utils/PrintingUtilities.java index 32864e78b..171d69d32 100644 --- a/src/qz/utils/PrintingUtilities.java +++ b/src/qz/utils/PrintingUtilities.java @@ -1,5 +1,6 @@ package qz.utils; +import com.sun.jna.platform.win32.*; import org.apache.commons.pool2.impl.GenericKeyedObjectPool; import org.apache.commons.ssl.Base64; import org.codehaus.jettison.json.JSONArray; @@ -9,15 +10,22 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import qz.common.Constants; +import qz.communication.WinspoolEx; import qz.printer.PrintOptions; import qz.printer.PrintOutput; +import qz.printer.PrintServiceMatcher; import qz.printer.action.PrintProcessor; import qz.printer.action.ProcessorFactory; +import qz.printer.info.NativePrinter; +import qz.printer.status.CupsUtils; +import qz.printer.status.job.WmiJobStatusMap; import qz.ws.PrintSocketClient; +import javax.print.PrintException; import java.awt.print.PrinterAbortException; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; @@ -219,4 +227,74 @@ public static void processPrintRequest(Session session, String UID, JSONObject p } } + public static void cancelJobs(Session session, String UID, JSONObject params) { + try { + NativePrinter printer = PrintServiceMatcher.matchPrinter(params.getString("printerName")); + if (printer == null) { + throw new PrintException("Printer \"" + params.getString("printerName") + "\" not found"); + } + int paramJobId = params.optInt("jobId", -1); + ArrayList jobIds = getActiveJobIds(printer); + + if (paramJobId >= 0) { + if (jobIds.contains(paramJobId)) { + jobIds.clear(); + jobIds.add(paramJobId); + } else { + String error = "Job# " + paramJobId + " is not part of the '" + printer.getName() + "' print queue"; + log.error(error); + PrintSocketClient.sendError(session, UID, error); + return; + } + } + log.info("Canceling {} jobs from {}", jobIds.size(), printer.getName()); + + for(int jobId : jobIds) { + cancelJobById(jobId, printer); + } + } + catch(JSONException | Win32Exception | PrintException e) { + log.error("Failed to cancel jobs", e); + PrintSocketClient.sendError(session, UID, e); + } + } + + private static void cancelJobById(int jobId, NativePrinter printer) { + if (SystemUtilities.isWindows()) { + WinNT.HANDLEByReference phPrinter = getWmiPrinter(printer); + // TODO: Change to "Winspool" when JNA 5.14.0+ is bundled + if (!WinspoolEx.INSTANCE.SetJob(phPrinter.getValue(), jobId, 0, null, WinspoolEx.JOB_CONTROL_DELETE)) { + Win32Exception e = new Win32Exception(Kernel32.INSTANCE.GetLastError()); + log.warn("Job deletion error for job#{}, {}", jobId, e); + } + } else { + CupsUtils.cancelJob(jobId); + } + } + + private static ArrayList getActiveJobIds(NativePrinter printer) { + if (SystemUtilities.isWindows()) { + WinNT.HANDLEByReference phPrinter = getWmiPrinter(printer); + Winspool.JOB_INFO_1[] jobs = WinspoolUtil.getJobInfo1(phPrinter); + ArrayList jobIds = new ArrayList<>(); + // skip retained jobs and complete jobs + int skipMask = (int)WmiJobStatusMap.RETAINED.getRawCode() | (int)WmiJobStatusMap.PRINTED.getRawCode(); + for(Winspool.JOB_INFO_1 job : jobs) { + if ((job.Status & skipMask) != 0) continue; + jobIds.add(job.JobId); + } + return jobIds; + } else { + return CupsUtils.listJobs(printer.getPrinterId()); + } + } + + private static WinNT.HANDLEByReference getWmiPrinter(NativePrinter printer) throws Win32Exception { + WinNT.HANDLEByReference phPrinter = new WinNT.HANDLEByReference(); + // TODO: Change to "Winspool" when JNA 5.14.0+ is bundled + if (!WinspoolEx.INSTANCE.OpenPrinter(printer.getName(), /*out*/ phPrinter, null)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return phPrinter; + } } diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index 62f33274f..faa46383b 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -307,6 +307,10 @@ private void processMessage(Session session, JSONObject json, SocketConnection c StatusMonitor.stopListening(connection); sendResult(session, UID, null); break; + case PRINTERS_CLEAR_QUEUE: + PrintingUtilities.cancelJobs(session, UID, params); + sendResult(session, UID, null); + break; case PRINT: PrintingUtilities.processPrintRequest(session, UID, params); break; diff --git a/src/qz/ws/SocketMethod.java b/src/qz/ws/SocketMethod.java index 0caf37998..e2e2e0558 100644 --- a/src/qz/ws/SocketMethod.java +++ b/src/qz/ws/SocketMethod.java @@ -5,6 +5,7 @@ public enum SocketMethod { PRINTERS_FIND("printers.find", true, "access connected printers"), PRINTERS_DETAIL("printers.detail", true, "access connected printers"), PRINTERS_START_LISTENING("printers.startListening", true, "listen for printer status"), + PRINTERS_CLEAR_QUEUE("printers.clearQueue", true, "cancel all pending jobs for a given printer"), PRINTERS_GET_STATUS("printers.getStatus", false), PRINTERS_STOP_LISTENING("printers.stopListening", false), PRINT("print", true, "print to %s"),