!tcpx.bsi [7] - TCPX-related functions 
!-----------------------------------------------------------------------
!EDIT HISTORY
! [7] 04-Aug-25 / jdm / Fn'TCPX'Read() wasn't respecting bytes arg 
! [6] 11-Jul-25 / jdm / Minor commenting/modernization; update traces;
!                           fix problem with read buf return 
! [5] 28-Feb-22 / jdm / Fix Fn'TCPX'Check() and Fn'TCPX'ChkQty()
! [4] 10-May-20 / jdm / Support UDP options 
! [3] 03-Sep-17 / jdm / fix missing xputarg @socket in Connect
! [2] 30-Aug-17 / jdm / add Fn'TCPX'ChkQty, Fn'TCPX'Accept(), 
!                           support var size packet, 
!                           misc other improvements
! [1] 18-Apr-17 / jdm / created
!-----------------------------------------------------------------------
!DESCRIPTION:
!   Collection of wrapper functions for the most common TCPX.SBR routines.
!   Improvements over just using XCALL TCPX include:
!   
!   - requires only the relevant parameters, with intelligent defaulting
!   
!   - named parameters (eliminate need to remember param order)
!
!   - integrated debugging via DEBUG.PRINT statements 
!
!   - pseudo-class design allows maintaining certain private variables
!     between calls, allowing intelligent defaulting of unspecified
!     parameters (like socket) and also ability to save and later return
!     last error information.
!
!   - helper logic to deal with complications such as pre-allocating
!     a dynamic buffer for the packet.
!
!   - separation of port, socket, and listensock in the Accept routine
!
!   - eliminates possibility of forgetting to clear buffer on accept and connect
!
!
!REQUIREMENTS
! - Requires /p (or /px) for named parameter support.
! - 6.5.1681.0+ for UDP options [4]
!
!NOTES
! There is only one data socket stored in the private variables,
! so if a single caller uses multiple sockets, it must specify the socket
! on each call (unless same as for prior call).
!-----------------------------------------------------------------------
!Functions:
!   Fn'TCPX'Connect(host,port,flags,timer,sockport)
!   Fn'TCPX'Close(socket)
!   Fn'TCPX'Read(buf,bytes,timer,socket)
!   Fn'TCPX'Write(buf,bytes,timer,socket)
!   Fn'TCPX'Check(timer,socket)
!   Fn'TCPX'ChkQty(timer,socket)      [2]
!   Fn'TCPX'Accept(port,flags,timer)  [2]
!   Fn'TCPX'ErrMsg$(status)    
!-----------------------------------------------------------------------

++ifndef INC_TCPX_BSI     ! skip if already included
define INC_TCPX_BSI = 1

++include'once ashinc:ashell.def
++include'once ashinc:types.def

![2] define TCPX_MAX_BUFSIZ = 4096

++pragma private_begin
    map1 m'socket,i,4
    map1 m'listensocket,i,4           ! [2] listening socket
    map1 m'lasterr,i,4
    map1 m'lasterrmsg$,s,100
++pragma private_end

![2] define convenient symbols for the "would block" pseudo-error
++ifndef EWOULDBLOCK
    define EWOULDBLOCK = -11        ! Linux/AIX TCPX code
++endif
++ifndef WSAEWOULDBLOCK
    define WSAEWOULDBLOCK = -10035  ! windows TCPX code
++endif


!---------------------------------------------------------------------
!Function:
!   Accept connection
!Params:
!   port  (num) [in] - port to accept connection on
!   flags (num) [in] - flags
!                       TCPXFLG_LISTEN - opens listening socket, returns immediately
!                       TCPXFLG_KEEPLISTEN - keep listening socket open
!                       TCPXFLG_ASYNC - async mode (don't wait)
!                       TCPXFLG_UDP - connectionless (UDP) [4]
!   timer (num) [in] - timeout (ms)
!   socket (num) [out] - either listening socket or data socket, depending on flags
!   listensocket (num) [in/out] - listening socket returned here when TCPXFLG_LISTEN;
!                       caller supplies listening socket here when TCPXFLG_ASYNC
!   buffer
!Returns:
!   status >= 0 for success (> 0 indicates # bytes in buffer)
!Module Vars:
!   sets m'socket based on socket
!   sets m'listensock based on listensock (if applic)
!Notes:
!---------------------------------------------------------------------
Function Fn'TCPX'Accept(port=0 as b2:inputonly, flags=0 as b4:inputonly, &
                        timer=0 as b4:inputonly, socket=0 as i4:outputonly, &
                        listensocket=0 as i4) as i4

    map1 locals
        map2 sockport,b,4
        
    if flags and TCPXFLG_ASYNC then
        sockport = listensocket
    else
        sockport = port
    endif
    
    trace.print (99,"tcpx") "$T ",ABC_CURRENT_ROUTINE$,port,sockport,flags,timer
    xcall TCPX, TCPOP_ACCEPT, .fn, "", sockport, flags, timer
    
    trace.print (99,"tcpx") "$T ",.fn

    if .fn < 0 then              ! on error
        m'lasterr = .fn
        if flags and TCPXFLG_LISTEN then
            m'listensocket = 0
        else
            m'socket = 0
        endif
    else                                    ! on success
        m'lasterr = 0
        if flags and TCPXFLG_LISTEN then    ! if we opened a listensock, return it
            listensocket = sockport
            m'listensocket = sockport
            trace.print (99,"tcpx") listensocket
            xputarg @listensocket
        else
            socket = sockport
            m'socket = sockport
            xputarg @socket                 ! else return the data socket
        endif
    endif

EndFunction

!---------------------------------------------------------------------
!Function:
!   Attempt synchronous connection to server
!Params:
!   host$ (str) [in] - host/ip (may contain :port)
!   port (num) [in] - port (if not overridden by host$:port)
!   flags (num) [in] 
!   timer (num) [in] - timeout (ms)
!   socket (num) [out] - connected socket
!Returns:
!   status
!Module Vars:
!   socket
!Notes:
!   we ignore any buffer returned
!---------------------------------------------------------------------
Function Fn'TCPX'Connect(host$ as s64:inputonly, port as b2:inputonly, &
                         flags=0 as b4:inputonly,  timer=0 as b4:inputonly, &
                         socket as i4:outputonly) as i4

    map1 locals
        map2 x,i,2
        
    x = instr(1,host$,":")      ! see if there is a :portno suffix
    if x > 0 then               ! [105]
       port = host$[x+1,-1]
       host$ = host$[1,x-1]
    endif

    m'socket = port
    
    xcall TCPX, TCPOP_CONNECT, .fn, "", m'socket, flags, timer, host$   
    
    trace.print (99,"tcpx") "$T ",ABC_CURRENT_ROUTINE$, host$, port, m'socket,flags,timer,.fn

    if .fn < 0 then
        m'lasterr = .fn
        m'socket = 0
    else
        m'lasterr = 0
    endif

    socket = m'socket   ! [3]
    xputarg @socket     ! [3]
EndFunction

!---------------------------------------------------------------------
!Function:
!   Close connection
!Params:
!   socket  (num) [in/out] - optional socket to close
!                            (return 0 to clear value in caller) [2]
!Returns:
!Module Vars:
!   retrieves socket from m'socket if not passed; sets m'socket to 0
!Notes:
!---------------------------------------------------------------------
Function Fn'TCPX'Close(socket as i4) as i4

    if socket = 0 then
        socket = m'socket
    endif
    
    if m'socket then
        xcall TCPX, TCPOP_CLOSE, .fn, "", socket, 0

        trace.print (99,"tcpx") "$T ", ABC_CURRENT_ROUTINE$, socket, m'socket, .fn

        if socket = m'socket then   ! if closed socket matches global one
            m'socket = 0            ! then reset the global socket var
        endif
    
        if .fn < 0 then
            m'lasterr = .fn
        else
            m'lasterr = 0
        endif
    else
        m'lasterr = 0
    endif
    
    socket = 0          ! [2]
    xputarg @socket     ! [2] reset passed socket 
    
EndFunction

!---------------------------------------------------------------------
!Function:
!   Read from the connection
!Params:
!   buffer (x0) [out] - any size buffer
!   bytes (num) [in] - # bytes to read (if 0, uses size of buffer)
!   timer (num) [in] - ms to wait
!   socket (num) [in] - optional socket (else m'socket)
!   udp (BOOLEAN) [in] - set to .TRUE for TCPOP_READ_UDP [4]
!Returns:
!   >=0 : # bytes read
!   <0  : error code
!Globals:
!Notes:
!   If non-blocking connection and timer=0 may return EWOULDBLOCK or
!   WSAEWOULDBLOCK (in which case caller will need to loop). Even in
!   blocking connection, may return with fewer than the requested 
!   bytes (again, caller will need to loop).
!
!   If the caller specifies 0 bytes, we'll try to loop to get all that
!   is available.
!---------------------------------------------------------------------
Function Fn'TCPX'Read(buf as x0:outputonly, &
                      bytes=0 as b4:inputonly, &
                      timer=0 as b4:inputonly, &
                      socket as i4:inputonly, &
                      udp=.FALSE as BOOLEAN:inputonly) as i4

    map1 locals
        map2 tbuf,x,8192
        map2 bytes'rcvd,b,4
        map2 status,f
        map2 tcpop,b,2      ! [4]
        map2 pos,i,4,1      ! [6]
        map2 maxbytes,b,4   ! [7]
        
    if socket = 0 then
        socket = m'socket
    endif

    if udp then                     ! [4]
        tcpop = TCPOP_READ_UDP
    else
        tcpop = TCPOP_READ
    endif

    trace.print (99,"tcpx") "$T ",ABC_CURRENT_ROUTINE$, bytes, timer
    
    ! if requested bytes = 0 (i.e. caller has no idea), then
    ! we need to use some reasonable temp buffer size (8192)
    ! and if we receive that many bytes, loop until we get less.
    maxbytes = ifelse(bytes#0,bytes,sizeof(tbuf))   ! [7]
    m'lasterr = 0    
    do

        trace.print (99,"tcpx") "$T ",ABC_CURRENT_ROUTINE$, bytes, maxbytes
        
        xcall TCPX, tcpop, status, tbuf, socket, bytes, timer
    
        trace.print (99,"tcpx") "$T tbuf=["+asc(tbuf)+"]", status, bytes
    
        if status < 0 then                  ! on error...
            if bytes'rcvd = 0 then          ! if error on first read, that's an error
                m'lasterr = status
            endif
            exit
        elseif status > 0 then              ! successful read
            bytes'rcvd += status            ! count the bytes read
            ![6] buf += tbuf[1;status]           ! and add them into our return buffer
            buf[pos;status] = tbuf[1;status]
            pos += status
        elseif timer # 0 then               ! [7] quit on timeout
            exit
        endif                               ! 
    loop until status >= maxbytes           ! [7] sizeof(tbuf)    ! keep going until satisfied or nothing left 
    
    xputarg @buf
    
    if bytes'rcvd then
        .fn = bytes'rcvd
    else
        .fn = status
    endif
    
    trace.print (99,"tcpx") "$T ", bytes'rcvd, buf
EndFunction

!---------------------------------------------------------------------
!Function:
!   Write to the connection
!Params:
!   buffer (x0) [out] - buffer to write
!   bytes (num) [in] - # bytes to write (if 0, uses len(buffer))
!   timer (num) [in] - ms to wait
!   socket (num) [in] - optional socket (else m'socket)
!Returns:
!   status
!Module vars:
!   sets m'lasterr
!Notes:
!---------------------------------------------------------------------
Function Fn'TCPX'Write(buf as x0:inputonly, &
                       bytes=0 as b4:inputonly, timer=0 as b4:inputonly, &
                       socket=0 as i4:inputonly) as i4

    if socket = 0 then
        socket = m'socket
    endif

    if bytes = 0 then
        bytes = len(buf)
    endif
    xcall TCPX, TCPOP_WRITE, .fn, buf, socket, bytes, timer

    trace.print (99,"tcpx") "$T ",ABC_CURRENT_ROUTINE$, "buf=["+asc(buf)+"]", bytes, .fn
    
    if .fn < 0 then
        m'lasterr = .fn
    else
        m'lasterr = 0
    endif
    
EndFunction

!---------------------------------------------------------------------
!Function:
!   Check if data avail to read
!Params:
!   timer (num) [in] - ms to wait
!   socket (num) [in] - optional socket (else m'socket)
!   flags (num) [in]  - optional flags (may be needed for checking on
!                           async connections)
!Returns:
!   status : 
!       < 0 : error (socket closed)
!       0   : no data available to read
!       > 0 : data available
!Module vars:
!   sets m'lasterr
!Notes:
!   [5] note that the buffer size needs to be at least 1 for this
!   to work (but passing a literal space, e.g. " " is acceptable; 
!   a literal null, e.g. "", is not)
!---------------------------------------------------------------------
Function Fn'TCPX'Check(timer=0 as b4:inputonly, &
                       socket=m'socket as i4:inputonly, &
                       flags=0 as b4:inputonly) as i4

    xcall TCPX, TCPOP_CHECK, .fn, " ", socket, flags, timer  ! [5]
    m'lasterr = ifelse(.fn < 0, .fn, 0)    
EndFunction


![2]
!---------------------------------------------------------------------
!Function:
!   Variation of Fn'TCPX'Check that returns # bytes available to read
!Params:
!   timer (num) [in]
!   socket (num) [in] - optional socket (else m'socket)
!Returns:
!   >= 0 : # bytes avail to read
!   < 0  : error
!Module vars:
!   sets m'lasterr
!Notes:
!   [5] Note that although the buffer doesn't receive any data, its
!   size sets a maximum on the return status. So with a size of 128,
!   a return value of 128 indicates AT LEAST 128 bytes available.
!---------------------------------------------------------------------
Function Fn'TCPX'ChkQty(timer=0 as b4:inputonly, &
                        socket=m'socket as i4:inputonly) as i4      ! [5]
    map1 locals
        map2 buffer,x,128       ! [5] 

    xcall TCPX, TCPOP_CHKQTY, .fn, buffer, socket, 0, timer 
    m'lasterr = ifelse(.fn < 0, .fn, 0)
EndFunction

!---------------------------------------------------------------------
!Function:
!   Return error message for last op
!Params:
!   status (num) [in] - optional status code (else use internal one)
!Returns:
!   status
!Globals:
!Notes:
!---------------------------------------------------------------------
Function Fn'TCPX'ErrMsg$(status as i4:inputonly) as s100

    if .argcnt < 1 then
        status = m'lasterr
    endif

    xcall TCPX, TCPOP_ERRMSG, abs(status), m'lasterrmsg$, m'socket, 0
    
    .fn = m'lasterrmsg$
EndFunction



++endif
