/*****************************************************************************/
/*
                                HyperSpi.c

A CGI-compliant script.

This application generates VMS System Performance Information HTML documents.  
Coupled with 'HyperSpi$agent.c', the data-collection application, it can 
profile, complete with graphics, fundamental system performance indicators as 
CPU usage, memory usage, IO values.

It operates in two distinct modes, text and graphic.  In text mode it returns 
an HTML stream to the browser comprising a hypertext page, with selection 
menu, node performance presentation page, data listing or dump page.  In 
graphic mode it returns a GIF image to the browser, first processing the 
specified data into an in-memory bitmap graph, then sending this image to the 
client via an internal GIF processor. 

A node preformance presentation page may have one or more HTML <IMG...> tags 
within it.  Each of these tags will separately cause the browser to invoke the 
application to generate a graphic from the data specified in the SRC= URL. 

If the /NODES= qualifier specifies a comma-separated list of node names these 
are used in the menus for listing the available nodes.  If this is not 
supplied the data directory is searched for current-day data files, those 
found have the respective node name extacted and these are used to list the 
nodes. 

Page colouration may be specified via the appropriate command-line qualifiers
(or corresponding logical name). Defaults for any not specified.  Specify as
/WHATEVER="" to NOT specify the corresponding colour (i.e. leave it to the
browser). See "Qualifiers" section below, and also about the logical name
"HYPERSPI$PARAM".

An example of changing the page colour to white and the banner to red!

  /PBGCOLOR="#ffffff" /PHBGCOLOR="#ff0000"

The script can format a page in either of two layouts.

  1. Tables are used to create a coloured header and button bar (DEFAULT).
     Default colours are white page with grey heading and button outlines.
  2. Textual header, horizontal rules and a textual button bar.
     No default colours.

Button labels are customizable (potentially to non-English language). They
comprise a label, equate symbol and URL-style path suitable for creating a
link. Multiple buttons are separated using the semicolon. Note that any such
button customization must provide escaped HTML-forbidden characters in the
button label and URI-forbidden characters in the path! The backslash
character, "\", escapes characters, including the button-delimitting "=" and
";". There are defaults, see DEFAULT_BUTTONS.

Here is an example of changing the button labels:

  /BUTTON="Selector;Help=/hyperspi/-/hyperspi.html"

Always have the equivalent of "Close" for the first button!  Additional buttons
may be created by adding "label=path;" elements to the button string.  In this
way an additional information page could be referenced as follows:

  /BUTTON="Selector;Help=/hyperspi/-/hyperspi.html;Other VMS=/vms/"

DIFFICULTY FITTING ALL THESE QUALIFIERS INTO A COMMAND LINE OR LOGICAL?
Use an existing, or create a new, DCL wrapper procedure for the script (same
name and directory) and build up a DCL symbol to provide the information. Up
to 1024 characters can be included in this way. For example:

  $ HYPERSPI$PARAM = "/BUTTON=""Close;Help=/hyperspi/-/hyperspihelp.html"""
  $ HYPERSPI$PARAM = HYPERSPI$PARAM + "/PBGCOLOR/PLINK/PVLINK"
  $ RUN HT_EXE:HYPERSPI


OSU ENVIRONMENT
---------------
Script responses are returned in OSU "raw" mode; the script taking care of the
full response header and correctly carriage-controlled data stream, text or
binary!!  Uses the CGILIB.C to engage in the dialog phase generating, storing
and then making available the equivalent of CGI variables.


"VANILLA" CGI ENVIRONMENT
-------------------------
Primarily for the likes of Netscape FastTrack.  This environment can accomodate
CGI variables that are not prefixed with "WWW_" and do not supply "KEY_xxxxx"
or "FORM_xxxxx" (which must be derived from "QUERY_STRING").  Full HTTP stream
(non-parsed header) is assumed as not supported so all output occurs with a
CGI-compliant header line (e.g. "Status: 200 Success") and record-oriented
output.


CGI VARIABLES
-------------
FORM_BUFFERED_IO        if non-null then buffered IO requested
FORM_CPU        if non-null then cpu usage requested
FORM_DO         what to do(!): "DUMP", "GRAPH", "LIST", "PAGE", "MENU"
FORM_DAY        the (first) day of the data file(s)
FORM_DIRECT_IO  if non-null then direct IO requested
FORM_EXTRACT    if non-null provide a link to allow graph extraction
FORM_HOUR       the (first) hour of the data
FORM_INCLUDE    additional category: PEAK, TOTAL (only with "user-mode-cpu"),
                                     HARD-FAULTS (only with "faults")
FORM_LIST_NODE  name of node to process data (see also FORM_NODE)
FORM_MINUTE     the (first) minute of the data
FORM_MONTH      the (first) month of the data file(s)
FORM_MSCP_IO    if non-null then MSCP IO requested
FORM_NODE       name of node to process data (used before FORM_LIST_NODE)
FORM_PERIOD     standard period (e.g. "until" now, "business", "today",
                "yesterday", or since a number of days)
FORM_TODAY      the last day of the data file(s)
FORM_TOMINUTE   the last minute of the data
FORM_TOHOUR     the last hour of the data
FORM_TOMONTH    the last month of the data file(s)
FORM_TOYEAR     the last year of the data file(s)
FORM_USER_MODE_CPU      if non-null then user-mode-cpu usage requested
FORM_WHAT       any data category can be placed in this comma-separated list
FORM_XMAG       number of times X axis should be enlarged
FORM_YEAR       the (first) year of the data file(s)
FORM_YMAG       number of times Y axis should be enlarged


QUALIFIERS
----------
/BUTTONS=       string containing button labels/paths
/CHARSET=       "Content-Type: text/html; charset=...", empty suppresses charset
/DBUG           turns on all "if (Debug)" statements
/DIRECTORY=     directory containing any node description HTML files
/HELP=          help HTML file URL
/NODES=         comma-separated list of node names collecting SPI data
/PBACKGROUND=   <body> background image path
/PBGCOLOR=      <body> background colour
/PBBGCOLOR=     button background color
/PBBORDER=      width of button border
/PHBGCOLOR=     heading background color
/PHBORDER=      width of heading and button-bar border
/PHLOCAL=       **** NOT USED BY THIS SCRIPT ***
/PHTEXT=        heading text colour
/PLAYOUT=       1 is coloured header & buttons, 2 is text & horizontal rules
/PLINK=         <body> link colour
/PTEXT=         <body> text colour
/PVLINK=        <body> visited link colour
/SHUTDOWN       causes the HYPERSPI$AGENT image/process to exit


LOGICAL NAMES
-------------
HYPERSPI$DATA          locates the data files
HYPERSPI$PARAM         equivalent to (overrides) the command line
                       parameters/qualifiers (define as a system-wide logical)


BUILD DETAILS
-------------
See BUILD_HYPERSPI.COM procedure.


COPYRIGHT
---------
Copyright (C) 1996-2003 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under the conditions of the GNU GENERAL PUBLIC LICENSE, version 2.


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
23-DEC-2003  MGD  v1.8.3, minor conditional mods to support IA64
12-APR-2003  MGD  v1.8.2, link colour changed to 0000cc
16-AUG-2002  MGD  v1.8.1, some accomodations for CSWS v1.2
09-SEP-2001  MGD  v1.8.0, provide /SHUTDOWN for the agent via $FORCEX
01-JUL-2001  MGD  v1.7.1, add 'SkipParameters' for direct OSU support
28-OCT-2000  MGD  v1.7.0, use CGILIB object module
12-APR-2000  MGD  v1.6.1, minor changes for CGILIB 1.4
07-AUG-1999  MGD  v1.6.0, use more of the CGILIB functionality
24-APR-1999  MGD  v1.5.0, use CGILIB.C,
                          standard CGI environment (Netscape FastTrack),
                          modify to use CgiVar()
02-OCT-1998  MGD  v1.4.0, provide content-type "; charset=..."
13-AUG-1998  MGD  v1.3.0, accomodations for OSU
                          bugfix; IncludeFile() file name length
13-AUG-1998  MGD  NOTE:   this application is not Y2K compliant (in the sense
                          data is stored in files named with a 2 digit year!)
06-MAY-1998  MGD  v1.2.0, general maintenance, cosmetic changes
11-SEP-1997  MGD  v1.1.1, upped MAX_NODE_COUNT_BEFORE_SELECT to 20
01-AUG-1997  MGD  v1.1.0, added /BODY= qualifier,
                          remove extraneous '/' from <IMG SRC=...>,
                          general maintenance
19-SEP-1995  MGD  v1.0.1, replace <CR><LF> carriage-control single newline,
                          still acceptable for HTTP, slightly more efficient
20-JUN-1995  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "1.8.3"
#define SOFTWARENM "HYPERSPI"
#ifdef __ALPHA
#  define SOFTWAREID  SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID  SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID  SOFTWARENM " VAX-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <lnmdef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unixio.h>

/* VMS related header files */
#include <descrip.h>
#include <libdef.h>
#include <libdtdef.h>
#include <rmsdef.h>
#include <rms.h>
#include <ssdef.h>
#include <stsdef.h>

/* application header file */
#include "HyperSpi.h"
#include <cgilib.h>

#ifndef __VAX
#   pragma nomember_alignment
#endif

#define boolean int
#define true 1
#define false 0
 
#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))
 
#define FI_LI __FILE__, __LINE__

/* for use by functions in PlotSpi.c */
#define PLOT_ON 0
#define PLOT_OFF 1
#define PLOT_XOR 2

#define MAX_SAMPLE_PERIOD_DAYS 31
#define MAX_NODE_COUNT_BEFORE_SELECT 20

#define DEFAULT_BUTTONS "Selector;Help=/hyperspi/-/hyperspi.html"

/* this macro just plugs in some script-specific code into ButtonBar() */
#define SCRIPT_SPECIFIC_BUTTON_CODE \
   ButtonInternal[0] = SelectorPtr;

#define DEFAULT_PS_BGCOLOR        "#ffffff"
#define DEFAULT_PS_TEXT           "#000000"
#define DEFAULT_PS_LINK           "#0000cc"
#define DEFAULT_PS_VLINK          "#0000cc"
#define DEFAULT_PS_HEADBGCOLOR    "#cccccc"
#define DEFAULT_PS_HEADBORDER     "0"
#define DEFAULT_PS_HEADTEXT       "#000000"
#define DEFAULT_PS_BUTTONBGCOLOR  "#ffffff"
#define DEFAULT_PS_BUTTONBORDER   "1"

#define PS_BACKGROUND     0
#define PS_BGCOLOR        1
#define PS_TEXT           2
#define PS_LINK           3
#define PS_VLINK          4
#define PS_HEADBGCOLOR    5
#define PS_HEADTEXT       6
#define PS_HEADBORDER     7
#define PS_BUTTONBGCOLOR  8
#define PS_BUTTONBORDER   9
#define PS_BODYTAG       10
#define PS_LAYOUT        11
#define PS_HEADLOCAL     12
#define PS_HEADPADDING   13

char  *PageScheme [16];

char  Utility [] = "HYPERSPI";

char  *DayName [] =
      { "", "Sunday", "Monday", "Tuesday", "Wednesday",
            "Thursday", "Friday", "Saturday", };

char  *MonthName [] =
      { "", "January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December" };

boolean  Debug,
         DoDump,
         DoGraph,
         DoList,
         DoComprehensiveMenu,
         DoShutdownAgent,
         DoSummaryMenu,
         DoPresentSummaryPage,
         ErrorReported,
         ExtractGraph,
         GifTransparentBackground = true,
         HttpHasBeenOutput,
         IncludePeak,
         IncludeTotal,
         ProvidePercentCPU,
         ProvidePercentUserModeCPU,
         ProvideBufferedIO,
         ProvidePeakBufferedIO,
         ProvideDirectIO,
         ProvidePeakDirectIO,
         ProvideMemory,
         ProvideMscpIO,
         ProvidePeakMscpIO,
         ProvidePageFaults,
         ProvidePeakPageFaults,
         ProvideHardPageFaults,
         ProvidePeakHardPageFaults,
         ProvideProcesses,
         OsuEnvironment,
         StdCgiEnvironment;

int  AvePercentCPU,
     AvePercentUserModeCPU,
     AveBufferedIO,
     AveDirectIO,
     AveMscpIO,
     AvePageFaults,
     AveHardPageFaults,
     AveNumberOfProcesses,
     AveSystemMemoryPercentInUse,
     AvePageSpacePercentInUse,
     CurrentJulianDate,
     CurrentMinuteFromStartOfPeriod,
     DataFileNameLength,
     DataFileSpecLength,
     DataFilesFoundCount,
     DataFilesProcessedCount,
     DataRecordsReadCount,
     DataRecordsProcessedCount,
     DataMinute,
     DataHour,
     DataDay,
     DataMonth,
     DataYear,
     FromJulianDate,
     FromDay,
     FromHour,
     FromMinute,
     FromMonth,
     FromYear,
     HaveDataY,
     MaxAveBufferedIO,
     MaxAveDirectIO,
     MaxAveMscpIO,
     MaxAvePageFaults,
     MaxAveHardPageFaults,
     MaxNumberOfProcesses,
     MaxPageSpacePercentInUse,
     MaxSystemMemoryPercentInUse,
     NodeCount,
     NumberOfCPUs,
     NumberOfDays,
     NumberOfDaysIntoData,
     NumberOfHours,
     NumberOfMinutesIntoData,
     NumberOfProcesses,
     PageSpaceMBytes,
     PageSpacePercentInUse,
     PeakPercentUserModeCPU,
     PeakBufferedIO,
     PeakDirectIO,
     PeakMscpIO,
     PeakPageFaults,
     PeakPercentCPU,
     PeakHardPageFaults,
     PercentCPU,
     PercentUserModeCPU,
     RecordSampleRate,
     SizeOfMarginX = 10,
     SizeOfMarginY = 20,
     SizeOfPlotX,
     SizeOfPlotY,
     StartMinuteOnFirstDay,
     StartMinuteOfData,
     SystemMemoryMBytes,
     SystemMemoryPercentInUse,
     ToJulianDate,
     ToMinute,
     ToHour,
     ToDay,
     ToMonth,
     ToYear,
     XMag,
     YMag;
     
float  ScalingFactorY;

char  DataFileName [256],
      DataFileSpec [256],
      DataNode [32],
      DateString [32],
      DefaultButtons [] = DEFAULT_BUTTONS,
      FileSpecification [256],
      HyperSpiUrl [256],
      SoftwareID [48],
      ToDateString [32],
      UnixDateTime [64];

char  *ButtonPtr = DefaultButtons,
      *CgiEnvironmentPtr,
      *CgiFormDoPtr,
      *CgiFormIncludePtr,
      *CgiFormNodePtr,
      *CgiFormPeriodPtr,
      *CgiFormWhatPtr,
      *CgiRequestMethodPtr,
      *CgiPathInfoPtr,
      *CgiPathTranslatedPtr,
      *CgiScriptNamePtr,
      *CgiServerNamePtr,
      *CgiServerSoftwarePtr,
      *CliCharsetPtr,
      *HyperSpiDirectoryPtr = "HYPERSPI$DATA:",
      *SelectorPtr;

char  SpiNodeNames [16][16] = { "" };

unsigned long  CurrentBinTime [2];
unsigned short  CurrentNumTime [7];

/* this structure is declared in HyperSpi.h */
struct HyperSpiData  SpiRecord;

/* external storage (declared in PlotSpi.c) */
extern boolean  DoPlotDiagnostic;
extern int  PixelPlotOperation,
            PlotScaleY;

/* function prototypes for graphing (from PlotSpi.c) */
int PlotAllocate (int XSize, int YSize);
int PlotAxes (int Operation);
int PlotPixel (int Operation, int AtX, int AtY, int XOrigin, int YOrigin);
int PlotLineX (int Operation, int FromX, int ToX, int AtY,
               int XOrigin, int YOrigin);
int PlotLineY (int Operation, int FromY, int ToY, int AtX,
               int XOrigin, int YOrigin);

/* required function prototypes */
int DumpRecord();
int ListProcessedRecord();
int GraphRecordCPU();
int GraphRecordMemory();
int GraphRecordProcesses();
int GraphRecordRange();
int SelectNodeNameByDataFileName();
int SummarizeRecord();
char* UniqueNumber();

