api: drop stale websocket / stream connections (#877)

This commit is contained in:
bicarus
2026-08-19 13:37:41 -07:00
committed by GitHub
parent 0934cce225
commit 3f6862908e
2 changed files with 80 additions and 9 deletions
+51 -9
View File
@@ -6,6 +6,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <chrono> #include <chrono>
#include <limits>
#include <map> #include <map>
#include <string> #include <string>
#include <thread> #include <thread>
@@ -47,6 +48,29 @@ namespace api {
return send_all(socket, text.data(), text.size()); return send_all(socket, text.data(), text.size());
} }
// a viewer leaving is normally noticed by a failing send, so a stream with no frame
// to push has to ask the socket instead
bool client_gone(SOCKET socket) {
fd_set read_set;
FD_ZERO(&read_set);
FD_SET(socket, &read_set);
// the socket is blocking with a receive timeout, so poll before touching it
timeval immediately {};
const int ready = select(0, &read_set, nullptr, nullptr, &immediately);
if (ready == 0) {
return false;
}
if (ready < 0) {
return true;
}
// consumed rather than peeked: a stray byte would otherwise sit in front of the
// FIN and keep hiding it for as long as the stream runs
char discard[256];
return recv(socket, discard, sizeof(discard), 0) <= 0;
}
std::string url_decode(const std::string &input) { std::string url_decode(const std::string &input) {
std::string out; std::string out;
out.reserve(input.size()); out.reserve(input.size());
@@ -355,18 +379,33 @@ namespace api {
if (!writer) { if (!writer) {
send_error(socket, "404 Not Found"); send_error(socket, "404 Not Found");
} else { } else {
std::vector<int> screens;
graphics_screens_get(screens);
// registration takes a raw swapchain index and never bounds it, so the
// capture range has to be enforced here rather than assumed
const auto streamable = [&screens](int screen) {
return screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
&& std::find(screens.begin(), screens.end(), screen)
!= screens.end();
};
// screen 1 is the subscreen in every game that has one; single-screen games // screen 1 is the subscreen in every game that has one; single-screen games
// only ever register screen 0, so resolve the default against what exists // only ever register screen 0, so resolve the default against what exists.
// left unclamped so a nonsense screen is reported as what was asked for
int screen = query_int(request, "screen", -1, 0, int screen = query_int(request, "screen", -1, 0,
static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO) - 1); std::numeric_limits<int>::max());
if (screen < 0) { if (screen < 0) {
std::vector<int> screens; screen = streamable(1) ? 1 : 0;
graphics_screens_get(screens);
screen = std::find(screens.begin(), screens.end(), 1) != screens.end()
? 1 : 0;
} }
if (!capture_pump::claim_screen(screen)) { // the default always lands on a screen that exists, so this is only ever
// an explicit request for one that cannot be captured
if (!streamable(screen)) {
log_warning("api::stream",
"screen {} is not available, refusing {}", screen, address);
send_error(socket, "404 Not Found");
} else if (!capture_pump::claim_screen(screen)) {
log_warning("api::stream", log_warning("api::stream",
"screen {} is already being streamed, refusing {}", "screen {} is already being streamed, refusing {}",
screen, address); screen, address);
@@ -399,8 +438,11 @@ namespace api {
screen, frame.pixels, 1, screen, frame.pixels, 1,
&frame.timestamp, &frame.width, &frame.height); &frame.timestamp, &frame.width, &frame.height);
if (ok && frame.pixels if (ok && frame.pixels) {
&& !writer->write(stream_send, frame)) { if (!writer->write(stream_send, frame)) {
break;
}
} else if (client_gone(socket)) {
break; break;
} }
+29
View File
@@ -12,6 +12,20 @@ using namespace headsocket;
namespace api { namespace api {
namespace {
// how long a single handshake read may stall before the connection is dropped;
// headsocket reads the request a byte at a time, so this is an idle timeout between
// bytes rather than a deadline for the whole handshake
constexpr int handshake_timeout_ms = 5000;
void set_recv_timeout(connection &conn, int milliseconds) {
DWORD timeout = static_cast<DWORD>(milliseconds);
setsockopt(conn.impl()->socket, SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
}
}
/* /*
* Client class declaration * Client class declaration
*/ */
@@ -37,6 +51,21 @@ namespace api {
HEADSOCKET_SERVER(WebSocketServer, web_socket_server); HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
public: public:
WebSocketController *websocket; WebSocketController *websocket;
protected:
bool handshake(connection &conn) override {
// headsocket runs the handshake on its single accept thread with a blocking
// recv, so a peer that connects and then says nothing would park that thread and
// leave every later connection sitting unaccepted in the backlog
set_recv_timeout(conn, handshake_timeout_ms);
const bool accepted = base_t::handshake(conn);
// from here the client thread owns the socket and wants to block on reads
set_recv_timeout(conn, 0);
return accepted;
}
}; };
void api::WebSocketServer::init() {} void api::WebSocketServer::init() {}