Creating remote TCP/IP printer ports from code
Posted by Davy Brion on 27th February 2008
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. So after wasting about 2 days on trying to get this to work, i figured i might as well put the solution online in case anyone needs to do this from .NET code again.
This is the signature of the function:
BOOL WINAPI XcvData(HANDLE hXcv, LPCWSTR pszDataName, PBYTE pInputData, DWORD cbInputData,
PBYTE pOutputData, DWORD cbOutputData, PDWORD pcbOutputNeeded, PDWORD pdwStatus);
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:
public enum PrinterAccess
{
ServerAdmin = 0×01,
ServerEnum = 0×02,
PrinterAdmin = 0×04,
PrinterUse = 0×08,
JobAdmin = 0×10,
JobRead = 0×20,
StandardRightsRequired = 0×000f0000,
PrinterAllAccess = (StandardRightsRequired | PrinterAdmin | PrinterUse)
}
[StructLayout(LayoutKind.Sequential)]
public struct PrinterDefaults
{
public IntPtr pDataType;
public IntPtr pDevMode;
public PrinterAccess DesiredAccess;
}
[DllImport("winspool.drv", SetLastError = true)]
public static extern int OpenPrinter(string printerName, out IntPtr phPrinter,
ref PrinterDefaults printerDefaults);
[DllImport("winspool.drv", SetLastError = true)]
public static extern int ClosePrinter(IntPtr phPrinter);
Allright, now can retrieve the handle with a call to OpenPrinter and we can cleanup afterwards by passing the handle to the ClosePrinter function.
Now we need a C# definition of a struct that can be marshalled to a PORT_DATA_1 struct:
public const int MAX_PORTNAME_LEN = 64;
public const int MAX_NETWORKNAME_LEN = 49;
public const int MAX_SNMP_COMMUNITY_STR_LEN = 33;
public const int MAX_QUEUENAME_LEN = 33;
public const int MAX_IPADDR_STR_LEN = 16;
public const int RESERVED_BYTE_ARRAY_SIZE = 540;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct PortData
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PORTNAME_LEN)]
public string sztPortName;
public UInt32 dwVersion;
public UInt32 dwProtocol;
public UInt32 cbSize;
public UInt32 dwReserved;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_NETWORKNAME_LEN)]
public string sztHostAddress;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_SNMP_COMMUNITY_STR_LEN)]
public string sztSNMPCommunity;
public UInt32 dwDoubleSpool;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_QUEUENAME_LEN)]
public string sztQueue;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_IPADDR_STR_LEN)]
public string sztIPAddress;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = RESERVED_BYTE_ARRAY_SIZE)]
public byte[] Reserved;
public UInt32 dwPortNumber;
public UInt32 dwSNMPEnabled;
public UInt32 dwSNMPDevIndex;
}
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 = “DBR_172.30.164.15″,
sztIPAddress = “172.30.164.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);
}
I don’t wanna look at any Windows functions for at least a couple of months
Tags: .NET Interop, P/Invoke
Posted in Software Development | 7 Comments »