/*****************************************************************************/
/*
'argc/argv' are only required to support OSU, in particular CgiLibOsuInit().
*/

main
(
int argc,
char *argv[]
)
{
   int  status;
   unsigned long  UnixTime;
   struct tm  *UnixTmPtr;

   /*********/
   /* begin */
   /*********/

   sprintf (SoftwareID, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion());

   if (getenv ("HYPERSPI$DBUG") != NULL) Debug = true;
   CgiLibEnvironmentSetDebug (Debug);

   CgiLibEnvironmentInit (argc, argv, false);
   CgiEnvironmentPtr = CgiLibEnvironmentName ();

   GetParameters ();

   if (DoShutdownAgent) exit (ShutdownAgent());

   SetPageScheme ();

   CgiLibResponseSetBody (PageScheme[PS_BODYTAG]);
   CgiLibResponseSetCharset (CliCharsetPtr);
   CgiLibResponseSetSoftwareID (SoftwareID);
   CgiLibResponseSetErrorMessage ("Reported by HyperSPI");

   CgiServerSoftwarePtr = CgiVar ("WWW_SERVER_SOFTWARE");
   CgiRequestMethodPtr = CgiVar ("WWW_REQUEST_METHOD");
   if (strcmp (CgiRequestMethodPtr, "GET"))
   {
      CgiLibResponseHeader (501, "text/html");
      fprintf (stdout, "Not implemented!\n");
      return;
   }

   CgiScriptNamePtr = CgiVar ("WWW_SCRIPT_NAME");
   CgiServerNamePtr = CgiVar ("WWW_SERVER_NAME");

   sys$gettim (&CurrentBinTime);
   sys$numtim (&CurrentNumTime, &CurrentBinTime);

   time (&UnixTime);
   UnixTmPtr = localtime (&UnixTime);
   if (!strftime (UnixDateTime, sizeof(UnixDateTime),
                  "%a, %d %b %Y %T", UnixTmPtr))
      strcpy (UnixDateTime, "[error]");
   if (Debug) fprintf (stdout, "UnixDateTime |%s|\n", UnixDateTime);

   GetRequest ();
   ValidateRequest ();

   if (DoSummaryMenu || DoComprehensiveMenu)
   {
      SummaryMenu ();
      exit (SS$_NORMAL);
   }

   /* create file specification for processing data */
   DataFileSpecLength =
      sprintf (DataFileSpec,
               "%sHYPERSPI_%s_%s_%%%%%%%%%%%%.DAT",
               HyperSpiDataDirectory, HyperSpiDataVersion, CgiFormNodePtr);
   if (Debug) fprintf (stdout, "DataFileSpec |%s|\n", DataFileSpec);

   if (DoPresentSummaryPage)
      PresentSummaryPage ();
   else   
   if (DoDump)
      DumpData ();
   else   
   if (DoGraph)
      GraphData ();
   else   
   if (DoList)
      ListProcessedData ();

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.  OSU scripts have
the 'method', 'url' and 'protocol' supplied as P1, P2, P3 (these being detected
and used by CGILIB), and are of no interest to this function.
*/

GetParameters ()

{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

   int  idx, status,
        SkipParameters;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

   /*********/
   /* begin */
   /*********/

   if ((clptr = getenv ("HYPERSPI$PARAM")) == NULL)
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   /* if CSWS (Apache) */
   if (getenv ("APACHE$SHARED_SOCKET"))
   {
      /* look for something non-space outside of quotes */
      for (cptr = clptr; *cptr; cptr++)
      {
         if (isspace(*cptr)) continue;
         if (*cptr != '\"') break;
         cptr++;
         while (*cptr && *cptr != '\"') cptr++;
         if (*cptr) cptr++;
      }
      /* if nothing outside of quotes then ignore the command line */
      if (!*cptr) return;
   }

   /* if OSU environment then skip P1, P2, P3 */
   if (getenv ("WWWEXEC_RUNDOWN_STRING") != NULL)
      SkipParameters = 3;
   else
      SkipParameters = 0;

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr != NULL) *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (SkipParameters)
      {
         SkipParameters--;
         continue;
      }

      if (strsame (aptr, "/BUTTONS=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         ButtonPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/CHARSET=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliCharsetPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/DIRECTORY=", 3))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         HyperSpiDirectoryPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/NODES=", 3))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         /* quick and nasty, no checks on array bounds (caveat emptor) */
         idx = 0;
         while (*cptr)
         {
            sptr = SpiNodeNames[idx++];
            while (*cptr && *cptr != ',') *sptr++ = toupper(*cptr++);
            *sptr = '\0';
            if (*cptr) cptr++;
         }
         SpiNodeNames[idx][0] = '\0';
         continue;
      }
      if (strsame (aptr, "/SHUTDOWN", 3))
      {
         DoShutdownAgent = true;
         continue;
      }

      if (GetPageParameter (aptr)) continue;

      if (*aptr != '/')
      {
         fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
                  Utility, aptr);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
      else
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
   }
}

/*****************************************************************************/
/*
Get command-line parameters associated with page scheme.
*/

boolean GetPageParameter (char *aptr)

{
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (strsame (aptr, "/PBACKGROUND=", 4))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_BACKGROUND] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PBGCOLOR=", 4))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_BGCOLOR] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PBBGCOLOR=", 5))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_BUTTONBGCOLOR] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PBBORDER=", 5))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_BUTTONBORDER] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PHBGCOLOR=", 5))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_HEADBGCOLOR] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PHBORDER=", 5))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_HEADBORDER] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PHTEXT=", 4))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_HEADTEXT] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PLAYOUT=", 4))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_LAYOUT] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PLINK=", 4))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_LINK] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PHLOCAL=", 4))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_HEADLOCAL] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PTEXT=", 4))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_TEXT] = cptr;
      return (true);
   }
   if (strsame (aptr, "/PVLINK=", 4))
   {
      for (cptr = aptr; *cptr && *cptr != '='; cptr++);
      if (*cptr) cptr++;
      PageScheme[PS_VLINK] = cptr;
      return (true);
   }
   return (false);
}

/*****************************************************************************/
/*
Set the page layout and colouration.
*/

SetPageScheme ()

{
   int  size;
   char  *sptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SetPageScheme()\n");

   if (PageScheme[PS_LAYOUT] == NULL)
      PageScheme[PS_LAYOUT] = "1";

   if (PageScheme[PS_BACKGROUND] == NULL)
      PageScheme[PS_BACKGROUND] = "";

   if (PageScheme[PS_HEADLOCAL] == NULL)
      PageScheme[PS_HEADLOCAL] = "";

   if (PageScheme[PS_LAYOUT][0] == '2')
   {
      if (PageScheme[PS_BGCOLOR] == NULL) PageScheme[PS_BGCOLOR] = "";
      if (PageScheme[PS_TEXT] == NULL) PageScheme[PS_TEXT] = "";
      if (PageScheme[PS_LINK] == NULL) PageScheme[PS_LINK] = "";
      if (PageScheme[PS_VLINK] == NULL) PageScheme[PS_VLINK] = "";
      if (PageScheme[PS_HEADBGCOLOR] == NULL) PageScheme[PS_HEADBGCOLOR] = "";
      if (PageScheme[PS_HEADBORDER] == NULL) PageScheme[PS_HEADBORDER] = "";
      if (PageScheme[PS_HEADTEXT] == NULL) PageScheme[PS_HEADTEXT] = "";
      if (PageScheme[PS_BUTTONBGCOLOR] == NULL) PageScheme[PS_BUTTONBGCOLOR] = "";
      if (PageScheme[PS_BUTTONBORDER] == NULL) PageScheme[PS_BUTTONBORDER] = "";
   }
   else
   {
      if (PageScheme[PS_BGCOLOR] == NULL)
         PageScheme[PS_BGCOLOR] = DEFAULT_PS_BGCOLOR;
      if (PageScheme[PS_TEXT] == NULL)
         PageScheme[PS_TEXT] = DEFAULT_PS_TEXT;
      if (PageScheme[PS_LINK] == NULL)
         PageScheme[PS_LINK] = DEFAULT_PS_LINK;
      if (PageScheme[PS_VLINK] == NULL)
         PageScheme[PS_VLINK] = DEFAULT_PS_VLINK;
      if (PageScheme[PS_HEADBGCOLOR] == NULL)
         PageScheme[PS_HEADBGCOLOR] = DEFAULT_PS_HEADBGCOLOR;
      if (PageScheme[PS_HEADBORDER] == NULL)
         PageScheme[PS_HEADBORDER] = DEFAULT_PS_HEADBORDER;
      if (PageScheme[PS_HEADTEXT] == NULL)
         PageScheme[PS_HEADTEXT] = DEFAULT_PS_HEADTEXT;
      if (PageScheme[PS_BUTTONBGCOLOR] == NULL)
         PageScheme[PS_BUTTONBGCOLOR] = DEFAULT_PS_BUTTONBGCOLOR;
      if (PageScheme[PS_BUTTONBORDER] == NULL)
         PageScheme[PS_BUTTONBORDER] = DEFAULT_PS_BUTTONBORDER;
   }

   /* <BODY> tag attributes */
   size = strlen(PageScheme[PS_BACKGROUND]) +
          strlen(PageScheme[PS_BGCOLOR]) +
          strlen(PageScheme[PS_TEXT]) +
          strlen(PageScheme[PS_LINK]) +
          strlen(PageScheme[PS_VLINK]);
   if (size)
   {
      if ((sptr = calloc (1, size+64)) == NULL) exit (vaxc$errno);
      PageScheme[PS_BODYTAG] = sptr;
      if (PageScheme[PS_BACKGROUND][0])
         sptr += sprintf (sptr, " BACKGROUND=\"%s\"", PageScheme[PS_BACKGROUND]);
      if (PageScheme[PS_BGCOLOR][0])
         sptr += sprintf (sptr, " BGCOLOR=\"%s\"", PageScheme[PS_BGCOLOR]);
      if (PageScheme[PS_TEXT][0])
         sptr += sprintf (sptr, " TEXT=\"%s\"", PageScheme[PS_TEXT]);
      if (PageScheme[PS_LINK][0])
         sptr += sprintf (sptr, " LINK=\"%s\"", PageScheme[PS_LINK]);
      if (PageScheme[PS_VLINK][0])
         sptr += sprintf (sptr, " VLINK=\"%s\"", PageScheme[PS_VLINK]);
   }
   else
      PageScheme[PS_BODYTAG] = "";

   if (PageScheme[PS_HEADBGCOLOR][0])
   {
      if ((sptr = calloc (1, strlen(PageScheme[PS_HEADBGCOLOR])+16)) == NULL)
         exit (vaxc$errno);
      sprintf (sptr, " BGCOLOR=\"%s\"", PageScheme[PS_HEADBGCOLOR]);
      PageScheme[PS_HEADBGCOLOR] = sptr;
      PageScheme[PS_HEADPADDING] = "10";
   }
   else
      PageScheme[PS_HEADPADDING] = "0";

   if (PageScheme[PS_BUTTONBGCOLOR][0])
   {
      if ((sptr = calloc (1, strlen(PageScheme[PS_BUTTONBGCOLOR])+16)) == NULL)
         exit (vaxc$errno);
      sprintf (sptr, " BGCOLOR=\"%s\"", PageScheme[PS_BUTTONBGCOLOR]);
      PageScheme[PS_BUTTONBGCOLOR] = sptr;
   }
}

/*****************************************************************************/
/*
Provides a divider for top and bottom of the content of the page. This can be
a coloured bar (using <TABLE>) or a horizontal rule depending on the page
layout. "Buttons" providing script-internal and/or additional user-specified
links ('ButtonPtr' string) can be placed with(in) this bar. All button labels
are derived from 'ButtonPtr', with script-internal buttons using link-paths
set up via 'ButtonInternal[]' array, or any user-specified path depending on
requirement. An empty path (i.e. PathPtr[0] == '\0') obviously does not have a
link created, it just displays the button label. For a button-bar at the top
of the document use 1, bottom of the document use 2, and for just a bar with
no buttons at all use 0.
*/

ButtonBar (int Top1Bottom2)

{
#define MAX_BUTTON_COUNT 8

   static int  ButtonCount = -1;
   static char  *ButtonInternal [MAX_BUTTON_COUNT],
                *ButtonLabel [MAX_BUTTON_COUNT],
                *ButtonPath [MAX_BUTTON_COUNT];

   int  idx;
   char  *PathPtr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ButtonBar() %d\n", Top1Bottom2);

   if (ButtonCount == -1)
   {
      char  *cptr, *sptr;

      if (Debug) fprintf (stdout, "|%s|\n", ButtonPtr);
      cptr = ButtonPtr;
      for (ButtonCount = 0;
           ButtonCount < MAX_BUTTON_COUNT && *cptr;
           ButtonCount++)
      {
         for (sptr = cptr; *sptr && *sptr != '=' && *sptr != ';'; sptr++)
            if (*sptr == '\\') memcpy (sptr, sptr+1, strlen(sptr));
         if (*sptr == '=') *sptr++ = '\0';
         ButtonLabel[ButtonCount] = cptr;
         cptr = sptr;
         for (sptr = cptr; *sptr && *sptr != ';'; sptr++)
            if (*sptr == '\\') memcpy (sptr, sptr+1, strlen(sptr));
         if (*sptr) *sptr++ = '\0';
         ButtonPath[ButtonCount] = cptr;
         cptr = sptr;
      }
   }

   if (Top1Bottom2)
   {
      /***********************************/
      /* set up script-specified buttons */
      /***********************************/

      SCRIPT_SPECIFIC_BUTTON_CODE
   }

   if (PageScheme[PS_LAYOUT][0] == '2')
   {
      /************/
      /* format 2 */
      /************/

      if (Top1Bottom2 == 2 || !Top1Bottom2)
      {
         fprintf (stdout, "<HR ALIGN=left SIZE=2 WIDTH=95%%>\n");
         if (!Top1Bottom2) return;
      }

      fprintf (stdout, "<FONT SIZE=-1><NOBR>\n");
      for (idx = 0; idx < ButtonCount; idx++)
      {
         if (ButtonInternal[idx] == NULL)
            PathPtr = ButtonPath[idx];
         else
            PathPtr = ButtonInternal[idx];
         if (idx) fprintf (stdout, "&nbsp;");
         if (PathPtr[0])
            fprintf (stdout, "[<A HREF=\"%s\">%s</A>]\n",
                     PathPtr, ButtonLabel[idx]);
         else
            fprintf (stdout, "[%s]\n", ButtonLabel[idx]);
      }
      fprintf (stdout, "</NOBR></FONT>\n");

      if (Top1Bottom2 == 1)
         fprintf (stdout, "<HR ALIGN=left SIZE=2 WIDTH=95%%>\n");
   }
   else
   {
      /************/
      /* format 1 */
      /************/

      fprintf (stdout,
"<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=0>\
<TR><TD HEIGHT=2></TD></TR>\
</TABLE>\n\
<TABLE BORDER=%s CELLPADDING=%s CELLSPACING=0 WIDTH=100%%>\n\
<TR><TD%s>\n",
         PageScheme[PS_HEADBORDER],
         PageScheme[PS_HEADBORDER],
         PageScheme[PS_HEADBGCOLOR]);

      if (ButtonCount == 0 || !Top1Bottom2)
         fprintf (stdout, "&nbsp;\n");
      else
      {
         fprintf (stdout,
"<TABLE BORDER=%s CELLPADDING=1 CELLSPACING=0>\n",
            PageScheme[PS_BUTTONBORDER]);

         for (idx = 0; idx < ButtonCount; idx++)
         {
            if (ButtonInternal[idx] == NULL)
               PathPtr = ButtonPath[idx];
            else
               PathPtr = ButtonInternal[idx];
            if (PathPtr[0])
               fprintf (stdout,
"<TD ALIGN=center%s><FONT SIZE=-1>\
<NOBR>&nbsp;&nbsp;<A HREF=\"%s\">%s</A>&nbsp;&nbsp;</NOBR></FONT></TD>\n",
                  PageScheme[PS_BUTTONBGCOLOR], PathPtr, ButtonLabel[idx]);
            else
               fprintf (stdout,
"<TD ALIGN=center%s><FONT SIZE=-1>\
<NOBR>&nbsp;&nbsp;%s&nbsp;&nbsp;</NOBR></FONT></TD>\n",
                  PageScheme[PS_BUTTONBGCOLOR], ButtonLabel[idx]);
         }

         fprintf (stdout, "</TR></TABLE>\n");
      }

      fprintf (stdout,
"</TD></TR>\n\
</TABLE>\n\
<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=0>\
<TR><TD HEIGHT=2></TD></TR>\
</TABLE>\n");
   }
}

