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

Clear Queue (cancel-jobs) #1191

Merged
merged 13 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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