Skip to content

Instantly share code, notes, and snippets.

@caloni
Created July 9, 2025 14:46
Show Gist options
  • Select an option

  • Save caloni/a2f29105a80d54b6b0b30a0c7b5ca39e to your computer and use it in GitHub Desktop.

Select an option

Save caloni/a2f29105a80d54b6b0b30a0c7b5ca39e to your computer and use it in GitHub Desktop.
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