/*****************************************************************************/
/*
                                  WAStee.c

   +-----------------------------------------------------------------------+
   | The logical name WASD_SERVER_DAILY might be a more flexible approach! |
   | See "WASD Features, Utilities and Facilities"   -and/or-              |
   |     wasd_root:[src.httpd]httpd.c HttpdSysOutDaily()                   |
   +-----------------------------------------------------------------------+

WAStee is a utility to generate time-stamped log files containing intervals of
a long-lived WASD server process, and/or to consolidate all process log files
generated during the defined period.  It is the tee in a PIPE sequence.
And yes, an intended (somewhat lame) homophone.

The log files are generated as WASD_SERVER_LOGS:node_yyyymmdd.LOG and
reflect a shortened file name of the default, primary server log file. 
Alternate location for the log files may be specified using the logical name
WASTEE_LOCATION.  SYSPRV is used to create the file so the account must
possess that privilege, or the image INSTALLed with it, and the location permit
SYSPRV write access.

This utility is UNSUITABLE for sites using multiple instances and/or
environments on a node.  Only the first of multiple server processes will have
the log teed.  Subsequent processes see a message of the ilk:

%WASTEE-E-OPEN, 14-MAR-2021 01:28:11 WASD_SERVER_LOGS:KLAATU_20210308.LOG
-WASTEE-E-ERROR, file currently locked by another user

The teed logs may be identified (and distinguished from primary logs) using

  WASD_SERVER_LOGS:*_%%%%%%%%.LOG

Both the teed log file and primary log file have a %WASTEE-I-OPEN dd-mmm-yyyy
hh:mm:ss timestamp inserted each time a log file is opened (i.e. on interval
rollover and server startup), and a complementary %WASTEE-I-CLOSE at exit.

Intervals supported; DAILY, WEEKLY (default), MONTHLY. 

Interval can be changed from default by logical name WASTEE_INTERVAL.

Weekly days are 1..7 (Mon..Sun) defaulting to Mon.  The WASTEE_INTERVAL logical
name will accept a digit representing the weekday on which to rollover the log. 
For example, to rollover on Saturday

  $ DEFINE /SYSTEM WASTEE_INTERVAL WEEKLY6

The log file location may be specified using logical name WASTEE_LOCATION.

  $ DEFINE /SYSTEM WASTEE_LOCATION WASD_ROOT:[LOG_SERVER]

NOTE: just to emphasise; unlike the regular process logs where a new file is
created each time the [STARTUP]STARTUP.COM procedure is executed, these teed
logs accumulate any and all startups within the interval being logged.  The
regular server process log is also populated.  The PIPE introduces some latency
to this that the Server Statistics process Output report cannot control.


COMMAND-LINE
------------
When outside of a PIPE environment the utility merely exits without a qualifier.
All but the /ppf qualifier are intended as general command-line operation.

/next     display next interval log file name
/ppf      defines job-level logicals containing process-permanent file names
/this     display current interval log file name
/version  display utility version information


PIPED WASD LOGS
---------------
To WAStee WASD server process logs prepend the following TEN lines to
WASD_ROOT:[STARTUP]STARTUP_SERVER.COM (despite the "DO NOT MODIFY THIS
PROCEDURE!" :-) so it looks like this:

$! ~~~~ see WASD_ROOT:[SRC.UTILS]WASTEE.C ~~~
$ if f$trnlnm("SYS$PIPE") .eqs. ""
$ then
$    arch_name = f$edit(f$getsyi("arch_name"),"upcase")
$    if arch_name .eqs. "ALPHA" then arch_name = "axp"
$    wastee = "$wasd_root:[startup]wastee_''arch_name'.exe"
$    wastee /ppf
$    pipe @WASD_ROOT:[STARTUP]STARTUP_SERVER.COM | wastee
$    exit
$ endif
$!-----------------------------------------------------------------------------
$! STARTUP_SERVER.COM
8< snip 8<


BUILD DETAILS
-------------
$ @BUILD_WASTEE BUILD  !compile+link
$ @BUILD_WASTEE LINK   !link-only

NOTE: links executable as WASD_ROOT:[STARTUP]WASTEE_<arch>.EXE


COPYRIGHT
---------
Copyright (C) 2021 Mark G.Daniel

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


VERSION HISTORY
---------------
20-MAR-2021  MGD  v1.1.0, refinements based on field-test
10-MAR-2021  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "1.1.0"
#define SOFTWARENM "WASTEE"
#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 <ints.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <types.h>

/* VMS-related header files */
#include <descrip.h>
#include <jpidef.h>
#include <fabdef.h>
#include <iledef.h>
#include <lnmdef.h>
#include <namdef.h>
#include <libdtdef.h>
#include <lib$routines.h>
#include <prvdef.h>
#include <ssdef.h>
#include <syidef.h>
#include <starlet.h>

