/*****************************************************************************/
/*
                                  netIO.c

The fundamental structure and code for the a/synchronous $QIO with the network. 
If the NETIO structure contain a Transport Layer Security (secure-sockets)
pointer it implicitly uses the TLS/SSL encrypted equivalent.  If the NEIO
struct contains a pointer to an HTTP/2 stream struct then uses the HTTP/2
pointer within that to perform HTTP/2 I/O (of course the HTTP/2 I/O ultimately
uses a NETIO struct to mediate the actual I/O, most often via TLS/SSL).

Reading and writing data size now have no architectural limit.  It's handled
internally.  Same for SSL/TLS.  With the potential for data size/length greater
than a single QIO the IO status block is deprecated in favour of
|ioptr->Read/WriteCount| and |ioptr->Read/WriteStatus|.


$QIO WRITE BUFFER
-----------------
Using logical name WASD_QIO_MAXSEG as described in NetIoQioMaxSeg() maximising
the $QIO write buffer definitely results the most efficient transfer of network
data.  Note the MONITOR MODES data for interupt, kernel, user and idle
percentages.  Roughly half the higher modes for the same USER mode.

$QIO at MSS * ((65535 / MSS) - 1)
i.e. for a MSS of 1460 would use 62780 where a maximum buffer would write 43
full MSS datagrams.  The 62780 also corresponds to the mysterious "Socket
buffer bytes" and "Socket buffer quota" values.

  $ tcpip show device /full bg29146
  Device_socket:  bg29146     Type: STREAM
                                                      RECEIVE       SEND
                              Queued I/O                    0             0
  Q0LEN         0             Socket buffer bytes           0         62780
  QLEN          0             Socket buffer quota       62780         62780
  8< snip 8<

The test setup and monitor results:

  $ curl -vo /dev/null http://n.n.n.n/stream/octet/

  HP TCP/IP Services for OpenVMS Alpha Version V5.7 - ECO 5
  on a Digital Personal WorkStation  running OpenVMS V8.4-2L1

  $QIO at MSS
  $ curl -vo /dev/null http://192.168.1.3/stream/octet/
  Interrupt State                  39 |***************
  Kernel Mode                      15 |******
  User Mode                        20 |********
  Idle Time                        26 |**********

  $QIO at MSS
  $ curl -vo /dev/null http://192.168.1.3/stream/qio/
  Interrupt State                  32 |************
  Kernel Mode                      49 |*******************
  User Mode                         6 |**
  Idle Time                        12 |****

  $QIO at (65535 / MSS) * MSS
  $ curl -vo /dev/null http://192.168.1.3/stream/octet/
  Interrupt State                  17 |******
  Kernel Mode                       4 |*
  User Mode                        17 |******
  Idle Time                        62 |************************

  $QIO at (65535 / MSS) * MSS
  $ curl -vo /dev/null http://192.168.1.3/stream/qio/
  Interrupt State                  19 |*******
  Kernel Mode                       9 |***
  Executive Mode                    1 |
  User Mode                         1 |
  Idle Time                        71 |****************************

  VSI TCP/IP Services for OpenVMS x86_64 Version X6.0
  on an innotek GmbH VirtualBox running OpenVMS V9.1-A (2 x CPU)

  $QIO at MSS 
  $ curl -vo /dev/null http://192.168.1.86/stream/octet/
  Interrupt State                  58 |***********
  MP Synchronization                3 |
  Kernel Mode                      19 |***
  User Mode                         8 |*
  Idle Time                       112 |**********************

  $QIO at MSS 
  $ curl -vo /dev/null http://192.168.1.86/stream/qio/
  Interrupt State                  80 |****************
  MP Synchronization                1 |
  Kernel Mode                      50 |**********
  User Mode                         1 |
  Idle Time                        68 |*************

  $QIO at (65535 / MSS) * MSS
  $ curl -vo /dev/null http://192.168.1.86/stream/octet/
  Interrupt State                  33 |******
  Kernel Mode                       1 |
  User Mode                         7 |*
  Idle Time                       159 |*******************************

  $QIO at (65535 / MSS) * MSS
  $ curl -vo /dev/null http://192.168.1.86/stream/qio/
  Interrupt State                  32 |******
  Kernel Mode                       4 |
  Idle Time                       164 |********************************


There was also a corresponding increase in data transfer rate (MB/s).


VERSION HISTORY
---------------
15-JUL-2020  MGD  bugfix; NetIoWriteStatus() and NetIoReadStatus()
18-APR-2020  MGD  NetIoQioMaxSeg() tune QIO to TCP MSS
24-DEC-2019  MGD  NetIoWriteStatus() and NetIoReadStatus() count parameter
20-JAN-2018  MGD  refactor NetPeek() into NetIoPeek()
                  NetIoRead() and NetIoWrite() ..IOsb.Status = 0
                  NetIoRead() and NetIoWrite() blocking NetIo..Ast() each I/O
11-AUG-2015  MGD  restructure of network I/O abstractions
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#  undef __VMS_VER
#  define __VMS_VER 70000000
#  undef __CRTL_VER
#  define __CRTL_VER 70000000
#else
#  ifdef WASD_VMS_V7
#     undef _VMS__V6__SOURCE
#     define _VMS__V6__SOURCE
#     undef __VMS_VER
#     define __VMS_VER 70000000
#     undef __CRTL_VER
#     define __CRTL_VER 70000000
#   endif
#endif

#include <stdio.h>
#include <ctype.h>

#include <iodef.h>

#include "wasd.h"

#define WASD_MODULE "NETIO"

/* calculate $QIO buffer size */
#define NETIO_QIO_SIZE(mss) (mss * ((65535 / mss) - 1))

