Launching a network race connects to each machine in turn and retries while one is not listening yet, because they finish loading at different moments. That part is deliberate and stays. What was not deliberate is that the wait slept without pumping messages, so Windows saw a process that had stopped answering and painted the whole thing "Not responding" - for up to two minutes, with no indication of which peer was missing, how long remained, or any way out. It runs before the engine block, so there is no render loop keeping the window alive either. Three changes, both transports: Pump while waiting. Every sleep on the connect path goes through NetTransport_PumpAndSleep, so the window keeps painting and can be moved. It is re-entrancy guarded, because dispatching a message can run application code that reaches a connect of its own, and nested pumping would deliver messages twice and let an inner wait swallow the escape meant for the outer one. Shorten the deadline. Two minutes suited the arcade, where a pod that was still booting would always answer eventually on a LAN with nothing else to go wrong. Over the internet a machine silent for twenty seconds is not coming. RP412CONNECTWAIT, 2 to 300, default 20, documented in environ.ini. Say what is happening. The title bar names the peer and counts down, and ESC gives up at once - the title being the one surface guaranteed to exist this early, since there is no renderer yet to draw a progress screen with. The cancel latch is cleared when a connect sequence begins so that an escape pressed during one race cannot cancel the next. The Winsock path is only partly fixed and the code now says so: connect() there is still blocking, since the socket is only made nonblocking after it succeeds, so an unreachable host - filtered rather than refused - still sits in the OS SYN retry for around twenty seconds. Fixing that needs FIONBIO before connect() and a select() on our own timeout. Left for when LAN play comes up; a Steam host goes through SteamNetTransport::Connect, which is fully covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
510 lines
13 KiB
C++
510 lines
13 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4nettransport.h"
|
|
#include "..\munga\appmgr.h"
|
|
#include <Ws2tcpip.h>
|
|
|
|
//########################################################################
|
|
// Waiting for a peer without hanging the window
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
Logical gInWait = False; // re-entrancy guard
|
|
Logical gWaitCancelled = False;
|
|
char gOriginalTitle[256] = "";
|
|
Logical gTitleSaved = False;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// How long Connect keeps redialling.
|
|
//
|
|
// It was two minutes, which was right for what it was written for: an
|
|
// arcade console redialling a pod that is still booting and WILL answer,
|
|
// on a LAN with nothing else to go wrong. Over Steam the same two minutes
|
|
// is a host staring at a dead window, because a peer that has not answered
|
|
// in twenty seconds is not coming - it never launched, or it is behind
|
|
// something the relay cannot cross.
|
|
//
|
|
int
|
|
NetTransport_ConnectWaitSeconds()
|
|
{
|
|
static int cached = -1;
|
|
|
|
if (cached < 0)
|
|
{
|
|
const char *setting = getenv("RP412CONNECTWAIT");
|
|
|
|
cached = (setting != NULL) ? atoi(setting) : 20;
|
|
if (cached < 2) { cached = 2; }
|
|
if (cached > 300) { cached = 300; }
|
|
}
|
|
return cached;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
NetTransport_SetWaitProgress(const char *text)
|
|
{
|
|
if (ghWnd == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!gTitleSaved)
|
|
{
|
|
GetWindowTextA(ghWnd, gOriginalTitle, sizeof(gOriginalTitle) - 1);
|
|
gTitleSaved = True;
|
|
}
|
|
|
|
//
|
|
// The title bar because it is the one surface guaranteed to exist
|
|
// here: this runs before the engine block, so there is no renderer to
|
|
// draw a progress screen with yet.
|
|
//
|
|
SetWindowTextA(ghWnd, (text != NULL) ? text : gOriginalTitle);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
NetWaitResult
|
|
NetTransport_PumpAndSleep(int milliseconds)
|
|
{
|
|
//
|
|
// Dispatching a message can run application code, and that code can
|
|
// reach a connect of its own. Nested pumping would then deliver the
|
|
// same messages twice and let an inner wait consume the escape meant
|
|
// for the outer one, so an inner call just sleeps.
|
|
//
|
|
if (gInWait)
|
|
{
|
|
Sleep(milliseconds);
|
|
return NetWaitContinue;
|
|
}
|
|
|
|
gInWait = True;
|
|
|
|
NetWaitResult result = NetWaitContinue;
|
|
MSG message;
|
|
|
|
while (PeekMessage(&message, NULL, 0, 0, PM_REMOVE))
|
|
{
|
|
if (message.message == WM_QUIT)
|
|
{
|
|
//
|
|
// Put it back: the message loop that owns the shutdown has
|
|
// to see this, not us.
|
|
//
|
|
PostQuitMessage((int) message.wParam);
|
|
result = NetWaitQuit;
|
|
break;
|
|
}
|
|
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE)
|
|
{
|
|
gWaitCancelled = True;
|
|
}
|
|
TranslateMessage(&message);
|
|
DispatchMessage(&message);
|
|
}
|
|
|
|
if (result == NetWaitContinue && gWaitCancelled)
|
|
{
|
|
result = NetWaitCancelled;
|
|
}
|
|
if (result == NetWaitContinue)
|
|
{
|
|
Sleep(milliseconds);
|
|
}
|
|
|
|
gInWait = False;
|
|
return result;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// Cleared when a connect sequence starts, so an escape pressed during one
|
|
// race does not cancel the next one.
|
|
//
|
|
void
|
|
NetTransport_ClearWaitCancel()
|
|
{
|
|
gWaitCancelled = False;
|
|
}
|
|
|
|
//########################################################################
|
|
// WinsockNetTransport - the TCP wire the arcade always used, moved
|
|
// verbatim out of L4NET.CPP behind the NetTransport seam. Behavior
|
|
// notes preserved from the original:
|
|
// - Connect retries while the remote refuses (WSAECONNREFUSED): the
|
|
// console feeds eggs in [pilots] order with ACKs, so earlier pods
|
|
// are listening before later pods open - the retry covers the race.
|
|
// - Sockets go nonblocking once connected; listeners are nonblocking
|
|
// from creation.
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
class WinsockNetTransport:
|
|
public NetTransport
|
|
{
|
|
public:
|
|
WinsockNetTransport():
|
|
started(False)
|
|
{
|
|
}
|
|
|
|
Logical
|
|
Startup()
|
|
{
|
|
if (started)
|
|
{
|
|
return True;
|
|
}
|
|
int result = WSAStartup(MAKEWORD(2, 2), &winsockData);
|
|
if (result != NO_ERROR)
|
|
{
|
|
DEBUG_STREAM << "ERROR: WSAStartup() failed with " << result
|
|
<< "!\n" << std::flush;
|
|
return False;
|
|
}
|
|
started = True;
|
|
return True;
|
|
}
|
|
|
|
void
|
|
Cleanup()
|
|
{
|
|
if (started)
|
|
{
|
|
WSACleanup();
|
|
started = False;
|
|
}
|
|
}
|
|
|
|
int
|
|
GetLocalAddresses(
|
|
unsigned long *addresses,
|
|
int max_count
|
|
)
|
|
{
|
|
char name[255];
|
|
PHOSTENT hostinfo;
|
|
|
|
if (gethostname(name, sizeof(name)) != 0)
|
|
{
|
|
DEBUG_STREAM << "ERROR: gethostname() failed!" << std::endl << std::flush;
|
|
return 0;
|
|
}
|
|
if ((hostinfo = gethostbyname(name)) == NULL)
|
|
{
|
|
DEBUG_STREAM << "ERROR: gethostbyname() failed!" << std::endl << std::flush;
|
|
return 0;
|
|
}
|
|
|
|
int count = 0;
|
|
for (int i = 0; hostinfo->h_addr_list[i] != NULL && count < max_count; ++i)
|
|
{
|
|
addresses[count++] = *((unsigned long *) hostinfo->h_addr_list[i]);
|
|
}
|
|
|
|
// loopback rounds out the list (single-machine testing)
|
|
if (count < max_count)
|
|
{
|
|
addresses[count++] = 0x0100007F;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
Logical
|
|
Resolve(
|
|
const char *host_name,
|
|
SOCKADDR_IN *address
|
|
)
|
|
{
|
|
// numeric ip[:port] only - the egg carries addresses, not
|
|
// names; port stays 0 when absent (caller applies default)
|
|
int buffer_size = sizeof(SOCKADDR_IN);
|
|
return WSAStringToAddressA(
|
|
(LPSTR) host_name, AF_INET, NULL,
|
|
(LPSOCKADDR) address, &buffer_size) == 0;
|
|
}
|
|
|
|
Connection
|
|
Connect(
|
|
const SOCKADDR_IN *remote,
|
|
int local_port
|
|
)
|
|
{
|
|
DEBUG_STREAM << "Opening connection to "
|
|
<< inet_ntoa(remote->sin_addr) << ":" << ntohs(remote->sin_port)
|
|
<< "...\n" << std::flush;
|
|
|
|
//
|
|
// Retry-while-refused, bounded: the egg-ACK ordering means
|
|
// the peer may not be listening yet. A refused TCP socket
|
|
// is dead - every attempt needs a fresh one.
|
|
//
|
|
//
|
|
// NOTE the connect() below is still BLOCKING - the socket only
|
|
// goes nonblocking once it has succeeded - so an unreachable
|
|
// host (filtered rather than refused) sits in the OS SYN retry
|
|
// for around twenty seconds with nothing we can do about it.
|
|
// Pumping between redials fixes the refused case, which is the
|
|
// common one; the unreachable case needs the socket made
|
|
// nonblocking before connect() and a select() on our own
|
|
// timeout. Not done here because this is the LAN/direct path -
|
|
// a Steam host goes through SteamNetTransport::Connect.
|
|
//
|
|
DWORD deadline =
|
|
GetTickCount() + (DWORD) NetTransport_ConnectWaitSeconds() * 1000;
|
|
char progress[256];
|
|
|
|
sprintf(
|
|
progress,
|
|
"Red Planet - connecting to %s ...",
|
|
inet_ntoa(remote->sin_addr)
|
|
);
|
|
NetTransport_SetWaitProgress(progress);
|
|
|
|
for (;;)
|
|
{
|
|
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (sock == INVALID_SOCKET)
|
|
{
|
|
DEBUG_STREAM << "ERROR: socket() failed with "
|
|
<< WSAGetLastError() << "!\n";
|
|
return InvalidConnection;
|
|
}
|
|
|
|
bool reuse_address = true;
|
|
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
|
|
(char *) &reuse_address, sizeof(bool)))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on socket; setsockopt() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(sock);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
// bind the game port: the mesh identifies peers by
|
|
// address AND port, so the source port must be ours
|
|
// (port 0 = ephemeral, for console channels)
|
|
sockaddr_in local_endpoint;
|
|
memset(&local_endpoint, 0, sizeof(local_endpoint));
|
|
local_endpoint.sin_family = AF_INET;
|
|
local_endpoint.sin_port = htons((unsigned short) local_port);
|
|
local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY;
|
|
if (bind(sock, (sockaddr *) &local_endpoint, sizeof(local_endpoint)))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not bind local socket; bind() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(sock);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
if (connect(sock, (sockaddr *) remote, sizeof(SOCKADDR_IN)) == 0)
|
|
{
|
|
unsigned long enable = 1;
|
|
if (ioctlsocket(sock, FIONBIO, &enable))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not set actively opened socket to nonblocking; ioctlsocket() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(sock);
|
|
return InvalidConnection;
|
|
}
|
|
NetTransport_SetWaitProgress(NULL);
|
|
return (Connection) sock;
|
|
}
|
|
|
|
int wsa_error = WSAGetLastError();
|
|
closesocket(sock);
|
|
if (wsa_error != WSAECONNREFUSED ||
|
|
(LONG)(GetTickCount() - deadline) >= 0)
|
|
{
|
|
DEBUG_STREAM << "ERROR: connect() failed with "
|
|
<< wsa_error << "!\n" << std::flush;
|
|
NetTransport_SetWaitProgress(NULL);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
//
|
|
// Peer not up yet - redial, answering the window meanwhile.
|
|
//
|
|
DWORD left = (DWORD)((LONG)(deadline - GetTickCount()) / 1000);
|
|
|
|
sprintf(
|
|
progress,
|
|
"Red Planet - connecting to %s ... %us left, ESC to cancel",
|
|
inet_ntoa(remote->sin_addr),
|
|
(unsigned) left
|
|
);
|
|
NetTransport_SetWaitProgress(progress);
|
|
|
|
for (int slept = 0; slept < 250; slept += 50)
|
|
{
|
|
NetWaitResult wait = NetTransport_PumpAndSleep(50);
|
|
if (wait != NetWaitContinue)
|
|
{
|
|
DEBUG_STREAM << "Connect "
|
|
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
|
|
<< " by the user\n" << std::flush;
|
|
NetTransport_SetWaitProgress(NULL);
|
|
return InvalidConnection;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Connection
|
|
Listen(
|
|
int local_port,
|
|
int backlog
|
|
)
|
|
{
|
|
DEBUG_STREAM << "Starting to listen on port " << local_port
|
|
<< "...\n" << std::flush;
|
|
|
|
SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (listener == INVALID_SOCKET)
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not create listener socket; socket() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
return InvalidConnection;
|
|
}
|
|
|
|
bool reuse_address = true;
|
|
if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR,
|
|
(char *) &reuse_address, sizeof(bool)))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on listener socket; setsockopt() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(listener);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
sockaddr_in local_endpoint;
|
|
memset(&local_endpoint, 0, sizeof(local_endpoint));
|
|
local_endpoint.sin_family = AF_INET;
|
|
local_endpoint.sin_port = htons((unsigned short) local_port);
|
|
local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY;
|
|
if (bind(listener, (sockaddr *) &local_endpoint, sizeof(local_endpoint)))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not bind listener socket; bind() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(listener);
|
|
return InvalidConnection;
|
|
}
|
|
if (listen(listener, backlog))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not listen on listener socket; listen() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(listener);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
unsigned long enable = 1;
|
|
if (ioctlsocket(listener, FIONBIO, &enable))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not set listener socket to nonblocking; ioctlsocket() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(listener);
|
|
return InvalidConnection;
|
|
}
|
|
return (Connection) listener;
|
|
}
|
|
|
|
Connection
|
|
Accept(Connection listener)
|
|
{
|
|
SOCKET accepted = accept((SOCKET) listener, NULL, 0);
|
|
if (accepted == INVALID_SOCKET)
|
|
{
|
|
return InvalidConnection;
|
|
}
|
|
return (Connection) accepted;
|
|
}
|
|
|
|
void
|
|
Close(Connection connection)
|
|
{
|
|
shutdown((SOCKET) connection, SD_BOTH);
|
|
closesocket((SOCKET) connection);
|
|
}
|
|
|
|
int
|
|
Send(
|
|
Connection connection,
|
|
const void *data,
|
|
int size
|
|
)
|
|
{
|
|
return send((SOCKET) connection, (const char *) data, size, 0);
|
|
}
|
|
|
|
int
|
|
Receive(
|
|
Connection connection,
|
|
void *buffer,
|
|
int size
|
|
)
|
|
{
|
|
int received = recv((SOCKET) connection, (char *) buffer, size, 0);
|
|
if (received > 0)
|
|
{
|
|
return received;
|
|
}
|
|
if (received == 0)
|
|
{
|
|
return ReceiveDisconnected;
|
|
}
|
|
|
|
DWORD error = WSAGetLastError();
|
|
switch (error)
|
|
{
|
|
case WSAECONNRESET:
|
|
// hard drop reads the same as an orderly close upstairs
|
|
return ReceiveDisconnected;
|
|
case WSAEWOULDBLOCK:
|
|
return ReceiveNoData;
|
|
default:
|
|
DEBUG_STREAM << "WinsockNetTransport::Receive: recv returned an unexpected error: WSAGetLastError = "
|
|
<< error << std::endl << std::flush;
|
|
return ReceiveNoData;
|
|
}
|
|
}
|
|
|
|
Logical
|
|
GetRemoteAddress(
|
|
Connection connection,
|
|
SOCKADDR_IN *address
|
|
)
|
|
{
|
|
int size = sizeof(SOCKADDR_IN);
|
|
memset(address, 0, size);
|
|
return getpeername((SOCKET) connection, (sockaddr *) address, &size) == 0;
|
|
}
|
|
|
|
private:
|
|
Logical started;
|
|
WSADATA winsockData;
|
|
};
|
|
|
|
WinsockNetTransport gWinsockTransport;
|
|
NetTransport *gTransport = &gWinsockTransport;
|
|
}
|
|
|
|
NetTransport *
|
|
NetTransport_Get()
|
|
{
|
|
return gTransport;
|
|
}
|
|
|
|
void
|
|
NetTransport_Set(NetTransport *transport)
|
|
{
|
|
gTransport = (transport != NULL) ? transport : &gWinsockTransport;
|
|
}
|