A window that answers while it waits
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>
This commit is contained in:
+180
-2
@@ -2,8 +2,138 @@
|
||||
#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
|
||||
@@ -117,7 +247,28 @@ namespace
|
||||
// the peer may not be listening yet. A refused TCP socket
|
||||
// is dead - every attempt needs a fresh one.
|
||||
//
|
||||
DWORD deadline = GetTickCount() + 120 * 1000;
|
||||
//
|
||||
// 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);
|
||||
@@ -164,6 +315,7 @@ namespace
|
||||
closesocket(sock);
|
||||
return InvalidConnection;
|
||||
}
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return (Connection) sock;
|
||||
}
|
||||
|
||||
@@ -174,9 +326,35 @@ namespace
|
||||
{
|
||||
DEBUG_STREAM << "ERROR: connect() failed with "
|
||||
<< wsa_error << "!\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
Sleep(250); // peer not up yet - redial
|
||||
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,3 +124,44 @@ NetTransport *
|
||||
NetTransport_Get();
|
||||
void
|
||||
NetTransport_Set(NetTransport *transport);
|
||||
|
||||
//########################################################################
|
||||
// Waiting for a peer without hanging the window.
|
||||
//
|
||||
// Connect retries until the peer answers, because the egg-ACK ordering
|
||||
// means it may still be booting - that part is deliberate. 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. It happens before the engine
|
||||
// block, so there is no render loop keeping the window alive either.
|
||||
//
|
||||
// Sleep through this instead. It pumps, so the window keeps drawing and
|
||||
// can be moved; it watches for a cancel; and it is re-entrancy guarded,
|
||||
// because dispatching a message can run application code that reaches a
|
||||
// connect of its own.
|
||||
//########################################################################
|
||||
|
||||
enum NetWaitResult
|
||||
{
|
||||
NetWaitContinue, // carry on waiting
|
||||
NetWaitCancelled, // the user pressed escape
|
||||
NetWaitQuit // WM_QUIT arrived; the caller must unwind
|
||||
};
|
||||
|
||||
NetWaitResult
|
||||
NetTransport_PumpAndSleep(int milliseconds);
|
||||
|
||||
// How long Connect keeps redialling, in seconds. RP412CONNECTWAIT
|
||||
// overrides; see the definition for why the default came down from 120.
|
||||
int
|
||||
NetTransport_ConnectWaitSeconds();
|
||||
|
||||
// Progress, shown in the window title while a connect is outstanding.
|
||||
// Pass NULL to put the original title back.
|
||||
void
|
||||
NetTransport_SetWaitProgress(const char *text);
|
||||
|
||||
// Forget a previous escape, so one race's cancel cannot cancel the next.
|
||||
// Call before starting a connect sequence.
|
||||
void
|
||||
NetTransport_ClearWaitCancel();
|
||||
|
||||
@@ -385,10 +385,29 @@ namespace
|
||||
target.Clear();
|
||||
target.SetIPv4(remote_fake_ip, fake_port);
|
||||
|
||||
// mirror the TCP retry-while-refused loop, bounded: the
|
||||
// egg-ACK ordering means the peer may not be listening yet
|
||||
DWORD deadline = GetTickCount() + 120 * 1000;
|
||||
//
|
||||
// Mirror the TCP retry-while-refused loop, bounded: the
|
||||
// egg-ACK ordering means the peer may not be listening yet.
|
||||
//
|
||||
// Every wait below goes through NetTransport_PumpAndSleep, so
|
||||
// the window keeps painting and answering instead of being
|
||||
// declared "Not responding" for the whole attempt. There is no
|
||||
// render loop yet at this point - this runs before the engine
|
||||
// block - so nothing else is keeping it alive.
|
||||
//
|
||||
DWORD wait_seconds = (DWORD) NetTransport_ConnectWaitSeconds();
|
||||
DWORD started = GetTickCount();
|
||||
DWORD deadline = started + wait_seconds * 1000;
|
||||
int attempt = 0;
|
||||
char progress[256];
|
||||
|
||||
sprintf(
|
||||
progress,
|
||||
"Red Planet - connecting to %s ...",
|
||||
inet_ntoa(remote->sin_addr)
|
||||
);
|
||||
NetTransport_SetWaitProgress(progress);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
++attempt;
|
||||
@@ -397,6 +416,7 @@ namespace
|
||||
if (handle == k_HSteamNetConnection_Invalid)
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: ConnectByIPAddress refused the call\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
AddConnection(handle, remote->sin_addr.S_un.S_addr, remote->sin_port);
|
||||
@@ -425,7 +445,34 @@ namespace
|
||||
{
|
||||
break;
|
||||
}
|
||||
Sleep(25);
|
||||
|
||||
//
|
||||
// Count down out loud. Two minutes of a silent frozen
|
||||
// window gave a host nothing to act on - not which peer
|
||||
// was missing, not how long was left, not a way out.
|
||||
//
|
||||
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);
|
||||
|
||||
NetWaitResult wait = NetTransport_PumpAndSleep(25);
|
||||
if (wait != NetWaitContinue)
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: connect "
|
||||
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
|
||||
<< " after " << ((GetTickCount() - started) / 1000)
|
||||
<< "s\n" << std::flush;
|
||||
SteamNetworkingSockets()->CloseConnection(handle, 0, "cancelled", false);
|
||||
RemoveConnection(handle);
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
}
|
||||
if (state == k_ESteamNetworkingConnectionState_Connected)
|
||||
{
|
||||
@@ -436,6 +483,7 @@ namespace
|
||||
}
|
||||
DEBUG_STREAM << "SteamNetTransport: connect succeeded (attempt "
|
||||
<< attempt << ")\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return (Connection) handle;
|
||||
}
|
||||
|
||||
@@ -446,10 +494,27 @@ namespace
|
||||
RemoveConnection(handle);
|
||||
if ((LONG)(GetTickCount() - deadline) >= 0)
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: connect timed out\n" << std::flush;
|
||||
DEBUG_STREAM << "SteamNetTransport: connect timed out after "
|
||||
<< wait_seconds << "s (RP412CONNECTWAIT)\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
Sleep(1000);
|
||||
|
||||
//
|
||||
// A second between redials, still answering the window.
|
||||
//
|
||||
for (int slept = 0; slept < 1000; slept += 50)
|
||||
{
|
||||
NetWaitResult wait = NetTransport_PumpAndSleep(50);
|
||||
if (wait != NetWaitContinue)
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: connect "
|
||||
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
|
||||
<< " between attempts\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -687,6 +687,12 @@ Logical
|
||||
// booting; runs before the engine block so nothing is waiting.
|
||||
//
|
||||
NetTransport_Get()->Startup();
|
||||
|
||||
//
|
||||
// An escape pressed during the last race must not cancel this one.
|
||||
//
|
||||
NetTransport_ClearWaitCancel();
|
||||
|
||||
const char *cursor = remote_pod_list;
|
||||
while (*cursor != '\0' && gRemotePodCount < maxRemotePods)
|
||||
{
|
||||
|
||||
@@ -321,6 +321,22 @@ namespace
|
||||
"# the old arrival-time behaviour if you want to compare.\n"
|
||||
"#RP412NETCLOCK=0\n"
|
||||
"\n"
|
||||
"# How long to keep redialling a player who has not answered yet, in\n"
|
||||
"# seconds. 2 to 300, default 20.\n"
|
||||
"#\n"
|
||||
"# Launching a race connects to each machine in turn, and retries while\n"
|
||||
"# one is not listening yet, because they finish loading at different\n"
|
||||
"# moments. The wait used to be two minutes, which suited the arcade: a\n"
|
||||
"# pod that was still booting would always answer eventually, on a LAN\n"
|
||||
"# with nothing else to go wrong. Over the internet a machine that has\n"
|
||||
"# been silent for twenty seconds is not coming - it never launched, or\n"
|
||||
"# something between you cannot be crossed - so the default is twenty.\n"
|
||||
"#\n"
|
||||
"# The window stays alive throughout and the title bar counts down, and\n"
|
||||
"# ESC gives up immediately. Raise this if you play with someone whose\n"
|
||||
"# machine loads very slowly.\n"
|
||||
"RP412CONNECTWAIT=20\n"
|
||||
"\n"
|
||||
"# ---- Test harness -----------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# The knobs that make a run repeatable and measurable. All are off\n"
|
||||
|
||||
Reference in New Issue
Block a user