Managed Interop. – Printer ports

 

At work, i had to find a way to create new TCP/IP printer ports on a remote print server from the .NET code of my application. I couldn’t use WMI, so i had to find something else. Luckily, the XcvData Windows function does just that. Unfortunately, it is a royal pain in the ass to use and there’s not a lot of documentation available on how to use it. And i certainly didn’t find anything on how to call it from .NET code.

here is the Native way :

               HANDLE hXcv=NULL;
    DWORD dwFuncResult;
    PRINTER_DEFAULTSW Default ;//= {NULL,  NULL,SERVER_ACCESS_ADMINISTER };
    Default.pDatatype = NULL; Default.pDevMode = NULL; 
Default.DesiredAccess = SERVER_ACCESS_ADMINISTER;
                
            if (pServerName = ConstructXcvName(NULL, 
L"Local Port", L"XcvMonitor"))
            {
                dwFuncResult = OpenPrinterW((PWSTR) pServerName,
 &hXcv, &Default);
                if (dwFuncResult && hXcv!=NULL)
                {
                    DWORD dwStatus, cbInputData  =100,
cbOutputNeeded = 0;
                    PBYTE  pOutputData = new BYTE [ cbInputData];
    
                    if(XcvData(hXcv, L"AddPort", (PBYTE) m_strPortName, 
                        sizeof(WCHAR) * (wcslen(m_strPortName) + 1), // 
                        pOutputData, cbInputData , 
& cbOutputNeeded, &dwStatus))
                    {
                        if(dwStatus == ERROR_SUCCESS) 
                            {

                            } 
                else if (dwStatus == ERROR_ALREADY_EXISTS)
                    {
                        m_strErr.Format("Add Port Failed %d.
"(::GetLastError()));
                        OutputDebugString(m_strErr);
                    }
                    else
                    {
                    m_strErr.Format("Add Port Failed %d."
(::GetLastError()));
                    OutputDebugString(m_strErr);
                    }
                }
                else
                {
                    m_strErr.Format("XcvData Add Port Failed %d."
,::GetLastError()));
                       OutputDebugString(m_strErr);
                m_bReturn=FALSE;
                }

                    delete pOutputData;
                    ClosePrinter(hXcv);
                    }
                else
                    {
                        m_strErr.Format("Open printer Failed %d."
,(::GetLastError()));
                        OutputDebugString(m_strErr);
                        m_bReturn=FALSE;
                    }
            }
            else
                {
                m_strErr.Format("Open port Failed %(::GetLastError()));
                OutputDebugString(m_strErr);
                m_bReturn=FALSE;
                }
        }

Just looking at that makes me feel bad for everyone who’s ever had to code against Windows API’s. Anyway, according to the documentation, the first parameter (hXcv) should be a handle to the print server (which you can retrieve with a call to OpenPrinter), the second parameter (pszDataName) has to be “AddPort” if you want the function to create a new port. And then comes the fun part… the third parameter (pInputData) should be a pointer to a PORT_DATA_1 structure and the fourth parameter has to contain the size in bytes of the PORT_DATA_1 structure you passed as the third argument. The other parameters can be ignored (nice API design btw) except for the last one, which is an out parameter that will return a numeric code which will indicate either success or the cause of the failure.

I had a lot of problems trying to pass a pointer to a valid PORT_DATA_1 structure. The structure looks like this:

typedef struct _PORT_DATA_1 {
    WCHAR  sztPortName[MAX_PORTNAME_LEN];
    DWORD  dwVersion;
    DWORD  dwProtocol;
    DWORD  cbSize;
    DWORD  dwReserved;
    WCHAR  sztHostAddress[MAX_NETWORKNAME_LEN];
    WCHAR  sztSNMPCommunity[MAX_SNMP_COMMUNITY_STR_LEN];
    DWORD  dwDoubleSpool;
    WCHAR  sztQueue[MAX_QUEUENAME_LEN];
    WCHAR  sztIPAddress[MAX_IPADDR_STR_LEN];
    BYTE   Reserved[540];
    DWORD  dwPortNumber;
    DWORD  dwSNMPEnabled;
    DWORD  dwSNMPDevIndex;
} PORT_DATA_1, *PPORT_DATA_1;

 

As you can see, the struct contains a couple of Unicode character arrays and even a byte array. Defining a struct in C# that could be marshalled to this turned out to be the tricky part in getting this stuff to work.

But first of all, we needed to be able to call the OpenPrinter function to retrieve a handle to the print server where we need to create the new printer port:

 

First of all, the struct has to have Sequential as its LayoutKind, and each string must be marshalled as a unicode string (.NET strings are unicode by default, but when marshalled to native code they are converted to ANSI strings, so the CharSet setting is definitely required). Then, for each array in the original struct, you need to make sure our string is converted to an array of the expected size. Marshalling those strings as ByValTStr and setting the SizeConst parameter did the trick there. Then there’s the byte array in the original struct. The function expects there to be a byte array of 540 elements. Marshalling it as ByValArray and setting the SizeConst makes that work as well.

Right, now we have the structure, so we still need a way to call the XcvData function:

  [DllImport("winspool.drv", SetLastError = true, 
CharSet = CharSet.Unicode)]

public static extern int XcvDataW
(IntPtr hXcv, string pszDataName, IntPtr pInputData, UInt32 cbInputData,
 out IntPtr pOutputData, 
UInt32 cbOutputData,out UInt32 pcbOutputNeeded, out UInt32 pdwStatus);

      Notice how the DllImport attribute has its CharSet parameter set to unicode as well. If you don’t do this, the function call will crash your app (can’t even catch an exception) because it expects pszDataName to be a unicode string and as mentioned earlier, without specifying CharSet.Unicode it would’ve been marshalled to an ANSI string. Happy times.

Anyways, creating a TCP/IP printer port on a remote server is now as simple as this:

IntPtr printerHandle;

InteropStuff.PrinterDefaults defaults = 
new InteropStuff.PrinterDefaults { DesiredAccess = 
InteropStuff.PrinterAccess.ServerAdmin };
InteropStuff.OpenPrinter(@”\myPrintServer,XcvMonitor 
Standard TCP/IP Port”, out printerHandle, ref defaults);
InteropStuff.PortData portData = new InteropStuff.PortData
            {
                dwVersion = 1, // has to be 1 for some unknown reason
                dwprotocol = 1, // 1 = RAW, 2 = LPR
                dwPortNumber = 9100, // 9100 = default port for RAW, 515 for LPR
                dwReserved = 0, // has to be 0 for some unknown reason
                sztPortName = “IP_198.23.124.15″,
                sztIPAddress = “198.23.124.15″,
                sztSNMPCommunity = “public”,
                dwSNMPEnabled = 1,
                dwSNMPDevIndex = 1

            };

               uint size = (uint)Marshal.SizeOf(portData);
             portData.cbSize = size;
              IntPtr pointer = Marshal.AllocHGlobal((int)size);
               Marshal.StructureToPtr(portData, pointer, true);

try
          {
            IntPtr outputData;UInt32 outputNeeded;UInt32 status;
         InteropStuff.XcvDataW(printerHandle, “AddPort”, pointer,
size,  out outputData, 0, out outputNeeded, out status);
            }
finally
            {
InteropStuff.ClosePrinter(printerHandle);
Marshal.FreeHGlobal(pointer);

            }

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s