Channels ▼


Monitoring Distributed Printers Under Novell Netware

Source Code Accompanies This Article. Download It Now.


Jim is a senior information systems specialist for Midland Mutual Life Insurance. He can be reached there at 250 East Broad Street, Mail Stop 101, Columbus, OH 43215 or on CompuServe, 75300,1663.

The power and flexibility of printing services under Novell NetWare 3.x can create headaches for LAN administrators and operations staff used to a centralized console facility. Print servers can run on the file servers, or on dedicated computers attached to the network. Printers can be attached directly to the print server or can reside on any DOS workstation attached to the network -- or the Internet.

This article describes a program that allows an operator to monitor multiple, possibly remote printers on a single screen.

Printing Under Novell NetWare

Novell NetWare/386 (later called NetWare 3.0) included (among other improvements) the ability to print to printers attached to DOS workstations anywhere on the network. Previously, under Advanced NetWare, Versions 2.1x, printers had to be attached directly to the file server. This meant that, unless you had a third-party print spooling system, it was fairly easy for one person to physically monitor the printers. Now, however, printers can easily be spread over as far a geographic area as the network itself occupies.

The LAN administrator defines a print server on a specific file server, configuring queues (identified by name), and printers (identified by number). Printers can be defined as either local or remote, and have one or more queues attached to them. Five local and up to 16 total printers are supported per print server.

The print server software can then be loaded to run on the file server (PSERVER.NLM), or can be run from a DOS workstation logged into the file server (PSERVER.EXE). Printers defined as local must be directly connected to the computer running the print server software. Remote printers, however, are dynamic. The user controlling a remote printer runs a TSR program (RPRINTER.EXE) identifying which print server and printer number the workstation controls. A workstation can control multiple remote printers, but each printer requires its own copy of RPRINTER.

Defining the Problem, Designing the Solution

I wanted a tool that would enable our operations staff to tell with a single glance whether printing operations on the monitored printers were proceeding normally. In addition, I wanted them to be able to tell how far large print jobs (in this case, bimonthly compensation statements) were from completion. Some reports have to be released manually by the operators, so this would cut down on the amount of idle time between print jobs. The specific information required was:

  • Printer status (out-of-paper, jammed, offline, and so on)
  • Information about the active print job (number of copies, copies completed, size of each, and percent complete with this copy)
In addition, some sort of visual indicator of printer trouble was desired.

What Tools does Novell Provide?

Novell provides PCONSOLE.EXE, a program used both to create the print server and to monitor queues and printers. In addition, the PSERVER program displays summary information about its printers on the console of the machine it is running on.

The problem with PCONSOLE is that although the printer status information is provided, the operator must navigate several layers of menus to check on different printers. This is even more difficult if the printers reside on different file servers. PCONSOLE is more than sufficient if you only need to monitor a single printer. It does not allow you to monitor several printers on a single screen, however.

The NetWare C Interface for DOS

Novell does provide a means to gather the desired information. It's called the NetWare C Interface for DOS and provides 400 C procedures broken into 20 categories, including -- new to version 1.2 -- print server services. These services allow a programmer to perform most PCONSOLE functions programmatically.

For the calls to be effective, you must establish a connection with, and identify yourself to the desired print server. Print servers communicate with clients via SPX, Novell's version of Xerox's SPP protocol. A user may only be logged into a single print server at a time.

Key Design Decisions

Because of our hardware standards, all DOS workstations have a VGA color monitor. This allows me to handle interface issues without having to allow for different monitors. Interface procedures can safely be VGA-specific (see Listing One, intrface.c, page 100). Listing Two (page 100) is intrface.h, the include files.

Our network standards require all print servers to run as NLMs and are named the same as the file server they run on. I can therefore treat the file server and print server as a single entity. I can also assume that a user will request information from no more than eight print servers at a time, because the NetWare workstation shells (NETx.COM, EMSNETx.EXE, XMSNETx.EXE) allow connection to only eight file servers at a time.

For simplicity's sake, the print server names and printer numbers will be read from a file. Entries in the file are in the format <print-server-name> / <printer-number>. The file can easily be edited with any ASCII editor.

Supporting Data Structures

Information about print servers is kept in a structure called (appropriately enough) PrintServer (see Listing Three, pmon.h, page 100). (Several of the field types are defined in the Novell header file "nit.h." WORD is defined as unsigned int, and BYTE is unsigned char.) The fields in this structure are shown in Table 1.

Table 1: Fields for PrintServer structure

  Field          Description

  ConnectionID   Nonzero indicates that this structure is valid.
  Name           Null-terminated name of the print server
  SPXConnection  SPX connection number used to communicate with this print
  Error          Nonzero indicates that one of the Print Server Services
                 calls failed when attempting to attach to or login to the
                 print server.
  Printers       Head of a linked list of printer information structures