/******************/
/* global storage */
/******************/

/********************/
/* external storage */
/********************/

extern uint  EfnWait,
             EfnNoWait,
             HttpdTickSecond;

extern char  ErrorSanityCheck[];

extern struct dsc$descriptor TcpIpDeviceDsc;

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern CONFIG_STRUCT  Config;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Allocate (global) memory for the network I/O structure and assign a channel to
the internet template device.  Return a pointer to the structure if successful
or NULL if not.  At a maximum of every second or so reports any channel
assignment failure until the maximum period is exceeded (currently one minute).
*/ 

NETIO_STRUCT* NetIoBegin ()

{
   static ulong  ExitTickSecond,
                 PrevTickSecond;
   int  status;
   ushort  channel;
   char  *cptr;
   NETIO_STRUCT  *ioptr;

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

   if (WATCH_MODULE (WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetIoBegin()");

   status = sys$assign (&TcpIpDeviceDsc, &channel, 0, 0);

   if (VMSnok (status))
   {
      /* manage channel assignment failure */
      if (HttpdTickSecond < ExitTickSecond)
      {
         if (HttpdTickSecond != PrevTickSecond)
         {
            ErrorNoticed (NULL, status, NULL, FI_LI);
            PrevTickSecond = HttpdTickSecond;
            if (!ExitTickSecond)
               ExitTickSecond = HttpdTickSecond + NET_ASSIGN_FAIL_MAX;
         }
         /* hmmm, probably BYTLM exhausted */
         if (status == SS$_EXQUOTA) return (NULL);
         /* shouldn't have exhausted these, but seem to have */
         if (status == SS$_NOIOCHAN) return (NULL);
         /* some other (serious) error */
      }
      ErrorExitVmsStatus (status, "sys$assign()", FI_LI);
   }
   ExitTickSecond = PrevTickSecond = 0;

   ioptr = VmGet (sizeof(NETIO_STRUCT));
   ioptr->Channel = channel;

   return (ioptr);
}

/*****************************************************************************/
/*
Proxy NETIO does not need the client data structure used by request processing
and so conserve a little memory by not allocating that (bit shonky I know). 
This is noted in NET.H as well.
*/ 

NETIO_STRUCT* NetIoProxyBegin ()

{
   NETIO_STRUCT  *ioptr;

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

   if (WATCH_MODULE (WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetIoProxyBegin()");

   ioptr = VmGet (sizeof(NETIO_STRUCT) - sizeof(CLIENT_STRUCT));

   return (ioptr);
}

/*****************************************************************************/
/*
Deassign the channel as necessary and free the network I/O structure.
If a TLS/SSL then do the equivalent.
*/ 

void NetIoEnd (NETIO_STRUCT *ioptr)

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET, "NetIoEnd()");

   if (ioptr->SesolaPtr)
      SesolaNetEnd (ioptr->SesolaPtr);
   else
   {
      sys$dassgn (ioptr->Channel);
      VmFree (ioptr, FI_LI);
   }
}

/*****************************************************************************/
/*
Write 'DataLength' bytes located at 'DataPtr' to the client either using either
the "raw" network, or via HTTP/2, or via the Secure Sockets Layer.

If 'AstFunction' zero then use sys$qiow(), waiting for completion. If
an AST completion address is supplied then use sys$qio().  If empty data
buffer is supplied (zero length) then declare an AST to service any AST
routine supplied.  If none then just return.

Explicitly declares any AST routine if an error occurs. The calling function
must not do any error recovery if an AST routine has been supplied but the
associated AST routine must!  If an AST was not supplied then the return
status can be checked.
*/ 

int NetIoWrite
(
NETIO_STRUCT *ioptr,
VOID_AST AstFunction,
void *AstParam,
void *DataPtr,
uint DataLength
)
{
   int  status;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET,
                 "NetIoWrite() !&X !&A !&X !UL",
                 ioptr, AstFunction, DataPtr, DataLength);

   if (DataPtr)
   {
      /* first call */
      if (!ioptr->Stream2Ptr || AstFunction)
      {
         /* not HTTP/2 or non-blocking */
         if (ioptr->WriteAstFunction)
         {
            HttpdStackTrace ("NetIoWrite()", FI_LI);
            status = SS$_BUGCHECK;
            ErrorNoticed (ioptr->RequestPtr, status, "!&A",
                          FI_LI, ioptr->WriteAstFunction);
            return (status);
         }
         ioptr->WriteCount = 0;
         ioptr->WriteStatus = 0;
         ioptr->WriteAstFunction = AstFunction;
         ioptr->WriteAstParam = AstParam;
         ioptr->WritePtr = DataPtr;
         ioptr->WriteLength = DataLength;
         ioptr->WriteIOsb.Count = 0;
         ioptr->WriteIOsb.Status = 0;
      }
   }

   if (ioptr->Stream2Ptr)
   {
      /**********/
      /* HTTP/2 */
      /**********/

      status = Http2NetIoWrite (ioptr, AstFunction, AstParam,
                                DataPtr, DataLength);
      return (status);
   }

   if (ioptr->SesolaPtr)
   {
      /*****************/
      /* secure socket */
      /*****************/

      status = SesolaNetIoWrite (ioptr, DataPtr, DataLength);
      return (status);
   }

   if (ioptr->VmsStatus)
   {
      /*********************************/
      /* deliver explicitly set status */
      /*********************************/

      if (ioptr->WriteAstFunction)
         SysDclAst (NetIoWriteAst, ioptr);
      else
         NetIoWriteAst (ioptr);
      return (ioptr->VmsStatus);
   }

   if (WATCHING (ioptr, WATCH_NETWORK_OCTETS))
   {
      int  dlen = ioptr->WriteLength - ioptr->WriteCount;
      if (dlen > ioptr->TcpMaxQio) dlen = ioptr->TcpMaxQio;
      WatchThis (WATCHITM(ioptr), WATCH_NETWORK,
                  "WRITE !UL (!UL/!UL) bytes (!&?non-blocking\rblocking\r)",
                  dlen, ioptr->WriteCount, ioptr->WriteLength,
                  ioptr->WriteAstFunction);
      WatchDataDump ((uchar*)ioptr->WritePtr + ioptr->WriteCount, dlen);
   }

   if (Watch.TriggerTxCount)
   {
      int  dlen = ioptr->WriteLength - ioptr->WriteCount;
      if (dlen > ioptr->TcpMaxQio) dlen = ioptr->TcpMaxQio;

      if (!ioptr->WatchTriggerTxPlus)
      {
         if (WatchTrigger ((uchar*)ioptr->WritePtr + ioptr->WriteCount,
                           dlen, Watch.TriggerTx))
         {
            ioptr->WatchTriggerTx = true;
            if (Watch.TriggerPlus) ioptr->WatchTriggerTxPlus = true;
         }
      }
      if (ioptr->WatchTriggerTx || ioptr->WatchTriggerTxPlus)
      {
         WatchSetTrigger (ioptr);
         ioptr->WatchTriggerTx = false;
      }
   }

   if (!ioptr->WriteLength)
      status = ioptr->WriteStatus = SS$_BUGCHECK;
   else
   if (ioptr->WriteAstFunction)
   {
      /*******************/
      /* non-blocking IO */
      /*******************/

      /* limit size of QIO */
      DataLength = ioptr->WriteLength - ioptr->WriteCount;
      if (DataLength > ioptr->TcpMaxQio) DataLength = ioptr->TcpMaxQio;

      status = sys$qio (EfnNoWait, ioptr->Channel,
                        IO$_WRITEVBLK, &ioptr->WriteIOsb,
                        &NetIoWriteAst, ioptr,
                        (uchar*)ioptr->WritePtr + ioptr->WriteCount,
                        DataLength,
                        0, 0, 0, 0);

      if (VMSnok (status)) ioptr->WriteStatus = status;
   }
   else
   {
      /***************/
      /* blocking IO */
      /***************/

      while (ioptr->WriteCount < ioptr->WriteLength)
      {
         /* limit size of QIO */
         DataLength = ioptr->WriteLength - ioptr->WriteCount;
         if (DataLength > ioptr->TcpMaxQio) DataLength = ioptr->TcpMaxQio;

         status = sys$qiow (EfnWait, ioptr->Channel,
                            IO$_WRITEVBLK, &ioptr->WriteIOsb, 0, 0,
                            (uchar*)ioptr->WritePtr + ioptr->WriteCount,
                            DataLength,
                            0, 0, 0, 0);

         if (VMSnok (status))
            ioptr->WriteStatus = status;
         else
            NetIoWriteAst (ioptr);

         if (VMSnok (ioptr->WriteStatus)) break;
      }
   }

   /****************/
   /* check status */
   /****************/

   if (VMSok (status)) return (status);

   /* if resource wait enabled the only quota not waited for is ASTLM */
   if (status == SS$_EXQUOTA)
   {
      /* no ASTs means not much of anything else can happen so just exit! */
      sys$canexh(0);
      /* make the message a little more meaningful */
      sys$exit (SS$_EXASTLM);
   }

   /* write failed, call AST explicitly, status in the IOsb */
   ioptr->WriteIOsb.Status = status;
   ioptr->WriteIOsb.Count = 0;
   if (ioptr->WriteAstFunction)
      SysDclAst (NetIoWriteAst, ioptr);
   else
      NetIoWriteAst (ioptr);

   return (status);
}

/*****************************************************************************/
/*
AST from NetIoWrite().  Call the AST function.
*/ 

void NetIoWriteAst (NETIO_STRUCT *ioptr)

{
   void  *AstParam;
   VOID_AST  AstFunction;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET,
                 "NetIoWriteAst() !&F !&X !&X !&S !UL !UL !UL !&A",
                 &NetIoWriteAst, ioptr, ioptr->VmsStatus,
                 ioptr->WriteIOsb.Status, ioptr->WriteIOsb.Count,
                 ioptr->WriteLength, ioptr->WriteCount+ioptr->WriteIOsb.Count,
                 ioptr->WriteAstFunction);

   if (ioptr->VmsStatus)
   {
      /* explicitly set status */
      ioptr->WriteIOsb.Status = ioptr->VmsStatus;
      ioptr->WriteIOsb.Count = 0;
   }

   ioptr->WriteStatus = ioptr->WriteIOsb.Status;

   if (ioptr->WriteIOsb.Status == SS$_NORMAL &&
       !(ioptr->WriteCount || ioptr->WriteLength))
   {
      /* special case, do not tally or report via WATCH */
      if (AstFunction = ioptr->WriteAstFunction)
      {
         AstParam = ioptr->WriteAstParam;
         ioptr->WriteAstFunction = ioptr->WriteAstParam = NULL;
         AstFunction (AstParam);
      }
      return;
   }

   if (WATCHPNT(ioptr))
   {
      /* the SS$_WASECC is returned when GZIPing network data */
      if (ioptr->WriteIOsb.Status != SS$_WASECC)
      {
         if (WATCH_CATEGORY(WATCH_NETWORK))
            WatchThis (WATCHITM(ioptr), WATCH_NETWORK,
"WRITE !&S !UL (!UL/!UL) bytes (!&?non-blocking\rblocking\r)",
                       ioptr->WriteIOsb.Status,
                       ioptr->WriteIOsb.Count,
                       ioptr->WriteCount + ioptr->WriteIOsb.Count,
                       ioptr->WriteLength, ioptr->WriteAstFunction);
         if (WATCH_CATEGORY(WATCH_RESPONSE))
            if (VMSnok (ioptr->WriteIOsb.Status))
               WatchThis (WATCHITM(ioptr), WATCH_RESPONSE,
                          "NETWORK !&S (!&?non-blocking\rblocking\r)",
                          ioptr->WriteIOsb.Status, ioptr->WriteAstFunction);
      }
   }


   if (VMSok (ioptr->WriteIOsb.Status))
   {
      ioptr->BlocksRawTx64++;
      ioptr->BlocksTallyTx64++;
      ioptr->BytesRawTx64 += ioptr->WriteIOsb.Count;
      ioptr->BytesTallyTx64 += ioptr->WriteIOsb.Count;
      ioptr->WriteCount += ioptr->WriteIOsb.Count;
      if (ioptr->WriteAstFunction)
      {
         if (ioptr->WriteCount < ioptr->WriteLength)
         {
            /* continue to write */
            NetIoWrite (ioptr, NULL, NULL, NULL, 0);
            return;
         }
      }
   }
   else
   {
      ioptr->WriteErrorCount++;
      /* just note the first error status */
      if (!ioptr->WriteErrorStatus)
         ioptr->WriteErrorStatus = ioptr->WriteIOsb.Status;
      if (ioptr->WriteIOsb.Status &&
          !(ioptr->WriteIOsb.Status == SS$_ABORT ||
            ioptr->WriteIOsb.Status == SS$_CANCEL ||
            ioptr->WriteIOsb.Status == SS$_CONNECFAIL ||
            ioptr->WriteIOsb.Status == SS$_IVCHAN ||
            ioptr->WriteIOsb.Status == SS$_LINKDISCON ||
            ioptr->WriteIOsb.Status == SS$_TIMEOUT ||
            ioptr->WriteIOsb.Status == SS$_UNREACHABLE ||
            ioptr->WriteIOsb.Status == SS$_VCCLOSED))
      {
         char  *sptr = "(none)";     
         REQUEST_STRUCT *rqptr;
         if (rqptr = ioptr->RequestPtr)
            if (rqptr->rqHeader.RequestUriPtr)
               sptr = rqptr->rqHeader.RequestUriPtr;
         ErrorNoticed (NULL, ioptr->WriteIOsb.Status, "!AZ", FI_LI, sptr);
      }
   }
      
   if (AstFunction = ioptr->WriteAstFunction)
   {
      AstParam = ioptr->WriteAstParam;
      ioptr->WriteAstFunction = ioptr->WriteAstParam = NULL;
      AstFunction (AstParam);
   }
}