int  louterrno;
char  interval[] = "weekly1";
FILE  *lout;

void WeekDay (int64*, ushort[], int, int);
void InaMinute ();
char* LogName (ushort[]);
void LogTime (int64*, ushort[], int);
void SetProcessName ();
void WASteeExit ();
void WASteeLogFileName (char*);
int WASteePPF();
void WASteeStamp (char*);

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

main (int argc, char** argv)
       
{
   static char  record [2048];

   if (argc > 1)
   {
      if (!strcasecmp(argv[1],"/ppf"))
      {
         /* set process name only if NOT interactive */
         if (WASteePPF ()) SetProcessName ("WASD:pipe");
      }
      else
      if (!strcasecmp(argv[1],"/this"))
      {
         ushort  time7 [7];
         LogTime (0, time7, 0);
         if (interval[0] == '?') exit (SS$_IVTIME);
         fprintf (stdout, "%%%s-I-%s, %s\n",
                  SOFTWARENM, interval, LogName(time7));
      }
      else
      if (!strcasecmp(argv[1],"/next"))
      {
         ushort  time7 [7];
         LogTime (0, time7, 1);
         if (interval[0] == '?') exit (SS$_IVTIME);
         fprintf (stdout, "%%%s-I-%s, %s\n",
                  SOFTWARENM, interval, LogName(time7));
      }
      else
      if (!strcasecmp(argv[1],"/version"))
         puts (SOFTWAREID);
      exit (SS$_NORMAL);
   }
   else
   if (!getenv ("SYS$PIPE"))
      exit (SS$_NORMAL);

   atexit (WASteeExit);

   SetProcessName ("WAStee");
   InaMinute ();
   if (interval[0] == '?') exit (SS$_IVTIME);
   for (;;)
   {
      if (!fgets (record, sizeof(record), stdin)) break;
      if (fputs (record, stdout) == EOF) break;
      fsync (fileno(stdout));
      if (lout)
      {
         if (fputs (record, lout) == EOF) break;
         fsync (fileno(lout));
      }
   }
   exit (vaxc$errno);
}

/*****************************************************************************/
/*
On image exit put a timestamp into the log(s).
*/

void WASteeExit ()
       
{
   WASteeStamp ("CLOSE");
   if (lout) fclose (lout);
}

/*****************************************************************************/
/*
Put a timestamp into both primary and teed logs.
*/

void WASteeStamp (char *cptr)
       
{
   static char  stamp [48+256];
   static $DESCRIPTOR (StampDsc, stamp);
   static $DESCRIPTOR (StampFaoDsc, "%!AZ-!AZ-!AZ, !20%D !AZ\n\0");

   sys$fao (&StampFaoDsc, 0, &StampDsc,
            SOFTWARENM, louterrno ? "E" : "I", cptr, 0, LogName(NULL));
   fputs (stamp, stdout);
   fsync (fileno(stdout));
   fputs (stamp, lout);
   fsync (fileno(lout));
}

/*****************************************************************************/
/*
Called once a minute.
*/

void InaMinute ()
       
{
   static ulong  SysPrvMask [2] = { PRV$M_SYSPRV, 0 };

   static int64  d1min64 = -600000000;  /* one minute delta */
   static int  newlog, status,
               phour = -1,
               pday = -1,
               pmonth = -1;
   static int64  time64;
   static ushort  time7 [7],
                  roll7 [7];
   static char  *lname;

   sys$gettim (&time64);
   sys$numtim (&time7, &time64);

   if (time7[3] != phour)
   {
      phour = time7[3];
      LogTime (&time64, roll7, 0);
      newlog = ((pmonth < 0 && pday < 0) ||
                (pmonth != roll7[1] || pday != roll7[2]));

      if (newlog)
      {
         pmonth = roll7[1];
         pday = roll7[2];

         lname = LogName (roll7);

         sys$setprv (1, &SysPrvMask, 0, 0);
         if (lout)
         {
            WASteeStamp ("CLOSE");
            fclose (lout);
         }
         if (!(lout = fopen (lname, "a", "shr=get"))) louterrno = vaxc$errno;
         sys$setprv (0, &SysPrvMask, 0, 0);

         WASteeStamp ("OPEN");
         if (louterrno)
            fprintf (stdout, "-%s-E-ERROR, %s\n",
                     SOFTWARENM, strerror(EVMSERR, louterrno));
         else
            WASteeLogFileName (lname);
      }
   }

   status = sys$setimr (0, &d1min64, InaMinute, 0, 0);
   if (!(status & 1)) exit (status);
}

/*****************************************************************************/
/*
Generate and return a pointer to a log file name.
*/

char* LogName (ushort time7ptr[])
       
