Windows NT Printer Control

Dr. Dobb's Journal May 1997

Control print jobs before they control you

By Paul Trout

Paul is a technology coordinator for the College of Applied Human Sciences at Colorado State University where he develops network management and control applications. He can be contacted at trout@cahs.colostate.edu.

The College of Applied Human Sciences at Colorado State University maintains four student computer labs for about 4000 undergraduate and graduate students. Each lab has a dedicated file server running Windows NT Server 3.51, and a lab operator workstation running Windows NT Workstation 3.51. Early in the spring semester, printer paper and toner usage increased sharply. In one lab, for instance, paper usage went from about 2000 sheets a week to almost 12,000 sheets. After some analysis, I determined the increase was due to many users printing unnecessary copies of documents and presentations. Often they would reprint the entire document after changing only one page. The college technology staff got together and we came up with three possible solutions:

In this article, I'll explain how to move a print job from one printer to another and present a function library to perform this and several other tasks -- listing all printers and print servers in a domain, and pausing, resuming, and deleting a print job. The function library is available electronically (see "Availability," page 3).

Note that NT's printing nomenclature is slightly different from other server and network operating systems. Under NT, a "print server" is a computer with a local printer set up to be accessed by other computers across the network. Also, a "printer" is not necessarily the actual physical device responsible for producing the printout. It's the software configuration on the print server, defining the print driver, port connection, rights and privileges, and most importantly, the actual print queue itself. Under NT, it is unnecessary to create a separate print queue and then configure a printer to service the queue. When the printer is created, NT creates the queue and assigns it to the printer.

Also note that the Win32 API has a complete set of printer and print job control functions. While I didn't use all of them, I did use the following functions:

While not in the same category as the spooler and printer control functions, CreateFile, GetFileSize, ReadFile, WriteFile, and CloseHandle are also important. I used CreateFile both for opening an existing file and creating a new one. GetFileSize returns the length of a file. ReadFile and WriteFile are the Win32 equivalents of Standard C fread and fwrite functions. CreateFile returns a file handle, but there is no equivalent CloseFile function. Instead, it falls upon the more generic CloseHandle to take a handle, determine its type, and properly close it.

From the perspective of a C programmer, these functions are safe to use. The functions that return information as either structures, arrays, or both (EnumPrinters, GetJob, AddJob) do so in a unique and well-planned manner. You pass a pointer to a buffer, the size of the buffer to the function, and a pointer to an integer variable that will hold the bytes required. If the returned data will not fit in the buffer, the call returns an error and initializes a bytes-required variable to the proper amount. You reallocate the buffer to the new size and reissue the call -- and this time, success!

This is important, because almost all of these functions return varying amounts of information, depending on the level of data requested. For example, Microsoft has defined two levels of print-job data -- JOB_INFO_1 and JOB_INFO_2 -- each described by a structure. JOB_INFO_1 has the basic print-job information: printer name, machine name, user name, document ID, datatype, status, priority, position in queue, total pages, and pages printed. Good information, but not enough for our purposes.

JOB_INFO_2 contains all of the JOB_INFO_1 fields, plus print processor, print-processor parameters, print-driver name, and a structure defining all of the special printer initialization and environment information for the job. It is this structure that indicates whether the job gets printed in landscape or portrait orientation. For moving a print job, it is crucial to have access to all of the JOB_INFO_2 data. For deleting, pausing/resuming, or changing the priority, the JOB_INFO_1 data is sufficient. By returning the amount of data required for the results, Microsoft is getting more mileage out of a single function and making our lives easier.

None of these functions return an error code, per se. Instead, they return either True or False as an indication of success or failure. However, to get a precise reason for failure, it is necessary to call the Win32 function GetLastError, which returns a 32-bit error code describing the failure. The most common error code I encountered was "Access Denied" because I opened the printer without printer-administration authority.

Print-Job Move Algorithm

The algorithm I designed to move the print job is:

1. Determine the spooled file path. Call AddJob to get the job ID, path, and filename for the destination job. Using the path and filename returned from AddJob, parse it backwards to remove the filename. The path should be identical for both the source and destination jobs. Generally, it is in the \winnt35\system32\spool\printers directory on whichever drive NT is stored.

