Files
quality_app/old code/tray/src/qz/printer/status/WmiPrinterStatusThread.java
Quality System Admin c96039542d updated control access
2025-10-16 02:36:32 +03:00

269 lines
12 KiB
Java
Executable File

package qz.printer.status;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.*;
import com.sun.jna.ptr.PointerByReference;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.printer.status.job.WmiJobStatusMap;
import qz.printer.status.printer.NativePrinterStatus;
import qz.printer.status.printer.WmiPrinterStatusMap;
import java.util.*;
import static qz.printer.status.printer.WmiPrinterStatusMap.*;
public class WmiPrinterStatusThread extends Thread {
private static final Logger log = LogManager.getLogger(StatusMonitor.class);
private final Winspool spool = Winspool.INSTANCE;
private final String printerName;
private final HashMap<Integer, String> docNames = new HashMap<>();
private final HashMap<Integer, ArrayList<Integer>> pendingJobStatuses = new HashMap<>();
private final HashMap<Integer, Integer> lastJobStatusCodes = new HashMap<>();
private boolean holdsJobs;
private int statusField;
private int attributeField;
// Last "combined" printer status, see also combineStatus()
private int lastPrinterStatus;
private boolean wasOk = false;
private boolean closing = false;
private WinNT.HANDLE hChangeObject;
private WinDef.DWORDByReference pdwChangeResult;
Winspool.PRINTER_NOTIFY_OPTIONS listenOptions;
Winspool.PRINTER_NOTIFY_OPTIONS statusOptions;
// Honor translated strings, if available
private static final ArrayList<String> invalidNames = new ArrayList<>();
static {
try {
invalidNames.add(User32Util.loadString("%SystemRoot%\\system32\\localspl.dll,108"));
invalidNames.add(User32Util.loadString("%SystemRoot%\\system32\\localspl.dll,107"));
} catch(Exception e) {
log.warn("Unable to obtain strings, defaulting to en-US values.", e);
invalidNames.add("Local Downlevel Document");
invalidNames.add("Remote Downlevel Document");
}
}
public WmiPrinterStatusThread(Winspool.PRINTER_INFO_2 printerInfo2) {
super("Printer Status Monitor " + printerInfo2.pPrinterName);
printerName = printerInfo2.pPrinterName;
holdsJobs = (printerInfo2.Attributes & Winspool.PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS) > 0;
statusField = printerInfo2.Status;
attributeField = printerInfo2.Attributes;
lastPrinterStatus = combineStatus(statusField, attributeField);
listenOptions = new Winspool.PRINTER_NOTIFY_OPTIONS();
listenOptions.Version = 2;
listenOptions.Flags = Winspool.PRINTER_NOTIFY_OPTIONS_REFRESH;
listenOptions.Count = 2;
Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[] mem = (Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[])
new Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference().toArray(2);
mem[0].Type = Winspool.JOB_NOTIFY_TYPE;
mem[0].setFields(new short[] {Winspool.JOB_NOTIFY_FIELD_STATUS, Winspool.JOB_NOTIFY_FIELD_DOCUMENT });
mem[1].Type = Winspool.PRINTER_NOTIFY_TYPE;
mem[1].setFields(new short[] {Winspool.PRINTER_NOTIFY_FIELD_STATUS, Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES });
listenOptions.pTypes = mem[0];
statusOptions = new Winspool.PRINTER_NOTIFY_OPTIONS();
statusOptions.Version = 2;
// Status option 'refresh' leads to a loss of data associated with our lock. I don't know why.
// statusOptions.Flags = Winspool.PRINTER_NOTIFY_OPTIONS_REFRESH;
statusOptions.Count = 2;
mem = (Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[])
new Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference().toArray(2);
mem[0].Type = Winspool.JOB_NOTIFY_TYPE;
mem[0].setFields(new short[] { Winspool.JOB_NOTIFY_FIELD_STATUS, Winspool.JOB_NOTIFY_FIELD_DOCUMENT });
mem[1].Type = Winspool.PRINTER_NOTIFY_TYPE;
mem[1].setFields(new short[] { Winspool.PRINTER_NOTIFY_FIELD_STATUS, Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES });
statusOptions.pTypes = mem[0];
}
@Override
public void run() {
attachToSystem();
if (hChangeObject != null) {
while(!closing) {
waitOnChange();
if (closing) { break; }
ingestChange();
}
}
}
private void attachToSystem() {
WinNT.HANDLEByReference phPrinterObject = new WinNT.HANDLEByReference();
spool.OpenPrinter(printerName, phPrinterObject, null);
pdwChangeResult = new WinDef.DWORDByReference();
//The second param determines what kind of event releases our lock
//See https://msdn.microsoft.com/en-us/library/windows/desktop/dd162722(v=vs.85).aspx
hChangeObject = spool.FindFirstPrinterChangeNotification(phPrinterObject.getValue(), Winspool.PRINTER_CHANGE_JOB, 0, listenOptions);
}
private void waitOnChange() {
Kernel32.INSTANCE.WaitForSingleObject(hChangeObject, WinBase.INFINITE);
}
private void ingestChange() {
PointerByReference dataPointer = new PointerByReference();
if (spool.FindNextPrinterChangeNotification(hChangeObject, pdwChangeResult, statusOptions, dataPointer)) {
// Many events fire with dataPointer == null, see also https://stackoverflow.com/questions/16283827
if (dataPointer.getValue() != null) {
Winspool.PRINTER_NOTIFY_INFO data = Structure.newInstance(Winspool.PRINTER_NOTIFY_INFO.class, dataPointer.getValue());
data.read();
for (Winspool.PRINTER_NOTIFY_INFO_DATA d: data.aData) {
decodeStatus(d);
}
sendPendingStatuses();
Winspool.INSTANCE.FreePrinterNotifyInfo(data.getPointer());
}
} else {
issueError();
}
}
private void decodeStatus(Winspool.PRINTER_NOTIFY_INFO_DATA d) {
if (d.Type == Winspool.PRINTER_NOTIFY_TYPE) {
if (d.Field == Winspool.PRINTER_NOTIFY_FIELD_STATUS) { // Printer Status Changed
statusField = d.NotifyData.adwData[0];
} else if (d.Field == Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES) { // Printer Attributes Changed
attributeField = d.NotifyData.adwData[0];
holdsJobs = (d.NotifyData.adwData[0] & Winspool.PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS) != 0;
} else {
log.warn("Unknown event field {}", d.Field);
}
int combinedStatus = combineStatus(statusField, attributeField);
if (combinedStatus != lastPrinterStatus) {
Status[] statuses = NativeStatus.fromWmiPrinterStatus(combinedStatus, printerName);
StatusMonitor.statusChanged(statuses);
// If the printer was in an error state before and is not now, send an 'OK'
boolean isOk = (combinedStatus & NOT_OK_MASK) == 0;
if (isOk && !wasOk) {
// If the status is 0x00000000, fromWmiPrinterStatus returns 'OK'. We don't want to send a duplicate.
if (combinedStatus != 0) StatusMonitor.statusChanged(new Status[]{new Status(NativePrinterStatus.OK, printerName, 0)});
}
wasOk = isOk;
lastPrinterStatus = combinedStatus;
}
} else if (d.Type == Winspool.JOB_NOTIFY_TYPE) {
// Job Name Set or Changed
if (d.Field == Winspool.JOB_NOTIFY_FIELD_DOCUMENT) {
// The element containing our Doc name is not always the first item of the event
// The Job name is only sent once, catalog it for later statuses
docNames.put(d.Id, d.NotifyData.Data.pBuf.getWideString(0));
// Job Status Changed
} else if (d.Field == Winspool.JOB_NOTIFY_FIELD_STATUS) {
//If there is no list for a given ID, create a new one and add it to the collection under said ID
ArrayList<Integer> statusList = pendingJobStatuses.computeIfAbsent(d.Id, k -> new ArrayList<>());
statusList.add(d.NotifyData.adwData[0]);
}
}
}
/**
* Bitwise-safe combination of statusField and attributeField's PRINTER_ATTRIBUTE_WORK_OFFLINE.
*
* Due to PRINTER_ATTRIBUTE_WORK_OFFLINE's overlapping bitwise value, we must use a
* non-overlapping value, ATTRIBUTE_WORK_OFFLINE.
*
* See also: https://stackoverflow.com/questions/41437023
*/
private static int combineStatus(int statusField, int attributeField) {
int workOfflineFlag = (attributeField & Winspool.PRINTER_ATTRIBUTE_WORK_OFFLINE) == 0 ? 0 : (int)WmiPrinterStatusMap.ATTRIBUTE_WORK_OFFLINE.getRawCode();
return statusField | workOfflineFlag;
}
private void sendPendingStatuses() {
if (pendingJobStatuses.size() == 0) return;
for (Iterator<Map.Entry<Integer, ArrayList<Integer>>> i = pendingJobStatuses.entrySet().iterator(); i.hasNext();) {
Map.Entry<Integer, ArrayList<Integer>> jobCodesEntry = i.next();
ArrayList<Integer> codes = jobCodesEntry.getValue();
int jobId = jobCodesEntry.getKey();
// Wait until we have a real docName
if (invalidNames.contains(docNames.get(jobId))) continue;
// Workaround for double 'printed' statuses
if (holdsJobs && docNames.get(jobId) == null && codes.size() == 1 && codes.get(0) == (int)WmiJobStatusMap.PRINTED.getRawCode()) {
i.remove();
lastJobStatusCodes.remove(jobId);
continue;
}
for (int code: codes) {
int oldStatusCode = lastJobStatusCodes.getOrDefault(jobId, 0);
// This only sets status flags if they are not in oldStatusCode
int statusToReport = code & (~oldStatusCode);
if (statusToReport != 0) {
StatusMonitor.statusChanged(NativeStatus.fromWmiJobStatus(statusToReport, printerName, jobId, docNames.get(jobId)));
}
lastJobStatusCodes.put(jobId, code);
}
i.remove();
int code = codes.get(codes.size() - 1);
boolean isFinalCode = (code & (int)WmiJobStatusMap.DELETED.getRawCode()) > 0;
// If the printer holds jobs, the last event we will see is 'printed' or 'deleted' and not 'printing', otherwise it will be just 'deleted'.
if (holdsJobs) {
isFinalCode |= (code & (int)WmiJobStatusMap.PRINTED.getRawCode()) > 0;
isFinalCode &= (code & (int)WmiJobStatusMap.PRINTING.getRawCode()) == 0;
}
// If that was the last status we will see from a job, remove it from our lists.
if (isFinalCode) {
docNames.remove(jobId);
lastJobStatusCodes.remove(jobId);
}
}
}
private void issueError() {
int errorCode = Kernel32.INSTANCE.GetLastError();
log.error("WMI Error number: {}, This should be reported", errorCode);
Status[] unknownError = { new Status(NativePrinterStatus.UNMAPPED, printerName, WmiPrinterStatusMap.UNKNOWN_STATUS.getRawCode()) };
StatusMonitor.statusChanged(unknownError);
try {
//if the error repeats, we don't want to lock up the cpu
Thread.sleep(1000);
}
catch(Exception ignore) {}
}
@Override
public void interrupt() {
closing = true;
spool.FindClosePrinterChangeNotification(hChangeObject);
super.interrupt();
}
public static ArrayList<Status> getAllStatuses() {
ArrayList<Status> statuses = new ArrayList<>();
Winspool.PRINTER_INFO_2[] wmiPrinters = WinspoolUtil.getAllPrinterInfo2();
for(Winspool.PRINTER_INFO_2 printerInfo2 : wmiPrinters) {
WinNT.HANDLEByReference phPrinter = new WinNT.HANDLEByReference();
Winspool.INSTANCE.OpenPrinter(printerInfo2.pPrinterName, phPrinter, null);
for(Winspool.JOB_INFO_1 info : WinspoolUtil.getJobInfo1(phPrinter)) {
Collections.addAll(statuses, NativeStatus.fromWmiJobStatus(info.Status, printerInfo2.pPrinterName, info.JobId, info.pDocument));
}
Collections.addAll(statuses, NativeStatus.fromWmiPrinterStatus(combineStatus(printerInfo2.Status, printerInfo2.Attributes), printerInfo2.pPrinterName));
}
return statuses;
}
}