/*****************************************************************************/
/*
Get the request CGI variables.
*/

GetRequest ()

{
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetRequest()\n");

   /* determine what has been requested */

   CgiFormWhatPtr = CgiVar ("WWW_FORM_WHAT");
   for (cptr = CgiFormWhatPtr; *cptr; cptr++) *cptr = toupper(*cptr);

   if (strsame (CgiFormWhatPtr, "CPU", -1)) ProvidePercentCPU = true;
   if (strstr (CgiFormWhatPtr, ",CPU") != NULL) ProvidePercentCPU = true;
   cptr = CgiVar ("WWW_FORM_CPU");
   if (cptr[0]) ProvidePercentCPU = true;

   if (strstr (CgiFormWhatPtr, "USER_MODE") != NULL)
      ProvidePercentUserModeCPU= true;
   cptr = CgiVar ("WWW_FORM_USER_MODE_CPU");
   if (cptr[0]) ProvidePercentUserModeCPU = true;

   if (strstr (CgiFormWhatPtr, "BUFFERED") != NULL) ProvideBufferedIO = true;
   cptr = CgiVar ("WWW_FORM_BUFFERED_IO");
   if (cptr[0]) ProvideBufferedIO = true;

   if (strstr (CgiFormWhatPtr, "PEAK_BUFFERED") != NULL)
      ProvidePeakBufferedIO = true;
   cptr = CgiVar ("WWW_FORM_PEAK_BUFFERED_IO");
   if (cptr[0]) ProvidePeakBufferedIO = true;

   if (strstr (CgiFormWhatPtr, "DIRECT") != NULL) ProvideDirectIO = true;
   cptr = CgiVar ("WWW_FORM_DIRECT_IO");
   if (cptr[0]) ProvideDirectIO = true;

   if (strstr (CgiFormWhatPtr, "PEAK_DIRECT") != NULL)
      ProvidePeakDirectIO = true;
   cptr = CgiVar ("WWW_FORM_PEAK_DIRECT_IO");
   if (cptr[0]) ProvidePeakDirectIO = true;

   if (strstr (CgiFormWhatPtr, "MSCP") != NULL) ProvideMscpIO = true;
   cptr = CgiVar ("WWW_FORM_MSCP_IO");
   if (cptr[0]) ProvideMscpIO = true;

   if (strstr (CgiFormWhatPtr, "PEAK_MSCP") != NULL) ProvidePeakMscpIO = true;
   cptr = CgiVar ("WWW_FORM_PEAK_MSCP_IO");
   if (cptr[0]) ProvidePeakMscpIO = true;

   if (strstr (CgiFormWhatPtr, "FAULTS") != NULL) ProvidePageFaults = true;
   cptr = CgiVar ("WWW_FORM_FAULTS");
   if (cptr[0]) ProvidePageFaults = true;

   if (strstr (CgiFormWhatPtr, "PEAK_FAULTS") != NULL)
      ProvidePeakHardPageFaults = true;
   cptr = CgiVar ("WWW_FORM_PEAK_FAULTS");
   if (cptr[0]) ProvidePeakPageFaults = true;

   if (strstr (CgiFormWhatPtr, "HARD_FAULTS") != NULL)
      ProvideHardPageFaults = true;
   cptr = CgiVar ("WWW_FORM_HARD_FAULTS");
   if (cptr[0]) ProvideHardPageFaults = true;

   if (strstr (CgiFormWhatPtr, "PEAK_HARD_FAULTS") != NULL)
      ProvidePeakHardPageFaults = true;
   cptr = CgiVar ("WWW_FORM_PEAK_HARD_FAULTS");
   if (cptr[0]) ProvidePeakHardPageFaults = true;

   if (strstr (CgiFormWhatPtr, "MEMORY") != NULL) ProvideMemory = true;
   cptr = CgiVar ("WWW_FORM_MEMORY");
   if (cptr[0]) ProvideMemory = true;

   if (strstr (CgiFormWhatPtr, "PROCESSES") != NULL) ProvideProcesses = true;
   cptr = CgiVar ("WWW_FORM_PROCESSES");
   if (cptr[0]) ProvideProcesses = true;

   CgiFormIncludePtr = CgiVar ("WWW_FORM_INCLUDE");
   for (cptr = CgiFormIncludePtr; *cptr; cptr++) *cptr = toupper(*cptr);
   if (strstr (CgiFormIncludePtr, "PEAK") != NULL) IncludePeak = true;
   if (strstr (CgiFormIncludePtr, "TOTAL") != NULL) IncludeTotal = true;

   cptr = CgiVar ("WWW_FORM_FROM");
   sscanf (cptr, "%4d-%2d-%2d %2d:%2d",
           &FromYear, &FromMonth, &FromDay, &FromHour, &FromMinute);

   cptr = CgiVar ("WWW_FORM_TO");
   sscanf (cptr, "%4d-%2d-%2d %2d:%2d",
           &ToYear, &ToMonth, &ToDay, &ToHour, &ToMinute);

   CgiFormDoPtr = CgiVar ("WWW_FORM_DO");

   CgiFormNodePtr = CgiVar ("WWW_FORM_NODE");
   if (!CgiFormNodePtr[0])
      CgiFormNodePtr = CgiVar ("WWW_FORM_LIST_NODE");
   if (CgiFormNodePtr[0])
   {
      /* ensure the node name is in upper case */
      for (cptr = CgiFormNodePtr; *cptr; cptr++) *cptr = toupper(*cptr);
   }

   cptr = CgiVar ("WWW_FORM_EXTRACT");
   if (cptr[0]) ExtractGraph = true;

   CgiFormPeriodPtr = CgiVar ("WWW_FORM_PERIOD");

   cptr = CgiVar ("WWW_FORM_XMAG");
   if (isdigit(cptr[0]))
      XMag = atoi (cptr);
   else
      XMag = 1;
   if (XMag > 4) XMag = 4;
   if (XMag < 1) XMag = 1;

   cptr = CgiVar ("WWW_FORM_YMAG");
   if (isdigit(cptr[0]))
      YMag = atoi (cptr);
   else
      YMag = 1;
   if (YMag > 4) YMag = 4;
   if (YMag < 1) YMag = 1;
}

/*****************************************************************************/
/*
Process the request parameters (e.g. time, node name).  Verify the parameters 
are within constraints, particular time.  Process the time components into 
values the application can use.
*/

ValidateRequest ()