2. Open the source job spool file using the CreateFile function. The filename is the job ID, converted to ASCII, and left padded with zeros to a length of five characters. The extension is .SPL. Use the path from step 1. Open the source file with the OPEN_EXISTING creation distribution. This ensures the call fails if the source does not already exist.

3. Open the destination spool file with the CreateFile function. Open the destination file with CREATE_NEW creation distribution to ensure failure if the file exists already.

4. Use GetFileSize to find the file length for the source, and then ReadFile and Writefile to copy the spooled data from the source to the destination.

5. Pause the destination job by calling SetJob with the JOB_CONTROL_PAUSE flag. This ensures that the job does not get printed until all is ready.

6. Use GetJob to retrieve the level-2 job data for both the source and destination jobs. Copy the user name, document name, notify name, data type, print processor, DEVMODE, priority, start time, until time, and total pages fields from source to destination to ensure correct printout.

7. Delete the source job with SetJob and the JOB_CONTROL_CANCEL flag.

8. Resume the destination job with SetJob and the JOB_CONTROL_RESUME flag.

9. Submit the destination job for actual printing with the ScheduleJob.

The Library Functions

I have written a function library for moving print jobs and the supporting tasks. At the heart of this library is a printer data structure containing the smallest amount of data necessary to work with each printer. The printer-structure fields are:

GetLocalPrinters returns an array of printer structures and the number of local printers attached to the machine; see Listing One It uses EnumPrinters with the PRINTER_ENUM_LOCAL flag to retrieve all of the local print providers. GetLocalPrinters returns a pointer to an array of printer structures with the name fields initialized and sets a variable to the number of printers found.

FreePrinterArray (Listing Two) releases all of the memory associated with the array of structures returned by GetLocalPrinters. It frees the name and spool fields for each structure before finally freeing the array of structures. This function can be called any time after GetLocalPrinters -- even before the GetLocalSpoolPaths function has been called to initialize the spool fields.

OpenAllLocalPrinters (Listing Three) uses the OpenPrinter function to open a handle for each local printer. It opens each printer with the PRINTER_ACCESS_ADMINISTER authority, so each call will fail if the user does not have the rights to administer the printer. OpenAllLocalPrinters takes a pointer to an array of printer structures and initializes the conn_used and conn fields.

CloseAllLocalPrinters (Listing Four) uses the ClosePrinter function to close each handle opened with OpenAllLocalPrinters. CloseAllLocalPrinters operates on an array of printer structures in the same manner as OpenAllLocalPrinters.

GetLocalSpoolPaths (available electronically) initializes the spool field for each printer structure in the array returned by GetLocalPrinters. It calls AddJob to create a new print job for the printer. It separates the path from the complete path and filename combination returned by AddJob and copies it to the spool field of the printer structure. Finally, it uses SetJob to delete the job. GetLocalSpoolPaths initializes the spool field of the printer structure.

GetLocalDriverNames (available electronically) initializes the driver field for each printer structure in the array returned by GetLocalPrinters. It calls GetPrinter to retrieve the level-2 printer information for the printer and then copies the pDriverName into the driver field.

MovePrintJob (available electronically) actually moves a print job from one printer to another. This function is the application of the algorithm described earlier, but before it can execute, it must determine and store the connections, spool file paths, and driver names for each printer. Thus, GetLocalPrinters, OpenAllLocalPrinters, GetLocalSpoolPaths, and GetLocalDriverNames must be called before MovePrintJob. MovePrintJob uses two single printer structures for the source and destination printer names and spool file paths. It does not modify the contents of either structure.

The library contains no static data, and while it makes extensive use of dynamic-memory allocation, the good design of the Win32 functions keeps it remarkably free of memory leaks and buffer overruns.