/*****************************************************************************/
/*
Deliver the supplied VMS status code via AST while maintaining the network
write in-progress conditions.  As with other I/O the delivery needs to be
decoupled in case it gets called again during that delivery.
*/ 

int NetIoWriteStatus
(
NETIO_STRUCT *ioptr,
VOID_AST AstFunction,
void *AstParam,
int AstStatus,
int WriteCount
)
{
   int  status;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET,
                 "NetIoWriteStatus() !&X !&A !&S",
                 ioptr, AstFunction, AstStatus);

   if (ioptr->WriteAstFunction)
   {
      char  buf [256];
      FaoToBuffer (buf, sizeof(buf), NULL, "!&A", ioptr->WriteAstFunction);
      ErrorExitVmsStatus (SS$_BUGCHECK, buf, FI_LI);
   }
   ioptr->WriteAstFunction = AstFunction;
   ioptr->WriteAstParam = AstParam;
   if (!WriteCount) ioptr->WritePtr = NULL;
   ioptr->WriteCount = 0;
   ioptr->WriteLength = WriteCount;
   ioptr->WriteIOsb.Count = WriteCount;
   ioptr->WriteIOsb.Status = AstStatus;

   if (AstFunction)
      SysDclAst (NetIoWriteAst, ioptr);
   else
      NetIoWriteAst (ioptr);

   return (AstStatus);
}