Information about individual printers is stored in a structure called Printer (see Table 2). The NetWare shell allows no more than eight concurrent connections, so print server information will be stored in an eight-element array.

Table 2: Fields for Printer structure

  Field        Description

  Printer      Printer number
  Status       Nonzero indicates that one of the Print Server
               Services calls failed when attempting to get printer
               or job status information for this printer.
  Problem      1 = offline, 2 = out of paper, other value = no trouble
  HasJob       True indicates that a job is active on the printer.
  Copies       Number of copies requested
  CopiesDone   Number of copies completed
  JobSize      Size (in bytes) of one copy
  BytesDone    Number of bytes sent so far for this copy
  PercentDone  Percent of this copy that has been sent so far
  Next         Pointer to the next printer for this print server, or

Connections: Preferred, Default, and Primary

The shell can communicate with up to eight different file servers. How, then, does the shell determine at any one time which server to send packets to? Packets are sent to file servers based on the classification of the connection to the server.

Packets will be sent to the Preferred server, if a Preferred connection has been set. If not, packets will go to the Default server, and if there is no Default server, to the Primary server.

The Preferred server must be explicitly set by calling the SetPreferredConnectionID( ) API routine.

The Default server is the server that the current default drive maps to, if the drive is a network drive. For example, if the current drive is Z: and drive Z: is mapped to file server FS1 directory SYS:PUBLIC, then FS1 is the default server.

The primary server is used only if the current default drive is not a network drive, and there is no preferred server. In this case, the primary server is the server whose entry is the lowest numbered in the shell's tables (usually the first server that the shell is connected to).

The Code: PMON.C

The program PMON.C (Listing Four, page 100) first determines whether a configuration filename was passed on the command line. If not, the default "PMON.CFG" is used. Initialize( ) is called and prints a copyright banner and initializes the array of PrintServer structures to 0. It then calls OpenConfigFile( ), which tries to open the configuration file. If it can't, it prints an error message and terminates. Initialize( ) then calls BuildServerList( ) and closes the configuration file.

