/*****************************************************************************/
/*
                                  WOTSUP.c

The WASD Over-The-Shoulder Uptime Picket is designed to monitor WASD in a
production environment for the purpose of alerting operations staff to
conditions which might cause that production to be adversely impacted.

What follows could be considered the WOTSUP doc (thanks to Ben Burke for this
obvious - but not originally to me - pun).  As a matter of fact it seems to fit
so well that this section should have a mascot ...

                                /\  /\ 
                               |  \ \ `\
                               |   \ \  `\
                                \   \ \   |
                                 \   \|   |
                                  \   \   |
                                   \  |\  |
                                 .-' .--. |
                               /^\  ' .-. (
                               /\    /   \ \
                               | |   |   |  |
                            .'`|O|/  |__O|.'--.
                           {   '/___,          \ 
                           `. ';(_Y_) `-'/    .'
                             \  \\\_| .'  _.-'
                              `'.`._.'.''`
                         jgs     `-..'

             http://www.geocities.com/joan_stark/textcartoon.htm

Back to the serious stuff ...

Alert triggers include:

  o  server image exit and/or startup (default)
  o  server process non-existent or suspended (default)
  o  percentage thresholds on process quotas (optional)
  o  rates of HTTP status counter change (optional)
  o  maximum period without request processing (optional)

Alert reports can be delivered via any combination of:

  o  OPCOM message
  o  MAIL (VMS and RFC822)
  o  site-specific DCL command executed in a spawned subprocess
  o  log file entry

The utility monitors the server environment by periodically polling various
server data.  The default interval is 30 seconds but can specified between 1
and 60 using the /INTERVAL qualifier.  As the utility requires access to global
memory accounting a per-system WOTSUP is required for each node to be monitored.

The following (somewhat contrived) example illustrates the format and content
of a WOTSUP report delivered via OPCOM.  Reports delivered via other mechanisms
have the same content and similar format.

  %%%%%%%%%%  OPCOM   7-MAY-2005 22:27:18.28  %%%%%%%%%%%
  Message from user SYSTEM on KLAATU
  Over-The-Shoulder (WASD_WOTSUP) reports:
  1. server STARTUP after exit of %X00000001 (%SYSTEM-S-NORMAL)
  2. pagfilcnt:395432 pgflquota:500000 79% <= 80%

Multiple items are consolidated and provided in the one report delivery if
noted during the same poll.  An item is not re-reported unless it has
subsequently returned to the non-alert threshold and then exceeded it again. 


NOTE ON MONIKER
---------------
You may think justifying the use of the cute acronym WOTSUP from a (somewhat
meaningful) functional description required no little effort, and you'd be
right!  The 'WASD' is obvious.  'Over-The-Shoulder' refers to the approach the
utility takes; standing outside the server proper and looking over its shoulder
(so to speak) at process data and accounting global memory.  'Uptime' should
also be obvious; the utility is intended to assist in maximising server uptime.
'Picket' refers to it's role as lookout in the sense of this (partial)
dictionary definition:

  picket

  n 1: a person employed to watch for something to happen [syn: lookout,
  lookout man, sentinel, sentry, watch, spotter, scout] 2: a detachment of
  troops guarding an army from surprise attack ... 4: a vehicle performing
  sentinel duty ...

There is also a very oblique reference intended to that famous signature line
belonging to the cartoon character Bugs Bunny, "Nyahh, what's up, Doc?", and in
this environment implies, "what's happening?"  Of course WASD should be up but
we want to know if it's not, or if there's any likelihood it soon might not be. 
And, yes, I know WOTSDOWN might be a more accurate moniker but apart from the
slightly negative connotation it's almost impossible to generate such an
acronym without really torturing the intended meaning :-)  So, WOTSUP?


MULTIPLE INSTANCES
------------------
WOTSUP will monitor the quotas and status of all server processes associated
with multi-instance sites.  It will also report on process exit (e.g.
STOP/IMAGE/ID=, and of course error exits) and process disappearance (e.g.
STOP/ID=).  With multi-instance restarts, exit data in the global section is
shared between multiple server processes, and so this will always reflect the
most recent process exit.  Generally WOTSUP reports on multi-instance restarts
and single instance failures in a relatively consistent and easily
understandable manner.  It will provide a more complex sequence of reports and
report items where the number of instances is being administratively increased
or decreased, and this can require more careful interpretation.


PROCESS QUOTAS
--------------
Quotas are checked by comparing the $GETJPI count against the limit as a
percentage.  The following quotas are checked:

  ASTlm  BIOlm  BYTlm  DIOlm  Enqlm  Fillm  Pgflquo  Tqlm

Should any one of more of these have reached the minimum available percentage
as specified by the /QUOTA qualifier a report is issued.  The default is 30%.
Individual process quotas may be set using the appropriate keyword with the
/QUOTA qualifier specifying the individual quota percentage, though this
granularity of control should seldom be necessary.  For example

  /QUOTA=(20,BYTLM:20,PGFLQUO:50)

sets all quotas to a minimum of 20% but specifically sets BYTlm to 20% and
Pgflquo to 50%.


HTTP RESPONSE STATUS
--------------------
The command-line /HTTP qualifier allows specified HTTP status groups and
individual status codes to be monitored.  Status *groups* available are 2xx,
3xx, 4xx and 5xx, although the only meaningful ones for such attention are the
4xx (not found, forbidden, etc.) and 5xx (server errors).  Special categories
are 0xx, those requests that fail before any significant processing can be
accomplished, and 403, forbidden responses.  Individual status codes might
include 409, 500, 502, etc.

NOTE: setting an individual status code in any groups resets that group's
monitoring so to monitor any individual status code in a group requires that
all codes of interest in that group be specified.  There is one exception to
this (for backwards compatibility) 403 (forbidden).

The specified groups/codes are monitored every sixty seconds and any increase
beyond the specified threshold is reported.  Groups and thresholds are provided
to the qualifier via a single digit and using the following syntax.

  <HTTP-status-group>:<threshold-integer>

An example would be

  5:10

which would intrepreted as; for status group 5xx (500, 501, 502, etc.) any
increase of ten or more over any one minute should be reported.  The qualifier
allows multiple values to be specified as in the following example

  /HTTP=(4:20,403:10,5:10)

where a threshold of 20 is set for the 4xx group, 10 specifically for the 403
status (forbidden), and 10 for the 5xx group.

To report on individual status codes specify a full three digits and the
threshold using the same syntax as for status groups.

  <HTTP-status-code>:<threshold-integer>

This example monitors forbidden access (50 per minute), server internal errors
(any 1 in a minute), bad gateway (10 in the minute) and service unavailable (2
per minute).

  /HTTP=(403:50,500:1,502:10,503:2)


SERVER GOING QUIET
------------------
The /QUIET qualifier allows a maximum expected period with no requests to be
specified in seconds.  (Somewhat obviously) if no requests are received for the
period a report is issued.  This period may be exceeded by up to /INTERVAL
seconds.


SPAWNED DCL REPORTING
---------------------
The command provided by the /SPAWN qualifier may be anything the subprocess can
execute.  It would most commonly be the execution of a DCL procedure that would
deliver the reporting in some site-specific manner.  For example:

  /SPAWN=@PRODUCTION-DISK:[SUPPORT]WOTSUP_REPORT.COM

The spawned command accesses the lines of the report for delivery via the
multi-valued system-table logical name WASD_WOTSUP_REPORT.  The following DCL
commands provide an example of how to retrieve the values from such a logical.

  $ IDX = 0
  $ REPORT_LOOP:
  $    LINE = F$TRNLNM ("WASD_WOTSUP_REPORT", "LNM$SYSTEM", IDX)
  $    IF LINE .EQS. "" THEN GOTO END_REPORT_LOOP
  $    WRITE SYS$OUTPUT LINE
  $    IDX = IDX + 1
  $    GOTO REPORT_LOOP
  $ END_REPORT_LOOP:
  $!(all WOTSUP reports should contain at least four lines)
  $ IF IDX .LT. 4 THEN GOTO REPORT_LOGICAL_NAME_ERROR


DETACHED PROCESS
----------------
The WOTSUP utility is intended to be run as a detached process.  The DCL
procedure WOTSUP.COM provides such an infrastructure.  WOTSUP can also be run
at the command line to provide some control over such a detached process.

  $ WOTSUP /DO=RESTART
  $ WOTSUP /DO=EXIT

The first causes the WOTSUP image to exit and the WOTSUP.COM procedure to
restart it.  In this way a changed WASD_WOTSUP_PARAM logical name value can be
reread by the utility changing it's run-time behaviour.  The second causes the
image to exit and the detached process to delete itself.


USAGE EXAMPLES
--------------
When the server is noticed to have exited, started or restarted, or the server
process no longer exists (this set of events are always reported), and the
first reporting to OPCOM CENTRAL and CLUSTER, the second to OPER1.

  $ WOTSUP /OPCOM
  $ WOTSUP /OPCOM=OPER1

Report server exit, startup, etc., any server process quota reaching 30% (the
default) or less of maximum (the second example down to 15%), and reporting to
OPCOM CLUSTER only as well as mailing a message to OPERATOR account.

  $ WOTSUP /QUOTA /OPCOM=CLUSTER /MAIL=OPERATOR
  $ WOTSUP /QUOTA=15 /OPCOM=CLUSTER /MAIL=OPERATOR

These examples add HTTP status reporting, the first any one per minute 5xx
(server error) status, the second any one internal server error (500), ten bad
gateways (502), any server unavailbilities (503), along with any 403
(forbidden) increase, and logging the reports to a site-specific file in
addition to those destinations in the above examples.

  $ WOTSUP /QUOTA=15 /HTTP=5:1 /OPCOM=CLUSTER /MAIL=OPERATOR -
           /LOG=PRODUCTION_LOGS:WOTSUP_REPORTS.LOG
  $ WOTSUP /QUOTA=15 /HTTP=(403:1,500:1,502:10,503:2) /OPCOM=CLUSTER -
           /MAIL=OPERATOR /LOG=PRODUCTION_LOGS:WOTSUP_REPORTS.LOG

This example expands the one above, adding a server quiet period of three
minutes, reporting to two mail destinations (one via RFC822) and spawning a
custom DCL command to report using a site-specific mechanism.

  $ WOTSUP /QUOTA=15 /HTTP=(5:10,403:1) /QUIET=180 /OPCOM=CLUSTER -
           /MAIL="OPERATOR,prod-mgr@home.net" -
           /LOG=PRODUCTION_LOGS:WOTSUP_REPORTS.LOG -
           /SPAWN=@PRODUCTION_DCL:WOTSUP_REPORTS.COM

This final example reports to all of the specified destinations as a
pre-production, or in-production change, integrity check of the reporting.

  $ WOTSUP /CHECK -
           /MAIL="OPERATOR,prod-mgr@home.net" -
           /LOG=PRODUCTION_LOGS:WOTSUP_REPORTS.LOG -
           /SPAWN=@PRODUCTION_DCL:WOTSUP_REPORTS.COM


LOGICAL NAMES
-------------
These are read each time they are required, and therefore can be modified
without restarting the utility, in contrast to the command-line qualifiers that
are only read at utility startup.  If present they override anything supplied
at the command-line.  These are accessed using LNM$FILE_DEV and so can be
defined in process, job or system tables (etc.)  Using the system-level table
allows the value to be modified without restarting the utility.

WASD_WOTSUP_LOG        same as /LOG qualifier
WASD_WOTSUP_MAIL       same as /MAIL qualifier
WASD_WOTSUP_OPCOM      same as /OPCOM qualifier
WASD_WOTSUP_SHUSH      if defined reports only to log and OPCOM
WASD_WOTSUP_SPAWN      same as /SPAWN qualifier

The following logical name is used by non-interactive WOTSUP in place of any
parameters supplied by the command-line.  Defining this as a system-level
logical allows the logical value to be modified and the utility restarted
(obviating the need to modify WOTSUP wrapper procedures).

WASD_WOTSUP_PARAM      if present used instead of command-line parameters

The following system-level logical names are created and used by the utility.

WASD_WOTSUP_PID        the PID of an executing, non-interactive WOTSUP
WASD_WOTSUP_REPORT     this is a multi-value logical created by WOTSUP in
                       the SYSTEM table containing the lines of report text
                       available to a spawned reporting DCL command


QUALIFIERS
----------
/CHECK                  interactive check of /MAIL, /OPCOM and /SPAWN reporting
/DBUG                   turns on all "if (Debug)" statements
/DO=<keyword>           controls a detached process; DELETE, EXIT, RESTART
/EXIT=<integer>         seconds wait after exit without restart before alert
/HTTP=<number>:<delta>  HTTP status group/code counter threshold (per minute)
/HELP                   display brief usage information
/INTERVAL=<integer>     seconds between checking (default 15)
/LOG=<filename>         write alerts to this file as well
/MAIL[=<address(es)>]   one or more comma-separated email addresses
                        (default is SYSTEM, can be RFC822 addresses)
/OPCOM[=<target>]       target is CENTRAL, PRINTER, ... OPER12
                        (default is CENTRAL)
/QUIET[=<integer>]      maximum seconds without any request being processed
                        (default is 300 - 5 minutes)
/QUOTA=<integer>        alert when a process quota gets below this percentage
/SPAWN=<string>         when a report is required execute this command
                        (there is a parameter as P1 containing a string)


REQUIRED PRIVILEGES
-------------------
WORLD      for access to the server process' JPI data
SYSLCK     for access to system locks for multi-instance support
SYSPRV     for access to the global section containing the server data
           to create logical names in the LNM$SYSTEM table


BUILD DETAILS
-------------
See BUILD_WOTSUP.COM


COPYRIGHT
---------
*/
char  CopyrightInfo [] =
"Copyright (C) 2005-2022 Mark G.Daniel.\n\
\n\
Licensed under the Apache License, Version 2.0 (the \"License\");\n\
you may not use this file except in compliance with the License.\n\
You may obtain a copy of the License at\n\
\n\
   http://www.apache.org/licenses/LICENSE-2.0\n\
\n\
Unless required by applicable law or agreed to in writing, software\n\
distributed under the License is distributed on an \"AS IS\" BASIS,\n\
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\
See the License for the specific language governing permissions and\n\
limitations under the License.\n";
/*


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
18-DEC-2020  MGD  v1.1.6, VAX no longer implemented
                          use native 64bit data (rather than ulong[2])
07-FEB-2016  MGD  v1.1.5, WASD v11 |LastExitBinTime| becomes |LastExitTime64|
20-AUG-2013  MGD  v1.1.4, SysTrnLnm() becomes SysTrnLnm2() to avoid WASD.H
                            function prototype collision (differing arguments)
02-JAN-2013  MGD  v1.1.3, bugfix; ReportLogFile() log file name pointer
30-AUG-2009  MGD  v1.1.2, minor mod for WASD v10.0
                          WASD_WOTSUP_SHUSH logical name
10-FEB-2008  MGD  v1.1.1, OverTheShoulder() check for GBLSEC mismatch
24-SEP-2006  MGD  v1.1.0, improve granularity of HTTP status code monitoring
                            enhances /HTTP= parameters and reporting available
                          monitor all of multiple instance processes
                          modify email subject line to indicate report item(s)
                          increase default interval from 15 to 30 seconds
                          (all require support available with WASD v9.2.0)
24-APR-2006  MGD  v1.0.1, bugfix; GetParameters() /QUIET parameter test
07-MAY-2005  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "v1.1.6"
#define SOFTWARENM "WOTSUP"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  error VAX no longer implemented
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

/* VMS related header files */
#include <descrip.h>
#include <dvidef.h>
#include <iodef.h>
#include <jpidef.h>
#include <lckdef.h>
#include <libclidef.h>
#include <libdtdef.h>
#include <lkidef.h>
#include <lnmdef.h>
#include <maildef.h>
#include <mail$routines.h>
#include <psldef.h>
#include <prvdef.h>
#include <secdef.h>
#include <ssdef.h>
#include <stsdef.h>
#include <syidef.h>