/*****************************************************************************/
/*
Queue up a read from the client over the network. If 'AstFunction' 
is zero then no I/O completion AST routine is called.  If it is non-zero then 
the function pointed to by the parameter is called when the network write 
completes.

Explicitly declares any AST routine if an error occurs. The calling function
must not do any error recovery if an AST routine has been supplied but the
associated AST routine must!  If an AST was not supplied then the return
status can be checked.
*/ 

int NetIoRead
(
NETIO_STRUCT *ioptr,
VOID_AST AstFunction,
void *AstParam,
void *DataPtr,
uint DataSize
)
{
   int  status;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET,
                  "NetIoRead() !&X !&A !&X !UL",
                  ioptr, AstFunction, DataPtr, DataSize);

   if (DataPtr)
   {
      /* first call */
      if (ioptr->ReadAstFunction)
      {
         char  buf [256];
         HttpdStackTrace ("NetIoRead()", FI_LI);
         FaoToBuffer (buf, sizeof(buf), NULL, "!&A", ioptr->ReadAstFunction);
         status = SS$_BUGCHECK;
         ErrorNoticed (ioptr->RequestPtr, status, buf, FI_LI);
         return (status);
      }
      ioptr->ReadCount = 0;
      ioptr->ReadStatus = 0;
      ioptr->ReadAstFunction = AstFunction;
      ioptr->ReadAstParam = AstParam;
      ioptr->ReadPtr = DataPtr;
      ioptr->ReadSize = DataSize;
      ioptr->ReadIOsb.Count = 0;
      ioptr->ReadIOsb.Status = 0;
   }

   if (ioptr->Stream2Ptr)
   {
      /**********/
      /* HTTP/2 */
      /**********/

      status = Http2NetIoRead (ioptr);
      return (status);
   }

   if (ioptr->SesolaPtr)
   {
      /*****************/
      /* secure socket */
      /*****************/

      status = SesolaNetIoRead (ioptr, DataPtr, DataSize);
      return (status);
   }

   if (WATCHING (ioptr, WATCH_NETWORK))
      WatchThis (WATCHITM(ioptr), WATCH_NETWORK,
                 "!AZ !UL/!UL bytes (!&?non-blocking\rblocking\r)",
                 ioptr->ReadSize & NETIO_DATA_FILL_BUF ? "FILL" : "READ",
                 ioptr->ReadCount, ioptr->ReadSize & ~NETIO_DATA_FILL_BUF,
                 ioptr->ReadAstFunction);

   if (ioptr->VmsStatus)
   {
      /*********************************/
      /* deliver explicitly set status */
      /*********************************/

      if (ioptr->ReadAstFunction)
         SysDclAst (NetIoReadAst, ioptr);
      else
         NetIoReadAst (ioptr);
      return (ioptr->VmsStatus);
   }

   if (!ioptr->ReadSize)
      status = ioptr->ReadStatus = SS$_BUGCHECK;
   else
   if (ioptr->ReadAstFunction)
   {
      /*******************/
      /* non-blocking IO */
      /*******************/

      DataSize = ioptr->ReadSize & ~NETIO_DATA_FILL_BUF;
      DataSize -= ioptr->ReadCount;
      if (DataSize > ioptr->TcpMaxQio) DataSize = ioptr->TcpMaxQio;

      status = sys$qio (EfnNoWait, ioptr->Channel,
                        IO$_READVBLK, &ioptr->ReadIOsb,
                        &NetIoReadAst, ioptr,
                        (uchar*)ioptr->ReadPtr+ioptr->ReadCount, DataSize,
                        0, 0, 0, 0);

      if (VMSnok (status)) ioptr->ReadStatus = status;
   }
   else
   {
      /***************/
      /* blocking IO */
      /***************/

      for (;;)
      {
         DataSize = ioptr->ReadSize & ~NETIO_DATA_FILL_BUF;
         DataSize -= ioptr->ReadCount;
         if (DataSize > ioptr->TcpMaxQio) DataSize = ioptr->TcpMaxQio;

         status = sys$qiow (EfnWait, ioptr->Channel,
                            IO$_READVBLK, &ioptr->ReadIOsb, 0, 0,
                            (uchar*)ioptr->ReadPtr+ioptr->ReadCount, DataSize,
                            0, 0, 0, 0);

         if (VMSnok (status))
            ioptr->ReadStatus = status;
         else
            NetIoReadAst (ioptr);

         if (VMSnok (ioptr->ReadStatus)) break;
      }
   }

   /****************/
   /* check status */
   /****************/

   /* if I/O successful */
   if (VMSok (status)) return (status);

   /* with resource wait enabled the only quota not waited for is ASTLM */
   if (status == SS$_EXQUOTA)
   {
      /* no ASTs means not much of anything else can happen so just exit! */
      sys$canexh(0);
      /* make the message a little more meaningful */
      sys$exit (SS$_EXASTLM);
   }

   /* write failed, call AST explicitly, status in the IOsb */
   ioptr->ReadIOsb.Status = status;
   ioptr->ReadIOsb.Count = 0;
   if (ioptr->ReadAstFunction)
      SysDclAst (NetIoReadAst, ioptr);
   else
      NetIoReadAst (ioptr);

   return (status);
}

/*****************************************************************************/
/*
AST from NetIoRead().  Call the AST function.
*/ 

void NetIoReadAst (NETIO_STRUCT *ioptr)

{
   void  *AstParam;
   VOID_AST  AstFunction;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET,
                 "NetIoReadAst() !&F !&X !&X !&S !UL !UL !&A",
                 NetIoReadAst, ioptr, ioptr->VmsStatus,
                 ioptr->ReadIOsb.Status, ioptr->ReadIOsb.Count,
                 ioptr->ReadCount, ioptr->ReadAstFunction);

   if (VMSok (ioptr->ReadIOsb.Status))
   {
      /* zero bytes with a normal status (once seen with TGV-Multinet) */
//      if (!ioptr->ReadIOsb.Count) ioptr->ReadIOsb.Status = SS$_ABORT;
   }

   if (ioptr->VmsStatus)
   {
      /* explicitly set status */
      ioptr->ReadIOsb.Status = ioptr->VmsStatus;
      ioptr->ReadIOsb.Count = 0;
   }

   ioptr->ReadStatus = ioptr->ReadIOsb.Status;

   if (ioptr->ReadIOsb.Status == SS$_NORMAL &&
       !(ioptr->ReadCount || ioptr->ReadSize))
   {
      /* special case, do not tally or report via WATCH */
      if (AstFunction = ioptr->ReadAstFunction)
      {
         AstParam = ioptr->ReadAstParam;
         ioptr->ReadAstFunction = ioptr->ReadAstParam = NULL;
         AstFunction (AstParam);
      }
      return;
   }

   if (WATCHPNT(ioptr))
   {
      if (WATCH_CATEGORY(WATCH_NETWORK) ||
          WATCH_CATEGORY(WATCH_NETWORK_OCTETS))
      {
         WatchThis (WATCHITM(ioptr), WATCH_NETWORK,
"!AZ !&S !UL (!UL/!UL) bytes (!&?non-blocking\rblocking\r)",
                     ioptr->ReadSize & NETIO_DATA_FILL_BUF ? "FILL" : "READ",
                     ioptr->ReadIOsb.Status,
                     ioptr->ReadIOsb.Count,
                     ioptr->ReadCount + ioptr->ReadIOsb.Count,
                     ioptr->ReadSize & ~NETIO_DATA_FILL_BUF,
                     ioptr->ReadAstFunction);
         if (WATCH_CATEGORY(WATCH_NETWORK_OCTETS))
            if (VMSok(ioptr->ReadIOsb.Status))
               WatchDataDump (ioptr->ReadPtr + ioptr->ReadCount,
                              ioptr->ReadIOsb.Count);
      }
   }

   if (VMSok (ioptr->ReadIOsb.Status))
   {
      ioptr->BlocksRawRx64++;
      ioptr->BlocksTallyRx64++;
      ioptr->BytesRawRx64 += ioptr->ReadIOsb.Count;
      ioptr->BytesTallyRx64 += ioptr->ReadIOsb.Count;
      ioptr->ReadCount += ioptr->ReadIOsb.Count;
      if (ioptr->ReadAstFunction)
      {
         if (ioptr->ReadSize & NETIO_DATA_FILL_BUF)
         {
            if (ioptr->ReadCount < (ioptr->ReadSize & ~NETIO_DATA_FILL_BUF))
            {
               /* read more to fill buffer */
               NetIoRead (ioptr, NULL, NULL, NULL, 0);
               return;
            }
         }
      }

      if (Watch.TriggerRxCount)
      {
         if (!ioptr->WatchTriggerRxPlus)
         {
            if (WatchTrigger (ioptr->ReadPtr, ioptr->ReadCount,
                              Watch.TriggerRx))
            {
               ioptr->WatchTriggerRx = true;
               if (Watch.TriggerPlus) ioptr->WatchTriggerRxPlus = true;
            }
         }
         if (ioptr->WatchTriggerRx || ioptr->WatchTriggerRxPlus)
         {
            WatchSetTrigger (ioptr);
            ioptr->WatchTriggerRx = false;
         }
      }
   }
   else
   {
      ioptr->ReadErrorCount++;
      /* just note the first error status */
      if (!ioptr->ReadErrorStatus)
         ioptr->ReadErrorStatus = ioptr->ReadIOsb.Status;
      if (!(ioptr->ReadIOsb.Status == SS$_ABORT ||
            ioptr->ReadIOsb.Status == SS$_CANCEL ||
            ioptr->ReadIOsb.Status == SS$_CONNECFAIL ||
            ioptr->ReadIOsb.Status == SS$_IVCHAN ||
            ioptr->ReadIOsb.Status == SS$_LINKDISCON ||
            ioptr->ReadIOsb.Status == SS$_TIMEOUT ||
            ioptr->ReadIOsb.Status == SS$_UNREACHABLE ||
            ioptr->ReadIOsb.Status == SS$_VCCLOSED))
      {
         char  *sptr = "(none)";     
         REQUEST_STRUCT *rqptr;
         if (rqptr = ioptr->RequestPtr)
            if (rqptr->rqHeader.RequestUriPtr)
               sptr = rqptr->rqHeader.RequestUriPtr;
         ErrorNoticed (NULL, ioptr->ReadIOsb.Status, "!AZ", FI_LI, sptr);
      }
   }

   if (AstFunction = ioptr->ReadAstFunction)
   {
      AstParam = ioptr->ReadAstParam;
      ioptr->ReadAstFunction = ioptr->ReadAstParam = NULL;
      AstFunction (AstParam);
   }
}

/*****************************************************************************/
/*
Deliver the supplied VMS status code via AST while maintaining the network
write in-progress conditions.  As with other I/O the delivery needs to be
decoupled in case it gets called again during that delivery.  Primarily used to
indicate SS$_CANCEL in HTTP/2 processing.
*/ 

void NetIoReadStatus
(
NETIO_STRUCT *ioptr,
VOID_AST AstFunction,
void *AstParam,
int AstStatus,
uint ReadCount
)
{
   int  status;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET,
                 "NetIoReadStatus() !&X !UL !&A !&S",
                 ioptr, ReadCount, AstFunction, AstStatus);

   if (ioptr->ReadAstFunction)
   {
      char  buf [256];
      FaoToBuffer (buf, sizeof(buf), NULL, "!&A", ioptr->ReadAstFunction);
      ErrorExitVmsStatus (SS$_BUGCHECK, buf, FI_LI);
   }
   ioptr->ReadAstFunction = AstFunction;
   ioptr->ReadAstParam = AstParam;
   if (!ReadCount)
   {
      ioptr->ReadPtr = NULL;
      ioptr->ReadSize = 0;
   }
   ioptr->ReadCount = 0;
   ioptr->ReadIOsb.Count = ReadCount;
   ioptr->ReadIOsb.Status = AstStatus;

   if (AstFunction)
      SysDclAst (NetIoReadAst, ioptr);
   else
      NetIoReadAst (ioptr);
}

/****************************************************************************/
/*
Any $QIO I/O currently outstanding?
*/

BOOL NetIoInProgress (NETIO_STRUCT *ioptr)

{
   int  channel, status;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET, "NetIoInProgress()");

   if (ioptr->Stream2Ptr)
      return (Http2NetIoInProgress (ioptr->Stream2Ptr));

   if (ioptr->SesolaPtr)
      return (SesolaNetIoInProgress (ioptr->SesolaPtr));

   return (ioptr->WriteAstFunction || ioptr->ReadAstFunction);
}

/****************************************************************************/
/*
Cancel network I/O in-progress.
*/

void NetIoCancel (NETIO_STRUCT *ioptr)

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET, "NetIoCancel()");

   ioptr->VmsStatus = SS$_CANCEL;

   if (ioptr->Stream2Ptr)
      Http2NetIoCancel (ioptr);
   else
   if (ioptr->SesolaPtr)
      SesolaNetIoCancel (ioptr);
   else
   if (ioptr->WriteAstFunction || ioptr->ReadAstFunction)
      sys$cancel (ioptr->Channel);
}

/****************************************************************************/
/*
Just close the socket (channel), bang!
*/

int NetIoCloseSocket (NETIO_STRUCT *ioptr)

{
   int  channel, status;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET, "NetIoCloseSocket()");

   if (ioptr->Stream2Ptr) return (SS$_NORMAL);

   if (!ioptr->Channel) return (SS$_NORMAL);

   status = sys$dassgn (channel = ioptr->Channel);
   ioptr->Channel = 0;

   if (WATCH_CATEGORY(WATCH_CONNECT))
      WatchThis (WATCHITM(ioptr), WATCH_CONNECT, "CLOSE channel !UL !&S",
                 channel, status);

   return (status);
}