{
   static long  LibJulianDate = LIB$K_JULIAN_DATE;
   static char  *MonthName [] = 
      { "???", "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
        "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };

   int  status;
   unsigned long  BinTime [2],
                  DeltaTime [2];
   unsigned short  NumTime [7];
   char  Scratch [256];
   $DESCRIPTOR (TempDsc, "");

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ValidateRequest()\n");

   if (!CgiFormDoPtr[0])
      DoSummaryMenu = true;
   else
   if (toupper(CgiFormDoPtr[0]) == 'G')
      DoGraph = true;
   else
   if (toupper(CgiFormDoPtr[0]) == 'P')
      DoPresentSummaryPage = true;
   else
   if (toupper(CgiFormDoPtr[0]) == 'C')
      DoComprehensiveMenu = true;
   else
   if (toupper(CgiFormDoPtr[0]) == 'M')
      DoSummaryMenu = true;
   else
   if (toupper(CgiFormDoPtr[0]) == 'D')
      DoDump = true;
   else
   if (toupper(CgiFormDoPtr[0]) == 'L')
      DoList = true;
   else
   {
      sprintf (Scratch, "Cannot do \"%s\"", CgiFormDoPtr);
      CgiLibResponseError (FI_LI, 0, Scratch);
      exit (SS$_NORMAL);
   }

   if ((DoPresentSummaryPage || DoGraph || DoList || DoDump) &&
       !CgiFormNodePtr[0])
   {
      CgiLibResponseError (FI_LI, 0, "Node name not specified.");
      exit (SS$_NORMAL);
   }

   /********************/
   /* standard periods */
   /********************/

   if (CgiFormPeriodPtr[0] && toupper(CgiFormPeriodPtr[0]) != 'O')
   {
      /* a standard time period has been specified, other than "other" */
      FromHour = 0;
      FromMinute = 0;
      ToHour = 23;
      ToMinute = 59;
      FromDay = ToDay =  CurrentNumTime[2];
      FromMonth = ToMonth =  CurrentNumTime[1];
      FromYear = ToYear =  CurrentNumTime[0];

      if (toupper(CgiFormPeriodPtr[0]) == 'U')
      {
         /* "until_now" (7am to this hour) */
         FromHour = 7;
         ToHour = CurrentNumTime[3];
      }
      else
      if (toupper(CgiFormPeriodPtr[0]) == 'B')
      {
         /* "business" hours (7am to 7pm) */
         FromHour = 7;
         ToHour = 18;
      }
      else
      if (toupper(CgiFormPeriodPtr[0]) == 'T')
      {
         /* "today" */
         ToHour = 23;
      }
      else
      if (toupper(CgiFormPeriodPtr[0]) == 'S' ||
          toupper(CgiFormPeriodPtr[0]) == 'Y')
      {
         /* "since_yesterday" until now, or all of "yesterday" */
         TempDsc.dsc$a_pointer = "1 00:00:00.00";
         TempDsc.dsc$w_length = 13;
         if (VMSnok (status = sys$bintim (&TempDsc, &DeltaTime)))
         {
            CgiLibResponseError (FI_LI, status, CgiFormPeriodPtr);
            exit (SS$_NORMAL);
         }
         lib$sub_times (&CurrentBinTime, &DeltaTime, &BinTime);
         sys$numtim (&NumTime, &BinTime);
         FromDay = NumTime[2];
         FromMonth = NumTime[1];
         FromYear = NumTime[0];
         if (toupper(CgiFormPeriodPtr[0]) == 'S')
            ToHour = CurrentNumTime[3];
         else
         {
            ToDay = NumTime[2];
            ToMonth = NumTime[1];
            ToYear = NumTime[0];
         }
      }
      else
      if (isdigit(CgiFormPeriodPtr[0]))
      {
         TempDsc.dsc$a_pointer = Scratch;
         TempDsc.dsc$w_length =
            sprintf (Scratch, "%s 00:00:00.00", CgiFormPeriodPtr);
         if (VMSnok (status = sys$bintim (&TempDsc, &DeltaTime)))
         {
            CgiLibResponseError (FI_LI, status, CgiFormPeriodPtr);
            exit (SS$_NORMAL);
         }
         lib$sub_times (&CurrentBinTime, &DeltaTime, &BinTime);
         sys$numtim (&NumTime, &BinTime);
         FromDay = NumTime[2];
         FromMonth = NumTime[1];
         FromYear = NumTime[0];
      }
      else
      {
         CgiLibResponseError (FI_LI, 0, "Periods are \"until_now\"");
         exit (SS$_NORMAL);
      }
   }

   /*******************/
   /* time components */
   /*******************/

   /* non-specified date components default to those of the current day */
   if (!FromDay) FromDay = CurrentNumTime[2];
   if (!FromMonth) FromMonth = CurrentNumTime[1];
   if (!FromYear) FromYear = CurrentNumTime[0];
   if (!ToDay) ToDay =  CurrentNumTime[2];
   if (!ToMonth) ToMonth =  CurrentNumTime[1];
   if (!ToYear) ToYear =  CurrentNumTime[0];

   /* bit of a sanity check (prevents 'MonthName[x]' access violating, etc.) */
   if (FromMonth < 1 || FromMonth > 12) FromMonth = 0;
   if (ToMonth < 1 || ToMonth > 12) ToMonth = 0;

   /* ensure the commencement date/time is acceptable */
   TempDsc.dsc$a_pointer = DateString;
   TempDsc.dsc$w_length = 
      sprintf (DateString, "%d-%s-%d %02.02d:%02.02d",
               FromDay, MonthName[FromMonth], FromYear, FromHour, FromMinute);
   if (Debug) fprintf (stdout, "DateString |%s|\n", DateString);
   if (VMSnok (status = sys$bintim (&TempDsc, &BinTime)))
   {
      sprintf (Scratch, "%02.02d:%02.02d %02.02d/%02.02d/%02.02d",
               FromHour, FromMinute, FromDay, FromMonth, FromYear);
      CgiLibResponseError (FI_LI, status, Scratch);
      exit (SS$_NORMAL);
   }
   /* get the commencement julian date (number of days in epoch) */
   lib$cvt_from_internal_time (&LibJulianDate, &FromJulianDate, &BinTime);

   /* ensure the conclusion date/time is acceptable */
   TempDsc.dsc$a_pointer = ToDateString;
   TempDsc.dsc$w_length = 
      sprintf (ToDateString, "%d-%s-%d %02.02d:%02.02d",
               ToDay, MonthName[ToMonth], ToYear, ToHour, ToMinute);
   if (Debug) fprintf (stdout, "ToDateString |%s|\n", ToDateString);
   if (VMSnok (status = sys$bintim (&TempDsc, &BinTime)))
   {
      sprintf (Scratch, "%02.02d:%02.02d %02.02d/%02.02d/%02.02d",
               ToHour, ToMinute, ToDay, ToMonth, ToYear);
      CgiLibResponseError (FI_LI, status, Scratch);
      exit (SS$_NORMAL);
   }
   /* get the conclusion julian date (number of days in epoch) */
   lib$cvt_from_internal_time (&LibJulianDate, &ToJulianDate, &BinTime);

   /***************************/
   /* calculate period values */
   /***************************/

   /* using julian dates, get number of days and hours sample period covers */
   NumberOfHours = ((ToJulianDate - FromJulianDate) * 24) + ToHour - FromHour;
   NumberOfDays = (NumberOfHours / 24);

   if (DoPresentSummaryPage || DoGraph || DoList || DoDump)
   {
      if (NumberOfHours < 0 || (NumberOfHours == 0 && ToMinute <= FromMinute))
      {
         sprintf (Scratch,
"Beginning of period (%04.04d-%02.02d-%02.02d %02.02d:%02.02d) \
does not precede end (%04.04d-%02.02d-%02.02d %02.02d:%02.02d)!",
         FromYear, FromMonth, FromDay, FromHour, FromMinute,
         ToYear, ToMonth, ToDay, ToHour, ToMinute);
         CgiLibResponseError (FI_LI, 0, Scratch);
         exit (SS$_NORMAL);
      }
      if (NumberOfHours < 1)
      {
         CgiLibResponseError (FI_LI, 0, "Little point to such a short period!");
         exit (SS$_NORMAL);
      }
      if (NumberOfDays > MAX_SAMPLE_PERIOD_DAYS)
      {
         sprintf (Scratch, "Maximum sample period is %d days.",
                  MAX_SAMPLE_PERIOD_DAYS);
         CgiLibResponseError (FI_LI, 0, Scratch);
         exit (SS$_NORMAL);
      }
   }

   /* rate at which the data (and any graphic) must be X-axis compressed */
   if (NumberOfHours <= 12)
      RecordSampleRate = 1;
   else
   if (NumberOfHours <= 24)
      RecordSampleRate = 2;
   else
      RecordSampleRate = (NumberOfDays + 1) * 2;

   /* add one to number of hours, for graphing purposes, re-calculate days */
   NumberOfHours++;
   NumberOfDays = (NumberOfHours / 24);

   /* the number of minutes from midnight data begins being processed */
   StartMinuteOnFirstDay = (FromHour * 60) + FromMinute;

   /* the current minute from the start of the specified period */
   lib$cvt_from_internal_time (&LibJulianDate, &CurrentJulianDate,
                               &CurrentBinTime);
   CurrentMinuteFromStartOfPeriod =
      ((((CurrentJulianDate - FromJulianDate) * 24) +
        (CurrentNumTime[3] - FromHour)) * 60) +
       CurrentNumTime[4];

   if (Debug)
      fprintf (stdout,
"FromJulianDate: %d ToJulianDate: %d\n\
NumberOfDays: %d NumberOfHours: %d RecordSampleRate: %d\n",
      FromJulianDate, ToJulianDate,
      NumberOfDays, NumberOfHours, RecordSampleRate);
}

/*****************************************************************************/
/*
Provide either a standard or comprehensive menu allowing System Performance 
Information system, category and period to be specified.
*/

SummaryMenu ()

{
   char  *CheckedPtr,
         *TodayCheckedPtr,
         *UntilNowCheckedPtr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SummaryMenu()\n");

   if (!DoComprehensiveMenu) CheckedPtr = " CHECKED";

   CgiLibResponseHeader (200, "text/html");

   fprintf (stdout,
"<HTML>\n\
<HEAD>\n\
<META NAME=\"generator\" CONTENT=\"%s\">\n\
<META NAME=\"date\" CONTENT=\"%s\">\n\
<META NAME=\"environment\" CONTENT=\"%s\">\n\
<TITLE>HyperSPI, Summary Selector</TITLE>\n\
</HEAD>\n\
<BODY%s>\n\
<TABLE BORDER=0 CELLPADDING=%s CELLSPACING=0 WIDTH=100%%%s>\n\
<TR><TD ALIGN=left VALIGN=bottom>\n\
<FONT COLOR=\"%s\">\n\
&nbsp;<FONT SIZE=+1><SUP>*</SUP><B>Hyper</B></FONT><FONT SIZE=+3><B>SPI</B></FONT>\n\
</FONT>\n\
</TD><TD ALIGN=center VALIGN=bottom>\n\
<FONT COLOR=\"%s\" SIZE=+2><B>\n\
Summary Selector\n\
</B></FONT>\n\
</TD><TD ALIGN=right VALIGN=bottom>\n\
<FONT COLOR=\"%s\">\n\
%02.02d %s %d &nbsp;%02.02d:%02.02d\n\
</FONT>\n\
</TD></TR>\n\
</TABLE>\n\
<BLOCKQUOTE>\n\
<P>\n\
<FORM ACTION=\"%s\">\n",
   SoftwareID, UnixDateTime, CgiEnvironmentPtr,
   PageScheme[PS_BODYTAG],
   PageScheme[PS_HEADPADDING],
   PageScheme[PS_HEADBGCOLOR],
   PageScheme[PS_HEADTEXT],
   PageScheme[PS_HEADTEXT],
   PageScheme[PS_HEADTEXT],
   CurrentNumTime[2], MonthName[CurrentNumTime[1]], CurrentNumTime[0],
   CurrentNumTime[3], CurrentNumTime[4],
   CgiScriptNamePtr);

   HttpHasBeenOutput = true;

   SelectNodeNames ();

   if (DoComprehensiveMenu)
   {
      fputs (
"or ... node <INPUT TYPE=text NAME=node SIZE=6 MAXLENGTH=6><BR>",
      stdout);
   }

   if (CurrentNumTime[3] >= 7)
   {
      UntilNowCheckedPtr = " CHECKED";
      TodayCheckedPtr = "";
   }
   else
   {
      UntilNowCheckedPtr = "";
      TodayCheckedPtr = " CHECKED";
   }

   fprintf (stdout,
"<P>\n\
<INPUT TYPE=radio NAME=period VALUE=until_now%s>\
 since 7am <FONT SIZE=-1>(until now)</FONT><BR>\
<INPUT TYPE=radio NAME=period VALUE=today%s> today<BR>\n\
<INPUT TYPE=radio NAME=period VALUE=since_yesterday>\
 since yesterday  <FONT SIZE=-1>(until now)</FONT><BR>\n\
<INPUT TYPE=radio NAME=period VALUE=yesterday> yesterday<BR>\n\
<INPUT TYPE=radio NAME=period VALUE=7> previous week &nbsp;\n\
<INPUT TYPE=radio NAME=period VALUE=14> two weeks &nbsp;\n\
<INPUT TYPE=radio NAME=period VALUE=28> four weeks<BR>\n\
<NOBR>\n\
<INPUT TYPE=radio NAME=period VALUE=other>\
 from\n\
<INPUT TYPE=text SIZE=16 MAXLENGTH=16 NAME=from \
VALUE=\"%04.04d-%02.02d-%02.02d %02.02d:00\">\
 to\n\
<INPUT TYPE=text SIZE=16 MAXLENGTH=16 NAME=to \
VALUE=\"%04.04d-%02.02d-%02.02d 23:59\">\
 &nbsp;<FONT SIZE=-1>(yyyy-mm-dd hh:mm)</FONT>\n\
</NOBR>\n\
<P>\n\
<INPUT TYPE=checkbox NAME=cpu VALUE=yes CHECKED> CPU<BR>\n\
<INPUT TYPE=checkbox NAME=memory VALUE=yes%s> memory<BR>\n\
<INPUT TYPE=checkbox NAME=processes VALUE=yes> processes<BR>\n",
   UntilNowCheckedPtr, TodayCheckedPtr,
   CurrentNumTime[0], CurrentNumTime[1], CurrentNumTime[2],
   CurrentNumTime[3],
   CurrentNumTime[0], CurrentNumTime[1], CurrentNumTime[2],
   CheckedPtr);

   if (DoComprehensiveMenu)
   {
      fputs (
"<INPUT TYPE=checkbox NAME=faults VALUE=yes> paging (soft) \
 <INPUT TYPE=checkbox NAME=peak_faults VALUE=yes> peak<BR>\n",
      stdout);
   }

   fprintf (stdout,
"<INPUT TYPE=checkbox NAME=hard_faults VALUE=yes%s> paging",
   CheckedPtr);
   if (DoComprehensiveMenu)
   {
      fputs (
" (hard)\
 <INPUT TYPE=checkbox NAME=peak_hard_faults VALUE=yes> peak",
      stdout);
   }
   fputs ("<BR>\n", stdout);

   fputs (
"<INPUT TYPE=checkbox NAME=direct_IO VALUE=yes> disk IO",
   stdout);
   if (DoComprehensiveMenu)
   {
      fputs (
" <INPUT TYPE=checkbox NAME=peak_direct_IO VALUE=yes> peak",
      stdout);
   }
   fputs ("<BR>\n", stdout);

   fputs (
"<INPUT TYPE=checkbox NAME=buffered_IO VALUE=yes> other IO",
   stdout);
   if (DoComprehensiveMenu)
   {
      fputs (
" <INPUT TYPE=checkbox NAME=peak_buffered_IO VALUE=yes> peak",
      stdout);
   }
   fputs ("<BR>\n", stdout);

   fputs (
"<INPUT TYPE=checkbox NAME=mscp_IO VALUE=yes> MSCP IO",
   stdout);
   if (DoComprehensiveMenu)
   {
      fputs (
" <INPUT TYPE=checkbox NAME=peak_mscp_IO VALUE=yes> peak",
      stdout);
   }
   fputs ("<BR>\n", stdout);

   if (DoComprehensiveMenu)
   {
      fputs (
"<INPUT TYPE=checkbox NAME=include VALUE=peak>\
 <I>include peak plot (where applicable)</I>\n\
<P>\n\
<INPUT TYPE=radio NAME=do VALUE=page CHECKED>graph\n\
 <I>(X,Y magnification:\
<SELECT NAME=Xmag SIZE=1>\n\
<OPTION VALUE=1 SELECTED>1 \n\
<OPTION VALUE=2>2 \n\
<OPTION VALUE=4>4 \n\
</SELECT>,\
<SELECT NAME=Ymag SIZE=1>\n\
<OPTION VALUE=1 SELECTED> 1 \n\
<OPTION VALUE=2> 2 \n\
<OPTION VALUE=4> 4 \n\
</SELECT>)</I><BR>\n\
<INPUT TYPE=radio NAME=do VALUE=list> list<BR>\n\
<INPUT TYPE=radio NAME=do VALUE=dump> dump<BR>\n\
<INPUT TYPE=hidden NAME=extract VALUE=yes>\n\
<P>\n",
      stdout);
   }
   else
   {
      fputs (
"<INPUT TYPE=hidden NAME=do VALUE=page>\n",
      stdout);
   }

   fputs (
"<P>\n\
<INPUT TYPE=submit VALUE=process> \n",
   stdout);

   if (!DoComprehensiveMenu)
   { 
      fputs (
"<INPUT TYPE=checkbox NAME=Ymag VALUE=2> double-height graph &nbsp;&nbsp;\n",
         stdout);
   }

   fprintf (stdout,
"<INPUT TYPE=reset VALUE=reset>\n\
</FORM>\n\
</BLOCKQUOTE>\n\
<P>\n");

   SelectorPtr = NULL;

   ButtonBar (2);

   fprintf (stdout,
"</BODY>\n\
</HTML>\n");
}

/*****************************************************************************/
/*
Output a list of node names that data can be selected from.  Get these either 
from a command-line-supplied list or from data files present for the current 
day.
*/ 

SelectNodeNames ()

{
   int  idx;
   char  *CheckedPtr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SelectNodeNames()\n");

   if (SpiNodeNames[0][0])
   {
      NodeCount = 0;
      for (idx = 0; SpiNodeNames[idx][0]; idx++) NodeCount++;
      if (NodeCount <= MAX_NODE_COUNT_BEFORE_SELECT)
      {
         CheckedPtr = " CHECKED";
         for (idx = 0; SpiNodeNames[idx][0]; idx++)
         {
            fprintf (stdout,
               "<INPUT TYPE=radio NAME=list_node VALUE=\"%s\"%s> %s<BR>\n",
               SpiNodeNames[idx], CheckedPtr, SpiNodeNames[idx]);
            CheckedPtr = "";
         }
      }
      else
      {
         CheckedPtr = " SELECTED";
         fprintf (stdout, "<SELECT NAME=list_node SIZE=%d>\n",
                  MAX_NODE_COUNT_BEFORE_SELECT);
         for (idx = 0; SpiNodeNames[idx][0]; idx++)
         {
            fprintf (stdout, "<OPTION VALUE=\"%s\"%s> %s\n",
                     SpiNodeNames[idx], CheckedPtr, SpiNodeNames[idx]);
            CheckedPtr = "";
         }
         fputs ("</SELECT>\n", stdout);
      }
   }
   else
   {
      /* create file specification for getting node names (today's files) */
      DataFileSpecLength =
         sprintf (DataFileSpec,
                  "%sHYPERSPI_%s_*_%02.02d%02.02d%02.02d.DAT",
                  HyperSpiDataDirectory, HyperSpiDataVersion,
                  FromDay, FromMonth, FromYear%100);
      if (Debug) fprintf (stdout, "DataFileSpec |%s|\n", DataFileSpec);

      /* count the number of node data files */
      ProcessDataFiles (NULL, false);
      NodeCount = DataFilesFoundCount;

      if (NodeCount <= MAX_NODE_COUNT_BEFORE_SELECT)
         ProcessDataFiles (&SelectNodeNameByDataFileName, false);
      else
      {
         fprintf (stdout, "<SELECT NAME=list_node SIZE=%d>\n",
                  MAX_NODE_COUNT_BEFORE_SELECT);
         ProcessDataFiles (&SelectNodeNameByDataFileName, false);
         fputs ("</SELECT>\n", stdout);
      }
   }
}

/*****************************************************************************/
/*
Called by pointer to function each time a matching data file is returned by 
sys$search(0 in function FindDataFiles().  Output the node name associated 
with the data file.
*/ 

SelectNodeNameByDataFileName ()

{
   static char  *CheckedPtr = " CHECKED";
   static char  *SelectedPtr = " SELECTED";

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "SelectNodeNameByFileName() |%s|\n", DataFileName);

   if (NodeCount < MAX_NODE_COUNT_BEFORE_SELECT)
   {
      fprintf (stdout,
         "<INPUT TYPE=radio NAME=list_node VALUE=\"%s\"%s> %s<BR>\n",
         DataNode, CheckedPtr, DataNode);
      CheckedPtr = "";
   }
   else
   {
      fprintf (stdout, "<OPTION VALUE=\"%s\"%s> %s\n",
               DataNode, SelectedPtr, DataNode);
      SelectedPtr = "";
   }
}

/*****************************************************************************/
/*
Generates an HTML page containing information about the selected node, and the 
any additional performance information and graphs.
*/

PresentSummaryPage ()

{
   int  PeriodHours;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "PresentSummaryPage()\n");

   CgiLibResponseHeader (200, "text/html");

   fprintf (stdout,
"<HTML>\n\
<HEAD>\n\
<META NAME=\"generator\" CONTENT=\"%s\">\n\
<META NAME=\"date\" CONTENT=\"%s\">\n\
<META NAME=\"environment\" CONTENT=\"%s\">\n\
<TITLE>HyperSPI Summary: %s</TITLE>\n\
</HEAD>\n\
<BODY%s>\n\
<TABLE BORDER=0 CELLPADDING=%s CELLSPACING=0 WIDTH=100%%%s>\n\
<TR><TD ALIGN=left VALIGN=bottom>\n\
<FONT COLOR=\"%s\">\n\
&nbsp;<FONT SIZE=+1><SUP>*</SUP><B>Hyper</B></FONT><FONT SIZE=+3><B>SPI</B></FONT>\n\
</FONT>\n\
</TD><TD ALIGN=center VALIGN=bottom>\n\
<FONT COLOR=\"%s\" SIZE=+2><B>\n\
Summary for %s\n\
</B></FONT>\n\
</TD><TD ALIGN=right VALIGN=bottom>\n\
<FONT COLOR=\"%s\">\n\
%02.02d %s %d &nbsp;%02.02d:%02.02d\n\
</FONT>\n\
</TD></TR>\n\
</TABLE>\n\
<BLOCKQUOTE>\n\
<P>\n\
<TABLE BORDER=0 CELLPADDING=2 CELLSPACING=2>\n\
<TR><TH ALIGN=right>Period:</TH><TD>",
   SoftwareID, UnixDateTime, CgiEnvironmentPtr,
   CgiFormNodePtr,
   PageScheme[PS_BODYTAG],
   PageScheme[PS_HEADPADDING],
   PageScheme[PS_HEADBGCOLOR],
   PageScheme[PS_HEADTEXT],
   PageScheme[PS_HEADTEXT],
   CgiFormNodePtr,
   PageScheme[PS_HEADTEXT],
   CurrentNumTime[2], MonthName[CurrentNumTime[1]], CurrentNumTime[0],
   CurrentNumTime[3], CurrentNumTime[4]);

   fflush (stdout);
   HttpHasBeenOutput = true;

   if ((!NumberOfDays && NumberOfHours == 24) || NumberOfDays == 1)
      fputs ("1 day", stdout);
   else
   if (NumberOfDays > 1)
      fprintf (stdout, "%d days", NumberOfDays);

   if ((PeriodHours = (NumberOfHours % 24)) == 1)
      fputs ("1 hour", stdout);
   else
   if (PeriodHours > 1)
      fprintf (stdout, " %d hours", PeriodHours);

   fprintf (stdout,
", from %02.02d %s %d %02.02d:%02.02d to %02.02d %s %d %02.02d:%02.02d\n\
</TD></TR>\n",
   FromDay, MonthName[FromMonth], FromYear, FromHour, FromMinute,
   ToDay, MonthName[ToMonth], ToYear, ToHour, ToMinute);

   /* summarize the data and calculate required averages */
   SummarizeData ();

   if (DataRecordsProcessedCount)
   {
      IncludeFile (CgiFormNodePtr);

      fprintf (stdout,
"<TR><TH ALIGN=right>CPUs:</TH><TD ALIGN=left>%d</TD></TR>\n\
<TR><TH ALIGN=right>Memory:</TH><TD>%dMb physical, %dMb page space \
(%dMb</TT> total)</TD></TR>\n\
<TR><TH ALIGN=right>Processes:</TH><TD>%d average, %d peak</TD></TR>\n\
<TR><TD COLSPAN=2><FONT SIZE=-1>(X axes are marked at hour intervals)</TD></TR>\n\
</TABLE>\n\
<P>\n",
         NumberOfCPUs,
         SystemMemoryMBytes, PageSpaceMBytes,
         SystemMemoryMBytes+PageSpaceMBytes,
         AveNumberOfProcesses, MaxNumberOfProcesses);

      if (ProvidePercentCPU) PresentCPU ();
      if (ProvideMemory) PresentMemory ();
      if (ProvideProcesses) PresentProcesses ();
      if (ProvidePageFaults) PresentPageFaults ();
      if (ProvidePeakPageFaults) PresentPeakPageFaults ();
      if (ProvideHardPageFaults) PresentHardPageFaults ();
      if (ProvidePeakHardPageFaults) PresentPeakHardPageFaults ();
      if (ProvideBufferedIO) PresentBufferedIO ();
      if (ProvidePeakBufferedIO) PresentPeakBufferedIO ();
      if (ProvideDirectIO) PresentDirectIO ();
      if (ProvidePeakDirectIO) PresentPeakDirectIO ();
      if (ProvideMscpIO) PresentMscpIO ();
      if (ProvidePeakMscpIO) PresentPeakMscpIO ();
   }
   else
      fputs ("No data available.\n", stdout);

   fprintf (stdout,
"</BLOCKQUOTE>\n\
<P>\n");

   SelectorPtr = CgiScriptNamePtr;

   ButtonBar (2);

   fprintf (stdout,
"</BODY>\n\
</HTML>\n");
}