/* this header file contains the accounting structure definitions */
#include "../httpd/wasd.h"

/* only with 7.3-2 and later? */
#ifndef SS$_FORCEX
#  define SS$_FORCEX 11228
#endif

/* these make it easier to have both VAX and Alpha declarations */
#define OPC$_RQ_RQST 3
#define OPC$M_OPR_CENTRAL 0x1
#define OPC$M_OPR_PRINTER 0x2
#define OPC$M_OPR_TAPES 0x4
#define OPC$M_OPR_DISKS 0x8
#define OPC$M_OPR_DEVICES 0x10
#define OPC$M_OPR_CARDS 0x20
#define OPC$M_OPR_NETWORK 0x40
#define OPC$M_OPR_CLUSTER 0x80
#define OPC$M_OPR_SECURITY 0x100
#define OPC$M_OPR_REPLY 0x200
#define OPC$M_OPR_SOFTWARE 0x400
#define OPC$M_OPR_LICENSE 0x800
#define OPC$M_OPR_USER1 0x1000
#define OPC$M_OPR_USER2 0x2000
#define OPC$M_OPR_USER3 0x4000
#define OPC$M_OPR_USER4 0x8000
#define OPC$M_OPR_USER5 0x10000
#define OPC$M_OPR_USER6 0x20000
#define OPC$M_OPR_USER7 0x40000
#define OPC$M_OPR_USER8 0x80000
#define OPC$M_OPR_USER9 0x100000
#define OPC$M_OPR_USER10 0x200000
#define OPC$M_OPR_USER11 0x400000
#define OPC$M_OPR_USER12 0x800000

#define BOOL int
#define TRUE 1
#define FALSE 0
 
#define FI_LI  "WOTSUP", __LINE__

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))

#define DEFAULT_INTERVAL_SECONDS   30
#define DEFAULT_QUIET_SECONDS     300
#define DEFAULT_QUOTA_MIN_PERCENT  30
#define INSTANCE_INTERVAL_SECONDS  75
#define SUSPENDED_RETRY_SECONDS    30

#define WASD_WOTSUP_LOG    "WASD_WOTSUP_LOG"
#define WASD_WOTSUP_MAIL   "WASD_WOTSUP_MAIL"
#define WASD_WOTSUP_OPCOM  "WASD_WOTSUP_OPCOM"
#define WASD_WOTSUP_SHUSH  "WASD_WOTSUP_SHUSH"
#define WASD_WOTSUP_SPAWN  "WASD_WOTSUP_SPAWN"
#define WASD_WOTSUP_PID    "WASD_WOTSUP_PID"
#define WASD_WOTSUP_REPORT "WASD_WOTSUP_REPORT"

#define INSTANCE_MAX 8

char  ErrorSanityCheck [] = "sanity check",
      Utility [] = "WOTSUP";

BOOL  DoCheck,
      Debug;

int  CliStatusGroup403Delta,
     CliOpcomTarget,
     CliReportExitType,
     DefaultOpcomTarget = OPC$M_OPR_CENTRAL | OPC$M_OPR_CLUSTER,
     ExitStatus,
     InstanceCount = -1, 
     InstanceCountChange,
     InstanceGroupNumber = 1,  /* default group number */
     IntervalSeconds = DEFAULT_INTERVAL_SECONDS,
     JpiMode,
     JpiPid,
     PercentMinAst = DEFAULT_QUOTA_MIN_PERCENT,
     PercentMinBio = DEFAULT_QUOTA_MIN_PERCENT,
     PercentMinByt = DEFAULT_QUOTA_MIN_PERCENT,
     PercentMinDio = DEFAULT_QUOTA_MIN_PERCENT,
     PercentMinEnq = DEFAULT_QUOTA_MIN_PERCENT,
     PercentMinFil = DEFAULT_QUOTA_MIN_PERCENT,
     PercentMinPg = DEFAULT_QUOTA_MIN_PERCENT,
     PercentMinTq = DEFAULT_QUOTA_MIN_PERCENT,
     PreviousInstanceCount,
     PreviousStartupCount,
     QuietSeconds,
     SecondsAfterExit;

int  CliStatusCodeNumber [RESPONSE_STATUS_CODE_MAX],
     CliStatusCountDelta [RESPONSE_STATUS_CODE_MAX],
     CliStatusGroupDelta [1+5],
     InstancePid [INSTANCE_MAX];

int64 PreviousLastExitTime64;

unsigned long  SetPrvMask [2] =
   { PRV$M_SYSLCK | PRV$M_SYSPRV | PRV$M_WORLD, 0 };

unsigned short  SyiClusterNodes,
                SyiNodeLength;

char  *JpiModeName,
      *CliLogPtr,
      *CliMailToPtr,
      *CliSpawnCommandPtr,
      *CommandLinePtr;

char  CommandLine [256],
      JpiPrcNam [16],
      JpiUserName [13],
      MailPersonal [64],
      SyiNodeName [16];

int  HttpdGblSecLength;
HTTPD_GBLSEC  *HttpdGblSecPtr;

ACCOUNTING_STRUCT  *AccountingPtr;

struct AnExitHandler  ExitHandler;

/* required prototypes */
GetInstancePid ();
LookBack (int*);
ReportLine (char*, ...);
ReportServerPidName ();
char* ReportSubj (char*, ...);
BOOL SetHttpStatusCode (int, int);
char* SysGetMsg (int);
char* SysTrnLnm2 (char*, char*);
 
/*****************************************************************************/
/*
*/

int main ()

{
   static short  PrcNamLength;
   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      unsigned char  *buf_addr;
      void  *ret_len;
   }
   JpiItems [] =
   {
     { sizeof(JpiPid), JPI$_PID, &JpiPid, 0 },
     { sizeof(JpiMode), JPI$_MODE, &JpiMode, 0 },
     { sizeof(JpiUserName)-1, JPI$_USERNAME, &JpiUserName, 0 },
     { sizeof(JpiPrcNam)-1, JPI$_PRCNAM, &JpiPrcNam, &PrcNamLength },
     { 0,0,0,0 }
   },
   SyiItems [] =
   {
     { sizeof(SyiClusterNodes), SYI$_CLUSTER_NODES, &SyiClusterNodes, 0 },
     { sizeof(SyiNodeName)-1, SYI$_NODENAME, &SyiNodeName, &SyiNodeLength },
     { 0,0,0,0 }
   };

   int  status;
   char  *cptr;

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

   status = sys$setprv (1, &SetPrvMask, 0, 0);
   if (VMSnok (status) || status == SS$_NOTALLPRIV)
      ErrorExit (status, "$SETPRV()", FI_LI);

   status = sys$getsyi (0, 0, 0, &SyiItems, 0, 0, 0);
   if (VMSnok (status)) ErrorExit (status, "$GETSYI()", FI_LI);
   SyiNodeName[SyiNodeLength] = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", SyiNodeName);

   status = sys$getjpiw (0, 0, 0, &JpiItems, 0, 0, 0);
   if (VMSnok (status)) ErrorExit (status, "$GETJPIW()", FI_LI);

   switch (JpiMode)
   {
      case JPI$K_INTERACTIVE : JpiModeName = "INTERACTIVE"; break;
      case JPI$K_NETWORK     : JpiModeName = "NETWORK"; break;
      case JPI$K_OTHER       : JpiModeName = "DETACHED"; break;
      case JPI$K_BATCH       : JpiModeName = "BATCH"; break;
      default : JpiModeName = "?";
   }

   JpiPrcNam[PrcNamLength] = '\0';
   for (cptr = JpiUserName; *cptr && *cptr != ' '; cptr++);
   *cptr = '\0';
   if (Debug)
      fprintf (stdout, "%08.08X %d |%s|%s|%s|\n",
               JpiPid, JpiMode, JpiModeName, JpiUserName, JpiPrcNam);

   GetParameters ();

   if (JpiMode == JPI$K_INTERACTIVE)
      fprintf (stdout, "%%%s-I-INTERACTIVE, %s as %s\n%s",
               Utility, JpiUserName, JpiPrcNam, CopyrightInfo);

   /* build a personal name in case it's needed when mailing */
   sprintf (MailPersonal, "WOTSUP on %s", SyiNodeName);

   if (DoCheck)
   {
      ReportSubj ("/CHECK only, please ignore");
      ReportLine ("!AZ /CHECK only, please ignore!!", SOFTWAREID);
      ReportLine (NULL);
      exit (SS$_NORMAL);
   }

   if (JpiMode != JPI$K_INTERACTIVE)
   {
      $DESCRIPTOR (LogFaoDsc,
"%%%%%%%%%%  WOTSUP  !%D  %%%%%%%%%%%\n\
Message from user !AZ on !AZ\n\
Over-The-Shoulder (!AZ) !AZ mode startup\n\
!AZ\0");
      char  Buffer [512];
      $DESCRIPTOR (BufferDsc, Buffer);

      fprintf (stdout, "%%%s-I-SOFTWAREID, %s\n\n", Utility, SOFTWAREID); 

      status = sys$fao (&LogFaoDsc, 0, &BufferDsc,
                        0, JpiUserName, SyiNodeName, JpiPrcNam,
                        JpiModeName, CommandLinePtr);
      if (VMSnok (status) || status == SS$_BUFFEROVF)
         ErrorExit (status, "$FAO()", FI_LI);

      fprintf (stdout, "%s\n\n", Buffer);

      ReportLogFile (Buffer);

      ReportOpcom (DefaultOpcomTarget,
                   "Over-The-Shoulder (!AZ) !AZ mode startup\r\n\!AZ",
                   JpiPrcNam, JpiModeName, CommandLinePtr);

      sprintf (Buffer, "%08.08X", JpiPid);
      SysCreLnm (WASD_WOTSUP_PID, Buffer);

      /* set up and declare the exit handler */
      ExitHandler.HandlerAddress = &LookBack;
      ExitHandler.ArgCount = 1;
      ExitHandler.ExitStatusPtr = &ExitStatus;
      if (VMSnok (status = sys$dclexh (&ExitHandler)))
         ErrorExit (status, "$DCLEXH()", FI_LI);
   }

   MapGlobalSection ();

   AccountingPtr = &HttpdGblSecPtr->Accounting;

   LookAtHttp (1);

   OverTheShoulder ();
}

