Skip to content

Commit

Permalink
Clear Queue (cancel-jobs) (#1191)
Browse files Browse the repository at this point in the history
Queue clearing support for CUPS, Winspool
---------

Co-authored-by: Tres Finocchiaro <[email protected]>
  • Loading branch information
Vzor- and tresf authored Nov 1, 2023
1 parent 37cd150 commit 0d6d085
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 14 deletions.
21 changes: 21 additions & 0 deletions js/qz-tray.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<null|Error>}
* @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.
*
Expand Down
4 changes: 4 additions & 0 deletions sample.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ <h3 class="panel-title">Printer</h3>
<button type="button" class="btn btn-default btn-sm" data-toggle="modal" data-target="#askFileModal">Set To File</button>
<button type="button" class="btn btn-default btn-sm" data-toggle="modal" data-target="#askHostModal">Set To Host</button>
</div>
<button type="button" class="btn btn-warning btn-sm" onclick="clearQueue($('#printerSearch').val());">Clear Queue</button>
</div>
</div>
</div>
Expand Down Expand Up @@ -2082,6 +2083,9 @@ <h4 class="panel-title">Options</h4>
qz.print(config, printData).catch(displayError);
}

function clearQueue(printer) {
qz.printers.clearQueue(printer).catch(displayError);
}

/// Pixel Printers ///
function printHTML() {
Expand Down
28 changes: 28 additions & 0 deletions src/qz/communication/WinspoolEx.java
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions src/qz/printer/status/Cups.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
68 changes: 54 additions & 14 deletions src/qz/printer/status/CupsUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -205,6 +205,29 @@ static void endSubscription(int id) {
cups.ippDelete(response);
}

public static ArrayList<Integer> 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<Integer> 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);
Expand All @@ -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<Integer> parseJobIds(Pointer response) {
ArrayList<Pointer> attributes = getAttributes(response);
ArrayList<Integer> 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<Pointer> getAttributes(Pointer response) {
ArrayList<Pointer> 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<Pointer> 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(", ");
}
}

Expand Down
78 changes: 78 additions & 0 deletions src/qz/utils/PrintingUtilities.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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<Integer> 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<Integer> getActiveJobIds(NativePrinter printer) {
if (SystemUtilities.isWindows()) {
WinNT.HANDLEByReference phPrinter = getWmiPrinter(printer);
Winspool.JOB_INFO_1[] jobs = WinspoolUtil.getJobInfo1(phPrinter);
ArrayList<Integer> 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;
}
}
4 changes: 4 additions & 0 deletions src/qz/ws/PrintSocketClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/qz/ws/SocketMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down

0 comments on commit 0d6d085

Please sign in to comment.