/*****************************************************************************/
/*
Place an HTML link into the HTTP output stream to get a plot-data, GIF image, 
generated dynamically by this application, embedded in the current HTML 
document.  The 'UniqueNumber()' is used to place a unique component into the 
URL of the graph GIF URL, ensuring a cached version is not retrieved.  The 
'WhatPtr' is the name of the performance count to graph.  The optional 
'IncludePtr' allows selected graphs to contain additional information.
*/ 

GraphImageLink
(
boolean InlineImage,
char *WhatPtr,
char *IncludePtr
)
{
   char  *StartPtr,
         *EndPtr;

   if (Debug) fprintf (stdout, "GraphImageLink()\n");

   if (InlineImage)
   {
      StartPtr = "<IMG ALIGN=top ALT=\"[graph]\" SRC=";
      EndPtr = ">";
   }
   else
   {
      StartPtr = "<I>(<A HREF=";
      EndPtr = ">Extract graph.</A>)</I>";
   }

   fprintf (stdout,
"%s\"%s?do=graph&unique=%s&%s=yes&include=%s\
&Xmag=%d&Ymag=%d&node=%s\
&from=%04.04d-%02.02d-%02.02d%%20%02.02d:%02.02d\
&to=%04.04d-%02.02d-%02.02d%%20%02.02d:%02.02d\
\"%s\n",
   StartPtr,
   CgiScriptNamePtr, UniqueNumber(), WhatPtr, IncludePtr, 
   XMag, YMag, CgiFormNodePtr,
   FromYear, FromMonth, FromDay, FromHour, FromMinute,
   ToYear, ToMonth, ToDay, ToHour, ToMinute,
   EndPtr);
}

/*****************************************************************************/
/*
CPU usage.  Place textual information and a graphic GIF image link into the 
current HTML document being generated.
*/ 

SectionHeading (char *String)

{
   if (Debug) fprintf (stdout, "SectionHeading() |%s|\n", String);

   fprintf (stdout,
"</BLOCKQUOTE>\n\
<P>\n\
<FONT SIZE=+2>\
&nbsp;&nbsp;<B><U>%s</U></B>\
</FONT>\n\
<P>\n\
<BLOCKQUOTE>\n",
   String);
}

/*****************************************************************************/
/*
CPU usage.  Place textual information and a graphic GIF image link into the 
current HTML document being generated.
*/ 

PresentCPU ()

{
   if (Debug) fprintf (stdout, "PresentCPU()\n");

   SectionHeading ("CPU Usage");
   GraphImageLink (true, "cpu", "peak");
   fprintf (stdout,
"<BR>Y axis is percentage. \
Histogram is average, plot is peak \
<I>(total of available %d CPU(s))</I>.\n\
<BR>Overall <TT>%d%%</TT> average, <TT>%d%%</TT> peak \
<I>(user-mode <TT>%d%%</TT> average, <TT>%d%%</TT> peak)</I>.\n",
   NumberOfCPUs,
   AvePercentCPU, PeakPercentCPU,
   AvePercentUserModeCPU, PeakPercentUserModeCPU);
   if (ExtractGraph) GraphImageLink (false, "cpu", "peak");
}

/*****************************************************************************/
/*
Page space and physical memory usage.  Place textual information and a graphic 
GIF image link into the current HTML document being generated.
*/ 

PresentMemory ()

{
   if (Debug) fprintf (stdout, "PresentMemory()\n");

   SectionHeading ("Memory Usage");
   GraphImageLink (true, "memory", "");
   fprintf (stdout,
"<BR>Y axis is percentage. \
Histogram is page space (%dMb) usage, plot is physical memory (%dMb).\n\
<BR>Overall page space <TT>%d%%</TT> average, <TT>%d%%</TT> peak; \
physical memory <TT>%d%%</TT> average, <TT>%d%%</TT> peak.\n",
   PageSpaceMBytes, SystemMemoryMBytes,
   AvePageSpacePercentInUse, MaxPageSpacePercentInUse,
   AveSystemMemoryPercentInUse, MaxSystemMemoryPercentInUse);
   if (ExtractGraph) GraphImageLink (false, "memory", "");
}

/*****************************************************************************/
/*
Number of processes.  Place textual information and a graphic GIF image link 
into the current HTML document being generated.
*/ 

PresentProcesses ()

{
   if (Debug) fprintf (stdout, "PresentProcesses()\n");

   SectionHeading ("Number of Processes");
   GraphImageLink (true, "processes", "");
   SetScaling (MaxNumberOfProcesses);
   fprintf (stdout,
"<BR>Y axis is number of processes (full-scale %d).\n\
<BR>Overall <TT>%d</TT> average <I>(maximum <TT>%d</TT>)</I>.\n",
   PlotScaleY, AveNumberOfProcesses, MaxNumberOfProcesses);
   if (ExtractGraph) GraphImageLink (false, "processes", "");
}

/*****************************************************************************/
/*
Buffered IO (non-disk, non-tape, i.e. network, terminal).  Place textual 
information and a graphic GIF image link into the current HTML document being 
generated.
*/ 

PresentBufferedIO ()

{
   char  *ExplainPtr;

   if (Debug) fprintf (stdout, "PresentBufferedIO()\n");

   SectionHeading ("Buffered IO (network, terminal, etc.)");
   if (IncludePeak)
   {
      GraphImageLink (true, "buffered_IO", "peak");
      SetScaling (PeakBufferedIO);
      ExplainPtr = " Histogram is average, plot is peak.";
   }
   else
   {
      GraphImageLink (true, "buffered_IO", "");
      SetScaling (MaxAveBufferedIO);
      ExplainPtr = "";
   }
   fprintf (stdout,
"<BR>Y axis is IOs per-second (full-scale %d).%s\n\
<BR>Overall <TT>%d</TT> average <I>(maximum <TT>%d</TT>)</I>, \
peak <TT>%d</TT>.\n",
   PlotScaleY, ExplainPtr, AveBufferedIO, MaxAveBufferedIO, PeakBufferedIO);
   if (IncludePeak)
      if (ExtractGraph) GraphImageLink (false, "buffered_IO", "peak"); else;
   else
      if (ExtractGraph) GraphImageLink (false, "buffered_IO", "");
}

/*****************************************************************************/
/*
Peak buffered IO.  Place textual information and a graphic GIF image link 
into the current HTML document being generated.
*/ 

PresentPeakBufferedIO ()

{
   if (Debug) fprintf (stdout, "PresentPeakBufferedIO()\n");

   SectionHeading ("Peak Buffered IO (network, terminal, etc.)");
   GraphImageLink (true, "peak_buffered_IO", "");
   SetScaling (PeakBufferedIO);
   fprintf (stdout,
"<BR>Y axis is IOs per-second (full-scale %d). \n\
<BR>Peak <TT>%d</TT> per-second.\n",
   PlotScaleY, PeakBufferedIO);
   if (ExtractGraph) GraphImageLink (false, "peak_buffered_IO", "");
}

/*****************************************************************************/
/*
Direct IO (disk, tape, etc).  Place textual information and a graphic GIF 
image link into the current HTML document being generated.
*/ 

PresentDirectIO ()

{
   char  *ExplainPtr;

   if (Debug) fprintf (stdout, "PresentDirectIO()\n");

   SectionHeading ("Direct IO (disk, tape, etc.)");
   if (IncludePeak)
   {
      GraphImageLink (true, "direct_IO", "peak");
      SetScaling (PeakDirectIO);
      ExplainPtr = " Histogram is average, plot is peak.";
   }
   else
   {
      GraphImageLink (true, "direct_IO", "");
      SetScaling (MaxAveDirectIO);
      ExplainPtr = "";
   }
   fprintf (stdout,
"<BR>Y axis is IOs per-second (full-scale %d).%s\n\
<BR>Overall <TT>%d</TT> average <I>(maximum <TT>%d</TT>)</I>, \
peak <TT>%d</TT>.\n",
   PlotScaleY, ExplainPtr, AveDirectIO, MaxAveDirectIO, PeakDirectIO);
   if (IncludePeak)
      if (ExtractGraph) GraphImageLink (false, "direct_IO", "peak"); else;
   else
      if (ExtractGraph) GraphImageLink (false, "direct_IO", "");
}

/*****************************************************************************/
/*
Peak direct IO.  Place textual information and a graphic GIF image link 
into the current HTML document being generated.
*/ 

PresentPeakDirectIO ()

{
   if (Debug) fprintf (stdout, "PresentPeakDirectIO()\n");

   SectionHeading ("Peak Direct IO (disk, tape, etc.)");
   GraphImageLink (true, "peak_direct_IO", "");
   SetScaling (PeakDirectIO);
   fprintf (stdout,
"<BR>Y axis is IOs per-second (full-scale %d). \n\
<BR>Peak <TT>%d</TT> per-second.\n",
   PlotScaleY, PeakDirectIO);
   if (ExtractGraph) GraphImageLink (false, "peak_direct_IO", "");
}

/*****************************************************************************/
/*
MSCP IO.  Place textual information and a graphic GIF image link into the 
current HTML document being generated.
*/ 

PresentMscpIO ()

{
   char  *ExplainPtr;

   if (Debug) fprintf (stdout, "PresentMscpIO()\n");

   SectionHeading ("MSCP IO (served disk and tape)");
   if (IncludePeak)
   {
      GraphImageLink (true, "mscp_IO", "peak");
      SetScaling (PeakMscpIO);
      ExplainPtr = " Histogram is average, plot is peak.";
   }
   else
   {
      GraphImageLink (true, "mscp_IO", "");
      SetScaling (MaxAveMscpIO);
      ExplainPtr = "";
   }
   fprintf (stdout,
"<BR>Y axis is IOs per-second (full-scale %d).%s\n\
<BR>Overall <TT>%d</TT> average <I>(maximum <TT>%d</TT>)</I>, \
peak <TT>%d</TT>.\n",
   PlotScaleY, ExplainPtr, AveMscpIO, MaxAveMscpIO, PeakMscpIO);
   if (IncludePeak)
      if (ExtractGraph) GraphImageLink (false, "mscp_IO", "peak"); else;
   else
      if (ExtractGraph) GraphImageLink (false, "mscp_IO", "");
}

/*****************************************************************************/
/*
Peak mscp IO.  Place textual information and a graphic GIF image link 
into the current HTML document being generated.
*/ 

PresentPeakMscpIO ()

{
   if (Debug) fprintf (stdout, "PresentPeakMscpIO()\n");

   SectionHeading ("Peak MSCP IO (served disk and tape)");
   GraphImageLink (true, "peak_mscp_IO", "");
   SetScaling (PeakMscpIO);
   fprintf (stdout,
"<BR>Y axis is IOs per-second (full-scale %d). \n\
<BR>Peak <TT>%d</TT> per-second.\n",
   PlotScaleY, PeakMscpIO);
   if (ExtractGraph) GraphImageLink (false, "peak_mscp_IO", "");
}

/*****************************************************************************/
/*
Page faulting IO.  Place textual information and a graphic GIF image link into 
the current HTML document being generated.
*/ 

PresentPageFaults ()

{
   char  *ExplainPtr;

   if (Debug) fprintf (stdout, "PresentPageFaults()\n");

   SectionHeading ("Paging (Soft)");
   if (IncludePeak)
   {
      GraphImageLink (true, "faults", "peak");
      SetScaling (PeakPageFaults);
      ExplainPtr = " Histogram is average, plot is peak.";
   }
   else
   {
      GraphImageLink (true, "faults", "");
      SetScaling (MaxAvePageFaults);
      ExplainPtr = "";
   }
   fprintf (stdout,
"<BR>Y axis is page faults per-second (full-scale %d).%s\n\
<BR>Overall <TT>%d</TT> average <I>(maximum <TT>%d</TT>)</I>, \
peak <TT>%d</TT>.\n",
   PlotScaleY, ExplainPtr, AvePageFaults, MaxAvePageFaults, PeakPageFaults);
   if (IncludePeak)
      if (ExtractGraph) GraphImageLink (false, "faults", "peak"); else;
   else
      if (ExtractGraph) GraphImageLink (false, "faults", "");
}

/*****************************************************************************/
/*
Peak paging to disk.  Place textual information and a graphic GIF image link 
into the current HTML document being generated.
*/ 

PresentPeakPageFaults ()

{
   if (Debug) fprintf (stdout, "PresentPeakPageFaults()\n");

   SectionHeading ("Peak Paging (Soft)");
   GraphImageLink (true, "peak_faults", "");
   SetScaling (PeakPageFaults);
   fprintf (stdout,
"<BR>Y axis is page faults per-second (full-scale %d). \n\
<BR>Peak <TT>%d</TT> per-second.\n",
   PlotScaleY, PeakPageFaults);
   if (ExtractGraph) GraphImageLink (false, "peak_faults", "");
}

/*****************************************************************************/
/*
Page faulting IO.  Place textual information and a graphic GIF image link into 
the current HTML document being generated.
*/ 

PresentHardPageFaults ()

{
   char  *ExplainPtr;

   if (Debug) fprintf (stdout, "PresentHardPageFaults()\n");

   SectionHeading ("Paging To Disk");
   if (IncludePeak)
   {
      GraphImageLink (true, "hard_faults", "peak");
      SetScaling (PeakHardPageFaults);
      ExplainPtr = " Histogram is average, plot is peak.";
   }
   else
   {
      GraphImageLink (true, "hard_faults", "");
      SetScaling (MaxAveHardPageFaults);
      ExplainPtr = "";
   }
   fprintf (stdout,
"<BR>Y axis is IOs per-second (full-scale %d).%s\n\
<BR>Overall <TT>%d</TT> average <I>(maximum <TT>%d</TT>)</I>, \
peak <TT>%d</TT>.\n",
   PlotScaleY, ExplainPtr, AveHardPageFaults, MaxAveHardPageFaults,
   PeakHardPageFaults);
   if (IncludePeak)
      if (ExtractGraph) GraphImageLink (false, "hard_faults", "peak"); else;
   else
      if (ExtractGraph) GraphImageLink (false, "hard_faults", "");
}

/*****************************************************************************/
/*
Peak paging to disk.  Place textual information and a graphic GIF image link 
into the current HTML document being generated.
*/ 

PresentPeakHardPageFaults ()

{
   if (Debug) fprintf (stdout, "PresentPeakHardPageFaults()\n");

   SectionHeading ("Peak Paging To Disk");
   GraphImageLink (true, "peak_hard_faults", "");
   SetScaling (PeakHardPageFaults);
   fprintf (stdout,
"<BR>Y axis is IOs per-second (full-scale %d). \n\
<BR>Peak <TT>%d</TT> per-second.\n",
   PlotScaleY, PeakHardPageFaults);
   if (ExtractGraph) GraphImageLink (false, "peak_hard_faults", "");
}

/*****************************************************************************/
/*
Read all records for the specified node in the specified time range, 
calculating the average and maximum for each of the data categories.  Assumes 
it is only to be called once as it does not initialize any of the storage.
*/ 