/*****************************************************************************/
/*
Declared exit handler.
*/

int LookBack (int *ExitStatusPtr)

{
   int  status;
   char  *cptr;
   $DESCRIPTOR (LogFaoDsc,
"%%%%%%%%%%  WOTSUP  !%D  %%%%%%%%%%%\n\
Message from user !AZ on !AZ\n\
Over-The-Shoulder (!AZ) !AZ mode !AZ\0");
   char  Buffer [512];
   $DESCRIPTOR (BufferDsc, Buffer);

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

   if (Debug) fprintf (stdout, "LookBack() %%X%08.08X\n", *ExitStatusPtr);

   status = *ExitStatusPtr;

   if (status == SS$_NORMAL)
      cptr = "REQUESTED RESTART";
   else
   if (status == SS$_FORCEX)
      cptr = "REQUESTED EXIT";
   else
      cptr = "ERROR EXIT";

   sys$fao (&LogFaoDsc, 0, &BufferDsc,
            0, JpiUserName, SyiNodeName, JpiPrcNam, JpiModeName, cptr);

   fprintf (stdout, "%s\n\n", Buffer);
}

/*****************************************************************************/
/*
Loop, checking on the server, sleeping 'interval' seconds between checks.
*/

int OverTheShoulder ()

{
   BOOL  LookNow;
   int  cnt;

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

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

   for (;;)
   {
      if (HttpdGblSecPtr->GblSecVersion != HTTPD_GBLSEC_VERSION_NUMBER ||
          HttpdGblSecPtr->GblSecLength != sizeof(HTTPD_GBLSEC))
      {
         ReportSubj ("GBLSEC mismatch");
         ReportLine ("Global section mismatch, rebuild WOTSUP?");
         /* report it */
         ReportLine (NULL);
         exit (SS$_ABORT);
      }

      GetInstancePid ();

      if (InstanceCount != PreviousInstanceCount)
      {
         /*
            If the number of instances changed don't report it immediately.
            This also has the desirable side-effect of delaying any initial
            report by this interval allowing things to settle down a bit
            first (whatever this means exactly).
         */
         if (InstanceCountChange >= INSTANCE_INTERVAL_SECONDS)
         {
            if (PreviousInstanceCount)
            {
               ReportSubj ("INSTANCES:!UL", InstanceCount);
               ReportLine ("number of instances has changed from !UL to !UL",
                           PreviousInstanceCount, InstanceCount);
            }
            InstanceCountChange = 0;
            PreviousInstanceCount = InstanceCount;
            LookNow = TRUE;
         }
         else
         {
            InstanceCountChange += IntervalSeconds;
            LookNow = FALSE;
         }
      }
      else
         LookNow = TRUE;

      if (LookNow)
      {
         LookAtServer ();

         for (cnt = 0; cnt < INSTANCE_MAX; cnt++) LookAtProcess (cnt);

         LookAtHttp (0);

         /* if there is anything to report then this will do it */
         ReportLine (NULL);
      }

      sleep (IntervalSeconds);
   }
}

/*****************************************************************************/
/*
Check that the server still exists, has not exited and/or restarted.
*/

int LookAtServer ()

{
   static BOOL  QuietReported;
   static unsigned long  PrevRequestCount,
                         RequestSeconds;

   BOOL  IncludeServerList = FALSE;
   int  idx, status,
        ReportExitType;
   unsigned long  CurrentSeconds,
                  RequestCount;

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

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

   if (AccountingPtr->LastExitTime64)
   {
      if (PreviousLastExitTime64 &&
          AccountingPtr->LastExitTime64 != PreviousLastExitTime64)
      {
         /* startup count has changed since the utility initialized */
         ReportSubj ("EXIT:%X!8XL", AccountingPtr->LastExitStatus);
         ReportLine ("server PID !8XL exit %X!8XL (!AZ)",
                     AccountingPtr->LastExitPid,
                     AccountingPtr->LastExitStatus,
                     SysGetMsg(AccountingPtr->LastExitStatus));
         IncludeServerList = TRUE;
      }
      PreviousLastExitTime64 = AccountingPtr->LastExitTime64;
   }

   if (AccountingPtr->StartupCount &&
       PreviousStartupCount != AccountingPtr->StartupCount)
   {
      if (PreviousStartupCount)
      {
         /* startup count has changed since the utility initialized */
         ReportSubj ("STARTUP:!UL", AccountingPtr->StartupCount);
         ReportLine ("server STARTUP (!UL)", AccountingPtr->StartupCount);
         IncludeServerList = TRUE;
      }
      PreviousStartupCount = AccountingPtr->StartupCount;
   }

   if (IncludeServerList) ReportServerPidName();

   if (QuietSeconds)
   {
      RequestCount = AccountingPtr->ProcessingTotalCount[HTTP12];
      if (PrevRequestCount && RequestCount == PrevRequestCount)
      {
         CurrentSeconds = time(NULL);
         if (CurrentSeconds - RequestSeconds > QuietSeconds)
         {
            if (!QuietReported)
            {
               ReportSubj ("QUIET");
               ReportLine ("no requests processed for !UL seconds",
                           CurrentSeconds - RequestSeconds);
               QuietReported = TRUE;
            }
         }
      }
      else
      {
         QuietReported = FALSE;
         PrevRequestCount = RequestCount;
         RequestSeconds = time(NULL);
      }
   }
}

/*****************************************************************************/
/*
Check that the server process still exists, and that each of the relevant
process quotas has not reached it's minimum remaining quotas.  As a process is
normally on occasion in a SUSPENDED state allow for this by retrying a limited
number of times.
*/ 

int LookAtProcess (int InstanceIndex)

