Click here to Skip to main content
15,878,814 members
Articles / Programming Languages / C#
Article

Printers and SafeHandles (Part 2)

Rate me:
Please Sign up or sign in to vote.
3.00/5 (7 votes)
16 Apr 20073 min read 104.8K   3K   36   24
Using SafeHandles to monitor a printer.

Sample Image

Introduction

A component that can change default printer settings should also be able to watch if some program or other is changing these settings. Even in the extreme case that someone deletes the printer, the handle of this watcher should be closed properly.

The easiest way of handling this, would be deriving a class from a SafeWaitHandle and overriding the ReleaseHandle function to close the printer change notification. But, like most classes in the System.Threading namespace, the SafeWaitHandle is sealed. I first tried to write a non-sealed version of a SafeWaitHandle, but I got stuck in Mutexes, ThreadAffinities, and LsaPolicies. It is much easier to wrap another SafeHandle around the WaitHandle.

A SafeWaitPrinterHandle

To obtain notification changes of a printer or a print server, you call the Windows function FindFirstPrinterChangeNotification. This handle has to be closed with the FindClosePrinterChangeNotification function.

C#
sealed class qSafeWaitPrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    private ManualResetEvent _ManualResetEvent;
    private RegisteredWaitHandle _PrinterChangeNotification;
    
    internal qSafeWaitPrinterHandle(qSafePrinterHandle SafePrinterHandle)
        : base(true)
    {
        // do not even try if our printer is invalid
        if (SafePrinterHandle.IsInvalid) return;
        this.handle = FindFirstPrinterChangeNotification(SafePrinterHandle, 
                      (uint)Printer_Change.ALL, 0,  
                      new qPrinterNotifyOptions(false));
    }

    protected override bool ReleaseHandle()
    {
        // if we have a valid handle
        if (!base.IsInvalid)
        {
            if (!FindClosePrinterChangeNotification(base.handle))
                return false;
            // the function succeeded, so we reset our handle.
            base.handle = IntPtr.Zero;
        }
        return true;
    }
}

Monitoring a PrintServer

Before using the qSafeWaitPrinterHandle, we have to explain the difference between a printer and a print server. A server has forms, ports, drivers, ... and of course, printers and print jobs.

Server Properties

Printers use this information to print. If you add a port or form on the print server, all printers will be capable of using that port or form. It is not possible to add a form to only one printer. Therefore, if we want to monitor printer changes, we have to monitor the printer and also the server of this printer.

This is done by changing the second parameter of the FindFirstPrinterChangeNotification function. One can opt for a combination of the following fields:

C#
public enum Printer_Change : uint
{
    ADD_PRINTER =                 0x00000001,
    SET_PRINTER  =                0x00000002,
    DELETE_PRINTER =              0x00000004,
    FAILED_CONNECTION_PRINTER  =  0x00000008,
    ADD_JOB =                     0x00000100,
    SET_JOB =                     0x00000200,
    DELETE_JOB =                  0x00000400,
    WRITE_JOB =                   0x00000800,
    ADD_FORM =                    0x00010000,
    SET_FORM =                    0x00020000,
    DELETE_FORM =                 0x00040000,
    ADD_PORT =                    0x00100000,
    CONFIGURE_PORT =              0x00200000,
    DELETE_PORT =                 0x00400000,
    ADD_PRINT_PROCESSOR =         0x01000000,
    DELETE_PRINT_PROCESSOR =      0x04000000,
    ADD_PRINTER_DRIVER =          0x10000000,
    SET_PRINTER_DRIVER =          0x20000000,
    DELETE_PRINTER_DRIVER =       0x40000000,
    TIMEOUT =                     0x80000000,
    ALL =                         0x7777FFFF
}

The information returned is limited: if there is a new print job, we will receive a signal that a job is added, no more. Which printer, which user, how many pages, ..., this information has to be queried otherwise.

Monitoring a Printer

If we want detailed information about which printer added what job, we have to fine-tune the fourth parameter of the FindFirstPrinterChangeNotification function. Here, we have to specify in an array which printer and/or job changes we want to monitor:

C#
[StructLayout(LayoutKind.Sequential)]
sealed class qPrinterNotifyOptionsType
{
    public short wPrinterType;
    public short wPrinterReserved0;
    public int dwPrinterReserved1;
    public int dwPrinterReserved2;
    public int PrinterFieldCount;
    public IntPtr pPrinterFields;

    public short wJobType;
    public short wJobReserved0;
    public int dwJobReserved1;
    public int dwJobReserved2;
    public int JobFieldCount;
    public IntPtr pJobFields;

    public qPrinterNotifyOptionsType()
    {
        this.wPrinterType = 0;
        this.PrinterFieldCount = 20;
        this.pPrinterFields = Marshal.AllocCoTaskMem(42);

        Marshal.WriteInt16(this.pPrinterFields, 0, 
                          (short)Printer_Notify_Field_Indexes.PRINTER_NAME);
        Marshal.WriteInt16(this.pPrinterFields, 2, 
                          (short)Printer_Notify_Field_Indexes.SHARE_NAME);
        Marshal.WriteInt16(this.pPrinterFields, 4, 
                          (short)Printer_Notify_Field_Indexes.PORT_NAME);
        ...
        Marshal.WriteInt16(this.pPrinterFields, 40, 
                          (short)Printer_Notify_Field_Indexes.OBJECT_GUID);
        this.wJobType = 1;
        this.JobFieldCount = 22;
        this.pJobFields = Marshal.AllocCoTaskMem(46);
        Marshal.WriteInt16(this.pJobFields, 0, 
                          (short)Job_Notify_Field_Indexes.PRINTER_NAME);
        Marshal.WriteInt16(this.pJobFields, 2, 
                          (short)Job_Notify_Field_Indexes.MACHINE_NAME);
        ...
        Marshal.WriteInt16(this.pJobFields, 44, 
                          (short)Job_Notify_Field_Indexes.BYTES_PRINTED);
    }

    ~qPrinterNotifyOptionsType()
    {
        Marshal.FreeCoTaskMem(this.pJobFields);
        Marshal.FreeCoTaskMem(this.pPrinterFields);
    }
}

The SafeWaitHandle

To actually start monitoring our printer, we have to register a delegate and provide a callback function. We can use a ManualResetEvent and a RegisteredWaitHandle.

C#
sealed class qSafeWaitPrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    private ManualResetEvent _ManualResetEvent;
    private RegisteredWaitHandle _RegisteredWaitHandle;
    ...

    internal qSafeWaitPrinterHandle(qSafePrinterHandle SafePrinterHandle)
    : base(true)
    {
        // if the handle to our printer is not valid, we do not even try.
        if (SafePrinterHandle.IsInvalid) return;
        // get a handle to a waiting object,
        // providing the fields we are interested in
        this.handle = FindFirstPrinterChangeNotification(SafePrinterHandle, 
                     (uint)Printer_Change.ALL, 0, 
                     new qPrinterNotifyOptions(false));
        if (!this.IsInvalid)
        {
            // create a new ManualResetEvent            
            _ManualResetEvent = new ManualResetEvent(false);
            // assign the handle to the ManualResetEvent
            _ManualResetEvent.SafeWaitHandle = 
                        new SafeWaitHandle(this.handle,true);
            // start waiting
            _RegisteredWaitHandle = 
              ThreadPool.RegisterWaitForSingleObject(_ManualResetEvent, 
              new WaitOrTimerCallback(PrinterNotifyWaitCallback), 
                                      null, -1, true);
        }
        else
            System.Windows.Forms.MessageBox.Show("Invalid handle");
    }
    
    protected override bool ReleaseHandle()
    {
        // if we have a valid handle
        if (this.IsInvalid)
    {
            if (!this.IsInvalid)
            {
                // Unregister the change notification
                _RegisteredWaitHandle.Unregister(_ManualResetEvent);
                // Close the handle to our waiting object
                if (!FindClosePrinterChangeNotification(this.handle))
                    // If the closing failes,
                    // the ReleaseHandleFailed MDA will be activated
                    return false;
                // The function succeeded, so no double work
                _ManualResetEvent.SafeWaitHandle.SetHandleAsInvalid();
                // close the ManualResetEvent
                _ManualResetEvent.Close();
            }
            return true;
        }
    }
}