BuildServerList( ) reads each line of the configuration file, ignoring blank lines and lines that begin with a semi-colon. It calls strtok( ) to return the server name, which is checked for validity. Next, the API routine GetConnection-ID( ) is called to get the connection ID for this server. If the user is not logged into the server (or the server doesn't exist), we print an error message and exit. Then strtok( ) is called again to retrieve the printer number from the line read. If strtok( ) returns NULL, or the first character returned is not a digit, an error message is issued and the program exits. Otherwise, AddPrinterToServer( ) is called. AddPrinterToServer( ) gets memory for a Printer structure and links it into the list for the proper print server.

After returning from Initialize( ), main( ) turns off the cursor and calls ScreenSetup( ), which clears the screen with a Bright White on Blue attribute and displays a copyright message, a header for the printer information, and an exit message. main( ) then loops until a key is pressed, calling MainLoop( ) and then waiting for half a second before calling MainLoop( ) again. If a key is pressed, the loop exits, the keyboard is flushed, the cursor is turned on again and placed in the lower-right corner of the screen, and the program exits to DOS.

MainLoop( ) checks each of the PrintServer array elements to see if it is valid, then calls LoginToServer( ) and walks the chain of printers for that server calling GetPrinterInformation( ) and DisplayPrinterInformation( ). MainLoop( ) then logs out of the print server by calling DetachFromServer( ). LoginToServer( ) first verifies that the entry is valid. Then SetPreferredConnection-ID( ) is called to make sure that we are communicating with the correct file server. PSAttachToPrintServer is called and establishes an SPX connection with the print server. Finally, PSLogin ToPrintServer( ) is called to finish the login process.

GetPrinterInformation( ) is passed an SPX connection and a printer structure. PSGetPrinterStatus is called, passing the SPX connection and printer number, returning the Trouble indicator (into Problem) and the HasJob flag. If the HasJob flag is true, PSGetPrintJobStatus is called, again passing the SPX connection and printer number, returning all remaining fields in the Printer structure except PercentDone, which is computed.

Display Printer Information( ) calls sprintf( ) to format the information into a string. Then the line on which this printer is to be displayed is cleared, and PutStrNoAttr( ) is called to write the string to the VGA video buffer. Finally, if the Problem flag is 1 (offline, usually indicating a jam), the attribute of the line is changed to Ox9C -- blinking Red on Blue; if the Problem is 2 (out-of-paper), the attribute is changed to 0x9E -- blinking Yellow on Blue.

Products Mentioned

Novell NetWare 3.11 Novell Inc. 112E 1700 South Provo, UT 84606 800-526-5463 $3,495

Novell NetWare C Interface -- DOS Novell Inc. Development Products Division 5918 West Courtyard Dr. Austin, TX 78730 800-733-9673 $295

Note that no error checking is done for a NULL return from calloc( ). The Printer structure is only 25 bytes, and at most there could be 128 printers (eight print servers times 16 printers per server), so the greatest amount of memory requested would be 3200 bytes.

This brings to mind two questions: Why bother with the linked lists at all? and Why not just allocate an array of 128 Printer entries and be done with it?

Well, there is a large amount of overhead required to log into a print server. Furthermore, a user can be logged into only one print server at a time. Therefore, this data structuring allows me to log into each print server once (per loop in MainLoop( )), get the information for all desired printers on this print server, and then go on to the next print server.

Finishing Up

This program was a fairly simple effort, involving the more complicated areas of NetWare programming (IPX/SPX and VAPs/NLMs) only indirectly. However, it achieved its design goal of allowing the operators to tell at a glance whether there were any problems with the printers they were monitoring.

by V. James Krammes

<a name="0264_0011">

#include <dos.h>
extern char far *screen;
void CursorOff()
    union REGS regs;
    regs.h.ah = 1; = 0x20; = 0;

void CursorOn()
    union REGS regs;
    regs.h.ah = 1; = 11; = 12;

void CursorAt(r,c)
    unsigned char r,c;
    union REGS regs;
    regs.h.ah = 02; = 0;
    regs.h.dh = r;
    regs.h.dl = c;

void _cls(attr)
    unsigned char attr;
    union REGS regs;
    regs.h.ah = 0x06;                       /* scroll window up */ = 0;                          /* clear entire window */ = attr; = 0;                          /* upper left = (0,0) */
    regs.h.dh = 23;
    regs.h.dl = 79;

void clearline(row,attr)
    unsigned char row,attr;
    union REGS regs;
    regs.h.ah = 0x06;                       /* scroll window up */ = 1;                          /* clear 1 line */ = attr; = row;                        /* upper left = (row,0) */ = 0;
    regs.h.dh = row;                        /* lower right = (row,79) */
    regs.h.dl = 79;

void PutStrNoAttr(row,col,str,len)
    unsigned char row,col;
    int len;
    char *str;
    register int offset = (row * 160) + (col << 1);
    while (len--) {
        *(screen + offset) = *str++;
        offset += 2;

void putattr(row,col,attr,cnt)
    unsigned char row,col,attr;
    int cnt;
    register int offset = (row * 160) + (col << 1) + 1;
    while (cnt--) {
        *(screen + offset) = attr;
        offset += 2;

<a name="0264_0012">
<a name="0264_0013">
<a name="0264_0013">

void CursorOff(void);
void CursorOn(void);
void CursorAt(unsigned char,unsigned char);
void _cls(unsigned char);
void clearline(unsigned char,unsigned char);
void PutStrNoAttr(unsigned char,unsigned char,char *,int);
void putattr(unsigned char,unsigned char,unsigned char,int);

<a name="0264_0014">
<a name="0264_0015">
<a name="0264_0015">

/* PMON.H - header file for PMON: printer monitor. */

/** define structures **/
typedef struct _Printer {
            BYTE        Printer;
            int     Status;
            BYTE        Problem;
            BYTE        HasJob;
            WORD        Copies;
            WORD        CopiesDone;
            long        JobSize;
            long        BytesDone;
            float       PercentDone;
            struct _Printer *next;
} Printer;

typedef struct _PrintServer {
            WORD        ConnectionID;
            char        Name[48];
            WORD        SPXConnection;
            int     Error;
            Printer     *Printers;
} PrintServer;

<a name="0264_0016">
<a name="0264_0017">
<a name="0264_0017">

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <nit.h>
#include <npt.h>
#include <string.h>
#include "pmon.h"
#include "intrface.h"

char ConfigFileName[128];
FILE *ConfigFile;

unsigned DelayValue = 500;

PrintServer PS[8];
Printer     *P;

char far *screen;

void OpenConfigFile(void)
    ConfigFile = fopen(ConfigFileName,"r");
    if (!ConfigFile) {
           printf("Unable to open configuration file \"%s\"\n",ConfigFileName);

void strrep(char *s,char c1,char c2)
    while (*s) {
        if (*s == c1)
            *s = c2;

void AddPrinterToServer(int psnum,BYTE prnum)
    register Printer *p = PS[psnum].Printers;
    if (!p) {
        PS[psnum].Printers = calloc(1,sizeof(Printer));
        PS[psnum].Printers->Printer = prnum;
    } else {
        while (p->next)
            p = p->next;
        p->next = calloc(1,sizeof(Printer));
        p = p->next;
        p->Printer = prnum;

void BuildServerList(void)
    char buf[256];
    char *ptr;
    int status;
    WORD ConnectionID;
    while (strlen(buf)) {
        if (buf[0] != ';' && strlen(buf)) {
            ptr = strtok(buf,"/");
            if (!ptr || strlen(ptr) > 47) {
                          printf("Error: expected <printserver>/<printer#>\n");
              printf("       found \"%s\"\n",buf);
            status = GetConnectionID(ptr,&ConnectionID);
            if (status) {
         printf("Error: You are not logged in to server \"%s\"\n",ptr);
            if (!PS[ConnectionID-1].ConnectionID) {
                PS[ConnectionID-1].ConnectionID = ConnectionID;
            ptr = strtok(NULL," \n\t;");
            if (!ptr || !isdigit(*ptr)) {
              printf("Error: expected <printserver>/<printer#>\n");
              printf("       found \"%s\"\n",buf);

void Initialize()
    register int i;
  printf("PMON 1.00 - (C) 1991 The Midland Mutual Life Insurance Company\n\n");
    screen = (char far *) MK_FP(0xB800,0x0000);
    for (i=0; i < 8; i++)
        memset((char *) &PS[i],0,sizeof(PrintServer));

void ScreenSetup(void)
    static char *title =
        "(C) 1991 The Midland Mutual Life Insurance Company";
    static char *header =
        "  Server  /P#   Status   #Copies   Size of 1   Done So Far  Percent";
    static char *footer =
        "Press any Key to Exit";
    PutStrNoAttr(0,(80-strlen(title)) >> 1,title,strlen(title));
    PutStrNoAttr(23,(80-strlen(footer)) >> 1,footer,strlen(footer));

void LoginToServer(int i)
    register int status;
    if (PS[i].ConnectionID) {
               status = PSAttachToPrintServer(PS[i].Name,&PS[i].SPXConnection);
        if (status)
            PS[i].Error = status;
        else {
               status = PSLoginToPrintServer(PS[i].SPXConnection,&CAL);
            if (status)
                PS[i].Error = status;

void DetachFromServer(int i)
    if (PS[i].ConnectionID)

void GetPrinterInformation(WORD spxid,Printer *p)
    char sdummy[128];
    BYTE dummy;
    WORD wdummy;
    register int status;
    status = PSGetPrinterStatus(spxid,p->Printer,&dummy,&(p->Problem),
    p->Status = status;
    if (p->HasJob) {
        if (p->JobSize == 0.0 || p->BytesDone == 0.0)
            p->PercentDone = 0.0;
        p->PercentDone = (float) (p->BytesDone) / (float) (p->JobSize) * 100.0;
char *TroubleDescription(BYTE code)
    static char *Desc[] = { "   OK   ",
                " JAMMED ",
                "No Paper" };
    if (code == 1)
        return Desc[1];
    else if (code == 2)
        return Desc[2];
        return Desc[0];
char *CommaString(long num)
    static char buf2[15];
    char buf1[15];
    register int p1,p2;
    int positions = 0;
    p1 = strlen(buf1);
    p2 = (p1 > 9) ? p1 + 3 : (p1 > 6) ? p1 + 2 : (p1 > 3) ? p1 + 1 : p1;
    while (p1 >= 0) {
        buf2[p2--] = buf1[p1--];
        if (++positions % 3 == 0)
            buf2[p2--] = ',';
    return buf2;

void DisplayPrinterInformation(PrintServer *ps,Printer *p,int row)
    char buf[80],s1[15],s2[15];
    if (row <= 21) {
        if (ps->Error)
            sprintf(buf,"%-10s/%02d  Error #%d",ps->Name,
        else if (p->Status)
            sprintf(buf,"%-10s/%02d  Error #%d",ps->Name,
        else if (!p->HasJob)
            sprintf(buf,"%-10s/%02d  %-8s",ps->Name,p->Printer,
        else {
                sprintf(buf,"%-10s/%02d  %-8s   %02d/%02d   %11s  %11s  %5.2f",
        if (p->Problem == 1)
        else if (p->Problem == 2)

void MainLoop(void)
    register Printer *p;
    register int i;
    int CurrentRow = 4;
    for (i=0; i<8; i++)
        if (PS[i].ConnectionID) {
            p = PS[i].Printers;
            while (p) {
                p = p->next;

main(int argc,char *argv[])
    if (argc == 1)
    else if (argc == 2)
    else {
        printf("Usage - PMON [<config-file>]\n");
    while (!kbhit()) {
    if (!getch())
    return 0;

Copyright © 1991, Dr. Dobb's Journal

Related Reading

More Insights

Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.