{
   static BOOL  AstReported [INSTANCE_MAX],
                BioReported [INSTANCE_MAX],
                BytReported [INSTANCE_MAX],
                DioReported [INSTANCE_MAX],
                EnqReported [INSTANCE_MAX],
                FilReported [INSTANCE_MAX],
                NonExprReported [INSTANCE_MAX],
                PgReported [INSTANCE_MAX],
                TqReported [INSTANCE_MAX];

   static unsigned long  HttpdPid [INSTANCE_MAX];

   static int  NonExprSeconds [INSTANCE_MAX];

   static unsigned long  JpiAstCnt,
                         JpiAstLm,
                         JpiBioCnt,
                         JpiBytLm,
                         JpiBytCnt,
                         JpiBioLm,
                         JpiDioCnt,
                         JpiDioLm,
                         JpiEnqCnt,
                         JpiEnqLm,
                         JpiFilCnt,
                         JpiFilLm,
                         JpiPagFilCnt,
                         JpiPgFlQuota,
                         JpiPid,
                         JpiPrcCnt,
                         JpiPrcLm,
                         JpiTqCnt,
                         JpiTqLm;

   static int64  ConnectTime64,
                 CurrentTime64,
                 JpiLoginTime64;

   static char  JpiPrcNam [16];

   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      unsigned char  *buf_addr;
      void  *ret_len;
   }
      JpiItems [] =
   {
      { sizeof(JpiAstCnt), JPI$_ASTCNT, &JpiAstCnt, 0 },
      { sizeof(JpiAstLm), JPI$_ASTLM, &JpiAstLm, 0 },
      { sizeof(JpiBioCnt), JPI$_BIOCNT, &JpiBioCnt, 0 },
      { sizeof(JpiBioLm), JPI$_BIOLM, &JpiBioLm, 0 },
      { sizeof(JpiBytCnt), JPI$_BYTCNT, &JpiBytCnt, 0 },
      { sizeof(JpiBytLm), JPI$_BYTLM, &JpiBytLm, 0 },
      { sizeof(JpiDioCnt), JPI$_DIOCNT, &JpiDioCnt, 0 },
      { sizeof(JpiDioLm), JPI$_DIOLM, &JpiDioLm, 0 },
      { sizeof(JpiEnqCnt), JPI$_ENQCNT, &JpiEnqCnt, 0 },
      { sizeof(JpiEnqLm), JPI$_ENQLM, &JpiEnqLm, 0 },
      { sizeof(JpiFilCnt), JPI$_FILCNT, &JpiFilCnt, 0 },
      { sizeof(JpiFilLm), JPI$_FILLM, &JpiFilLm, 0 },
      { sizeof(JpiPagFilCnt), JPI$_PAGFILCNT, &JpiPagFilCnt, 0 },
      { sizeof(JpiPgFlQuota), JPI$_PGFLQUOTA, &JpiPgFlQuota, 0 },
      { sizeof(JpiPid), JPI$_PID, &JpiPid, 0 },
      { sizeof(JpiPrcNam), JPI$_PRCNAM, &JpiPrcNam, 0 },
      { sizeof(JpiTqCnt), JPI$_TQCNT, &JpiTqCnt, 0 },
      { sizeof(JpiTqLm), JPI$_TQLM, &JpiTqLm, 0 },
      {0,0,0,0}
   };

   int  attempt, idx, status,
        CurrentSeconds;
   unsigned short  Length;
   char  *cptr; 
   
   /*********/
   /* begin */
   /*********/

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

   if (HttpdPid[InstanceIndex] != InstancePid [InstanceIndex])
   {
      AstReported[InstanceIndex] = BioReported[InstanceIndex] =
         BytReported[InstanceIndex] = DioReported[InstanceIndex] =
         EnqReported[InstanceIndex] = FilReported[InstanceIndex] =
         NonExprReported[InstanceIndex] = PgReported[InstanceIndex] =
         TqReported[InstanceIndex] = FALSE;
      NonExprSeconds[InstanceIndex] = 0;
   }
   HttpdPid[InstanceIndex] = InstancePid[InstanceIndex];

   /* if just reseting the PID-related info */
   if (!HttpdPid[InstanceIndex]) return (SS$_NORMAL);

   if (Debug) fprintf (stdout, "%08.08X\n", HttpdPid[InstanceIndex]);

   /* try once per second for however many */
   attempt = SUSPENDED_RETRY_SECONDS;
   for (;;)
   {
      status = sys$getjpiw (0, &HttpdPid[InstanceIndex], 0, &JpiItems, 0, 0, 0);
      if (Debug) fprintf (stdout, "sys$getjpiw() %%X%08.08X\n", status);
      if (VMSok (status)) break;
      if (status == SS$_SUSPENDED)
      {
         sleep (1);
         if (attempt--) continue;
         /* after retrying for SUSPENDED_RETRY_SECONDS it's still suspended */
         ReportSubj ("SERVER:%X!8XL", status);
         ReportLine ("server PID !8XL is %X!8XL (!AZ)",
                     HttpdPid[InstanceIndex], status, SysGetMsg(status));
         return (SS$_NORMAL);
      }
      if (status == SS$_NONEXPR)
      {
         ReportSubj ("SERVER:%X!8XL", status);
         ReportLine ("server PID !8XL is %X!8XL (!AZ)",
                     HttpdPid[InstanceIndex], status, SysGetMsg(status));
         /* "Make it so, Number One" */
         InstancePid[InstanceIndex] = 0;
         return (SS$_NORMAL);
      }
      ErrorExit (status, "$GETJPIW()", FI_LI);
   }

   JpiPrcNam[15] = '\0';
   for (cptr = JpiPrcNam; *cptr && *cptr != ' '; cptr++);
   *cptr = '\0';
   if (Debug) fprintf (stdout, "JpiPrcNam |%s|\n", JpiPrcNam);

   sys$gettim (&CurrentTime64);
   lib$sub_times (&CurrentTime64, &JpiLoginTime64, &ConnectTime64);

   if (JpiAstLm)
   {
      if (JpiAstCnt * 100 / JpiAstLm <= PercentMinAst)
      {
         if (!AstReported[InstanceIndex])
         {
            ReportSubj ("QUOTA:astlm");
            ReportLine ("!8XL !AZ astcnt:!UL astlm:!UL !UL% <= !UL%",
                        JpiPid, JpiPrcNam,
                        JpiAstCnt, JpiAstLm,
                        JpiAstCnt * 100 / JpiAstLm, PercentMinAst);
            AstReported[InstanceIndex] = TRUE;
         }
      }
      else
         AstReported[InstanceIndex] = FALSE;
   }

   if (JpiBioLm)
   {
      if (JpiBioCnt * 100 / JpiBioLm <= PercentMinBio)
      {
         if (!BioReported[InstanceIndex])
         {
            ReportSubj ("QUOTA:biolm");
            ReportLine ("!8XL !AZ biocnt:!UL biolm:!UL !UL% <= !UL%",
                        JpiPid, JpiPrcNam,
                        JpiBioCnt, JpiBioLm,
                        JpiBioCnt * 100 / JpiBioLm, PercentMinBio);
            BioReported[InstanceIndex] = TRUE;
         }
      }
      else
         BioReported[InstanceIndex] = FALSE;
   }

   if (JpiBytLm)
   {
      if (JpiBytCnt * 100 / JpiBytLm <= PercentMinByt)
      {
         if (!BytReported[InstanceIndex])
         {
            ReportSubj ("QUOTA:bytlm");
            ReportLine ("!8XL !AZ bytcnt:!UL bytlm:!UL !UL% <= !UL%",
                        JpiPid, JpiPrcNam,
                        JpiBytCnt, JpiBytLm,
                        JpiBytCnt * 100 / JpiBytLm, PercentMinByt);
            BytReported[InstanceIndex] = TRUE;
         }
      }
      else
         BytReported[InstanceIndex] = FALSE;
   }

   if (JpiDioLm)
   {
      if (JpiDioCnt * 100 / JpiDioLm <= PercentMinDio)
      {
         if (!DioReported[InstanceIndex])
         {
            ReportSubj ("QUOTA:diolm");
            ReportLine ("!8XL !AZ diocnt:!UL diolm:!UL !UL% <= !UL%",
                        JpiPid, JpiPrcNam,
                        JpiDioCnt, JpiDioLm,
                        JpiDioCnt * 100 / JpiDioLm, PercentMinDio);
            DioReported[InstanceIndex] = TRUE;
         }
      }
      else
         DioReported[InstanceIndex] = FALSE;
   }

   if (JpiEnqLm)
   {
      if (JpiEnqCnt * 100 / JpiEnqLm <= PercentMinEnq)
      {
         if (!EnqReported[InstanceIndex])
         {
            ReportSubj ("QUOTA:enqlm");
            ReportLine ("!8XL !AZ enqcnt:!UL enqlm:!UL !UL% <= !UL%",
                        JpiPid, JpiPrcNam,
                        JpiEnqCnt, JpiEnqLm,
                        JpiEnqCnt * 100 / JpiEnqLm, PercentMinEnq);
            EnqReported[InstanceIndex] = TRUE;
         }
      }
      else
         EnqReported[InstanceIndex] = FALSE;
   }

   if (JpiFilLm)
   {
      if (JpiFilCnt * 100 / JpiFilLm <= PercentMinFil)
      {
         if (!FilReported[InstanceIndex])
         {
            ReportSubj ("QUOTA:fillm");
            ReportLine ("!8XL !AZ filcnt:!UL fillm:!UL !UL% <= !UL%",
                        JpiPid, JpiPrcNam,
                        JpiFilCnt, JpiFilLm,
                        JpiFilCnt * 100 / JpiFilLm, PercentMinFil);
            FilReported[InstanceIndex] = TRUE;
         }
      }
      else
         FilReported[InstanceIndex] = FALSE;
   }

   if (JpiTqLm)
   {
      if (JpiTqCnt * 100 / JpiTqLm <= PercentMinTq)
      {
         if (!TqReported[InstanceIndex])
         {
            ReportSubj ("QUOTA:tqlm");
            ReportLine ("!8XL !AZ tqcnt:!UL tqlm:!UL !UL% <= !UL%",
                        JpiPid, JpiPrcNam,
                        JpiTqCnt, JpiTqLm,
                        JpiTqCnt * 100 / JpiTqLm, PercentMinTq);
            TqReported[InstanceIndex] = TRUE;
         }
      }
      else
         TqReported[InstanceIndex] = FALSE;
   }

   if (JpiPgFlQuota)
   {
      if (JpiPagFilCnt * 100 / JpiPgFlQuota <= PercentMinPg)
      {
         if (!PgReported[InstanceIndex])
         {
            ReportSubj ("QUOTA:pgflquota");
            ReportLine ("!8XL !AZ pagfilcnt:!UL pgflquota:!UL !UL% <= !UL%",
                        JpiPid, JpiPrcNam,
                        JpiPagFilCnt, JpiPgFlQuota,
                        JpiPagFilCnt * 100 / JpiPgFlQuota, PercentMinPg);
            PgReported[InstanceIndex] = TRUE;
         }
      }
      else
         PgReported[InstanceIndex] = FALSE;
   }

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Check the HTTP response status code counters once per minute to ascertain
whether any have increased greater than the allowed threshold.  Operates in
two modes.  After the initial call, with 'DoCheck' true, it is called using a
timer every sixty seconds to check the server HTTP counters against the
thresholds.  If any has exceeded it has it's corresponding alert boolean set. 
When called with 'DoCheck' false it checks these booleans and reports any that
are set.
*/

int LookAtHttp (BOOL DoCheck)

{
   static BOOL  StatusCode403Alert;
   static BOOL  StatusCodeCountAlert [RESPONSE_STATUS_CODE_MAX],
                StatusCodeGroupAlert [1+5];
   static int  PrevStatusCode403Count,
               StatusCode403Count;
   static int  PrevStatusCodeGroup [1+5],
               StatusCodeGroup [1+5];
   /* RESPONSE_STATUS_CODE_MAX is defined in [SRC.HTTPD]WASD.H */
   static unsigned long  PrevStatusCodeCount [RESPONSE_STATUS_CODE_MAX],
                         StatusCodeCount [RESPONSE_STATUS_CODE_MAX];
   static unsigned long  OneMinuteDelta [2] = { -600000000, -1 };

   int  idx, status;

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

   if (Debug) fprintf (stdout, "LookAtHttp() %d\n", DoCheck);

   if (DoCheck)
   {
      /* check the code groups (1xx, 2xx, 3xx, 4xx, 5xx) */
      for (idx = 0; idx <= 5; idx++)
      {
         if (idx == 1 || !CliStatusGroupDelta[idx]) continue;
         StatusCodeGroup[idx] = AccountingPtr->ResponseStatusCodeGroup[idx];
         if (PrevStatusCodeGroup[idx])
            if (StatusCodeGroup[idx] >= PrevStatusCodeGroup[idx] +
                                        CliStatusGroupDelta[idx])
               StatusCodeGroupAlert[idx] = TRUE;
            else
               StatusCodeGroupAlert[idx] = FALSE;
         else
         {
            StatusCodeGroupAlert[idx] = FALSE;
            PrevStatusCodeGroup[idx] = StatusCodeGroup[idx];
         }
      }

      /* check the individual code counts (100, 101, 200, 201 ... 505) */
      for (idx = 1; idx < RESPONSE_STATUS_CODE_MAX; idx++)
      {
         if (!CliStatusCountDelta[idx]) continue;
         StatusCodeCount[idx] = AccountingPtr->ResponseStatusCodeCount[idx];
         if (PrevStatusCodeCount[idx])
            if (StatusCodeCount[idx] >= PrevStatusCodeCount[idx] +
                                        CliStatusCountDelta[idx])
               StatusCodeCountAlert[idx] = TRUE;
            else
               StatusCodeCountAlert[idx] = FALSE;
         else
         {
            StatusCodeCountAlert[idx] = FALSE;
            PrevStatusCodeCount[idx] = StatusCodeCount[idx];
         }
      }

      StatusCode403Count = AccountingPtr->RequestForbiddenCount;
      if (CliStatusGroup403Delta && PrevStatusCode403Count)
         if (StatusCode403Count > PrevStatusCode403Count +
                                  CliStatusGroup403Delta)
            StatusCode403Alert = TRUE;
         else
            StatusCode403Alert = FALSE;
      else
      {
         StatusCode403Alert = FALSE;
         PrevStatusCode403Count = StatusCode403Count;
      }

      status = sys$setimr (0, &OneMinuteDelta, &LookAtHttp, 1, 0);
      if (VMSnok (status)) ErrorExit (status, "$SETIMR()", FI_LI);
   }
   else
   {
      /* report code groups */
      for (idx = 0; idx <= 5; idx++)
      {
         if (StatusCodeGroupAlert[idx])
         {
            ReportSubj ("HTTP:!ULxx", idx);
            ReportLine ("HTTP !ULxx curr:!UL > prev:!UL + !UL",
                        idx, StatusCodeGroup[idx],
                        PrevStatusCodeGroup[idx], CliStatusGroupDelta[idx]);
            StatusCodeGroupAlert[idx] = FALSE;
            PrevStatusCodeGroup[idx] = StatusCodeGroup[idx];
         }
      }

      /* report individual codes */
      for (idx = 1; idx < RESPONSE_STATUS_CODE_MAX; idx++)
      {
         if (StatusCodeCountAlert[idx])
         {
            ReportSubj ("HTTP:!UL", CliStatusCodeNumber[idx]);
            ReportLine ("HTTP !UL curr:!UL > prev:!UL + !UL",
                        CliStatusCodeNumber[idx], StatusCodeCount[idx],
                        PrevStatusCodeCount[idx], CliStatusCountDelta[idx]);
            StatusCodeCountAlert[idx] = FALSE;
            PrevStatusCodeCount[idx] = StatusCodeCount[idx];
         }
      }

      if (StatusCode403Alert)
      {
         ReportSubj ("HTTP:403");
         ReportLine ("HTTP 403 curr:!UL > prev:!UL + !UL",
                     StatusCode403Count, PrevStatusCode403Count,
                     CliStatusGroup403Delta);
         StatusCode403Alert = FALSE;
         PrevStatusCode403Count = StatusCode403Count;
      }
   }
}

/*****************************************************************************/
/*
Create a report line containing the PID and process name of each process in the
InstancePid[] array.
*/ 

ReportServerPidName ()