{
   static ushort  SyiNodeLength;
   static int  SyiNodeNameItem = SYI$_NODENAME;
   static char  SyiNodeName [16];
   static $DESCRIPTOR (SyiNodeNameDsc, SyiNodeName);

   static char  logname [256];

   int  status;
   char  *location;
   ushort  slen;

   if (!SyiNodeName[0])
   {
      status = lib$getsyi (&SyiNodeNameItem, 0, &SyiNodeNameDsc, &slen, 0, 0);
      if (!(status & 1)) exit (status);
      SyiNodeName[slen] = '\0';
   }

   if (!time7ptr) return (logname);

   if (!(location = getenv ("WASTEE_LOCATION")))
      location = "WASD_SERVER_LOGS:";

   sprintf (logname, "%s%s_%d%02d%02d.LOG",
            location, SyiNodeName, time7ptr[0], time7ptr[1], time7ptr[2]);

   return (logname);
}

/****************************************************************************/
/*
Calculate the data/time of the current/next log and populate the vector time.
*/

void LogTime (int64 *time64ptr, ushort time7ptr[], int next)

{
   static int64  d1day64 = (int64)-600000000 * 60 * 24;  /* one day delta */

   int64  time64;
   char  *cptr;

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

   if (time64ptr)
      time64 = *time64ptr;
   else
      sys$gettim (&time64);
   sys$numtim (time7ptr, &time64);

   if (!(cptr = getenv ("WASTEE_INTERVAL"))) cptr = "W1";

   if (*cptr == 'D')
   {
      strcpy (interval, "DAILY");
      if (next) time64 -= d1day64;
      sys$numtim (time7ptr, &time64);
   }
   else
   if (*cptr == 'W')
   {
      /* does the weekday also specify a day of the week integer */
      int  dayof = 1;
      while (*cptr && !isdigit(*cptr)) cptr++;
      if (*cptr >= '1' && *cptr <= '7') dayof = *cptr - '0';
      strcpy (interval, "WEEKLY");
      interval[6] = dayof + '0';
      WeekDay (&time64, time7ptr, dayof, next);
   }
   else
   if (*cptr == 'M')
   {
      int  pmonth;
      strcpy (interval, "MONTHLY");
      /* add a day until the month changes (q&d) */
      pmonth = time7ptr[1];
      for (;;)
      {
         if (next) 
            time64 -= d1day64;
         else
            time64 += d1day64;
         sys$numtim (time7ptr, &time64);
         if (time7ptr[1] != pmonth) break;
      }
      if (!next) time64 -= d1day64;
   }
   else
      strcpy (interval, "?");
}

/*****************************************************************************/
/*
Get the day of the week (1..7) and calculate the date (year/month/day) adjusted
for that day of the week.  Adjust the numeric time structure provided.
Return the day of the month, as calculated by the specified weekday.
*/

void WeekDay (int64 *time64ptr, ushort time7ptr[], int dayof, int next)
       
{
   static int  LibDayOfWeek = LIB$K_DAY_OF_WEEK;
   static int64  d1day64 = (int64)-600000000 * 60 * 24;  /* one day delta */
   static int64  dweek64;
   static int  aday, wday;
   static int64  ptime64;

   if (ptime64 == *time64ptr)
   {
      /* same time as previous call, no need to recalculate */
      sys$numtim (time7ptr, &dweek64);
      return;
   }
   ptime64 = *time64ptr;

   /* return 1 for Mon, 2 for Tue .. 7 for Sun */
   lib$cvt_from_internal_time (&LibDayOfWeek, &wday, time64ptr);
   if (wday < dayof)
   {
      aday = dayof - wday - 7;
      if (next) aday += 7;
      dweek64 = *time64ptr - (d1day64 * aday);
   }
   else
   {
      aday = wday - dayof;
      if (next) aday -= 7;
      dweek64 = *time64ptr + (d1day64 * aday);
   }
   sys$numtim (time7ptr, &dweek64);
}

/****************************************************************************/
/*
Set the process name so it's unique.  Must be a maximum of ten characters.
*/

void SetProcessName (char *name)

{
   static unsigned long  JpiPidItem = JPI$_PID;
   static char  PrcNam [16];
   static $DESCRIPTOR (PrcNamDsc, PrcNam);

   int  status;
   unsigned long  ScriptPid;
   struct dsc$descriptor_s  *dscptr;

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

   lib$getjpi (&JpiPidItem, 0, 0, &ScriptPid, 0, 0);
   dscptr = &PrcNamDsc;
   dscptr->dsc$w_length = sprintf (PrcNam, "%s_%4.4X",
                                   name, ScriptPid & 0xffff);
   status = sys$setprn (dscptr);
   if (!(status & 1)) exit (status);
}

/*****************************************************************************/
/*
Get the values of the SYS$INPUT and SYS$OUTPUT process-permanent files (PPFs)
and create job-level logical names containing those values that can be used by
the HTTPd server for admin purposes.

Derived from WASD_ROOT:[SRC.HTTPD]HTTPD.C HttpdProcessInfo().
*/