The functions are designed to be used in a specific order. GetLocalPrinters initializes an array of printer structures and counts the local printers. OpenAllLocalPrinters initializes a connection for each printer in the array. GetLocalSpoolPaths retrieves the path for spool files for each printer. GetLocalDriverNames retrieves the driver name for each printer. Once these four functions have been called, MovePrintJob may be used. Before the application exits, CloseAllLocalPrinters and FreePrinterArray must be called (in that order). CloseAllLocalPrinters closes the connections and FreePrinterArray releases all memory associated with the printer structure.

Limitations

The MovePrintJob function does not work on a remote printer, because the AddJob function does not work properly for a remote printer. This failure is documented in the Win32 reference manual, but no reason is given. When the call fails, GetLastError returns ERROR_INVALID_PARAMETER. Without AddJob, it is impossible to create a new print job for the destination printer and to retrieve the spool file path of the destination printer.

Because MovePrintJob cannot be used on a remote printer, it is necessary to run the program performing the move on the same machine the printers are attached to. This is not a problem in the computer labs because each lab has a Windows NT Workstation at the lab operator's desk. We will simply make this the print server and run a monitoring program on it.

If the source job has been spooled in landscape mode, MovePrintJob does not orient the destination in landscape mode. I have been unable to figure out why, but the DEVMODE structure in the level-2 printer information for the source is not initialized for landscape orientation when it should be. All of the documentation indicates the DEVMODE structure is the holder of special environment information about the print job, but it has never been initialized in over three months of testing.

Conclusions

There is a huge demand for after-market add-on tools for both Windows NT and Windows 95. While there are many offerings available for command shells and processors, security enhancements, and Internet packages, there are almost none for printer and print-job management. I hope I have demonstrated this is not due to a lack of API support, nor inadequate functional documentation.

References

All of the research necessary for this article was done with the online documentation included with Microsoft Visual C++, Professional Edition. I specifically used the Win32 Software Development Kit Function Reference and Overviews.

Acknowledgments

I would like to thank the College of Applied Human Sciences at Colorado State University for supporting this development, and my colleagues, Don Hennen and Greg Bundy, for their help in defining the problem and suggesting possible solutions.

DDJ

Listing One

/****************************************************************************** GetLocalPrinters uses EnumPrinters with the PRINTER_ENUM_LOCAL flag to
*   retrieve all of the local printers. It then counts them, allocates an 
*   array of printer structures, initializes name fields, sets numprinters, 
*   and returns a pointer to the array of printer structures.
****************************************************************************/
struct printer *GetLocalPrinters(unsigned int *numprinters) {
  char            *databuff;           /* Buffer for data */
  unsigned int     ctr;                /* FOR loop counter */
  unsigned int     cur_prt=0;          /* Which printer  */
  unsigned int     slen;               /* Scratch variable */
  struct printer  *localprns;          /* Local printers */
  DWORD            bytes_req;          /* Bytes req'd for data */
  DWORD            structs_ret;        /* # of structs returned */
  BOOL             rtc;                /* Win32 return code */

/************************************************************ * Allocate the buffer for the returned data, Call EnumPrinters * If the call fails because the bufffer is not large enough * free the buffer, reallocate it, and reissue the call ************************************************************/ databuff=(char *)calloc(1,sizeof(PRINTER_INFO_1)); rtc=EnumPrinters(PRINTER_ENUM_LOCAL,NULL,LEVEL1,databuff,\ sizeof(PRINTER_INFO_1),&bytes_req,&structs_ret); if(rtc==FALSE && bytes_req>0 && structs_ret==0) { free(databuff); databuff=(char *)calloc(bytes_req,sizeof(char)); rtc=EnumPrinters(PRINTER_ENUM_LOCAL,NULL,LEVEL1,databuff,\ bytes_req,&bytes_req,&structs_ret); } /************************************************************************ * Count the number of printers by stepping thru the returned structures, * and incrementing the count whenever the Flags field is set to * PRINTER_ENUM_ICON8, ISPRINTER is a more mnemonic name for the flag. *************************************************************************/ *numprinters=0; for(ctr=0; ctr<structs_ret; ctr++) if( ((PRINTER_INFO_1 *)(databuff)+ctr)->Flags & ISPRINTER) (*numprinters)++; /*************************************************************************** * Allocate the array of printer structures to return, and copy printer names * from returned data to appropriate fields. The cur_prt variable is req'd * because EnumPrinters call will have returned data about print objects other * than printers. Also initialize spool field to NULL so that FreePrinterArray * can be used before GetLocalSpoolPaths function has been called. ************************************************************/ localprns=(struct printer *)calloc((*numprinters),\ sizeof(struct printer)); for(ctr=0; ctr<structs_ret; ctr++) if( ((PRINTER_INFO_1 *)(databuff)+ctr)->Flags & ISPRINTER) { slen=strlen(((PRINTER_INFO_1 *)(databuff)+ctr)->pName); localprns[cur_prt].name=(char *)calloc(slen+1,\ sizeof(char)); strcpy(localprns[cur_prt].name,\ ((PRINTER_INFO_1 *)(databuff)+ctr)->pName); localprns[cur_prt].spool=NULL; localprns[cur_prt].driver=NULL; cur_prt++; } free(databuff); /* Free working buffer */ return(localprns); /* Return array of printers */ }

