Skip to content

Clear Queue (cancel-jobs) #1191

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

Merged
merged 13 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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] todo
* @param {number} [options.jobId] todo
*
* @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
25 changes: 25 additions & 0 deletions src/qz/communication/WinspoolEx.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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;

@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
43 changes: 43 additions & 0 deletions src/qz/utils/PrintingUtilities.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package qz.utils;

import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.Winspool;
import com.sun.jna.platform.win32.WinspoolUtil;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.commons.ssl.Base64;
import org.codehaus.jettison.json.JSONArray;
Expand All @@ -9,15 +13,21 @@
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 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 +229,37 @@ 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 (SystemUtilities.isWindows()) {
WinNT.HANDLEByReference phPrinter = new WinNT.HANDLEByReference();
WinspoolEx.INSTANCE.OpenPrinter(printer.getName(), phPrinter, null);
Winspool.JOB_INFO_1[] jobs = WinspoolUtil.getJobInfo1(phPrinter);
// skip retained jobs and complete jobs
int skipMask = (int)WmiJobStatusMap.RETAINED.getRawCode() | (int)WmiJobStatusMap.PRINTED.getRawCode();
int deletedCount = 0;
for (Winspool.JOB_INFO_1 job : jobs) {
if ((job.Status & skipMask) != 0) continue;
boolean result = WinspoolEx.INSTANCE.SetJob(phPrinter.getValue(), job.JobId, 0, null, WinspoolEx.JOB_CONTROL_DELETE);
if (result) {
deletedCount++;
} else {
log.warn("Job deletion error for job#{}, error code:{}", job.JobId, Kernel32.INSTANCE.GetLastError());
}
}
log.info("Deleting {} jobs", deletedCount);
} else {
ArrayList<Integer> jobIds = CupsUtils.listJobs(printer.getPrinterId());
log.info("Deleting {} jobs", jobIds.size());
for(int jobId : jobIds) {
CupsUtils.cancelJob(jobId);
}
}
}
catch(JSONException e) {
log.error("Failed to cancel jobs", e);
PrintSocketClient.sendError(session, UID, e);
}
}
}
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