The Callback Function

When the wait handle is signaled, some steps have to be taken:

  • Check if we have a valid handle
  • Call the Windows function FindNextPrinterChangeNotification
  • The print server:
    • Check if we have a server change or not
    • What change took place on the server
  • The printer:
    • Check if we have a printer change or not
    • Read how many fields are updated
    • Read for each field, the type: Job Changed or Printer Changed
    • If the Job is changed, read which Job number
    • Read for each field what has changed
    • Read for each field the data that changed
    • Free the NotifyInfo
  • Start waiting all over again
C#
private void PrinterNotifyWaitCallback(object state, bool timedOut)
{
    // Check if we have a valid handle
    if (!this.IsInvalid)
    {
        IntPtr pni = new IntPtr();
        uint uuu = 0;
        string sss;
        if (qStatic.FindNextPrinterChangeNotification(this, 
            out uuu, IntPtr.Zero, ref pni) != 0)
        {
            // Server Notification?
            if (uuu != 0)
            {
                sss = "Server Changed : ";
                if ((uuu & (uint)Printer_Change.ADD_FORM) == 
                    (uint)Printer_Change.ADD_FORM;
                    sss += "Form Added ";
                if ((uuu & (uint)Printer_Change.ADD_PORT) == 
                    (uint)Printer_Change.ADD_PORT;
                    sss += "Port Added ";
                ...
                if ((uuu & (uint)Printer_Change.WRITE_JOB) == 
                    (uint)Printer_Change.WRITE_JOB;
                    sss += "Job Written ";
                System.Diagnostics.Debug.WriteLine(sss);    
            }

            if (pni != IntPtr.Zero)
            {
                // How many fields are intitialized?
                int count = Marshal.ReadInt32(pni, 
                                   (PrintComponent.IntPtrSize * 2));
                int jobid;
                int nnn;
                // For each initialized field
                for (int ii = 0; ii < count; ii++)
                {
                    // reset values
                    sss = "";
                    nnn = 0;
                    // calculate start offset
                    int j = (PrintComponent.IntPtrSize * 5) * ii;
                    // Printer or Job change
                    short type = Marshal.ReadInt16(pni, 
                                    (j + (PrintComponent.IntPtrSize * 3)));
                    // Field that is changed
                    short field = Marshal.ReadInt16(pni, 
                                  (j + (PrintComponent.IntPtrSize * 3) + 2));
                    if (type == 0) // Printer Notification
                    {
                        Printer_Notify_Field_Indexes fi = 
                                      (Printer_Notify_Field_Indexes)field;
                        sss = fi.ToString() + " ";
                        switch (field)
                        {
                          case (short)Printer_Notify_Field_Indexes.PRINTER_NAME:
                            // read a string
                            sss += 
                              Marshal.PtrToStringUni(Marshal.ReadIntPtr(pni, 
                              (j + (PrintComponent.IntPtrSize * 7))));
                            break;
                          case (short)Printer_Notify_Field_Indexes.DEVMODE:
                            // read a IntPtr to a devmode
                            DevMode d = new DevMode(Marshal.ReadIntPtr(pni, 
                              (j + (PrintComponent.IntPtrSize * 7))));
                            break;
                          case (short)Printer_Notify_Field_Indexes.ATTRIBUTES:
                            // read an integer
                            nnn = Marshal.ReadInt32(pni, (j + 
                              (PrintComponent.IntPtrSize * 6)));
                            sss += nnn.ToString();
                            break;
                          case (short)Printer_Notify_Field_Indexes.START_TIME:
                            // read an integer and convert it to a timespan
                            nnn = Marshal.ReadInt32(pni, 
                                  (j + (PrintComponent.IntPtrSize * 6)));
                            TimeSpan ts = new TimeSpan(nnn / 60, nnn % 60, 0);
                            sss += ts.ToString();
                            break;
                          ...
                        }
                        System.Diagnostics.Debug.WriteLine("Printer : " + 
                                                           sss);
                    }
                    if (type == 1)  //Job Notifiction
                    {
                        Job_Notify_Field_Indexes fi = 
                                (Job_Notify_Field_Indexes)field;
                        sss = fi.ToString() + " ";
                        switch (field)
                        {
                          case (short)Job_Notify_Field_Indexes.PRINTER_NAME:
                            sss += 
                              Marshal.PtrToStringUni(Marshal.ReadIntPtr(pni, 
                              (j + (PrintComponent.IntPtrSize * 7))));
                            break;
                          case (short)Job_Notify_Field_Indexes.DEVMODE:
                            DevMode d = new DevMode(Marshal.ReadIntPtr(pni, 
                              (j + (PrintComponent.IntPtrSize * 7))));
                            sss += "Changed";
                            break;
                          case (short)Job_Notify_Field_Indexes.STATUS:
                            nnn = Marshal.ReadInt32(pni, 
                                  (j + (PrintComponent.IntPtrSize * 6)));
                            Job_Status stat = (Job_Status)nnn;
                            sss += stat.ToString();
                            break;
                          ...
                        }
                        jobid = Marshal.ReadInt32(pni, 
                                (j + (PrintComponent.IntPtrSize * 5)));
                        System.Diagnostics.Debug.WriteLine("Job : " + 
                                           jobid.ToString() + " " + sss);
                    }
                // Free the info
                FreePrinterNotifyInfo(pni);
            }
        }
        // Start waiting all over again
        _RegisteredWaitHandle = 
          ThreadPool.RegisterWaitForSingleObject(
              (ManualResetEvent)_ManualResetEvent, 
               new WaitOrTimerCallback(PrinterNotifyWaitCallback), 
               null, -1, true);
    }
}

Final Steps

We still have to update our qSafePrinterHandle to properly initialize and close the qSafeWaitPrinterHandle

C#
public sealed class qSafePrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    ...
    private qSafeWaitPrinterHandle _SafeWaitPrinterHandle;
    private qPrinterDefaults printerDefaults = new qPrinterDefaults(true);

    internal qSafePrinterHandle(string printername)
        : base(true)
    {
        if (_SafeWaitPrinterHandle != null)
            _SafeWaitPrinterHandle.Close();
        if (OpenPrinter(printername, out this.handle, ref printerDefaults))
            _SafeWaitPrinterHandle = new qSafeWaitPrinterHandle(this);
    }

    public string PrinterName
    {
        ...
        set
        {
            if (value != PrinterName)
            {
                this.ReleaseHandle();
                if (OpenPrinter(value, out this.handle, ref _PrinterDefaults))
                    _SafeWaitPrinterHandle = new qSafeWaitPrinterHandle(this);
            }
        }
    }

    protected override bool ReleaseHandle()
    {
       if (_SafeWaitPrinterHandle !=null)
           _SafeWaitPrinterHandle.Close();
       // Only close printer if handle is valid
       if (IsInvalid) 
            return true;
       // If the closing failes, the ReleaseHandleFailed MDA will be activated
       if (!qStatic.ClosePrinter(this.handle))
            return false;
       this.SetHandle(IntPtr.Zero);
       return true; 
    }

}

Printing a local document produces the following output:

// Pause Printer
Printer : STATUS 1
// Print Document
Printer : CJOBS 1
Job : 2 PRINTER_NAME Adobe PDF
Job : 2 MACHINE_NAME \\QUIENSABE
Job : 2 PORT_NAME 
Job : 2 USER_NAME Quiensabe
Job : 2 NOTIFY_NAME Quiensabe
Job : 2 DATATYPE NT EMF 1.008
Job : 2 PRINT_PROCESSOR WinPrint
Job : 2 PARAMETERS 
Job : 2 DRIVER_NAME Adobe PDF Converter
Job : 2 DEVMODE Changed
Job : 2 STATUS SPOOLING
Job : 2 STATUS_STRING 
Job : 2 DOCUMENT qSafeWaitPrinterHandle.cs
Job : 2 PRIORITY 1
Job : 2 POSITION 1
Job : 2 SUBMITTED 1/08/2006 12:39:01
Job : 2 START_TIME 0
Job : 2 UNTIL_TIME 0
Job : 2 TIME 0
Job : 2 TOTAL_PAGES 0
Job : 2 PAGES_PRINTED 0
Job : 2 TOTAL_BYTES 0
Job : 2 TOTAL_PAGES 1
Job : 2 TOTAL_BYTES 111872
Job : 2 TOTAL_PAGES 6
Job : 2 TOTAL_BYTES 632268
Job : 2 STATUS 0
// Resume Printer
Printer : STATUS 0
Job : 2 STATUS 0
Job : 2 PORT_NAME My Documents\*.pdf
Job : 2 STATUS PRINTING
Job : 2 PAGES_PRINTED 0
Job : 2 PAGES_PRINTED 0
Job : 2 PAGES_PRINTED 0
Job : 2 PAGES_PRINTED 1
Job : 2 PAGES_PRINTED 1
Job : 2 PAGES_PRINTED 2
Job : 2 PAGES_PRINTED 3
Job : 2 PAGES_PRINTED 3
Job : 2 PAGES_PRINTED 4
Job : 2 PAGES_PRINTED 5
Job : 2 PAGES_PRINTED 5
Job : 2 STATUS DELETING, PRINTING, PRINTED
Job : 2 STATUS DELETING, PRINTING, PRINTED
Job : 2 STATUS DELETING, PRINTING, PRINTED
Job : 2 PORT_NAME 
Job : 2 STATUS DELETING, PRINTED
Job : 2 PAGES_PRINTED 6
Printer : CJOBS 0
Job : 2 STATUS DELETING, PRINTED, DELETED

History

  • 5 August 2006: Initial version
  • 1 September 2006: Source code added
  • 13 April 2007: Network support added and code updated

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer
Belgium Belgium
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionThanks for posting! Pin
Wes Jones27-Aug-14 14:41
Wes Jones27-Aug-14 14:41 
Generalc# service on printers Pin
balu1234530-Jul-08 7:46
balu1234530-Jul-08 7:46 
GeneralRe: c# service on printers Pin
quiensabe31-Jul-08 16:40
quiensabe31-Jul-08 16:40 
GeneralNetwork printer notifications Pin
88Keys24-Sep-07 13:25
88Keys24-Sep-07 13:25 
GeneralRe: Network printer notifications Pin
quiensabe4-Oct-07 18:46
quiensabe4-Oct-07 18:46 
QuestionMonitor multiple printers? Pin
88Keys29-Aug-07 9:49
88Keys29-Aug-07 9:49 
AnswerRe: Monitor multiple printers? Pin
quiensabe30-Aug-07 11:20
quiensabe30-Aug-07 11:20 
QuestionRe: Monitor multiple printers? Pin
88Keys4-Sep-07 22:44
88Keys4-Sep-07 22:44 
GeneralRe: Monitor multiple printers? Pin
88Keys10-Sep-07 11:52
88Keys10-Sep-07 11:52 
AnswerRe: Monitor multiple printers? Pin
quiensabe11-Sep-07 8:20
quiensabe11-Sep-07 8:20 
GeneralRe: Monitor multiple printers? Pin
88Keys11-Sep-07 10:54
88Keys11-Sep-07 10:54 
AnswerRe: Monitor multiple printers? Pin
quiensabe13-Sep-07 12:05
quiensabe13-Sep-07 12:05 
GeneralSource Code Pin
quiensabe21-Feb-07 7:15
quiensabe21-Feb-07 7:15 
GeneralRe: Source Code Pin
vensou25-Feb-07 12:27
vensou25-Feb-07 12:27 
GeneralRe: Source Code Pin
Alexsander Antunes14-Mar-07 2:42
professionalAlexsander Antunes14-Mar-07 2:42 
QuestionRe: Source Code Pin
WH3029-Mar-07 10:57
WH3029-Mar-07 10:57 
GeneralSource Code Pin
quiensabe8-Oct-06 19:42
quiensabe8-Oct-06 19:42 
GeneralSource Code Pin
gurujimani28-Aug-06 16:34
gurujimani28-Aug-06 16:34 
GeneralRe: Source Code Pin
Ri Qen-Sin31-Aug-06 20:13
Ri Qen-Sin31-Aug-06 20:13 
GeneralRe: Source Code Pin
Super Lloyd8-Oct-06 19:10
Super Lloyd8-Oct-06 19:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.