Back to Article

Listing Two

/*************************************************************************** FreePrinterArray frees all of the allocated space for an array of printer 
*   array of printer structures.  It frees the name and spool
*   fields, and then the entire array.
***************************************************************************/
void FreePrinterArray(struct printer *localprns, \
                      unsigned int    numprinters ) {
  unsigned int ctr;                    /* FOR loop counter */
  /************************************************************
  * Do not simply frre the array of structures.  You must also
  * free the name, spool, and driver  fields.
  ************************************************************/
  for(ctr=0; ctr<numprinters; ctr++) {
    free(localprns[ctr].name);
    if(localprns[ctr].spool!=NULL)
      free(localprns[ctr].spool);
    if(localprns[ctr].driver!=NULL)
      free(localprns[ctr].driver);
  }
  free(localprns);
}

Back to Article

Listing Three

/***************************************************************************** OpenAllLocalPrinters takes the array of printer structures, and the number
*   of printers returned by GetLocalPrinters, and opens a handle for each one 
*   with OpenPrinter.
****************************************************************************/
void OpenAllLocalPrinters(struct printer *localprns, \
                          unsigned int    numprinters) {
  unsigned int     ctr;                /* FOR loop counter */
  PRINTER_DEFAULTS opener;             /* Default behavior */
  BOOL             rtc;                /* Win32 return code */

/*************************************************************************** * Initialize the PRINTER_DEFAULTS structure. The only field to worry about * is the DesiredAccess field. If this is not set to PRINTER_ALL_ACCESS, you * will not be able to change the level 2 data on the destination job during * a move, nor will you be able to retrieve the printer driver * name with GetPrinter during the GetLocalDriverNames call. ****************************************************************************/ opener.pDatatype=NULL; opener.pDevMode=NULL; opener.DesiredAccess=PRINTER_ALL_ACCESS;

for(ctr=0; ctr<numprinters; ctr++) { rtc=OpenPrinter(localprns[ctr].name,&localprns[ctr].conn,\ &opener); if(rtc==TRUE) localprns[ctr].conn_used=TRUE; else localprns[ctr].conn_used=FALSE; } }

Back to Article

Listing Four

/*************************************************************** CloseAllLocalPrinters takes the array of printer structures,
*   and the number of printers returned by by GetLocalPrinters,
*   and closes the connection for each one with ClosePrinter.
**************************************************************/
void CloseAllLocalPrinters(struct printer *localprns, \
                          unsigned int    numprinters) {
  unsigned int     ctr;                /* FOR loop counter */
  BOOL             rtc;                /* Win32 return code */
  for(ctr=0; ctr<numprinters; ctr++) {
    if(localprns[ctr].conn_used==TRUE) {
      rtc=ClosePrinter(localprns[ctr].conn);
      if(rtc==TRUE)
        localprns[ctr].conn_used=FALSE;
      else
        localprns[ctr].conn_used=TRUE;
    }
    else
      continue;
  }
}

Back to Article


Copyright © 1997, Dr. Dobb's Journal