{
   static unsigned long  JpiPid;
   static char  JpiPrcNam [16];

   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      unsigned char  *buf_addr;
      void  *ret_len;
   }
      JpiItems [] =
   {
      { sizeof(JpiPrcNam), JPI$_PRCNAM, &JpiPrcNam, 0 },
      {0,0,0,0}
   };

   int  cnt = 0,
        idx, status;
   char  *cptr, *sptr; 
   char  ListBuffer [21+(INSTANCE_MAX*28)+1];
   
   /*********/
   /* begin */
   /*********/

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

   sptr = ListBuffer;
   for (idx = 0; idx < INSTANCE_MAX; idx++)
   {
      if (!InstancePid[idx]) continue;
      status = sys$getjpiw (0, &InstancePid[idx], 0, &JpiItems, 0, 0, 0);
      if (Debug) fprintf (stdout, "sys$getjpiw() %%X%08.08X\n", status);

      /* only time it continues is when the process does not exist */
      if (VMSnok (status) && status == SS$_NONEXPR) continue;

      if (cnt++)
         sptr += sprintf( sptr, ", ");
      else
         sptr += sprintf (sptr, "server PID%s ",
                          InstanceCount > 1 ? "s are" : " is");
      if (VMSok (status))
      {
         JpiPrcNam[15] = '\0';
         for (cptr = JpiPrcNam; *cptr && *cptr != ' '; cptr++);
         *cptr = '\0';
         sptr += sprintf (sptr, "%08.08X (%s)", InstancePid[idx], JpiPrcNam);
      }
      else
         sptr += sprintf (sptr, "%08.08X (%%X%08.08X)",
                          InstancePid[idx], status);
   }
   if (!cnt)
      sprintf (sptr, "no server process%s found",
               InstanceCount > 1 ? "es" : "");

   ReportLine (ListBuffer);
}

/*****************************************************************************/
/*
See [SRC.HTTPD]CONTROL.C for other information.  Uses the VMS Distributed Lock
Manager to keep track of how many servers are currently executing on the
node/cluster.  NL locks indicate interest (used by this utility), CR locks
indicate a server waiting for a group directive.  This function enqueues a
single NL lock, then periodically get all the locks associated with that
resource and counts up the number of CR locks - giving the number of servers!
*/

GetInstancePid ()