int WASteePPF ()

{
   static $DESCRIPTOR (InpNameDsc, "WASTEE_SYS$INPUT");
   static $DESCRIPTOR (OutNameDsc, "WASTEE_SYS$OUTPUT");
   static $DESCRIPTOR (LnmJobDsc, "LNM$JOB");
   static $DESCRIPTOR (LnmProcessDsc, "LNM$PROCESS");
   static $DESCRIPTOR (SysOutputDsc, "SYS$OUTPUT");
   static $DESCRIPTOR (SysInputDsc, "SYS$INPUT");
   static int  JpiModeItem = JPI$_MODE;
   static ulong  JpiMode;
   static ushort  InpLen,
                  OutLen,
                  ValueLen;

   static char  LogValue [64],
                SysInput [256],
                SysOutput [256];
   static struct _ile3  TrnLnmItem [] =
   {
      { sizeof(LogValue)-1, LNM$_STRING, LogValue, &ValueLen },
      { 0,0,0,0 }
   };
   static struct _ile3  CreLnmInpItem [] =
   {
      { 0, LNM$_STRING, SysInput, 0 },
      { 0,0,0,0 }
   };
   static struct _ile3  CreLnmOutItem [] =
   {
      { 0, LNM$_STRING, SysOutput, 0 },
      { 0,0,0,0 }
   };

   int  status, inplen, outlen;
   struct FAB  aFab;
   struct NAM  aNam;

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

   /* check process mode */
   status = lib$getjpi (&JpiModeItem, 0, 0, &JpiMode, 0, 0);
   if (!(status & 1)) exit (status);
   /* if interactive then do not proceed */
   if (JpiMode == 3) return (0);

   status = sys$trnlnm (0, &LnmProcessDsc, &SysInputDsc, 0, &TrnLnmItem);
   if (!(status & 1)) exit (status);

   if (*(ushort*)LogValue == 0x001b)
   {
      aFab = cc$rms_fab;
      aFab.fab$l_nam = &aNam;
      aFab.fab$b_fac = FAB$M_GET;
      aFab.fab$w_ifi = *(ushort*)(LogValue+2) | FAB$M_PPF_IND;
      aNam = cc$rms_nam;
      aNam.nam$l_rsa = SysInput;
      aNam.nam$b_rss = sizeof(SysInput)-1;

      status = sys$display (&aFab, 0, 0);

      SysInput[inplen = aNam.nam$b_rsl] = '\0';
      if (!(status & 1)) exit (status);
   }
   else
      strcpy (SysInput, "PPF?");

   status = sys$trnlnm (0, &LnmProcessDsc, &SysOutputDsc, 0, &TrnLnmItem);
   if (!(status & 1)) exit (status);

   if (*(ushort*)LogValue == 0x001b)
   {
      aFab = cc$rms_fab;
      aFab.fab$l_nam = &aNam;
      aFab.fab$b_fac = FAB$M_GET;
      aFab.fab$w_ifi = *(ushort*)(LogValue+2) | FAB$M_PPF_IND;

      aNam = cc$rms_nam;
      aNam.nam$l_rsa = SysOutput;
      aNam.nam$b_rss = sizeof(SysOutput)-1;

      status = sys$display (&aFab, 0, 0);

      SysOutput[outlen = aNam.nam$b_rsl] = '\0';
      if (!(status & 1)) exit (status);
   }
   else
      strcpy (SysOutput, "PPF?");

   CreLnmInpItem[0].ile3$w_length = inplen;
   status = sys$crelnm (0, &LnmJobDsc, &InpNameDsc, 0, &CreLnmInpItem);
   if (!(status & 1)) exit (status);

   CreLnmOutItem[0].ile3$w_length = outlen;
   status = sys$crelnm (0, &LnmJobDsc, &OutNameDsc, 0, &CreLnmOutItem);
   if (!(status & 1)) exit (status);

   return (1);
}

/*****************************************************************************/
/*
Define a job-level logical name containing the teed log file name.  Can be used
by the HTTPd server admin report.
*/

void WASteeLogFileName (char *lname)

{
   static $DESCRIPTOR (LnmJobDsc, "LNM$JOB");
   static $DESCRIPTOR (LogNameDsc, "WASTEE_LOGFILE");

   static struct _ile3  CreLnmItem [] =
   {
      { 0, LNM$_STRING, 0, 0 },
      { 0,0,0,0 }
   };

   int  status;

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

   CreLnmItem[0].ile3$w_length = strlen(lname);
   CreLnmItem[0].ile3$ps_bufaddr = lname;
   status = sys$crelnm (0, &LnmJobDsc, &LogNameDsc, 0, &CreLnmItem);
   if (!(status & 1)) exit (status);
}

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

