api: drop stale websocket / stream connections (#877)
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
|||||||
Reference in New Issue
Block a user