{
   static char  NodeLockName [32],
                PrevInstanceString [32];
   static $DESCRIPTOR (NodeLockNameDsc, NodeLockName);
   static LKIDEF  LkiLocks [32];
   static struct lksb  NodeLockLksb;

   static struct
   {
      unsigned short  TotalLength,  /* bits 0..15 */
                      LockLength;  /* bits 16..30 */
   } ReturnLength;

   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      unsigned char  *buf_addr;
      void  *ret_len;
   }
   LkiItems [] =
   {
      { sizeof(LkiLocks), LKI$_LOCKS, &LkiLocks, &ReturnLength },
      {0,0,0,0}
   };

   int  cnt, idx, status,
        LockCount,
        LockNameMagic;
   char  *cptr, *sptr, *zptr;
   unsigned long  LockPid [INSTANCE_MAX];
   IO_SB  IOsb;
   LKIDEF  *lkiptr;

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

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

   if (!NodeLockName[0])
   {
      /************************/
      /* build and queue lock */
      /************************/

      if (InstanceGroupNumber > INSTANCE_ENV_NUMBER_MAX)
         ErrorExit (SS$_BUGCHECK, "Group number range.", FI_LI);

      /* a byte comprising two 4 bit fields, version and server group number */
      LockNameMagic = ((HTTPD_LOCK_VERSION & 0xf) << 4) |
                      (InstanceGroupNumber & 0xf);

      /* build the (binary) resource name for the node lock */
      zptr = (sptr = NodeLockName) + sizeof(NodeLockName)-1;
      for (cptr = HTTPD_NAME; *cptr && sptr < zptr; *sptr++ = *cptr++);
      if (sptr < zptr) *sptr++ = (char)LockNameMagic;
      for (cptr = SyiNodeName; *cptr && sptr < zptr; *sptr++ = *cptr++);
      if (sptr < zptr) *sptr++ = (char)0x05;
      NodeLockNameDsc.dsc$w_length = sptr - NodeLockName;

      /* enqueue a just-interested NL lock */
      status = sys$enqw (0, LCK$K_NLMODE, &NodeLockLksb,
                         LCK$M_EXPEDITE | LCK$M_SYSTEM,
                         &NodeLockNameDsc, 0, 0, 0, 0, 0, 2, 0);
      if (VMSok (status)) status = NodeLockLksb.lksb$w_status;
      if (VMSnok (status)) ErrorExit (status, "$ENQW()", FI_LI);
   }

   /*****************************************/
   /* get and count the node instance locks */
   /*****************************************/

   status = sys$getlkiw (0, &NodeLockLksb.lksb$l_lkid, &LkiItems, &IOsb,
                         0, 0, 0);
   if (VMSok (status)) status = NodeLockLksb.lksb$w_status;
   if (VMSnok (status)) ErrorExit (status, "$GETLKIW()", FI_LI);;

   if (ReturnLength.LockLength)
   {
      /* if insufficient buffer space */
      if (ReturnLength.LockLength & 0x8000)
         ErrorExit (SS$_BADPARAM, "$GETLKIW()", FI_LI);;
      LockCount = ReturnLength.TotalLength / ReturnLength.LockLength;
   }
   else
      LockCount = 0;
   if (Debug) fprintf (stdout, "%d\n", LockCount);

   InstanceCount = 0;
   lkiptr = &LkiLocks;
   for (cnt = LockCount; cnt; cnt--)
   {
      if (Debug) fprintf (stdout, "lki$b_grmode: %d\n", lkiptr->lki$b_grmode);
      if (lkiptr->lki$b_grmode != LCK$K_NLMODE)
      {
         if (Debug)
            fprintf (stdout, "%d %08x\n", InstanceCount, lkiptr->lki$l_pid);
         if (InstanceCount >= INSTANCE_MAX)
            ErrorExit (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
         LockPid[InstanceCount++] = lkiptr->lki$l_pid;
      }
      lkiptr++;
   }

   /**************************************************/
   /* reorganise PID array so each element is stable */
   /**************************************************/

   /* correlate existing PIDs in the instance array with the lock array */
   for (idx = 0; idx < INSTANCE_MAX; idx++)
   {
      if (!InstancePid[idx]) continue;
      for (cnt = 0; cnt < InstanceCount; cnt++)
      {
         if (InstancePid[idx] == LockPid[cnt])
         {
            /* this lock PID is already in the instance array */
            LockPid[cnt] = 0;
            break;
         }
      }
   }
   /* now place any new PIDs from the lock to the instance array */
   for (cnt = 0; cnt < InstanceCount; cnt++)
   {
      if (!LockPid[cnt]) continue;
      /* find an empty instance array element and use it */
      for (idx = 0; idx < INSTANCE_MAX; idx++)
         if (!InstancePid[idx]) break;
      if (idx >= INSTANCE_MAX)
         ErrorExit (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
      InstancePid[idx] = LockPid[cnt];
   }

   if (Debug)
      for (idx = 0; idx < INSTANCE_MAX; idx++)
         fprintf (stdout, "%d %08x\n", idx, InstancePid[idx]);
}

/*****************************************************************************/
/*
If 'FormatString' is non-NULL it is expected to contain a $FAO formatting
string and the call any required parameters.  The resultant string has required
carriage-control added and is stored as a series of lines in a static buffer.
The resulting string will be used as a subject line in the alerting email.
*/

char* ReportSubj
(
char *FormatString,
...
)
{
   static int  BufferLength,
               ItemCount;
   static char  Buffer [256];
   static $DESCRIPTOR (BufferDsc, "");
   static $DESCRIPTOR (CommaFaoDsc, ", ");
   static $DESCRIPTOR (FaoDsc, "");

   int  status, argcnt;
   unsigned short  ShortLength;
   unsigned long  *vecptr;
   unsigned long  FaoVector [31+2];
   va_list  argptr;

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

   va_count (argcnt);
   if (argcnt > 31) ErrorExit (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   if (Debug)
      fprintf (stdout, "ReportSubj() %d |%s|\n", argcnt, FormatString);

   if (FormatString)
   {
      if (!BufferLength)
      {
         /*********************/
         /* prepend some info */
         /*********************/

         BufferDsc.dsc$a_pointer = Buffer;
         BufferDsc.dsc$w_length = sizeof(Buffer)-1;

         FaoDsc.dsc$a_pointer = "WOTSUP on !AZ reports ";
         FaoDsc.dsc$w_length = strlen(FaoDsc.dsc$a_pointer);

         vecptr = FaoVector;
         *vecptr++ = SyiNodeName;
         *vecptr = 0;

         status = sys$faol (&FaoDsc, &ShortLength, &BufferDsc, &FaoVector);
         if (VMSnok (status) || status == SS$_BUFFEROVF)
            ErrorExit (status, "$FAOL()", FI_LI);
         BufferLength += ShortLength;
         Buffer[BufferLength] = '\0';
         if (Debug) fprintf (stdout, "|%s|\n", Buffer);
      }

      /*************************/
      /* now append the report */
      /*************************/

      BufferDsc.dsc$a_pointer = Buffer + BufferLength;
      BufferDsc.dsc$w_length = sizeof(Buffer)-1 - BufferLength;

      if (ItemCount++)
      {
         status = sys$faol (&CommaFaoDsc, &ShortLength, &BufferDsc);
         /* not worried about buffer overflow with the email subject line */
         if (VMSnok (status)) ErrorExit (status, "$FAOL()", FI_LI);
         BufferLength += ShortLength;
         Buffer[BufferLength] = '\0';
      }

      vecptr = FaoVector;
      va_start (argptr, FormatString);
      for (argcnt -= 1; argcnt; argcnt--)
         *vecptr++ = va_arg (argptr, unsigned long);
      va_end (argptr);
      *vecptr = 0;

      BufferDsc.dsc$a_pointer = Buffer + BufferLength;
      BufferDsc.dsc$w_length = sizeof(Buffer)-1 - BufferLength;

      FaoDsc.dsc$a_pointer = FormatString;
      FaoDsc.dsc$w_length = strlen(FormatString);

      status = sys$faol (&FaoDsc, &ShortLength, &BufferDsc, &FaoVector);
      /* not worried about buffer overflow with the email subject line */
      if (VMSnok (status)) ErrorExit (status, "$FAOL()", FI_LI);
      BufferLength += ShortLength;
      Buffer[BufferLength] = '\0';
      if (Debug) fprintf (stdout, "|%s|\n", Buffer);

      return;
   }

   /************************************/
   /* return the built-up subject line */
   /************************************/

   /* if there is nothing to report then report that! */
   if (!BufferLength) "BUGCHECK?";

   /* reset subj line (without emptying the to-be returned string) */
   BufferLength = 0;
   ItemCount = 0;

   return (Buffer);
}

/*****************************************************************************/
/*
If 'FormatString' is non-NULL it is expected to contain a $FAO formatting
string and the call any required parameters.  The resultant string has required
carriage-control added and is stored as a series of lines in a static buffer.

When 'FormatString' is NULL the buffered text is sent to the OPCOM, Mail and/or
spawn targets.  Check the logical names containing the OPCOM, Mail and spawn
targets for the most recent settings (can be changed dynamically without
restarting the utility).  If these logical names do not exist fall back to
anything specified at the original command-line.
*/

int ReportLine
(
char *FormatString,
...
)
{
   static int  BufferLength,
               ItemCount;
   static char  Buffer [2048];
   static char  *OpcomTextPtr;
   static $DESCRIPTOR (BufferDsc, "");
   static $DESCRIPTOR (FaoDsc, "");

   int  status, argcnt,
        OpcomTarget;
   unsigned short  ShortLength;
   unsigned long  *vecptr;
   unsigned long  FaoVector [32];
   char  *cptr,
         *MailToPtr,
         *SpawnCommandPtr;
   char  MailTo [256],
         SpawnCommand [256];
   va_list  argptr;
   FILE  *LogFilePtr;

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

   va_count (argcnt);
   if (argcnt > 31) ErrorExit (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   if (Debug)
      fprintf (stdout, "ReportLine() %d |%s|\n", argcnt, FormatString);

   if (FormatString)
   {
      if (!BufferLength)
      {
         /****************************************/
         /* prepend a leading informational line */
         /****************************************/

         BufferDsc.dsc$a_pointer = Buffer;
         BufferDsc.dsc$w_length = sizeof(Buffer)-1;

         FaoDsc.dsc$a_pointer =
"%%%%%%%%%%  WOTSUP  !%D  %%%%%%%%%%%\r\n\
Message from user !AZ on !AZ\r\n\
Over-The-Shoulder (!AZ) reports:";
         FaoDsc.dsc$w_length = strlen(FaoDsc.dsc$a_pointer);

         vecptr = FaoVector;
         *vecptr++ = 0;
         *vecptr++ = JpiUserName;
         *vecptr++ = SyiNodeName;
         *vecptr++ = JpiPrcNam;
         *vecptr = 0;

         status = sys$faol (&FaoDsc, &ShortLength, &BufferDsc, &FaoVector);
         if (VMSnok (status) || status == SS$_BUFFEROVF)
            ErrorExit (status, "$FAOL()", FI_LI);
         BufferLength += ShortLength;
         Buffer[BufferLength] = '\0';
         if (Debug) fprintf (stdout, "|%s|\n", Buffer);

         /* for OPCOM purposes find the start of the third line */
         OpcomTextPtr = Buffer + BufferLength;
         while (OpcomTextPtr > Buffer && *OpcomTextPtr != '\n') OpcomTextPtr--;
         if (*OpcomTextPtr == '\n') OpcomTextPtr++;
      }

      /*******************************/
      /* now append the line of text */
      /*******************************/

      BufferDsc.dsc$a_pointer = Buffer + BufferLength;
      BufferDsc.dsc$w_length = sizeof(Buffer)-1 - BufferLength;

      FaoDsc.dsc$a_pointer = "\r\n!UL. ";
      FaoDsc.dsc$w_length = 7;

      status = sys$fao (&FaoDsc, &ShortLength, &BufferDsc, ++ItemCount);
      if (VMSnok (status) || status == SS$_BUFFEROVF)
         ErrorExit (status, "$FAO()", FI_LI);
      BufferLength += ShortLength;

      vecptr = FaoVector;
      va_start (argptr, FormatString);
      for (argcnt -= 1; argcnt; argcnt--)
         *vecptr++ = va_arg (argptr, unsigned long);
      va_end (argptr);
      *vecptr++ = 0;

      BufferDsc.dsc$a_pointer = Buffer + BufferLength;
      BufferDsc.dsc$w_length = sizeof(Buffer)-1 - BufferLength;

      FaoDsc.dsc$a_pointer = FormatString;
      FaoDsc.dsc$w_length = strlen(FormatString);

      status = sys$faol (&FaoDsc, &ShortLength, &BufferDsc, &FaoVector);
      if (VMSnok (status) || status == SS$_BUFFEROVF)
         ErrorExit (status, "$FAOL()", FI_LI);
      BufferLength += ShortLength;
      Buffer[BufferLength] = '\0';
      if (Debug) fprintf (stdout, "|%s|\n", Buffer);

      return;
   }

   /* if there is nothing to report then just return */
   if (!BufferLength) return;

   /**********************/
   /* get latest targets */
   /**********************/

   if (cptr = SysTrnLnm2 (WASD_WOTSUP_OPCOM, NULL))
   {
      OpcomTarget = OpcomTargetOf(cptr);
      if (!OpcomTarget) ErrorExit (SS$_BADPARAM, WASD_WOTSUP_OPCOM, FI_LI);
   }
   else
      OpcomTarget = CliOpcomTarget;

   if (cptr = SysTrnLnm2 (WASD_WOTSUP_MAIL, MailTo))
   {
      if (*cptr)
         MailToPtr = MailTo;
      else
         ErrorExit (SS$_BADPARAM, WASD_WOTSUP_MAIL, FI_LI);
   }
   else
      MailToPtr = CliMailToPtr;

   if (cptr = SysTrnLnm2 (WASD_WOTSUP_SPAWN, SpawnCommand))
   {
      while (*cptr && isspace(*cptr)) cptr++;
      if (!*cptr) ErrorExit (SS$_BADPARAM, WASD_WOTSUP_SPAWN, FI_LI);
      SpawnCommandPtr = SpawnCommand;
   }
   else
      SpawnCommandPtr = CliSpawnCommandPtr;

   /*******************/
   /* send to targets */
   /*******************/

   if (OpcomTarget) ReportOpcom (OpcomTarget, "!AZ", OpcomTextPtr);

   if (!SysTrnLnm2 (WASD_WOTSUP_SHUSH, NULL))
   {
      if (MailToPtr)
         MailMessage (MailPersonal, MailToPtr, ReportSubj(NULL), Buffer);
      /* ensure email subject line is reset regardless of mailing message */
      ReportSubj (NULL);

      if (SpawnCommandPtr) ReportSpawn (SpawnCommandPtr, Buffer);
   }

   /* should be called last as it modifies the buffer content */
   ReportLogFile (Buffer);

   /****************/
   /* reset buffer */
   /****************/

   Buffer[BufferLength=0] = '\0';
   ItemCount = 0;
   OpcomTextPtr = NULL;
}

/*****************************************************************************/
/*
Write the specified text to any log file.  Modifies the text!
*/

int ReportLogFile (char *Buffer)

{
   int  status;
   char  *cptr, *sptr,
         *LogFileNamePtr;
   char  LogFileName [256];
   FILE  *LogFilePtr;

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

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

   if (SysTrnLnm2 (WASD_WOTSUP_LOG, LogFileName))
      LogFileNamePtr = LogFileName;
   else
      LogFileNamePtr = CliLogPtr;

   if (!LogFileNamePtr) return;

   LogFilePtr = fopen (LogFileNamePtr, "a", "shr=put");
   if (LogFilePtr)
   {
      /* remove the <CR> from <CR><LF>s before writing to the log file */
      for (cptr = sptr = Buffer; *cptr; *sptr++ = *cptr++)
         if (*(unsigned short*)cptr == '\r\n') cptr++;
      *sptr = '\0';
      fprintf (LogFilePtr, "%s\n\n", Buffer);
      fclose (LogFilePtr);
   }
   else
   {
      status = vaxc$errno;
      ReportOpcom (DefaultOpcomTarget,
"Over-The-Shoulder (!AZ) reports:\r\n\
failed to open log file !AZ, %X!8XL (!AZ)",
                   JpiPrcNam, LogFileNamePtr, status, SysGetMsg(status));
   }
}

/*****************************************************************************/
/*
Get the HTTPd server data from its global section.
Data is written by UpdateGlobalSection() in [SRC.HTTPD]SUPPORT.C module.
*/

int MapGlobalSection ()

{
   /* system global section, map into first available virtual address */
   static int MapFlags = SEC$M_SYSGBL | SEC$M_EXPREG;
   /* it is recommended to map into any virtual address in the region (P0) */
   static unsigned long  InAddr [2] = { 0x200, 0x200 };

   static char  HttpdGblSecName [32];
   static $DESCRIPTOR (HttpdGblSecNameDsc, HttpdGblSecName);
   static $DESCRIPTOR (HttpdGblSecNameFaoDsc, GBLSEC_NAME_FAO);

   int  status,
        ByteSize,
        PageSize;
   unsigned short  ShortLength;
   unsigned long  RetAddr [2];

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

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

   if (HttpdGblSecPtr) return (SS$_NORMAL);

   /* only need to map it the one time */
   sys$fao (&HttpdGblSecNameFaoDsc, &ShortLength, &HttpdGblSecNameDsc,
            HTTPD_NAME, HTTPD_GBLSEC_VERSION_NUMBER,
            InstanceGroupNumber, "HTTPD");
   HttpdGblSecNameDsc.dsc$w_length = ShortLength;
   if (Debug) fprintf (stdout, "|%s|\n", HttpdGblSecName);

   /* map the specified global section */
   status = sys$mgblsc (&InAddr, &RetAddr, 0, MapFlags, &HttpdGblSecNameDsc,
                        0, 0);
   if (Debug)
      fprintf (stdout, "sys$mgblsc() %%X%08.08X begin:%d end:%d\n",
               status, RetAddr[0], RetAddr[1]);

   if (VMSok (status))
   {
      ByteSize = (RetAddr[1]+1) - RetAddr[0];
      PageSize = (RetAddr[1]+1) - RetAddr[0] >> 9;
      HttpdGblSecPtr = (HTTPD_GBLSEC*)RetAddr[0];
      HttpdGblSecLength = ByteSize;
   }
   else
   {
      HttpdGblSecPtr = NULL;
      HttpdGblSecLength = ByteSize = PageSize = 0;
      if (status == SS$_NOSUCHSEC)
      {
         fprintf (stdout,
"%%%s-E-SERVER, no such server!\n\
-SYSTEM-E-NOSUCHSEC, no such (global) section\n",
                  Utility);
         exit (SS$_NOSUCHSEC | STS$M_INHIB_MSG);
      }
      else
         exit (status);
   }

   if (HttpdGblSecPtr->GblSecVersion != HTTPD_GBLSEC_VERSION_NUMBER ||
       HttpdGblSecPtr->GblSecLength != sizeof(HTTPD_GBLSEC))
   {
      fprintf (stdout,
"%%%s-E-GBLSEC_MISMATCH, global section mismatch, rebuild WOTSUP?\n",
               Utility);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Use the VMS callable mail interface to create and send a VMS mail message.  
'To' can be a list of comma-separated addresses.  'Subject' is a
null-terminated string. 'Body' is a null-terminated string of '\n'-separated
lines of plain text.  Just truncates anything longer than 255 characters (body
excluded, body records included)!
*/ 

int MailMessage
(
char *PersonalName,
char *To,
char *Subject,
char *Body
)
{
   int  status;
   unsigned long  SendContext = 0;
   char  *cptr, *sptr;

   struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   }
   BodyPartItem [] =
   {
      { 0, MAIL$_SEND_RECORD, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   PersonalNameItem [] =
   {
      { 0, MAIL$_SEND_PERS_NAME, PersonalName, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SendUserNameItem [] =
   {
      { 0, MAIL$_SEND_USERNAME, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SubjectItem [] =
   {
      { 0, MAIL$_SEND_SUBJECT, Subject, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   NoSignalItem [] =
   {
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   NullItem = {0,0,0,0};

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

   if (Debug)
     fprintf (stdout, "MailMessage() |%s|\n|%s|\n|%s|\n",
              PersonalName, To, Subject);

   if (PersonalName != NULL && PersonalName[0])
   {
      PersonalNameItem[0].buf_len = strlen(PersonalName);
      if (PersonalNameItem[0].buf_len > 255) PersonalNameItem[0].buf_len = 255;
      status = mail$send_begin (&SendContext, &PersonalNameItem, &NullItem);
   }
   else
      status = mail$send_begin (&SendContext, &NoSignalItem, &NullItem);

   if (VMSnok (status)) ErrorExit (status, "MAIL$SEND_BEGIN", FI_LI);

   /* a single, or multiple comma-separated addresses */
   cptr = To;
   while (*cptr)
   {
      if (!*cptr) break;
      sptr = cptr;
      while (*cptr && *cptr != ',') cptr++;
      if (*cptr) *cptr++ = '\0';

      SendUserNameItem[0].buf_addr = sptr;
      SendUserNameItem[0].buf_len = strlen(sptr);
      if (SendUserNameItem[0].buf_len > 255) SendUserNameItem[0].buf_len = 255;

      if (Debug)
         fprintf (stdout, "address |%s|\n",
                 (char*)SendUserNameItem[0].buf_addr);
      status = mail$send_add_address (&SendContext, &SendUserNameItem,
                                      &NullItem);
      if (VMSnok (status)) ErrorExit (status, sptr, FI_LI);
   }

   SubjectItem[0].buf_len = strlen(Subject);
   if (SubjectItem[0].buf_len > 255) SubjectItem[0].buf_len = 255;
   status = mail$send_add_attribute (&SendContext, &SubjectItem, &NullItem);
   if (VMSnok (status)) ErrorExit (status, "MAIL$SEND_ADD_ATTRIBUTE", FI_LI);

   cptr = Body;
   while (*cptr)
   {
      BodyPartItem[0].buf_addr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      BodyPartItem[0].buf_len = cptr - (char*)BodyPartItem[0].buf_addr;
      if (BodyPartItem[0].buf_len > 255) BodyPartItem[0].buf_len = 255;
      if (*cptr) cptr++;
      status = mail$send_add_bodypart (&SendContext, &BodyPartItem, &NullItem);
      if (VMSnok (status)) ErrorExit (status, "MAIL$SEND_ADD_BODYPART", FI_LI);
   }

   status = mail$send_message (&SendContext, &NoSignalItem, &NoSignalItem);
   if (VMSnok (status)) ErrorExit (status, "MAIL$SEND_MESSAGE", FI_LI);

   mail$send_end (&SendContext, &NullItem, &NullItem);

   return (SS$_NORMAL);
}

/****************************************************************************/
/*
$FAO formatted print statement to OPCOM.  A fixed-size, internal buffer of 986
bytes maximum is used and the result output as an OPCOM message.
*/

int ReportOpcom
(
int OpcomTarget,
char *FormatString,
...
)
{
   static $DESCRIPTOR (FaoDsc, "");
   static $DESCRIPTOR (OpcomDsc, "");
   static $DESCRIPTOR (OpcomMsgDsc, "");

   int  status,
        argcnt;
   unsigned short  ShortLength;
   unsigned long  *vecptr;
   unsigned long  FaoVector [31+1];
   va_list  argptr;
   struct
   {
      unsigned long  TargetType;
      unsigned long  RequestId;
      char  MsgText [986+1];
   } OpcomMsg;

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

   va_count (argcnt);

   if (Debug)
      fprintf (stdout, "ReportOpcom() |%s| %d\n", FormatString, argcnt);

   if (argcnt > 31+2) exit (SS$_OVRMAXARG);

   vecptr = FaoVector;
   va_start (argptr, FormatString);
   for (argcnt -= 2; argcnt; argcnt--)
      *vecptr++ = va_arg (argptr, unsigned long);
   va_end (argptr);
   *vecptr = 0;

   FaoDsc.dsc$a_pointer = FormatString;
   FaoDsc.dsc$w_length = strlen(FormatString);

   OpcomMsgDsc.dsc$a_pointer = &OpcomMsg.MsgText;
   OpcomMsgDsc.dsc$w_length = sizeof(OpcomMsg.MsgText)-1;

   status = sys$faol (&FaoDsc, &ShortLength, &OpcomMsgDsc, &FaoVector);
   if (VMSnok (status)) ErrorExit (status, "$FAOL()", FI_LI);

   OpcomMsg.MsgText[ShortLength] = '\0';
   if (Debug) fprintf (stdout, "%d |%s|\n", ShortLength, OpcomMsg.MsgText);

   OpcomMsg.TargetType = OPC$_RQ_RQST + ((OpcomTarget & 0xffffff) << 8);
   OpcomMsg.RequestId = 0;

   OpcomDsc.dsc$a_pointer = &OpcomMsg;
   OpcomDsc.dsc$w_length = ShortLength + 8;

   status = sys$sndopr (&OpcomDsc, 0);
   if (VMSnok (status)) ErrorExit (status, "$SNDOPR()", FI_LI);

   return (status);
}

/*****************************************************************************/
/*
Return an integer value representing the OPCOM target string.
Zero indicates an error.
*/

int OpcomTargetOf (char *cptr)

{
   int  OpcomTarget;

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

   if (Debug) fprintf (stdout, "OpcomTargetOf() |%s|\n", cptr);

   OpcomTarget = 0;
   while (*cptr)
   {
      while (*cptr == '(' || *cptr == ',' || *cptr == ')') cptr++;
      if (!*cptr) break;
      if (strsame (cptr, "ALL", 3))
         OpcomTarget = 0xffffffff;
      else
      if (strsame (cptr, "CENTRAL", 7))
         OpcomTarget |= OPC$M_OPR_CENTRAL;
      else
      if (strsame (cptr, "PRINTER", 7))
         OpcomTarget |= OPC$M_OPR_PRINTER;
      else
      if (strsame (cptr, "TAPES", 5))
         OpcomTarget |= OPC$M_OPR_TAPES;
      else
      if (strsame (cptr, "DISKS", 5))
         OpcomTarget |= OPC$M_OPR_DISKS;
      else
      if (strsame (cptr, "DEVICES", 7))
         OpcomTarget |= OPC$M_OPR_DEVICES;
      else
      if (strsame (cptr, "CARDS", 5))
         OpcomTarget |= OPC$M_OPR_CARDS;
      else
      if (strsame (cptr, "NETWORK", 7))
         OpcomTarget |= OPC$M_OPR_NETWORK;
      else
      if (strsame (cptr, "CLUSTER", 7))
         OpcomTarget |= OPC$M_OPR_CLUSTER;
      else
      if (strsame (cptr, "SECURITY", 8))
         OpcomTarget |= OPC$M_OPR_SECURITY;
      else
      if (strsame (cptr, "REPLY", 5))
         OpcomTarget |= OPC$M_OPR_REPLY;
      else
      if (strsame (cptr, "SOFTWARE", 8))
         OpcomTarget |= OPC$M_OPR_SOFTWARE;
      else
      if (strsame (cptr, "LICENSE", 7))
         OpcomTarget |= OPC$M_OPR_LICENSE;
      else
      if (strsame (cptr, "OPER2", 5))
         OpcomTarget |= OPC$M_OPR_USER2;
      else
      if (strsame (cptr, "OPER3", 5))
         OpcomTarget |= OPC$M_OPR_USER3;
      else
      if (strsame (cptr, "OPER4", 5))
         OpcomTarget |= OPC$M_OPR_USER4;
      else
      if (strsame (cptr, "OPER5", 5))
         OpcomTarget |= OPC$M_OPR_USER5;
      else
      if (strsame (cptr, "OPER6", 5))
         OpcomTarget |= OPC$M_OPR_USER6;
      else
      if (strsame (cptr, "OPER7", 5))
         OpcomTarget |= OPC$M_OPR_USER7;
      else
      if (strsame (cptr, "OPER8", 5))
         OpcomTarget |= OPC$M_OPR_USER8;
      else
      if (strsame (cptr, "OPER9", 5))
         OpcomTarget |= OPC$M_OPR_USER9;
      else
      if (strsame (cptr, "OPER10", 6))
         OpcomTarget |= OPC$M_OPR_USER10;
      else
      if (strsame (cptr, "OPER11", 6))
         OpcomTarget |= OPC$M_OPR_USER11;
      else
      if (strsame (cptr, "OPER12", 6))
         OpcomTarget |= OPC$M_OPR_USER12;
      else
      /* must be here after OPER1n for the obvious reason */
      if (strsame (cptr, "OPER1", 5))
         OpcomTarget |= OPC$M_OPR_USER1;
      else
         return (0);
      while (isalnum(*cptr)) cptr++;
   }
   if (Debug) fprintf (stdout, "%08.08X\n", OpcomTarget);
   return (OpcomTarget);
}

/*****************************************************************************/
/*
Parse and set the /QUOTA=.. qualifier from the command-line.  It can be
followed by a single integer, and/or one or more keywords for ASTLM, BIOLM,
etc, followed by an integer value for the invidual quota.  For example:
/QUOTA=(30,BYTLM:20,PGFLQUO:50).
*/

ProcessQuotaOf (char *cptr)

{
   int  status, percent;
   int  *QuotaPtr;
   char  *sptr;

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

   if (Debug) fprintf (stdout, "ProcessQuotaOf() |%s|\n", cptr);

   while (*cptr)
   {
      while (*cptr == '(' || *cptr == ',' || *cptr == ')') cptr++;
      if (!*cptr) break;
      if (isdigit(*cptr))
         QuotaPtr = NULL;
      else
      if (strsame (cptr, "ASTLM:", 6))
         QuotaPtr = &PercentMinAst;
      else
      if (strsame (cptr, "BIOLM:", 6))
         QuotaPtr = &PercentMinBio;
      else
      if (strsame (cptr, "BYTLM:", 6))
         QuotaPtr = &PercentMinByt;
      else
      if (strsame (cptr, "DIOLM:", 6))
         QuotaPtr = &PercentMinDio;
      else
      if (strsame (cptr, "ENQLM:", 6))
         QuotaPtr = &PercentMinEnq;
      else
      if (strsame (cptr, "FILLM:", 6))
         QuotaPtr = &PercentMinFil;
      else
      if (strsame (cptr, "PGFLQUO:", 8))
         QuotaPtr = &PercentMinPg;
      else
      if (strsame (cptr, "TQLM:", 5))
         QuotaPtr = &PercentMinTq;
      else
      {
         for (sptr = cptr; *sptr && *sptr != ',' && *sptr != ')'; sptr++);
         *sptr = '\0';
         fprintf (stdout, "%%%s-E-UNRKEY, \"%s\" is an unrecognised keyword\n",
                  Utility, cptr);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      while (*cptr && !isdigit(*cptr)) cptr++;
      if (*cptr == ':') cptr++;
      percent = atoi(cptr);
      if (percent <= 0 || percent > 99)
      {
         for (sptr = cptr; *sptr && *sptr != ',' && *sptr != ')'; sptr++);
         *sptr = '\0';
         fprintf (stdout, "%%%s-E-QUOTA, \"%s\" must be >= 1 percent <= 99\n",
                  Utility, cptr);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
      if (QuotaPtr)
         *QuotaPtr = percent;
      else
      {
         PercentMinAst = PercentMinBio = PercentMinByt =
            PercentMinDio = PercentMinEnq = PercentMinFil =
            PercentMinTq = PercentMinPg = percent;
      }
      while (isdigit(*cptr)) cptr++;
   }
}

/*****************************************************************************/
/*
Using the CRTL system() call spawn a subprocess to perform the DCL command
provided.  A double-quote delimited parameter is provided on the command-line
that supplies the report string.
*/

int ReportSpawn
(
char *DclCommand,
char *ReportText
)
{
   int  status;

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

   if (Debug)
      fprintf (stdout, "ReportSpawn() |%s|%s|\n", DclCommand, ReportText);

   SysCreLnm (WASD_WOTSUP_REPORT, ReportText);
   status = system (DclCommand);
   if (VMSnok (status)) ErrorExit (status, "system()", FI_LI);
}

/*****************************************************************************/
/*
Translate a logical name using LNM$FILE_DEV. Return a pointer to the value
string, or NULL if the name does not exist.  If 'LogValue' is supplied the
logical name is translated into that (assumed to be large enough), otherwise
it's translated into an internal static buffer.
*/

char* SysTrnLnm2
(
char *LogName,
char *LogValue
)
{
   static unsigned short  ShortLength;
   static char  StaticLogValue [256];
   static $DESCRIPTOR (LogNameDsc, "");
   static $DESCRIPTOR (LnmFileDevDsc, "LNM$FILE_DEV");
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { 255, LNM$_STRING, 0, &ShortLength },
      { 0,0,0,0 }
   };

   int  status;
   char  *cptr;

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

   if (Debug) fprintf (stdout, "SysTrnLnm2() |%s|\n", LogName);

   LogNameDsc.dsc$a_pointer = LogName;
   LogNameDsc.dsc$w_length = strlen(LogName);
   if (LogValue)
      cptr = LnmItems[0].buf_addr = LogValue;
   else
      cptr = LnmItems[0].buf_addr = StaticLogValue;

   status = sys$trnlnm (0, &LnmFileDevDsc, &LogNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (!(status & 1))
   {
      if (Debug) fprintf (stdout, "|(null)|\n");
      return (NULL);
   }

   cptr[ShortLength] = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", cptr);
   return (cptr);
}

/*****************************************************************************/
/*
Create a logical name in the system table.  If that name contains <CR><LF>
delimtted lines the create a translation index for each (0..127).  Used to pass
multi-line values to a spawned reporting agent.
*/

SysCreLnm
(
char *LogName,
char *LogValue
)
{
#define LNM_INDEX_MAX 127

   static $DESCRIPTOR (LogTableDsc, "LNM$SYSTEM");
   static $DESCRIPTOR (LogNameDsc, "");

   int  idx, status;
   char  *cptr, *sptr;
   struct {
      short int  buf_len;
      short int  item;
      unsigned char   *buf_addr;
      unsigned short  *ret_len;
   }
   CreLnmItem [LNM_INDEX_MAX+1];

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

   if (Debug) fprintf (stdout, "SysCreLnm() |%s|\n", LogName, LogValue);

   idx = 0;
   cptr = LogValue;
   while (*cptr && idx < LNM_INDEX_MAX)
   {
      for (sptr = cptr; *sptr && *(unsigned short*)sptr != '\r\n'; sptr++);
      CreLnmItem[idx].item = LNM$_STRING;
      CreLnmItem[idx].buf_addr = (unsigned char*)cptr;
      CreLnmItem[idx++].buf_len = sptr - cptr;
      if (Debug) fprintf (stdout, "|%.*s|\n", sptr-cptr, cptr); 
      cptr = sptr;
      if (*(unsigned short*)cptr == '\r\n') cptr += 2;
   }
   memset (&CreLnmItem[idx], 0, sizeof(CreLnmItem[idx]));
   LogNameDsc.dsc$a_pointer = LogName;
   LogNameDsc.dsc$w_length = strlen(LogName);
   status = sys$crelnm (0, &LogTableDsc, &LogNameDsc, 0, &CreLnmItem);
   if (VMSnok (status)) ErrorExit (status, "$CRELNM()", FI_LI);
}

/*****************************************************************************/
/*
Provide some extra information (particularly source module and line) when the
image exits.  Needs to be explicitly called (not an exit handler).
*/

ErrorExit
(
int  StatusValue,
char *Explanation,
char *SourceCodeModule,
int SourceCodeLine
)
{
   /*********/
   /* begin */
   /*********/

   fprintf (stdout,
"%%%s-F-EXIT, %s (%%X%08.08X)\n\
-%s-I-WHERE, module:%s line:%d\n",
            Utility, Explanation, StatusValue,
            Utility, SourceCodeModule, SourceCodeLine);
   _exit (StatusValue & 0x7fffffff);
}

/*****************************************************************************/
/*
Return a pointer to the message string corresponding to the supplied VMS status
value.
*/

char* SysGetMsg (int VmsStatus)

{
   static char  Message [256];

   int  status;
   short int  Length;
   $DESCRIPTOR (MessageDsc, Message);

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

   Message[0] = '\0';
   if (VmsStatus)
   {
      /* text component only */
      sys$getmsg (VmsStatus, &Length, &MessageDsc, 0xe, 0);
      Message[Length] = '\0';
   }
   if (Message[0])
      return (Message);
   else
      return ("(internal error)");
}
 
/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration logical containing the equivalent.
*/

GetParameters ()

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

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

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

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

   if (JpiMode == JPI$K_INTERACTIVE)
      clptr = NULL;
   else
      clptr = getenv ("WASD_WOTSUP_PARAM");

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

   CommandLinePtr = calloc (1, strlen(clptr)+1);
   strcpy (CommandLinePtr, clptr);

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr && *aptr == '/') *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 (strsame (aptr, "/CHECK", -1))
      {
         DoCheck = TRUE;
         continue;
      }

      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = TRUE;
         continue;
      }

      if (strsame (aptr, "/DO=", 4))
      {
         unsigned long  Pid;
         cptr = SysTrnLnm2 (WASD_WOTSUP_PID, NULL);
         if (!cptr) exit (SS$_NONEXPR);
         Pid = strtoul (cptr, NULL, 16);
         if (!Pid) exit (SS$_NONEXPR);
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         if (strsame (cptr, "DELETE", -1))
            exit (sys$delprc (&Pid, 0));
         if (strsame (cptr, "EXIT", -1))
            exit (sys$forcex (&Pid, 0, SS$_FORCEX));
         if (strsame (cptr, "RESTART", -1))
            exit (sys$forcex (&Pid, 0, SS$_NORMAL));
         fprintf (stdout, "%%%s-E-UNRKEY, \"%s\" is an unrecognised keyword\n",
                  Utility, cptr);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
         continue;
      }

      if (strsame (aptr, "/EXIT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         SecondsAfterExit = atoi(cptr);
         if (SecondsAfterExit <= 0 || SecondsAfterExit > 300)
         {
            fprintf (stdout, "%%%s-E-EXIT, must be >= 1 seconds <= 300\n",
                     Utility);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         continue;
      }

      if (strsame (aptr, "/HELP", 4))
      {
         ShowHelp ();
         exit (SS$_NORMAL);
      }

      if (strsame (aptr, "/HTTP=", 4))
      {
         int  CodeDelta, CodeNumber;
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         while (*cptr)
         {
            while (*cptr == '(' || *cptr == ',' || *cptr == ')') cptr++;
            if (!*cptr) break;
            CodeNumber = atoi(cptr);
            while (isdigit(*cptr)) cptr++;
            if (*cptr) cptr++;
            CodeDelta = atoi(cptr);
            if (CodeDelta < 0)
            {
               fprintf (stdout, "%%%s-E-HTTP, delta must be > 0\n", Utility);
               exit (STS$K_ERROR | STS$M_INHIB_MSG);
            }
            while (isdigit(*cptr)) cptr++;
            if ((CodeNumber >= 0 && CodeNumber <= 5) || CodeNumber == 403)
            {
               /* HTTP status code group */
               if (CodeNumber == 403)
                  CliStatusGroup403Delta = CodeDelta;
               else
                  CliStatusGroupDelta[CodeNumber] = CodeDelta;
            }
            else
            if (CodeNumber >= 100 && CodeNumber <= 505)
            {
               /* individual HTTP status code */
               if (!SetHttpStatusCode (CodeNumber, CodeDelta))
               {
                  fprintf (stdout, "%%%s-E-HTTP, code %d unknown\n",
                           CodeNumber, Utility);
                  exit (STS$K_ERROR | STS$M_INHIB_MSG);
               }
               /* get the code group from the number and reset the group */
               CodeNumber /= 100;
               CliStatusGroupDelta[CodeNumber] = 0;
            }
            else
            {
               fprintf (stdout,
"%%%s-E-HTTP, must be >= 0 and <= 5 or >= 100 and <= 505\n",
                        Utility);
               exit (STS$K_ERROR | STS$M_INHIB_MSG);
            }
         }
         continue;
      }

      if (strsame (aptr, "/INTERVAL=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         IntervalSeconds = atoi(cptr);
         if (IntervalSeconds <= 0 || IntervalSeconds > 60)
         {
            fprintf (stdout, "%%%s-E-INTERVAL, must be >= 1 seconds <= 60\n",
                     Utility, cptr);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         continue;
      }

      if (strsame (aptr, "/LOG=", 4))
      {
         CliLogPtr = "SYS$OUTPUT";
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         cptr++;
         CliLogPtr = cptr;
         continue;
      }

      if (strsame (aptr, "/MAIL=", 4))
      {
         CliMailToPtr = "SYSTEM";
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         cptr++;
         if (*cptr == '(') cptr++;
         CliMailToPtr = cptr;
         while (*cptr) cptr++;
         if (*(cptr-1) == ')') *(cptr-1) = '\0';
         continue;
      }

      if (strsame (aptr, "/OPCOM=", 4))
      {
         CliOpcomTarget = DefaultOpcomTarget;
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         cptr++;
         CliOpcomTarget = OpcomTargetOf(cptr);
         if (!CliOpcomTarget)
         {
            fprintf (stdout, "%%%s-E-UNRKEY, \"%s\" is an unrecognised keyword\n",
                     Utility, cptr);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         continue;
      }

      if (strsame (aptr, "/QUIET=", 4))
      {
         QuietSeconds = DEFAULT_QUIET_SECONDS;
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         cptr++;
         QuietSeconds = atoi(cptr);
         if (QuietSeconds <= 0 || QuietSeconds < IntervalSeconds)
         {
            fprintf (stdout, "%%%s-E-QUIET, must be >= /INTERVAL\n",
                     Utility, cptr);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         continue;
      }

      if (strsame (aptr, "/QUOTA=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         ProcessQuotaOf (cptr);
         continue;
      }

      if (strsame (aptr, "/SPAWN=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         if (!*cptr)
         {
            fprintf (stdout, "%%%s-W-VALREQ, missing qualifier or keyword value\n \\%s\\\n",
                     Utility, aptr);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         CliSpawnCommandPtr = cptr;
         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);
      }
   }
}

/*****************************************************************************/
/*
Set the delta against the HTTP status code number.
Based on array elements from [SRC.HTTPD]REQUEST.C RequestHttpStatusCode().
*/

BOOL SetHttpStatusCode
(
int CodeNumber,
int CodeDelta
)
{
   int  idx;

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

   switch (CodeNumber)
   {
      case 100 : idx = 1; break;
      case 101 : idx = 2; break;
      case 200 : idx = 3; break;
      case 201 : idx = 4; break;
      case 202 : idx = 5; break;
      case 203 : idx = 6; break;
      case 204 : idx = 7; break;
      case 205 : idx = 8; break;
      case 206 : idx = 9; break;
      case 300 : idx = 10; break;
      case 301 : idx = 11; break;
      case 302 : idx = 12; break;
      case 303 : idx = 13; break;
      case 304 : idx = 14; break;
      case 305 : idx = 15; break;
      case 306 : idx = 16; break;
      case 307 : idx = 17; break;
      case 400 : idx = 18; break;
      case 401 : idx = 19; break;
      case 402 : idx = 20; break;
      case 403 : idx = 21; break;
      case 404 : idx = 22; break;
      case 405 : idx = 23; break;
      case 406 : idx = 24; break;
      case 407 : idx = 25; break;
      case 408 : idx = 26; break;
      case 409 : idx = 27; break;
      case 410 : idx = 28; break;
      case 411 : idx = 29; break;
      case 412 : idx = 30; break;
      case 413 : idx = 31; break;
      case 414 : idx = 32; break;
      case 415 : idx = 33; break;
      case 416 : idx = 34; break;
      case 417 : idx = 35; break;
      case 500 : idx = 36; break;
      case 501 : idx = 37; break;
      case 502 : idx = 38; break;
      case 503 : idx = 39; break;
      case 504 : idx = 40; break;
      case 505 : idx = 41; break;
      default  : return (FALSE);
   }
   CliStatusCodeNumber[idx] = CodeNumber;
   CliStatusCountDelta[idx] = CodeDelta;
   return (TRUE);
}

/****************************************************************************/
/*
*/
 
int ShowHelp ()
 
{
   fprintf (stdout,
"%%%s-I-HELP, usage for the WASD Over-The-Shoulder Uptime Picket (%s)\n\
\n\
The WOTSUP utility is designed to look at WASD in a production environment,\n\
over it's shoulder so-to-speak, to alert operations staff of conditions which\n\
might cause that production to be adversely impacted.  Triggers include server\n\
image exit and/or startup, percentage thresholds on process quotas, rates of\n\
HTTP status counter change, maximum period without request processing.  Alert\n\
reports are delivered via OPCOM message and/or email and/or a custom DCL\n\
command executed in a spawned subprocess, and/or log file.\n\
\n\
$ WOTSUP [qualifiers..] [parameters..]\n\
\n\
/CHECK /DO=<keyword> /EXIT[=<type>] /HELP /HTTP=(<number>:<delta>[,..])\n\
/INTERVAL=<seconds> /LOG=<filename> /MAIL[=<address>(es)] /OPCOM[=<target>]\n\
/QUIET[=<seconds>] /QUOTA[=<percent>] /SPAWN=<DCL-command>\n\
\n\
Usage examples:\n\
\n\
$ WOTSUP /DO=RESTART\n\
$ WOTSUP /OPCOM=OPER12 /MAIL=OPERATOR /CHECK\n\
$ WOTSUP /QUOTA=20 /HTTP=5:20 /OPCOM /MAIL=OPERATOR /SPAWN=@SUPPORT:WOTSUP.COM\n\
\n",
   Utility, SOFTWAREID);
 
   return (SS$_NORMAL);
}
 
/****************************************************************************/
/*
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.
*/ 
 
BOOL 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);
}
 
/*****************************************************************************/