SummarizeData ()

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SummarizeData()\n");

   ProcessDataFiles (&SummarizeRecord, true);

   /* the averaging must be by the actual sample rate */
   DataRecordsProcessedCount /= RecordSampleRate;

   if (!DataRecordsProcessedCount) return;

   AvePercentCPU /= DataRecordsProcessedCount;
   AvePercentUserModeCPU /= DataRecordsProcessedCount;
   AveSystemMemoryPercentInUse /= DataRecordsProcessedCount;
   AveNumberOfProcesses /= DataRecordsProcessedCount;
   AvePageSpacePercentInUse /= DataRecordsProcessedCount;
   AvePageFaults /= DataRecordsProcessedCount;
   AveHardPageFaults /= DataRecordsProcessedCount;
   AveBufferedIO /= DataRecordsProcessedCount;
   AveDirectIO /= DataRecordsProcessedCount;
   AveMscpIO /= DataRecordsProcessedCount;
}

/*****************************************************************************/
/*
Set the various accumulators according to the data in the current 'SpiRecord'.
*/

SummarizeRecord ()

{
   static unsigned long  RecordCount = 0,
                         NumberOfProcesses = 0,
                         SystemMemoryPercentInUse = 0,
                         PageSpacePercentInUse = 0,
                         PercentCPU = 0,
                         PercentUserModeCPU = 0,
                         BufferedIO = 0,
                         DirectIO = 0,
                         MscpIO = 0,
                         PageFaults = 0,
                         HardPageFaults = 0;

   unsigned long  tmp;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SummarizeRecord()\n");

   /* usually (though not unconditionally) these remain constant! */
   if (SpiRecord.NumberOfCPUs > NumberOfCPUs)
      NumberOfCPUs = SpiRecord.NumberOfCPUs;
   if (SpiRecord.SystemMemoryMBytes > SystemMemoryMBytes)
      SystemMemoryMBytes = SpiRecord.SystemMemoryMBytes;
   if (SpiRecord.PageSpaceMBytes > PageSpaceMBytes)
      PageSpaceMBytes = SpiRecord.PageSpaceMBytes;

   /*
      The percentage CPU values VERY OCCASIONALLY get slightly above
      100%.  I attribute this behaviour to slight inconsistancies
      between obtaining system times and actually obtaining the CPU
      usage data, i.e. to the delta-time used to calculate the percentage.
      I chose to "massage" the data at the display end rather than the
      collection/recording end so that this behaviour could be monitored
      by using the "dump" facility to examine the actual data.
   */
   if (SpiRecord.PercentCPU > 100) SpiRecord.PercentCPU = 100;
   if (SpiRecord.PeakPercentCPU > 100) SpiRecord.PeakPercentCPU = 100;
   if (SpiRecord.PercentUserModeCPU > 100)
      SpiRecord.PercentUserModeCPU = 100;
   if (SpiRecord.PeakPercentUserModeCPU > 100)
      SpiRecord.PeakPercentUserModeCPU = 100;

   /* peaks are always taken as absolutes! */
   if (SpiRecord.PeakPercentCPU > PeakPercentCPU)
      PeakPercentCPU = SpiRecord.PeakPercentCPU;
   if (SpiRecord.PeakPercentUserModeCPU > PeakPercentUserModeCPU)
      PeakPercentUserModeCPU = SpiRecord.PeakPercentUserModeCPU;
   if (SpiRecord.PeakBufferedIO > PeakBufferedIO)
      PeakBufferedIO = SpiRecord.PeakBufferedIO;
   if (SpiRecord.PeakDirectIO > PeakDirectIO)
      PeakDirectIO = SpiRecord.PeakDirectIO;
   if (SpiRecord.PeakMscpIO > PeakMscpIO)
      PeakMscpIO = SpiRecord.PeakMscpIO;
   if (SpiRecord.PeakPageFaults > PeakPageFaults)
      PeakPageFaults = SpiRecord.PeakPageFaults;
   if (SpiRecord.PeakHardPageFaults > PeakHardPageFaults)
      PeakHardPageFaults = SpiRecord.PeakHardPageFaults;

   if (RecordSampleRate > 1)
   {
      /* X axis compression, i.e. less than one record per plot point */
      PercentCPU += SpiRecord.PercentCPU;
      PercentUserModeCPU += SpiRecord.PercentUserModeCPU;
      NumberOfProcesses += SpiRecord.NumberOfProcesses;
      SystemMemoryPercentInUse += SpiRecord.SystemMemoryPercentInUse;
      PageSpacePercentInUse += SpiRecord.PageSpacePercentInUse;
      BufferedIO += SpiRecord.BufferedIO;
      DirectIO += SpiRecord.DirectIO;
      MscpIO += SpiRecord.MscpIO;
      PageFaults += SpiRecord.PageFaults;
      HardPageFaults += SpiRecord.HardPageFaults;

      if (++RecordCount < RecordSampleRate) return;

      /* average by dividing the accumlated values by the record sample rate */
      SpiRecord.PercentCPU = PercentCPU / RecordCount;
      SpiRecord.PercentUserModeCPU = PercentUserModeCPU / RecordCount;
      SpiRecord.NumberOfProcesses = NumberOfProcesses / RecordCount;
      SpiRecord.SystemMemoryPercentInUse =
         SystemMemoryPercentInUse / RecordCount;
      SpiRecord.PageSpacePercentInUse = PageSpacePercentInUse / RecordCount;
      SpiRecord.BufferedIO = BufferedIO / RecordCount;
      SpiRecord.DirectIO = DirectIO / RecordCount;
      SpiRecord.MscpIO = MscpIO / RecordCount;
      SpiRecord.PageFaults = PageFaults / RecordCount;
      SpiRecord.HardPageFaults = HardPageFaults / RecordCount;

      /* reset the accumulators to zero */
      RecordCount =
      PercentCPU = PercentUserModeCPU = NumberOfProcesses =
      SystemMemoryPercentInUse = PageSpacePercentInUse =
      BufferedIO = DirectIO = MscpIO =
      PageFaults = HardPageFaults = 0;
   }

   /* this will need to be divided by the number of records processed */
   AvePercentCPU += SpiRecord.PercentCPU;

   /* this will need to be divided by the number of records processed */
   AvePercentUserModeCPU += SpiRecord.PercentUserModeCPU;

   /* this will need to be divided by the number of records processed */
   AveNumberOfProcesses += SpiRecord.NumberOfProcesses;
   if (SpiRecord.NumberOfProcesses > MaxNumberOfProcesses)
      MaxNumberOfProcesses = SpiRecord.NumberOfProcesses;

   /* this will need to be divided by the number of records processed */
   AveSystemMemoryPercentInUse += SpiRecord.SystemMemoryPercentInUse;
   if (SpiRecord.SystemMemoryPercentInUse > MaxSystemMemoryPercentInUse)
      MaxSystemMemoryPercentInUse = SpiRecord.SystemMemoryPercentInUse;

   /* this will need to be divided by the number of records processed */
   AvePageSpacePercentInUse += SpiRecord.PageSpacePercentInUse;
   if (SpiRecord.PageSpacePercentInUse > MaxPageSpacePercentInUse)
      MaxPageSpacePercentInUse = SpiRecord.PageSpacePercentInUse;

   AveBufferedIO += (tmp = SpiRecord.BufferedIO / 60);
   if (tmp > MaxAveBufferedIO) MaxAveBufferedIO = tmp;

   AveDirectIO += (tmp = SpiRecord.DirectIO / 60);
   if (tmp > MaxAveDirectIO) MaxAveDirectIO = tmp;

   AveMscpIO += (tmp = SpiRecord.MscpIO / 60);
   if (tmp > MaxAveMscpIO) MaxAveMscpIO = tmp;

   AvePageFaults += (tmp = SpiRecord.PageFaults / 60);
   if (tmp > MaxAvePageFaults) MaxAvePageFaults = tmp;

   AveHardPageFaults += (tmp = SpiRecord.HardPageFaults / 60);
   if (tmp > MaxAveHardPageFaults) MaxAveHardPageFaults = tmp;
}

/*****************************************************************************/
/*
Set the Y axis scaling factor and maximum value represented on the X axis 
according to the specified value (maximum out of the to-be plotted data).
*/

SetScaling (int  Maximum)

{
   if (Debug) fprintf (stdout, "SetScaling() %d\n", Maximum);

   if (Maximum <= 10)
   {
      ScalingFactorY = 10.0;
      PlotScaleY = 10;
   }
   else
   if (Maximum <= 25)
   {
      ScalingFactorY = 4.0;
      PlotScaleY = 25;
   }
   else
   if (Maximum <= 50)
   {
      ScalingFactorY = 2.0;
      PlotScaleY = 50;
   }
   else
   if (Maximum <= 100)
   {
      ScalingFactorY = 1.0;
      PlotScaleY = 100;
   }
   else
   if (Maximum <= 250)
   {
      ScalingFactorY = 0.4;
      PlotScaleY = 250;
   }
   else
   if (Maximum <= 500)
   {
      ScalingFactorY = 0.2;
      PlotScaleY = 500;
   }
   else
   if (Maximum <= 1000)
   {
      ScalingFactorY = 0.1;
      PlotScaleY = 1000;
   }
   else
   if (Maximum <= 2500)
   {
      ScalingFactorY = 0.04;
      PlotScaleY = 2500;
   }
   else
   if (Maximum <= 5000)
   {
      ScalingFactorY = 0.02;
      PlotScaleY = 5000;
   }
   else
   if (Maximum <= 10000)
   {
      ScalingFactorY = 0.01;
      PlotScaleY = 10000;
   }
   else
   if (Maximum <= 25000)
   {
      ScalingFactorY = 0.004;
      PlotScaleY = 25000;
   }
   else
   if (Maximum <= 50000)
   {
      ScalingFactorY = 0.002;
      PlotScaleY = 50000;
   }
   else
   if (Maximum <= 100000)
   {
      ScalingFactorY = 0.001;
      PlotScaleY = 100000;
   }
   else
   {
      ScalingFactorY = 0.0001;
      PlotScaleY = 1000000;
   }

   if (Debug) fprintf (stdout, "ScalingFactorY: %f\n", ScalingFactorY);
}

/*****************************************************************************/
/*
The application is being called to plot a graph of the data of the specified 
node in the specified time range, and generate a GIF image of that graph and 
return it to the browser.
*/

GraphData ()

{
   int  Xcnt, Ycnt, AtX, AtY;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GraphData()\n");

#ifdef GIF_EXPIRED

   CgiLibResponseHeader (200, "image/gif",
                         "Expires: Fri, 13 Jan 1978 14:30:00 GMT\n");

#else

   CgiLibResponseHeader (200, "image/gif");

#endif
   fflush (stdout);

   SizeOfPlotX = NumberOfHours * 60 * XMag / RecordSampleRate;
   SizeOfPlotY = 100 * YMag;
   if (Debug) fprintf (stdout, "size %d,%d\n", SizeOfPlotX, SizeOfPlotY);

   HaveDataY = SizeOfMarginY / 6;

   PlotAllocate (SizeOfPlotX + SizeOfMarginX*3,
                 SizeOfPlotY + SizeOfMarginY + SizeOfMarginY/2);

   if (ProvidePercentCPU || ProvidePercentUserModeCPU)
   {
      /* no scaling to be done for the X axis so plot it now */
      PlotAxes (0);
      /* process specified data calling 'GraphRecordCPU()' for each record */
      ProcessDataFiles (&GraphRecordCPU, true);
   }
   else
   if (ProvideMemory)
   {
      /* no scaling to be done for the X axis so plot it now */
      PlotAxes (0);
      /* process specified data calling 'GraphRecordMemory()' for each record */
      ProcessDataFiles (&GraphRecordMemory, true);
   }
   else
   if (ProvideBufferedIO || ProvidePeakBufferedIO ||
       ProvideDirectIO ||  ProvidePeakDirectIO ||
       ProvideMscpIO || ProvidePeakMscpIO ||
       ProvidePageFaults || ProvidePeakPageFaults  ||
       ProvideHardPageFaults || ProvidePeakHardPageFaults ||
       ProvideProcesses)
   {
      /* summarize the data to determine the required Y axis */
      SummarizeData ();

      /* set the scaling factor for the Y axis */
      if (ProvideBufferedIO)
         if (IncludePeak)
            SetScaling (PeakBufferedIO);
         else
            SetScaling (MaxAveBufferedIO);
      else 
      if (ProvidePeakBufferedIO)
         SetScaling (PeakBufferedIO);
      else 
      if (ProvideDirectIO)
         if (IncludePeak)
            SetScaling (PeakDirectIO);
         else
            SetScaling (MaxAveDirectIO);
      else 
      if (ProvidePeakDirectIO)
         SetScaling (PeakDirectIO);
      else 
      if (ProvideMscpIO)
         if (IncludePeak)
            SetScaling (PeakMscpIO);
         else
            SetScaling (MaxAveMscpIO);
      else 
      if (ProvidePeakMscpIO)
         SetScaling (PeakMscpIO);
      else 
      if (ProvidePageFaults)
         if (IncludePeak)
            SetScaling (PeakPageFaults);
         else
            SetScaling (MaxAvePageFaults);
      else 
      if (ProvidePeakPageFaults)
         SetScaling (PeakPageFaults);
      else 
      if (ProvideHardPageFaults)
         if (IncludePeak)
            SetScaling (PeakHardPageFaults);
         else
            SetScaling (MaxAveHardPageFaults);
      else 
      if (ProvidePeakHardPageFaults)
         SetScaling (PeakHardPageFaults);
      else 
      if (ProvideProcesses)
         SetScaling (MaxNumberOfProcesses);

      /* plot the X and Y axes after summarizing the data and setting scaling */
      PlotAxes (0);

      if (ProvideProcesses)
      {
         /* process data calling 'GraphRecordProcesses()' for each record */
         ProcessDataFiles (&GraphRecordProcesses, true);
      }
      else
      {
         /* process data calling 'GraphRecordRange()' for each record */
         ProcessDataFiles (&GraphRecordRange, true);
      }
   }
   else
   {
      CgiLibResponseError (FI_LI, 0, "Internal error.");
      exit (SS$_NORMAL);
   }

   /* if the current time is within the graphed X axis timescale */
   if (CurrentMinuteFromStartOfPeriod < NumberOfHours * 60)
   {
      /* place a horizontal mark at the end of the data available line */
      AtX = (CurrentMinuteFromStartOfPeriod / RecordSampleRate) * XMag;
      AtY = SizeOfMarginY / 4;
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = -3; Ycnt < 3 + YMag; Ycnt++)
            PlotPixel (0, AtX+Xcnt, HaveDataY+Ycnt, SizeOfMarginX, 0);
   }

   /* generate and return the GIF image to the browser */
   GifImage ("");
}

/*****************************************************************************/
/*
Plot a CPU record.  Either total or user-mode usage, as a line from zero to 
the percentage value uasage.  Optionally and additionally, plot peak total or 
user-mode usage, or total usage (for use with the user-mode line plot) as a 
single point.  Plot has a Y axis fixed to representPeak 100 percent.
*/ 

GraphRecordCPU ()

{
   static int  RecordCount = 0;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GraphRecordCPU()\n");

   /*
      The percentage CPU values VERY OCCASIONALLY get slightly above
      100%.  I attribute this behaviour to slight inconsistancies
      between obtaining system times and actually obtaining the CPU
      usage data, i.e. to the delta-time used to calculate the percentage.
      I chose to "massage" the data at the display end rather than the
      collection/recording end so that this behaviour could be monitored
      by using the "dump" facility to examine the actual data.
   */
   if (SpiRecord.PercentCPU > 100) SpiRecord.PercentCPU = 100;
   if (SpiRecord.PeakPercentCPU > 100) SpiRecord.PeakPercentCPU = 100;
   if (SpiRecord.PercentUserModeCPU > 100) SpiRecord.PercentUserModeCPU = 100;
   if (SpiRecord.PeakPercentUserModeCPU > 100)
      SpiRecord.PeakPercentUserModeCPU = 100;

   if (RecordSampleRate > 1)
   {
      /* X axis compression, i.e. less than one record per plot point */
      PercentCPU += SpiRecord.PercentCPU;
      PercentUserModeCPU += SpiRecord.PercentUserModeCPU;
      /* get the maximum of the peak values read */
      if (SpiRecord.PeakPercentCPU > PeakPercentCPU)
          PeakPercentCPU = SpiRecord.PeakPercentCPU;
      if (SpiRecord.PeakPercentUserModeCPU > PeakPercentUserModeCPU)
          PeakPercentUserModeCPU = SpiRecord.PeakPercentUserModeCPU;

      if (++RecordCount < RecordSampleRate) return;

      /* get the average of the values read */
      SpiRecord.PercentCPU = PercentCPU / RecordCount;
      SpiRecord.PercentUserModeCPU = PercentUserModeCPU / RecordCount;
      /* maximum of the peak values read */
      SpiRecord.PeakPercentCPU = PeakPercentCPU;
      SpiRecord.PeakPercentUserModeCPU = PeakPercentUserModeCPU;

      PercentCPU = PercentUserModeCPU = PeakPercentCPU =
         PeakPercentUserModeCPU = RecordCount = 0;
   }

   /*********************/
   /* plot the value(s) */
   /*********************/

   {
      int  Xcnt, Ycnt, ToY, AtX, AtY;

      AtX = (NumberOfMinutesIntoData / RecordSampleRate) * XMag;

      /* plot the fact we have data for this minute */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = 0; Ycnt < YMag; Ycnt++)
            PlotPixel (0, AtX+Xcnt, HaveDataY+Ycnt, SizeOfMarginX, 0);

      if (ProvidePercentCPU)
         ToY = SpiRecord.PercentCPU * YMag;
      else
      if (ProvidePercentUserModeCPU)
         ToY = SpiRecord.PercentUserModeCPU * YMag;

      /* plot as line from zero to whatever the value is */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         PlotLineY (0, 0, ToY, AtX+Xcnt, SizeOfMarginX, SizeOfMarginY);

      /* any extra data to plot? (is plotted in single pixels) */

      if (IncludePeak)
      {
         /* CPU peak usage has been requested to be included */
         if (ProvidePercentCPU)
            AtY = SpiRecord.PeakPercentCPU * YMag;
         else
         if (ProvidePercentUserModeCPU)
            AtY = SpiRecord.PeakPercentUserModeCPU * YMag;
         else;
      }
      else
      if (IncludeTotal)
      {
         /* CPU total usage has been requested to be included */
         AtY = SpiRecord.PercentCPU * YMag;
      }
      else
      {
         /* no, nothing extra to plot! */
         return;
      }

      /* plot the extra value as a point */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = 0; Ycnt < YMag; Ycnt++)
            PlotPixel (PLOT_XOR, AtX+Xcnt, AtY+Ycnt,
                       SizeOfMarginX, SizeOfMarginY);
   }
}

/*****************************************************************************/
/*
Plot page space usage as a line from zero to percentage used.  Plot physical 
memory as a point at the percentage used.  Plot has a Y axis fixed to 
represent 100 percent.
*/ 

GraphRecordMemory ()

{
   static int  RecordCount = 0;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GraphRecordMemory()\n");

   if (RecordSampleRate > 1)
   {
      /* X axis compression, i.e. less than one record per plot point */
      SystemMemoryPercentInUse += SpiRecord.SystemMemoryPercentInUse;
      PageSpacePercentInUse += SpiRecord.PageSpacePercentInUse;

      if (++RecordCount < RecordSampleRate) return;

      /* get the average of the values read */
      SpiRecord.SystemMemoryPercentInUse =
         SystemMemoryPercentInUse / RecordCount;
      SpiRecord.PageSpacePercentInUse =
         PageSpacePercentInUse / RecordCount;

      SystemMemoryPercentInUse = PageSpacePercentInUse = RecordCount = 0;
   }

   /*******************/
   /* plot the values */
   /*******************/

   {
      int  Xcnt, Ycnt, ToY, AtX, AtY;

      AtX = (NumberOfMinutesIntoData / RecordSampleRate) * XMag;

      /* plot the fact we have data for this minute */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = 0; Ycnt < YMag; Ycnt++)
            PlotPixel (0, AtX+Xcnt, HaveDataY+Ycnt, SizeOfMarginX, 0);

      ToY = SpiRecord.PageSpacePercentInUse * YMag;
      /* plot as line from zero to whatever the value is */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         PlotLineY (0, 0, ToY, AtX+Xcnt, SizeOfMarginX, SizeOfMarginY);

      AtY = SpiRecord.SystemMemoryPercentInUse * YMag;
      /* plot a point */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = 0; Ycnt < YMag; Ycnt++)
            PlotPixel (PLOT_XOR, AtX+Xcnt, AtY+Ycnt,
                       SizeOfMarginX, SizeOfMarginY);
   }
}

/*****************************************************************************/
/*
Plot the number oif processes on the system as plot-points (forming a
semi-continuous line).  Requires a variable range for the Y axis.  That is data
that  can vary considerably in the maximum value represented on the Y axis. 
Employs  a previously set variable 'ScalingFactorY' to adjust the plot values
to fall  within the Y axis.  'ScalingFactorY' must be set after scanning all of
the  specified data to determine the maximum value.
*/ 

GraphRecordProcesses ()

{
   static int  RecordCount = 0;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GraphRecordProcesses()\n");

   if (RecordSampleRate > 1)
   {
      /* X axis compression, i.e. less than one record per plot point */
      NumberOfProcesses += SpiRecord.NumberOfProcesses;
      /* return if still averaging according to sample rate */
      if (++RecordCount < RecordSampleRate) return;
      /* get the average of the values read */
      SpiRecord.NumberOfProcesses = NumberOfProcesses / RecordCount;
      /* reset the accumulator */
      NumberOfProcesses = RecordCount = 0;
   }

   /*******************/
   /* plot the value */
   /*******************/

   {
      int  Xcnt, Ycnt, AtX, AtY;

      AtX = (NumberOfMinutesIntoData / RecordSampleRate) * XMag;

      /* plot the fact we have data for this minute */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = 0; Ycnt < YMag; Ycnt++)
            PlotPixel (0, AtX+Xcnt, HaveDataY+Ycnt, SizeOfMarginX, 0);

      AtY = SpiRecord.NumberOfProcesses * ScalingFactorY * YMag;
      /* plot a point */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = 0; Ycnt < YMag; Ycnt++)
            PlotPixel (PLOT_XOR, AtX+Xcnt, AtY+Ycnt,
                       SizeOfMarginX, SizeOfMarginY);
   }
}

/*****************************************************************************/
/*
Plot values that require a variable range for the Y axis.  That is data that 
can vary considerably in the maximum value represented on the Y axis.  Employs 
a previously set variable 'ScalingFactorY' to adjust the plot values to fall 
within the Y axis.  'ScalingFactorY' must be set after scanning all of the 
specified data to determine the maximum value.
*/

GraphRecordRange ()

{
   static int  RecordCount = 0;
   static unsigned long  StaticPeakValue = 0,
                         StaticValue = 0;

    unsigned long  PeakValue,
                   Value;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GraphRecordRange()\n");

   if (ProvideBufferedIO)
   {
      Value = SpiRecord.BufferedIO;
      PeakValue = SpiRecord.PeakBufferedIO;
   }
   else
   if (ProvidePeakBufferedIO)
   {
      Value = PeakValue = SpiRecord.PeakBufferedIO;
   }
   else
   if (ProvideDirectIO)
   {
      Value = SpiRecord.DirectIO;
      PeakValue = SpiRecord.PeakDirectIO;
   }
   else
   if (ProvidePeakDirectIO)
   {
      Value = PeakValue = SpiRecord.PeakDirectIO;
   }
   else
   if (ProvideMscpIO)
   {
      Value = SpiRecord.MscpIO;
      PeakValue = SpiRecord.PeakMscpIO;
   }
   else
   if (ProvidePeakMscpIO)
   {
      Value = PeakValue = SpiRecord.PeakMscpIO;
   }
   else
   if (ProvidePageFaults)
   {
      Value = SpiRecord.PageFaults;
      PeakValue = SpiRecord.PeakPageFaults;
   }
   else
   if (ProvidePeakPageFaults)
   {
      Value = PeakValue = SpiRecord.PeakPageFaults;
   }
   else
   if (ProvideHardPageFaults)
   {
      Value = SpiRecord.HardPageFaults;
      PeakValue = SpiRecord.PeakHardPageFaults;
   }
   else
   if (ProvidePeakHardPageFaults)
   {
      Value = PeakValue = SpiRecord.PeakHardPageFaults;
   }

   if (RecordSampleRate > 1)
   {
      /* X axis compression, i.e. less than one record per plot point */
      StaticValue += Value; 
      /* get the maximum of the peak values read */
      if (PeakValue > StaticPeakValue) StaticPeakValue = PeakValue; 

      if (++RecordCount < RecordSampleRate) return;

      /* get the average of the values read */
      Value = StaticValue / RecordCount;
      /* maximum of the peak values read */
      PeakValue = StaticPeakValue;

      StaticValue = StaticPeakValue = RecordCount = 0;
   }
   if (Debug) fprintf (stdout, "Value: %d PeakValue: %d\n", Value, PeakValue);

   /*********************/
   /* plot the value(s) */
   /*********************/

   {
      int  Xcnt, Ycnt, ToY, AtX, AtY;

      AtX = (NumberOfMinutesIntoData / RecordSampleRate) * XMag;

      /* plot the fact we have data for this minute */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = 0; Ycnt < YMag; Ycnt++)
            PlotPixel (0, AtX+Xcnt, HaveDataY+Ycnt, SizeOfMarginX, 0);

      if (ProvideBufferedIO ||
          ProvideDirectIO ||
          ProvideMscpIO ||
          ProvidePageFaults ||
          ProvideHardPageFaults)
         ToY = (int)((float)(Value / 60) * ScalingFactorY) * YMag;
      else
         ToY = (int)((float)PeakValue * ScalingFactorY) * YMag;

      /* plot as line from zero to whatever the value is */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         PlotLineY (0, 0, ToY, AtX+Xcnt, SizeOfMarginX, SizeOfMarginY);

      if (IncludePeak)
         AtY = (int)((float)PeakValue * ScalingFactorY) * YMag;
      else
      {
         /* no, nothing extra to plot! */
         return;
      }

      /* plot peak value as a point */
      for (Xcnt = 0; Xcnt < XMag; Xcnt++)
         for (Ycnt = 0; Ycnt < YMag; Ycnt++)
            PlotPixel (PLOT_XOR, AtX+Xcnt, AtY+Ycnt,
                       SizeOfMarginX, SizeOfMarginY);
   }
}

/*****************************************************************************/
/*
*/

ListProcessedData ()

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ListProcessedData()\n");

   CgiLibResponseHeader (200, "text/html");

   fprintf (stdout,
"<HTML>\n\
<HEAD>\n\
<META NAME=\"generator\" CONTENT=\"%s\">\n\
<META NAME=\"date\" CONTENT=\"%s\">\n\
<META NAME=\"environment\" CONTENT=\"%s\">\n\
<TITLE>HyperSPI Processed Data</TITLE>\n\
</HEAD>\n\
<BODY%s>\n\
<TABLE BORDER=0 CELLPADDING=%s CELLSPACING=0 WIDTH=100%%%s>\n\
<TR><TD ALIGN=left VALIGN=bottom>\n\
<FONT COLOR=\"%s\">\n\
&nbsp;<FONT SIZE=+1><SUP>*</SUP><B>Hyper</B></FONT><FONT SIZE=+3><B>SPI</B></FONT>\n\
</FONT>\n\
</TD><TD ALIGN=center VALIGN=bottom>\n\
<FONT COLOR=\"%s\" SIZE=+2><B>\n\
Processed Data\n\
</B></FONT>\n\
</TD><TD ALIGN=right VALIGN=bottom>\n\
<FONT COLOR=\"%s\">\n\
%02.02d %s %d &nbsp;%02.02d:%02.02d\n\
</FONT>\n\
</TD></TR>\n\
</TABLE>\n",
   SoftwareID, UnixDateTime, CgiEnvironmentPtr,
   PageScheme[PS_BODYTAG],
   PageScheme[PS_HEADPADDING],
   PageScheme[PS_HEADBGCOLOR],
   PageScheme[PS_HEADTEXT],
   PageScheme[PS_HEADTEXT],
   PageScheme[PS_HEADTEXT],
   CurrentNumTime[2], MonthName[CurrentNumTime[1]], CurrentNumTime[0],
   CurrentNumTime[3], CurrentNumTime[4]);

   HttpHasBeenOutput = true;

   ProcessDataFiles (&ListProcessedRecord, true);

   if (DataFilesProcessedCount)
      fputs ("</PRE>\n", stdout);
   else
      fputs ("</PRE>\n<P>No matching data files.\n", stdout);

   fprintf (stdout,
"<P>\n\
<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=2 WIDTH=100%%>\n\
<TR><TD%s>\n\
<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=2>\n\
<TR><TD><FONT SIZE=-1>&nbsp;</FONT></TD></TR>\n\
</TABLE>\n\
</TD></TR>\n\
</TABLE>\n\
</BODY>\n\
</HTML>\n",
      PageScheme[PS_HEADBGCOLOR]);
}

/*****************************************************************************/
/*
*/

ListProcessedRecord ()

{
   static int  PrevDay = -1,
               PrevHour = -1;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ListProcessedRecord()\n");

   if (DataDay != PrevDay)
   {
      if (PrevDay != -1) fputs ("</PRE>\n", stdout);
      PrevDay = DataDay;
      fprintf (stdout, 
"<P>\n\
<FONT SIZE=+1><B>%s &nbsp;%02.02d %s %d</B></FONT>\n\
<P><B>%s</B>\n\
<P><PRE>",
      CgiFormNodePtr, DataDay, MonthName[DataMonth], DataYear,
      DataFileName);
   }

   if (SpiRecord.Hour != PrevHour)
   {
      if (PrevHour != -1) fputs ("\n", stdout);
      PrevHour = SpiRecord.Hour;
      fprintf (stdout,
"hh:mm  CPU usr  mem pge  b-IO peak  d-IO peak  mscp peak  \
 flts  peak  hard peak\n\
-----  --- ---  --- ---  ---- ----  ---- ----  ---- ----  \
----- -----  ---- ----\n");
   }

   fprintf (stdout,
"%02.02d:%02.02d  %3d %3d  %3d %3d  \
%4d %4d  %4d %4d  %4d %4d  %5d %5d  %4d %4d\n",
   SpiRecord.Hour, SpiRecord.Minute,
   SpiRecord.PercentCPU, SpiRecord.PercentUserModeCPU,
   SpiRecord.SystemMemoryPercentInUse,
   SpiRecord.PageSpacePercentInUse,
   SpiRecord.BufferedIO / 60, SpiRecord.PeakBufferedIO,
   SpiRecord.DirectIO / 60, SpiRecord.PeakDirectIO,
   SpiRecord.MscpIO / 60, SpiRecord.PeakMscpIO,
   SpiRecord.PageFaults / 60, SpiRecord.PeakPageFaults,
   SpiRecord.HardPageFaults / 60, SpiRecord.PeakHardPageFaults);
}

/*****************************************************************************/
/*
*/

DumpData ()

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "DumpData()\n");

   CgiLibResponseHeader (200, "text/html");

   fprintf (stdout,
"<HTML>\n\
<HEAD>\n\
<META NAME=\"generator\" CONTENT=\"%s\">\n\
<META NAME=\"date\" CONTENT=\"%s\">\n\
<META NAME=\"environment\" CONTENT=\"%s\">\n\
<TITLE>HyperSPI Data Dump</TITLE>\n\
</HEAD>\n\
<BODY%s>\n\
<TABLE BORDER=0 CELLPADDING=%s CELLSPACING=0 WIDTH=100%%%s>\n\
<TR><TD ALIGN=left VALIGN=bottom>\n\
<FONT COLOR=\"%s\">\n\
&nbsp;<FONT SIZE=+1><SUP>*</SUP><B>Hyper</B></FONT><FONT SIZE=+3><B>SPI</B></FONT>\n\
</FONT>\n\
</TD><TD ALIGN=center VALIGN=bottom>\n\
<FONT COLOR=\"%s\" SIZE=+2><B>\n\
Data Dump\n\
</B></FONT>\n\
</TD><TD ALIGN=right VALIGN=bottom>\n\
<FONT COLOR=\"%s\">\n\
%02.02d %s %d &nbsp;%02.02d:%02.02d\n\
</FONT>\n\
</TD></TR>\n\
</TABLE>\n",
   SoftwareID, UnixDateTime, CgiEnvironmentPtr,
   PageScheme[PS_BODYTAG],
   PageScheme[PS_HEADPADDING],
   PageScheme[PS_HEADBGCOLOR],
   PageScheme[PS_HEADTEXT],
   PageScheme[PS_HEADTEXT],
   PageScheme[PS_HEADTEXT],
   CurrentNumTime[2], MonthName[CurrentNumTime[1]], CurrentNumTime[0],
   CurrentNumTime[3], CurrentNumTime[4]);

   HttpHasBeenOutput = true;

   ProcessDataFiles (&DumpRecord, true);

   if (DataFilesProcessedCount)
      fputs ("</PRE>\n", stdout);
   else
      fputs ("</PRE>\n<P>No matching data files.\n", stdout);

   fprintf (stdout,
"<P>\n\
<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=2 WIDTH=100%%>\n\
<TR><TD%s>\n\
<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=2>\n\
<TR><TD><FONT SIZE=-1>&nbsp;</FONT></TD></TR>\n\
</TABLE>\n\
</TD></TR>\n\
</TABLE>\n\
</BODY>\n\
</HTML>\n",
      PageScheme[PS_HEADBGCOLOR]);
}

