Skip to content

Commit

Permalink
Improve printer status by adding job status information (qzind#757)
Browse files Browse the repository at this point in the history
Add Wmi and CUPS Job Status mappings for more details with printer status
- Bump JNA to 5.7.0
- Remove CUPS XML parsing in favor of JNA
- De-dupe repeat status messages
- Combine Job Status + Printer Status into single API
Co-authored-by: Kyle Berezin <[email protected]>
  • Loading branch information
tresf authored Mar 30, 2021
1 parent 271c209 commit 79d47fb
Show file tree
Hide file tree
Showing 25 changed files with 1,952 additions and 468 deletions.
Binary file removed lib/communication/jna-5.6.0.jar
Binary file not shown.
Binary file added lib/communication/jna-5.7.0.jar
Binary file not shown.
Binary file removed lib/communication/jna-platform-5.6.0.jar
Binary file not shown.
Binary file added lib/communication/jna-platform-5.7.0.jar
Binary file not shown.
12 changes: 5 additions & 7 deletions sample.html
Original file line number Diff line number Diff line change
Expand Up @@ -1094,7 +1094,7 @@ <h4 class="panel-title">Event Log</h4>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<div id="printersLog"></div>
<pre id="printersLog"></pre>
</div>
</div>
<hr />
Expand Down Expand Up @@ -2641,7 +2641,7 @@ <h4 class="panel-title">Options</h4>
});

qz.printers.setPrinterCallbacks(function(streamEvent) {
addPrintersLog(streamEvent.message, streamEvent.severity);
addPrintersLog(streamEvent);
});

qz.file.setFileCallbacks(function(streamEvent) {
Expand Down Expand Up @@ -2747,11 +2747,9 @@ <h4 class="panel-title">Options</h4>
}
}

