Created
July 9, 2025 14:46
-
-
Save caloni/a2f29105a80d54b6b0b30a0c7b5ca39e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| using System.Threading; | |
| class PrinterMonitor | |
| { | |
| private const int PRINTER_CHANGE_ADD_JOB = 0x00000100; | |
| private const int PRINTER_CHANGE_SET_JOB = 0x00000200; | |
| private const int PRINTER_CHANGE_DELETE_JOB = 0x00000400; | |
| private const int PRINTER_CHANGE_WRITE_JOB = 0x00000800; | |
| private const uint INFINITE = 0xFFFFFFFF; | |
| private const uint WAIT_OBJECT_0 = 0x00000000; | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct PRINTER_NOTIFY_INFO | |
| { | |
| public uint Version; | |
| public uint Flags; | |
| public uint Count; | |
| // Followed by PRINTER_NOTIFY_INFO_DATA array | |
| } | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct PRINTER_NOTIFY_INFO_DATA | |
| { | |
| public ushort Type; | |
| public ushort Field; | |
| public uint Reserved; | |
| public uint Id; | |
| public NotifyData Data; | |
| } | |
| [StructLayout(LayoutKind.Explicit)] | |
| public struct NotifyData | |
| { | |
| [FieldOffset(0)] public uint cbBuf; | |
| [FieldOffset(4)] public IntPtr pBuf; | |
| [FieldOffset(0)] public uint dwData; | |
| } | |
| [DllImport("winspool.drv", CharSet = CharSet.Unicode, SetLastError = true)] | |
| static extern bool OpenPrinterW(string pPrinterName, out IntPtr phPrinter, IntPtr pDefault); | |
| [DllImport("winspool.drv", SetLastError = true)] | |
| static extern IntPtr FindFirstPrinterChangeNotification( | |
| IntPtr hPrinter, | |
| int fdwFilter, | |
| int fdwOptions, | |
| IntPtr pPrinterNotifyOptions | |
| ); | |
| [DllImport("winspool.drv", SetLastError = true)] | |
| static extern bool FindNextPrinterChangeNotification( | |
| IntPtr hChange, | |
| out int pdwChange, | |
| IntPtr pPrinterNotifyOptions, | |
| out IntPtr ppPrinterNotifyInfo | |
| ); | |
| [DllImport("winspool.drv", SetLastError = true)] | |
| static extern bool ClosePrinter(IntPtr hPrinter); | |
| [DllImport("kernel32.dll", SetLastError = true)] | |
| static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); | |
| const ushort JOB_NOTIFY_TYPE = 0x02; | |
| const ushort JOB_NOTIFY_FIELD_PRINTER_NAME = 0x00; | |
| const ushort JOB_NOTIFY_FIELD_MACHINE_NAME = 0x01; | |
| const ushort JOB_NOTIFY_FIELD_PORT_NAME = 0x02; | |
| const ushort JOB_NOTIFY_FIELD_USER_NAME = 0x03; | |
| const ushort JOB_NOTIFY_FIELD_NOTIFY_NAME = 0x04; | |
| const ushort JOB_NOTIFY_FIELD_DATATYPE = 0x05; | |
| const ushort JOB_NOTIFY_FIELD_PRINT_PROCESSOR = 0x06; | |
| const ushort JOB_NOTIFY_FIELD_PARAMETERS = 0x07; | |
| const ushort JOB_NOTIFY_FIELD_DRIVER_NAME = 0x08; | |
| const ushort JOB_NOTIFY_FIELD_DEVMODE = 0x09; | |
| const ushort JOB_NOTIFY_FIELD_STATUS = 0x0A; | |
| const ushort JOB_NOTIFY_FIELD_STATUS_STRING = 0x0B; | |
| const ushort JOB_NOTIFY_FIELD_SECURITY_DESCRIPTOR = 0x0C; | |
| const ushort JOB_NOTIFY_FIELD_DOCUMENT = 0x0D; | |
| const ushort JOB_NOTIFY_FIELD_PRIORITY = 0x0E; | |
| const ushort JOB_NOTIFY_FIELD_POSITION = 0x0F; | |
| const ushort JOB_NOTIFY_FIELD_SUBMITTED = 0x10; | |
| const ushort JOB_NOTIFY_FIELD_START_TIME = 0x11; | |
| const ushort JOB_NOTIFY_FIELD_UNTIL_TIME = 0x12; | |
| const ushort JOB_NOTIFY_FIELD_TIME = 0x13; | |
| const ushort JOB_NOTIFY_FIELD_TOTAL_PAGES = 0x14; | |
| const ushort JOB_NOTIFY_FIELD_PAGES_PRINTED = 0x15; | |
| const ushort PRINTER_NOTIFY_CATEGORY_ALL = 0x001000; | |
| const ushort PRINTER_NOTIFY_CATEGORY_3D = 0x002000; | |
| [StructLayout(LayoutKind.Sequential)] | |
| struct PRINTER_NOTIFY_OPTIONS | |
| { | |
| public uint Version; | |
| public uint Flags; | |
| public uint Count; | |
| public IntPtr pTypes; | |
| } | |
| [StructLayout(LayoutKind.Sequential)] | |
| struct PRINTER_NOTIFY_OPTIONS_TYPE | |
| { | |
| public ushort Type; // JOB_NOTIFY_TYPE | |
| public ushort Reserved0; | |
| public uint Reserved1; | |
| public uint Reserved2; | |
| public uint Count; | |
| public IntPtr pFields; | |
| } | |
| private static IntPtr CreateJobNotifyOptions() | |
| { | |
| // Interested in Document Name, Status, and User Name | |
| ushort[] fields = new ushort[] | |
| { | |
| JOB_NOTIFY_FIELD_DOCUMENT, | |
| JOB_NOTIFY_FIELD_STATUS, | |
| JOB_NOTIFY_FIELD_USER_NAME | |
| }; | |
| int fieldSize = Marshal.SizeOf(typeof(ushort)); | |
| IntPtr pFieldArray = Marshal.AllocHGlobal(fields.Length * fieldSize); | |
| for (int i = 0; i < fields.Length; i++) | |
| { | |
| Marshal.WriteInt16(pFieldArray, i * fieldSize, (short)fields[i]); | |
| } | |
| PRINTER_NOTIFY_OPTIONS_TYPE type = new PRINTER_NOTIFY_OPTIONS_TYPE | |
| { | |
| Type = JOB_NOTIFY_TYPE, | |
| Count = (uint)fields.Length, | |
| pFields = pFieldArray | |
| }; | |
| IntPtr pType = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(PRINTER_NOTIFY_OPTIONS_TYPE))); | |
| Marshal.StructureToPtr(type, pType, false); | |
| PRINTER_NOTIFY_OPTIONS options = new PRINTER_NOTIFY_OPTIONS | |
| { | |
| Version = 2, | |
| Count = 1, | |
| pTypes = pType | |
| }; | |
| IntPtr pOptions = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(PRINTER_NOTIFY_OPTIONS))); | |
| Marshal.StructureToPtr(options, pOptions, false); | |
| return pOptions; | |
| } | |
| private static string DecodeJobStatus(uint status) | |
| { | |
| string result = ""; | |
| if ((status & 0x00000001) != 0) result += "Paused, "; | |
| if ((status & 0x00000002) != 0) result += "Error, "; | |
| if ((status & 0x00000004) != 0) result += "Deleting, "; | |
| if ((status & 0x00000008) != 0) result += "Spooling, "; | |
| if ((status & 0x00000010) != 0) result += "Printing, "; | |
| if ((status & 0x00000020) != 0) result += "Offline, "; | |
| if ((status & 0x00000040) != 0) result += "Paper Out, "; | |
| if ((status & 0x00000080) != 0) result += "Printed, "; | |
| if ((status & 0x00000100) != 0) result += "Deleted, "; | |
| if ((status & 0x00000200) != 0) result += "Blocked Device, "; | |
| if ((status & 0x00000400) != 0) result += "User Intervention, "; | |
| if ((status & 0x00000800) != 0) result += "Restarted, "; | |
| if ((status & 0x00001000) != 0) result += "Complete, "; | |
| if ((status & 0x00002000) != 0) result += "Retained, "; | |
| if ((status & 0x00004000) != 0) result += "Rendering Locally, "; | |
| return result.Trim().TrimEnd(','); | |
| } | |
| static void Main() | |
| { | |
| string printerName = "Microsoft Print to PDF"; // Replace with your printer's name | |
| IntPtr hPrinter; | |
| if (!OpenPrinterW(printerName, out hPrinter, IntPtr.Zero)) | |
| { | |
| Console.WriteLine("Failed to open printer. Error: " + Marshal.GetLastWin32Error()); | |
| return; | |
| } | |
| IntPtr pNotifyOptions = CreateJobNotifyOptions(); | |
| IntPtr hChange = FindFirstPrinterChangeNotification( | |
| hPrinter, | |
| PRINTER_CHANGE_ADD_JOB | PRINTER_CHANGE_SET_JOB | PRINTER_CHANGE_DELETE_JOB, | |
| PRINTER_NOTIFY_CATEGORY_ALL, | |
| pNotifyOptions | |
| ); | |
| if (hChange == IntPtr.Zero || hChange == new IntPtr(-1)) | |
| { | |
| Console.WriteLine("Failed to create change notification. Error: " + Marshal.GetLastWin32Error()); | |
| ClosePrinter(hPrinter); | |
| return; | |
| } | |
| Console.WriteLine("Monitoring print job changes. Press Ctrl+C to exit..."); | |
| while (true) | |
| { | |
| uint waitResult = WaitForSingleObject(hChange, INFINITE); | |
| if (waitResult == WAIT_OBJECT_0) | |
| { | |
| if (FindNextPrinterChangeNotification(hChange, out int change, IntPtr.Zero, out IntPtr pInfo)) | |
| { | |
| Console.WriteLine($"Printer change detected: 0x{change:X}"); | |
| if (pInfo != IntPtr.Zero) | |
| { | |
| PRINTER_NOTIFY_INFO notifyInfo = Marshal.PtrToStructure<PRINTER_NOTIFY_INFO>(pInfo); | |
| IntPtr pData = pInfo + Marshal.SizeOf<PRINTER_NOTIFY_INFO>(); | |
| for (int i = 0; i < notifyInfo.Count; i++) | |
| { | |
| var data = Marshal.PtrToStructure<PRINTER_NOTIFY_INFO_DATA>(pData); | |
| ushort type = data.Type; | |
| ushort field = data.Field; | |
| Console.Write($" - Job ID: {data.Id}, Field: {field} "); | |
| if (type == JOB_NOTIFY_TYPE) | |
| { | |
| switch (field) | |
| { | |
| case JOB_NOTIFY_FIELD_DOCUMENT: | |
| case JOB_NOTIFY_FIELD_USER_NAME: | |
| string strValue = Marshal.PtrToStringUni(data.Data.pBuf); | |
| Console.WriteLine($"→ {strValue}"); | |
| break; | |
| case JOB_NOTIFY_FIELD_STATUS: | |
| uint status = data.Data.dwData; | |
| Console.WriteLine($"→ Status = 0x{status:X8} ({DecodeJobStatus(status)})"); | |
| break; | |
| default: | |
| Console.WriteLine($"→ (field type not handled)"); | |
| break; | |
| } | |
| } | |
| pData += Marshal.SizeOf<PRINTER_NOTIFY_INFO_DATA>(); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| Console.WriteLine("Failed to get next notification. Error: " + Marshal.GetLastWin32Error()); | |
| break; | |
| } | |
| } | |
| else | |
| { | |
| Console.WriteLine("Wait failed."); | |
| break; | |
| } | |
| } | |
| Marshal.FreeHGlobal(((PRINTER_NOTIFY_OPTIONS)Marshal.PtrToStructure(pNotifyOptions, typeof(PRINTER_NOTIFY_OPTIONS))).pTypes); | |
| Marshal.FreeHGlobal(pNotifyOptions); | |
| ClosePrinter(hPrinter); | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment