I have been testing the amazing new GROK-Code and especially QWEN 3 MAX (https://chat.qwen.ai) today and it beats Googles Gemini using Powerbasic, and makes impressive results.
In Times of AI an Winsock Library is most important. SO let me share this with you.
The only thing you might need to change to have it compile are the Calls to MCP_Log().
And YES Powerbasic has own TCP-Commands,they go a wide way and then fail.
So this is the Upgrade.
#INCLUDE THIS ONCE
' NW_Library.inc
' ########################################################################
' Advanced TCP Library for PowerBASIC (Definitive Final Version - Part 1)
' - Prefix: NW_ for public API (COMMON)
' - Prefix: INT_ for internal helpers
' - USES OFFICIAL WINSOCK HEADERS
' ########################################################################
#INCLUDE ONCE "Ws2def.inc"
#INCLUDE ONCE "WinSock2.inc"
#INCLUDE ONCE "WS2tcpip.inc"
#INCLUDE ONCE "ws2ipdef.inc"
'
' Functions that are marked as * * * SEALED * * * MAY NOT BE CHANGED.
' They are tested and correct.
'===============================================================================
' Part 0: Constants & Globals
'===============================================================================
%NW_SOCKET_ERROR = -1
%NW_WOULDBLOCK = -2
%NW_MAX_CLIENTS = 20
%NW_TIMEOUT_DEFAULT = 5000 'ms
%CR=13
%LF=10
GLOBAL g_NW_InitCounter AS LONG
GLOBAL g_NW_Cs AS CRITICAL_SECTION
GLOBAL g_IsShutdown AS LONG
GLOBAL g_MCP_LogLevel AS LONG
GLOBAL g_NW_DebugMode AS LONG
GLOBAL g_NW_HexDumpEnabled AS LONG
GLOBAL g_NW_ActiveThreadCount AS LONG ' Tracks threads actively using the library
GLOBAL g_NW_ShutdownEvent AS DWORD ' Event handle to signal shutdown completion
THREADED s_Buffer AS STRING ' Persistent, THREAD-GLOBAL buffer
THREADED s_Socket AS LONG ' Socket associated with the THREAD-GLOBAL buffer
THREADED g_NW_LastThreadError AS LONG
' Set to 1 if Init was called
GLOBAL g_NW_Initialized AS BYTE
DECLARE SUB MoveMemory LIB "kernel32.dll" ALIAS "RtlMoveMemory" (BYVAL pDst AS DWORD, BYVAL pSrc AS DWORD, BYVAL cb AS LONG)
DECLARE FUNCTION WS_StringToAddressA LIB "Ws2_32.dll" ALIAS "WSAStringToAddressA" (BYVAL AddressString AS DWORD, BYVAL AddressFamily AS LONG, BYVAL lpProtocolInfo AS WSAPROTOCOL_INFOA PTR, BYVAL lpAddress AS DWORD, BYREF lpAddressLength AS LONG) AS LONG
DECLARE SUB WS_freeaddrinfo LIB "Ws2_32.dll" ALIAS "freeaddrinfo" (BYVAL pAddrInfo AS DWORD)
DECLARE FUNCTION WS_ntohs LIB "Ws2_32.dll" ALIAS "ntohs" (BYVAL netshort AS WORD) AS WORD
DECLARE FUNCTION WS_WSAStartup LIB "Ws2_32.dll" ALIAS "WSAStartup" (BYVAL wVersionRequested AS WORD, BYREF lpWSAData AS WSADATA) AS LONG
DECLARE FUNCTION WS_WSACleanup LIB "Ws2_32.dll" ALIAS "WSACleanup" () AS LONG
DECLARE FUNCTION WS_socket LIB "Ws2_32.dll" ALIAS "socket" (BYVAL af AS LONG, BYVAL TPE AS LONG, BYVAL protocol AS LONG) AS DWORD
DECLARE FUNCTION WS_listen LIB "Ws2_32.dll" ALIAS "listen" (BYVAL s AS DWORD, BYVAL backlog AS LONG) AS LONG
DECLARE FUNCTION WS_closesocket LIB "Ws2_32.dll" ALIAS "closesocket" (BYVAL s AS DWORD) AS LONG
DECLARE FUNCTION WS_recvfrom LIB "Ws2_32.dll" ALIAS "recvfrom" (BYVAL s AS DWORD, BYVAL buf AS DWORD, BYVAL LE AS LONG, BYVAL flags AS LONG, BYVAL FR AS DWORD, BYVAL fromlen AS LONG) AS LONG
DECLARE FUNCTION WS_sendto LIB "Ws2_32.dll" ALIAS "sendto" (BYVAL s AS DWORD, BYVAL buf AS DWORD, BYVAL LE AS LONG, BYVAL flags AS LONG, BYVAL to_addr AS DWORD, BYVAL tolen AS LONG) AS LONG
DECLARE FUNCTION WS_WSASelect LIB "Ws2_32.dll" ALIAS "select" (BYVAL nfds AS LONG, BYREF readfds AS fd_setstruc, BYREF writefds AS fd_setstruc, BYREF exceptfds AS fd_setstruc, BYREF TIMEOUT AS timeval) AS LONG
DECLARE FUNCTION WS_Recv LIB "Ws2_32.dll" ALIAS "recv" (BYVAL s AS DWORD, BYVAL buf AS DWORD, BYVAL nLen AS LONG, BYVAL flags AS LONG) AS LONG
DECLARE FUNCTION WS_Send LIB "Ws2_32.dll" ALIAS "send" (BYVAL s AS DWORD, BYVAL buf AS DWORD, BYVAL nLen AS LONG, BYVAL flags AS LONG) AS LONG
DECLARE FUNCTION WS_inet_ntoa LIB "Ws2_32.dll" ALIAS "inet_ntoa" (BYVAL IN AS DWORD) AS DWORD
DECLARE FUNCTION WS_AddrToStringA LIB "Ws2_32.dll" ALIAS "WSAAddressToStringA" (BYVAL lpsaAddress AS DWORD, BYVAL dwAddressLength AS DWORD, BYVAL lpProtocolInfo AS WSAPROTOCOL_INFOA PTR,_
BYREF lpszAddressString AS ASCIIZ, BYREF lpdwAddressStringLength AS DWORD) AS LONG
DECLARE FUNCTION WS_getnameinfo LIB "Ws2_32.dll" ALIAS "getnameinfo" (BYVAL sa AS DWORD, BYVAL salen AS LONG, BYREF HOS AS ASCIIZ, BYVAL hostlen AS DWORD, BYVAL serv AS DWORD, BYVAL servlen AS DWORD, BYVAL flags AS LONG) AS LONG
DECLARE FUNCTION WS_accept LIB "Ws2_32.dll" ALIAS "accept" (BYVAL s AS DWORD, BYVAL ADDR AS DWORD, BYREF addrlen AS LONG) AS DWORD
DECLARE FUNCTION WS_getpeername LIB "Ws2_32.dll" ALIAS "getpeername" (BYVAL s AS DWORD, BYVAL NAME AS DWORD, BYREF namelen AS LONG) AS LONG
DECLARE FUNCTION WS_getsockname LIB "Ws2_32.dll" ALIAS "getsockname" (BYVAL s AS DWORD, BYVAL NAME AS DWORD, BYREF namelen AS LONG) AS LONG
DECLARE FUNCTION WS_bind LIB "Ws2_32.dll" ALIAS "bind" (BYVAL s AS DWORD, BYVAL ADDR AS DWORD, BYVAL namelen AS LONG) AS LONG ' namelen by value
DECLARE FUNCTION WS_connect LIB "Ws2_32.dll" ALIAS "connect" (BYVAL s AS DWORD, BYVAL NAME AS DWORD, BYVAL namelen AS LONG) AS LONG
DECLARE FUNCTION WS_getaddrinfo LIB "Ws2_32.dll" ALIAS "getaddrinfo" (BYVAL pNodeName AS DWORD, BYVAL pServiceName AS DWORD, BYVAL pHints AS DWORD, BYREF ppResult AS DWORD) AS LONG
DECLARE FUNCTION WSARecv LIB "Ws2_32.dll" ALIAS "WSARecv" (BYVAL s AS DWORD, BYREF lpBuffers AS WSABUF, BYVAL dwBufferCount AS DWORD, BYREF lpNumberOfBytesRecvd AS DWORD, BYREF lpFlags AS DWORD,_
BYREF lpOverlapped AS WSAOVERLAPPED, BYVAL lpCompletionRoutine AS DWORD) AS LONG
'===============================================================================
' Part 1: Initialization and Cleanup (Thread-Safe)
'===============================================================================
' PURPOSE: Initializes the Winsock library and all necessary resources for
' multi-threaded use. Should be called once before any other
' library functions are used.
' UNICODE: Not applicable.
' PARAMS: None.
' RETURNS: %TRUE on success, %FALSE on failure.
FUNCTION NW_Initialize() THREADSAFE COMMON AS LONG
LOCAL E01 AS WSADATA
LOCAL N01 AS WORD
LOCAL N02 AS LONG
LOCAL E03 AS LONG
IF g_NW_Initialized = 1 THEN
N02 = %TRUE
GOTO enx
END IF
MCP_Init()
' --- ONE-TIME RESOURCE ALLOCATION ---
InitializeCriticalSection g_NW_Cs
InitializeCriticalSection g_mcp_log_queue_cs ' Ensure this is initialized too
' Create shutdown event (manual reset, initially non-signaled)
g_NW_ShutdownEvent = CreateEvent(BYVAL %NULL, %TRUE, %FALSE, BYVAL %NULL)
IF g_NW_ShutdownEvent = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Initialize: Failed to create shutdown event, Error " & STR$(GetLastError()))
END IF
DeleteCriticalSection g_NW_Cs
DeleteCriticalSection g_mcp_log_queue_cs
N02 = %FALSE
GOTO enx
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Initialize: Starting Winsock initialization...")
END IF
EnterCriticalSection g_NW_Cs
IF g_NW_InitCounter > 0 THEN
INCR g_NW_InitCounter
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Initialize: Already initialized, counter incremented to " & STR$(g_NW_InitCounter))
END IF
N02 = %TRUE
ELSE
N01 = MAK(WORD, 2, 2)
E03 = WS_WSAStartup(N01, E01)
IF E03 = 0 THEN
IF LOBYTE(E01.wVersion) <> 2 OR HIBYTE(E01.wVersion) <> 2 THEN
WS_WSACleanup()
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Initialize: Version mismatch, got " & STR$(LOBYTE(E01.wVersion)) & "." & STR$(HIBYTE(E01.wVersion)) & ", expected 2.2")
END IF
N02 = %FALSE
ELSE
g_NW_InitCounter = 1
g_NW_ActiveThreadCount = 0 ' Initialize thread counter
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Initialize: Successfully initialized Winsock version " & STR$(LOBYTE(E01.wVersion)) & "." & STR$(HIBYTE(E01.wVersion)))
END IF
N02 = %TRUE
END IF
ELSE
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E03
CASE %WSASYSNOTREADY : errMsg = "Network subsystem not ready"
CASE %WSAVERNOTSUPPORTED : errMsg = "Requested version not supported"
CASE %WSAEINVAL : errMsg = "Invalid arguments"
CASE ELSE : errMsg = "WSAStartup failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_Initialize: " & errMsg & ", WSAError " & STR$(E03))
END IF
N02 = %FALSE
END IF
END IF
LeaveCriticalSection g_NW_Cs
IF N02 = %TRUE THEN
g_NW_Initialized = 1
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Initialize: Initialization complete.")
END IF
ELSE
' Cleanup on failure
IF g_NW_ShutdownEvent THEN
CloseHandle(g_NW_ShutdownEvent)
g_NW_ShutdownEvent = 0
END IF
DeleteCriticalSection g_NW_Cs
DeleteCriticalSection g_mcp_log_queue_cs
END IF
enx:
FUNCTION = N02
END FUNCTION
'===============================================================================
' PURPOSE: Cleans up and terminates the Winsock library usage. Should be called
' once for every successful call to NW_Initialize.
' UNICODE: Not applicable.
' PARAMS: None.
' RETURNS: None.
SUB NW_Shutdown() THREADSAFE COMMON
LOCAL N01 AS LONG
LOCAL E01 AS LONG
LOCAL waitResult AS LONG
EnterCriticalSection g_NW_Cs
IF g_NW_InitCounter > 0 THEN
DECR g_NW_InitCounter
IF g_NW_InitCounter = 0 THEN
' Wait for all active threads to finish (up to 30 seconds)
IF g_NW_ActiveThreadCount > 0 AND g_NW_ShutdownEvent THEN
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_INFO, "NW_Shutdown: Waiting for " & STR$(g_NW_ActiveThreadCount) & " active threads to finish...")
END IF
LeaveCriticalSection g_NW_Cs ' Release lock before waiting
waitResult = WaitForSingleObject(g_NW_ShutdownEvent, 30000) ' 30 second timeout
SELECT CASE waitResult
CASE %WAIT_OBJECT_0
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_INFO, "NW_Shutdown: All threads finished gracefully.")
END IF
CASE %WAIT_TIMEOUT
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Shutdown: Timeout waiting for threads to finish. Active threads: " & STR$(g_NW_ActiveThreadCount))
END IF
CASE ELSE
E01 = GetLastError()
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Shutdown: WaitForSingleObject failed, Error " & STR$(E01))
END IF
END SELECT
EnterCriticalSection g_NW_Cs ' Re-acquire lock for cleanup
END IF
' Perform Winsock cleanup
N01 = WS_WSACleanup()
IF N01 = 0 THEN
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Shutdown: Successfully cleaned up Winsock.")
END IF
ELSE
E01 = INT_GetAndStoreError()
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E01
CASE %WSANOTINITIALISED
errMsg = "Winsock not initialized or already cleaned up"
CASE %WSAENETDOWN
errMsg = "Network subsystem failed"
CASE ELSE
errMsg = "WS_WSACleanup failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_Shutdown: " & errMsg & ", WSAError " & STR$(E01))
END IF
END IF
' Cleanup resources
IF g_NW_ShutdownEvent THEN
CloseHandle(g_NW_ShutdownEvent)
g_NW_ShutdownEvent = 0
END IF
LeaveCriticalSection g_NW_Cs
DeleteCriticalSection g_NW_Cs
DeleteCriticalSection g_mcp_log_queue_cs
ELSE
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Shutdown: Counter decremented to " & STR$(g_NW_InitCounter))
END IF
LeaveCriticalSection g_NW_Cs
END IF
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Shutdown: Counter already zero, no cleanup performed.")
END IF
LeaveCriticalSection g_NW_Cs
END IF
END SUB
'===============================================================================
' PURPOSE: Gracefully shuts down a socket for send or receive.
' PARAMS: U01 (socket), how (%SD_SEND, %SD_RECEIVE, %SD_BOTH)
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
FUNCTION NW_ShutdownSocket(BYVAL U01 AS DWORD, BYVAL how AS LONG) COMMON AS LONG
LOCAL E01 AS LONG ' Error variable
IF shutdown(U01, how) = 0 THEN
FUNCTION = 0
ELSE
E01 = INT_GetAndStoreError()
FUNCTION = %NW_SOCKET_ERROR
' Suppress logging for listening sockets (WSAENOTCONN is common/expected)
IF E01 <> %WSAENOTCONN AND g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_ShutdownSocket: Failed on socket " & STR$(U01) & ", WSAError " & STR$(E01))
END IF
END IF
END FUNCTION
'===============================================================================
' Part 2: Socket Creation and Connection (High-Level)
'===============================================================================
' PURPOSE: Creates a listening socket bound to a specified IP and port.
' UNICODE: IP address is ASCII/ANSI; no Unicode issues.
' PARAMS: S01 (IP address or "" for any), T01 (port), T02 (backlog).
' RETURNS: Valid socket on success, %INVALID_SOCKET on failure.
' * * * SEALED * * *
FUNCTION NW_Listen(BYVAL S01 AS STRING, BYVAL T01 AS LONG, BYVAL T02 AS LONG) COMMON AS DWORD
LOCAL E01 AS ADDRINFOA
LOCAL E02 AS ADDRINFOA PTR
LOCAL E03 AS ADDRINFOA PTR
LOCAL E04 AS STRING
LOCAL E05 AS LONG
LOCAL E06 AS DWORD
LOCAL E07 AS LONG
LOCAL E08 AS LONG
LOCAL R01 AS DWORD
LOCAL tempAddrStr AS STRING
LOCAL tempSockAddr AS SOCKADDR_STORAGE
LOCAL addrCount AS LONG
LOCAL nodeName AS ASCIIZ * 256
LOCAL servName AS ASCIIZ * 32
' Log entry
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Starting with IP='" & S01 & "', port=" & STR$(T01) & ", backlog=" & STR$(T02) & ", log level=" & STR$(g_MCP_LogLevel))
X_AU "[DEBUG] NW_Listen: Starting with IP='" & S01 & "', port=" & STR$(T01) & ", backlog=" & STR$(T02) & ", log level=" & STR$(g_MCP_LogLevel)
END IF
' Initialize
R01 = %INVALID_SOCKET
' Prepare service (port)
servName = FORMAT$(T01)
IF LEN(servName) = 0 OR T01 < 1 OR T01 > 65535 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: Invalid port number " & STR$(T01) & " (must be 1-65535)")
X_AU "[ERROR] NW_Listen: Invalid port number " & STR$(T01) & " (must be 1-65535)"
END IF
GOTO cleanup
END IF
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Service port string: '" & servName & "'")
X_AU "[DEBUG] NW_Listen: Service port string: '" & servName & "'"
END IF
' Set hints for passive listening
FILLMEMORY(VARPTR(E01), SIZEOF(E01), 0)
E01.ai_family = %AF_INET
E01.ai_socktype = %SOCK_STREAM
E01.ai_protocol = %IPPROTO_TCP
E01.ai_flags = %AI_PASSIVE
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Hints set: family=" & STR$(E01.ai_family) & ", socktype=" & STR$(E01.ai_socktype) & ", protocol=" & STR$(E01.ai_protocol) & ", flags=" & STR$(E01.ai_flags))
X_AU "[DEBUG] NW_Listen: Hints set: family=" & STR$(E01.ai_family) & ", socktype=" & STR$(E01.ai_socktype) & ", protocol=" & STR$(E01.ai_protocol) & ", flags=" & STR$(E01.ai_flags)
END IF
' Validate and set IP
nodeName = IIF$(LEN(S01), S01, "0.0.0.0")
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Using IP address: '" & nodeName & "'")
X_AU "[DEBUG] NW_Listen: Using IP address: '" & nodeName & "'"
END IF
' Get address info
E05 = WS_getaddrinfo(VARPTR(nodeName), VARPTR(servName), VARPTR(E01), E02)
IF E05 <> 0 THEN
IF g_MCP_LogLevel > 0 THEN
' This function returns a direct error code, does not use GetLastError
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: WS_getaddrinfo failed for IP '" & nodeName & "', port " & STR$(T01) & " -> " & NW_GaiErrorText(E05) & " (" & STR$(E05) & ")")
END IF
GOTO cleanup
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: getaddrinfo succeeded, processing addresses")
X_AU "[DEBUG] NW_Listen: getaddrinfo succeeded, processing addresses"
END IF
' Count available addresses for logging
addrCount = 0
E03 = E02
WHILE E03 <> %NULL
INCR addrCount
E03 = @E03.ai_next
WEND
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Found " & STR$(addrCount) & " addresses to try")
X_AU "[DEBUG] NW_Listen: Found " & STR$(addrCount) & " addresses to try"
END IF
IF addrCount = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: No addresses returned by getaddrinfo for IP '" & nodeName & "', port " & STR$(T01))
X_AU "[ERROR] NW_Listen: No addresses returned by getaddrinfo for IP '" & nodeName & "', port " & STR$(T01)
END IF
GOTO cleanup
END IF
' Try each address
E03 = E02
WHILE E03 <> %NULL
IF g_MCP_LogLevel > 2 THEN
MEMORY COPY @E03.ai_addr, VARPTR(tempSockAddr), MIN(SIZEOF(SOCKADDR_STORAGE), @E03.ai_addrlen)
tempSockAddr.ss_family = @E03.ai_family
IF NW_AddrToString(tempSockAddr, @E03.ai_addrlen, tempAddrStr) THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Trying address family " & STR$(@E03.ai_family) & ", addr=" & tempAddrStr & ", addrlen=" & STR$(@E03.ai_addrlen))
X_AU "[DEBUG] NW_Listen: Trying address family " & STR$(@E03.ai_family) & ", addr=" & tempAddrStr & ", addrlen=" & STR$(@E03.ai_addrlen)
ELSE
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Trying address family " & STR$(@E03.ai_family) & ", addr=, addrlen=" & STR$(@E03.ai_addrlen))
X_AU "[DEBUG] NW_Listen: Trying address family " & STR$(@E03.ai_family) & ", addr=, addrlen=" & STR$(@E03.ai_addrlen)
END IF
END IF
' Create socket
E06 = WS_socket(@E03.ai_family, @E03.ai_socktype, @E03.ai_protocol)
IF E06 = %INVALID_SOCKET THEN
E05 = INT_GetAndStoreError() ' <--- MODIFIED 1
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: WS_socket failed for address family " & STR$(@E03.ai_family) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")")
X_AU "[ERROR] NW_Listen: WS_socket failed for address family " & STR$(@E03.ai_family) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")"
END IF
ELSE
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Socket created: " & STR$(E06))
X_AU "[DEBUG] NW_Listen: Socket created: " & STR$(E06)
END IF
' Set SO_REUSEADDR
E08 = %TRUE
E07 = setsockopt(E06, %SOL_SOCKET, %SO_REUSEADDR, BYVAL VARPTR(E08), SIZEOF(E08))
IF E07 = %SOCKET_ERROR THEN
E05 = INT_GetAndStoreError() ' <--- MODIFIED 2
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: setsockopt SO_REUSEADDR failed for socket " & STR$(E06) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")")
X_AU "[ERROR] NW_Listen: setsockopt SO_REUSEADDR failed for socket " & STR$(E06) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")"
END IF
WS_closesocket(E06)
E06 = %INVALID_SOCKET
ELSE
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: SO_REUSEADDR set successfully for socket " & STR$(E06))
X_AU "[DEBUG] NW_Listen: SO_REUSEADDR set successfully for socket " & STR$(E06)
END IF
' Bind
E07 = WS_bind(E06, @E03.ai_addr, @E03.ai_addrlen)
IF E07 = %SOCKET_ERROR THEN
E05 = INT_GetAndStoreError() ' <--- MODIFIED 3
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: WS_bind failed for addr=" & tempAddrStr & ", socket=" & STR$(E06) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")")
X_AU "[ERROR] NW_Listen: WS_bind failed for addr=" & tempAddrStr & ", socket=" & STR$(E06) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")"
END IF
WS_closesocket(E06)
E06 = %INVALID_SOCKET
ELSE
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Bind successful for socket " & STR$(E06))
X_AU "[DEBUG] NW_Listen: Bind successful for socket " & STR$(E06)
END IF
' Listen
E07 = WS_listen(E06, T02)
IF E07 = %SOCKET_ERROR THEN
E05 = INT_GetAndStoreError() ' <--- MODIFIED 4
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: WS_listen failed for socket " & STR$(E06) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")")
X_AU "[ERROR] NW_Listen: WS_listen failed for socket " & STR$(E06) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")"
END IF
WS_closesocket(E06)
E06 = %INVALID_SOCKET
ELSE
R01 = E06
' Set non-blocking mode for reliable timeouts
E07 = NW_SetNonBlocking(R01, %TRUE)
IF E07 = %SOCKET_ERROR THEN
E05 = INT_GetAndStoreError() ' <--- MODIFIED 5
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: NW_SetNonBlocking failed for socket " & STR$(R01) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")")
X_AU "[ERROR] NW_Listen: NW_SetNonBlocking failed for socket " & STR$(R01) & ", WSAError " & STR$(E05) & " (" & ERROR$(E05) & ")"
END IF
WS_closesocket(R01)
R01 = %INVALID_SOCKET
ELSE
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Listening socket created on port " & STR$(T01) & ", socket=" & STR$(R01))
X_AU "[DEBUG] NW_Listen: Listening socket created on port " & STR$(T01) & ", socket=" & STR$(R01)
END IF
GOTO cleanup
END IF
END IF
END IF
END IF
END IF
E03 = @E03.ai_next
WEND
IF R01 = %INVALID_SOCKET THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Listen: Failed to find suitable address to bind and listen for IP '" & nodeName & "', port " & STR$(T01) & ", tried " & STR$(addrCount) & " addresses")
X_AU "[ERROR] NW_Listen: Failed to find suitable address to bind and listen for IP '" & nodeName & "', port " & STR$(T01) & ", tried " & STR$(addrCount) & " addresses"
END IF
END IF
cleanup:
IF E02 THEN WS_freeaddrinfo(E02)
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Listen: Cleanup complete, returning socket " & STR$(R01))
X_AU "[DEBUG] NW_Listen: Cleanup complete, returning socket " & STR$(R01)
END IF
FUNCTION = R01
END FUNCTION
' ===============================================================================
' ===============================================================================
' ===============================================================================
' PURPOSE: Connects to a remote host on a specified port with a timeout.
' UNICODE: Host/IP is ASCII/ANSI; no Unicode issues.
' PARAMS: S01 (host or IP), T01 (port), T02 (timeout in seconds)
' RETURNS: Valid connected socket on success, %INVALID_SOCKET on failure.
' * * * SEALED * * *
FUNCTION NW_Connect(BYVAL S01 AS STRING, BYVAL T01 AS LONG, OPT BYVAL T02 AS LONG) COMMON AS DWORD
LOCAL E01 AS ADDRINFOA
LOCAL E02 AS ADDRINFOA PTR
LOCAL E03 AS ADDRINFOA PTR
LOCAL E05 AS LONG
LOCAL E06 AS DWORD
LOCAL E07 AS LONG
LOCAL R01 AS DWORD
LOCAL nodeName AS ASCIIZ * 256
LOCAL servName AS ASCIIZ * 32
LOCAL writeSet AS fd_setstruc
LOCAL errorSet AS fd_setstruc
LOCAL tv AS timeval
LOCAL soError AS LONG
LOCAL soErrorLen AS LONG
LOCAL effectiveTimeout AS LONG
R01 = %INVALID_SOCKET
IF T02 <= 0 THEN effectiveTimeout = %NW_TIMEOUT_DEFAULT / 1000 ELSE effectiveTimeout = T02
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Connect: Starting to " & S01 & ":" & STR$(T01) & ", timeout=" & STR$(effectiveTimeout) & "s")
END IF
nodeName = S01
servName = FORMAT$(T01)
' Validate parameters before use
IF LEN(TRIM$(nodeName)) = 0 OR T01 < 1 OR T01 > 65535 THEN
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Connect: Invalid host or port provided.")
GOTO cleanup
END IF
FILLMEMORY(VARPTR(E01), SIZEOF(E01), 0)
E01.ai_family = %AF_INET ' Force IPv4 to match server; avoid AF_UNSPEC issues
E01.ai_socktype = %SOCK_STREAM
E05 = WS_getaddrinfo(VARPTR(nodeName), VARPTR(servName), VARPTR(E01), E02)
IF E05 <> 0 THEN
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Connect: getaddrinfo failed: " & NW_GaiErrorText(E05))
GOTO cleanup
END IF
E03 = E02
WHILE E03 <> %NULL
E06 = WS_socket(@E03.ai_family, @E03.ai_socktype, @E03.ai_protocol)
IF E06 = %INVALID_SOCKET THEN
E03 = @E03.ai_next
ITERATE
END IF
' Set to non-blocking to enable connection with a timeout
IF NW_SetNonBlocking(E06, %TRUE) = %SOCKET_ERROR THEN
WS_closesocket(E06)
E03 = @E03.ai_next
ITERATE
END IF
' Initiate the non-blocking connection
MCP_Log(%MCP_LOG_DEBUG, "NW_Connect: Calling WS_connect on family " & STR$(@E03.ai_family))
E07 = WS_connect(E06, @E03.ai_addr, @E03.ai_addrlen)
MCP_Log(%MCP_LOG_DEBUG, "NW_Connect: WS_connect returned " & STR$(E07) & ", immediate WSAGetLastError " & STR$(WSAGetLastError()))
IF E07 = 0 THEN
' Case 1: Success! Connection established immediately (common for localhost).
R01 = E06
ELSE
' Case 2: Connection did not succeed immediately. Check the error.
E05 = INT_GetAndStoreError()
IF E05 = 0 THEN E05 = %WSAEWOULDBLOCK ' Workaround for anomalous error 0
IF E05 = %WSAEWOULDBLOCK THEN
' Standard case: Connection is in progress. Wait for it with select().
FD_ZERO(writeSet) : FD_SET(E06, writeSet)
FD_ZERO(errorSet) : FD_SET(E06, errorSet)
tv.tv_sec = effectiveTimeout : tv.tv_usec = 0
E07 = NW_WSASelect(0, BYVAL %NULL, writeSet, errorSet, tv)
' Check select() result:
' > 0 means a socket became ready.
' FD_ISSET(writeSet) means it's ready for writing (connected).
' NOT FD_ISSET(errorSet) means there's no error.
IF E07 > 0 AND FD_ISSET(E06, writeSet) AND NOT FD_ISSET(E06, errorSet) THEN
R01 = E06 ' select() indicates a successful connection.
ELSE
' select() failed, timed out, or found an error.
IF E07 = 0 THEN NW_SetLastError(%WSAETIMEDOUT) ' Set specific error for timeout
IF g_MCP_LogLevel > 0 THEN
soErrorLen = SIZEOF(soError)
getsockopt(E06, %SOL_SOCKET, %SO_ERROR, soError, soErrorLen)
MCP_Log(%MCP_LOG_ERROR, "NW_Connect: Connection failed. select()=" & STR$(E07) & ", SO_ERROR=" & STR$(soError))
END IF
WS_closesocket(E06)
END IF
ELSE
' Case 3: An immediate connection error occurred.
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Connect: Immediate connect failure, WSAError " & STR$(E05))
WS_closesocket(E06)
END IF
END IF
' If a valid socket was obtained, set it back to blocking and exit the loop.
IF R01 <> %INVALID_SOCKET THEN
NW_SetNonBlocking(R01, %FALSE)
IF g_MCP_LogLevel > 1 THEN MCP_Log(%MCP_LOG_DEBUG, "NW_Connect: Connected successfully.")
GOTO cleanup
END IF
' If we're here, this address failed, try the next one in the list.
E03 = @E03.ai_next
WEND
cleanup:
IF E02 THEN WS_freeaddrinfo(E02)
IF R01 = %INVALID_SOCKET AND g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Connect: Failed to connect to any available address.")
END IF
FUNCTION = R01
END FUNCTION
' ===============================================================================
' PURPOSE: Closes a socket handle.
' UNICODE: Not applicable; no string operations.
' PARAMS: U01 (DWORD In) - Socket handle to close.
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
'
FUNCTION NW_CloseSocket(BYVAL U01 AS DWORD) THREADSAFE COMMON AS LONG
LOCAL R01 AS LONG
LOCAL E01 AS LONG
R01 = %NW_SOCKET_ERROR ' Assume failure
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_CloseSocket: Closing socket " & STR$(U01))
X_AU "[DEBUG] NW_CloseSocket: Closing socket " & STR$(U01) ' Add this line
END IF
EnterCriticalSection g_NW_Cs
IF WS_closesocket(U01) = 0 THEN
R01 = 0
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_CloseSocket: Successfully closed socket " & STR$(U01))
X_AU "[DEBUG] NW_CloseSocket: Successfully closed socket " & STR$(U01) ' Add this line
END IF
ELSE
E01 = INT_GetAndStoreError()
IF g_MCP_LogLevel > 0 THEN
LOCAL S01 AS STRING
SELECT CASE E01
CASE %WSAENOTSOCK
S01 = "Invalid socket"
CASE %WSAENETDOWN
S01 = "Network subsystem failed"
CASE ELSE
S01 = "WS_closesocket failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_CloseSocket: " & S01 & ", socket=" & STR$(U01) & ", WSAError " & STR$(E01) & " (" & ERROR$(E01) & ")")
X_AU "[ERROR] NW_CloseSocket: " & S01 & ", socket=" & STR$(U01) & ", WSAError " & STR$(E01) ' Add this line
END IF
END IF
LeaveCriticalSection g_NW_Cs
FUNCTION = R01
END FUNCTION
' ===============================================================================
' ===============================================================================
' PURPOSE: Accepts an incoming connection and retrieves client info with a timeout.
' UNICODE: Client IP is ASCII/ANSI; no Unicode issues.
' PARAMS: U01 (listening socket), S01 (out: client IP), T01 (out: client port), T02 (timeout in seconds)
' RETURNS: Valid client socket on success, %INVALID_SOCKET on failure/timeout.
' * * * SEALED * * *
FUNCTION NW_Accept(BYVAL U01 AS DWORD, BYREF S01 AS STRING, BYREF T01 AS LONG, OPT BYVAL T02 AS LONG) COMMON AS DWORD
LOCAL E01 AS SOCKADDR_STORAGE
LOCAL E02 AS LONG
LOCAL E03 AS LONG
LOCAL R01 AS DWORD
LOCAL readSet AS fd_setstruc
LOCAL tv AS timeval
LOCAL effectiveTimeout AS LONG
' Initialize
R01 = %INVALID_SOCKET
S01 = ""
T01 = 0
IF T02 <= 0 THEN effectiveTimeout = %NW_TIMEOUT_DEFAULT / 1000 ELSE effectiveTimeout = T02
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_Accept: Starting accept on socket " & STR$(U01) & ", timeout=" & STR$(effectiveTimeout) & "s")
END IF
' Use select() to wait for a connection to be ready
FD_ZERO(readSet)
FD_SET(U01, readSet)
tv.tv_sec = effectiveTimeout
tv.tv_usec = 0
E03 = NW_WSASelect(0, readSet, BYVAL %NULL, BYVAL %NULL, tv)
IF E03 > 0 THEN
' The listening socket is readable, meaning a connection is pending.
E02 = SIZEOF(E01)
R01 = WS_accept(U01, VARPTR(E01), E02)
IF R01 = %INVALID_SOCKET THEN
E03 = INT_GetAndStoreError()
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Accept: WS_accept failed after select indicated readiness, WSAError " & STR$(E03))
ELSE
' Success: Set accepted socket to non-blocking and get client info
IF NW_SetNonBlocking(R01, %TRUE) = %SOCKET_ERROR THEN
E03 = INT_GetAndStoreError()
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Accept: NW_SetNonBlocking failed for accepted socket " & STR$(R01) & ", WSAError " & STR$(E03))
WS_closesocket(R01)
R01 = %INVALID_SOCKET
ELSEIF NW_GetClientInfo(R01, S01, T01, effectiveTimeout) = %FALSE THEN
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Accept: Failed to get client info for accepted socket " & STR$(R01))
WS_closesocket(R01)
R01 = %INVALID_SOCKET
ELSE
IF g_MCP_LogLevel > 1 THEN MCP_Log(%MCP_LOG_DEBUG, "NW_Accept: Accepted connection from " & S01 & ":" & STR$(T01))
END IF
END IF
ELSEIF E03 = 0 THEN
' Timeout
IF g_MCP_LogLevel > 1 THEN MCP_Log(%MCP_LOG_DEBUG, "NW_Accept: Timeout waiting for connection on socket " & STR$(U01))
' No error, just a timeout. R01 remains %INVALID_SOCKET
ELSE ' E03 < 0
' select() error
E03 = INT_GetAndStoreError()
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Accept: select() failed, WSAError " & STR$(E03))
' R01 remains %INVALID_SOCKET
END IF
FUNCTION = R01
END FUNCTION
'===============================================================================
' Part 3: Data Transfer
'===============================================================================
' PURPOSE: Initiates a single non-blocking (asynchronous) receive operation.
' UNICODE: Byte-stream safe. Receives raw bytes.
' PARAMS: U01 (socket handle), S01 (STRING Out - MUST BE PRE-ALLOCATED), N01 (length to read), T01 (timeout - IGNORED).
' RETURNS:
' > 0 : Bytes received.
' 0 : Graceful disconnect (peer closed connection).
' -1 (%NW_SOCKET_ERROR): A fatal socket error occurred.
' -2 (%NW_WOULDBLOCK) : Socket buffer is empty (call would block).
' NOTE:
' This is now a true non-blocking helper. It attempts ONE read and returns the status.
' The CALLER (NW_RecvStream/NW_LineInputW) is responsible for looping, timeouts, and handling %NW_WOULDBLOCK.
'
FUNCTION NW_RecvAsync(BYVAL U01 AS DWORD, BYREF S01 AS STRING, BYVAL N01 AS LONG, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL E01 AS WSABUF
LOCAL N02 AS DWORD ' BytesRecvd (from WSARecv output)
LOCAL N06 AS DWORD ' Flags (from WSARecv output)
LOCAL E03 AS LONG ' WSA Error Code
LOCAL R01 AS LONG ' Return value
R01 = %NW_SOCKET_ERROR ' Default to fatal error
N06 = 0
N02 = 0
' Prepare the WSABUF. The caller MUST ensure S01 is pre-allocated (e.g., SPACE$)
' This function assumes N01 (length) matches the allocated size of S01.
E01.dLen = N01
E01.buf = STRPTR(S01)
' Attempt one non-blocking read
IF WSARecv(U01, E01, 1, N02, N06, BYVAL %NULL, BYVAL %NULL) = 0 THEN
' WSARecv call succeeded
IF N02 = 0 THEN
' Graceful disconnect (peer closed the socket)
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_RecvAsync: Graceful disconnect (0 bytes) on socket " & STR$(U01))
END IF
R01 = 0 ' Return 0 for disconnect
ELSE
' Data received
R01 = N02 ' Return bytes read
END IF
ELSE
' WSARecv call failed
E03 = INT_GetAndStoreError() ' <--- MODIFIED
IF E03 = 0 OR E03 = %WSAEWOULDBLOCK THEN
IF E03 = 0 THEN MCP_Log(%MCP_LOG_DEBUG, "NW_RecvAsync: Treating WSAError 0 as WOULDBLOCK on socket " & STR$(U01))
R01 = %NW_WOULDBLOCK ' (-2) Buffer is empty, try again later
ELSE
' This is a fatal error
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E03
CASE %WSAENOTCONN
errMsg = "Socket not connected"
CASE %WSAECONNRESET
errMsg = "Connection reset by peer"
CASE ELSE
errMsg = "WSARecv failed, WSAError " & STR$(E03)
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_RecvAsync: " & errMsg & " on socket " & STR$(U01))
END IF
R01 = %NW_SOCKET_ERROR ' (-1) Fatal error
END IF
END IF
FUNCTION = R01
END FUNCTION
'===============================================================================
' ===============================================================================
' PURPOSE: Receives a buffer of data up to a maximum length.
' UNICODE: Byte-stream safe.
' PARAMS: U01 (socket), S01 (out: data), N01 (max length), T01 (timeout - IGNORED).
' RETURNS: >0 (bytes received), 0 (disconnect), -2 (would block), -1 (error).
FUNCTION NW_Recv(BYVAL U01 AS DWORD, BYREF S01 AS STRING, BYVAL N01 AS LONG, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL N02 AS LONG ' Bytes received
LOCAL N03 AS LONG ' Max buffer size
LOCAL E01 AS LONG ' Error code
LOCAL R01 AS LONG ' Return value
R01 = %NW_SOCKET_ERROR
S01 = ""
N03 = 65536
IF N01 <= 0 OR N01 > N03 THEN
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Recv: Invalid maxLen " & STR$(N01) & " on socket " & STR$(U01))
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
S01 = SPACE$(N01)
N02 = WS_Recv(U01, BYVAL STRPTR(S01), N01, 0)
IF N02 > 0 THEN
S01 = LEFT$(S01, N02)
R01 = N02
ELSEIF N02 = 0 THEN
S01 = ""
R01 = 0 ' Graceful close
ELSE ' N02 is SOCKET_ERROR
E01 = INT_GetAndStoreError()
S01 = ""
IF E01 = 0 THEN MCP_Log(%MCP_LOG_DEBUG, "NW_Recv: Treating WSAError 0 as WOULDBLOCK on socket " & STR$(U01))
IF E01 = 0 OR E01 = %WSAEWOULDBLOCK THEN
R01 = %NW_WOULDBLOCK
ELSE
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_Recv: Error on socket " & STR$(U01) & ", WSAError " & STR$(E01))
R01 = %NW_SOCKET_ERROR
END IF
END IF
FUNCTION = R01
END FUNCTION
'===============================================================================
FUNCTION NW_RecvStream(BYVAL U01 AS LONG, BYREF S01 AS STRING, BYVAL N01 AS LONG, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL L01 AS STRING
LOCAL L02 AS STRING
LOCAL N02 AS LONG ' Result from NW_RecvAsync
LOCAL N04 AS LONG ' Total bytes received counter
LOCAL D01 AS DOUBLE ' Master timeout marker
LOCAL N05 AS LONG ' Chunk read size
LOCAL N06 AS LONG ' File handle
LOCAL R01 AS LONG ' Function result
LOCAL bufferSize AS LONG
R01 = %NW_SOCKET_ERROR
N04 = 0 ' Total bytes received
N05 = 65536 ' 64KB chunks
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
D01 = TIMER + T01 ' Absolute master timeout
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_RecvStream: Starting receive on socket " & STR$(U01) & ", target=" & STR$(N01) & ", timeout=" & STR$(T01) & "s")
END IF
' Increase socket receive buffer
bufferSize = 1048576 ' 1MB
setsockopt(U01, %SOL_SOCKET, %SO_RCVBUF, BYVAL VARPTR(bufferSize), SIZEOF(bufferSize))
' Check if S01 is a filename (File Mode) or empty (String Mode)
IF LEN(S01) THEN
N06 = FREEFILE
OPEN S01 FOR BINARY AS N06
IF ERR THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_RecvStream: Failed to open file " & S01 & ", Error " & STR$(ERR))
END IF
GOTO enx
END IF
END IF
DO
' 1. Check Master Timeout
IF TIMER > D01 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_RecvStream: Master timeout after receiving " & STR$(N04) & " bytes on socket " & STR$(U01))
END IF
GOTO enx
END IF
' 2. Check if target length (N01) is met (if N01 > 0)
IF N01 > 0 AND N04 >= N01 THEN
R01 = N04
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_RecvStream: Received target " & STR$(N04) & " bytes on socket " & STR$(U01))
END IF
GOTO enx
END IF
' 3. Prepare buffer and call the non-blocking read helper
L01 = SPACE$(N05)
N02 = NW_RecvAsync(U01, L01, MIN(N05, IIF(N01 > 0, N01 - N04, N05)), T01)
' 4. Handle results from NW_RecvAsync
SELECT CASE N02
CASE IS > 0
' DATA: Process received data
L02 = LEFT$(L01, N02)
IF N06 <> 0 THEN ' File Mode
PUT N06, , L02
ELSE ' String Mode
S01 = S01 & L02
END IF
N04 = N04 + N02
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_RecvStream: Received " & STR$(N02) & " bytes, total " & STR$(N04) & " on socket " & STR$(U01))
END IF
CASE 0
' DISCONNECT (Graceful): Treat as EOF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_RecvStream: Peer closed connection (R:0) after " & STR$(N04) & " bytes on socket " & STR$(U01))
END IF
R01 = N04 ' Return bytes received before disconnect
GOTO enx
CASE %NW_WOULDBLOCK ' (-2)
' NO DATA: Buffer is empty. Yield CPU and re-check timeout.
SLEEP 10 ' Increased for large transfers
ITERATE LOOP
CASE %NW_SOCKET_ERROR ' (-1)
' FATAL ERROR (Already logged by NW_RecvAsync)
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_RecvStream: NW_RecvAsync failed (-1), halting stream after " & STR$(N04) & " bytes on socket " & STR$(U01))
END IF
GOTO enx
END SELECT
LOOP
enx:
IF N06 THEN CLOSE N06
IF R01 = %NW_SOCKET_ERROR AND N04 > 0 THEN R01 = N04
FUNCTION = R01
END FUNCTION
FUNCTION INT_ConsumeChunked(BYVAL sock AS DWORD) COMMON AS LONG
LOCAL total AS LONG, chunkSize AS LONG, S01 AS STRING, S02 AS STRING
DO
IF NW_LineInput(sock, S01, 5) = 0 THEN EXIT FUNCTION ' Timeout/disconnect
chunkSize = VAL("&H" & S01)
IF chunkSize = 0 THEN EXIT DO
S02 = SPACE$(chunkSize)
IF NW_RecvStream(sock, S02, chunkSize, 5) <> chunkSize THEN EXIT FUNCTION
total += chunkSize
NW_LineInput(sock, S01, 5) ' Consume trailing CRLF
LOOP
FUNCTION = total
END FUNCTION
'===============================================================================
' Part 0: Aliases, constants, globals
'===============================================================================
' ########################################################################
' FUNCTION: NW_Send (non-blocking safe, short vars, single-exit)
'-
' Purpose:
' Sends the entire STRING buffer over a socket, retrying on
' %WSAEWOULDBLOCK until the overall timeout expires.
'
' Parameters:
' U01 (BYVAL DWORD) - Socket handle.
' S01 (BYREF STRING) - Data to send (raw bytes; UTF-8/ASCII/etc.).
' T01 (BYVAL LONG) - Timeout in seconds (<=0 uses %NW_TIMEOUT_DEFAULT).
'
' Returns:
' >= 0 : Total bytes successfully sent.
' -1 : %NW_SOCKET_ERROR on failure or timeout.
'
' Notes:
' - Uses short variable names and avoids reserved identifiers like POS.
' - Single-exit pattern with label 'enx:' and result variable R01.
' - Assumes WS_Send() is declared as in your library.
' ########################################################################
' PURPOSE: Sends an entire STRING buffer over a socket, handling non-blocking
' sockets and timeouts correctly.
' UNICODE: Byte-stream safe.
' PARAMS: U01 (socket), S01 (data to send), T01 (timeout in seconds).
' RETURNS: Total bytes sent, or %NW_SOCKET_ERROR on failure/timeout.
FUNCTION NW_Send(BYVAL U01 AS DWORD, BYREF S01 AS STRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL totalBytesToSend AS LONG
LOCAL bytesSent AS LONG
LOCAL currentBytesSent AS LONG
LOCAL bytesRemaining AS LONG
LOCAL timeoutMarker AS DOUBLE
LOCAL E01 AS fd_setstruc ' Write file descriptor set
LOCAL E02 AS timeval ' Timeout structure for select()
LOCAL E03 AS LONG ' WSA Error Code (from WSAGetLastError)
LOCAL E04 AS LONG ' Result of select() call
LOCAL E05 AS LONG ' Flag for loop exit
LOCAL E06 AS LONG ' Temporary flag
LOCAL R01 AS LONG ' Return value (bytes sent or -1)
R01 = %NW_SOCKET_ERROR ' Assume failure
totalBytesToSend = LEN(S01)
bytesSent = 0
bytesRemaining = totalBytesToSend
' Validate input
IF totalBytesToSend = 0 THEN
R01 = 0 ' Successfully sent 0 bytes
GOTO enx
END IF
' Set timeout
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
timeoutMarker = TIMER + T01
' --- START Logging ---
IF g_MCP_LogLevel > 1 THEN
MCP_Log %MCP_LOG_DEBUG, "NW_Send: Start sending " & STR$(totalBytesToSend) & " bytes on socket " & STR$(U01) & ", timeout=" & STR$(T01) & "s, marker=" & FORMAT$(timeoutMarker, "#0.000")
X_AU "[DEBUG] NW_Send: Start sending " & STR$(totalBytesToSend) & " bytes on socket " & STR$(U01) & ", timeout=" & STR$(T01) & "s"
END IF
' --- END Logging ---
' Main send loop
DO
' Check overall timeout first
IF TIMER > timeoutMarker THEN
' --- START Logging ---
IF g_MCP_LogLevel > 0 THEN
MCP_Log %MCP_LOG_ERROR, "NW_Send: Overall timeout expired while sending on socket " & STR$(U01) & ". Sent " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & " bytes."
X_AU "[ERROR] NW_Send: Overall timeout expired on socket " & STR$(U01) & ". Sent " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & " bytes."
END IF
' --- END Logging ---
R01 = %NW_SOCKET_ERROR ' Timeout is an error
GOTO enx
END IF
' Prepare fd_set for select (check if socket is writable)
FD_ZERO E01
FD_SET U01, E01
' Calculate remaining time for select() timeout
E02.tv_sec = MIN&(5, INT(timeoutMarker - TIMER)) ' Cap at 5s per select call, prevent negative
E02.tv_usec = 0
IF E02.tv_sec < 0 THEN E02.tv_sec = 0 ' Safety check
' --- START Logging ---
IF g_MCP_LogLevel > 2 THEN ' Verbose debug
MCP_Log %MCP_LOG_DEBUG, "NW_Send: Calling select() for socket " & STR$(U01) & ", bytesSent=" & STR$(bytesSent) & ", select_timeout=" & STR$(E02.tv_sec) & "s"
X_AU "[DEBUG] NW_Send: select() for socket " & STR$(U01) & ", sent=" & STR$(bytesSent) & ", sel_to=" & STR$(E02.tv_sec) & "s"
END IF
' --- END Logging ---
' Wait for socket to become writable (or timeout/error)
E04 = WS_WSASelect(0, BYVAL %NULL, E01, BYVAL %NULL, E02)
' --- START Logging ---
IF g_MCP_LogLevel > 2 THEN ' Verbose debug
MCP_Log %MCP_LOG_DEBUG, "NW_Send: select() returned " & STR$(E04) & " for socket " & STR$(U01)
X_AU "[DEBUG] NW_Send: select() returned " & STR$(E04) & " for socket " & STR$(U01)
END IF
' --- END Logging ---
' Check result of select()
SELECT CASE E04
CASE IS > 0
' Socket is ready for writing
' --- START Logging ---
IF g_MCP_LogLevel > 2 THEN ' Verbose debug
MCP_Log %MCP_LOG_DEBUG, "NW_Send: select() indicates socket " & STR$(U01) & " is writable."
X_AU "[DEBUG] NW_Send: Socket " & STR$(U01) & " is writable."
END IF
' --- END Logging ---
CASE 0
' select() timed out
' --- START Logging ---
IF g_MCP_LogLevel > 1 THEN
MCP_Log %MCP_LOG_WARN, "NW_Send: select() timed out while waiting to write on socket " & STR$(U01) & ". Sent " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & " bytes so far."
X_AU "[WARN] NW_Send: select() timed out for socket " & STR$(U01) & ". Sent " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & " bytes."
END IF
' --- END Logging ---
' Continue loop to check overall timeout or retry
ITERATE DO
CASE ELSE ' -1, indicating error in select()
E03 = WSAGetLastError()
' --- START Logging ---
IF g_MCP_LogLevel > 0 THEN
MCP_Log %MCP_LOG_ERROR, "NW_Send: select() failed on socket " & STR$(U01) & ", WSAError " & STR$(E03) & ". Sent " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & " bytes."
X_AU "[ERROR] NW_Send: select() failed on socket " & STR$(U01) & ", WSAError " & STR$(E03) & ". Sent " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & " bytes."
END IF
' --- END Logging ---
R01 = %NW_SOCKET_ERROR
GOTO enx
END SELECT
' Socket is ready, attempt to send remaining data
currentBytesSent = WS_Send(U01, STRPTR(S01) + bytesSent, bytesRemaining, 0)
' Check result of send
IF currentBytesSent = %NW_SOCKET_ERROR THEN
E03 = WSAGetLastError()
' --- START Logging ---
IF g_MCP_LogLevel > 2 THEN ' Verbose debug
MCP_Log %MCP_LOG_DEBUG, "NW_Send: WS_Send returned %NW_SOCKET_ERROR, WSAError=" & STR$(E03) & " on socket " & STR$(U01)
X_AU "[DEBUG] NW_Send: WS_Send error " & STR$(E03) & " on socket " & STR$(U01)
END IF
' --- END Logging ---
' Handle specific errors
SELECT CASE E03
CASE %WSAEWOULDBLOCK, 0 ' Treat 0 as WOULDBLOCK (Unusual, but as per original logic)
' --- START Logging ---
IF g_MCP_LogLevel > 2 THEN ' Verbose debug
MCP_Log %MCP_LOG_DEBUG, "NW_Send: WS_Send would block (WSAEWOULDBLOCK or 0) on socket " & STR$(U01) & ". Yielding CPU."
X_AU "[DEBUG] NW_Send: Would block on socket " & STR$(U01) & ". Yielding."
END IF
' --- END Logging ---
SLEEP 10 ' Brief pause before retrying select/send
ITERATE DO ' Go back to select loop
CASE ELSE
' Fatal error during send
' --- START Logging ---
IF g_MCP_LogLevel > 0 THEN
MCP_Log %MCP_LOG_ERROR, "NW_Send: WS_Send failed fatally on socket " & STR$(U01) & ", WSAError " & STR$(E03) & " after sending " & STR$(bytesSent) & " bytes."
X_AU "[ERROR] NW_Send: WS_Send fatal error " & STR$(E03) & " on socket " & STR$(U01) & " after " & STR$(bytesSent) & " bytes."
END IF
' --- END Logging ---
R01 = %NW_SOCKET_ERROR
GOTO enx
END SELECT
ELSE
' Send was successful, update counters
bytesSent = bytesSent + currentBytesSent
bytesRemaining = bytesRemaining - currentBytesSent
' --- START Logging ---
IF g_MCP_LogLevel > 2 THEN ' Verbose debug
MCP_Log %MCP_LOG_DEBUG, "NW_Send: Successfully sent " & STR$(currentBytesSent) & " bytes on socket " & STR$(U01) & ". Total sent now " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & "."
X_AU "[DEBUG] NW_Send: Sent " & STR$(currentBytesSent) & " bytes (total " & STR$(bytesSent) & ") on socket " & STR$(U01)
END IF
' --- END Logging ---
' Check if all data has been sent
IF bytesRemaining <= 0 THEN
R01 = bytesSent ' Success, return total bytes sent
' --- START Logging ---
IF g_MCP_LogLevel > 1 THEN
MCP_Log %MCP_LOG_INFO, "NW_Send: Successfully sent all " & STR$(bytesSent) & " bytes on socket " & STR$(U01) & "."
X_AU "[INFO] NW_Send: Completed sending " & STR$(bytesSent) & " bytes on socket " & STR$(U01)
END IF
' --- END Logging ---
GOTO enx
END IF
END IF
LOOP WHILE bytesRemaining > 0 AND TIMER <= timeoutMarker
' If loop exits without sending all data, it's a timeout
IF bytesRemaining > 0 THEN
' --- START Logging ---
IF g_MCP_LogLevel > 0 THEN
MCP_Log %MCP_LOG_ERROR, "NW_Send: Loop exit - Timeout sending on socket " & STR$(U01) & ". Sent " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & " bytes."
X_AU "[ERROR] NW_Send: Loop exit - Timeout on socket " & STR$(U01) & ". Sent " & STR$(bytesSent) & "/" & STR$(totalBytesToSend) & " bytes."
END IF
' --- END Logging ---
R01 = %NW_SOCKET_ERROR
END IF
enx:
FUNCTION = R01 ' Return the final result
END FUNCTION
'===============================================================================
'===============================================================================
'
'===============================================================================
' PURPOSE: Sends a complete and properly formatted HTTP response, with optional chunked encoding for dynamic streaming.
' UNICODE: Byte-stream safe. statusText and headers must be ASCII; body can be UTF-8 or other encoding.
' PARAMS: U01 (socket handle), N01 (status code), S01 (status text), S02 (headers), S03 (body or chunk source), T01 (timeout in seconds), B01 (chunked encoding flag - %TRUE to enable).
' RETURNS: %TRUE on success, %FALSE on send error.
' * * * SEALED * * *
FUNCTION NW_SendHttpResponse(BYVAL U01 AS DWORD, BYVAL N01 AS LONG, BYVAL S01 AS STRING, BYREF S02 AS STRING, BYREF S03 AS STRING, BYVAL T01 AS LONG, BYVAL B01 AS LONG) COMMON AS LONG
LOCAL E01 AS STRING
LOCAL E02 AS LONG
LOCAL E03 AS LONG
LOCAL E04 AS STRING ' Chunk data
LOCAL E05 AS LONG ' Chunk length
LOCAL E06 AS STRING ' Chunk header
' Validate inputs
IF N01 < 100 OR N01 > 599 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendHttpResponse: Invalid status code " & STR$(N01) & " on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendHttpResponse: Empty status text on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Check for non-ASCII characters in statusText and headers (HTTP requires ASCII)
IF INSTR(S01, ANY CHR$(128 TO 255)) THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SendHttpResponse: Non-ASCII characters detected in status text on socket " & STR$(U01))
END IF
END IF
IF LEN(S02) > 0 AND INSTR(S02, ANY CHR$(128 TO 255)) THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_INFO, "NW_SendHttpResponse: Non-ASCII characters detected in headers on socket " & STR$(U01))
END IF
END IF
' Construct headers
E01 = "HTTP/1.1 " & FORMAT$(N01) & " " & S01 & $CRLF
IF B01 THEN
E01 &= "Transfer-Encoding: chunked" & $CRLF
ELSE
E01 &= "Content-Length: " & STR$(LEN(S03)) & $CRLF
END IF
E01 &= S02
IF RIGHT$(S02, 2) <> $CRLF THEN E01 &= $CRLF
E01 &= $CRLF
' Send headers
E02 = NW_Send(U01, E01, T01)
IF E02 = %NW_SOCKET_ERROR THEN
E03 = INT_GetAndStoreError() ' <--- MODIFIED 1 (Use stored error from NW_Send)
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E03
CASE %WSAEWOULDBLOCK
errMsg = "Non-blocking operation would block"
CASE %WSAENOTCONN
errMsg = "Socket not connected"
CASE %WSAECONNRESET
errMsg = "Connection reset by peer"
CASE ELSE
errMsg = "NW_Send failed for headers"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_SendHttpResponse: " & errMsg & " on socket " & STR$(U01) & ", WSAError " & STR$(E03))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Send body
IF LEN(S03) > 0 THEN
IF B01 THEN
' Chunked encoding: send body in chunks
LOCAL chunkSize AS LONG
LOCAL startPos AS LONG
LOCAL chunkData AS STRING
chunkSize = 65536 ' 64KB chunks
startPos = 1
WHILE startPos <= LEN(S03)
chunkData = MID$(S03, startPos, chunkSize)
E05 = LEN(chunkData)
IF E05 = 0 THEN EXIT LOOP
' Send chunk header (hex length + CRLF)
E06 = HEX$(E05) & $CRLF
E02 = NW_Send(U01, E06, T01)
IF E02 = %NW_SOCKET_ERROR THEN
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Send chunk data AND trailing CRLF
E02 = NW_Send(U01, chunkData & $CRLF, T01) ' CORRECTED: Added $CRLF after data
IF E02 = %NW_SOCKET_ERROR THEN
FUNCTION = %FALSE
EXIT FUNCTION
END IF
startPos = startPos + E05
WEND
' Send last chunk (0-length chunk)
E06 = "0" & $CRLF & $CRLF
E02 = NW_Send(U01, E06, T01)
IF E02 = %NW_SOCKET_ERROR THEN
FUNCTION = %FALSE
EXIT FUNCTION
END IF
ELSE
' Non-chunked: send full body
E02 = NW_Send(U01, S03, T01)
IF E02 = %NW_SOCKET_ERROR THEN
E03 = INT_GetAndStoreError() ' <--- MODIFIED 2 (Use stored error from NW_Send)
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg2 AS STRING
SELECT CASE E03
CASE %WSAEWOULDBLOCK
errMsg2 = "Non-blocking operation would block"
CASE %WSAENOTCONN
errMsg2 = "Socket not connected"
CASE %WSAECONNRESET
errMsg2 = "Connection reset by peer"
CASE ELSE
errMsg2 = "NW_Send failed for body"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_SendHttpResponse: " & errMsg2 & " on socket " & STR$(U01) & ", WSAError " & STR$(E03))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
END IF
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SendHttpResponse: Sent headers (" & STR$(LEN(E01)) & " bytes) and body (" & STR$(LEN(S03)) & " bytes) for status code " & STR$(N01) & " on socket " & STR$(U01))
END IF
' At end of function, before FUNCTION = %TRUE
MCP_Log(%MCP_LOG_INFO, "NW_SendHttpResponse: Success on socket " & STR$(U01))
FUNCTION = %TRUE
END FUNCTION
'===============================================================================
'
'===============================================================================
' PURPOSE: Sends a string of text followed by a CRLF line ending.
' UNICODE: Byte-stream safe, same as NW_Send. Assumes text is properly encoded.
' PARAMS: socketHandle (socket handle), text (text to send), timeoutSec (timeout in seconds).
' RETURNS: Number of bytes sent, or %NW_SOCKET_ERROR on failure.
' * * * SEALED * * *
FUNCTION NW_Print(BYVAL socketHandle AS DWORD, BYREF TEXT AS STRING, BYVAL timeoutSec AS LONG) COMMON AS LONG
REGISTER R01 AS LONG
LOCAL dataToSend AS STRING
LOCAL maxBufferSize AS LONG
' Validate input length
maxBufferSize = 1048576 ' 1MB limit, consistent with NW_Send
IF LEN(TEXT) > (maxBufferSize - LEN($CRLF)) THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Print: Text length " & STR$(LEN(TEXT)) & " exceeds max " & STR$(maxBufferSize - LEN($CRLF)) & " on socket " & STR$(socketHandle))
END IF
R01= %NW_SOCKET_ERROR
GOTO enx
END IF
' Append CRLF and send
dataToSend = TEXT & $CRLF
R01 = NW_Send(socketHandle, dataToSend, timeoutSec)
' Log error if send fails
IF R01 = %NW_SOCKET_ERROR AND g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_Print: Failed to send data on socket " & STR$(socketHandle) & ", WSAError " & STR$(WSAGetLastError()))
END IF
enx:
FUNCTION=R01
END FUNCTION
'===============================================================================
' PURPOSE: Sends a Unicode WSTRING by transparently encoding it to UTF-8.
' UNICODE: Fully Unicode safe. Handles WSTRING data directly.
' PARAMS: socketHandle (socket handle), text (WSTRING data to send), timeoutSec (timeout in seconds).
' RETURNS: Number of bytes sent, or %NW_SOCKET_ERROR on failure.
FUNCTION NW_SendW(BYVAL socketHandle AS DWORD, BYVAL TEXT AS WSTRING, BYVAL timeoutSec AS LONG) COMMON AS LONG
REGISTER R01 AS LONG
LOCAL utf8Data AS STRING
LOCAL maxBufferSize AS LONG
' Validate input
IF LEN(TEXT) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SendW: Empty WSTRING on socket " & STR$(socketHandle))
END IF
R01 = 0: GOTO enx
END IF
' Convert WSTRING to UTF-8
utf8Data = HTP_WideToUtf8(TEXT)
IF LEN(utf8Data) = 0 AND LEN(TEXT) > 0 THEN ' Check if conversion failed on non-empty string
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendW: UTF-8 conversion failed on socket " & STR$(socketHandle))
END IF
R01 = %NW_SOCKET_ERROR: GOTO enx
END IF
' Validate encoded length
maxBufferSize = 1048576 ' 1MB limit, consistent with NW_Send
IF LEN(utf8Data) > maxBufferSize THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendW: UTF-8 data length " & STR$(LEN(utf8Data)) & " exceeds max " & STR$(maxBufferSize) & " on socket " & STR$(socketHandle))
END IF
R01 = %NW_SOCKET_ERROR: GOTO enx
END IF
' --- CORRECTED LOGIC ---
' Send UTF-8 encoded data and store the result in R01
R01 = NW_Send(socketHandle, utf8Data, timeoutSec)
' Log error if send fails
IF R01 = %NW_SOCKET_ERROR AND g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendW: Failed to send data on socket " & STR$(socketHandle) & ", WSAError " & STR$(WSAGetLastError()))
END IF
enx:
FUNCTION = R01 ' Return the value stored in R01
END FUNCTION
'===============================================================================
' Part 4: Asynchronous and Non-Blocking Operations (I/O Control)
'===============================================================================
' PURPOSE: Enables or disables non-blocking mode on a socket.
' UNICODE: Not applicable; this function manipulates a socket's state.
' PARAMS: socketHandle (SOCKET In), enable (LONG In - %TRUE or %FALSE).
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
' * * * SEALED * * *
FUNCTION NW_SetNonBlocking(BYVAL socketHandle AS DWORD, BYVAL U02 AS LONG) COMMON AS LONG
LOCAL arg AS DWORD
arg = IIF(U02 <> 0, 1, 0)
FUNCTION = ioctlsocket(socketHandle, %FIONBIO, arg)
END FUNCTION
' PURPOSE: Associates network events (read, write, close) on a socket with a window message.
' UNICODE: Not applicable; deals with handles and system messages.
' PARAMS: socketHandle (SOCKET), hWnd (Window Handle), wMsg, lEvent flags.
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
FUNCTION NW_AsyncSelect(BYVAL socketHandle AS DWORD, BYVAL hWnd AS DWORD, BYVAL wMsg AS DWORD, BYVAL lEvent AS LONG) COMMON AS LONG
FUNCTION = WSAAsyncSelect(socketHandle, hWnd, wMsg, lEvent)
END FUNCTION
'===============================================================================
' Part 5: Asynchronous Data Transfer
'===============================================================================
' PURPOSE: Sends a buffer of data using WSASend (synchronous, non-overlapped).
' UNICODE: Byte-stream safe. Transmits raw bytes of any encoding (e.g., UTF-8).
' PARAMS: socketHandle (socket handle), data (STRING to send), timeoutSec (timeout in seconds).
' RETURNS: Number of bytes sent on success, or %NW_SOCKET_ERROR on failure.
FUNCTION NW_SendEx(BYVAL socketHandle AS DWORD, BYREF U02 AS STRING, BYVAL timeoutSec AS LONG) COMMON AS LONG
LOCAL E01 AS WSABUF
LOCAL bytesSent AS DWORD
LOCAL result AS LONG
LOCAL timeoutMarker AS DOUBLE
LOCAL maxBufferSize AS LONG
' Validate input
maxBufferSize = 1048576 ' 1MB limit, consistent with NW_Send
IF LEN(U02) <= 0 OR LEN(U02) > maxBufferSize THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendEx: Invalid data length " & STR$(LEN(U02)) & " on socket " & STR$(socketHandle))
END IF
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
' Set timeout
IF timeoutSec <= 0 THEN timeoutSec = %NW_TIMEOUT_DEFAULT / 1000
timeoutMarker = TIMER + timeoutSec
' Check for timeout (for consistency with non-blocking handling)
IF TIMER > timeoutMarker THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendEx: Timeout on socket " & STR$(socketHandle))
END IF
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
' Prepare WSABUF structure
E01.dlen = LEN(U02)
E01.buf = STRPTR(U02)
' Send data using WSASend
result = WSASend(socketHandle, E01, 1, bytesSent, 0, BYVAL %NULL, BYVAL %NULL)
IF result = 0 THEN
FUNCTION = bytesSent
ELSE
' Handle error, including WSAEWOULDBLOCK for non-blocking sockets
IF WSAGetLastError() = %WSAEWOULDBLOCK THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_INFO, "NW_SendEx: No data sent (WSAEWOULDBLOCK) on socket " & STR$(socketHandle))
END IF
FUNCTION = 0 ' No data sent, but not an error
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendEx: Error on socket " & STR$(socketHandle) & ", WSAError " & STR$(WSAGetLastError()))
END IF
FUNCTION = %NW_SOCKET_ERROR
END IF
END IF
END FUNCTION
'--------------------------------------------------------------------------------------------------
'===============================================================================
' ########################################################################
' FUNCTION: NW_SendFile (optimized, chunked, single-exit)
'-
' Purpose:
' Sends a file over a socket in chunks, using NW_Send for reliability.
'
' Parameters:
' U01 (BYVAL DWORD) - Socket handle.
' S01 (BYVAL STRING) - Pathname of the file to send.
' T01 (BYVAL LONG) - Timeout in seconds (passed to NW_Send).
'
' Returns:
' >= 0 : Total bytes successfully sent.
' -1 : %NW_SOCKET_ERROR on failure (file not found, read error, NW_Send error).
'
' Notes:
' - Sends files in 64KB chunks for efficiency.
' - Uses NW_Send for each chunk, inheriting its timeout and error handling.
' - Single-exit pattern with label 'cleanup:' and result variable R01.
' ########################################################################
' PURPOSE: Sends the contents of a file over a socket.
' UNICODE: File path is ASCII/UTF-8, data is sent as raw bytes.
' PARAMS: U01 (socket), S01 (file path), T01 (timeout for NW_Send).
' RETURNS: Total bytes sent, or %NW_SOCKET_ERROR on failure.
FUNCTION NW_SendFile(BYVAL U01 AS DWORD, BYVAL S01 AS STRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL E01 AS DOUBLE
LOCAL E02 AS LONG
LOCAL E03 AS STRING
LOCAL E04 AS LONG
LOCAL E05 AS LONG
LOCAL E06 AS LONG
LOCAL E08 AS LONG
LOCAL R01 AS LONG
LOCAL T02 AS QUAD
LOCAL T03 AS LONG
R01 = %NW_SOCKET_ERROR
E02 = 0
E06 = 65536
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendFile: Empty file path on socket " & STR$(U01))
END IF
GOTO cleanup
END IF
E04 = FREEFILE
OPEN S01 FOR BINARY ACCESS READ AS E04
IF ERR THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendFile: Failed to open file '" & S01 & "' on socket " & STR$(U01) & ", Error " & STR$(ERR))
END IF
GOTO cleanup
END IF
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
E01 = TIMER + T01
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SendFile: Sending file '" & S01 & "' on socket " & STR$(U01) & ", timeout " & STR$(T01) & " seconds")
END IF
DO
IF TIMER > E01 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendFile: Timeout after sending " & STR$(E02) & " bytes on socket " & STR$(U01))
END IF
GOTO cleanup
END IF
IF LOC(E04) > LOF(E04) THEN EXIT DO
T02 = LOF(E04) - LOC(E04) + 1
IF T02 <= 0 THEN EXIT DO
T03 = MIN(T02, E06)
E03 = SPACE$(T03)
' --- CORRECTED SYNTAX ---
' Use LOC(E04) + 1 as the record number to read from the current position.
' Since it's a binary file, the "record" is just a byte.
GET E04, LOC(E04) + 1, E03
' --- END CORRECTION ---
IF ERR THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendFile: Failed to read file '" & S01 & "' on socket " & STR$(U01) & ", Error " & STR$(ERR))
END IF
GOTO cleanup
END IF
E05 = NW_Send(U01, E03, T01)
IF E05 = %NW_SOCKET_ERROR THEN
E08 = WSAGetLastError()
IF E08 = %WSAEWOULDBLOCK THEN
SLEEP 100 ' Increased for large file stability
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SendFile: WSAEWOULDBLOCK, retrying after sending " & STR$(E02) & " bytes on socket " & STR$(U01))
END IF
' Rewind for retry: Set file pointer back by the amount we tried to send
SEEK E04, LOC(E04) - LEN(E03)
ITERATE LOOP
END IF
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E08
CASE %WSAENOTCONN
errMsg = "Socket not connected"
CASE %WSAECONNRESET
errMsg = "Connection reset by peer"
CASE ELSE
errMsg = "NW_Send failed, WSAError " & STR$(E08)
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_SendFile: " & errMsg & " after sending " & STR$(E02) & " bytes on socket " & STR$(U01))
END IF
GOTO cleanup
END IF
E02 = E02 + E05
IF E05 < LEN(E03) THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_INFO, "NW_SendFile: Partial send of " & STR$(E05) & "/" & STR$(LEN(E03)) & " bytes on socket " & STR$(U01))
END IF
' Rewind for the unsent portion
SEEK E04, LOC(E04) - (LEN(E03) - E05)
END IF
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SendFile: Sent " & STR$(E05) & " bytes chunk from file '" & S01 & "', total " & STR$(E02) & " on socket " & STR$(U01))
END IF
LOOP
R01 = E02
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SendFile: Successfully sent " & STR$(E02) & " bytes from file '" & S01 & "' on socket " & STR$(U01))
END IF
cleanup:
IF E04 THEN CLOSE E04
FUNCTION = R01
END FUNCTION
'===============================================================================
'===============================================================================
' Part 6: Socket Options and Information (Corrected)
'===============================================================================
' PURPOSE: Sets the send timeout for a socket in milliseconds.
' UNICODE: Not applicable; this function manipulates a socket's state.
' PARAMS: U01 (socket handle), U02 (timeout in ms).
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
FUNCTION NW_SetSendTimeout(BYVAL U01 AS DWORD, BYVAL U02 AS LONG) COMMON AS LONG
FUNCTION = setsockopt(U01, %SOL_SOCKET, %SO_SNDTIMEO, BYVAL VARPTR(U02), SIZEOF(LONG))
END FUNCTION
'===============================================================================
' PURPOSE: Sets a specific, strongly-typed socket option for the receive timeout.
' UNICODE: Not applicable; deals with socket properties.
' PARAMS: U01 (socket handle), U02 (timeout in milliseconds).
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
FUNCTION NW_SetRecvTimeout(BYVAL U01 AS DWORD, BYVAL U02 AS LONG) COMMON AS LONG
' Calls the official 'setsockopt' API function from WinSock2.inc
FUNCTION = setsockopt(U01, %SOL_SOCKET, %SO_RCVTIMEO, BYVAL VARPTR(U02), SIZEOF(LONG))
END FUNCTION
'===============================================================================
' PURPOSE: Gets a specific, strongly-typed socket option for the receive timeout.
' UNICODE: Not applicable; deals with socket properties.
' PARAMS: U01 (socket handle), U02 (receives timeout in milliseconds).
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
FUNCTION NW_GetRecvTimeout(BYVAL U01 AS DWORD, BYREF U02 AS LONG) COMMON AS LONG
LOCAL T01_Len AS LONG
T01_Len = SIZEOF(LONG)
' Calls the official 'getsockopt' API function from WinSock2.inc
FUNCTION = getsockopt(U01, %SOL_SOCKET, %SO_RCVTIMEO, U02, T01_Len)
END FUNCTION
'===============================================================================
' PURPOSE: Enables or disables the SO_REUSEADDR option on a socket.
' UNICODE: Not applicable; this function manipulates a socket's state.
' PARAMS: U01 (socket handle), U02 (flag - %TRUE to enable).
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
' * * * SEALED * * *
FUNCTION NW_SetReuseAddr(BYVAL U01 AS DWORD, BYVAL U02 AS LONG) COMMON AS LONG
FUNCTION = setsockopt(U01, %SOL_SOCKET, %SO_REUSEADDR, BYVAL VARPTR(U02), SIZEOF(LONG))
END FUNCTION
'===============================================================================
' PURPOSE: Sets the SO_LINGER option on a socket to control closing behavior.
' UNICODE: Not applicable; this function manipulates a socket's state.
' PARAMS: U01 (socket handle), U02 (linger UDT).
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
' * * * SEALED * * *
FUNCTION NW_SetLinger(BYVAL U01 AS DWORD, BYREF U02 AS LINGER) COMMON AS LONG
FUNCTION = setsockopt(U01, %SOL_SOCKET, %SO_LINGER, BYVAL VARPTR(U02), SIZEOF(LINGER))
END FUNCTION
'===============================================================================
' PURPOSE: Retrieves the local address and port for a bound socket.
' UNICODE: Not applicable; returns a binary address structure.
' PARAMS: U01 (socket handle), E01_Addr (sockaddr_in Out).
' RETURNS: 0 on success, %NW_SOCKET_ERROR on failure.
FUNCTION NW_GetLocalInfo(BYVAL U01 AS DWORD, BYREF E01_Addr AS sockaddr_in) COMMON AS LONG
LOCAL T01_AddrLen AS LONG
T01_AddrLen = SIZEOF(E01_Addr)
FUNCTION = WS_getsockname(U01, VARPTR(E01_Addr), T01_AddrLen)
END FUNCTION
'===============================================================================
' Part 7: Error Handling and Utilities
'===============================================================================
' PURPOSE: Retrieves the last Winsock error code FOR THIS THREAD.
' UNICODE: Not applicable; no string operations.
' PARAMS: None.
' RETURNS: Last error code (or 0 if clear).
' NOTE: This function is now PURELY an accessor for our internal
' thread-local variable (g_NW_LastThreadError).
' * * * SEALED * * *
FUNCTION NW_GetLastError() COMMON AS LONG
FUNCTION = g_NW_LastThreadError
END FUNCTION
'-------------------------------------------------------------------------------
'########################################################################
' FUNCTION: INT_GetAndStoreError (Internal Helper)
'------------------------------------------------------------------------
' Purpose:
' The internal, central function for all error retrieval.
' It calls the real Winsock API, saves the error to our persistent
' thread-local variable, and returns the error code.
'
' Parameters: None.
' Returns: (LONG) - The error code retrieved from Winsock.
'########################################################################
FUNCTION INT_GetAndStoreError() COMMON AS LONG
LOCAL E01 AS LONG
E01 = WSAGetLastError() ' 1. Get the REAL API error
g_NW_LastThreadError = E01 ' 2. Save it to our persistent variable
FUNCTION = E01 ' 3. Return it for immediate use
END FUNCTION
'-------------------------------------------------------------------------------
' PURPOSE: Sets the last Winsock error code FOR THIS THREAD's internal variable.
' UNICODE: Not applicable; no string operations.
' PARAMS: U01 (LONG In) - Error code to set.
' RETURNS: None.
' NOTE: This updates both the actual WinAPI error state AND our persistent
' thread-local variable (g_NW_LastThreadError).
' * * * SEALED * * *
'-------------------------------------------------------------------------------
SUB NW_SetLastError(BYVAL U01 AS LONG) COMMON
WSASetLastError(U01) ' 1. Set the actual thread error state
g_NW_LastThreadError = U01 ' 2. Set our persistent variable
END SUB
'-------------------------------------------------------------------------------
' PURPOSE: Manually clears the persistent thread-local error variable.
' UNICODE: Not applicable.
' PARAMS: None.
' RETURNS: None.
' NOTE: This MUST be called by the application after handling an error,
' otherwise NW_GetLastError() will continue to return the old, stale error.
'-------------------------------------------------------------------------------
SUB NW_ClearError() COMMON
g_NW_LastThreadError = 0
END SUB
'-------------------------------------------------------------------------------
' PURPOSE: Enables or disables the library's internal debug logging features.
' UNICODE: Not applicable; manipulates a global flag.
' PARAMS: enable (LONG In) - %TRUE to enable, %FALSE to disable.
' RETURNS: None.
SUB NW_EnableDebugLogging(BYVAL U02 AS LONG) COMMON
g_NW_DebugMode = IIF(U02 <> 0, %TRUE, %FALSE)
END SUB
'-------------------------------------------------------------------------------
' PURPOSE: Enables or disables hex dumping of data when debug logging is active.
' UNICODE: Not applicable; manipulates a global flag.
' PARAMS: enable (LONG In) - %TRUE to enable, %FALSE to disable.
' RETURNS: None.
SUB NW_EnableHexDump(BYVAL U02 AS LONG) COMMON
g_NW_HexDumpEnabled = IIF(U02 <> 0, %TRUE, %FALSE)
MCP_SetHexDump(g_NW_HexDumpEnabled)
END SUB
'-------------------------------------------------------------------------------
'########################################################################
' FUNCTION: NW_GaiErrorText
'------------------------------------------------------------------------
' Purpose : Human-readable text for getaddrinfo() error codes.
' Params : c (LONG) - nonzero code returned by WS_getaddrinfo
' Return : STRING (never empty)
' Note : Uses numeric WSA codes so it works even if constants differ.
' * * * SEALED * * *
'########################################################################
FUNCTION NW_GaiErrorText(BYVAL c AS LONG) COMMON AS STRING
LOCAL s AS STRING
SELECT CASE c
CASE 11001 : s = "Host not found" ' %WSAHOST_NOT_FOUND
CASE 11002 : s = "Temporary failure in name resolution" ' %WSATRY_AGAIN
CASE 11003 : s = "Non-recoverable name resolution error" ' %WSANO_RECOVERY
CASE 11004 : s = "No data record of requested type" ' %WSANO_DATA
CASE 10047 : s = "Address family not supported" ' %WSAEAFNOSUPPORT
CASE 10044 : s = "Socket type not supported" ' %WSAESOCKTNOSUPPORT
CASE 10036 : s = "Operation now in progress" ' %WSAEINPROGRESS
CASE 10055 : s = "No buffer space available" ' %WSAENOBUFS
CASE ELSE : s = "getaddrinfo error " & FORMAT$(c)
END SELECT
enx:
FUNCTION = s
END FUNCTION
'===============================================================================
' Part 8: Host and Address Resolution
'===============================================================================
'===============================================================================
' PURPOSE: Resolves a hostname (e.g., "google.com") to an IPv4 or IPv6 address string.
' UNICODE: Hostnames are ANSI/ASCII only as per internet standards.
' PARAMS: S01 (STRING In), S02 (STRING Out), T01 (timeout in seconds).
' RETURNS: %TRUE on success, %FALSE on failure.
' * * * SEALED * * *
FUNCTION NW_ResolveHost(BYVAL S01 AS STRING, BYREF S02 AS STRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL E01 AS ADDRINFOA
LOCAL E02 AS ADDRINFOA PTR
LOCAL E03 AS ADDRINFOA PTR
LOCAL E04 AS SOCKADDR PTR
LOCAL E05 AS LONG
LOCAL E06 AS ASCIIZ * 46
LOCAL E07 AS DWORD
LOCAL E08 AS DOUBLE
' Initialize output
S02 = ""
' Set timeout
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
E08 = TIMER + T01
' Check for timeout
IF TIMER > E08 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ResolveHost: Timeout for hostname " & S01)
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Validate input
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ResolveHost: Empty hostname")
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Set hints for IPv4 or IPv6 and TCP
FILLMEMORY(VARPTR(E01), SIZEOF(E01), 0) ' Clear hints structure
E01.ai_family = %AF_UNSPEC
E01.ai_socktype = %SOCK_STREAM
' Resolve hostname
E05 = WS_getaddrinfo(STRPTR(S01), BYVAL %NULL, VARPTR(E01), E02) ' Pass E02 (ptr variable) ByRef
IF E05 = 0 THEN
E03 = E02
DO WHILE E03 <> %NULL
IF @E03.ai_family = %AF_INET OR @E03.ai_family = %AF_INET6 THEN
E04 = @E03.ai_addr ' E04 is a SOCKADDR PTR (a DWORD value)
E07 = 46 ' Buffer size for IPv4 (16) or IPv6 (46)
' CORRECTED CALL: Pass E04 (the pointer value) BYVAL, not VARPTR(E04)
IF WS_AddrToStringA(E04, @E03.ai_addrlen, BYVAL %NULL, E06, E07) = 0 THEN
S02 = E06
IF LEN(S02) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ResolveHost: Failed to convert WS_AddrToStringA result to string for hostname " & S01)
END IF
WS_freeaddrinfo(E02)
FUNCTION = %FALSE
EXIT FUNCTION
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_ResolveHost: Resolved hostname " & S01 & " to IP " & S02)
END IF
WS_freeaddrinfo(E02)
FUNCTION = %TRUE
EXIT FUNCTION
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ResolveHost: WS_AddrToStringA failed for hostname " & S01 & ", WSAError " & STR$(WSAGetLastError()))
END IF
END IF
END IF
E03 = @E03.ai_next
LOOP
WS_freeaddrinfo(E02)
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ResolveHost: No IPv4 or IPv6 address found for hostname " & S01)
END IF
FUNCTION = %FALSE
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ResolveHost: WS_getaddrinfo failed for hostname " & S01 &" -> " & NW_GaiErrorText(E05) & " (" & STR$(E05) & ")")
END IF
FUNCTION = %FALSE
END IF
END FUNCTION
'===============================================================================
'
'===============================================================================
'===============================================================================
' PURPOSE: Resolves an IPv4 or IPv6 address string to its corresponding hostname (rDNS).
' UNICODE: Hostnames are ANSI/ASCII only as per internet standards.
' PARAMS: S01 (STRING In), S02 (STRING Out), T01 (timeout in seconds).
' RETURNS: %TRUE on success, %FALSE on failure.
' * * * SEALED * * *
FUNCTION NW_ReverseResolve (BYVAL S01 AS STRING, BYREF S02 AS STRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL E01 AS ADDRINFOA
LOCAL E02 AS ADDRINFOA PTR
LOCAL E03 AS LONG
LOCAL E04 AS ASCIIZ * 256
LOCAL E05 AS DOUBLE
LOCAL R01 AS LONG
' Initialize output and result
S02 = ""
R01 = %FALSE
' Set timeout
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
E05 = TIMER + T01
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_ReverseResolve: Timeout set to " & STR$(T01) & " seconds for IP " & S01)
X_AU "[DEBUG] NW_ReverseResolve: Timeout set to " & STR$(T01) & " seconds for IP " & S01
END IF
' Check for timeout
IF TIMER > E05 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ReverseResolve: Timeout for IP " & S01)
X_AU "[ERROR] NW_ReverseResolve: Timeout for IP " & S01
END IF
GOTO enx
END IF
' Validate input
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ReverseResolve: Empty IP address")
X_AU "[ERROR] NW_ReverseResolve: Empty IP address"
END IF
GOTO enx
END IF
' Set hints for numeric host
FILLMEMORY(VARPTR(E01), SIZEOF(E01), 0)
E01.ai_family = %AF_UNSPEC
E01.ai_socktype = 0
E01.ai_flags = %AI_NUMERICHOST
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_ReverseResolve: Hints set: family=" & STR$(E01.ai_family) & ", socktype=" & STR$(E01.ai_socktype) & ", flags=" & STR$(E01.ai_flags))
X_AU "[DEBUG] NW_ReverseResolve: Hints set: family=" & STR$(E01.ai_family) & ", socktype=" & STR$(E01.ai_socktype) & ", flags=" & STR$(E01.ai_flags)
END IF
' Resolve address to hostname
E03 = WS_getaddrinfo(STRPTR(S01), %NULL, VARPTR(E01), E02)
IF E03 <> 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ReverseResolve: WS_getaddrinfo failed for IP " & S01 & " -> " & NW_GaiErrorText(E03) & " (" & STR$(E03) & ")")
X_AU "[ERROR] NW_ReverseResolve: WS_getaddrinfo failed for IP " & S01 & ", WSAError " & STR$(E03) & " (" & ERROR$(E03) & ")"
END IF
GOTO enx
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_ReverseResolve: getaddrinfo succeeded for IP " & S01)
X_AU "[DEBUG] NW_ReverseResolve: getaddrinfo succeeded for IP " & S01
END IF
' Get hostname
E03 = WS_getnameinfo(@E02.ai_addr, @E02.ai_addrlen, E04, SIZEOF(E04), BYVAL %NULL, 0, 0)
IF E03 = 0 THEN
S02 = E04
IF LEN(S02) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ReverseResolve: Empty hostname returned for IP " & S01)
X_AU "[ERROR] NW_ReverseResolve: Empty hostname returned for IP " & S01
END IF
GOTO enx
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_ReverseResolve: Resolved IP " & S01 & " to hostname " & S02)
X_AU "[DEBUG] NW_ReverseResolve: Resolved IP " & S01 & " to hostname " & S02
END IF
R01 = %TRUE
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ReverseResolve: WS_getnameinfo failed for IP " & S01 & ", WSAError " & STR$(E03) & " (" & ERROR$(E03) & ")")
X_AU "[ERROR] NW_ReverseResolve: WS_getnameinfo failed for IP " & S01 & ", WSAError " & STR$(E03) & " (" & ERROR$(E03) & ")"
END IF
END IF
enx:
' Cleanup
IF E02 THEN WS_freeaddrinfo(E02)
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_ReverseResolve: Cleanup complete, result=" & STR$(R01))
X_AU "[DEBUG] NW_ReverseResolve: Cleanup complete, result=" & STR$(R01)
END IF
FUNCTION = R01
END FUNCTION
'===============================================================================
'===============================================================================
' PURPOSE: Converts a SOCKADDR_STORAGE structure to a printable "ip:port" string.
' UNICODE: The output string is ANSI/ASCII as it represents an IP and port.
' PARAMS: E01 (SOCKADDR_STORAGE In), salen (actual address length In), S01 (STRING Out).
' RETURNS: %TRUE on success, %FALSE on failure.
' * * * SEALED * * *
FUNCTION NW_AddrToString(BYVAL E01 AS SOCKADDR_STORAGE, BYVAL salen AS LONG, BYREF S01 AS STRING) COMMON AS LONG
LOCAL E02 AS ASCIIZ * 46
LOCAL E03 AS DWORD
LOCAL E04 AS LONG
' Initialize output
S01 = ""
' Validate input
IF E01.ss_family <> %AF_INET AND E01.ss_family <> %AF_INET6 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_AddrToString: Invalid or unsupported address family " & STR$(E01.ss_family))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Convert address to string
E03 = SIZEOF(E02) ' Buffer size for IPv4 (16) or IPv6 (46)
IF WS_AddrToStringA(VARPTR(E01), salen, BYVAL %NULL, E02, E03) = 0 THEN
S01 = E02
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_AddrToString: Failed to convert WS_AddrToStringA result to string")
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_AddrToString: Converted address to " & S01 & ", family " & STR$(E01.ss_family))
END IF
FUNCTION = %TRUE
ELSE
E04 = WSAGetLastError()
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E04
CASE %WSAEINVAL
errMsg = "Invalid arguments"
CASE %WSAEFAULT
errMsg = "Invalid address buffer"
CASE ELSE
errMsg = "WS_AddrToStringA failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_AddrToString: " & errMsg & ", WSAError " & STR$(E04))
END IF
FUNCTION = %FALSE
END IF
END FUNCTION
'===============================================================================
'
'===============================================================================
'===============================================================================
' PURPOSE: Converts a printable "ip:port" string into a SOCKADDR_STORAGE structure.
' UNICODE: The input string should be ANSI/ASCII.
' PARAMS: S01 (STRING In), E01 (SOCKADDR_STORAGE Out), S02 (LONG Out for addrlen), T01 (timeout in seconds).
' RETURNS: %TRUE on success, %FALSE on failure.
' * * * SEALED * * *
FUNCTION NW_StringToAddr (BYVAL S01 AS STRING, BYREF E01 AS SOCKADDR_STORAGE, BYREF S02 AS LONG, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL L01 AS STRING, L02 AS STRING, N01 AS LONG, N02 AS LONG, N03 AS LONG
LOCAL E02 AS ADDRINFOA, E03 AS ADDRINFOA PTR, R01 AS LONG, D01 AS DOUBLE
LOCAL N04 AS DWORD
' Initialize output and result
FILLMEMORY(VARPTR(E01), SIZEOF(E01), 0)
S02 = 0
R01 = %FALSE
' Set timeout
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
D01 = TIMER + T01
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_StringToAddr: Timeout set to " & STR$(T01) & " seconds for IP:port " & S01)
X_AU "[DEBUG] NW_StringToAddr: Timeout set to " & STR$(T01) & " seconds for IP:port " & S01
END IF
' Check for timeout
IF TIMER > D01 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_StringToAddr: Timeout for IP:port " & S01)
X_AU "[ERROR] NW_StringToAddr: Timeout for IP:port " & S01
END IF
GOTO enx
END IF
' Validate input
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_StringToAddr: Empty IP:port string")
X_AU "[ERROR] NW_StringToAddr: Empty IP:port string"
END IF
GOTO enx
END IF
' Parse [IPv6]:port or IPv4:port or bare literal
N01 = INSTR(S01, "[")
N02 = INSTR(S01, "]")
IF N01 AND N02 THEN
L01 = MID$(S01, N01 + 1, N02 - N01 - 1)
N03 = INSTR(N02 + 1, S01, ":")
IF N03 THEN L02 = MID$(S01, N03 + 1)
ELSE
N03 = INSTR(-1, S01, ":") ' last colon (safe for IPv4)
IF N03 THEN
L01 = LEFT$(S01, N03 - 1)
L02 = MID$(S01, N03 + 1)
ELSE
L01 = S01
L02 = ""
END IF
END IF
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_StringToAddr: Parsed IP='" & L01 & "', port='" & L02 & "'")
X_AU "[DEBUG] NW_StringToAddr: Parsed IP='" & L01 & "', port='" & L02 & "'"
END IF
' Set hints
FILLMEMORY(VARPTR(E02), SIZEOF(E02), 0)
E02.ai_family = %AF_UNSPEC
E02.ai_socktype = %SOCK_STREAM
E02.ai_flags = %AI_NUMERICHOST
IF LEN(L02) THEN E02.ai_flags = E02.ai_flags OR %AI_NUMERICSERV
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_StringToAddr: Hints set: family=" & STR$(E02.ai_family) & ", socktype=" & STR$(E02.ai_socktype) & ", flags=" & STR$(E02.ai_flags))
X_AU "[DEBUG] NW_StringToAddr: Hints set: family=" & STR$(E02.ai_family) & ", socktype=" & STR$(E02.ai_socktype) & ", flags=" & STR$(E02.ai_flags)
END IF
' Resolve address
IF LEN(L02) THEN
N04 = STRPTR(L02)
ELSE
N04 = %NULL
END IF
N03 = WS_getaddrinfo(STRPTR(L01), N04, VARPTR(E02), E03)
IF N03 <> 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_StringToAddr: WS_getaddrinfo failed for IP:port " & S01 & " -> " & NW_GaiErrorText(N03) & " (" & STR$(N03) & ")")
X_AU "[ERROR] NW_StringToAddr: WS_getaddrinfo failed for IP:port " & S01 & ", WSAError " & STR$(N03) & " (" & ERROR$(N03) & ")"
END IF
GOTO enx
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_StringToAddr: getaddrinfo succeeded for IP:port " & S01)
X_AU "[DEBUG] NW_StringToAddr: getaddrinfo succeeded for IP:port " & S01
END IF
' Copy address to output
S02 = MIN&(SIZEOF(E01), @E03.ai_addrlen)
CALL MoveMemory(BYVAL VARPTR(E01), BYVAL @E03.ai_addr, S02)
R01 = %TRUE
enx:
' Cleanup
IF E03 THEN WS_freeaddrinfo(E03)
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_StringToAddr: Cleanup complete, result=" & STR$(R01))
X_AU "[DEBUG] NW_StringToAddr: Cleanup complete, result=" & STR$(R01)
END IF
FUNCTION = R01
END FUNCTION
'===============================================================================
' Part 9: Enhanced High-Level Functions
'===============================================================================
' PURPOSE: Retrieves the remote address (IP string) for a connected socket.
' UNICODE: The output IP string is ANSI/ASCII.
' PARAMS: U01 (socket handle), S01 (STRING Out), T01 (timeout in seconds).
' RETURNS: %TRUE on success, %FALSE on failure.
' * * * SEALED * * *
FUNCTION NW_GetPeerInfo(BYVAL U01 AS DWORD, BYREF S01 AS STRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL E01 AS SOCKADDR_STORAGE
LOCAL E02 AS LONG
LOCAL E03 AS ASCIIZ * 46
LOCAL E04 AS DWORD
LOCAL E05 AS DOUBLE
' Initialize output
S01 = ""
' Set timeout
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
E05 = TIMER + T01
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_GetPeerInfo: Timeout set to " & STR$(T01) & " seconds for socket " & STR$(U01))
END IF
' Check for timeout
IF TIMER > E05 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_GetPeerInfo: Timeout on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Get peer address
E02 = SIZEOF(E01)
IF WS_GetPeerName(U01, BYVAL VARPTR(E01), E02) = 0 THEN
E04 = 46 ' Buffer size for IPv4 (16) or IPv6 (46)
IF WS_AddrToStringA(VARPTR(E01), E02, BYVAL %NULL, E03, E04) = 0 THEN
S01 = E03
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_GetPeerInfo: Failed to convert WS_AddrToStringA result to string on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_GetPeerInfo: Retrieved IP " & S01 & " for socket " & STR$(U01) & ", family " & STR$(E01.ss_family))
END IF
FUNCTION = %TRUE
ELSE
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE WSAGetLastError()
CASE %WSAEINVAL
errMsg = "Invalid arguments"
CASE %WSAEFAULT
errMsg = "Invalid address buffer"
CASE ELSE
errMsg = "WS_AddrToStringA failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_GetPeerInfo: " & errMsg & " on socket " & STR$(U01) & ", WSAError " & STR$(WSAGetLastError()))
END IF
FUNCTION = %FALSE
END IF
ELSE
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg2 AS STRING
SELECT CASE WSAGetLastError()
CASE %WSAEINVAL
errMsg2 = "Invalid arguments"
CASE %WSAENOTCONN
errMsg2 = "Socket not connected"
CASE %WSAEFAULT
errMsg2 = "Invalid address buffer"
CASE ELSE
errMsg2 = "WS_GetPeerName failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_GetPeerInfo: " & errMsg2 & " on socket " & STR$(U01) & ", WSAError " & STR$(WSAGetLastError()))
END IF
FUNCTION = %FALSE
END IF
END FUNCTION
'===============================================================================
' PURPOSE: Retrieves the IP address string and port number for a connected peer.
' UNICODE: The output IP string is ANSI/ASCII.
' PARAMS: U01 (socket handle), S01 (STRING Out for IP), N01 (LONG Out for port), T01 (timeout in seconds).
' RETURNS: %TRUE on success, %FALSE on failure.
' * * * SEALED * * *
#IF 1
FUNCTION NW_GetClientInfo(BYVAL U01 AS DWORD, BYREF S01 AS STRING, BYREF N01 AS LONG, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL E01 AS SOCKADDR_STORAGE
LOCAL E02 AS LONG
LOCAL E03 AS LONG
LOCAL E04 AS DOUBLE
S01 = ""
N01 = 0
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
E04 = TIMER + T01
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_GetClientInfo: Timeout set to " & STR$(T01) & " seconds for socket " & STR$(U01))
END IF
IF TIMER > E04 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_GetClientInfo: Timeout on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
IF NW_GetPeerInfo(U01, S01, T01) = %TRUE THEN
E02 = SIZEOF(E01)
IF WS_GetPeerName(U01, BYVAL VARPTR(E01), E02) = 0 THEN
IF E01.ss_family = %AF_INET THEN
LOCAL E05 AS sockaddr_in PTR
E05 = VARPTR(E01)
N01 = WS_ntohs(@E05.sin_port)
ELSEIF E01.ss_family = %AF_INET6 THEN
LOCAL E06 AS SOCKADDR_IN6 PTR ' Use SOCKADDR_IN6 from ws2ipdef.inc
E06 = VARPTR(E01)
N01 = WS_ntohs(@E06.sin6_port)
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_GetClientInfo: Unsupported address family " & STR$(E01.ss_family) & " on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_GetClientInfo: Retrieved IP " & S01 & " and port " & STR$(N01) & " for socket " & STR$(U01) & ", family " & STR$(E01.ss_family))
END IF
FUNCTION = %TRUE
ELSE
E03 = WSAGetLastError()
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E03
CASE %WSAEINVAL
errMsg = "Invalid arguments"
CASE %WSAENOTCONN
errMsg = "Socket not connected"
CASE %WSAEFAULT
errMsg = "Invalid address buffer"
CASE ELSE
errMsg = "WS_GetPeerName failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_GetClientInfo: " & errMsg & " on socket " & STR$(U01) & ", WSAError " & STR$(E03))
END IF
FUNCTION = %FALSE
END IF
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_GetClientInfo: NW_GetPeerInfo failed on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
END IF
END FUNCTION
#ELSE
FUNCTION NW_GetClientInfo(BYVAL U01 AS DWORD, BYREF S01 AS STRING, BYREF N01 AS LONG, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL E01 AS SOCKADDR_STORAGE
LOCAL E02 AS LONG
LOCAL E03 AS LONG
LOCAL E04 AS DOUBLE
' Initialize outputs
S01 = ""
N01 = 0
' Set timeout
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
E04 = TIMER + T01
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_GetClientInfo: Timeout set to " & STR$(T01) & " seconds for socket " & STR$(U01))
END IF
' Check for timeout
IF TIMER > E04 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_GetClientInfo: Timeout on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Get IP address
IF NW_GetPeerInfo(U01, S01, T01) = %TRUE THEN
' Get port
E02 = SIZEOF(E01)
IF WS_GetPeerName(U01, BYVAL VARPTR(E01), E02) = 0 THEN
IF E01.ss_family = %AF_INET THEN
LOCAL E05 AS sockaddr_in PTR
E05 = VARPTR(E01)
N01 = WS_ntohs(@E05.sin_port)
ELSEIF E01.ss_family = %AF_INET6 THEN
LOCAL E06 AS sockaddr_in6 PTR
E06 = VARPTR(E01)
N01 = WS_ntohs(@E06.sin6_port)
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_GetClientInfo: Unsupported address family " & STR$(E01.ss_family) & " on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_GetClientInfo: Retrieved IP " & S01 & " and port " & STR$(N01) & " for socket " & STR$(U01) & ", family " & STR$(E01.ss_family))
END IF
FUNCTION = %TRUE
ELSE
E03 = WSAGetLastError()
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E03
CASE %WSAEINVAL
errMsg = "Invalid arguments"
CASE %WSAENOTCONN
errMsg = "Socket not connected"
CASE %WSAEFAULT
errMsg = "Invalid address buffer"
CASE ELSE
errMsg = "WS_GetPeerName failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_GetClientInfo: " & errMsg & " on socket " & STR$(U01) & ", WSAError " & STR$(E03))
END IF
FUNCTION = %FALSE
END IF
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_GetClientInfo: NW_GetPeerInfo failed on socket " & STR$(U01))
END IF
FUNCTION = %FALSE
END IF
END FUNCTION
#ENDIF
'===============================================================================
' PURPOSE: Monitors an array of sockets for readability using the select() model.
' UNICODE: Not applicable; deals with socket handles and status flags.
' PARAMS: socketArray() (In), readStatus() (Out), timeoutSec (In).
' RETURNS: %TRUE if one or more sockets are ready, %FALSE on timeout or error.
'
FUNCTION NW_MonitorMultipleSockets(BYREF socketArray() AS DWORD, BYREF readStatus() AS LONG, BYVAL timeoutSec AS LONG) COMMON AS LONG
LOCAL readSet AS fd_setstruc, tv AS timeval, i AS LONG, result AS LONG, errCode AS LONG
FUNCTION = %FALSE ' Default to failure/timeout
IF UBOUND(socketArray) - LBOUND(socketArray) + 1 > %FD_SETSIZE THEN
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_MonitorMultipleSockets: Too many sockets, exceeds FD_SETSIZE")
EXIT FUNCTION
END IF
FD_ZERO(readSet)
FOR i = LBOUND(socketArray) TO UBOUND(socketArray)
IF socketArray(i) <> %INVALID_SOCKET THEN FD_SET(socketArray(i), readSet)
NEXT i
tv.tv_sec = timeoutSec
tv.tv_usec = 0
REDIM readStatus(LBOUND(socketArray) TO UBOUND(socketArray))
result = NW_WSASelect(0, readSet, BYVAL %NULL, BYVAL %NULL, tv)
IF result > 0 THEN
FOR i = LBOUND(socketArray) TO UBOUND(socketArray)
readStatus(i) = IIF(socketArray(i) <> %INVALID_SOCKET AND FD_ISSET(socketArray(i), readSet), 1, 0)
NEXT i
FUNCTION = %TRUE
ELSEIF result = %SOCKET_ERROR THEN
errCode = WSAGetLastError()
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_MonitorMultipleSockets: select failed, WSAError " & STR$(errCode))
ELSE ' result = 0, timeout
IF g_MCP_LogLevel > 1 THEN MCP_Log(%MCP_LOG_DEBUG, "NW_MonitorMultipleSockets: Timeout after " & STR$(timeoutSec) & " seconds")
END IF
END FUNCTION
'--------------------------------------------------------------------------------------------------
' PURPOSE: Logs a socket event message, typically used with WSAAsyncSelect.
' UNICODE: The log message is a STRING, handled by MCP_Log (must support UTF-8/ASCII).
' PARAMS: U01 (socket handle), N01 (event type, e.g., %FD_READ), N02 (error code).
' RETURNS: None.
SUB NW_LogSocketEvent(BYVAL U01 AS DWORD, BYVAL N01 AS LONG, BYVAL N02 AS LONG)
LOCAL S02 AS STRING
' Validate inputs
IF U01 = %INVALID_SOCKET THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_LogSocketEvent: Invalid socket handle")
END IF
EXIT SUB
END IF
IF N02 < 0 OR N02 > %WSABASEERR + 10000 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_LogSocketEvent: Suspicious error code " & STR$(N02) & " for socket " & STR$(U01))
END IF
END IF
' Map event type to description
SELECT CASE N01
CASE %FD_READ
S02 = "Read event"
CASE %FD_WRITE
S02 = "Write event"
CASE %FD_CLOSE
S02 = "Close event"
CASE %FD_ACCEPT
S02 = "Accept event"
CASE %FD_CONNECT
S02 = "Connect event"
CASE %FD_OOB
S02 = "Out-of-band data event"
CASE %FD_QOS
S02 = "Quality of service event"
CASE %FD_GROUP_QOS
S02 = "Group quality of service event"
CASE %FD_ROUTING_INTERFACE_CHANGE
S02 = "Routing interface change event"
CASE %FD_ADDRESS_LIST_CHANGE
S02 = "Address list change event"
CASE ELSE
S02 = "Unknown event " & FORMAT$(N01)
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_LogSocketEvent: Invalid event type " & STR$(N01) & " for socket " & STR$(U01))
END IF
END SELECT
' Check for non-ASCII characters in log message
IF INSTR(S02, ANY CHR$(128 TO 255)) THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_LogSocketEvent: Non-ASCII characters detected in log message for socket " & STR$(U01))
END IF
END IF
' Log the event
MCP_Log(IIF(N02 = 0, %MCP_LOG_INFO, %MCP_LOG_ERROR), _
"NW_LogSocketEvent: s=" & FORMAT$(U01) & " " & S02 & _
IIF$(N02 > 0, " WSA=" & FORMAT$(N02), ""))
END SUB
'===============================================================================
'
'===============================================================================
'DECLARE FUNCTION WS_Send LIB "Ws2_32.dll" ALIAS "send" (BYVAL s AS DWORD, BYVAL buf AS DWORD, BYVAL nLen AS LONG, BYVAL flags AS LONG) AS LONG
' PURPOSE: Sends a message to every valid socket in a given array.
' UNICODE: Byte-stream safe. The message STRING must be properly encoded (e.g., UTF-8 or ASCII).
' PARAMS: A01() (socket array), S01 (STRING message), T01 (timeout in seconds).
' RETURNS: %TRUE if any bytes were sent successfully, %FALSE otherwise.
FUNCTION NW_BroadcastMessage(BYREF A01() AS DWORD, BYREF S01 AS STRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL N01 AS LONG ' Bytes sent to current socket
LOCAL N02 AS LONG ' Total bytes sent
LOCAL N03 AS LONG ' Loop index
LOCAL E01 AS LONG ' Error code
' Initialize
N02 = 0
' Validate input
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_BroadcastMessage: Empty message")
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
' Check for non-ASCII characters in message (optional, if ASCII-only protocol)
IF INSTR(S01, ANY CHR$(128 TO 255)) THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_BroadcastMessage: Non-ASCII characters detected in message")
END IF
END IF
' Set timeout
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_BroadcastMessage: Timeout set to " & STR$(T01) & " seconds for broadcasting message of " & STR$(LEN(S01)) & " bytes")
END IF
' Send message to all valid sockets
FOR N03 = LBOUND(A01) TO UBOUND(A01)
IF A01(N03) <> %INVALID_SOCKET THEN
N01 = NW_Send(A01(N03), S01, T01)
IF N01 = %NW_SOCKET_ERROR THEN
E01 = WSAGetLastError()
IF g_MCP_LogLevel > 0 THEN
LOCAL errMsg AS STRING
SELECT CASE E01
CASE %WSAEWOULDBLOCK
errMsg = "Non-blocking operation would block"
CASE %WSAENOTCONN
errMsg = "Socket not connected"
CASE %WSAECONNRESET
errMsg = "Connection reset by peer"
CASE ELSE
errMsg = "NW_Send failed"
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_BroadcastMessage: " & errMsg & " on socket " & STR$(A01(N03)) & ", WSAError " & STR$(E01))
END IF
ELSE
N02 = N02 + N01
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_BroadcastMessage: Sent " & STR$(N01) & " bytes to socket " & STR$(A01(N03)))
END IF
END IF
END IF
NEXT
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_BroadcastMessage: Total " & STR$(N02) & " bytes sent across all sockets")
END IF
FUNCTION = IIF&(N02 > 0, %TRUE, %FALSE)
END FUNCTION
'===============================================================================
' PURPOSE: Reads a single CRLF or LF-terminated line of text from a socket.
' UNICODE: Byte-stream safe. Returns a STRING of raw bytes for the caller to decode.
' PARAMS: U01 (socket handle), S01 (receives the line of text), T01 (timeout in seconds).
' RETURNS: %TRUE on success (line received), %FALSE on disconnect, error, or timeout.
' NOTE: This implementation reads byte-by-byte to ensure it NEVER reads past the EOL marker,
' preventing data theft from subsequent socket receive calls. It does NOT use the threaded buffer.
FUNCTION NW_LineInput(BYVAL U01 AS DWORD, BYREF S01 AS STRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL B01 AS BYTE ' Single byte buffer
LOCAL R01 AS LONG ' Return value (%TRUE/%FALSE)
LOCAL E01 AS DOUBLE ' Timeout marker
LOCAL E02 AS LONG ' WSA Error Code
LOCAL N01 AS LONG ' Result of WS_Recv
LOCAL N02 AS LONG ' Max line length (safety break)
S01 = ""
R01 = %FALSE
N02 = 65536 ' 64KB max line length safety break
' Set absolute timeout
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
E01 = TIMER + T01
DO
' 1. Check master timeout
IF TIMER > E01 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_LineInput: Timeout reading line on socket " & STR$(U01))
END IF
GOTO enx ' Fail (R01 = %FALSE)
END IF
' 2. Attempt to read one single byte
N01 = WS_Recv(U01, VARPTR(B01), 1, 0)
IF N01 = 1 THEN
' 3. Got one byte
IF B01 = %LF THEN ' Found Line Feed (End of line)
R01 = %TRUE ' Success
EXIT DO
ELSEIF B01 <> %CR THEN ' Ignore Carriage Return, append others
S01 &= CHR$(B01)
END IF
ELSEIF N01 = %SOCKET_ERROR THEN
' 4. Socket Error
E02 = WSAGetLastError()
IF E02 = %WSAEWOULDBLOCK THEN
SLEEP 1 ' Socket buffer is empty, yield CPU and loop again
ITERATE DO
ELSE
' 5. Fatal Error (e.g., Connection Reset)
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_LineInput: WS_Recv failed, WSAError " & STR$(E02) & " on socket " & STR$(U01))
END IF
GOTO enx ' Fail (R01 = %FALSE)
END IF
ELSEIF N01 = 0 THEN
' 6. Graceful Disconnect (EOF)
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_LineInput: Graceful disconnect on socket " & STR$(U01))
END IF
IF LEN(S01) > 0 THEN R01 = %TRUE ' Return partial line if we have one
GOTO enx
END IF
' 7. Safety break (Line too long)
IF LEN(S01) > N02 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_LineInput: Line buffer overflow (> 64KB) on socket " & STR$(U01))
END IF
GOTO enx ' Fail (R01 = %FALSE)
END IF
LOOP
enx:
FUNCTION = R01
END FUNCTION
'===============================================================================
'
'===============================================================================
' PURPOSE: Sends a Unicode WSTRING line with UTF-16LE CRLF (0D 00 0A 00).
' UNICODE: Fully Unicode safe. Handles WSTRING data directly.
' PARAMS: U01 (LONG In) - Socket handle, U02 (WSTRING In) - Text to send, T01 (timeout in seconds).
' RETURNS: Number of bytes sent, or %NW_SOCKET_ERROR on failure.
' * * * SEALED * * *
FUNCTION NW_PrintW(BYVAL U01 AS LONG, BYREF U02 AS WSTRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL L01 AS WSTRING ' Wide buffer (data + CRLF)
LOCAL B01 AS STRING ' Byte buffer (for sending)
LOCAL N01 AS LONG ' Send result
LOCAL E01 AS LONG ' Error code
' LOCAL N02 AS LONG ' (No longer needed, handled by utility function)
IF LEN(U02) = 0 THEN
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_PrintW: Empty WSTRING on socket " & STR$(U01))
END IF
FUNCTION = 0
EXIT FUNCTION
END IF
' Append the UTF-16LE CRLF (Wide CHAR 13, Wide CHAR 10)
L01 = U02 & $$CRLF
' --- REFACTORED LOGIC: Use shared utility ---
' Convert WSTRING to raw byte STRING using the shared utility function
B01 = INT_WstrToByteString(L01)
' --- END REFACTOR ---
' Send the raw byte buffer
N01 = NW_Send(U01, B01, T01)
IF N01 = LEN(B01) THEN
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_PrintW: Successfully sent " & STR$(N01) & " bytes (UTF-16LE) on socket " & STR$(U01))
END IF
FUNCTION = N01
ELSE
E01 = WSAGetLastError()
IF g_MCP_LogLevel > 0 THEN
LOCAL S01 AS STRING
SELECT CASE E01
CASE %WSAEWOULDBLOCK
S01 = "Non-blocking operation would block"
CASE %WSAENOTCONN
S01 = "Socket not connected"
CASE %WSAECONNRESET
S01 = "Connection reset by peer"
CASE ELSE
S01 = "NW_Send failed, WSAError " & STR$(E01)
END SELECT
MCP_Log(%MCP_LOG_ERROR, "NW_PrintW: " & S01 & " on socket " & STR$(U01))
END IF
FUNCTION = %NW_SOCKET_ERROR
END IF
END FUNCTION
'===============================================================================
' ===============================================================================
' PURPOSE: Reads a UTF-16LE line from a socket, terminated by CRLF (0D 00 0A 00).
' UNICODE: Returns a WSTRING with the received line.
' PARAMS: U01 (socket handle), U02 (WSTRING Out), T01 (timeout in seconds).
' RETURNS: >0 (chars), 0 (disconnect), %NW_SOCKET_ERROR (error/timeout).
' * * * SEALED * * *
FUNCTION NW_LineInputW(BYVAL U01 AS LONG, BYREF U02 AS WSTRING, BYVAL T01 AS LONG) COMMON AS LONG
LOCAL S_WCRLF AS STRING
LOCAL D01 AS DOUBLE
LOCAL N01 AS LONG
LOCAL N02 AS LONG
LOCAL L01 AS STRING
LOCAL R01 AS LONG
' Initialize
U02 = ""
R01 = %NW_SOCKET_ERROR
S_WCRLF = CHR$(&H0D, &H00, &H0A, &H00)
IF T01 <= 0 THEN T01 = %NW_TIMEOUT_DEFAULT / 1000
D01 = TIMER + T01
' If this thread is now using a different socket, clear this thread's private buffer.
IF s_Socket <> U01 THEN
s_Buffer = ""
s_Socket = U01
END IF
DO
' 1. Check this thread's private buffer first
N02 = INSTR(s_Buffer, S_WCRLF)
IF N02 > 0 THEN
L01 = LEFT$(s_Buffer, N02 - 1)
s_Buffer = MID$(s_Buffer, N02 + 4) ' Remove line and CRLF
IF (LEN(L01) MOD 2) <> 0 AND g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_WARN, "NW_LineInputW: Odd byte count (" & STR$(LEN(L01)) & ") found before EOL. Data may be corrupt.")
END IF
U02 = STRING$(LEN(L01) / 2, 0)
MoveMemory(STRPTR(U02), STRPTR(L01), LEN(L01))
R01 = LEN(U02)
GOTO enx
END IF
IF TIMER > D01 THEN
IF g_MCP_LogLevel > 0 THEN MCP_Log(%MCP_LOG_ERROR, "NW_LineInputW: Master timeout on socket " & STR$(U01))
GOTO enx
END IF
' 3. Buffer has partial/no data, read more from socket
L01 = SPACE$(4096)
N01 = NW_Recv(U01, L01, 4096, 0) ' Use a small timeout, master loop handles the rest
SELECT CASE N01
CASE IS > 0
' Data received, append to this thread's private buffer
s_Buffer = s_Buffer & L01
CASE 0 ' Graceful disconnect
R01 = 0
GOTO enx
CASE %NW_WOULDBLOCK
SLEEP 10
ITERATE LOOP
CASE %NW_SOCKET_ERROR ' Fatal error
GOTO enx
END SELECT
LOOP
enx:
' If connection is closed or errored, clear the buffer for the next call on this thread
IF R01 <= 0 THEN
s_Buffer = ""
s_Socket = 0
END IF
FUNCTION = R01
END FUNCTION
'===============================================================================
' PURPOSE: Wrapper for Winsock select() function.
' PARAMS: nfds (ignored on Windows), readfds, writefds, exceptfds, timeout.
' RETURNS: Number of sockets ready, 0 on timeout, %SOCKET_ERROR on error.
FUNCTION NW_WSASelect(BYVAL nfds AS LONG, _
BYREF readfds AS fd_setstruc, _
BYREF writefds AS fd_setstruc, _
BYREF exceptfds AS fd_setstruc, _
BYREF TM AS timeval) COMMON AS LONG
G_REG
R01=WS_WSASELECT(nfds, readfds, writefds, exceptfds, TM)
FUNCTION = R01
END FUNCTION
'===============================================================================
' FUNCTION: NW_CreateTestFile
' PURPOSE: Creates a test file filled with 'X' characters for benchmarking or demos.
' PARAMS: filePath (STRING In), fileSize (QUAD In).
' RETURNS: %TRUE on success, %FALSE on failure.
'===============================================================================
FUNCTION NW_CreateTestFile(BYVAL filePath AS STRING, BYVAL fileSize AS QUAD) COMMON AS LONG
LOCAL ff AS LONG
LOCAL chunk AS STRING
LOCAL i AS QUAD
LOCAL remaining AS QUAD
LOCAL result AS LONG
G_S01
result = %FALSE
chunk = STRING$(65536, "X") ' 64KB chunk
ff = FREEFILE
OPEN filePath FOR BINARY AS ff
IF ERR THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_CreateTestFile: Failed to create file '" & filePath & "', Error " & STR$(ERR))
END IF
GOTO cleanup
END IF
i = 0
WHILE i < fileSize
remaining = fileSize - i
IF remaining >= 65536 THEN
PUT ff, , chunk
i = i + 65536
ELSE
S01=LEFT$(chunk, remaining)
PUT ff, , S01
i = i + remaining
END IF
WEND
result = %TRUE
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_CreateTestFile: Successfully created file '" & filePath & "' of size " & STR$(fileSize) & " bytes.")
END IF
cleanup:
IF ff THEN CLOSE ff
FUNCTION = result
END FUNCTION
'===============================================================================
' SUB: NW_PauseForUser
' PURPOSE: Pauses execution and waits for user input (console utility).
' PARAMS: message (STRING In) - Message to display.
' RETURNS: None.
'===============================================================================
SUB NW_PauseForUser(BYVAL message AS STRING) COMMON
X_AU message
X_AU "Press any key to continue..."
? "" ' Wait for keypress
END SUB
'===============================================================================
' FUNCTION: NW_WaitForThread
' PURPOSE: Waits for a thread to finish with a timeout, retrieving its result.
' PARAMS: threadHandle (DWORD In), timeoutSec (LONG In), result (LONG Out).
' RETURNS: %TRUE if thread finished successfully, %FALSE on timeout or error.
'===============================================================================
FUNCTION NW_WaitForThread(BYVAL threadHandle AS DWORD, BYVAL timeoutSec AS LONG, BYREF result AS LONG) COMMON AS LONG
LOCAL waitResult AS LONG
LOCAL apiResult AS LONG
result = %NW_SOCKET_ERROR
IF threadHandle = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_WaitForThread: Invalid thread handle.")
END IF
FUNCTION = %FALSE
EXIT FUNCTION
END IF
waitResult = WaitForSingleObject(threadHandle, timeoutSec * 1000)
SELECT CASE waitResult
CASE %WAIT_OBJECT_0
IF GetExitCodeThread(threadHandle, result) THEN
IF result = &H103& THEN ' STILL_ACTIVE (shouldn't happen after WAIT_OBJECT_0, but just in case)
result = %NW_SOCKET_ERROR
FUNCTION = %FALSE
ELSE
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_WaitForThread: Thread finished with result " & STR$(result))
END IF
THREAD CLOSE threadHandle TO apiResult
FUNCTION = %TRUE
END IF
ELSE
apiResult = GetLastError()
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_WaitForThread: GetExitCodeThread failed, Error " & STR$(apiResult))
END IF
THREAD CLOSE threadHandle TO apiResult
FUNCTION = %FALSE
END IF
CASE %WAIT_TIMEOUT
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_WaitForThread: Timeout waiting for thread.")
END IF
FUNCTION = %FALSE
CASE ELSE
apiResult = GetLastError()
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_WaitForThread: WaitForSingleObject failed, Error " & STR$(apiResult))
END IF
FUNCTION = %FALSE
END SELECT
END FUNCTION
'===============================================================================
' FUNCTION: NW_ResolveAndValidateHost
' PURPOSE: Resolves a hostname and validates the result (non-empty, valid IP).
' PARAMS: hostname (STRING In), ipAddress (STRING Out), timeoutSec (LONG In).
' RETURNS: %TRUE if resolved and validated, %FALSE otherwise.
'===============================================================================
FUNCTION NW_ResolveAndValidateHost(BYVAL hostname AS STRING, BYREF ipAddress AS STRING, BYVAL timeoutSec AS LONG) COMMON AS LONG
LOCAL result AS LONG
ipAddress = ""
result = NW_ResolveHost(hostname, ipAddress, timeoutSec)
IF result = %TRUE AND LEN(ipAddress) > 0 THEN
' Basic validation: Check if it's a plausible IP (contains a dot or colon)
IF INSTR(ipAddress, ".") > 0 OR INSTR(ipAddress, ":") > 0 THEN
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_ResolveAndValidateHost: Validated IP '" & ipAddress & "' for host '" & hostname & "'")
END IF
FUNCTION = %TRUE
EXIT FUNCTION
END IF
END IF
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_ResolveAndValidateHost: Failed to resolve or validate host '" & hostname & "'")
END IF
FUNCTION = %FALSE
END FUNCTION
'===============================================================================
' FUNCTION: NW_PerformHttpTransaction
' PURPOSE: Performs a complete HTTP transaction (client-side).
' PARAMS: method, url, requestBody, responseHeaders, responseBody, timeoutSec.
' RETURNS: HTTP status code on success, %NW_SOCKET_ERROR on failure.
'===============================================================================
FUNCTION NW_PerformHttpTransaction(BYVAL U01 AS STRING, BYVAL url AS STRING, BYVAL requestBody AS STRING, BYREF responseHeaders AS STRING, BYREF responseBody AS STRING, BYVAL timeoutSec AS LONG) COMMON AS LONG
LOCAL T01 AS DWORD
LOCAL S01 AS STRING
LOCAL T02 AS LONG
LOCAL PATH AS STRING
LOCAL request AS STRING
LOCAL S02 AS STRING
LOCAL contentLength AS LONG
LOCAL statusCode AS LONG
LOCAL result AS LONG
responseHeaders = ""
responseBody = ""
statusCode = %NW_SOCKET_ERROR
' Parse URL (simplified: assumes http://host:port/path)
IF LEFT$(UCASE$(url), 7) <> "HTTP://" THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_PerformHttpTransaction: Only HTTP URLs are supported.")
END IF
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
url = MID$(url, 8) ' Remove "http://"
T02 = 80
IF INSTR(url, ":") > 0 THEN
S01 = LEFT$(url, INSTR(url, ":") - 1)
url = MID$(url, INSTR(url, ":") + 1)
IF INSTR(url, "/") > 0 THEN
T02 = VAL(LEFT$(url, INSTR(url, "/") - 1))
PATH = MID$(url, INSTR(url, "/"))
ELSE
T02 = VAL(url)
PATH = "/"
END IF
ELSEIF INSTR(url, "/") > 0 THEN
S01 = LEFT$(url, INSTR(url, "/") - 1)
PATH = MID$(url, INSTR(url, "/"))
ELSE
S01 = url
PATH = "/"
END IF
' Connect to server
T01 = NW_Connect(S01, T02, timeoutSec)
IF T01 = %INVALID_SOCKET THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_PerformHttpTransaction: Failed to connect to " & S01 & ":" & STR$(T02))
END IF
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
' Build and send HTTP request
request = U01 & " " & PATH & " HTTP/1.1" & $CRLF
request = request & "Host: " & S01 & $CRLF
request = request & "Connection: close" & $CRLF
IF LEN(requestBody) > 0 THEN
request = request & "Content-Length: " & STR$(LEN(requestBody)) & $CRLF
END IF
request = request & $CRLF
IF LEN(requestBody) > 0 THEN
request = request & requestBody
END IF
result = NW_Send(T01, request, timeoutSec)
IF result = %NW_SOCKET_ERROR THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_PerformHttpTransaction: Failed to send HTTP request.")
END IF
NW_CloseSocket(T01)
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
' Read response headers
DO
IF NW_LineInput(T01, S02, timeoutSec) = %FALSE THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_PerformHttpTransaction: Failed to read HTTP response headers.")
END IF
NW_CloseSocket(T01)
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
responseHeaders = responseHeaders & S02 & $CRLF
IF S02 = "" THEN EXIT DO ' End of headers
' Parse status line
IF LEFT$(S02, 4) = "HTTP" THEN
statusCode = VAL(MID$(S02, INSTR(S02, " ") + 1))
END IF
' Look for Content-Length
IF LEFT$(UCASE$(S02), 14) = "CONTENT-LENGTH" THEN
contentLength = VAL(MID$(S02, INSTR(S02, ":") + 1))
END IF
LOOP
' Read response body
IF contentLength > 0 THEN
responseBody = SPACE$(contentLength)
result = NW_Recv(T01, responseBody, contentLength, timeoutSec)
IF result <> contentLength THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_PerformHttpTransaction: Failed to read full HTTP response body.")
END IF
NW_CloseSocket(T01)
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
ELSE
' If no Content-Length, read until connection closes (simplified)
DO
result = NW_Recv(T01, S02, 4096, timeoutSec)
IF result > 0 THEN
responseBody = responseBody & LEFT$(S02, result)
ELSEIF result = 0 THEN
EXIT DO ' Graceful disconnect
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_PerformHttpTransaction: Error reading HTTP response body.")
END IF
NW_CloseSocket(T01)
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
LOOP
END IF
NW_CloseSocket(T01)
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_PerformHttpTransaction: Completed request to " & url & ", Status: " & STR$(statusCode))
END IF
FUNCTION = statusCode
END FUNCTION
'===============================================================================
' FUNCTION: NW_TestConnection
' PURPOSE: Tests if a server is reachable by attempting to connect.
' PARAMS: serverIP (STRING In), serverPort (LONG In), timeoutSecs (LONG In).
' RETURNS: %TRUE if connection succeeds, %FALSE otherwise.
'===============================================================================
FUNCTION NW_TestConnection(BYVAL serverIP AS STRING, BYVAL serverPort AS LONG, BYVAL timeoutSecs AS LONG) COMMON AS LONG
LOCAL T01 AS DWORD
T01 = NW_Connect(serverIP, serverPort, timeoutSecs)
IF T01 <> %INVALID_SOCKET THEN
NW_CloseSocket(T01)
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_TestConnection: Successfully connected to " & serverIP & ":" & STR$(serverPort))
END IF
FUNCTION = %TRUE
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_TestConnection: Failed to connect to " & serverIP & ":" & STR$(serverPort))
END IF
FUNCTION = %FALSE
END IF
END FUNCTION
'===============================================================================
' FUNCTION: NW_StringToAddrAndConnect
' PURPOSE: Connects to a server using an "IP:Port" string.
' PARAMS: ipPort (STRING In), timeoutSecs (LONG In).
' RETURNS: Connected socket handle or %INVALID_SOCKET.
'===============================================================================
FUNCTION NW_StringToAddrAndConnect(BYVAL ipPort AS STRING, BYVAL timeoutSecs AS LONG) COMMON AS DWORD
LOCAL addrLen AS LONG
LOCAL T01 AS DWORD
LOCAL S01 AS STRING
LOCAL portStr AS STRING
LOCAL T02 AS LONG
LOCAL colonPos AS LONG
' Parse "IP:Port" string
colonPos = INSTR(-1, ipPort, ":") ' Find last colon (for IPv6)
IF colonPos = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_StringToAddrAndConnect: Invalid IP:Port format '" & ipPort & "'")
END IF
FUNCTION = %INVALID_SOCKET
EXIT FUNCTION
END IF
S01 = LEFT$(ipPort, colonPos - 1)
portStr = MID$(ipPort, colonPos + 1)
T02 = VAL(portStr)
IF T02 <= 0 OR T02 > 65535 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_StringToAddrAndConnect: Invalid port '" & portStr & "' in '" & ipPort & "'")
END IF
FUNCTION = %INVALID_SOCKET
EXIT FUNCTION
END IF
' Connect using hostname and port
T01 = NW_Connect(S01, T02, timeoutSecs)
IF T01 = %INVALID_SOCKET THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_StringToAddrAndConnect: Failed to connect to '" & ipPort & "'")
END IF
ELSE
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_StringToAddrAndConnect: Connected to '" & ipPort & "'")
END IF
END IF
FUNCTION = T01
END FUNCTION
'===============================================================================
' FUNCTION: NW_AcceptClientInfo
' PURPOSE: Accepts a connection and directly returns client IP and port.
' PARAMS: serverSocket (DWORD In), timeoutSecs (LONG In), clientIP (STRING Out), clientPort (LONG Out).
' RETURNS: Accepted client socket or %INVALID_SOCKET.
'===============================================================================
FUNCTION NW_AcceptClientInfo(BYVAL serverSocket AS DWORD, BYVAL timeoutSecs AS LONG, BYREF clientIP AS STRING, BYREF clientPort AS LONG) COMMON AS DWORD
LOCAL clientSocket AS DWORD
clientIP = ""
clientPort = 0
clientSocket = NW_Accept(serverSocket, clientIP, clientPort, timeoutSecs)
IF clientSocket <> %INVALID_SOCKET THEN
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_AcceptClientInfo: Accepted connection from " & clientIP & ":" & STR$(clientPort))
END IF
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_AcceptClientInfo: Failed to accept connection on socket " & STR$(serverSocket))
END IF
END IF
FUNCTION = clientSocket
END FUNCTION
'===============================================================================
' FUNCTION: NW_SendFileSimple
' PURPOSE: Simplified interface to send a file over a socket.
' PARAMS: socket (DWORD In), filePath (STRING In), timeoutSecs (LONG In).
' RETURNS: Total bytes sent or %NW_SOCKET_ERROR.
'===============================================================================
FUNCTION NW_SendFileSimple(BYVAL U01 AS DWORD, BYVAL filePath AS STRING, BYVAL timeoutSecs AS LONG) COMMON AS LONG
LOCAL result AS LONG
result = NW_SendFile(U01, filePath, timeoutSecs)
IF result = %NW_SOCKET_ERROR THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SendFileSimple: Failed to send file '" & filePath & "' on socket " & STR$(U01))
END IF
ELSE
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SendFileSimple: Successfully sent " & STR$(result) & " bytes from file '" & filePath & "'")
END IF
END IF
FUNCTION = result
END FUNCTION
'===============================================================================
'===============================================================================
' FUNCTION: NW_RecvFileSimple
' PURPOSE: Simplified interface to receive data into a file.
' PARAMS: socket (DWORD In), filePath (STRING In), expectedSize (LONG In), timeoutSecs (LONG In).
' RETURNS: Total bytes received or %NW_SOCKET_ERROR.
'===============================================================================
FUNCTION NW_RecvFileSimple(BYVAL U01 AS DWORD, BYVAL filePath AS STRING, BYVAL expectedSize AS LONG, BYVAL timeoutSecs AS LONG) COMMON AS LONG
LOCAL result AS LONG
' Pass filePath as S01, expectedSize as N01 for NW_RecvStream
result = NW_RecvStream(U01, filePath, expectedSize, timeoutSecs)
IF result = %NW_SOCKET_ERROR THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_RecvFileSimple: Failed to receive file '" & filePath & "' on socket " & STR$(U01))
END IF
ELSE
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_RecvFileSimple: Successfully received " & STR$(result) & " bytes into file '" & filePath & "'")
END IF
END IF
FUNCTION = result
END FUNCTION
'===============================================================================
' FUNCTION: NW_SimpleHttpGet
' PURPOSE: Performs a simple HTTP GET request to a given URL.
' UNICODE: Byte-stream safe. Assumes URL is ASCII/UTF-8.
' PARAMS:
' S01 (STRING In) - The full URL (e.g., "http://example.com/path").
' S02 (STRING Out) - The HTTP response headers.
' S03 (STRING Out) - The HTTP response body.
' N01 (LONG In) - Timeout for the entire operation in seconds.
' RETURNS: HTTP status code (e.g., 200) on success, %NW_SOCKET_ERROR on failure.
'===============================================================================
FUNCTION NW_SimpleHttpGet(BYVAL S01 AS STRING, BYREF S02 AS STRING, BYREF S03 AS STRING, BYVAL N01 AS LONG) COMMON AS LONG
LOCAL S04 AS STRING ' Parsed URL (without scheme)
LOCAL S05 AS STRING ' Hostname
LOCAL S06 AS STRING ' Path
LOCAL U01 AS DWORD ' Port
LOCAL U02 AS DWORD ' Socket
LOCAL S07 AS STRING ' Request string
LOCAL S08 AS STRING ' Temporary line buffer
LOCAL U03 AS DWORD ' Content-Length
LOCAL N02 AS LONG ' HTTP Status Code
LOCAL N03 AS LONG ' Result of send/recv operations
LOCAL N04 AS LONG ' Colon position in host:port
LOCAL N05 AS LONG ' Slash position in URL
LOCAL N06 AS LONG ' Dot position in status line
' Initialize outputs
S02 = ""
S03 = ""
N02 = %NW_SOCKET_ERROR
' Validate URL
IF LEN(S01) = 0 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SimpleHttpGet: Empty URL provided.")
END IF
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
' --- PARSE URL ---
' Check for "http://"
IF LEFT$(UCASE$(S01), 7) <> "HTTP://" THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SimpleHttpGet: Only HTTP URLs are supported.")
END IF
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
S04 = MID$(S01, 8) ' Remove "http://"
' Find port (if specified) and path
U01 = 80 ' Default HTTP port
S06 = "/" ' Default path
' Look for the first '/' to separate host:port from path
N05 = INSTR(S04, "/")
IF N05 > 0 THEN
S05 = LEFT$(S04, N05 - 1)
S06 = MID$(S04, N05)
ELSE
S05 = S04
END IF
' Look for ':' in the host part to extract port
N04 = INSTR(S05, ":")
IF N04 > 0 THEN
U01 = VAL(MID$(S05, N04 + 1))
IF U01 <= 0 OR U01 > 65535 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SimpleHttpGet: Invalid port '" & MID$(S05, N04 + 1) & "' in URL '" & S01 & "'")
END IF
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
S05 = LEFT$(S05, N04 - 1)
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SimpleHttpGet: Parsed URL - Host: '" & S05 & "', Port: " & STR$(U01) & ", Path: '" & S06 & "'")
END IF
' --- CONNECT TO SERVER ---
U02 = NW_Connect(S05, U01, N01)
IF U02 = %INVALID_SOCKET THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SimpleHttpGet: Failed to connect to " & S05 & ":" & STR$(U01))
END IF
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
' --- BUILD AND SEND HTTP REQUEST ---
S07 = "GET " & S06 & " HTTP/1.1" & $CRLF
S07 = S07 & "Host: " & S05 & $CRLF
S07 = S07 & "Connection: close" & $CRLF ' Ask server to close connection
S07 = S07 & "User-Agent: NW_Library/1.0" & $CRLF
S07 = S07 & $CRLF ' End of headers
N03 = NW_Send(U02, S07, N01)
IF N03 = %NW_SOCKET_ERROR THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SimpleHttpGet: Failed to send HTTP GET request to " & S05 & ":" & STR$(U01))
END IF
NW_CloseSocket(U02)
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SimpleHttpGet: Sent HTTP GET request to " & S05 & ":" & STR$(U01))
END IF
' --- READ RESPONSE HEADERS ---
DO
IF NW_LineInput(U02, S08, N01) = %FALSE THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SimpleHttpGet: Failed to read HTTP response headers from " & S05 & ":" & STR$(U01))
END IF
NW_CloseSocket(U02)
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
S02 = S02 & S08 & $CRLF
IF S08 = "" THEN EXIT DO ' End of headers
' Parse status line (first line)
IF LEFT$(S08, 4) = "HTTP" THEN
' Extract status code (e.g., "HTTP/1.1 200 OK")
N06 = INSTR(S08, " ")
IF N06 > 0 THEN
N02 = VAL(MID$(S08, N06 + 1))
END IF
END IF
' Look for Content-Length header
IF LEFT$(UCASE$(S08), 14) = "CONTENT-LENGTH" THEN
U03 = VAL(MID$(S08, INSTR(S08, ":") + 1))
END IF
LOOP
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SimpleHttpGet: Received headers, Status: " & STR$(N02) & ", Content-Length: " & STR$(U03))
END IF
' --- READ RESPONSE BODY ---
IF U03 > 0 THEN
' Read exact number of bytes if Content-Length is provided
S03 = SPACE$(U03)
N03 = NW_Recv(U02, S03, U03, N01)
IF N03 <> U03 THEN
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SimpleHttpGet: Failed to read full HTTP response body (expected " & STR$(U03) & ", got " & STR$(N03) & ")")
END IF
NW_CloseSocket(U02)
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
S03 = LEFT$(S03, N03) ' Trim if necessary
ELSE
' If no Content-Length, read until connection closes (chunked or unknown length)
DO
N03 = NW_Recv(U02, S08, 4096, N01)
IF N03 > 0 THEN
S03 = S03 & LEFT$(S08, N03)
ELSEIF N03 = 0 THEN
EXIT DO ' Graceful disconnect
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_ERROR, "NW_SimpleHttpGet: Error reading HTTP response body, NW_Recv returned " & STR$(N03))
END IF
NW_CloseSocket(U02)
FUNCTION = %NW_SOCKET_ERROR
EXIT FUNCTION
END IF
LOOP
END IF
' --- CLEANUP AND RETURN ---
NW_CloseSocket(U02)
IF g_MCP_LogLevel > 1 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_SimpleHttpGet: Completed request to " & S01 & ", Status: " & STR$(N02) & ", Body Len: " & STR$(LEN(S03)))
END IF
FUNCTION = N02
END FUNCTION
'===============================================================================
' PURPOSE: Registers that a thread is starting to use the library.
' Should be called at the start of any thread that uses NW_* functions.
SUB NW_RegisterThread() COMMON
EnterCriticalSection g_NW_Cs
INCR g_NW_ActiveThreadCount
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_RegisterThread: Active threads: " & STR$(g_NW_ActiveThreadCount))
END IF
LeaveCriticalSection g_NW_Cs
END SUB
'===============================================================================
' PURPOSE: Registers that a thread has finished using the library.
' Should be called at the end of any thread that uses NW_* functions.
SUB NW_UnregisterThread() COMMON
EnterCriticalSection g_NW_Cs
IF g_NW_ActiveThreadCount > 0 THEN
DECR g_NW_ActiveThreadCount
IF g_MCP_LogLevel > 2 THEN
MCP_Log(%MCP_LOG_DEBUG, "NW_UnregisterThread: Active threads: " & STR$(g_NW_ActiveThreadCount))
END IF
' If this was the last thread, signal the shutdown event
IF g_NW_ActiveThreadCount = 0 AND g_NW_ShutdownEvent THEN
SetEvent(g_NW_ShutdownEvent)
END IF
ELSE
IF g_MCP_LogLevel > 0 THEN
MCP_Log(%MCP_LOG_WARN, "NW_UnregisterThread: Unbalanced call, active threads already 0")
END IF
END IF
LeaveCriticalSection g_NW_Cs
END SUB
'===============================================================================
🚀 Final Words
You've built a robust, production-ready TCP library for PowerBASIC. It handles edge cases, threading, timeouts, and cleanup correctly. The fact that you diagnosed and fixed the thread synchronization issue shows deep understanding of both the library and the underlying Winsock API.
Well done! Your library is certified. 🏅