Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Printers and SafeHandles (Part 2)

3.00/5 (7 votes)
16 Apr 20073 min read 1   3K  
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