/****************************************************************************/
/*
Just close the socket (channel), bang!
*/

int NetIoDassgnChannel (NETIO_STRUCT *ioptr)

{
   int  channel, status;

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

   if (WATCHMOD (ioptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(ioptr), WATCH_MOD_NET, "NetIoDassgnChannel()");

   status = sys$dassgn (channel = ioptr->Channel);
   ioptr->Channel = 0;

   return (status);
}

/*****************************************************************************/
/*
Allows *testing* of NetIoWrite() for small, medium, large and huge buffers.
A request to UTI "/$/NetIoWriteTest/?<integer>" will invoke this function. 
Writes responses of ASCII in quantities from 1 byte to whatever.  Non-blocking
by default, negative quantity to make blocking.
*/ 

#if WATCH_MOD

void NetIoWriteTest (REQUEST_STRUCT *rqptr)

{
   int  ch, nonblock, size;
   char  *cptr, *sptr, *zptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_NET, "NetIoWriteTest()");

   if (rqptr->NotePadPtr != NetIoWriteTest)
   {
      rqptr->NotePadPtr = NetIoWriteTest;
      if (!(cptr = rqptr->rqHeader.QueryStringPtr)) cptr = "32767";
      size = atoi(cptr);
      if (size >= 0) nonblock = 1; else nonblock = 0;
      size = abs(size);

      cptr = VmGetHeap (rqptr, size+32);
      zptr = (sptr = cptr) + size;
      ch = '!';
      while (sptr < zptr)
      {
         while (ch <= '~' && sptr < zptr) *sptr++ = ch++;
         if (sptr < zptr) *sptr++ = '\n';
         ch = '!';
      }

      rqptr->rqResponse.NoGzip = true;
      ResponseHeader (rqptr, 200, "text/plain", size, NULL, NULL);
      if (nonblock)
      {
         NetWrite (rqptr, NetIoWriteTest, cptr, size);
         return;
      }
      NetWrite (rqptr, NULL, cptr, size);
   }

   RequestEnd2 (rqptr);
}

#endif /* WATCH_MOD */

/*****************************************************************************/
/*
Allows *testing* of NetIoRead() for small, medium, large and huge buffers.
Using cURL and a POSTed file to buffer.  Non-blocking by default, negative
quantity <integer> to make blocking.

  $ curl --insecure "-XPOST" --data-binary @<file_name> -
  "http[s]://<host_name>/$/NetIoReadTest/?<integer>"
*/ 

#if WATCH_MOD

void NetIoReadTest (REQUEST_STRUCT *rqptr)

{
   int  nonblock = 1;
   int64  size;
   ushort  slen;
   char  *cptr;
   char  buf [256];

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

   if (WATCHMOD (rqptr, WATCH_MOD_NET))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_NET, "NetIoReadTest()");

   if (rqptr->NotePadPtr != NetIoWriteTest)
   {
      rqptr->NotePadPtr = NetIoWriteTest;
      if (atoi(rqptr->rqHeader.QueryStringPtr) < 0) nonblock = 0;
      size = rqptr->rqHeader.ContentLength64;
      cptr = VmGetHeap (rqptr, size+32);
      size |= NETIO_DATA_FILL_BUF;
      if (nonblock)
      {
         NetRead (rqptr, &NetIoReadTest, cptr, size);
         return;
      }
      NetRead (rqptr, NULL, cptr, size);
   }

   rqptr->NetIoPtr->BytesRawRx64 += rqptr->NetIoPtr->ReadCount;
   rqptr->NetIoPtr->BytesTallyRx64 += rqptr->NetIoPtr->ReadCount;
   FaoToBuffer (buf, sizeof(buf), &slen, "status:!&S count:!UL\n",
                rqptr->NetIoPtr->ReadStatus,
                rqptr->NetIoPtr->ReadCount);
   rqptr->rqResponse.NoGzip = true;
   ResponseHeader (rqptr, 200, "text/plain", slen, NULL, NULL);
   NetWrite (rqptr, NULL, buf, slen);

   RequestEnd2 (rqptr);
}

#endif /* WATCH_MOD */

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