function addPrintersLog(msg, color) {
if (color == undefined) { color = ""; }

msg = '<div class="' + color + '">' + new Date().toString() + ": " + msg + "</div>";

function addPrintersLog(streamEvent) {
var icon = '<span class="fa ' + (streamEvent.eventType == 'JOB' ? 'fa-exchange' : 'fa-print') + '"></span>&nbsp;';
var msg = '<p class="' + (streamEvent.severity || "") + ' text-nowrap">' + icon + new Date().toString() + ": " + streamEvent.message + "</p>";
var $log = $("#printersLog");
$log.html($log.html() + msg);
}
Expand Down
20 changes: 14 additions & 6 deletions src/qz/printer/PrintServiceMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@
public class PrintServiceMatcher {
private static final Logger log = LoggerFactory.getLogger(PrintServiceMatcher.class);

public static NativePrinterMap getNativePrinterList() {
public static NativePrinterMap getNativePrinterList(boolean silent) {
NativePrinterMap printers = NativePrinterMap.getInstance();
printers.putAll(PrintServiceLookup.lookupPrintServices(null, null));
log.debug("Found {} printers", printers.size());
if(!silent) log.debug("Found {} printers", printers.size());
return printers;
}

public static NativePrinterMap getNativePrinterList() {
return getNativePrinterList(false);
}

public static String findPrinterName(String query) throws JSONException {
NativePrinter printer = PrintServiceMatcher.matchPrinter(query);

Expand All @@ -53,16 +57,17 @@ public static String findPrinterName(String query) throws JSONException {
*
* @param printerSearch Search query to compare against service names.
*/
public static NativePrinter matchPrinter(String printerSearch) {
public static NativePrinter matchPrinter(String printerSearch, boolean silent) {
NativePrinter exact = null;
NativePrinter begins = null;
NativePrinter partial = null;

log.debug("Searching for PrintService matching {}", printerSearch);
if(!silent) log.debug("Searching for PrintService matching {}", printerSearch);

printerSearch = printerSearch.toLowerCase(Locale.ENGLISH);

// Search services for matches
for(NativePrinter printer : getNativePrinterList().values()) {
for(NativePrinter printer : getNativePrinterList(silent).values()) {
if (printer.getName() == null) {
continue;
}
Expand Down Expand Up @@ -110,14 +115,17 @@ public static NativePrinter matchPrinter(String printerSearch) {
}

if (use != null) {
log.debug("Found match: {}", use.getPrintService().value().getName());
if(!silent) log.debug("Found match: {}", use.getPrintService().value().getName());
} else {
log.warn("Printer not found: {}", printerSearch);
}

return use;
}

public static NativePrinter matchPrinter(String printerSearch) {
return matchPrinter(printerSearch, false);
}

public static JSONArray getPrintersJSON() throws JSONException {
JSONArray list = new JSONArray();
Expand Down
10 changes: 6 additions & 4 deletions src/qz/printer/status/Cups.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package qz.printer.status;

import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.*;

/**
* Created by kyle on 3/14/17.
Expand All @@ -22,11 +19,15 @@ class IPP {
public static int TAG_NAME = INSTANCE.ippTagValue("Name");
public static int TAG_INTEGER = INSTANCE.ippTagValue("Integer");
public static int TAG_KEYWORD = INSTANCE.ippTagValue("keyword");
public static int TAG_ENUM = INSTANCE.ippTagValue("enum");
public static int TAG_SUBSCRIPTION = INSTANCE.ippTagValue("Subscription");
public static int GET_PRINTERS = INSTANCE.ippOpValue("CUPS-Get-Printers");
public static int GET_PRINTER_ATTRIBUTES = INSTANCE.ippOpValue("Get-Printer-Attributes");
public static int GET_JOB_ATTRIBUTES = INSTANCE.ippOpValue("Get-Job-Attributes");
public static int GET_SUBSCRIPTIONS = INSTANCE.ippOpValue("Get-Subscriptions");
public static int GET_NOTIFICATIONS = INSTANCE.ippOpValue("Get-Notifications");
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 final int INT_ERROR = 0;
public static final int INT_UNDEFINED = -1;
Expand Down Expand Up @@ -56,6 +57,7 @@ class IPP {
int ippEnumValue(String attrname, String enumstring);
int ippOpValue(String name);
int ippAddString(Pointer ipp, int group, int tag, String name, String charset, String value);
int ippAddStrings(Pointer ipp, int group, int tag, String name, int num_values, String language, StringArray values);
int ippAddInteger (Pointer ipp, int group, int tag, String name, int value);
int ippGetCount(Pointer attr);
int ippGetValueTag(Pointer ipp);
Expand Down
150 changes: 82 additions & 68 deletions src/qz/printer/status/CupsStatusHandler.java
Original file line number Diff line number Diff line change
@@ -1,100 +1,114 @@
package qz.printer.status;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import com.sun.jna.Pointer;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.printer.status.job.NativeJobStatus;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

/**
* Created by kyle on 4/27/17.
*/
public class CupsStatusHandler extends AbstractHandler {
private static final Logger log = LoggerFactory.getLogger(CupsStatusHandler.class);

private static String lastGuid;
private static Cups cups = Cups.INSTANCE;
private int lastEventNumber = 0;
private HashMap<String, ArrayList<Status>> lastPrinterStatusMap = new HashMap<>();
private HashMap<String, ArrayList<Status>> lastJobStatusMap = new HashMap<>();

public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
baseRequest.setHandled(true);
if (request.getReader().readLine() != null) {
try {
XMLInputFactory factory = XMLInputFactory.newInstance();
XMLEventReader eventReader = factory.createXMLEventReader(request.getReader());
parseXML(eventReader);
}
catch(Exception e) {
e.printStackTrace();
}
getNotifications();
}
}

private void parseXML(XMLEventReader eventReader) throws XMLStreamException {
boolean isEventDescription = false, isGuid = false, isFirstGuid = true, running = true;
String firstGuid = "";
String eventDescription = "";
private synchronized void getNotifications() {
Pointer response = CupsUtils.getStatuses(lastEventNumber + 1);

while(eventReader.hasNext() && running) {
XMLEvent event = eventReader.nextEvent();
switch(event.getEventType()) {
case XMLStreamConstants.START_ELEMENT:
StartElement startElement = event.asStartElement();
String qName = startElement.getName().getLocalPart();
if ("description".equalsIgnoreCase(startElement.getName().getLocalPart())) {
isEventDescription = true;
eventDescription = "";
}
if ("guid".equalsIgnoreCase(qName)) {
isGuid = true;
}
break;
case XMLStreamConstants.END_ELEMENT:
EndElement endElement = event.asEndElement();
if ("description".equalsIgnoreCase(endElement.getName().getLocalPart())) {
isEventDescription = false;
}
break;
case XMLStreamConstants.CHARACTERS:
Characters characters = event.asCharacters();
if (isEventDescription) {
eventDescription += characters.getData();
}
if (isGuid) {
String guid = characters.getData();
if (isFirstGuid) {
firstGuid = guid;
isFirstGuid = false;
}
if (guid.equals(lastGuid)) {
running = false;
break;
} else {
String printerName = StringUtils.substringBeforeLast(eventDescription, "\"");
printerName = StringUtils.substringAfter(printerName, "\"");
printerName = StringEscapeUtils.unescapeXml(printerName);
if (!printerName.isEmpty() && StatusMonitor.isListeningTo(printerName)) {
StatusMonitor.statusChanged(CupsUtils.getStatuses(printerName));
}
}
isGuid = false;
Pointer eventNumberAttr = cups.ippFindAttribute(response, "notify-sequence-number", Cups.IPP.TAG_INTEGER);
Pointer eventTypeAttr = cups.ippFindAttribute(response, "notify-subscribed-event", Cups.IPP.TAG_KEYWORD);
ArrayList<Status> statuses = new ArrayList<>();

while (eventNumberAttr != Pointer.NULL) {
lastEventNumber = cups.ippGetInteger(eventNumberAttr, 0);
Pointer printerNameAttr = cups.ippFindNextAttribute(response, "printer-name", Cups.IPP.TAG_NAME);

String printer = cups.ippGetString(printerNameAttr, 0, "");
String eventType = cups.ippGetString(eventTypeAttr, 0, "");
if (eventType.startsWith("job")) {
Pointer JobIdAttr = cups.ippFindNextAttribute(response, "notify-job-id", Cups.IPP.TAG_INTEGER);
Pointer jobStateAttr = cups.ippFindNextAttribute(response, "job-state", Cups.IPP.TAG_ENUM);
Pointer jobNameAttr = cups.ippFindNextAttribute(response, "job-name", Cups.IPP.TAG_NAME);
Pointer jobStateReasonsAttr = cups.ippFindNextAttribute(response, "job-state-reasons", Cups.IPP.TAG_KEYWORD);
int jobId = cups.ippGetInteger(JobIdAttr, 0);
String jobState = Cups.INSTANCE.ippEnumString("job-state", Cups.INSTANCE.ippGetInteger(jobStateAttr, 0));
String jobName = cups.ippGetString(jobNameAttr, 0, "");
// Statuses come in blocks eg. {printing, toner_low} We only want to display a status if it didn't exist in the last block
// Get the list of statuses from the last block associated with this printer
// '/' Is a documented invalid character for CUPS printer names. We will use that as a separator
String jobKey = printer + "/" + jobId;
ArrayList<Status> oldStatuses = lastJobStatusMap.getOrDefault(jobKey, new ArrayList<>());
ArrayList<Status> newStatuses = new ArrayList<>();

boolean completed = false;
int attrCount = cups.ippGetCount(jobStateReasonsAttr);
for (int i = 0; i < attrCount; i++) {
String reason = cups.ippGetString(jobStateReasonsAttr, i, "");
Status pending = NativeStatus.fromCupsJobStatus(reason, jobState, printer, jobId, jobName);
// If this status was one we didn't see last block, send it
if (!oldStatuses.contains(pending)) statuses.add(pending);
// If the job is complete, we need to remove it from our map
if ((pending.getCode() == NativeJobStatus.COMPLETE) ||
(pending.getCode() == NativeJobStatus.CANCELED)) {
completed = true;
}
break;
// regardless, remember the status for the next block
newStatuses.add(pending);
}
if (completed) {
lastJobStatusMap.remove(jobKey);
} else {
// Replace the old list with the new one
lastJobStatusMap.put(jobKey, newStatuses);
}
} else if (eventType.startsWith("printer")) {
Pointer printerStateAttr = cups.ippFindNextAttribute(response, "printer-state", Cups.IPP.TAG_ENUM);
Pointer printerStateReasonsAttr = cups.ippFindNextAttribute(response, "printer-state-reasons", Cups.IPP.TAG_KEYWORD);
String state = Cups.INSTANCE.ippEnumString("printer-state", Cups.INSTANCE.ippGetInteger(printerStateAttr, 0));
// Statuses come in blocks eg. {printing, toner_low} We only want to display a status if it didn't exist in the last block
// Get the list of statuses from the last block associated with this printer
ArrayList<Status> oldStatuses = lastPrinterStatusMap.getOrDefault(printer, new ArrayList<>());
ArrayList<Status> newStatuses = new ArrayList<>();

int attrCount = cups.ippGetCount(printerStateReasonsAttr);
for (int i = 0; i < attrCount; i++) {
String reason = cups.ippGetString(printerStateReasonsAttr, i, "");
Status pending = NativeStatus.fromCupsPrinterStatus(reason, state, printer);
// If this status was one we didn't see last block, send it
if (!oldStatuses.contains(pending)) statuses.add(pending);
// regardless, remember the status for the next block
newStatuses.add(pending);
}
// Replace the old list with the new one
lastPrinterStatusMap.put(printer, newStatuses);
} else {
log.debug("Unknown CUPS event type {}.", eventType);
}
eventNumberAttr = cups.ippFindNextAttribute(response, "notify-sequence-number", Cups.IPP.TAG_INTEGER);
eventTypeAttr = cups.ippFindNextAttribute(response, "notify-subscribed-event", Cups.IPP.TAG_KEYWORD);
}

lastGuid = firstGuid;
cups.ippDelete(response);
StatusMonitor.statusChanged(statuses.toArray(new Status[statuses.size()]));
}
}
Loading

0 comments on commit 79d47fb

Please sign in to comment.