/*****************************************************************************/
/*
*/

DumpRecord ()

{
   static int  PrevDay = -1,
               PrevHour = -1;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "DumpRecord()\n");

   if (DataDay != PrevDay)
   {
      if (PrevDay != -1) fputs ("</PRE>\n", stdout);
      PrevDay = DataDay;
      fprintf (stdout, 
"<P>\n\
<FONT SIZE=+1><B>%s &nbsp;%02.02d %s %d</B></FONT>\n\
<P><B>%s</B>\n\
<P><PRE>",
      CgiFormNodePtr, DataDay, MonthName[DataMonth], DataYear,
      DataFileName);
   }

   if (SpiRecord.Hour != PrevHour)
   {
      fputs ("\n", stdout);
      PrevHour = SpiRecord.Hour;
      fprintf (stdout,
"hh:mm  prc  CPU  pk  usr  pk  mem pge   buf-IO    peak  \
 dir-IO    peak  mscp-IO    peak   faults    peak   hard  peak\n\
-----  ---  --- ---  --- ---  --- ---  ------- -------  \
------- -------  ------- -------  ------- -------  ----- -----\n");
   }

   fprintf (stdout,
"%02.02d:%02.02d  %3d  %3d %3d  %3d %3d  %3d %3d  \
%7d %7d  %7d %7d  %7d %7d  %7d %7d  %5d %5d\n",
   SpiRecord.Hour, SpiRecord.Minute,
   SpiRecord.NumberOfProcesses,
   SpiRecord.PercentCPU, SpiRecord.PeakPercentCPU,
   SpiRecord.PercentUserModeCPU, SpiRecord.PeakPercentUserModeCPU,
   SpiRecord.SystemMemoryPercentInUse,
   SpiRecord.PageSpacePercentInUse,
   SpiRecord.BufferedIO, SpiRecord.PeakBufferedIO,
   SpiRecord.DirectIO, SpiRecord.PeakDirectIO,
   SpiRecord.MscpIO, SpiRecord.PeakMscpIO,
   SpiRecord.PageFaults, SpiRecord.PeakPageFaults,
   SpiRecord.HardPageFaults, SpiRecord.PeakHardPageFaults);
}

/*****************************************************************************/
/*
The first parameter to this function is the address (a pointer to) the 
function used to process each file name found.
*/ 
 
ProcessDataFiles
(
int (*ProcessFileFunction)(),
boolean OpenDataFile
)
{
   char  *cptr, *sptr;

   int  status;
   char  ExpandedFileName [256],
         Scratch [256];
   struct FAB  SearchFab;
   struct RAB  SearchRab;
   struct NAM  SearchNam;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessDataFiles() |%s|\n", DataFileSpec);

   DataFilesFoundCount = DataFilesProcessedCount =
      DataRecordsReadCount = DataRecordsProcessedCount = 0;

   SearchFab = cc$rms_fab;
   SearchFab.fab$l_fna = DataFileSpec;
   SearchFab.fab$b_fns = DataFileSpecLength;
   SearchFab.fab$l_fop = FAB$M_NAM;
   SearchFab.fab$l_nam = &SearchNam;

   SearchNam = cc$rms_nam;
   SearchNam.nam$l_esa = ExpandedFileName;
   SearchNam.nam$b_ess = sizeof(ExpandedFileName)-1;
   SearchNam.nam$l_rsa = DataFileName;
   SearchNam.nam$b_rss = sizeof(DataFileName)-1;

   if (VMSnok (status = sys$parse (&SearchFab, 0, 0)))
   {
      if (Debug) fprintf (stdout, "sys$parse() %%X%08.08X\n", status);
      CgiLibResponseError (FI_LI, status, DataFileSpec);
      exit (SS$_NORMAL);
   }

   while (VMSok (status = sys$search (&SearchFab, 0, 0)))
   {
      DataFilesFoundCount++;

      SearchNam.nam$l_ver[SearchNam.nam$b_ver] = '\0';
      DataFileNameLength = SearchNam.nam$b_rsl;
      if (Debug) fprintf (stdout, "DataFileName |%s|\n", DataFileName);

      /*
         Pull the node name and time components from the data file name.
         Format: "HYPERSPI_v_node_ddmmyy.DAT"
         ("v" represents the data file version, a single digit number)
      */
      cptr = SearchNam.nam$l_name;
      /* skip "HYPERSPI_version_" */
      while (*cptr && *cptr != '_') cptr++;
      if (*cptr) cptr++;
      while (*cptr && *cptr != '_') cptr++;
      if (*cptr) cptr++;
      /* get the node name this data represents */
      sptr = DataNode;
      while (*cptr && *cptr != '_') *sptr++ = *cptr++;
      *sptr = '\0';
      if (*cptr) cptr++;
      /* get the day, month and year */
      sptr = Scratch;
      if (*cptr) *sptr++ = *cptr++;
      if (*cptr) *sptr++ = *cptr++;
      *sptr = '\0';
      DataDay = atoi (Scratch);
      sptr = Scratch;
      if (*cptr) *sptr++ = *cptr++;
      if (*cptr) *sptr++ = *cptr++;
      *sptr = '\0';
      DataMonth = atoi (Scratch);
      sptr = Scratch;
      if (*cptr) *sptr++ = *cptr++;
      if (*cptr) *sptr++ = *cptr++;
      *sptr = '\0';
      DataYear = atoi (Scratch);
      if (Debug)
         fprintf (stdout, "data-file-name |%s|%d|%d|%d|\n",
                  DataNode, DataDay, DataMonth, DataYear);

      /* filter on year, month and day */
      if (DataYear <= 90)
         DataYear += 2000;
      else
         DataYear += 1900;
      if (DataYear < FromYear || DataYear > ToYear) continue;
      if (DataYear == FromYear)
      {
         if (DataMonth < FromMonth) continue;
         if (DataMonth == FromMonth && DataDay < FromDay) continue;
      }
      if (DataYear == ToYear)
      {
         if (DataMonth > ToMonth) continue;
         if (DataMonth == ToMonth && DataDay > ToDay) continue;
      }

      DataFilesProcessedCount++;

      /* can be used to just count the number of matching files! */
      if (ProcessFileFunction == NULL) continue;

      if (OpenDataFile)
         ProcessDataFileRecords (ProcessFileFunction);
      else
         /* by pointer, call the function to process this file name */
         (*ProcessFileFunction) ();
   }
   if (Debug) fprintf (stdout, "sys$search() %%X%08.08X\n", status);

   if (SearchFab.fab$l_sts == RMS$_FNF || SearchFab.fab$l_sts == RMS$_NMF)
      return (SS$_NORMAL);

   CgiLibResponseError (FI_LI, status, DataFileSpec);
   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Open the 'DataFileName', read each record and call a specified function to 
process it, then close the file.  The first parameter to this function is the 
address (a pointer to) the function used to process the SPI data record.
*/ 

ProcessDataFileRecords (int (*ProcessDataFunction)(struct HyperSpiData*))

{
   static long  LibJulianDate = LIB$K_JULIAN_DATE;
   static unsigned short  PrevNumTime [7] = {0,0,0,0,0,0,0};

   int  status;
   unsigned long  JulDate,
                  BinTime [2];
   struct FAB  DataFileFab;
   struct RAB  DataFileRab;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessDataFileRecords() |%s|\n", DataFileName);

   DataFileFab = cc$rms_fab;
   DataFileFab.fab$b_fac = FAB$M_GET;
   DataFileFab.fab$l_fna = DataFileName;  
   DataFileFab.fab$b_fns = DataFileNameLength;
   DataFileFab.fab$b_shr = FAB$M_SHRGET | FAB$M_SHRPUT;

   if (VMSnok (status = sys$open (&DataFileFab, 0, 0)))
   {
      if (Debug) fprintf (stdout, "sys$open() %%X%08.08X\n", status);
      if (status == RMS$_FNF)
      {
         CgiLibResponseError (FI_LI, status, DataNode);
         exit (SS$_NORMAL);
      }
      CgiLibResponseError (FI_LI, status, DataNode);
      exit (SS$_NORMAL);
   }

   DataFileRab = cc$rms_rab;
   DataFileRab.rab$l_fab = &DataFileFab;
   /* 2 buffers and read ahead performance option */
   DataFileRab.rab$b_mbf = 2;
   DataFileRab.rab$l_rop = RAB$M_RAH;
   DataFileRab.rab$l_ubf = (char*)&SpiRecord;
   DataFileRab.rab$w_usz = sizeof(SpiRecord);

   if (VMSnok (status = sys$connect (&DataFileRab, 0, 0)))
   {
      if (Debug) fprintf (stdout, "sys$connect() %%X%08.08X\n", status);
      sys$close (&DataFileFab, 0, 0);
      CgiLibResponseError (FI_LI, status, DataNode);
      exit (SS$_NORMAL);
   }

   while (VMSok (status = sys$get (&DataFileRab, 0, 0)))
   {
      DataRecordsReadCount++;

      /* filter on hour and minute */
      if (SpiRecord.Day == FromDay)
      {
         if (SpiRecord.Hour < FromHour) continue;
         if (SpiRecord.Hour == FromHour && SpiRecord.Minute < FromMinute)
            continue;
      }
      if (SpiRecord.Day == ToDay)
      {
         if (SpiRecord.Hour > ToHour) continue;
         if (SpiRecord.Hour == ToHour && SpiRecord.Minute > ToMinute) continue;
      }

      if (SpiRecord.Hour != PrevNumTime[3] ||
          SpiRecord.Day != PrevNumTime[2] ||
          SpiRecord.Month != PrevNumTime[1] ||
          SpiRecord.Year != PrevNumTime[0])
      {
         PrevNumTime[3] = SpiRecord.Hour;
         PrevNumTime[2] = SpiRecord.Day;
         PrevNumTime[1] = SpiRecord.Month;
         PrevNumTime[0] = SpiRecord.Year;

         lib$cvt_vectim (&PrevNumTime, &BinTime);
         lib$cvt_from_internal_time (&LibJulianDate, &JulDate, &BinTime);
         NumberOfDaysIntoData = JulDate - FromJulianDate;
         StartMinuteOfData = NumberOfDaysIntoData * 1440;
         if (Debug)
            fprintf (stdout,
               "NumberOfDaysIntoData: %d StartMinuteOfData: %d\n",
               NumberOfDaysIntoData, StartMinuteOfData);
      }
      NumberOfMinutesIntoData = StartMinuteOfData +
                                ((SpiRecord.Hour * 60) + SpiRecord.Minute) -
                                StartMinuteOnFirstDay;

      DataRecordsProcessedCount++;

      /* by pointer, call the function to process this SPI data record */
      (*ProcessDataFunction) (&SpiRecord);
   }

   sys$close (&DataFileFab, 0, 0);

   if (status == RMS$_EOF) status = SS$_NORMAL;
   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, DataNode);
      exit (SS$_NORMAL);
   }
}

/*****************************************************************************/
/*
The specified parameter is the name of the file, excluding directory and file 
type, just the name.  Construct a full HTML file name and open it, read each 
record just passing the contents into the HTTP output stream, the close the 
file.  If the file cannot be found the status RMS$_FNF is returned, all other 
errors are reported and the image exits.
*/ 

IncludeFile (char *Name)

{
   int  status;
   char  FileName [256],
         Line [256],
         Scratch [256];
   struct FAB  FileFab;
   struct RAB  FileRab;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "IncludeFile() |%s|\n", Name);

   sprintf (FileName, "%s%s.HTML", HyperSpiDirectoryPtr, Name);
   if (Debug) fprintf (stdout, "FileName |%s|\n", FileName);

   FileFab = cc$rms_fab;
   FileFab.fab$b_fac = FAB$M_GET;
   FileFab.fab$l_fna = FileName;  
   FileFab.fab$b_fns = strlen(FileName);  
   FileFab.fab$b_shr = FAB$M_SHRGET;

   if (VMSnok (status = sys$open (&FileFab, 0, 0)))
   {
      if (Debug) fprintf (stdout, "sys$open() %%X%08.08X\n", status);
      if (status == RMS$_FNF || status == RMS$_DNF) return (status);
      CgiLibResponseError (FI_LI, status, Name);
      exit (SS$_NORMAL);
   }

   FileRab = cc$rms_rab;
   FileRab.rab$l_fab = &FileFab;
   /* 2 buffers and read ahead performance option */
   FileRab.rab$b_mbf = 2;
   FileRab.rab$l_rop = RAB$M_RAH;
   FileRab.rab$l_ubf = Line;
   FileRab.rab$w_usz = sizeof(Line)-1;

   if (VMSnok (status = sys$connect (&FileRab, 0, 0)))
   {
      if (Debug) fprintf (stdout, "sys$connect() %%X%08.08X\n", status);
      sys$close (&FileFab, 0, 0);
      CgiLibResponseError (FI_LI, status, Name);
      exit (SS$_NORMAL);
   }

   while (VMSok (status = sys$get (&FileRab, 0, 0)))
   {
      Line[FileRab.rab$w_rsz++] = '\r';
      Line[FileRab.rab$w_rsz++] = '\n';
      Line[FileRab.rab$w_rsz] = '\0';
      if (Debug) fprintf (stdout, "Line |%s|\n", Line);
      fputs (Line, stdout);
   }

   sys$close (&FileFab, 0, 0);

   if (status == RMS$_EOF) return (status);

   CgiLibResponseError (FI_LI, status, Name);
   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Return a pointer to a unique number built up of time components.  Used to 
place a unique component into the URL of a graph GIF, ensuring a cached 
version is not retrieved.
*/

char* UniqueNumber ()

{
   static char  String [16];

   unsigned long  BinTime [2];
   unsigned short  NumTime [7];

   sys$gettim (&BinTime);
   sys$numtim (&NumTime, &BinTime);

   sprintf (String, "%d%d%d%d%d%d%d",
            NumTime[0] % 10, NumTime[1], NumTime[2], NumTime[3],
            NumTime[4], NumTime[5], NumTime[6]);
   return (String);
}

/*****************************************************************************/
/*
Translate the logical name HYPERSPI_AGENT_PID containing the agent process ID
(binary) and issue a $FORCEX against the image.
*/

int ShutdownAgent ()

{
   static $DESCRIPTOR (LnmSystemDsc, "LNM$SYSTEM");
   static $DESCRIPTOR (PidLogNameDsc, "HYPERSPI_AGENT_PID");

   static unsigned long  Pid;
   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      void   *buf_addr;
      unsigned short  *short_ret_len;
   }
   LnmPidItem [] =
   {
      { sizeof(Pid), LNM$_STRING, &Pid, 0 },
      { 0,0,0,0 }
   };

   int  status;

   /*********/
   /* begin */
   /*********/

   status = sys$trnlnm (0, &LnmSystemDsc, &PidLogNameDsc, 0, &LnmPidItem);
   if (VMSok (status)) status = sys$forcex (&Pid, 0, SS$_NORMAL);
   return (status);
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
true if two strings are the same, or false if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 

boolean strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}

/*****************************************************************************/

