From 7d5f528e1afea6f2dc697aae5709f9de6f63a4c5 Mon Sep 17 00:00:00 2001 From: drmext <71258889+drmext@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:01:46 +0000 Subject: [PATCH 01/17] overlay: tooltip static position and fix overlap (#883) Tooltips no longer follow the mouse after initially activating, and don't cover the main option when hovering above. --- src/spice2x/overlay/imgui/extensions.cpp | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/spice2x/overlay/imgui/extensions.cpp b/src/spice2x/overlay/imgui/extensions.cpp index 1dc60de..3bdc387 100644 --- a/src/spice2x/overlay/imgui/extensions.cpp +++ b/src/spice2x/overlay/imgui/extensions.cpp @@ -21,12 +21,36 @@ namespace ImGui { base.y + overlay::apply_scaling(1.0f)); } + // Tooltips follow the mouse and only avoid a small box around the cursor, so + // they can cover the option they describe. Anchor to the hovered item and let + // ImGui's popup placer pick a side that clears it and stays on-screen. The + // size is taken from the tooltip window, so it covers any content. + static void SetNextTooltipPos() { + ImGuiContext& g = *GImGui; + char name[32]; + ImFormatString(name, IM_COUNTOF(name), "##Tooltip_%02d", g.TooltipOverrideCount); + ImGuiWindow *tooltip = ImGui::FindWindowByName(name); + if (tooltip == nullptr || !tooltip->WasActive) { + // Reopened tooltips are hidden for one frame while measuring size. + return; + } + const ImRect item(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); + ImGui::SetNextWindowPos(ImGui::FindBestWindowPosForPopupEx( + item.Min, + ImGui::CalcWindowNextAutoFitSize(tooltip), + &tooltip->AutoPosLastDirection, + ImGui::GetPopupAllowedExtentRect(tooltip), + item, + ImGuiPopupPositionPolicy_Tooltip)); + } + void HelpTooltip(const char* desc) { ImGui::PushStyleColor(ImGuiCol_Border, bg); ImGui::PushStyleColor(ImGuiCol_BorderShadow, bg); ImGui::PushStyleColor(ImGuiCol_PopupBg, bg); ImGui::PushStyleColor(ImGuiCol_Text, fg); + SetNextTooltipPos(); ImGui::BeginTooltip(); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); ImGui::TextUnformatted(desc); @@ -49,6 +73,7 @@ namespace ImGui { ImGui::PushStyleColor(ImGuiCol_PopupBg, bg); ImGui::PushStyleColor(ImGuiCol_Text, fg); + SetNextTooltipPos(); ImGui::BeginTooltip(); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); if (desc && desc[0]) { From b9c8afbbc12452edc3f4ac50cc1eda9ed0ee7f61 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:33:28 -0700 Subject: [PATCH 02/17] Update CONTRIBUTING.md with DLL bundling guidelines Added note about bundling third-party DLL binaries in contributions. --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bff2252..17a64d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,7 @@ To contribute, fork the repo (just the main branch), make changes in your fork, * Any additions to built-in patches (modifications / removals are OK if we have a good reason). * Changes to the software license terms. * Localization / translation for UI text. It becomes too difficult to manage by the maintainers. +* Bundling third party DLL binary; e.g., Discord Social SDK, PC light control integration, and so on. These should be built as hook DLLs instead, optionally with [Spice SDK](https://github.com/spice2x/spice2x.github.io/wiki/Spice-SDK). ### Avoiding regressions From 8acd433ec6d217c400eec3a438e08dec66261a8c Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:20:41 -0700 Subject: [PATCH 03/17] api: notifications for video stream (#884) --- src/spice2x/api/stream_server.cpp | 32 +++++++++++++++++++++++++++ src/spice2x/api/websocket.cpp | 19 ++++++++++++---- src/spice2x/external/headsocket.h | 5 ++++- src/spice2x/overlay/notifications.cpp | 4 +++- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/spice2x/api/stream_server.cpp b/src/spice2x/api/stream_server.cpp index 99d720d..bd3216c 100644 --- a/src/spice2x/api/stream_server.cpp +++ b/src/spice2x/api/stream_server.cpp @@ -14,6 +14,7 @@ #include "capture_pump.h" #include "hooks/graphics/graphics.h" +#include "overlay/notifications.h" #include "stream_format.h" #include "util/logging.h" #include "util/utils.h" @@ -178,6 +179,13 @@ namespace api { // carry it too, or the client sees an opaque failure instead of the status. constexpr const char *cors_header = "Access-Control-Allow-Origin: *\r\n"; + // the port is unauthenticated, so a scanner hammering a busy/missing screen could + // otherwise flood the overlay; throttle failure toasts per distinct cause. kept under + // a second so it only swallows that, not a legitimate reconnect - substream itself + // switches screens with a 300ms gap, and only backs off to a full second once a + // retry has actually failed + constexpr double notification_throttle_seconds = 0.5; + void send_error(SOCKET socket, const char *status) { const std::string response = std::string("HTTP/1.0 ") + status + "\r\n" @@ -334,6 +342,11 @@ namespace api { if (slot < 0) { log_warning("api::stream", "client limit of {} hit", client_limit); + overlay::notifications::add_throttled( + overlay::notifications::Severity::Warning, + "api::stream.client_limit", + notification_throttle_seconds, + fmt::format("Video stream refused: client limit reached ({})", address)); send_error(client, "503 Service Unavailable"); closesocket(client); continue; @@ -410,16 +423,32 @@ namespace api { if (!streamable(screen)) { log_warning("api::stream", "screen {} is not available, refusing {}", screen, address); + overlay::notifications::add_throttled( + overlay::notifications::Severity::Warning, + fmt::format("api::stream.screen_unavailable.{}", screen), + notification_throttle_seconds, + fmt::format("Video stream refused: screen {} not available ({})", + screen, address)); send_error(socket, "404 Not Found"); } else if (!capture_pump::claim_screen(screen)) { log_warning("api::stream", "screen {} is already being streamed, refusing {}", screen, address); + overlay::notifications::add_throttled( + overlay::notifications::Severity::Warning, + fmt::format("api::stream.screen_claimed.{}", screen), + notification_throttle_seconds, + fmt::format("Video stream refused: screen {} already streaming ({})", + screen, address)); send_error(socket, "503 Service Unavailable"); } else { log_info("api::stream", "client connected: {} ({}, screen={}, fps={}, quality={})", address, request.path, screen, fps, quality); + overlay::notifications::add( + overlay::notifications::Severity::Success, + fmt::format("Video stream client connected ({}, screen {})", + address, screen)); const std::string header = "HTTP/1.0 200 OK\r\n" @@ -460,6 +489,9 @@ namespace api { capture_pump::release_screen(screen); log_info("api::stream", "client disconnected: {}", address); + overlay::notifications::add( + overlay::notifications::Severity::Info, + fmt::format("Video stream client disconnected ({})", address)); } } } diff --git a/src/spice2x/api/websocket.cpp b/src/spice2x/api/websocket.cpp index 18dec87..be7c67c 100644 --- a/src/spice2x/api/websocket.cpp +++ b/src/spice2x/api/websocket.cpp @@ -37,6 +37,15 @@ namespace api { private: ClientState *state = nullptr; + // headsocket doesn't expose the peer address on its own client API, but the + // sockaddr_in captured at accept time is sitting right there in the impl + std::string remote_address() const { + char address_data[INET_ADDRSTRLEN] {}; + inet_ntop(AF_INET, &this->_p->conn.impl()->from.sin_addr, + address_data, INET_ADDRSTRLEN); + return std::string(address_data); + } + protected: bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) override; @@ -120,19 +129,21 @@ namespace api { srv->websocket->controller->init_state(state); // log connection - log_info("api::websocket", "client connected"); + const auto address = this->remote_address(); + log_info("api::websocket", "client connected: {}", address); overlay::notifications::add( overlay::notifications::Severity::Success, - "API websocket client connected"); + fmt::format("API websocket client connected ({})", address)); } void WebSocketClient::on_disconnect() { // log disconnection - log_info("api::websocket", "client disconnected"); + const auto address = this->remote_address(); + log_info("api::websocket", "client disconnected: {}", address); overlay::notifications::add( overlay::notifications::Severity::Info, - "API websocket client disconnected"); + fmt::format("API websocket client disconnected ({})", address)); // get pointer to server auto srv = reinterpret_cast(server().get()); diff --git a/src/spice2x/external/headsocket.h b/src/spice2x/external/headsocket.h index e96a349..fdad8c5 100644 --- a/src/spice2x/external/headsocket.h +++ b/src/spice2x/external/headsocket.h @@ -1450,7 +1450,10 @@ void basic_tcp_server::accept_thread() while (_p->isRunning) { detail::connection_impl conn_impl; - conn_impl.socket = ::accept(_p->serverSocket, reinterpret_cast(&conn_impl.from), nullptr); + // addrlen must be a valid in/out pointer or the OS leaves conn_impl.from untouched, + // so the peer address silently reads back as 0.0.0.0 + int from_len = sizeof(conn_impl.from); + conn_impl.socket = ::accept(_p->serverSocket, reinterpret_cast(&conn_impl.from), &from_len); conn_impl.id = _p->nextClientID++; if (!_p->nextClientID) diff --git a/src/spice2x/overlay/notifications.cpp b/src/spice2x/overlay/notifications.cpp index ae3402b..da3bcc3 100644 --- a/src/spice2x/overlay/notifications.cpp +++ b/src/spice2x/overlay/notifications.cpp @@ -172,7 +172,9 @@ namespace overlay::notifications { // small gutter past the accent bar, then wrapped text ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f)); ImGui::SameLine(); - ImGui::PushTextWrapPos(win_pos.x + win_size.x - apply_scaling(TOAST_PAD_X)); + // wrap pos is in window-local space, not screen space - adding win_pos.x here + // pushed the boundary far past the window's own width, so it never wrapped + ImGui::PushTextWrapPos(win_size.x - apply_scaling(TOAST_PAD_X)); ImGui::TextUnformatted(n.text.c_str()); ImGui::PopTextWrapPos(); From d51de976b1c3159b14495aff56bcc8e746d82b82 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:11:07 -0700 Subject: [PATCH 04/17] graphics: move captures off-thread when streaming (#885) ## Link to GitHub Issue or related Pull Request, if one exists #0 ## Description of change Moves the API capture readback off the game's Present thread while a video stream client is connected. The readback is a `LockRect` plus a memcpy of the whole back buffer, roughly 635us at 720p and 1270us at 1080p. On the Present thread that comes out of the game's frame budget: TDJ (at 120Hz) dropped to 117fps with a 60fps stream running, and reading on a pool thread instead gave the full 120 back. Only streaming takes the off-thread path, gated on a new `capture_pump::screen_claimed()`. Screenshots, one-off API captures, and `THREAD_BAN` games all keep the existing inline read for compat reasons. A pool thread in `LockRect` while the Present thread sat inside `GetRenderTargetData` deadlocks DDR X2 for example. `CLAIMED[]` becomes `std::atomic` so the capture path does not take a lock on the Present thread. The read pool has a single worker so frames cannot be enqueued out of order, and both capture pools are never destroyed so a late read cannot queue onto a torn-down pool. The capture pipeline itself is unchanged: `GetRenderTargetData` is still synchronous on the Present thread. ## Testing DDR X2 World IIDX TDJ SDVX VM --- src/spice2x/api/capture_pump.cpp | 23 +++--- src/spice2x/api/capture_pump.h | 3 + .../backends/d3d9/d3d9_screenshot.cpp | 80 +++++++++++++++++-- src/spice2x/launcher/options.cpp | 5 +- 4 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/spice2x/api/capture_pump.cpp b/src/spice2x/api/capture_pump.cpp index fe75e56..ecedcab 100644 --- a/src/spice2x/api/capture_pump.cpp +++ b/src/spice2x/api/capture_pump.cpp @@ -1,6 +1,7 @@ #include "capture_pump.h" #include +#include #include #include "hooks/graphics/graphics.h" @@ -11,8 +12,8 @@ namespace api::capture_pump { std::array CONSUMER_M; - std::mutex CLAIMED_M; - std::array CLAIMED {}; + // read once per capture from the present thread, so it stays lock free + std::atomic CLAIMED[GRAPHICS_CAPTURE_SCREEN_NO] {}; bool valid_screen(int screen) { return 0 <= screen && screen < static_cast(GRAPHICS_CAPTURE_SCREEN_NO); @@ -37,14 +38,7 @@ namespace api::capture_pump { return false; } - std::lock_guard lock(CLAIMED_M); - - if (CLAIMED[screen]) { - return false; - } - - CLAIMED[screen] = true; - return true; + return !CLAIMED[screen].exchange(true); } void release_screen(int screen) { @@ -52,7 +46,14 @@ namespace api::capture_pump { return; } - std::lock_guard lock(CLAIMED_M); CLAIMED[screen] = false; } + + bool screen_claimed(int screen) { + if (!valid_screen(screen)) { + return false; + } + + return CLAIMED[screen]; + } } diff --git a/src/spice2x/api/capture_pump.h b/src/spice2x/api/capture_pump.h index b8ae9d0..650fb38 100644 --- a/src/spice2x/api/capture_pump.h +++ b/src/spice2x/api/capture_pump.h @@ -21,4 +21,7 @@ namespace api::capture_pump { // a screen carries one stream at a time; false when another connection already holds it bool claim_screen(int screen); void release_screen(int screen); + + // true while a video stream client holds this screen + bool screen_claimed(int screen); } diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp index e23f09c..ed8ddfb 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp @@ -15,6 +15,7 @@ #include #include +#include "api/capture_pump.h" #include "avs/game.h" #include "hooks/graphics/graphics.h" #include "misc/clipboard.h" @@ -169,6 +170,13 @@ ThreadPool &encode_pool() { return *instance; } +// where a capture's pixels are converted and handed to the api. never destroyed: the read +// pool below can still be working at process exit, and it queues onto this one +ThreadPool &capture_save_pool() { + static auto *instance = new ThreadPool(2); + return *instance; +} + // normalize the supported D3D formats to packed 24bpp RGB. callers screen the // format through surface_pixel_size first, so the black fill below is a fallback void surface_to_rgb( @@ -437,11 +445,49 @@ static void dispatch_capture_save(PendingCapture capture) { if (image_processing_must_be_inline()) { capture_process(); } else { - static auto pool = ThreadPool(2); - pool.add(std::move(capture_process)); + capture_save_pool().add(std::move(capture_process)); } } +// destroying the BackbufferCopy returns its surface to the pool, which is a device call, so +// it has to happen on whichever thread was cleared to do the read +static void read_and_dispatch_capture(int screen, BackbufferCopy copy) { + PendingCapture capture; + if (!read_capture_surface(copy, capture)) { + graphics_capture_skip(screen); + return; + } + + dispatch_capture_save(std::move(capture)); +} + +// Whether the readback runs on the present thread or a pool thread trades the game's frame +// time against the risk of two threads being inside the device at once. +// +// The read is a LockRect plus a row by row memcpy of the whole back buffer: roughly 635us at +// 720p and 1270us at 1080p. On the present thread that comes straight out of the game's frame +// budget, and at 120Hz with a 60fps stream running it measured as a drop to 117fps. Moving it +// to a pool thread gave the full 120 back. +// +// Only streaming is worth that trade. It is the only path that pays the cost on every frame, +// and it is the only one the user has opted into by connecting a client. Screenshots and the +// one off api captures stay inline: they are rare enough that a single slow frame does not +// matter, and the hazard being avoided is reproduced rather than theoretical, since a pool +// thread in LockRect while the present thread sat inside GetRenderTargetData deadlocked +// DDR X2, whose device has no internal locking. Games already known to dislike threaded image +// processing are excluded as well, on the assumption that whatever breaks them applies here. +static bool capture_read_off_thread(int screen) { + return api::capture_pump::screen_claimed(screen) && !image_processing_must_be_inline(); +} + +ThreadPool &capture_read_pool() { + // one worker, so reads finish in the order they were submitted: a second worker could + // overtake a descheduled one and enqueue a stale frame over a newer one. never destroyed, + // so a read still running at process exit cannot touch a dead pool + static auto *instance = new ThreadPool(1); + return *instance; +} + // by this point the pixels are plain memory, so none of this needs the device static void dispatch_screenshot_save(std::vector writes, size_t screen_count) { auto screenshot_process = [writes = std::move(writes), screen_count]() mutable { @@ -623,14 +669,34 @@ static void process_image_request( } if (!screenshot) { - PendingCapture capture; - if (!read_capture_surface(copies.front(), capture)) { - graphics_capture_skip(request.screen); + auto copy = std::move(copies.front()); + copies.clear(); + + if (capture_read_off_thread(request.screen)) { + try { + capture_read_pool().add( + [screen = request.screen, copy = std::move(copy)]() mutable { + // an escape from here would cross a thread boundary and terminate + try { + read_and_dispatch_capture(screen, std::move(copy)); + } catch (const std::exception &error) { + log_warning("graphics::d3d9", "capture read failed: {}", error.what()); + graphics_capture_skip(screen); + } catch (...) { + log_warning("graphics::d3d9", "capture read failed"); + graphics_capture_skip(screen); + } + }); + } catch (const std::exception &) { + // the copy went into the lambda before the queue could fail, so there is + // nothing left to read here and the client misses this frame + graphics_capture_skip(request.screen); + } + return; } - copies.clear(); - dispatch_capture_save(std::move(capture)); + read_and_dispatch_capture(request.screen, std::move(copy)); return; } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index de7beab..3bd1e1e 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1745,7 +1745,7 @@ static const std::vector OPTION_DEFINITIONS = { }, { // APIStreamEnable - .title = "API Video Stream Server Enable", + .title = "API Video Stream Server Enable (EXPERIMENTAL)", .name = "apistream", .desc = "Serves the mirrored screen as a video stream, on the API port plus two; " "alternative to API screen capture. Requires -api.\n\n" @@ -1754,7 +1754,8 @@ static const std::vector OPTION_DEFINITIONS = { "Parameters: screen (0-3), fps (1-60, default 30), q (1-100, default 70).\n\n" "Example with -api 1337: http://host:1339/stream.h264?fps=30&q=70\n\n" "VIEW ONLY - touch input still requires -api. " - "No password protection or encryption of any kind; video sent in the clear!", + "No password protection or encryption of any kind; video sent in the clear!\n\n" + "Streaming is known to cause older games to hang and crash.", .type = OptionType::Bool, .category = "Companion & API", }, From 95f10ed7333eb1e77e6c213d8a88e3ff23402564 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:38:48 -0700 Subject: [PATCH 05/17] api: get_streams (#886) ## Link to GitHub Issue or related Pull Request, if one exists #0 ## Description of change MSE method of streaming video needs the dimensions up front. This new api delivers that. Also provides an easier way to determine which screens are available and a way to discover port number. ## Testing See pending changes in substream project --- src/spice2x/README.md | 27 ++++- src/spice2x/api/modules/capture.cpp | 98 +++++++++++++++++++ src/spice2x/api/modules/capture.h | 1 + .../dart/spiceapi-websocket/spiceapi.dart | 1 + .../spiceapi-websocket/src/connection.dart | 4 +- .../src/wrappers/capture.dart | 47 +++++++++ .../dart/spiceapi/src/wrappers/capture.dart | 11 ++- .../api/resources/python/spiceapi/__init__.py | 1 + .../api/resources/python/spiceapi/capture.py | 33 +++++++ src/spice2x/api/stream_format.cpp | 14 +++ src/spice2x/api/stream_format.h | 5 + src/spice2x/api/stream_server.cpp | 17 ++++ src/spice2x/api/stream_server.h | 3 + src/spice2x/hooks/graphics/graphics.cpp | 21 ++++ src/spice2x/hooks/graphics/graphics.h | 3 + 15 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/capture.dart create mode 100644 src/spice2x/api/resources/python/spiceapi/capture.py diff --git a/src/spice2x/README.md b/src/spice2x/README.md index 11f5bfd..4425487 100644 --- a/src/spice2x/README.md +++ b/src/spice2x/README.md @@ -127,6 +127,22 @@ doesn't matter since the TCP protocol doesn't allow for out of order data, however this may change when/if support for UDP is being introduced. The only restriction is that the ID has to be a valid 64-bit unsigned integer. +#### Capture +- get_screens() + - returns the screen numbers the game has registered for capture +- get_jpg(screen: uint, quality: uint, divide: uint) + - returns the timestamp, width, height and base64 encoded JPEG of one screen + - all parameters are optional and default to screen 0, quality 70, divide 1 + - divide shrinks the image by that factor before encoding +- get_streams() + - returns a dict describing the HTTP video stream, or no data at all when + `-apistream` is not enabled and there is nothing to describe + - `port` is the stream server port + - `formats` lists the wire formats this build serves, each with a `name` + (`h264` or `mjpeg`) and the `path` to request them on + - `screens` lists every capturable screen with its `width`, `height`, and + `busy` + #### Card - insert(index: uint, card_id: hex) - inserts a card which gets read by the emulated card readers for the game @@ -298,6 +314,10 @@ stream over plain HTTP. Enable it with `-apistream`. It listens on the API port plus two, in the same way the WebSocket server uses the API port plus one, so `-api 1337` puts the stream on 1339. This means `-api` has to be enabled too. +Rather than working the port out, clients should ask the JSON API for it with +`capture.get_streams()`, which also reports which of the formats below this +build serves, the size of each screen and whether one is already taken. + Two formats are served: http://host:1339/stream.mjpg JPEG frames, multipart/x-mixed-replace @@ -323,9 +343,10 @@ The stream is view only. Touch and other input still go through the JSON API, so a companion app needs both. There is no authentication on the stream port - anyone who can reach it can watch the screen. -WinXP builds have no video stream. Neither encoder is compiled in, so every -endpoint returns 404, and the JSON API's JPEG screen capture is unavailable for -the same reason. +WinXP builds have no video stream. Neither encoder is compiled in, so nothing +listens on the stream port even with `-apistream`, `capture.get_streams()` +returns no data, and the JSON API's JPEG screen capture is unavailable for the +same reason. ## Native wrapper libraries Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python. diff --git a/src/spice2x/api/modules/capture.cpp b/src/spice2x/api/modules/capture.cpp index b8ab2b1..644d944 100644 --- a/src/spice2x/api/modules/capture.cpp +++ b/src/spice2x/api/modules/capture.cpp @@ -1,8 +1,11 @@ #include "capture.h" +#include #include #include #include #include "api/capture_pump.h" +#include "api/stream_format.h" +#include "api/stream_server.h" #include "external/rapidjson/document.h" #include "hooks/graphics/graphics.h" #include "hooks/graphics/jpeg_encoder.h" @@ -71,6 +74,7 @@ namespace api::modules { Capture::Capture() : Module("capture") { functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2); functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2); + functions["get_streams"] = std::bind(&Capture::get_streams, this, _1, _2); } /** @@ -141,4 +145,98 @@ namespace api::modules { CAPTURE_BUFFER.clear(); try_cached_response(screen, res); } + + /** + * get_streams() + */ + void Capture::get_streams(Request &req, Response &res) { + + auto &alloc = res.doc()->GetAllocator(); + + // nothing is listening without -apistream, so there is no stream to describe + const unsigned short port = stream_server_port(); + if (port == 0) { + return; + } + + Value formats(kArrayType); + for (const auto &[name, path] : stream_formats()) { + Value entry(kObjectType); + entry.AddMember("name", Value(name.c_str(), alloc), alloc); + entry.AddMember("path", Value(path.c_str(), alloc), alloc); + formats.PushBack(entry, alloc); + } + + std::vector screen_numbers; + graphics_screens_get(screen_numbers); + + // measuring a screen nobody has captured yet waits for the game to present, which can + // take as long as the whole request is allowed, so only one screen is measured per + // call and the rest are reported null until a later one settles them. which screen + // gets the attempt rotates, otherwise one that never presents would take every + // request and the screens behind it would stay unmeasured forever + int probe_screen = -1; + { + std::vector unmeasured; + for (const auto screen : screen_numbers) { + if (screen < static_cast(GRAPHICS_CAPTURE_SCREEN_NO) + && !graphics_capture_last_size(screen, nullptr, nullptr) + && !capture_pump::screen_claimed(screen)) { + unmeasured.push_back(screen); + } + } + + if (!unmeasured.empty()) { + static std::atomic probe_cursor { 0 }; + probe_screen = unmeasured[probe_cursor.fetch_add(1) % unmeasured.size()]; + } + } + + Value screens(kArrayType); + for (const auto screen : screen_numbers) { + if (screen >= static_cast(GRAPHICS_CAPTURE_SCREEN_NO)) { + continue; + } + + int width = 0; + int height = 0; + bool known = graphics_capture_last_size(screen, &width, &height); + + // a probe holds the screen for as long as it waits, so a second caller arriving + // during one would queue behind it and then take a wait of its own; let it report + // the screen as unmeasured instead and pick the size up once the first is done + static std::atomic probe_running { false }; + if (!known && screen == probe_screen && !probe_running.exchange(true)) { + std::shared_ptr pixels; + known = capture_pump::capture_direct( + screen, pixels, 1, nullptr, &width, &height); + probe_running = false; + } + + // a screen of unknown size cannot be described, and a client told about it could + // not size its decoder anyway; leaving it out until it has been measured beats + // handing over an entry that has to be treated as absent + if (!known) { + continue; + } + + Value entry(kObjectType); + entry.AddMember("screen", screen, alloc); + entry.AddMember("width", width, alloc); + entry.AddMember("height", height, alloc); + + // a screen carries one viewer at a time, so this is what decides whether a client + // can connect at all; still racy by the time it does, only more honest than not + entry.AddMember("busy", capture_pump::screen_claimed(screen), alloc); + + screens.PushBack(entry, alloc); + } + + Value info(kObjectType); + info.AddMember("port", port, alloc); + info.AddMember("formats", formats, alloc); + info.AddMember("screens", screens, alloc); + + res.add_data(info); + } } diff --git a/src/spice2x/api/modules/capture.h b/src/spice2x/api/modules/capture.h index fb8c55d..b994f3a 100644 --- a/src/spice2x/api/modules/capture.h +++ b/src/spice2x/api/modules/capture.h @@ -19,5 +19,6 @@ namespace api::modules { // function definitions void get_screens(Request &req, Response &res); void get_jpg(Request &req, Response &res); + void get_streams(Request &req, Response &res); }; } diff --git a/src/spice2x/api/resources/dart/spiceapi-websocket/spiceapi.dart b/src/spice2x/api/resources/dart/spiceapi-websocket/spiceapi.dart index 413db56..1491e74 100644 --- a/src/spice2x/api/resources/dart/spiceapi-websocket/spiceapi.dart +++ b/src/spice2x/api/resources/dart/spiceapi-websocket/spiceapi.dart @@ -11,6 +11,7 @@ part "src/exceptions.dart"; part "src/rc4.dart"; part "src/wrappers/analogs.dart"; part "src/wrappers/buttons.dart"; +part "src/wrappers/capture.dart"; part "src/wrappers/card.dart"; part "src/wrappers/coin.dart"; part "src/wrappers/control.dart"; diff --git a/src/spice2x/api/resources/dart/spiceapi-websocket/src/connection.dart b/src/spice2x/api/resources/dart/spiceapi-websocket/src/connection.dart index a27ec3f..089bbff 100644 --- a/src/spice2x/api/resources/dart/spiceapi-websocket/src/connection.dart +++ b/src/spice2x/api/resources/dart/spiceapi-websocket/src/connection.dart @@ -4,8 +4,8 @@ part of spiceapi; class Connection { // settings - static const _TIMEOUT = Duration(seconds: 2); - static const _BUFFER_SIZE = 1024 * 8; + static const _TIMEOUT = Duration(seconds: 3); + static const _BUFFER_SIZE = 1024 * 1024 * 8; // state final String host, pass; diff --git a/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/capture.dart b/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/capture.dart new file mode 100644 index 0000000..a6c3b88 --- /dev/null +++ b/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/capture.dart @@ -0,0 +1,47 @@ +part of spiceapi; + +class CaptureData { + int timestamp; + int width, height; + Uint8List data; +} + +var _base64DecoderInstance = Base64Decoder(); + +Future captureGetScreens(Connection con) { + var req = Request("capture", "get_screens"); + return con.request(req).then((res) { + return res.getData(); + }); +} + +Future captureGetJPG(Connection con, { + int screen = 0, + int quality = 70, + int divide = 1, +}) { + var req = Request("capture", "get_jpg"); + req.addParam(screen); + req.addParam(quality); + req.addParam(divide); + return con.request(req).then((res) { + var captureData = CaptureData(); + var data = res.getData(); + if (data.length > 0) captureData.timestamp = data[0]; + if (data.length > 1) captureData.width = data[1]; + if (data.length > 2) captureData.height = data[2]; + if (data.length > 3) { + captureData.data = _base64DecoderInstance.convert(data[3]); + } + return captureData; + }); +} + +/// Describes the HTTP video stream, or null when this spice2x serves none. +Future captureGetStreams(Connection con) { + var req = Request("capture", "get_streams"); + return con.request(req).then((res) { + var data = res.getData(); + return data.length > 0 ? data[0] : null; + }); +} diff --git a/src/spice2x/api/resources/dart/spiceapi/src/wrappers/capture.dart b/src/spice2x/api/resources/dart/spiceapi/src/wrappers/capture.dart index be15f14..a6c3b88 100644 --- a/src/spice2x/api/resources/dart/spiceapi/src/wrappers/capture.dart +++ b/src/spice2x/api/resources/dart/spiceapi/src/wrappers/capture.dart @@ -17,7 +17,7 @@ Future captureGetScreens(Connection con) { Future captureGetJPG(Connection con, { int screen = 0, - int quality = 60, + int quality = 70, int divide = 1, }) { var req = Request("capture", "get_jpg"); @@ -36,3 +36,12 @@ Future captureGetJPG(Connection con, { return captureData; }); } + +/// Describes the HTTP video stream, or null when this spice2x serves none. +Future captureGetStreams(Connection con) { + var req = Request("capture", "get_streams"); + return con.request(req).then((res) { + var data = res.getData(); + return data.length > 0 ? data[0] : null; + }); +} diff --git a/src/spice2x/api/resources/python/spiceapi/__init__.py b/src/spice2x/api/resources/python/spiceapi/__init__.py index f0da213..161201a 100644 --- a/src/spice2x/api/resources/python/spiceapi/__init__.py +++ b/src/spice2x/api/resources/python/spiceapi/__init__.py @@ -2,6 +2,7 @@ from .connection import Connection from .request import Request from .analogs import * from .buttons import * +from .capture import * from .card import * from .coin import * from .control import * diff --git a/src/spice2x/api/resources/python/spiceapi/capture.py b/src/spice2x/api/resources/python/spiceapi/capture.py new file mode 100644 index 0000000..f35b094 --- /dev/null +++ b/src/spice2x/api/resources/python/spiceapi/capture.py @@ -0,0 +1,33 @@ +import base64 + +from .connection import Connection +from .request import Request + + +def capture_get_screens(con: Connection): + res = con.request(Request("capture", "get_screens")) + return res.get_data() + + +def capture_get_jpg(con: Connection, screen: int = 0, quality: int = 70, divide: int = 1): + req = Request("capture", "get_jpg") + req.add_param(screen) + req.add_param(quality) + req.add_param(divide) + data = con.request(req).get_data() + + if len(data) < 4: + return None + + return { + "timestamp": data[0], + "width": data[1], + "height": data[2], + "data": base64.b64decode(data[3]), + } + + +def capture_get_streams(con: Connection): + """Describes the HTTP video stream, or None when this spice2x serves none.""" + data = con.request(Request("capture", "get_streams")).get_data() + return data[0] if data else None diff --git a/src/spice2x/api/stream_format.cpp b/src/spice2x/api/stream_format.cpp index 0fce3ce..4a06971 100644 --- a/src/spice2x/api/stream_format.cpp +++ b/src/spice2x/api/stream_format.cpp @@ -67,4 +67,18 @@ namespace api { return nullptr; } + + std::vector> stream_formats() { + std::vector> formats; + +#ifdef SPICE_JPEG + formats.emplace_back("mjpeg", "/stream.mjpg"); +#endif + +#ifdef SPICE_H264 + formats.emplace_back("h264", "/stream.h264"); +#endif + + return formats; + } } diff --git a/src/spice2x/api/stream_format.h b/src/spice2x/api/stream_format.h index ed04fb9..a0268de 100644 --- a/src/spice2x/api/stream_format.h +++ b/src/spice2x/api/stream_format.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "capture_pump.h" @@ -35,4 +37,7 @@ namespace api { // null when the path does not name a format this build supports std::unique_ptr make_stream_writer( const std::string &path, int quality, int fps); + + // name and path of every format compiled into this build, for clients to pick from + std::vector> stream_formats(); } diff --git a/src/spice2x/api/stream_server.cpp b/src/spice2x/api/stream_server.cpp index bd3216c..6c9ee2f 100644 --- a/src/spice2x/api/stream_server.cpp +++ b/src/spice2x/api/stream_server.cpp @@ -195,11 +195,25 @@ namespace api { "\r\n"; send_all(socket, response); } + + std::atomic LISTENING_PORT { 0 }; + } + + unsigned short stream_server_port() { + return LISTENING_PORT.load(); } StreamServer::StreamServer(unsigned short port) : port(port) { + // WinXP builds compile in neither encoder, so there would be nothing to serve and + // every request would 404; taking the port instead only invites confused clients + if (stream_formats().empty()) { + log_warning("api::stream", + "this build has no video encoders, the video stream is unavailable"); + return; + } + if (!this->open_listener()) { // the stream was asked for explicitly, so say plainly that it is not there log_warning("api::stream", @@ -212,6 +226,8 @@ namespace api { this->accept_worker(); }); + LISTENING_PORT = this->port; + // deliberately not logging a full URL; local IPs would leak into shared logs log_info("api::stream", "video stream is listening on port: {}", this->port); log_warning("api::stream", @@ -269,6 +285,7 @@ namespace api { StreamServer::~StreamServer() { this->running = false; + LISTENING_PORT = 0; if (this->listener != INVALID_SOCKET) { closesocket(this->listener); diff --git a/src/spice2x/api/stream_server.h b/src/spice2x/api/stream_server.h index 0e2646e..d8747d0 100644 --- a/src/spice2x/api/stream_server.h +++ b/src/spice2x/api/stream_server.h @@ -11,6 +11,9 @@ namespace api { + // 0 while no stream server is listening, so the API can tell clients not to look for one + unsigned short stream_server_port(); + class StreamServer { public: diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 6aa5866..61c5701 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -1481,6 +1481,27 @@ void graphics_capture_skip(int screen) { GRAPHICS_CAPTURE_CV[screen].notify_one(); } +bool graphics_capture_last_size(int screen, int *width, int *height) { + if (screen < 0 || screen >= static_cast(GRAPHICS_CAPTURE_SCREEN_NO)) { + return false; + } + + // consuming a frame clears the pixels but leaves the size, so this survives the read + std::lock_guard lock(GRAPHICS_CAPTURE_BUFFER_M[screen]); + const auto &capture = GRAPHICS_CAPTURE_BUFFER[screen]; + if (!capture.width || !capture.height) { + return false; + } + + if (width != nullptr) { + *width = capture.width; + } + if (height != nullptr) { + *height = capture.height; + } + return true; +} + bool graphics_capture_receive_raw(int screen, std::shared_ptr &out, int divide, uint64_t *timestamp, int *width, int *height) { diff --git a/src/spice2x/hooks/graphics/graphics.h b/src/spice2x/hooks/graphics/graphics.h index 4d85ed8..72e7d13 100644 --- a/src/spice2x/hooks/graphics/graphics.h +++ b/src/spice2x/hooks/graphics/graphics.h @@ -146,6 +146,9 @@ void graphics_capture_trigger(int screen); bool graphics_capture_consume(int *screen); void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height); void graphics_capture_skip(int screen); +// size of the last frame captured off this screen, before any caller side downscale; false +// until one has been captured, so it cannot report a size for a screen the game never drew +bool graphics_capture_last_size(int screen, int *width, int *height); // on success `out` owns packed 24bpp RGB pixels, width * height * 3 bytes bool graphics_capture_receive_raw(int screen, std::shared_ptr &out, int divide = 0, From e7b90271ca06e121b2136f0b159facc33bad2085 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:47:26 -0700 Subject: [PATCH 06/17] api: serve the websocket with wslay instead of headsocket (#887) `HeadSocket` library we are currently using is full of bugs and no longer maintained. Swap it out with `wslay`. --- src/spice2x/CMakeLists.txt | 13 +- src/spice2x/api/websocket.cpp | 731 ++++-- src/spice2x/external/headsocket.h | 2195 ----------------- src/spice2x/external/wslay/CMakeLists.txt | 33 + src/spice2x/external/wslay/LICENSE | 22 + src/spice2x/external/wslay/config.h | 9 + .../external/wslay/includes/wslay/wslay.h | 841 +++++++ .../external/wslay/includes/wslay/wslayver.h | 31 + src/spice2x/external/wslay/msvc_compat.h | 9 + src/spice2x/external/wslay/wslay_event.c | 1082 ++++++++ src/spice2x/external/wslay/wslay_event.h | 138 ++ src/spice2x/external/wslay/wslay_frame.c | 438 ++++ src/spice2x/external/wslay/wslay_frame.h | 77 + src/spice2x/external/wslay/wslay_macro.h | 39 + src/spice2x/external/wslay/wslay_net.c | 35 + src/spice2x/external/wslay/wslay_net.h | 54 + src/spice2x/external/wslay/wslay_queue.c | 77 + src/spice2x/external/wslay/wslay_queue.h | 53 + src/spice2x/licenses.txt | 46 + 19 files changed, 3538 insertions(+), 2385 deletions(-) delete mode 100644 src/spice2x/external/headsocket.h create mode 100644 src/spice2x/external/wslay/CMakeLists.txt create mode 100644 src/spice2x/external/wslay/LICENSE create mode 100644 src/spice2x/external/wslay/config.h create mode 100644 src/spice2x/external/wslay/includes/wslay/wslay.h create mode 100644 src/spice2x/external/wslay/includes/wslay/wslayver.h create mode 100644 src/spice2x/external/wslay/msvc_compat.h create mode 100644 src/spice2x/external/wslay/wslay_event.c create mode 100644 src/spice2x/external/wslay/wslay_event.h create mode 100644 src/spice2x/external/wslay/wslay_frame.c create mode 100644 src/spice2x/external/wslay/wslay_frame.h create mode 100644 src/spice2x/external/wslay/wslay_macro.h create mode 100644 src/spice2x/external/wslay/wslay_net.c create mode 100644 src/spice2x/external/wslay/wslay_net.h create mode 100644 src/spice2x/external/wslay/wslay_queue.c create mode 100644 src/spice2x/external/wslay/wslay_queue.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 973b867..0faaa2b 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -254,6 +254,7 @@ add_subdirectory(external/discord-rpc EXCLUDE_FROM_ALL) add_subdirectory(external/hash-library EXCLUDE_FROM_ALL) add_subdirectory(external/imgui EXCLUDE_FROM_ALL) add_subdirectory(external/minhook EXCLUDE_FROM_ALL) +add_subdirectory(external/wslay EXCLUDE_FROM_ALL) add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL) # libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own @@ -797,7 +798,7 @@ endfunction() add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES}) target_link_libraries(spicetools_spice_objs PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) target_link_libraries(spicetools_spice_objs PUBLIC winscard) if(NOT MSVC) @@ -837,7 +838,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES}) target_link_libraries(spicetools_spice_linux PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "") set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux") target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1) @@ -855,7 +856,7 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES}) # do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break target_link_libraries(spicetools_spice64 PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2 - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) target_link_libraries(spicetools_spice64 PUBLIC winscard) set_target_properties(spicetools_spice64 PROPERTIES PREFIX "") set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64") @@ -878,7 +879,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES}) # do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break target_link_libraries(spicetools_spice64_linux PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2 - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "") set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux") target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1) @@ -900,7 +901,7 @@ add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) # the configurator serves neither the API nor the video stream, so it needs no codecs target_link_libraries(spicetools_cfg PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features) target_link_libraries(spicetools_cfg PUBLIC winscard) set_target_properties(spicetools_cfg PROPERTIES PREFIX "") set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg") @@ -918,7 +919,7 @@ set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D. add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) target_link_libraries(spicetools_cfg_linux PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features) set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "") set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux") target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1) diff --git a/src/spice2x/api/websocket.cpp b/src/spice2x/api/websocket.cpp index be7c67c..f530c12 100644 --- a/src/spice2x/api/websocket.cpp +++ b/src/spice2x/api/websocket.cpp @@ -1,216 +1,579 @@ -#define HEADSOCKET_IMPLEMENTATION -#include "external/headsocket.h" +#include +#include #include "websocket.h" -#include "util/utils.h" -#include "util/rc4.h" -#include "util/logging.h" -#include "overlay/notifications.h" -#include "controller.h" -using namespace headsocket; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "external/wslay/msvc_compat.h" +#include + +#include "controller.h" +#include "external/hash-library/sha1.h" +#include "overlay/notifications.h" +#include "util/crypt.h" +#include "util/logging.h" 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; + constexpr int server_backlog = 4; + constexpr size_t client_limit = 8; - void set_recv_timeout(connection &conn, int milliseconds) { - DWORD timeout = static_cast(milliseconds); - setsockopt(conn.impl()->socket, SOL_SOCKET, SO_RCVTIMEO, + // a peer that connects and then says nothing must not hold a slot forever + constexpr int handshake_timeout_ms = 5000; + constexpr size_t request_size_limit = 8 * 1024; + constexpr uint64_t message_size_limit = 64 * 1024; + + // how long a quiet connection waits before the loop rechecks whether we are stopping + constexpr int idle_poll_ms = 500; + + // RFC 6455 appends this to the client key before hashing + constexpr const char *websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + constexpr double notification_throttle_seconds = 0.5; + + std::string trim(const std::string &text) { + const auto begin = text.find_first_not_of(" \t"); + if (begin == std::string::npos) { + return ""; + } + return text.substr(begin, text.find_last_not_of(" \t") - begin + 1); + } + + std::string to_lower(std::string text) { + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return text; + } + + bool contains_token(const std::string &value, const std::string &expected) { + size_t pos = 0; + while (pos < value.size()) { + const size_t end = value.find(',', pos); + if (to_lower(trim(value.substr(pos, end - pos))) == expected) { + return true; + } + if (end == std::string::npos) { + break; + } + pos = end + 1; + } + return false; + } + + bool valid_websocket_key(const std::string &key) { + if (key.size() != 24 || key[22] != '=' || key[23] != '=') { + return false; + } + + const std::string alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (size_t i = 0; i < 22; i++) { + if (alphabet.find(key[i]) == std::string::npos) { + return false; + } + } + return true; + } + + bool send_all(SOCKET socket, const std::string &text) { + size_t remaining = text.size(); + const char *cursor = text.data(); + + while (remaining > 0) { + const int sent = send(socket, cursor, static_cast(remaining), 0); + if (sent <= 0) { + return false; + } + cursor += sent; + remaining -= static_cast(sent); + } + + return true; + } + + void set_recv_timeout(SOCKET socket, int milliseconds) { + const DWORD timeout = static_cast(milliseconds); + setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)); } + + std::string accept_key(const std::string &client_key) { + SHA1 sha1; + sha1.add(client_key.data(), client_key.size()); + sha1.add(websocket_guid, strlen(websocket_guid)); + + unsigned char digest[SHA1::HashBytes] {}; + sha1.getHash(digest); + + return crypt::base64_encode(digest, sizeof(digest)); + } + + bool handshake(SOCKET socket) { + std::string request; + char byte = 0; + bool complete = false; + + while (request.size() < request_size_limit) { + const int read = recv(socket, &byte, 1, 0); + if (read <= 0) { + return false; + } + + request.push_back(byte); + if (request.size() >= 4 + && request.compare(request.size() - 4, 4, "\r\n\r\n") == 0) { + complete = true; + break; + } + } + + std::string key; + std::string version; + bool connection_upgrade = false; + bool upgrade = false; + const size_t request_line_end = request.find("\r\n"); + const std::string request_line = request.substr(0, request_line_end); + const size_t target_end = request_line.find(' ', 4); + const bool valid_request_line = request_line_end != std::string::npos + && request_line.compare(0, 4, "GET ") == 0 + && target_end != std::string::npos + && target_end > 4 + && target_end == request_line.rfind(' ') + && request_line.substr(target_end + 1) == "HTTP/1.1"; + size_t pos = request_line_end; + + while (pos != std::string::npos) { + const size_t end = request.find("\r\n", pos + 2); + if (end == std::string::npos || end == pos + 2) { + break; + } + + const std::string line = request.substr(pos + 2, end - pos - 2); + const size_t colon = line.find(':'); + if (colon != std::string::npos) { + const std::string name = to_lower(trim(line.substr(0, colon))); + const std::string value = trim(line.substr(colon + 1)); + + if (name == "sec-websocket-key") { + key = value; + } else if (name == "sec-websocket-version") { + version = value; + } else if (name == "connection") { + connection_upgrade = connection_upgrade + || contains_token(value, "upgrade"); + } else if (name == "upgrade") { + upgrade = upgrade || contains_token(value, "websocket"); + } + } + + pos = end; + } + + if (!complete || !valid_request_line || !connection_upgrade || !upgrade + || version != "13" || !valid_websocket_key(key)) { + send_all(socket, + "HTTP/1.1 400 Bad Request\r\n" + "Connection: close\r\n" + "Content-Length: 0\r\n" + "\r\n"); + return false; + } + + // deliberately echoes back no extension or subprotocol: naming one the client did + // not offer is a handshake failure, and none of them are wanted here + return send_all(socket, + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + accept_key(key) + "\r\n" + "\r\n"); + } + + // everything one connection needs; wslay hands this back to the callbacks + struct Session { + SOCKET socket = INVALID_SOCKET; + Controller *controller = nullptr; + ClientState *state = nullptr; + bool failed = false; + }; + + ssize_t recv_callback(wslay_event_context_ptr ctx, uint8_t *buffer, size_t length, + int flags, void *user_data) { + + (void) flags; + + auto *session = static_cast(user_data); + const int read = recv(session->socket, reinterpret_cast(buffer), + static_cast(length), 0); + + if (read > 0) { + return read; + } + + // the socket is non-blocking, so an empty one has to read as "nothing yet" + // rather than as a dead peer + if (read < 0 && WSAGetLastError() == WSAEWOULDBLOCK) { + wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK); + return -1; + } + + wslay_event_set_error(ctx, + read == 0 ? WSLAY_ERR_NO_MORE_MSG : WSLAY_ERR_CALLBACK_FAILURE); + return -1; + } + + ssize_t send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t length, + int flags, void *user_data) { + + (void) flags; + + auto *session = static_cast(user_data); + const int sent = send(session->socket, reinterpret_cast(data), + static_cast(length), 0); + + if (sent > 0) { + return sent; + } + + if (sent < 0 && WSAGetLastError() == WSAEWOULDBLOCK) { + wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK); + return -1; + } + + wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE); + return -1; + } + + void on_msg_recv(wslay_event_context_ptr ctx, + const struct wslay_event_on_msg_recv_arg *arg, void *user_data) { + + auto *session = static_cast(user_data); + + // pings and closes are wslay's business, it answers them itself + if (wslay_is_ctrl_frame(arg->opcode)) { + return; + } + + if (arg->opcode != WSLAY_BINARY_FRAME) { + log_warning("api::websocket", "ignoring a non-binary message"); + return; + } + + std::vector in(arg->msg, arg->msg + arg->msg_length); + std::vector out; + + if (session->state->cipher) { + session->state->cipher->crypt( + reinterpret_cast(in.data()), in.size()); + } + + session->controller->process_request(session->state, &in, &out); + + if (session->state->cipher) { + session->state->cipher->crypt( + reinterpret_cast(out.data()), out.size()); + } + + wslay_event_msg reply {}; + reply.opcode = WSLAY_BINARY_FRAME; + reply.msg = reinterpret_cast(out.data()); + reply.msg_length = out.size(); + + if (wslay_event_queue_msg(ctx, &reply) != 0) { + session->failed = true; + return; + } + + Controller::process_password_change(session->state); + } } - /* - * Client class declaration - */ - class WebSocketClient : public web_socket_client { - - // required class header - HEADSOCKET_CLIENT(WebSocketClient, web_socket_client); - - private: - ClientState *state = nullptr; - - // headsocket doesn't expose the peer address on its own client API, but the - // sockaddr_in captured at accept time is sitting right there in the impl - std::string remote_address() const { - char address_data[INET_ADDRSTRLEN] {}; - inet_ntop(AF_INET, &this->_p->conn.impl()->from.sin_addr, - address_data, INET_ADDRSTRLEN); - return std::string(address_data); - } - - protected: - bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) override; - - void on_accept() override; - void on_disconnect() override; - }; - - /* - * Server class declaration - */ - class WebSocketServer : public web_socket_server { - HEADSOCKET_SERVER(WebSocketServer, web_socket_server); - public: - 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() {} - - /* - * Controller state so we don't have to import headsocket stuff in our header - */ struct WebSocketControllerState { - std::shared_ptr server; + struct Client { + std::thread thread; + SOCKET socket = INVALID_SOCKET; + bool active = false; + }; + + Controller *controller = nullptr; + unsigned short port = 0; + SOCKET listener = INVALID_SOCKET; + bool wsa_started = false; + std::atomic_bool running { false }; + std::thread acceptor; + std::mutex clients_m; + std::array clients; + + bool open_listener(); + void accept_worker(); + void client_worker(int slot, SOCKET socket, std::string address); + void stop(); }; + bool WebSocketControllerState::open_listener() { + WSADATA wsa_data; + const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data); + if (error != 0) { + log_warning("api::websocket", "WSAStartup() returned {}", error); + return false; + } + this->wsa_started = true; + + this->listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (this->listener == INVALID_SOCKET) { + log_warning("api::websocket", "socket() returned {}", WSAGetLastError()); + return false; + } + + sockaddr_in address {}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = INADDR_ANY; + address.sin_port = htons(this->port); + + if (bind(this->listener, reinterpret_cast(&address), sizeof(address)) + == SOCKET_ERROR) { + log_warning("api::websocket", "bind() returned {}", WSAGetLastError()); + return false; + } + + if (listen(this->listener, server_backlog) == SOCKET_ERROR) { + log_warning("api::websocket", "listen() returned {}", WSAGetLastError()); + return false; + } + + return true; + } + + void WebSocketControllerState::accept_worker() { + + while (this->running) { + sockaddr_in client_address {}; + int client_address_size = sizeof(sockaddr_in); + + const SOCKET client = accept( + this->listener, reinterpret_cast(&client_address), + &client_address_size); + + if (client == INVALID_SOCKET) { + // on shutdown the listener is closed under us; otherwise do not spin + if (this->running) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + continue; + } + + if (!this->running) { + closesocket(client); + break; + } + + // formatted by hand rather than with inet_ntop, which needs a newer Windows than + // the XP toolchain targets, or inet_ntoa, which answers from a shared buffer + const uint32_t raw = ntohl(client_address.sin_addr.s_addr); + const std::string address = fmt::format("{}.{}.{}.{}", + (raw >> 24) & 0xff, (raw >> 16) & 0xff, (raw >> 8) & 0xff, raw & 0xff); + + int slot = -1; + { + std::lock_guard lock(this->clients_m); + for (size_t i = 0; i < this->clients.size(); i++) { + if (!this->clients[i].active) { + this->clients[i].active = true; + this->clients[i].socket = client; + slot = static_cast(i); + break; + } + } + } + + if (slot < 0) { + log_warning("api::websocket", "client limit of {} hit", client_limit); + overlay::notifications::add_throttled( + overlay::notifications::Severity::Warning, + "api::websocket.client_limit", + notification_throttle_seconds, + fmt::format("API websocket refused: client limit reached ({})", address)); + closesocket(client); + continue; + } + + // this thread is the only one that touches the thread objects, so the slot's + // previous occupant gets reaped here rather than being detached + if (this->clients[slot].thread.joinable()) { + this->clients[slot].thread.join(); + } + + // the handshake runs on the client thread on purpose: doing it here would put + // every later connection behind whatever this one is waiting for + this->clients[slot].thread = std::thread([this, slot, client, address] { + this->client_worker(slot, client, address); + }); + } + } + + void WebSocketControllerState::client_worker(int slot, SOCKET socket, std::string address) { + + // wslay writes a frame header and its payload as separate sends, so leaving Nagle on + // holds the payload back until the peer acknowledges the header, costing a delayed + // ack per message; requests here are small and latency sensitive + int nodelay = 1; + setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&nodelay), sizeof(nodelay)); + + set_recv_timeout(socket, handshake_timeout_ms); + + if (handshake(socket)) { + // wslay reads until the socket would block, so leaving it blocking would make + // every reply wait out a receive timeout before the send got a turn + u_long non_blocking = 1; + ioctlsocket(socket, FIONBIO, &non_blocking); + + Session session; + session.socket = socket; + session.controller = this->controller; + session.state = new ClientState(); + this->controller->init_state(session.state); + + log_info("api::websocket", "client connected: {}", address); + overlay::notifications::add( + overlay::notifications::Severity::Success, + fmt::format("API websocket client connected ({})", address)); + + wslay_event_callbacks callbacks {}; + callbacks.recv_callback = recv_callback; + callbacks.send_callback = send_callback; + callbacks.on_msg_recv_callback = on_msg_recv; + + wslay_event_context_ptr ctx = nullptr; + if (wslay_event_context_server_init(&ctx, &callbacks, &session) == 0) { + wslay_event_config_set_max_recv_msg_length(ctx, message_size_limit); + while (this->running && !session.failed + && (wslay_event_want_read(ctx) || wslay_event_want_write(ctx))) { + + fd_set read_set; + fd_set write_set; + FD_ZERO(&read_set); + FD_ZERO(&write_set); + + if (wslay_event_want_read(ctx)) { + FD_SET(socket, &read_set); + } + if (wslay_event_want_write(ctx)) { + FD_SET(socket, &write_set); + } + + // bounded so a silent connection still notices us shutting down + timeval timeout {}; + timeout.tv_usec = idle_poll_ms * 1000; + + if (select(0, &read_set, &write_set, nullptr, &timeout) < 0) { + break; + } + + if (FD_ISSET(socket, &read_set) && wslay_event_recv(ctx) != 0) { + break; + } + + // unconditional, so a reply queued by the read above goes out now + if (wslay_event_send(ctx) != 0) { + break; + } + } + + wslay_event_context_free(ctx); + } + + Controller::free_state(session.state); + delete session.state; + + log_info("api::websocket", "client disconnected: {}", address); + overlay::notifications::add( + overlay::notifications::Severity::Info, + fmt::format("API websocket client disconnected ({})", address)); + } + + closesocket(socket); + + std::lock_guard lock(this->clients_m); + this->clients[slot].socket = INVALID_SOCKET; + this->clients[slot].active = false; + } + + void WebSocketControllerState::stop() { + this->running = false; + + if (this->listener != INVALID_SOCKET) { + closesocket(this->listener); + this->listener = INVALID_SOCKET; + } + + // drops the client threads out of their blocking send/recv + { + std::lock_guard lock(this->clients_m); + for (auto &client : this->clients) { + if (client.socket != INVALID_SOCKET) { + ::shutdown(client.socket, SD_BOTH); + } + } + } + + if (this->acceptor.joinable()) { + this->acceptor.join(); + } + + // joining is what guarantees no client thread outlives this object + for (auto &client : this->clients) { + if (client.thread.joinable()) { + client.thread.join(); + } + } + + if (this->wsa_started) { + WSACleanup(); + this->wsa_started = false; + } + } + WebSocketController::WebSocketController(Controller *controller, uint16_t port) { this->controller = controller; - // create state this->state = new WebSocketControllerState(); + this->state->controller = controller; + this->state->port = port; - // start server - this->state->server = WebSocketServer::create(port); - this->state->server->websocket = this; - if (this->state->server->is_running()) { - log_info("api::websocket", "server listening on port: {}", port); - } else { + if (!this->state->open_listener()) { log_warning("api::websocket", "server failed to listen on port: {}", port); + return; } + + this->state->running = true; + this->state->acceptor = std::thread([this] { + this->state->accept_worker(); + }); + + log_info("api::websocket", "server listening on port: {}", port); } WebSocketController::~WebSocketController() { - - // stop server - this->state->server->stop(); - - // delete state + this->state->stop(); delete this->state; + this->state = nullptr; } void WebSocketController::free_socket() { - this->state->server->stop(); - } - - void WebSocketClient::on_accept() { - web_socket_client::on_accept(); - - // get pointer to server - auto srv = reinterpret_cast(server().get()); - if (!srv || !srv->websocket) { - log_fatal("api::websocket", "on_accept has no server"); - } - - // check for init - state = new ClientState(); - srv->websocket->controller->init_state(state); - - // log connection - const auto address = this->remote_address(); - log_info("api::websocket", "client connected: {}", address); - overlay::notifications::add( - overlay::notifications::Severity::Success, - fmt::format("API websocket client connected ({})", address)); - } - - void WebSocketClient::on_disconnect() { - - // log disconnection - const auto address = this->remote_address(); - log_info("api::websocket", "client disconnected: {}", address); - overlay::notifications::add( - overlay::notifications::Severity::Info, - fmt::format("API websocket client disconnected ({})", address)); - - // get pointer to server - auto srv = reinterpret_cast(server().get()); - if (!srv || !srv->websocket) { - log_fatal("api::websocket", "on_disconnect has no server"); - } - - // clean up state - srv->websocket->controller->free_state(state); - delete state; - state = nullptr; - - // call super - web_socket_client::on_disconnect(); - } - - /* - * This is where business actually happens, gets called on every datablock receive - */ - bool WebSocketClient::async_received_data(const data_block &db, uint8_t *ptr, size_t length) { - - // get pointer to server - auto srv = reinterpret_cast(server().get()); - if (!srv || !srv->websocket) { - log_fatal("api::websocket", "received datablock without server"); - } - - // check state - if (!state) { - log_fatal("api::websocket", "client with no state received datablock"); - } - - // check datablock type - switch (db.op) { - case opcode::binary: { - - // allocate buffers - std::vector in(ptr, ptr + length); - std::vector out; - - // crypt in-data - if (state->cipher) { - state->cipher->crypt(reinterpret_cast(in.data()), in.size()); - } - - // process request - srv->websocket->controller->process_request(state, &in, &out); - - // crypt out-data - if (state->cipher) { - state->cipher->crypt(reinterpret_cast(out.data()), out.size()); - } - - // send answer - push(out.data(), out.size()); - - // check for password change - srv->websocket->controller->process_password_change(state); - - break; - } - default: - log_warning("api::websocket", "datablock received with non-binary type"); - break; - } - - // always consume the datablock, nomnom - return true; + this->state->stop(); } } diff --git a/src/spice2x/external/headsocket.h b/src/spice2x/external/headsocket.h deleted file mode 100644 index fdad8c5..0000000 --- a/src/spice2x/external/headsocket.h +++ /dev/null @@ -1,2195 +0,0 @@ -/*///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -***** HeadSocket v0.1, created by Jan Pinter **** Minimalistic header only WebSocket server implementation in C++ ***** - Sources: https://github.com/P-i-N/HeadSocket, contact: Pinter.Jan@gmail.com - PUBLIC DOMAIN - no warranty implied or offered, use this at your own risk - ------------------------------------------------------------------------------------------------------------------------ - -Usage: -- use this as a regular header file, but in EXACTLY one of your C++ files (ie. main.cpp) you must define - HEADSOCKET_IMPLEMENTATION beforehand, like this: - - #define HEADSOCKET_IMPLEMENTATION - #include - -/*///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#ifndef __HEADSOCKET_H__ -#define __HEADSOCKET_H__ - -#include -#include -#include -#include - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace headsocket { - -/* Forward declarations */ -class connection; -class basic_tcp_server; -class basic_tcp_client; -class tcp_client; -class async_tcp_client; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -template using ptr = std::shared_ptr; -typedef size_t id_t; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace detail { - -/* Forward declarations */ -struct connection_impl; -struct basic_tcp_server_impl; -struct basic_tcp_client_impl; -struct async_tcp_client_impl; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -static bool handshake_websocket(connection &conn); - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -struct less_comparator -{ - bool operator()(const std::string &s1, const std::string &s2) const - { - return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char c1, char c2) -> bool - { - return tolower(c1) < tolower(c2); - }); - } -}; - -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class connection -{ -public: - connection(const detail::connection_impl &impl); - ~connection(); - - detail::connection_impl *impl() const { return _p.get(); } - - bool is_valid() const; - id_t id() const; - - size_t write(const void *ptr, size_t length); - size_t write(const std::string &text) { return write(text.c_str(), text.length()); } - size_t read(void *ptr, size_t length); - - bool force_write(const void *ptr, size_t length); - bool force_read(void *ptr, size_t length); - - bool read_line(std::string &output); - -private: - std::unique_ptr _p; -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -enum class opcode -{ - continuation = 0x00, - text = 0x01, - binary = 0x02, - connection_close = 0x08, - ping = 0x09, - pong = 0x0A -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -struct data_block -{ - opcode op; - size_t offset; - size_t length = 0; - bool is_completed = false; - - data_block(opcode opc, size_t off) - : op(opc) - , offset(off) - { - - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class basic_tcp_server : public std::enable_shared_from_this -{ -public: - int port() const; - void stop(); - bool is_running() const; - bool disconnect(ptr client); - bool disconnect(id_t id); - -protected: - struct protected_tag { }; - - virtual void init() { } - - explicit basic_tcp_server(int port); - virtual ~basic_tcp_server(); - - virtual bool handshake(connection &conn) = 0; - virtual ptr accept(connection &conn) = 0; - virtual void client_connected(ptr client) = 0; - virtual void client_disconnected(ptr client) = 0; - - std::unique_ptr _p; - -private: - template friend class tcp_server; - - void remove_disconnected() const; - - size_t acquire_clients() const; - void release_clients() const; - - ptr client_at(size_t index) const; - size_t num_clients() const; - - void accept_thread(); - void disconnect_thread(); -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - class basic_tcp_client - { - public: - enum { is_basic_tcp_client }; - - static const size_t invalid_operation = static_cast(-1); - - virtual ~basic_tcp_client(); - - bool disconnect(); - bool is_connected() const; - - ptr server() const; - id_t id() const; - - protected: - struct protected_tag { }; - - friend class basic_tcp_server; - - virtual void on_accept() { } - virtual void on_disconnect() { } - - basic_tcp_client(const std::string &address, int port); - basic_tcp_client(ptr server, connection &conn); - - std::unique_ptr _p; - }; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -struct protected_tag { }; - -#define HEADSOCKET_SERVER(className, baseClassName) \ - protected: \ - explicit className(int port): baseClassName(port) { init(); } \ - public: \ - typedef baseClassName base_t; \ - className(const protected_tag &, int port): className(port) { } \ - static headsocket::ptr create(int port) { return std::make_shared(protected_tag{}, port); } \ - protected: \ - void init() - -template -class tcp_server : public basic_tcp_server -{ - HEADSOCKET_SERVER(tcp_server, basic_tcp_server) { } - -public: - typedef T client_t; - typedef ptr client_ptr; - - virtual ~tcp_server() - { - base_t::stop(); - } - - class enumerator - { - public: - explicit enumerator(const tcp_server &server) - : _server(server) - , _count(server.acquire_clients()) - { - - } - - ~enumerator() - { - _server.release_clients(); - } - - const tcp_server &server() const { return _server; } - size_t size() const { return _count; } - - struct iterator - { - enumerator &e; - size_t index; - - iterator(enumerator &enu, size_t idx) - : e(enu) - , index(idx) - { - - } - - bool operator==(const iterator &iter) const { return iter.index == index && &iter.e == &e; } - bool operator!=(const iterator &iter) const { return iter.index != index || &iter.e != &e; } - ptr operator*() const { return std::dynamic_pointer_cast(e.server().client_at(index)); } - - iterator &operator++() - { - ++index; - return *this; - } - }; - - iterator begin() { return iterator(*this, 0); } - iterator end() { return iterator(*this, _count); } - - private: - const tcp_server &_server; - size_t _count; - }; - - enumerator clients() const { return enumerator(*this); } - -protected: - bool handshake(connection &conn) override { return true; } - - virtual void client_connected(client_ptr client) { } - - virtual void client_disconnected(client_ptr client) { } - -private: - enum { needs_basic_tcp_client = T::is_basic_tcp_client }; - - ptr accept(connection &conn) override - { - ptr newClient = T::create(shared_from_this(), conn); - return newClient->is_connected() ? newClient : nullptr; - } - - void client_connected(ptr client) override - { - client_connected(std::dynamic_pointer_cast(client)); - } - - void client_disconnected(ptr client) override - { - client_disconnected(std::dynamic_pointer_cast(client)); - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#define __HEADSOCKET_CLIENT_STATIC_CTORS(className) \ - className(const protected_tag &, const std::string &address, int port): className(address, port) { } \ - className(const protected_tag &, headsocket::ptr server, headsocket::connection &conn): className(server, conn) { } \ - static headsocket::ptr create(const std::string &address, int port) { return std::make_shared(protected_tag{}, address, port); } \ - static headsocket::ptr create(headsocket::ptr server, headsocket::connection &conn) { return std::make_shared(protected_tag{}, server, conn); } - -#define HEADSOCKET_CLIENT_BASE(className) \ - protected: \ - className(const std::string &address, int port); \ - className(ptr server, connection &conn); \ - public: \ - __HEADSOCKET_CLIENT_STATIC_CTORS(className) - -#define HEADSOCKET_CLIENT(className, baseClassName) \ - protected: \ - className(const std::string &address, int port): baseClassName(address, port) { } \ - className(headsocket::ptr server, headsocket::connection &conn): baseClassName(server, conn) { } \ - public: \ - __HEADSOCKET_CLIENT_STATIC_CTORS(className) - -class tcp_client : public basic_tcp_client -{ - HEADSOCKET_CLIENT_BASE(tcp_client) - -public: - typedef basic_tcp_client base_t; - enum { is_tcp_client }; - - virtual ~tcp_client(); - - virtual size_t write(const void *ptr, size_t length); - virtual size_t read(void *ptr, size_t length); - - bool force_write(const void *ptr, size_t length); - bool force_read(void *ptr, size_t length); - - bool read_line(std::string &output); -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class async_tcp_client : public basic_tcp_client -{ - HEADSOCKET_CLIENT_BASE(async_tcp_client) - -public: - typedef basic_tcp_client base_t; - enum { is_async_tcp_client }; - - virtual ~async_tcp_client(); - - void push(const void *ptr, size_t length); - void push(const std::string &text); - size_t peek() const; - size_t pop(void *ptr, size_t length); - -protected: - void on_accept() override { init_threads(); } - void on_disconnect() override { kill_threads(); } - - virtual void init_threads(); - - virtual size_t async_write_handler(uint8_t *ptr, size_t length); - virtual size_t async_read_handler(uint8_t *ptr, size_t length); - - virtual bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) { return false; } - - virtual void push(const void *ptr, size_t length, opcode opcode); - - void kill_threads(); - - std::unique_ptr _ap; - -private: - void write_thread(); - void read_thread(); -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class web_socket_client : public async_tcp_client -{ - HEADSOCKET_CLIENT_BASE(web_socket_client) - -public: - static const size_t frame_size_limit = 128 * 1024; - - typedef async_tcp_client base_t; - enum { is_web_socket_client }; - - virtual ~web_socket_client(); - - size_t peek(opcode *op) const; - -protected: - size_t async_write_handler(uint8_t *ptr, size_t length) override; - size_t async_read_handler(uint8_t *ptr, size_t length) override; - -private: - struct frame_header - { - bool fin; - opcode op; - bool masked; - size_t payload_length; - uint32_t masking_key; - - size_t write(uint8_t *ptr, size_t length) const; - size_t read(const uint8_t *ptr, size_t length); - }; - - size_t _payload_size = 0; - frame_header _current_header; -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -template -class web_socket_server : public tcp_server -{ - HEADSOCKET_SERVER(web_socket_server, tcp_server) { } - -public: - virtual ~web_socket_server() - { - base_t::stop(); - } - -protected: - bool handshake(connection &conn) override { return detail::handshake_websocket(conn); } - -private: - enum { needs_web_socket_client = T::is_web_socket_client }; -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class http_server : public tcp_server -{ - HEADSOCKET_SERVER(http_server, tcp_server) { } - -public: - ~http_server() - { - stop(); - } - - struct response - { - std::string content_type = "text/html"; - std::string message = ""; - }; - - struct parameter - { - std::string name; - std::string value; - bool boolean; - int integer; - double real; - }; - - typedef std::map parameters_t; - -protected: - virtual bool request(const std::string &path, const parameters_t ¶ms, response &resp) { return false; } - -private: - bool handshake(connection &conn) final override; - - ptr accept(connection &conn) final override { return nullptr; } - void client_connected(client_ptr client) final override { } - void client_disconnected(client_ptr client) final override { } -}; - -} - -#endif // __HEADSOCKET_H__ - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#ifdef HEADSOCKET_IMPLEMENTATION -#ifndef __HEADSOCKET_H_IMPL__ -#define __HEADSOCKET_H_IMPL__ - -#include -#include -#include -#include -#include -#include -#include -#include - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#ifndef HEADSOCKET_PLATFORM_OVERRIDE -#ifdef _WIN32 -#define HEADSOCKET_PLATFORM_WINDOWS -#elif __ANDROID__ -#define HEADSOCKET_PLATFORM_ANDROID -#define HEADSOCKET_PLATFORM_NIX -#elif __APPLE__ -#include "TargetConditionals.h" -#ifdef TARGET_OS_MAC -#define HEADSOCKET_PLATFORM_MAC -#endif -#elif __linux -#define HEADSOCKET_PLATFORM_NIX -#elif __unix -#define HEADSOCKET_PLATFORM_NIX -#elif __posix -#define HEADSOCKET_PLATFORM_NIX -#else -#error Unsupported platform! -#endif -#endif - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#if defined(HEADSOCKET_PLATFORM_WINDOWS) -#pragma comment(lib, "ws2_32.lib") -#include -#include -#include -#include - -#elif defined(HEADSOCKET_PLATFORM_ANDROID) || defined(HEADSOCKET_PLATFORM_NIX) -#include -#include -#include -#include -#include -#endif - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#define HEADSOCKET_LOCK_SUFFIX(var, suffix) std::lock_guard __scope_lock##suffix(var); -#define HEADSOCKET_LOCK_SUFFIX2(var, suffix) HEADSOCKET_LOCK_SUFFIX(var, suffix) -#define HEADSOCKET_LOCK(var) HEADSOCKET_LOCK_SUFFIX2(var, __LINE__) - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace headsocket { - -namespace detail { -#if defined(HEADSOCKET_PLATFORM_WINDOWS) -typedef SOCKET socket_type; -static const int socket_error = SOCKET_ERROR; -static const SOCKET invalid_socket = INVALID_SOCKET; -void close_socket(socket_type s) { closesocket(s); } -#define HEADSOCKET_SPRINTF sprintf_s -#elif defined(HEADSOCKET_PLATFORM_ANDROID) || defined(HEADSOCKET_PLATFORM_NIX) -typedef int socket_type; -static const int socket_error = -1; -static const int invalid_socket = -1; -void close_socket(socket_type s) { close(s); } -#define HEADSOCKET_SPRINTF sprintf -#endif -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace detail { - -class sha1 -{ -public: - typedef uint32_t digest32_t[5]; - typedef uint8_t digest8_t[20]; - - inline static uint32_t rotate_left(uint32_t value, size_t count) { return (value << count) ^ (value >> (32 - count)); } - - sha1() - { - _digest[0] = 0x67452301; - _digest[1] = 0xEFCDAB89; - _digest[2] = 0x98BADCFE; - _digest[3] = 0x10325476; - _digest[4] = 0xC3D2E1F0; - } - - ~sha1() - { - - } - - void process_byte(uint8_t octet) - { - _block[_block_byte_index++] = octet; - ++_byte_count; - - if (_block_byte_index == 64) - { - _block_byte_index = 0; - process_block(); - } - } - - void process_block(const void *start, const void *end) - { - const uint8_t *begin = static_cast(start); - - while (begin != end) - process_byte(*begin++); - } - - void process_bytes(const void *data, size_t len) - { - process_block(data, static_cast(data) + len); - } - - const uint32_t *get_digest(digest32_t digest) - { - size_t bitCount = _byte_count * 8; - process_byte(0x80); - - if (_block_byte_index > 56) - { - while (_block_byte_index != 0) - process_byte(0); - - while (_block_byte_index < 56) - process_byte(0); - } - else - while (_block_byte_index < 56) - process_byte(0); - - process_byte(0); - process_byte(0); - process_byte(0); - process_byte(0); - - for (int i = 24; i >= 0; i -= 8) - process_byte(static_cast((bitCount >> i) & 0xFF)); - - memcpy(digest, _digest, 5 * sizeof(uint32_t)); - return digest; - } - - const uint8_t *get_digest_bytes(digest8_t digest) - { - digest32_t d32; - get_digest(d32); - size_t s[] = { 24, 16, 8, 0 }; - - for (size_t i = 0, j = 0; i < 20; ++i, j = i % 4) - digest[i] = ((d32[i >> 2] >> s[j]) & 0xFF); - - return digest; - } - -private: - void process_block() - { - uint32_t w[80], s[] = { 24, 16, 8, 0 }; - - for (size_t i = 0, j = 0; i < 64; ++i, j = i % 4) - w[i / 4] = j ? (w[i / 4] | (_block[i] << s[j])) : (_block[i] << s[j]); - - for (size_t i = 16; i < 80; i++) - w[i] = rotate_left((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1); - - digest32_t dig = { _digest[0], _digest[1], _digest[2], _digest[3], _digest[4] }; - - for (size_t f, k, i = 0; i < 80; ++i) - { - if (i < 20) - f = (dig[1] & dig[2]) | (~dig[1] & dig[3]), k = 0x5A827999; - else if (i < 40) - f = dig[1] ^ dig[2] ^ dig[3], k = 0x6ED9EBA1; - else if (i < 60) - f = (dig[1] & dig[2]) | (dig[1] & dig[3]) | (dig[2] & dig[3]), k = 0x8F1BBCDC; - else - f = dig[1] ^ dig[2] ^ dig[3], k = 0xCA62C1D6; - - uint32_t temp = static_cast(rotate_left(dig[0], 5) + f + dig[4] + k + w[i]); - dig[4] = dig[3]; - dig[3] = dig[2]; - dig[2] = rotate_left(dig[1], 30); - dig[1] = dig[0]; - dig[0] = temp; - } - - for (size_t i = 0; i < 5; ++i) - _digest[i] += dig[i]; - } - - digest32_t _digest; - uint8_t _block[64]; - size_t _block_byte_index = 0; - size_t _byte_count = 0; -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -struct utils -{ - static std::string base64_encode(const void *ptr, size_t length) - { - static const char *encoding_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - static size_t mod_table[] = { 0, 2, 1 }; - - std::string result(4 * ((length + 2) / 3), '='); - - if (ptr && length) - { - const uint8_t *input = reinterpret_cast(ptr); - - for (size_t i = 0, j = 0, triplet = 0; i < length; triplet = 0) - { - for (size_t k = 0; k < 3; ++k) - triplet = (triplet << 8) | (i < length ? static_cast(input[i++]) : 0); - - for (size_t k = 4; k--;) - result[j++] = encoding_table[(triplet >> k * 6) & 0x3F]; - } - - for (size_t i = 0; i < mod_table[length % 3]; i++) - result[result.length() - 1 - i] = '='; - } - - return result; - } - - static size_t xor32(uint32_t key, void *ptr, size_t length) - { - uint8_t *data = reinterpret_cast(ptr); - uint8_t *mask = reinterpret_cast(&key); - - for (size_t i = 0; i < length; ++i, ++data) - *data = (*data) ^ mask[i % 4]; - - return length; - } - - static std::string url_encode(const std::string &str) - { - std::ostringstream result; - result.fill('0'); - result << std::hex; - - for (std::string::const_iterator i = str.begin(), n = str.end(); i != n; ++i) - { - auto c = (*i); - - if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') - result << c; - else - result << '%' << std::setw(2) << static_cast(c); - } - - return result.str(); - } - - static std::string url_decode(const std::string &str) - { - std::ostringstream result; - - for (size_t i = 0, S = str.length(); i < S; ++i) - { - auto c = str[i]; - - if (c == '%') - { - char hexBuff[3] = { 0, 0, 0 }; - hexBuff[0] = str[++i]; - hexBuff[1] = str[++i]; - - int value; - sscanf(hexBuff, "%x", &value); - result << static_cast(value); - } - else if (c == '+') - result << ' '; - else - result << c; - } - - return result.str(); - } - - static uint16_t swap16bits(uint16_t x) { return ((x & 0x00FF) << 8) | ((x & 0xFF00) >> 8); } - - static uint32_t swap32bits(uint32_t x) - { - return ((x & 0x000000FF) << 24) | ((x & 0x0000FF00) << 8) | ((x & 0x00FF0000) >> 8) | ((x & 0xFF000000) >> 24); - } - - static uint64_t swap64bits(uint64_t x) - { - return - ((x & 0x00000000000000FFULL) << 56) | ((x & 0x000000000000FF00ULL) << 40) | ((x & 0x0000000000FF0000ULL) << 24) | - ((x & 0x00000000FF000000ULL) << 8) | ((x & 0x000000FF00000000ULL) >> 8) | ((x & 0x0000FF0000000000ULL) >> 24) | - ((x & 0x00FF000000000000ULL) >> 40) | ((x & 0xFF00000000000000ULL) >> 56); - } - - static std::string trim(const std::string &str) - { - size_t trimLeft = 0, trimRight = str.length() - 1; - - while (trimLeft < str.length() && isspace(str[trimLeft])) - ++trimLeft; - - while (trimRight < str.length() && isspace(str[trimRight])) - --trimRight; - - return (trimRight >= str.length() || trimLeft >= str.length() || trimRight < trimLeft) - ? std::string("") - : str.substr(trimLeft, trimRight - trimLeft + 1); - } - - static std::string cut_front(std::string &str, char delimiter = ' ', bool first = true, bool hungry = true) - { - std::string result; - - auto pos = first ? str.find(delimiter) : str.rfind(delimiter); - if (pos == std::string::npos) - { - if (hungry) - { - result = str; - str = ""; - } - } - else - { - result = str.substr(0, pos); - str = str.substr(pos + 1); - } - - return result; - } - - static std::string cut_back(std::string &str, char delimiter = ' ', bool first = true, bool hungry = true) - { - std::string result; - - auto pos = first ? str.rfind(delimiter) : str.find(delimiter); - if (pos == std::string::npos) - { - if (hungry) - { - result = str; - str = ""; - } - } - else - { - result = str.substr(pos + 1); - str = str.substr(0, pos); - } - - return result; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -struct critical_section -{ - mutable std::atomic_bool consumer_lock; - - critical_section() - { - consumer_lock = false; - } - - void lock() const { while (consumer_lock.exchange(true)); } - void unlock() const { consumer_lock = false; } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -template -struct lockable_value : M -{ - T value; - - T *operator->() { return &value; } - const T *operator->() const { return &value; } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -struct semaphore -{ - mutable std::atomic_size_t count; - mutable std::mutex mutex; - mutable std::condition_variable cv; - - semaphore() - { - count = 0; - } - - void lock(size_t minCount = 0) const - { - std::unique_lock lock(mutex); - cv.wait(lock, [&]()->bool { return count > minCount; }); - lock.release(); - } - - void unlock() - { - mutex.unlock(); - } - - void notify() - { - { - std::lock_guard lock(mutex); - ++count; - } - - cv.notify_one(); - } - - void consume() const - { - if (count) - --count; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -struct data_block_buffer -{ - std::vector blocks; - std::vector buffer; - - data_block_buffer() - { - buffer.reserve(65536); - } - - data_block &block_begin(opcode op) - { - blocks.emplace_back(op, buffer.size()); - return blocks.back(); - } - - data_block &block_end() - { - blocks.back().is_completed = true; - return blocks.back(); - } - - void block_remove() - { - if (blocks.empty()) - return; - - buffer.resize(blocks.back().offset); - blocks.pop_back(); - } - - void write(const void *ptr, size_t length) - { - if (!length) - return; - - buffer.resize(buffer.size() + length); - memcpy(buffer.data() + buffer.size() - length, reinterpret_cast(ptr), length); - blocks.back().length += length; - } - - size_t read(void *ptr, size_t length) - { - if (!ptr || blocks.empty() || !blocks.front().is_completed) - return 0; - - data_block &db = blocks.front(); - size_t result = db.length >= length ? length : db.length; - - if (result) - { - memcpy(ptr, buffer.data() + db.offset, result); - buffer.erase(buffer.begin(), buffer.begin() + result); - } - - if (!(db.length -= result)) - blocks.erase(blocks.begin()); - else - blocks.front().op = opcode::continuation; - - if (result) for (auto &block : blocks) if (block.offset > db.offset) - block.offset -= result; - - return result; - } - - size_t peek(opcode *op = nullptr) const - { - if (blocks.empty() || !blocks.front().is_completed) - return 0; - - if (op) - *op = blocks.front().op; - - return blocks.front().length; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -//--------------------------------------------------------------------------------------------------------------------- -bool handshake_websocket(connection &conn) -{ - std::string line, key; - - while (conn.read_line(line)) - { - if (line.empty()) - break; - - if (!memcmp(line.c_str(), "Sec-WebSocket-Key: ", 19)) - key = line.substr(19); - } - - if (key.empty()) - return false; - - key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - - detail::sha1 sha; - detail::sha1::digest8_t digest; - sha.process_bytes(key.c_str(), key.length()); - - std::string response = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "; - response += detail::utils::base64_encode(sha.get_digest_bytes(digest), 20); - response += "\r\n\r\n"; - - return conn.force_write(response.c_str(), response.length()); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#ifdef HEADSOCKET_PLATFORM_WINDOWS -void set_thread_name(const char *name) -{ -} -#else -void set_thread_name(const char *name) -{ - -} -#endif - -} // namespace detail; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace detail { - -struct connection_impl -{ - detail::socket_type socket = detail::invalid_socket; - sockaddr_in from; - size_t id = 0; - - void assign(const connection_impl &impl) - { - socket = impl.socket; - from = impl.from; - id = impl.id; - } - - void close() - { - if (socket != detail::invalid_socket) - { - detail::close_socket(socket); - socket = detail::invalid_socket; - } - } -}; - -} - -//--------------------------------------------------------------------------------------------------------------------- -connection::connection(const detail::connection_impl &impl) - : _p(std::make_unique()) -{ - _p->assign(impl); -} - -//--------------------------------------------------------------------------------------------------------------------- -connection::~connection() -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -bool connection::is_valid() const { return _p->socket != detail::invalid_socket; } - -//--------------------------------------------------------------------------------------------------------------------- -size_t connection::id() const { return _p->id; } - -//--------------------------------------------------------------------------------------------------------------------- -size_t connection::write(const void *ptr, size_t length) -{ - if (!is_valid()) - return detail::socket_error; - - if (!ptr || !length) - return 0; - - int result = send(_p->socket, static_cast(ptr), static_cast(length), 0); - - if (!result || result == detail::socket_error) - return 0; - - return static_cast(result); -} - -//--------------------------------------------------------------------------------------------------------------------- -bool connection::force_write(const void *ptr, size_t length) -{ - if (!is_valid()) - return false; - - if (!ptr) - return true; - - const char *chPtr = static_cast(ptr); - - while (length) - { - int result = send(_p->socket, chPtr, static_cast(length), 0); - - if (!result || result == detail::socket_error) - return false; - - length -= static_cast(result); - chPtr += result; - } - - return true; -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t connection::read(void *ptr, size_t length) -{ - if (!is_valid()) - return detail::socket_error; - - if (!ptr || !length) - return 0; - - int result = recv(_p->socket, static_cast(ptr), static_cast(length), 0); - - if (!result || result == detail::socket_error) - return 0; - - return static_cast(result); -} - -//--------------------------------------------------------------------------------------------------------------------- -bool connection::force_read(void *ptr, size_t length) -{ - if (!is_valid()) - return false; - - if (!ptr) - return true; - - char *chPtr = static_cast(ptr); - - while (length) - { - int result = recv(_p->socket, chPtr, static_cast(length), 0); - - if (!result || result == detail::socket_error) - return false; - - length -= static_cast(result); - chPtr += result; - } - - return true; -} - -//--------------------------------------------------------------------------------------------------------------------- -bool connection::read_line(std::string &output) -{ - if (!is_valid()) - return false; - - output = ""; - - while (true) - { - char ch; - int r = recv(_p->socket, &ch, 1, 0); - - if (!r || r == detail::socket_error) - return false; - - if (ch == '\n') - break; - else if (ch != '\r') - output += ch; - } - - return true; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace detail { - -struct basic_tcp_client_ref -{ - size_t refCount = 0; - ptr client; - - basic_tcp_client_ref(ptr c) - : client(c) - { - - } -}; - -struct basic_tcp_server_impl -{ - std::atomic_bool isRunning; - std::atomic_bool disconnectThreadQuit; - sockaddr_in local; - detail::lockable_value> connections; - detail::semaphore disconnectSemaphore; - int port = 0; - detail::socket_type serverSocket = invalid_socket; - std::unique_ptr acceptThread; - std::unique_ptr disconnectThread; - id_t nextClientID = 1; - - basic_tcp_server_impl() - { - isRunning = false; - disconnectThreadQuit = false; - } -}; - -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -//--------------------------------------------------------------------------------------------------------------------- -basic_tcp_server::basic_tcp_server(int port) - : _p(std::make_unique()) -{ -#ifdef HEADSOCKET_PLATFORM_WINDOWS - WSADATA wsaData; - WSAStartup(0x101, &wsaData); -#endif - - _p->local.sin_family = AF_INET; - _p->local.sin_addr.s_addr = INADDR_ANY; - _p->local.sin_port = htons(static_cast(port)); - - _p->serverSocket = socket(AF_INET, SOCK_STREAM, 0); - - int opt_enable = 1; - setsockopt(_p->serverSocket, SOL_SOCKET, SO_REUSEADDR, (const char*) &opt_enable, sizeof(int)); - - if (bind(_p->serverSocket, reinterpret_cast(&_p->local), sizeof(_p->local)) != 0) - return; - - if (listen(_p->serverSocket, 8)) - return; - - _p->isRunning = true; - _p->port = port; - _p->acceptThread = std::make_unique(std::bind(&basic_tcp_server::accept_thread, this)); - _p->disconnectThread = std::make_unique(std::bind(&basic_tcp_server::disconnect_thread, this)); -} - -//--------------------------------------------------------------------------------------------------------------------- -basic_tcp_server::~basic_tcp_server() -{ - stop(); - -#ifdef HEADSOCKET_PLATFORM_WINDOWS - WSACleanup(); -#endif -} - -//--------------------------------------------------------------------------------------------------------------------- -int basic_tcp_server::port() const { return _p->port; } - -//--------------------------------------------------------------------------------------------------------------------- -void basic_tcp_server::stop() -{ - if (_p->isRunning.exchange(false)) - { - detail::close_socket(_p->serverSocket); - - { - acquire_clients(); - - for (size_t i = 0, S = num_clients(); i < S; ++i) - client_at(i)->disconnect(); - - release_clients(); - } - - if (_p->acceptThread) - { - _p->acceptThread->join(); - _p->acceptThread = nullptr; - } - - if (_p->disconnectThread) - { - _p->disconnectThreadQuit = true; - _p->disconnectSemaphore.notify(); - - _p->disconnectThread->join(); - _p->disconnectThread = nullptr; - } - } -} - -//--------------------------------------------------------------------------------------------------------------------- -bool basic_tcp_server::is_running() const { return _p->isRunning; } - -//--------------------------------------------------------------------------------------------------------------------- -bool basic_tcp_server::disconnect(ptr client) -{ - bool found = false; - - if (client) - { - { - HEADSOCKET_LOCK(_p->connections); - for (size_t i = 0, S = _p->connections->size(); i < S; ++i) - if (_p->connections->at(i).client == client) - { - found = true; - break; - } - } - - if (found && !client->disconnect()) - { - client_disconnected(client); - _p->disconnectSemaphore.notify(); - } - } - - return found; -} - -//--------------------------------------------------------------------------------------------------------------------- -bool basic_tcp_server::disconnect(id_t id) -{ - bool found = false; - - if (id) - { - ptr client; - - { - HEADSOCKET_LOCK(_p->connections); - for (size_t i = 0, S = _p->connections->size(); i < S; ++i) - if (_p->connections->at(i).client->id() == id) - { - client = _p->connections->at(i).client; - found = true; - break; - } - } - - if (found && !client->disconnect()) - { - client_disconnected(client); - _p->disconnectSemaphore.notify(); - } - } - - return found; -} - -//--------------------------------------------------------------------------------------------------------------------- -ptr basic_tcp_server::client_at(size_t index) const -{ - HEADSOCKET_LOCK(_p->connections); - return index < _p->connections->size() ? _p->connections->at(index).client : nullptr; -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t basic_tcp_server::num_clients() const -{ - HEADSOCKET_LOCK(_p->connections); - return _p->connections->size(); -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t basic_tcp_server::acquire_clients() const -{ - HEADSOCKET_LOCK(_p->connections); - - for (auto &clientRef : _p->connections.value) - ++clientRef.refCount; - - return _p->connections->size(); -} - -//--------------------------------------------------------------------------------------------------------------------- -void basic_tcp_server::release_clients() const -{ - HEADSOCKET_LOCK(_p->connections); - - for (auto &clientRef : _p->connections.value) - --clientRef.refCount; - - remove_disconnected(); -} - -//--------------------------------------------------------------------------------------------------------------------- -void basic_tcp_server::remove_disconnected() const -{ - size_t i = 0; - - while (i < _p->connections->size()) - { - auto &clientRef = _p->connections.value[i]; - - if (!clientRef.client->is_connected() && clientRef.refCount == 0) - { - clientRef.client->on_disconnect(); - _p->connections->erase(_p->connections->begin() + i); - } - else - ++i; - } -} - -//--------------------------------------------------------------------------------------------------------------------- -void basic_tcp_server::accept_thread() -{ - detail::set_thread_name("BaseTcpServer::acceptThread"); - - while (_p->isRunning) - { - detail::connection_impl conn_impl; - // addrlen must be a valid in/out pointer or the OS leaves conn_impl.from untouched, - // so the peer address silently reads back as 0.0.0.0 - int from_len = sizeof(conn_impl.from); - conn_impl.socket = ::accept(_p->serverSocket, reinterpret_cast(&conn_impl.from), &from_len); - conn_impl.id = _p->nextClientID++; - - if (!_p->nextClientID) - ++_p->nextClientID; - - if (!_p->isRunning) - break; - - if (conn_impl.socket != detail::invalid_socket) - { - connection conn(conn_impl); - - ptr newClient; - bool failed = false; - - if (handshake(conn)) - { - if ((newClient = accept(conn))) - { - newClient->on_accept(); - - HEADSOCKET_LOCK(_p->connections); - _p->connections->push_back(newClient); - } - else { - failed = true; - } - } - else { - failed = true; - } - - if (failed) - { - conn_impl.close(); - --_p->nextClientID; - - if (!_p->nextClientID) - --_p->nextClientID; - } - else - client_connected(newClient); - } - } -} - -//--------------------------------------------------------------------------------------------------------------------- -void basic_tcp_server::disconnect_thread() -{ - detail::set_thread_name("BaseTcpServer::disconnectThread"); - - while (!_p->disconnectThreadQuit) - { - { - HEADSOCKET_LOCK(_p->disconnectSemaphore); - HEADSOCKET_LOCK(_p->connections); - - remove_disconnected(); - _p->disconnectSemaphore.consume(); - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace detail { - -struct basic_tcp_client_impl -{ - std::atomic_int refCount; - std::atomic_bool isConnected; - std::weak_ptr server; - connection conn = detail::connection_impl(); - std::string address = ""; - int port = 0; - - basic_tcp_client_impl() - { - refCount = 0; - isConnected = false; - } -}; - -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -//--------------------------------------------------------------------------------------------------------------------- -basic_tcp_client::basic_tcp_client(const std::string &address, int port) - : _p(std::make_unique()) -{ - struct addrinfo *result = nullptr, *ptr = nullptr, hints; - - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = IPPROTO_TCP; - - char buff[16]; - HEADSOCKET_SPRINTF(buff, "%d", port); - - if (getaddrinfo(address.c_str(), buff, &hints, &result)) - return; - - for (ptr = result; ptr != nullptr; ptr = ptr->ai_next) - { - _p->conn.impl()->socket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol); - - if (!_p->conn.is_valid()) - return; - - if (connect(_p->conn.impl()->socket, ptr->ai_addr, static_cast(ptr->ai_addrlen)) == detail::socket_error) - { - detail::close_socket(_p->conn.impl()->socket); - _p->conn.impl()->socket = detail::invalid_socket; - continue; - } - - break; - } - - freeaddrinfo(result); - - if (!_p->conn.is_valid()) - return; - - _p->address = address; - _p->port = port; - _p->isConnected = true; -} - -//--------------------------------------------------------------------------------------------------------------------- -basic_tcp_client::basic_tcp_client(ptr server, connection &conn) - : _p(std::make_unique()) -{ - _p->server = server; - _p->conn.impl()->assign(*(conn.impl())); - _p->isConnected = true; -} - -//--------------------------------------------------------------------------------------------------------------------- -basic_tcp_client::~basic_tcp_client() -{ - disconnect(); -} - -//--------------------------------------------------------------------------------------------------------------------- -bool basic_tcp_client::disconnect() -{ - bool wasConnected = _p->isConnected.exchange(false); - - if (wasConnected) - { - _p->conn.impl()->close(); - - ptr s = server(); - - if (s) - s->disconnect(_p->conn.id()); - } - - return wasConnected; -} - -//--------------------------------------------------------------------------------------------------------------------- -bool basic_tcp_client::is_connected() const { return _p->isConnected; } - -//--------------------------------------------------------------------------------------------------------------------- -ptr basic_tcp_client::server() const { return _p->server.lock(); } - -//--------------------------------------------------------------------------------------------------------------------- -id_t basic_tcp_client::id() const { return _p->conn.id(); } - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -//--------------------------------------------------------------------------------------------------------------------- -tcp_client::tcp_client(const std::string &address, int port) - : base_t(address, port) -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -tcp_client::tcp_client(ptr server, connection &conn) - : base_t(server, conn) -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -tcp_client::~tcp_client() -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t tcp_client::write(const void *ptr, size_t length) { return _p->conn.write(ptr, length); } - -//--------------------------------------------------------------------------------------------------------------------- -size_t tcp_client::read(void *ptr, size_t length) { return _p->conn.read(ptr, length); } - -//--------------------------------------------------------------------------------------------------------------------- -bool tcp_client::force_write(const void *ptr, size_t length) { return _p->conn.force_write(ptr, length); } - -//--------------------------------------------------------------------------------------------------------------------- -bool tcp_client::force_read(void *ptr, size_t length) { return _p->conn.force_read(ptr, length); } - -//--------------------------------------------------------------------------------------------------------------------- -bool tcp_client::read_line(std::string &output) { return _p->conn.read_line(output); } - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace detail { - -struct async_tcp_client_impl -{ - detail::semaphore writeSemaphore; - detail::lockable_value writeBlocks; - detail::lockable_value readBlocks; - std::unique_ptr writeThread; - std::unique_ptr readThread; - std::atomic_int threadCounter = { 0 }; -}; - -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -//--------------------------------------------------------------------------------------------------------------------- -async_tcp_client::async_tcp_client(const std::string &address, int port) - : base_t(address, port) - , _ap(std::make_unique()) -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -async_tcp_client::async_tcp_client(ptr server, connection &conn) - : base_t(server, conn) - , _ap(new detail::async_tcp_client_impl()) -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -async_tcp_client::~async_tcp_client() -{ - disconnect(); - - _ap->writeSemaphore.notify(); - _ap->writeThread->join(); - _ap->readThread->join(); -} - -//--------------------------------------------------------------------------------------------------------------------- -void async_tcp_client::push(const void *ptr, size_t length, opcode opcode) -{ - if (!ptr) - return; - - { - HEADSOCKET_LOCK(_ap->writeBlocks); - _ap->writeBlocks->block_begin(opcode); - _ap->writeBlocks->write(ptr, length); - _ap->writeBlocks->block_end(); - } - - _ap->writeSemaphore.notify(); -} - -//--------------------------------------------------------------------------------------------------------------------- -void async_tcp_client::push(const void *ptr, size_t length) -{ - push(ptr, length, opcode::binary); -} - -//--------------------------------------------------------------------------------------------------------------------- -void async_tcp_client::push(const std::string &text) -{ - push(text.c_str(), text.length(), opcode::text); -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t async_tcp_client::peek() const -{ - HEADSOCKET_LOCK(_ap->readBlocks); - return _ap->readBlocks->peek(nullptr); -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t async_tcp_client::pop(void *ptr, size_t length) -{ - if (!ptr) - return invalid_operation; - - if (!length) - return 0; - - HEADSOCKET_LOCK(_ap->readBlocks); - return _ap->readBlocks->read(ptr, length); -} - -//--------------------------------------------------------------------------------------------------------------------- -void async_tcp_client::init_threads() -{ - _ap->threadCounter = 0; - _ap->writeThread = std::make_unique(std::bind(&async_tcp_client::write_thread, this)); - _ap->readThread = std::make_unique(std::bind(&async_tcp_client::read_thread, this)); -} - -//--------------------------------------------------------------------------------------------------------------------- -void async_tcp_client::write_thread() -{ - ++_ap->threadCounter; - detail::set_thread_name("AsyncTcpClient::writeThread"); - - std::vector buffer(1024 * 1024); - - while (_p->isConnected) - { - size_t written = 0; - { - HEADSOCKET_LOCK(_ap->writeSemaphore); - - if (!_p->isConnected) - break; - - written = async_write_handler(buffer.data(), buffer.size()); - } - - if (written == invalid_operation) - break; - - if (!written) - buffer.resize(buffer.size() * 2); - else - { - const char *cursor = reinterpret_cast(buffer.data()); - - while (written) - { - int result = send(_p->conn.impl()->socket, cursor, static_cast(written), 0); - - if (!result || result == detail::socket_error) - break; - - cursor += result; - written -= static_cast(result); - } - } - } - - kill_threads(); - --_ap->threadCounter; -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t async_tcp_client::async_write_handler(uint8_t *ptr, size_t length) -{ - HEADSOCKET_LOCK(_ap->writeBlocks); - - size_t toWrite = _ap->writeBlocks->peek(nullptr); - size_t toConsume = length > toWrite ? toWrite : length; - _ap->writeBlocks->read(ptr, toConsume); - - if (toWrite == toConsume) - _ap->writeSemaphore.consume(); - - return toConsume; -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t async_tcp_client::async_read_handler(uint8_t *ptr, size_t length) -{ - HEADSOCKET_LOCK(_ap->readBlocks); - - _ap->readBlocks->block_begin(opcode::binary); - _ap->readBlocks->write(ptr, length); - _ap->readBlocks->block_end(); - - return length; -} - -//--------------------------------------------------------------------------------------------------------------------- -void async_tcp_client::read_thread() -{ - ++_ap->threadCounter; - detail::set_thread_name("AsyncTcpClient::readThread"); - - std::vector buffer(1024 * 1024); - size_t bufferBytes = 0, consumed = 0; - - while (_p->isConnected) - { - while (true) - { - int result = static_cast(bufferBytes); - - if (!result || !consumed) - { - result = recv( - _p->conn.impl()->socket, - reinterpret_cast(buffer.data() + bufferBytes), - static_cast(buffer.size() - bufferBytes), - 0); - - if (!result || result == detail::socket_error) - { - consumed = invalid_operation; - break; - } - - bufferBytes += static_cast(result); - } - - consumed = async_read_handler(buffer.data(), bufferBytes); - - if (!consumed) - { - if (bufferBytes == buffer.size()) - buffer.resize(buffer.size() * 2); - } - else - break; - } - - if (consumed == invalid_operation) - break; - - bufferBytes -= consumed; - - if (bufferBytes) - memcpy(buffer.data(), buffer.data() + consumed, bufferBytes); - } - - kill_threads(); - --_ap->threadCounter; -} - -//--------------------------------------------------------------------------------------------------------------------- -void async_tcp_client::kill_threads() -{ - if (std::this_thread::get_id() == _ap->readThread->get_id()) - _ap->writeSemaphore.notify(); - - disconnect(); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -//--------------------------------------------------------------------------------------------------------------------- -web_socket_client::web_socket_client(const std::string &address, int port) - : base_t(address, port) -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -web_socket_client::web_socket_client(ptr server, connection &conn) - : base_t(server, conn) -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -web_socket_client::~web_socket_client() -{ - -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t web_socket_client::peek(opcode *op) const -{ - HEADSOCKET_LOCK(_ap->readBlocks); - return _ap->readBlocks->peek(op); -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t web_socket_client::async_write_handler(uint8_t *ptr, size_t length) -{ - uint8_t *cursor = ptr; - HEADSOCKET_LOCK(_ap->writeBlocks); - - while (length >= 16) - { - opcode op{}; - size_t toWrite = _ap->writeBlocks->peek(&op); - size_t toConsume = (length - 15) > frame_size_limit ? frame_size_limit : (length - 15); - toConsume = toConsume > toWrite ? toWrite : toConsume; - - frame_header header; - header.fin = (toWrite - toConsume) == 0; - header.op = op; - header.masked = false; - header.payload_length = toConsume; - - size_t headerSize = header.write(cursor, length); - cursor += headerSize; - length -= headerSize; - _ap->writeBlocks->read(cursor, toConsume); - cursor += toConsume; - length -= toConsume; - - if (header.fin) - _ap->writeSemaphore.consume(); - - if (!_ap->writeBlocks->peek(&op)) - break; - } - - return cursor - ptr; -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t web_socket_client::async_read_handler(uint8_t *ptr, size_t length) -{ - uint8_t *cursor = ptr; - HEADSOCKET_LOCK(_ap->readBlocks); - - if (!_payload_size) - { - opcode prevOpcode = _current_header.op; - size_t headerSize = _current_header.read(cursor, length); - - if (!headerSize) - return 0; - else if (headerSize == invalid_operation) - return invalid_operation; - - _payload_size = _current_header.payload_length; - cursor += headerSize; - length -= headerSize; - - if (_current_header.op != opcode::continuation) - _ap->readBlocks->block_begin(_current_header.op); - else - _current_header.op = prevOpcode; - } - - if (_payload_size) - { - size_t toConsume = length >= _payload_size ? _payload_size : length; - - if (toConsume) - { - _ap->readBlocks->write(cursor, toConsume); - _payload_size -= toConsume; - cursor += toConsume; - length -= toConsume; - } - } - - if (!_payload_size) - { - if (_current_header.masked) - { - //data_block &db = _ap->readBlocks->blocks.back(); - size_t len = _current_header.payload_length; - detail::utils::xor32(_current_header.masking_key, _ap->readBlocks->buffer.data() + _ap->readBlocks->buffer.size() - len, len); - } - - if (_current_header.fin) - { - data_block &db = _ap->readBlocks->blocks.back(); - - switch (_current_header.op) - { - case opcode::ping: - push(_ap->readBlocks->buffer.data() + db.offset, db.length, opcode::pong); - break; - - case opcode::text: - _ap->readBlocks->buffer.push_back(0); - ++db.length; - break; - - case opcode::connection_close: - kill_threads(); - break; - - default: - break; - } - - if (_current_header.op == opcode::text || _current_header.op == opcode::binary) - { - _ap->readBlocks->block_end(); - if (async_received_data(db, _ap->readBlocks->buffer.data() + db.offset, db.length)) - _ap->readBlocks->block_remove(); - } - } - } - - return cursor - ptr; -} - -//--------------------------------------------------------------------------------------------------------------------- -#define HAVE_ENOUGH_BYTES(num) if (length < num) return 0; else length -= num; -size_t web_socket_client::frame_header::read(const uint8_t *ptr, size_t length) -{ - const uint8_t *cursor = ptr; - HAVE_ENOUGH_BYTES(2); - this->fin = ((*cursor) & 0x80) != 0; - this->op = static_cast((*cursor++) & 0x0F); - - this->masked = ((*cursor) & 0x80) != 0; - uint8_t byte = (*cursor++) & 0x7F; - - if (byte < 126) - this->payload_length = byte; - else if (byte == 126) - { - HAVE_ENOUGH_BYTES(2); - this->payload_length = detail::utils::swap16bits(*(reinterpret_cast(cursor))); - cursor += 2; - } - else if (byte == 127) - { - HAVE_ENOUGH_BYTES(8); - uint64_t length64 = detail::utils::swap64bits(*(reinterpret_cast(cursor))) & 0x7FFFFFFFFFFFFFFFULL; - this->payload_length = static_cast(length64); - cursor += 8; - } - - if (this->masked) - { - HAVE_ENOUGH_BYTES(4); - this->masking_key = *(reinterpret_cast(cursor)); - cursor += 4; - } - - return cursor - ptr; -} - -//--------------------------------------------------------------------------------------------------------------------- -size_t web_socket_client::frame_header::write(uint8_t *ptr, size_t length) const -{ - uint8_t *cursor = ptr; - HAVE_ENOUGH_BYTES(2); - *cursor = this->fin ? 0x80 : 0x00; - *cursor++ |= static_cast(this->op); - - *cursor = this->masked ? 0x80 : 0x00; - - if (this->payload_length < 126) - *cursor++ |= static_cast(this->payload_length); - else if (this->payload_length < 65536) - { - HAVE_ENOUGH_BYTES(2); - *cursor++ |= 126; - *reinterpret_cast(cursor) = detail::utils::swap16bits(static_cast(this->payload_length)); - cursor += 2; - } - else - { - HAVE_ENOUGH_BYTES(8); - *cursor++ |= 127; - *reinterpret_cast(cursor) = detail::utils::swap64bits(static_cast(this->payload_length)); - cursor += 8; - } - - if (this->masked) - { - HAVE_ENOUGH_BYTES(4); - *reinterpret_cast(cursor) = this->masking_key; - cursor += 4; - } - - return cursor - ptr; -} -#undef HAVE_ENOUGH_BYTES - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -namespace detail { - -} - -//--------------------------------------------------------------------------------------------------------------------- -bool http_server::handshake(connection &conn) -{ - std::string requestLine; - - if (!conn.read_line(requestLine)) - return false; - - std::string headerLine; - while (conn.read_line(headerLine)) - { - if (headerLine.empty()) - break; - } - - std::string method = detail::utils::cut_front(requestLine); - std::string path = detail::utils::url_decode(detail::utils::cut_front(requestLine)); - - if (!path.empty() && path.front() == '/') path = path.substr(1); - if (!path.empty() && path.back() == '/') path = path.substr(0, path.length() - 1); - - std::string params_get = detail::utils::cut_back(path, '?', false, false); - std::string version = detail::utils::cut_front(requestLine); - - parameters_t params; - std::string param_str; - while (!(param_str = detail::utils::cut_front(params_get, '&')).empty()) - { - parameter param; - param.name = detail::utils::cut_front(param_str, '='); - param.value = param_str; - param.integer = atoi(param_str.c_str()); - param.real = atof(param_str.c_str()); - param.boolean = (param.integer != 0) || (param_str == "true"); - - params[param.name] = param; - } - - response resp; - if (path != "favicon.ico" && request(path, params, resp)) - { - std::stringstream ss; - ss << version << " 200 OK\r\n"; - ss << "Content-Type: " << resp.content_type << "\r\n"; - ss << "Content-Length: " << resp.message.length() << "\r\n\r\n"; - ss << resp.message; - - conn.write(ss.str()); - } - else - { - conn.write(version); - conn.write(" 404 Not Found\r\n"); - } - - return false; -} - -} -#endif -#endif diff --git a/src/spice2x/external/wslay/CMakeLists.txt b/src/spice2x/external/wslay/CMakeLists.txt new file mode 100644 index 0000000..fa4ae8a --- /dev/null +++ b/src/spice2x/external/wslay/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.12) +project(wslay C) + +set(WSLAY_HEADERS + msvc_compat.h + wslay_event.h + wslay_frame.h + wslay_macro.h + wslay_net.h + wslay_queue.h + includes/wslay/wslay.h + includes/wslay/wslayver.h +) +set(WSLAY_SOURCES + wslay_event.c + wslay_frame.c + wslay_net.c + wslay_queue.c +) + +add_library(wslay STATIC ${WSLAY_HEADERS} ${WSLAY_SOURCES}) + +# config.h here is checked in rather than generated, see the note in it +target_compile_definitions(wslay PRIVATE HAVE_CONFIG_H) +target_include_directories(wslay PRIVATE ${PROJECT_SOURCE_DIR}) +target_include_directories(wslay PUBLIC ${PROJECT_SOURCE_DIR}/includes) + +if(MSVC) + target_compile_options(wslay PRIVATE "/FI${PROJECT_SOURCE_DIR}/msvc_compat.h") +else() + # upstream code, and not ours to keep warning clean + target_compile_options(wslay PRIVATE -w) +endif() diff --git a/src/spice2x/external/wslay/LICENSE b/src/spice2x/external/wslay/LICENSE new file mode 100644 index 0000000..6080330 --- /dev/null +++ b/src/spice2x/external/wslay/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/spice2x/external/wslay/config.h b/src/spice2x/external/wslay/config.h new file mode 100644 index 0000000..bef9106 --- /dev/null +++ b/src/spice2x/external/wslay/config.h @@ -0,0 +1,9 @@ +/* Hand written stand-in for the autotools/CMake generated config.h. Spice only + builds this for Windows on x86, so the probes have single known answers. */ +#ifndef CONFIG_H +#define CONFIG_H + +#define HAVE_WINSOCK2_H +/* x86 and x86_64 are little endian, so WORDS_BIGENDIAN stays undefined */ + +#endif /* CONFIG_H */ diff --git a/src/spice2x/external/wslay/includes/wslay/wslay.h b/src/spice2x/external/wslay/includes/wslay/wslay.h new file mode 100644 index 0000000..7ab8501 --- /dev/null +++ b/src/spice2x/external/wslay/includes/wslay/wslay.h @@ -0,0 +1,841 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef WSLAY_H +#define WSLAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +/* + * wslay/wslayver.h is generated from wslay/wslayver.h.in by + * configure. The projects which do not use autotools can set + * WSLAY_VERSION macro from outside to avoid to generating wslayver.h + */ +#ifndef WSLAY_VERSION +# include +#endif /* WSLAY_VERSION */ + +enum wslay_error { + WSLAY_ERR_WANT_READ = -100, + WSLAY_ERR_WANT_WRITE = -101, + WSLAY_ERR_PROTO = -200, + WSLAY_ERR_INVALID_ARGUMENT = -300, + WSLAY_ERR_INVALID_CALLBACK = -301, + WSLAY_ERR_NO_MORE_MSG = -302, + WSLAY_ERR_CALLBACK_FAILURE = -400, + WSLAY_ERR_WOULDBLOCK = -401, + WSLAY_ERR_NOMEM = -500 +}; + +/* + * Status codes defined in RFC6455 + */ +enum wslay_status_code { + WSLAY_CODE_NORMAL_CLOSURE = 1000, + WSLAY_CODE_GOING_AWAY = 1001, + WSLAY_CODE_PROTOCOL_ERROR = 1002, + WSLAY_CODE_UNSUPPORTED_DATA = 1003, + WSLAY_CODE_NO_STATUS_RCVD = 1005, + WSLAY_CODE_ABNORMAL_CLOSURE = 1006, + WSLAY_CODE_INVALID_FRAME_PAYLOAD_DATA = 1007, + WSLAY_CODE_POLICY_VIOLATION = 1008, + WSLAY_CODE_MESSAGE_TOO_BIG = 1009, + WSLAY_CODE_MANDATORY_EXT = 1010, + WSLAY_CODE_INTERNAL_SERVER_ERROR = 1011, + WSLAY_CODE_TLS_HANDSHAKE = 1015 +}; + +enum wslay_io_flags { + /* + * There is more data to send. + */ + WSLAY_MSG_MORE = 1 +}; + +/* + * Callback function used by wslay_frame_send() function when it needs + * to send data. The implementation of this function must send at most + * len bytes of data in data. flags is the bitwise OR of zero or more + * of the following flag: + * + * WSLAY_MSG_MORE + * There is more data to send + * + * It provides some hints to tune performance and behaviour. user_data + * is one given in wslay_frame_context_init() function. The + * implementation of this function must return the number of bytes + * sent. If there is an error, return -1. The return value 0 is also + * treated an error by the library. + */ +typedef ssize_t (*wslay_frame_send_callback)(const uint8_t *data, size_t len, + int flags, void *user_data); +/* + * Callback function used by wslay_frame_recv() function when it needs + * more data. The implementation of this function must fill at most + * len bytes of data into buf. The memory area of buf is allocated by + * library and not be freed by the application code. flags is always 0 + * in this version. user_data is one given in + * wslay_frame_context_init() function. The implementation of this + * function must return the number of bytes filled. If there is an + * error, return -1. The return value 0 is also treated an error by + * the library. + */ +typedef ssize_t (*wslay_frame_recv_callback)(uint8_t *buf, size_t len, + int flags, void *user_data); +/* + * Callback function used by wslay_frame_send() function when it needs + * new mask key. The implementation of this function must write + * exactly len bytes of mask key to buf. user_data is one given in + * wslay_frame_context_init() function. The implementation of this + * function return 0 on success. If there is an error, return -1. + */ +typedef int (*wslay_frame_genmask_callback)(uint8_t *buf, size_t len, + void *user_data); + +struct wslay_frame_callbacks { + wslay_frame_send_callback send_callback; + wslay_frame_recv_callback recv_callback; + wslay_frame_genmask_callback genmask_callback; +}; + +/* + * The opcode defined in RFC6455. + */ +enum wslay_opcode { + WSLAY_CONTINUATION_FRAME = 0x0u, + WSLAY_TEXT_FRAME = 0x1u, + WSLAY_BINARY_FRAME = 0x2u, + WSLAY_CONNECTION_CLOSE = 0x8u, + WSLAY_PING = 0x9u, + WSLAY_PONG = 0xau +}; + +/* + * Macro that returns 1 if opcode is control frame opcode, otherwise + * returns 0. + */ +#define wslay_is_ctrl_frame(opcode) ((opcode >> 3) & 1) + +/* + * Macros that represent and return reserved bits: RSV1, RSV2, RSV3. + * These macros assume that rsv is constructed by ((RSV1 << 2) | + * (RSV2 << 1) | RSV3) + */ +#define WSLAY_RSV_NONE ((uint8_t)0) +#define WSLAY_RSV1_BIT (((uint8_t)1) << 2) +#define WSLAY_RSV2_BIT (((uint8_t)1) << 1) +#define WSLAY_RSV3_BIT (((uint8_t)1) << 0) + +#define wslay_get_rsv1(rsv) ((rsv >> 2) & 1) +#define wslay_get_rsv2(rsv) ((rsv >> 1) & 1) +#define wslay_get_rsv3(rsv) (rsv & 1) + +struct wslay_frame_iocb { + /* 1 for fragmented final frame, 0 for otherwise */ + uint8_t fin; + /* + * reserved 3 bits. rsv = ((RSV1 << 2) | (RSV << 1) | RSV3). + * RFC6455 requires 0 unless extensions are negotiated. + */ + uint8_t rsv; + /* 4 bit opcode */ + uint8_t opcode; + /* payload length [0, 2**63-1] */ + uint64_t payload_length; + /* 1 for masked frame, 0 for unmasked */ + uint8_t mask; + /* part of payload data */ + const uint8_t *data; + /* bytes of data defined above */ + size_t data_length; +}; + +struct wslay_frame_context; +typedef struct wslay_frame_context *wslay_frame_context_ptr; + +/* + * Initializes ctx using given callbacks and user_data. This function + * allocates memory for struct wslay_frame_context and stores the + * result to *ctx. The callback functions specified in callbacks are + * copied to ctx. user_data is stored in ctx and it will be passed to + * callback functions. When the user code finished using ctx, it must + * call wslay_frame_context_free to deallocate memory. + */ +int wslay_frame_context_init(wslay_frame_context_ptr *ctx, + const struct wslay_frame_callbacks *callbacks, + void *user_data); + +/* + * Deallocates memory pointed by ctx. + */ +void wslay_frame_context_free(wslay_frame_context_ptr ctx); + +/* + * Send WebSocket frame specified in iocb. ctx must be initialized + * using wslay_frame_context_init() function. iocb->fin must be 1 if + * this is a fin frame, otherwise 0. iocb->rsv is reserved bits. + * iocb->opcode must be the opcode of this frame. iocb->mask must be + * 1 if this is masked frame, otherwise 0. iocb->payload_length is + * the payload_length of this frame. iocb->data must point to the + * payload data to be sent. iocb->data_length must be the length of + * the data. This function calls send_callback function if it needs + * to send bytes. This function calls gen_mask_callback function if + * it needs new mask key. This function returns the number of payload + * bytes sent. Please note that it does not include any number of + * header bytes. If it cannot send any single bytes of payload, it + * returns WSLAY_ERR_WANT_WRITE. If the library detects error in iocb, + * this function returns WSLAY_ERR_INVALID_ARGUMENT. If callback + * functions report a failure, this function returns + * WSLAY_ERR_INVALID_CALLBACK. This function does not always send all + * given data in iocb. If there are remaining data to be sent, adjust + * data and data_length in iocb accordingly and call this function + * again. + */ +ssize_t wslay_frame_send(wslay_frame_context_ptr ctx, + struct wslay_frame_iocb *iocb); + +/* + * Write WebSocket frame specified in iocb to buf of length + * buflen. ctx must be initialized using wslay_frame_context_init() + * function. iocb->fin must be 1 if this is a fin frame, otherwise 0. + * iocb->rsv is reserved bits. iocb->opcode must be the opcode of + * this frame. iocb->mask must be 1 if this is masked frame, + * otherwise 0. iocb->payload_length is the payload_length of this + * frame. iocb->data must point to the payload data to be + * sent. iocb->data_length must be the length of the data. Unlike + * wslay_frame_send, this function does not call send_callback + * function. This function calls gen_mask_callback function if it + * needs new mask key. This function returns the number of bytes + * written to a buffer. Unlike wslay_frame_send, it includes the + * number of header bytes. Instead, the number of payload bytes + * written is assigned to *pwpayloadlen if this function succeeds. If + * there is not enough space left in a buffer, it returns 0. If the + * library detects error in iocb, this function returns + * WSLAY_ERR_INVALID_ARGUMENT. If callback functions report a + * failure, this function returns WSLAY_ERR_INVALID_CALLBACK. This + * function does not always send all given data in iocb. If there are + * remaining data to be sent, adjust data and data_length in iocb + * accordingly and call this function again. + */ +ssize_t wslay_frame_write(wslay_frame_context_ptr ctx, + struct wslay_frame_iocb *iocb, uint8_t *buf, + size_t buflen, size_t *pwpayloadlen); + +/* + * Receives WebSocket frame and stores it in iocb. This function + * returns the number of payload bytes received. This does not + * include header bytes. In this case, iocb will be populated as + * follows: iocb->fin is 1 if received frame is fin frame, otherwise + * 0. iocb->rsv is reserved bits of received frame. iocb->opcode is + * opcode of received frame. iocb->mask is 1 if received frame is + * masked, otherwise 0. iocb->payload_length is the payload length of + * received frame. iocb->data is pointed to the buffer containing + * received payload data. This buffer is allocated by the library and + * must be read-only. iocb->data_length is the number of payload + * bytes recieved. This function calls recv_callback if it needs to + * receive additional bytes. If it cannot receive any single bytes of + * payload, it returns WSLAY_ERR_WANT_READ. If the library detects + * protocol violation in a received frame, this function returns + * WSLAY_ERR_PROTO. If callback functions report a failure, this + * function returns WSLAY_ERR_INVALID_CALLBACK. This function does + * not always receive whole frame in a single call. If there are + * remaining data to be received, call this function again. This + * function ensures frame alignment. + */ +ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx, + struct wslay_frame_iocb *iocb); + +struct wslay_event_context; +/* Pointer to the event-based API context */ +typedef struct wslay_event_context *wslay_event_context_ptr; + +struct wslay_event_on_msg_recv_arg { + /* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */ + uint8_t rsv; + /* opcode */ + uint8_t opcode; + /* received message */ + const uint8_t *msg; + /* message length */ + size_t msg_length; + /* + * Status code iff opcode == WSLAY_CONNECTION_CLOSE. If no status + * code is included in the close control frame, it is set to 0. + */ + uint16_t status_code; +}; + +/* + * Callback function invoked by wslay_event_recv() when a message is + * completely received. + */ +typedef void (*wslay_event_on_msg_recv_callback)( + wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, + void *user_data); + +struct wslay_event_on_frame_recv_start_arg { + /* fin bit; 1 for final frame, or 0. */ + uint8_t fin; + /* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */ + uint8_t rsv; + /* opcode of the frame */ + uint8_t opcode; + /* payload length of ths frame */ + uint64_t payload_length; +}; + +/* + * Callback function invoked by wslay_event_recv() when a new frame + * starts to be received. This callback function is only invoked once + * for each frame. + */ +typedef void (*wslay_event_on_frame_recv_start_callback)( + wslay_event_context_ptr ctx, + const struct wslay_event_on_frame_recv_start_arg *arg, void *user_data); + +struct wslay_event_on_frame_recv_chunk_arg { + /* chunk of payload data */ + const uint8_t *data; + /* length of data */ + size_t data_length; +}; + +/* + * Callback function invoked by wslay_event_recv() when a chunk of + * frame payload is received. + */ +typedef void (*wslay_event_on_frame_recv_chunk_callback)( + wslay_event_context_ptr ctx, + const struct wslay_event_on_frame_recv_chunk_arg *arg, void *user_data); + +/* + * Callback function invoked by wslay_event_recv() when a frame is + * completely received. + */ +typedef void (*wslay_event_on_frame_recv_end_callback)( + wslay_event_context_ptr ctx, void *user_data); + +/* + * Callback function invoked by wslay_event_recv() when it wants to + * receive more data from peer. The implementation of this callback + * function must read data at most len bytes from peer and store them + * in buf and return the number of bytes read. flags is always 0 in + * this version. + * + * If there is an error, return -1 and set error code + * WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay + * event-based API on the whole assumes non-blocking I/O. If the cause + * of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK + * instead. This is important because it tells wslay_event_recv() to + * stop receiving further data and return. + */ +typedef ssize_t (*wslay_event_recv_callback)(wslay_event_context_ptr ctx, + uint8_t *buf, size_t len, + int flags, void *user_data); + +/* + * Callback function invoked by wslay_event_send() when it wants to + * send more data to peer. The implementation of this callback + * function must send data at most len bytes to peer and return the + * number of bytes sent. flags is the bitwise OR of zero or more of + * the following flag: + * + * WSLAY_MSG_MORE + * There is more data to send + * + * It provides some hints to tune performance and behaviour. + * + * If there is an error, return -1 and set error code + * WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay + * event-based API on the whole assumes non-blocking I/O. If the cause + * of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK + * instead. This is important because it tells wslay_event_send() to + * stop sending data and return. + */ +typedef ssize_t (*wslay_event_send_callback)(wslay_event_context_ptr ctx, + const uint8_t *data, size_t len, + int flags, void *user_data); + +/* + * Callback function invoked by wslay_event_send() when it wants new + * mask key. As described in RFC6455, only the traffic from WebSocket + * client is masked, so this callback function is only needed if an + * event-based API is initialized for WebSocket client use. + */ +typedef int (*wslay_event_genmask_callback)(wslay_event_context_ptr ctx, + uint8_t *buf, size_t len, + void *user_data); + +struct wslay_event_callbacks { + wslay_event_recv_callback recv_callback; + wslay_event_send_callback send_callback; + wslay_event_genmask_callback genmask_callback; + wslay_event_on_frame_recv_start_callback on_frame_recv_start_callback; + wslay_event_on_frame_recv_chunk_callback on_frame_recv_chunk_callback; + wslay_event_on_frame_recv_end_callback on_frame_recv_end_callback; + wslay_event_on_msg_recv_callback on_msg_recv_callback; +}; + +/* + * Initializes ctx as WebSocket Server. user_data is an arbitrary + * pointer, which is directly passed to each callback functions as + * user_data argument. + * + * On success, returns 0. On error, returns one of following negative + * values: + * + * WSLAY_ERR_NOMEM + * Out of memory. + */ +int wslay_event_context_server_init( + wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks, + void *user_data); + +/* + * Initializes ctx as WebSocket client. user_data is an arbitrary + * pointer, which is directly passed to each callback functions as + * user_data argument. + * + * On success, returns 0. On error, returns one of following negative + * values: + * + * WSLAY_ERR_NOMEM + * Out of memory. + */ +int wslay_event_context_client_init( + wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks, + void *user_data); + +/* + * Releases allocated resources for ctx. + */ +void wslay_event_context_free(wslay_event_context_ptr ctx); + +/* + * Sets a bit mask of allowed reserved bits. + * Currently only permitted values are WSLAY_RSV1_BIT to allow PMCE + * extension (see RFC-7692) or WSLAY_RSV_NONE to disable. + * + * Default: WSLAY_RSV_NONE + */ +void wslay_event_config_set_allowed_rsv_bits(wslay_event_context_ptr ctx, + uint8_t rsv); + +/* + * Enables or disables buffering of an entire message for non-control + * frames. If val is 0, buffering is enabled. Otherwise, buffering is + * disabled. If wslay_event_on_msg_recv_callback is invoked when + * buffering is disabled, the msg_length member of struct + * wslay_event_on_msg_recv_arg is set to 0. + * + * The control frames are always buffered regardless of this function call. + * + * This function must not be used after the first invocation of + * wslay_event_recv() function. + */ +void wslay_event_config_set_no_buffering(wslay_event_context_ptr ctx, int val); + +/* + * Sets maximum length of a message that can be received. The length + * of message is checked by wslay_event_recv() function. If the length + * of a message is larger than this value, reading operation is + * disabled (same effect with wslay_event_shutdown_read() call) and + * close control frame with WSLAY_CODE_MESSAGE_TOO_BIG is queued. If + * buffering for non-control frames is disabled, the library checks + * each frame payload length and does not check length of entire + * message. + * + * The default value is (1u << 31)-1. + */ +void wslay_event_config_set_max_recv_msg_length(wslay_event_context_ptr ctx, + uint64_t val); + +/* + * Sets callbacks to ctx. The callbacks previously set by this function + * or wslay_event_context_server_init() or + * wslay_event_context_client_init() are replaced with callbacks. + */ +void wslay_event_config_set_callbacks( + wslay_event_context_ptr ctx, const struct wslay_event_callbacks *callbacks); + +/* + * Receives messages from peer. When receiving + * messages, it uses wslay_event_recv_callback function. Single call + * of this function receives multiple messages until + * wslay_event_recv_callback function sets error code + * WSLAY_ERR_WOULDBLOCK. + * + * When close control frame is received, this function automatically + * queues close control frame. Also this function calls + * wslay_event_set_read_enabled() with second argument 0 to disable + * further read from peer. + * + * When ping control frame is received, this function automatically + * queues pong control frame. + * + * In case of a fatal errror which leads to negative return code, this + * function calls wslay_event_set_read_enabled() with second argument + * 0 to disable further read from peer. + * + * wslay_event_recv() returns 0 if it succeeds, or one of the + * following negative error codes: + * + * WSLAY_ERR_CALLBACK_FAILURE + * User defined callback function is failed. + * + * WSLAY_ERR_NOMEM + * Out of memory. + * + * When negative error code is returned, application must not make any + * further call of wslay_event_recv() and must close WebSocket + * connection. + */ +int wslay_event_recv(wslay_event_context_ptr ctx); + +/* + * Sends queued messages to peer. When sending a + * message, it uses wslay_event_send_callback function. Single call of + * wslay_event_send() sends multiple messages until + * wslay_event_send_callback sets error code WSLAY_ERR_WOULDBLOCK. + * + * If ctx is initialized for WebSocket client use, wslay_event_send() + * uses wslay_event_genmask_callback to get new mask key. + * + * When a message queued using wslay_event_queue_fragmented_msg() is + * sent, wslay_event_send() invokes + * wslay_event_fragmented_msg_callback for that message. + * + * After close control frame is sent, this function calls + * wslay_event_set_write_enabled() with second argument 0 to disable + * further transmission to peer. + * + * If there are any pending messages, wslay_event_want_write() returns + * 1, otherwise returns 0. + * + * In case of a fatal errror which leads to negative return code, this + * function calls wslay_event_set_write_enabled() with second argument + * 0 to disable further transmission to peer. + * + * wslay_event_send() returns 0 if it succeeds, or one of the + * following negative error codes: + * + * WSLAY_ERR_CALLBACK_FAILURE + * User defined callback function is failed. + * + * WSLAY_ERR_NOMEM + * Out of memory. + * + * When negative error code is returned, application must not make any + * further call of wslay_event_send() and must close WebSocket + * connection. + */ +int wslay_event_send(wslay_event_context_ptr ctx); + +/* + * Writes queued messages to a buffer. Unlike wslay_event_send(), this + * function writes messages into the given buffer. It does not use + * wslay_event_send_callback function. Single call of + * wslay_event_write() writes multiple messages until there is not + * enough space left in a buffer. + * + * If ctx is initialized for WebSocket client use, wslay_event_write() + * uses wslay_event_genmask_callback to get new mask key. + * + * buf is a pointer to buffer and its capacity is given in buflen. It + * should have at least 14 bytes. + * + * When a message queued using wslay_event_queue_fragmented_msg() is + * sent, wslay_event_write() invokes + * wslay_event_fragmented_msg_callback for that message. + * + * After close control frame is sent, this function calls + * wslay_event_set_write_enabled() with second argument 0 to disable + * further transmission to peer. + * + * If there are any pending messages, wslay_event_want_write() returns + * 1, otherwise returns 0. + * + * In case of a fatal errror which leads to negative return code, this + * function calls wslay_event_set_write_enabled() with second argument + * 0 to disable further transmission to peer. + * + * wslay_event_write() returns the number of bytes written to a buffer + * if it succeeds, or one of the following negative error codes: + * + * WSLAY_ERR_CALLBACK_FAILURE + * User defined callback function is failed. + * + * WSLAY_ERR_NOMEM + * Out of memory. + * + * When negative error code is returned, application must not make any + * further call of wslay_event_write() and must close WebSocket + * connection. + */ +ssize_t wslay_event_write(wslay_event_context_ptr ctx, uint8_t *buf, + size_t buflen); + +struct wslay_event_msg { + uint8_t opcode; + const uint8_t *msg; + size_t msg_length; +}; + +/* + * Queues message specified in arg. + * + * This function supports both control and non-control messages and + * the given message is sent without fragmentation. If fragmentation + * is needed, use wslay_event_queue_fragmented_msg() function instead. + * + * This function just queues a message and does not send + * it. wslay_event_send() function call sends these queued messages. + * + * wslay_event_queue_msg() returns 0 if it succeeds, or returns the + * following negative error codes: + * + * WSLAY_ERR_NO_MORE_MSG + * Could not queue given message. The one of possible reason is that + * close control frame has been queued/sent and no further queueing + * message is not allowed. + * + * WSLAY_ERR_INVALID_ARGUMENT + * The given message is invalid. + * + * WSLAY_ERR_NOMEM + * Out of memory. + */ +int wslay_event_queue_msg(wslay_event_context_ptr ctx, + const struct wslay_event_msg *arg); + +/* + * Extended version of wslay_event_queue_msg which allows to set reserved bits. + */ +int wslay_event_queue_msg_ex(wslay_event_context_ptr ctx, + const struct wslay_event_msg *arg, uint8_t rsv); + +/* + * Specify "source" to generate message. + */ +union wslay_event_msg_source { + int fd; + void *data; +}; + +/* + * Callback function called by wslay_event_send() to read message data + * from source. The implementation of + * wslay_event_fragmented_msg_callback must store at most len bytes of + * data to buf and return the number of stored bytes. If all data is + * read (i.e., EOF), set *eof to 1. If no data can be generated at the + * moment, return 0. If there is an error, return -1 and set error + * code WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). + */ +typedef ssize_t (*wslay_event_fragmented_msg_callback)( + wslay_event_context_ptr ctx, uint8_t *buf, size_t len, + const union wslay_event_msg_source *source, int *eof, void *user_data); + +struct wslay_event_fragmented_msg { + /* opcode */ + uint8_t opcode; + /* "source" to generate message data */ + union wslay_event_msg_source source; + /* Callback function to read message data from source. */ + wslay_event_fragmented_msg_callback read_callback; +}; + +/* + * Queues a fragmented message specified in arg. + * + * This function supports non-control messages only. For control frames, + * use wslay_event_queue_msg() or wslay_event_queue_close(). + * + * This function just queues a message and does not send + * it. wslay_event_send() function call sends these queued messages. + * + * wslay_event_queue_fragmented_msg() returns 0 if it succeeds, or + * returns the following negative error codes: + * + * WSLAY_ERR_NO_MORE_MSG + * Could not queue given message. The one of possible reason is that + * close control frame has been queued/sent and no further queueing + * message is not allowed. + * + * WSLAY_ERR_INVALID_ARGUMENT + * The given message is invalid. + * + * WSLAY_ERR_NOMEM + * Out of memory. + */ +int wslay_event_queue_fragmented_msg( + wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg); + +/* + * Extended version of wslay_event_queue_fragmented_msg which allows to set + * reserved bits. + */ +int wslay_event_queue_fragmented_msg_ex( + wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg, + uint8_t rsv); + +/* + * Queues close control frame. This function is provided just for + * convenience. wslay_event_queue_msg() can queue a close control + * frame as well. status_code is the status code of close control + * frame. reason is the close reason encoded in UTF-8. reason_length + * is the length of reason in bytes. reason_length must be less than + * 123 bytes. + * + * If status_code is 0, reason and reason_length is not used and close + * control frame with zero-length payload will be queued. + * + * This function just queues a message and does not send + * it. wslay_event_send() function call sends these queued messages. + * + * wslay_event_queue_close() returns 0 if it succeeds, or returns the + * following negative error codes: + * + * WSLAY_ERR_NO_MORE_MSG + * Could not queue given message. The one of possible reason is that + * close control frame has been queued/sent and no further queueing + * message is not allowed. + * + * WSLAY_ERR_INVALID_ARGUMENT + * The given message is invalid. + * + * WSLAY_ERR_NOMEM + * Out of memory. + */ +int wslay_event_queue_close(wslay_event_context_ptr ctx, uint16_t status_code, + const uint8_t *reason, size_t reason_length); + +/* + * Sets error code to tell the library there is an error. This + * function is typically used in user defined callback functions. See + * the description of callback function to know which error code + * should be used. + */ +void wslay_event_set_error(wslay_event_context_ptr ctx, int val); + +/* + * Query whehter the library want to read more data from peer. + * + * wslay_event_want_read() returns 1 if the library want to read more + * data from peer, or returns 0. + */ +int wslay_event_want_read(wslay_event_context_ptr ctx); + +/* + * Query whehter the library want to send more data to peer. + * + * wslay_event_want_write() returns 1 if the library want to send more + * data to peer, or returns 0. + */ +int wslay_event_want_write(wslay_event_context_ptr ctx); + +/* + * Prevents the event-based API context from reading any further data + * from peer. + * + * This function may be used with wslay_event_queue_close() if the + * application detects error in the data received and wants to fail + * WebSocket connection. + */ +void wslay_event_shutdown_read(wslay_event_context_ptr ctx); + +/* + * Prevents the event-based API context from sending any further data + * to peer. + */ +void wslay_event_shutdown_write(wslay_event_context_ptr ctx); + +/* + * Returns 1 if the event-based API context allows read operation, or + * return 0. + * + * After wslay_event_shutdown_read() is called, + * wslay_event_get_read_enabled() returns 0. + */ +int wslay_event_get_read_enabled(wslay_event_context_ptr ctx); + +/* + * Returns 1 if the event-based API context allows write operation, or + * return 0. + * + * After wslay_event_shutdown_write() is called, + * wslay_event_get_write_enabled() returns 0. + */ +int wslay_event_get_write_enabled(wslay_event_context_ptr ctx); + +/* + * Returns 1 if a close control frame has been received from peer, or + * returns 0. + */ +int wslay_event_get_close_received(wslay_event_context_ptr ctx); + +/* + * Returns 1 if a close control frame has been sent to peer, or + * returns 0. + */ +int wslay_event_get_close_sent(wslay_event_context_ptr ctx); + +/* + * Returns status code received in close control frame. If no close + * control frame has not been received, returns + * WSLAY_CODE_ABNORMAL_CLOSURE. If received close control frame has no + * status code, returns WSLAY_CODE_NO_STATUS_RCVD. + */ +uint16_t wslay_event_get_status_code_received(wslay_event_context_ptr ctx); + +/* + * Returns status code sent in close control frame. If no close + * control frame has not been sent, returns + * WSLAY_CODE_ABNORMAL_CLOSURE. If sent close control frame has no + * status code, returns WSLAY_CODE_NO_STATUS_RCVD. + */ +uint16_t wslay_event_get_status_code_sent(wslay_event_context_ptr ctx); + +/* + * Returns the number of queued messages. + */ +size_t wslay_event_get_queued_msg_count(wslay_event_context_ptr ctx); + +/* + * Returns the sum of queued message length. It only counts the + * message length queued using wslay_event_queue_msg() or + * wslay_event_queue_close(). + */ +size_t wslay_event_get_queued_msg_length(wslay_event_context_ptr ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* WSLAY_H */ diff --git a/src/spice2x/external/wslay/includes/wslay/wslayver.h b/src/spice2x/external/wslay/includes/wslay/wslayver.h new file mode 100644 index 0000000..b783c19 --- /dev/null +++ b/src/spice2x/external/wslay/includes/wslay/wslayver.h @@ -0,0 +1,31 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef WSLAYVER_H +#define WSLAYVER_H + +/* normally substituted by the build system; pinned to the vendored release */ +#define WSLAY_VERSION "1.1.1" + +#endif /* WSLAYVER_H */ diff --git a/src/spice2x/external/wslay/msvc_compat.h b/src/spice2x/external/wslay/msvc_compat.h new file mode 100644 index 0000000..e8871e0 --- /dev/null +++ b/src/spice2x/external/wslay/msvc_compat.h @@ -0,0 +1,9 @@ +#pragma once + +#ifdef _MSC_VER +#include +#ifndef _SSIZE_T_DEFINED +#define _SSIZE_T_DEFINED +typedef SSIZE_T ssize_t; +#endif +#endif diff --git a/src/spice2x/external/wslay/wslay_event.c b/src/spice2x/external/wslay/wslay_event.c new file mode 100644 index 0000000..4c29fe4 --- /dev/null +++ b/src/spice2x/external/wslay/wslay_event.c @@ -0,0 +1,1082 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "wslay_event.h" + +#include +#include +#include + +#include "wslay_frame.h" +#include "wslay_net.h" +#include "wslay_macro.h" +/* Start of utf8 dfa */ +/* Copyright (c) 2008-2010 Bjoern Hoehrmann + * See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. + * + * Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#define UTF8_ACCEPT 0 +#define UTF8_REJECT 12 + +/* clang-format off */ +static const uint8_t utf8d[] = { + /* + * The first part of the table maps bytes to character classes that + * to reduce the size of the transition table and create bitmasks. + */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8, + + /* + * The second part is a transition table that maps a combination + * of a state of the automaton and a character class to a state. + */ + 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, + 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, + 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, + 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, + 12,36,12,12,12,12,12,12,12,12,12,12, +}; +/* clang-format on */ + +static uint32_t decode(uint32_t *state, uint32_t *codep, uint32_t byte) { + uint32_t type = utf8d[byte]; + + *codep = (*state != UTF8_ACCEPT) ? (byte & 0x3fu) | (*codep << 6) + : (0xff >> type) & (byte); + + *state = utf8d[256 + *state + type]; + return *state; +} + +/* End of utf8 dfa */ + +static ssize_t wslay_event_frame_recv_callback(uint8_t *buf, size_t len, + int flags, void *user_data) { + struct wslay_event_frame_user_data *e = + (struct wslay_event_frame_user_data *)user_data; + return e->ctx->callbacks.recv_callback(e->ctx, buf, len, flags, e->user_data); +} + +static ssize_t wslay_event_frame_send_callback(const uint8_t *data, size_t len, + int flags, void *user_data) { + struct wslay_event_frame_user_data *e = + (struct wslay_event_frame_user_data *)user_data; + return e->ctx->callbacks.send_callback(e->ctx, data, len, flags, + e->user_data); +} + +static int wslay_event_frame_genmask_callback(uint8_t *buf, size_t len, + void *user_data) { + struct wslay_event_frame_user_data *e = + (struct wslay_event_frame_user_data *)user_data; + return e->ctx->callbacks.genmask_callback(e->ctx, buf, len, e->user_data); +} + +static int wslay_event_byte_chunk_init(struct wslay_event_byte_chunk **chunk, + size_t len) { + *chunk = malloc(sizeof(struct wslay_event_byte_chunk) + len); + if (*chunk == NULL) { + return WSLAY_ERR_NOMEM; + } + memset(*chunk, 0, sizeof(struct wslay_event_byte_chunk)); + if (len) { + (*chunk)->data = (uint8_t *)(*chunk) + sizeof(**chunk); + (*chunk)->data_length = len; + } + return 0; +} + +static void wslay_event_byte_chunk_free(struct wslay_event_byte_chunk *c) { + free(c); +} + +static void wslay_event_byte_chunk_copy(struct wslay_event_byte_chunk *c, + size_t off, const uint8_t *data, + size_t data_length) { + memcpy(c->data + off, data, data_length); +} + +static void wslay_event_imsg_set(struct wslay_event_imsg *m, uint8_t fin, + uint8_t rsv, uint8_t opcode) { + m->fin = fin; + m->rsv = rsv; + m->opcode = opcode; + m->msg_length = 0; +} + +static void wslay_event_imsg_chunks_free(struct wslay_event_imsg *m) { + while (!wslay_queue_empty(&m->chunks)) { + struct wslay_event_byte_chunk *chunk = wslay_struct_of( + wslay_queue_top(&m->chunks), struct wslay_event_byte_chunk, qe); + wslay_queue_pop(&m->chunks); + wslay_event_byte_chunk_free(chunk); + } +} + +static void wslay_event_imsg_reset(struct wslay_event_imsg *m) { + m->opcode = 0xffu; + m->utf8state = UTF8_ACCEPT; + wslay_event_imsg_chunks_free(m); +} + +static int wslay_event_imsg_append_chunk(struct wslay_event_imsg *m, + size_t len) { + if (len == 0) { + return 0; + } else { + int r; + struct wslay_event_byte_chunk *chunk; + if ((r = wslay_event_byte_chunk_init(&chunk, len)) != 0) { + return r; + } + wslay_queue_push(&m->chunks, &chunk->qe); + m->msg_length += len; + return 0; + } +} + +static int wslay_event_omsg_non_fragmented_init(struct wslay_event_omsg **m, + uint8_t opcode, uint8_t rsv, + const uint8_t *msg, + size_t msg_length) { + *m = malloc(sizeof(struct wslay_event_omsg) + msg_length); + if (!*m) { + return WSLAY_ERR_NOMEM; + } + memset(*m, 0, sizeof(struct wslay_event_omsg)); + (*m)->fin = 1; + (*m)->opcode = opcode; + (*m)->rsv = rsv; + (*m)->type = WSLAY_NON_FRAGMENTED; + if (msg_length) { + (*m)->data = (uint8_t *)(*m) + sizeof(**m); + memcpy((*m)->data, msg, msg_length); + (*m)->data_length = msg_length; + } + return 0; +} + +static int wslay_event_omsg_fragmented_init( + struct wslay_event_omsg **m, uint8_t opcode, uint8_t rsv, + const union wslay_event_msg_source source, + wslay_event_fragmented_msg_callback read_callback) { + *m = calloc(1, sizeof(struct wslay_event_omsg)); + if (!*m) { + return WSLAY_ERR_NOMEM; + } + (*m)->opcode = opcode; + (*m)->rsv = rsv; + (*m)->type = WSLAY_FRAGMENTED; + (*m)->source = source; + (*m)->read_callback = read_callback; + return 0; +} + +static void wslay_event_omsg_free(struct wslay_event_omsg *m) { free(m); } + +static uint8_t *wslay_event_flatten_queue(struct wslay_queue *queue, + size_t len) { + if (len == 0) { + return NULL; + } else { + size_t off = 0; + uint8_t *buf = malloc(len); + if (!buf) { + return NULL; + } + while (!wslay_queue_empty(queue)) { + struct wslay_event_byte_chunk *chunk = wslay_struct_of( + wslay_queue_top(queue), struct wslay_event_byte_chunk, qe); + wslay_queue_pop(queue); + memcpy(buf + off, chunk->data, chunk->data_length); + off += chunk->data_length; + wslay_event_byte_chunk_free(chunk); + assert(off <= len); + } + assert(len == off); + return buf; + } +} + +static int wslay_event_is_msg_queueable(wslay_event_context_ptr ctx) { + return ctx->write_enabled && (ctx->close_status & WSLAY_CLOSE_QUEUED) == 0; +} + +int wslay_event_queue_close(wslay_event_context_ptr ctx, uint16_t status_code, + const uint8_t *reason, size_t reason_length) { + if (!wslay_event_is_msg_queueable(ctx)) { + return WSLAY_ERR_NO_MORE_MSG; + } else if (reason_length > 123) { + return WSLAY_ERR_INVALID_ARGUMENT; + } else { + uint8_t msg[128]; + size_t msg_length; + struct wslay_event_msg arg; + uint16_t ncode; + int r; + if (status_code == 0) { + msg_length = 0; + } else { + ncode = htons(status_code); + memcpy(msg, &ncode, 2); + if (reason_length) { + memcpy(msg + 2, reason, reason_length); + } + msg_length = reason_length + 2; + } + arg.opcode = WSLAY_CONNECTION_CLOSE; + arg.msg = msg; + arg.msg_length = msg_length; + r = wslay_event_queue_msg(ctx, &arg); + if (r == 0) { + ctx->close_status |= WSLAY_CLOSE_QUEUED; + } + return r; + } +} + +static int wslay_event_queue_close_wrapper(wslay_event_context_ptr ctx, + uint16_t status_code, + const uint8_t *reason, + size_t reason_length) { + int r; + ctx->read_enabled = 0; + if ((r = wslay_event_queue_close(ctx, status_code, reason, reason_length)) && + r != WSLAY_ERR_NO_MORE_MSG) { + return r; + } + return 0; +} + +static int wslay_event_verify_rsv_bits(wslay_event_context_ptr ctx, + uint8_t rsv) { + return ((rsv & ~ctx->allowed_rsv_bits) == 0); +} + +int wslay_event_queue_msg(wslay_event_context_ptr ctx, + const struct wslay_event_msg *arg) { + return wslay_event_queue_msg_ex(ctx, arg, WSLAY_RSV_NONE); +} + +int wslay_event_queue_msg_ex(wslay_event_context_ptr ctx, + const struct wslay_event_msg *arg, uint8_t rsv) { + int r; + struct wslay_event_omsg *omsg; + if (!wslay_event_is_msg_queueable(ctx)) { + return WSLAY_ERR_NO_MORE_MSG; + } + /* RSV1 is not allowed for control frames */ + if ((wslay_is_ctrl_frame(arg->opcode) && + (arg->msg_length > 125 || wslay_get_rsv1(rsv))) || + !wslay_event_verify_rsv_bits(ctx, rsv)) { + return WSLAY_ERR_INVALID_ARGUMENT; + } + if ((r = wslay_event_omsg_non_fragmented_init( + &omsg, arg->opcode, rsv, arg->msg, arg->msg_length)) != 0) { + return r; + } + if (wslay_is_ctrl_frame(arg->opcode)) { + wslay_queue_push(&ctx->send_ctrl_queue, &omsg->qe); + } else { + wslay_queue_push(&ctx->send_queue, &omsg->qe); + } + ++ctx->queued_msg_count; + ctx->queued_msg_length += arg->msg_length; + return 0; +} + +int wslay_event_queue_fragmented_msg( + wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg) { + return wslay_event_queue_fragmented_msg_ex(ctx, arg, WSLAY_RSV_NONE); +} + +int wslay_event_queue_fragmented_msg_ex( + wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg, + uint8_t rsv) { + int r; + struct wslay_event_omsg *omsg; + if (!wslay_event_is_msg_queueable(ctx)) { + return WSLAY_ERR_NO_MORE_MSG; + } + if (wslay_is_ctrl_frame(arg->opcode) || + !wslay_event_verify_rsv_bits(ctx, rsv)) { + return WSLAY_ERR_INVALID_ARGUMENT; + } + if ((r = wslay_event_omsg_fragmented_init( + &omsg, arg->opcode, rsv, arg->source, arg->read_callback)) != 0) { + return r; + } + wslay_queue_push(&ctx->send_queue, &omsg->qe); + ++ctx->queued_msg_count; + return 0; +} + +void wslay_event_config_set_callbacks( + wslay_event_context_ptr ctx, + const struct wslay_event_callbacks *callbacks) { + ctx->callbacks = *callbacks; +} + +static int +wslay_event_context_init(wslay_event_context_ptr *ctx, + const struct wslay_event_callbacks *callbacks, + void *user_data) { + int i, r; + struct wslay_frame_callbacks frame_callbacks = { + wslay_event_frame_send_callback, wslay_event_frame_recv_callback, + wslay_event_frame_genmask_callback}; + *ctx = calloc(1, sizeof(struct wslay_event_context)); + if (!*ctx) { + return WSLAY_ERR_NOMEM; + } + wslay_event_config_set_callbacks(*ctx, callbacks); + (*ctx)->user_data = user_data; + (*ctx)->frame_user_data.ctx = *ctx; + (*ctx)->frame_user_data.user_data = user_data; + if ((r = wslay_frame_context_init(&(*ctx)->frame_ctx, &frame_callbacks, + &(*ctx)->frame_user_data)) != 0) { + wslay_event_context_free(*ctx); + return r; + } + (*ctx)->read_enabled = (*ctx)->write_enabled = 1; + wslay_queue_init(&(*ctx)->send_queue); + wslay_queue_init(&(*ctx)->send_ctrl_queue); + (*ctx)->queued_msg_count = 0; + (*ctx)->queued_msg_length = 0; + for (i = 0; i < 2; ++i) { + wslay_queue_init(&(*ctx)->imsgs[i].chunks); + wslay_event_imsg_reset(&(*ctx)->imsgs[i]); + } + (*ctx)->imsg = &(*ctx)->imsgs[0]; + (*ctx)->obufmark = (*ctx)->obuflimit = (*ctx)->obuf; + (*ctx)->status_code_sent = WSLAY_CODE_ABNORMAL_CLOSURE; + (*ctx)->status_code_recv = WSLAY_CODE_ABNORMAL_CLOSURE; + (*ctx)->max_recv_msg_length = (1u << 31) - 1; + return 0; +} + +int wslay_event_context_server_init( + wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks, + void *user_data) { + int r; + if ((r = wslay_event_context_init(ctx, callbacks, user_data)) != 0) { + return r; + } + (*ctx)->server = 1; + return 0; +} + +int wslay_event_context_client_init( + wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks, + void *user_data) { + int r; + if ((r = wslay_event_context_init(ctx, callbacks, user_data)) != 0) { + return r; + } + (*ctx)->server = 0; + return 0; +} + +void wslay_event_context_free(wslay_event_context_ptr ctx) { + int i; + if (!ctx) { + return; + } + for (i = 0; i < 2; ++i) { + wslay_event_imsg_chunks_free(&ctx->imsgs[i]); + wslay_queue_deinit(&ctx->imsgs[i].chunks); + } + + while (!wslay_queue_empty(&ctx->send_queue)) { + struct wslay_event_omsg *omsg = wslay_struct_of( + wslay_queue_top(&ctx->send_queue), struct wslay_event_omsg, qe); + wslay_queue_pop(&ctx->send_queue); + wslay_event_omsg_free(omsg); + } + wslay_queue_deinit(&ctx->send_queue); + + while (!wslay_queue_empty(&ctx->send_ctrl_queue)) { + struct wslay_event_omsg *omsg = wslay_struct_of( + wslay_queue_top(&ctx->send_ctrl_queue), struct wslay_event_omsg, qe); + wslay_queue_pop(&ctx->send_ctrl_queue); + wslay_event_omsg_free(omsg); + } + wslay_queue_deinit(&ctx->send_ctrl_queue); + + wslay_frame_context_free(ctx->frame_ctx); + wslay_event_omsg_free(ctx->omsg); + free(ctx); +} + +static void wslay_event_call_on_frame_recv_start_callback( + wslay_event_context_ptr ctx, const struct wslay_frame_iocb *iocb) { + if (ctx->callbacks.on_frame_recv_start_callback) { + struct wslay_event_on_frame_recv_start_arg arg; + arg.fin = iocb->fin; + arg.rsv = iocb->rsv; + arg.opcode = iocb->opcode; + arg.payload_length = iocb->payload_length; + ctx->callbacks.on_frame_recv_start_callback(ctx, &arg, ctx->user_data); + } +} + +static void wslay_event_call_on_frame_recv_chunk_callback( + wslay_event_context_ptr ctx, const struct wslay_frame_iocb *iocb) { + if (ctx->callbacks.on_frame_recv_chunk_callback) { + struct wslay_event_on_frame_recv_chunk_arg arg; + arg.data = iocb->data; + arg.data_length = iocb->data_length; + ctx->callbacks.on_frame_recv_chunk_callback(ctx, &arg, ctx->user_data); + } +} + +static void +wslay_event_call_on_frame_recv_end_callback(wslay_event_context_ptr ctx) { + if (ctx->callbacks.on_frame_recv_end_callback) { + ctx->callbacks.on_frame_recv_end_callback(ctx, ctx->user_data); + } +} + +static int wslay_event_is_valid_status_code(uint16_t status_code) { + return (1000 <= status_code && status_code <= 1011 && status_code != 1004 && + status_code != 1005 && status_code != 1006) || + (3000 <= status_code && status_code <= 4999); +} + +static int wslay_event_config_get_no_buffering(wslay_event_context_ptr ctx) { + return (ctx->config & WSLAY_CONFIG_NO_BUFFERING) > 0; +} + +int wslay_event_recv(wslay_event_context_ptr ctx) { + struct wslay_frame_iocb iocb; + ssize_t r; + while (ctx->read_enabled) { + memset(&iocb, 0, sizeof(iocb)); + r = wslay_frame_recv(ctx->frame_ctx, &iocb); + if (r >= 0) { + int new_frame = 0; + /* RSV1 is not allowed on control and continuation frames */ + if ((!wslay_event_verify_rsv_bits(ctx, iocb.rsv)) || + (wslay_get_rsv1(iocb.rsv) && + (wslay_is_ctrl_frame(iocb.opcode) || + iocb.opcode == WSLAY_CONTINUATION_FRAME)) || + (ctx->server && !iocb.mask) || (!ctx->server && iocb.mask)) { + if ((r = wslay_event_queue_close_wrapper(ctx, WSLAY_CODE_PROTOCOL_ERROR, + NULL, 0)) != 0) { + return (int)r; + } + break; + } + if (ctx->imsg->opcode == 0xffu) { + if (iocb.opcode == WSLAY_TEXT_FRAME || + iocb.opcode == WSLAY_BINARY_FRAME || + iocb.opcode == WSLAY_CONNECTION_CLOSE || + iocb.opcode == WSLAY_PING || iocb.opcode == WSLAY_PONG) { + wslay_event_imsg_set(ctx->imsg, iocb.fin, iocb.rsv, iocb.opcode); + new_frame = 1; + } else { + if ((r = wslay_event_queue_close_wrapper( + ctx, WSLAY_CODE_PROTOCOL_ERROR, NULL, 0)) != 0) { + return (int)r; + } + break; + } + } else if (ctx->ipayloadlen == 0 && ctx->ipayloadoff == 0) { + if (iocb.opcode == WSLAY_CONTINUATION_FRAME) { + ctx->imsg->fin = iocb.fin; + } else if (iocb.opcode == WSLAY_CONNECTION_CLOSE || + iocb.opcode == WSLAY_PING || iocb.opcode == WSLAY_PONG) { + ctx->imsg = &ctx->imsgs[1]; + wslay_event_imsg_set(ctx->imsg, iocb.fin, iocb.rsv, iocb.opcode); + } else { + if ((r = wslay_event_queue_close_wrapper( + ctx, WSLAY_CODE_PROTOCOL_ERROR, NULL, 0)) != 0) { + return (int)r; + } + break; + } + new_frame = 1; + } + if (new_frame) { + if (ctx->imsg->msg_length + iocb.payload_length > + ctx->max_recv_msg_length) { + if ((r = wslay_event_queue_close_wrapper( + ctx, WSLAY_CODE_MESSAGE_TOO_BIG, NULL, 0)) != 0) { + return (int)r; + } + break; + } + ctx->ipayloadlen = iocb.payload_length; + wslay_event_call_on_frame_recv_start_callback(ctx, &iocb); + if (!wslay_event_config_get_no_buffering(ctx) || + wslay_is_ctrl_frame(iocb.opcode)) { + if ((r = wslay_event_imsg_append_chunk(ctx->imsg, + iocb.payload_length)) != 0) { + ctx->read_enabled = 0; + return (int)r; + } + } + } + /* If RSV1 bit is set then it is too early for utf-8 validation */ + if ((!wslay_get_rsv1(ctx->imsg->rsv) && + ctx->imsg->opcode == WSLAY_TEXT_FRAME) || + ctx->imsg->opcode == WSLAY_CONNECTION_CLOSE) { + size_t i; + if (ctx->imsg->opcode == WSLAY_CONNECTION_CLOSE) { + i = 2; + } else { + i = 0; + } + for (; i < iocb.data_length; ++i) { + uint32_t codep; + if (decode(&ctx->imsg->utf8state, &codep, iocb.data[i]) == + UTF8_REJECT) { + if ((r = wslay_event_queue_close_wrapper( + ctx, WSLAY_CODE_INVALID_FRAME_PAYLOAD_DATA, NULL, 0)) != + 0) { + return (int)r; + } + break; + } + } + } + if (ctx->imsg->utf8state == UTF8_REJECT) { + break; + } + wslay_event_call_on_frame_recv_chunk_callback(ctx, &iocb); + if (iocb.data_length > 0) { + if (!wslay_event_config_get_no_buffering(ctx) || + wslay_is_ctrl_frame(iocb.opcode)) { + struct wslay_event_byte_chunk *chunk; + chunk = wslay_struct_of(wslay_queue_tail(&ctx->imsg->chunks), + struct wslay_event_byte_chunk, qe); + wslay_event_byte_chunk_copy(chunk, ctx->ipayloadoff, iocb.data, + iocb.data_length); + } + ctx->ipayloadoff += iocb.data_length; + } + if (ctx->ipayloadoff == ctx->ipayloadlen) { + if (ctx->imsg->fin && + (ctx->imsg->opcode == WSLAY_TEXT_FRAME || + ctx->imsg->opcode == WSLAY_CONNECTION_CLOSE) && + ctx->imsg->utf8state != UTF8_ACCEPT) { + if ((r = wslay_event_queue_close_wrapper( + ctx, WSLAY_CODE_INVALID_FRAME_PAYLOAD_DATA, NULL, 0)) != 0) { + return (int)r; + } + break; + } + wslay_event_call_on_frame_recv_end_callback(ctx); + if (ctx->imsg->fin) { + if (ctx->callbacks.on_msg_recv_callback || + ctx->imsg->opcode == WSLAY_CONNECTION_CLOSE || + ctx->imsg->opcode == WSLAY_PING) { + struct wslay_event_on_msg_recv_arg arg; + uint16_t status_code = 0; + uint8_t *msg = NULL; + size_t msg_length = 0; + if (!wslay_event_config_get_no_buffering(ctx) || + wslay_is_ctrl_frame(iocb.opcode)) { + msg = wslay_event_flatten_queue(&ctx->imsg->chunks, + ctx->imsg->msg_length); + if (ctx->imsg->msg_length && !msg) { + ctx->read_enabled = 0; + return WSLAY_ERR_NOMEM; + } + msg_length = ctx->imsg->msg_length; + } + if (ctx->imsg->opcode == WSLAY_CONNECTION_CLOSE) { + const uint8_t *reason; + size_t reason_length; + if (ctx->imsg->msg_length >= 2) { + memcpy(&status_code, msg, 2); + status_code = ntohs(status_code); + if (!wslay_event_is_valid_status_code(status_code)) { + free(msg); + if ((r = wslay_event_queue_close_wrapper( + ctx, WSLAY_CODE_PROTOCOL_ERROR, NULL, 0)) != 0) { + return (int)r; + } + break; + } + reason = msg + 2; + reason_length = ctx->imsg->msg_length - 2; + } else { + reason = NULL; + reason_length = 0; + } + ctx->close_status |= WSLAY_CLOSE_RECEIVED; + ctx->status_code_recv = + status_code == 0 ? WSLAY_CODE_NO_STATUS_RCVD : status_code; + if ((r = wslay_event_queue_close_wrapper(ctx, status_code, reason, + reason_length)) != 0) { + free(msg); + return (int)r; + } + } else if (ctx->imsg->opcode == WSLAY_PING) { + struct wslay_event_msg pong_arg; + pong_arg.opcode = WSLAY_PONG; + pong_arg.msg = msg; + pong_arg.msg_length = ctx->imsg->msg_length; + if ((r = wslay_event_queue_msg(ctx, &pong_arg)) && + r != WSLAY_ERR_NO_MORE_MSG) { + ctx->read_enabled = 0; + free(msg); + return (int)r; + } + } + if (ctx->callbacks.on_msg_recv_callback) { + arg.rsv = ctx->imsg->rsv; + arg.opcode = ctx->imsg->opcode; + arg.msg = msg; + arg.msg_length = msg_length; + arg.status_code = status_code; + ctx->error = 0; + ctx->callbacks.on_msg_recv_callback(ctx, &arg, ctx->user_data); + } + free(msg); + } + wslay_event_imsg_reset(ctx->imsg); + if (ctx->imsg == &ctx->imsgs[1]) { + ctx->imsg = &ctx->imsgs[0]; + } + } + ctx->ipayloadlen = ctx->ipayloadoff = 0; + } + } else { + if (r != WSLAY_ERR_WANT_READ || + (ctx->error != WSLAY_ERR_WOULDBLOCK && ctx->error != 0)) { + if ((r = wslay_event_queue_close_wrapper(ctx, 0, NULL, 0)) != 0) { + return (int)r; + } + return WSLAY_ERR_CALLBACK_FAILURE; + } + break; + } + } + return 0; +} + +static void +wslay_event_on_non_fragmented_msg_popped(wslay_event_context_ptr ctx) { + ctx->omsg->fin = 1; + ctx->opayloadlen = ctx->omsg->data_length; + ctx->opayloadoff = 0; +} + +static struct wslay_event_omsg * +wslay_event_send_ctrl_queue_pop(wslay_event_context_ptr ctx) { + /* + * If Close control frame is queued, we don't send any control frame + * other than Close. + */ + if (ctx->close_status & WSLAY_CLOSE_QUEUED) { + while (!wslay_queue_empty(&ctx->send_ctrl_queue)) { + struct wslay_event_omsg *msg = wslay_struct_of( + wslay_queue_top(&ctx->send_ctrl_queue), struct wslay_event_omsg, qe); + wslay_queue_pop(&ctx->send_ctrl_queue); + if (msg->opcode == WSLAY_CONNECTION_CLOSE) { + return msg; + } else { + wslay_event_omsg_free(msg); + } + } + return NULL; + } else { + struct wslay_event_omsg *msg = wslay_struct_of( + wslay_queue_top(&ctx->send_ctrl_queue), struct wslay_event_omsg, qe); + wslay_queue_pop(&ctx->send_ctrl_queue); + return msg; + } +} + +int wslay_event_send(wslay_event_context_ptr ctx) { + struct wslay_frame_iocb iocb; + ssize_t r; + while (ctx->write_enabled && + (!wslay_queue_empty(&ctx->send_queue) || + !wslay_queue_empty(&ctx->send_ctrl_queue) || ctx->omsg)) { + if (!ctx->omsg) { + if (wslay_queue_empty(&ctx->send_ctrl_queue)) { + ctx->omsg = wslay_struct_of(wslay_queue_top(&ctx->send_queue), + struct wslay_event_omsg, qe); + wslay_queue_pop(&ctx->send_queue); + } else { + ctx->omsg = wslay_event_send_ctrl_queue_pop(ctx); + if (ctx->omsg == NULL) { + break; + } + } + if (ctx->omsg->type == WSLAY_NON_FRAGMENTED) { + wslay_event_on_non_fragmented_msg_popped(ctx); + } + } else if (!wslay_is_ctrl_frame(ctx->omsg->opcode) && + ctx->frame_ctx->ostate == PREP_HEADER && + !wslay_queue_empty(&ctx->send_ctrl_queue)) { + wslay_queue_push_front(&ctx->send_queue, &ctx->omsg->qe); + ctx->omsg = wslay_event_send_ctrl_queue_pop(ctx); + if (ctx->omsg == NULL) { + break; + } + /* ctrl message has WSLAY_NON_FRAGMENTED */ + wslay_event_on_non_fragmented_msg_popped(ctx); + } + if (ctx->omsg->type == WSLAY_NON_FRAGMENTED) { + memset(&iocb, 0, sizeof(iocb)); + iocb.fin = 1; + iocb.opcode = ctx->omsg->opcode; + iocb.rsv = ctx->omsg->rsv; + iocb.mask = ctx->server ^ 1; + iocb.data = ctx->omsg->data; + iocb.data_length = ctx->opayloadlen; + if (ctx->opayloadoff) { + iocb.data += ctx->opayloadoff; + iocb.data_length -= ctx->opayloadoff; + } + iocb.payload_length = ctx->opayloadlen; + r = wslay_frame_send(ctx->frame_ctx, &iocb); + if (r >= 0) { + ctx->opayloadoff += (uint64_t)r; + if (ctx->opayloadoff == ctx->opayloadlen) { + --ctx->queued_msg_count; + ctx->queued_msg_length -= ctx->omsg->data_length; + if (ctx->omsg->opcode == WSLAY_CONNECTION_CLOSE) { + uint16_t status_code = 0; + ctx->write_enabled = 0; + ctx->close_status |= WSLAY_CLOSE_SENT; + if (ctx->omsg->data_length >= 2) { + memcpy(&status_code, ctx->omsg->data, 2); + status_code = ntohs(status_code); + } + ctx->status_code_sent = + status_code == 0 ? WSLAY_CODE_NO_STATUS_RCVD : status_code; + } + wslay_event_omsg_free(ctx->omsg); + ctx->omsg = NULL; + } else { + break; + } + } else { + if (r != WSLAY_ERR_WANT_WRITE || + (ctx->error != WSLAY_ERR_WOULDBLOCK && ctx->error != 0)) { + ctx->write_enabled = 0; + return WSLAY_ERR_CALLBACK_FAILURE; + } + break; + } + } else { + if (ctx->omsg->fin == 0 && ctx->obuflimit == ctx->obufmark) { + int eof = 0; + r = ctx->omsg->read_callback(ctx, ctx->obuf, sizeof(ctx->obuf), + &ctx->omsg->source, &eof, ctx->user_data); + if (r == 0 && eof == 0) { + break; + } else if (r < 0) { + ctx->write_enabled = 0; + return WSLAY_ERR_CALLBACK_FAILURE; + } + ctx->obuflimit = ctx->obuf + r; + if (eof) { + ctx->omsg->fin = 1; + } + ctx->opayloadlen = (uint64_t)r; + ctx->opayloadoff = 0; + } + memset(&iocb, 0, sizeof(iocb)); + iocb.fin = ctx->omsg->fin; + iocb.opcode = ctx->omsg->opcode; + iocb.rsv = ctx->omsg->rsv; + iocb.mask = ctx->server ? 0 : 1; + iocb.data = ctx->obufmark; + iocb.data_length = (size_t)(ctx->obuflimit - ctx->obufmark); + iocb.payload_length = ctx->opayloadlen; + r = wslay_frame_send(ctx->frame_ctx, &iocb); + if (r >= 0) { + ctx->obufmark += r; + if (ctx->obufmark == ctx->obuflimit) { + ctx->obufmark = ctx->obuflimit = ctx->obuf; + if (ctx->omsg->fin) { + --ctx->queued_msg_count; + wslay_event_omsg_free(ctx->omsg); + ctx->omsg = NULL; + } else { + ctx->omsg->opcode = WSLAY_CONTINUATION_FRAME; + /* RSV1 is not set on continuation frames */ + ctx->omsg->rsv = (uint8_t)(ctx->omsg->rsv & ~WSLAY_RSV1_BIT); + } + } else { + break; + } + } else { + if (r != WSLAY_ERR_WANT_WRITE || + (ctx->error != WSLAY_ERR_WOULDBLOCK && ctx->error != 0)) { + ctx->write_enabled = 0; + return WSLAY_ERR_CALLBACK_FAILURE; + } + break; + } + } + } + return 0; +} + +ssize_t wslay_event_write(wslay_event_context_ptr ctx, uint8_t *buf, + size_t buflen) { + struct wslay_frame_iocb iocb; + ssize_t r; + uint8_t *buf_last = buf; + size_t wpayloadlen; + while (ctx->write_enabled && + (!wslay_queue_empty(&ctx->send_queue) || + !wslay_queue_empty(&ctx->send_ctrl_queue) || ctx->omsg)) { + if (!ctx->omsg) { + if (wslay_queue_empty(&ctx->send_ctrl_queue)) { + ctx->omsg = wslay_struct_of(wslay_queue_top(&ctx->send_queue), + struct wslay_event_omsg, qe); + wslay_queue_pop(&ctx->send_queue); + } else { + ctx->omsg = wslay_event_send_ctrl_queue_pop(ctx); + if (ctx->omsg == NULL) { + break; + } + } + if (ctx->omsg->type == WSLAY_NON_FRAGMENTED) { + wslay_event_on_non_fragmented_msg_popped(ctx); + } + } else if (!wslay_is_ctrl_frame(ctx->omsg->opcode) && + ctx->frame_ctx->ostate == PREP_HEADER && + !wslay_queue_empty(&ctx->send_ctrl_queue)) { + wslay_queue_push_front(&ctx->send_queue, &ctx->omsg->qe); + ctx->omsg = wslay_event_send_ctrl_queue_pop(ctx); + if (ctx->omsg == NULL) { + break; + } + /* ctrl message has WSLAY_NON_FRAGMENTED */ + wslay_event_on_non_fragmented_msg_popped(ctx); + } + if (ctx->omsg->type == WSLAY_NON_FRAGMENTED) { + memset(&iocb, 0, sizeof(iocb)); + iocb.fin = 1; + iocb.opcode = ctx->omsg->opcode; + iocb.rsv = ctx->omsg->rsv; + iocb.mask = ctx->server ^ 1; + iocb.data = ctx->omsg->data; + iocb.data_length = ctx->opayloadlen; + if (ctx->opayloadoff) { + iocb.data += ctx->opayloadoff; + iocb.data_length -= ctx->opayloadoff; + } + iocb.payload_length = ctx->opayloadlen; + r = wslay_frame_write(ctx->frame_ctx, &iocb, buf_last, buflen, + &wpayloadlen); + if (r > 0) { + assert((size_t)r <= buflen); + + buf_last += r; + buflen -= (size_t)r; + + ctx->opayloadoff += wpayloadlen; + if (ctx->opayloadoff == ctx->opayloadlen) { + --ctx->queued_msg_count; + ctx->queued_msg_length -= ctx->omsg->data_length; + if (ctx->omsg->opcode == WSLAY_CONNECTION_CLOSE) { + uint16_t status_code = 0; + ctx->write_enabled = 0; + ctx->close_status |= WSLAY_CLOSE_SENT; + if (ctx->omsg->data_length >= 2) { + memcpy(&status_code, ctx->omsg->data, 2); + status_code = ntohs(status_code); + } + ctx->status_code_sent = + status_code == 0 ? WSLAY_CODE_NO_STATUS_RCVD : status_code; + } + wslay_event_omsg_free(ctx->omsg); + ctx->omsg = NULL; + } else { + break; + } + } else if (r == 0) { + return buf_last - buf; + } else { + return WSLAY_ERR_CALLBACK_FAILURE; + } + } else { + if (ctx->omsg->fin == 0 && ctx->obuflimit == ctx->obufmark) { + int eof = 0; + r = ctx->omsg->read_callback(ctx, ctx->obuf, sizeof(ctx->obuf), + &ctx->omsg->source, &eof, ctx->user_data); + if (r == 0 && eof == 0) { + break; + } else if (r < 0) { + ctx->write_enabled = 0; + return WSLAY_ERR_CALLBACK_FAILURE; + } + ctx->obuflimit = ctx->obuf + r; + if (eof) { + ctx->omsg->fin = 1; + } + ctx->opayloadlen = (uint64_t)r; + ctx->opayloadoff = 0; + } + memset(&iocb, 0, sizeof(iocb)); + iocb.fin = ctx->omsg->fin; + iocb.opcode = ctx->omsg->opcode; + iocb.rsv = ctx->omsg->rsv; + iocb.mask = ctx->server ? 0 : 1; + iocb.data = ctx->obufmark; + iocb.data_length = (size_t)(ctx->obuflimit - ctx->obufmark); + iocb.payload_length = ctx->opayloadlen; + r = wslay_frame_write(ctx->frame_ctx, &iocb, buf_last, buflen, + &wpayloadlen); + if (r > 0) { + assert((size_t)r <= buflen); + + buf_last += r; + buflen -= (size_t)r; + + ctx->obufmark += wpayloadlen; + if (ctx->obufmark == ctx->obuflimit) { + ctx->obufmark = ctx->obuflimit = ctx->obuf; + if (ctx->omsg->fin) { + --ctx->queued_msg_count; + wslay_event_omsg_free(ctx->omsg); + ctx->omsg = NULL; + } else { + ctx->omsg->opcode = WSLAY_CONTINUATION_FRAME; + /* RSV1 is not set on continuation frames */ + ctx->omsg->rsv = (uint8_t)(ctx->omsg->rsv & ~WSLAY_RSV1_BIT); + } + } else { + break; + } + } else if (r == 0) { + return buf_last - buf; + } else { + return WSLAY_ERR_CALLBACK_FAILURE; + } + } + } + return buf_last - buf; +} + +void wslay_event_set_error(wslay_event_context_ptr ctx, int val) { + ctx->error = val; +} + +int wslay_event_want_read(wslay_event_context_ptr ctx) { + return ctx->read_enabled; +} + +int wslay_event_want_write(wslay_event_context_ptr ctx) { + return ctx->write_enabled && + (!wslay_queue_empty(&ctx->send_queue) || + !wslay_queue_empty(&ctx->send_ctrl_queue) || ctx->omsg); +} + +void wslay_event_shutdown_read(wslay_event_context_ptr ctx) { + ctx->read_enabled = 0; +} + +void wslay_event_shutdown_write(wslay_event_context_ptr ctx) { + ctx->write_enabled = 0; +} + +int wslay_event_get_read_enabled(wslay_event_context_ptr ctx) { + return ctx->read_enabled; +} + +int wslay_event_get_write_enabled(wslay_event_context_ptr ctx) { + return ctx->write_enabled; +} + +int wslay_event_get_close_received(wslay_event_context_ptr ctx) { + return (ctx->close_status & WSLAY_CLOSE_RECEIVED) > 0; +} + +int wslay_event_get_close_sent(wslay_event_context_ptr ctx) { + return (ctx->close_status & WSLAY_CLOSE_SENT) > 0; +} + +void wslay_event_config_set_allowed_rsv_bits(wslay_event_context_ptr ctx, + uint8_t rsv) { + /* We currently only allow WSLAY_RSV1_BIT or WSLAY_RSV_NONE */ + ctx->allowed_rsv_bits = rsv & WSLAY_RSV1_BIT; +} + +void wslay_event_config_set_no_buffering(wslay_event_context_ptr ctx, int val) { + if (val) { + ctx->config |= WSLAY_CONFIG_NO_BUFFERING; + } else { + ctx->config &= (uint32_t)~WSLAY_CONFIG_NO_BUFFERING; + } +} + +void wslay_event_config_set_max_recv_msg_length(wslay_event_context_ptr ctx, + uint64_t val) { + ctx->max_recv_msg_length = val; +} + +uint16_t wslay_event_get_status_code_received(wslay_event_context_ptr ctx) { + return ctx->status_code_recv; +} + +uint16_t wslay_event_get_status_code_sent(wslay_event_context_ptr ctx) { + return ctx->status_code_sent; +} + +size_t wslay_event_get_queued_msg_count(wslay_event_context_ptr ctx) { + return ctx->queued_msg_count; +} + +size_t wslay_event_get_queued_msg_length(wslay_event_context_ptr ctx) { + return ctx->queued_msg_length; +} diff --git a/src/spice2x/external/wslay/wslay_event.h b/src/spice2x/external/wslay/wslay_event.h new file mode 100644 index 0000000..e30c3d1 --- /dev/null +++ b/src/spice2x/external/wslay/wslay_event.h @@ -0,0 +1,138 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef WSLAY_EVENT_H +#define WSLAY_EVENT_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* HAVE_CONFIG_H */ + +#include + +#include "wslay_queue.h" + +struct wslay_event_byte_chunk { + struct wslay_queue_entry qe; + uint8_t *data; + size_t data_length; +}; + +struct wslay_event_imsg { + uint8_t fin; + uint8_t rsv; + uint8_t opcode; + uint32_t utf8state; + struct wslay_queue chunks; + size_t msg_length; +}; + +enum wslay_event_msg_type { WSLAY_NON_FRAGMENTED, WSLAY_FRAGMENTED }; + +struct wslay_event_omsg { + struct wslay_queue_entry qe; + uint8_t fin; + uint8_t opcode; + uint8_t rsv; + enum wslay_event_msg_type type; + + uint8_t *data; + size_t data_length; + + union wslay_event_msg_source source; + wslay_event_fragmented_msg_callback read_callback; +}; + +struct wslay_event_frame_user_data { + wslay_event_context_ptr ctx; + void *user_data; +}; + +enum wslay_event_close_status { + WSLAY_CLOSE_RECEIVED = 1 << 0, + WSLAY_CLOSE_QUEUED = 1 << 1, + WSLAY_CLOSE_SENT = 1 << 2 +}; + +enum wslay_event_config { WSLAY_CONFIG_NO_BUFFERING = 1 << 0 }; + +struct wslay_event_context { + /* config status, bitwise OR of enum wslay_event_config values*/ + uint32_t config; + /* maximum message length that can be received */ + uint64_t max_recv_msg_length; + /* 1 if initialized for server, otherwise 0 */ + uint8_t server; + /* bitwise OR of enum wslay_event_close_status values */ + uint8_t close_status; + /* status code in received close control frame */ + uint16_t status_code_recv; + /* status code in sent close control frame */ + uint16_t status_code_sent; + wslay_frame_context_ptr frame_ctx; + /* 1 if reading is enabled, otherwise 0. Upon receiving close + control frame this value set to 0. If any errors in read + operation will also set this value to 0. */ + uint8_t read_enabled; + /* 1 if writing is enabled, otherwise 0 Upon completing sending + close control frame, this value set to 0. If any errors in write + opration will also set this value to 0. */ + uint8_t write_enabled; + /* imsg buffer to allow interleaved control frame between + non-control frames. */ + struct wslay_event_imsg imsgs[2]; + /* Pointer to imsgs to indicate current used buffer. */ + struct wslay_event_imsg *imsg; + /* payload length of frame currently being received. */ + uint64_t ipayloadlen; + /* next byte offset of payload currently being received. */ + uint64_t ipayloadoff; + /* error value set by user callback */ + int error; + /* Pointer to the message currently being sent. NULL if no message + is currently sent. */ + struct wslay_event_omsg *omsg; + /* Queue for non-control frames */ + struct wslay_queue /**/ send_queue; + /* Queue for control frames */ + struct wslay_queue /**/ send_ctrl_queue; + /* Size of send_queue + size of send_ctrl_queue */ + size_t queued_msg_count; + /* The sum of message length in send_queue */ + size_t queued_msg_length; + /* Buffer used for fragmented messages */ + uint8_t obuf[4096]; + uint8_t *obuflimit; + uint8_t *obufmark; + /* payload length of frame currently being sent. */ + uint64_t opayloadlen; + /* next byte offset of payload currently being sent. */ + uint64_t opayloadoff; + struct wslay_event_callbacks callbacks; + struct wslay_event_frame_user_data frame_user_data; + void *user_data; + uint8_t allowed_rsv_bits; +}; + +#endif /* WSLAY_EVENT_H */ diff --git a/src/spice2x/external/wslay/wslay_frame.c b/src/spice2x/external/wslay/wslay_frame.c new file mode 100644 index 0000000..fa065ee --- /dev/null +++ b/src/spice2x/external/wslay/wslay_frame.c @@ -0,0 +1,438 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "wslay_frame.h" + +#include +#include +#include + +#include "wslay_net.h" + +#define wslay_min(A, B) (((A) < (B)) ? (A) : (B)) + +int wslay_frame_context_init(wslay_frame_context_ptr *ctx, + const struct wslay_frame_callbacks *callbacks, + void *user_data) { + *ctx = malloc(sizeof(struct wslay_frame_context)); + if (*ctx == NULL) { + return -1; + } + memset(*ctx, 0, sizeof(struct wslay_frame_context)); + (*ctx)->istate = RECV_HEADER1; + (*ctx)->ireqread = 2; + (*ctx)->ostate = PREP_HEADER; + (*ctx)->user_data = user_data; + (*ctx)->ibufmark = (*ctx)->ibuflimit = (*ctx)->ibuf; + (*ctx)->callbacks = *callbacks; + return 0; +} + +void wslay_frame_context_free(wslay_frame_context_ptr ctx) { free(ctx); } + +ssize_t wslay_frame_send(wslay_frame_context_ptr ctx, + struct wslay_frame_iocb *iocb) { + if (iocb->data_length > iocb->payload_length) { + return WSLAY_ERR_INVALID_ARGUMENT; + } + if (ctx->ostate == PREP_HEADER) { + uint8_t *hdptr = ctx->oheader; + memset(ctx->oheader, 0, sizeof(ctx->oheader)); + *hdptr |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u); + *hdptr |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u); + /* Suppress stubborn gcc-10 warning */ + *hdptr |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu); + ++hdptr; + *hdptr |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u); + if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) { + return WSLAY_ERR_INVALID_ARGUMENT; + } + if (iocb->payload_length < 126) { + *hdptr |= (uint8_t)iocb->payload_length; + ++hdptr; + } else if (iocb->payload_length < (1 << 16)) { + uint16_t len = htons((uint16_t)iocb->payload_length); + *hdptr |= 126; + ++hdptr; + memcpy(hdptr, &len, 2); + hdptr += 2; + } else if (iocb->payload_length < (1ull << 63)) { + uint64_t len = hton64(iocb->payload_length); + *hdptr |= 127; + ++hdptr; + memcpy(hdptr, &len, 8); + hdptr += 8; + } else { + /* Too large payload length */ + return WSLAY_ERR_INVALID_ARGUMENT; + } + if (iocb->mask) { + if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) != + 0) { + return WSLAY_ERR_INVALID_CALLBACK; + } else { + ctx->omask = 1; + memcpy(hdptr, ctx->omaskkey, 4); + hdptr += 4; + } + } + ctx->ostate = SEND_HEADER; + ctx->oheadermark = ctx->oheader; + ctx->oheaderlimit = hdptr; + ctx->opayloadlen = iocb->payload_length; + ctx->opayloadoff = 0; + } + if (ctx->ostate == SEND_HEADER) { + ptrdiff_t len = ctx->oheaderlimit - ctx->oheadermark; + ssize_t r; + int flags = 0; + if (iocb->data_length > 0) { + flags |= WSLAY_MSG_MORE; + } + r = ctx->callbacks.send_callback(ctx->oheadermark, (size_t)len, flags, + ctx->user_data); + if (r > 0) { + if (r > len) { + return WSLAY_ERR_INVALID_CALLBACK; + } else { + ctx->oheadermark += r; + if (ctx->oheadermark == ctx->oheaderlimit) { + ctx->ostate = SEND_PAYLOAD; + } else { + return WSLAY_ERR_WANT_WRITE; + } + } + } else { + return WSLAY_ERR_WANT_WRITE; + } + } + if (ctx->ostate == SEND_PAYLOAD) { + size_t totallen = 0; + if (iocb->data_length > 0) { + if (ctx->omask) { + uint8_t temp[4096]; + const uint8_t *datamark = iocb->data, + *datalimit = iocb->data + iocb->data_length; + while (datamark < datalimit) { + size_t datalen = (size_t)(datalimit - datamark); + const uint8_t *writelimit = + datamark + wslay_min(sizeof(temp), datalen); + size_t writelen = (size_t)(writelimit - datamark); + ssize_t r; + size_t i; + for (i = 0; i < writelen; ++i) { + temp[i] = datamark[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4]; + } + r = ctx->callbacks.send_callback(temp, writelen, 0, ctx->user_data); + if (r > 0) { + if ((size_t)r > writelen) { + return WSLAY_ERR_INVALID_CALLBACK; + } else { + datamark += r; + ctx->opayloadoff += (uint64_t)r; + totallen += (size_t)r; + } + } else { + if (totallen > 0) { + break; + } else { + return WSLAY_ERR_WANT_WRITE; + } + } + } + } else { + ssize_t r; + r = ctx->callbacks.send_callback(iocb->data, iocb->data_length, 0, + ctx->user_data); + if (r > 0) { + if ((size_t)r > iocb->data_length) { + return WSLAY_ERR_INVALID_CALLBACK; + } else { + ctx->opayloadoff += (uint64_t)r; + totallen = (size_t)r; + } + } else { + return WSLAY_ERR_WANT_WRITE; + } + } + } + if (ctx->opayloadoff == ctx->opayloadlen) { + ctx->ostate = PREP_HEADER; + } + return (ssize_t)totallen; + } + return WSLAY_ERR_INVALID_ARGUMENT; +} + +ssize_t wslay_frame_write(wslay_frame_context_ptr ctx, + struct wslay_frame_iocb *iocb, uint8_t *buf, + size_t buflen, size_t *pwpayloadlen) { + uint8_t *buf_last = buf; + size_t i; + size_t hdlen; + + *pwpayloadlen = 0; + + if (iocb->data_length > iocb->payload_length) { + return WSLAY_ERR_INVALID_ARGUMENT; + } + + switch (ctx->ostate) { + case PREP_HEADER: + case PREP_HEADER_NOBUF: + hdlen = 2; + if (iocb->payload_length < 126) { + /* nothing to do */ + } else if (iocb->payload_length < (1 << 16)) { + hdlen += 2; + } else if (iocb->payload_length < (1ull << 63)) { + hdlen += 8; + } + if (iocb->mask) { + hdlen += 4; + } + + if (buflen < hdlen) { + ctx->ostate = PREP_HEADER_NOBUF; + return 0; + } + + memset(buf_last, 0, hdlen); + *buf_last |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u); + *buf_last |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u); + /* Suppress stubborn gcc-10 warning */ + *buf_last |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu); + ++buf_last; + *buf_last |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u); + if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) { + return WSLAY_ERR_INVALID_ARGUMENT; + } + if (iocb->payload_length < 126) { + *buf_last |= (uint8_t)iocb->payload_length; + ++buf_last; + } else if (iocb->payload_length < (1 << 16)) { + uint16_t len = htons((uint16_t)iocb->payload_length); + *buf_last |= 126; + ++buf_last; + memcpy(buf_last, &len, 2); + buf_last += 2; + } else if (iocb->payload_length < (1ull << 63)) { + uint64_t len = hton64(iocb->payload_length); + *buf_last |= 127; + ++buf_last; + memcpy(buf_last, &len, 8); + buf_last += 8; + } else { + /* Too large payload length */ + return WSLAY_ERR_INVALID_ARGUMENT; + } + if (iocb->mask) { + if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) != + 0) { + return WSLAY_ERR_INVALID_CALLBACK; + } else { + ctx->omask = 1; + memcpy(buf_last, ctx->omaskkey, 4); + buf_last += 4; + } + } + ctx->ostate = SEND_PAYLOAD; + ctx->opayloadlen = iocb->payload_length; + ctx->opayloadoff = 0; + + buflen -= (size_t)(buf_last - buf); + /* fall through */ + case SEND_PAYLOAD: + if (iocb->data_length > 0) { + size_t writelen = wslay_min(buflen, iocb->data_length); + + if (ctx->omask) { + for (i = 0; i < writelen; ++i) { + *buf_last++ = + iocb->data[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4]; + } + } else { + memcpy(buf_last, iocb->data, writelen); + buf_last += writelen; + } + + ctx->opayloadoff += writelen; + *pwpayloadlen = writelen; + } + + if (ctx->opayloadoff == ctx->opayloadlen) { + ctx->ostate = PREP_HEADER; + } + + return buf_last - buf; + default: + return WSLAY_ERR_INVALID_ARGUMENT; + } +} + +static void wslay_shift_ibuf(wslay_frame_context_ptr ctx) { + ptrdiff_t len = ctx->ibuflimit - ctx->ibufmark; + memmove(ctx->ibuf, ctx->ibufmark, (size_t)len); + ctx->ibuflimit = ctx->ibuf + len; + ctx->ibufmark = ctx->ibuf; +} + +static ssize_t wslay_recv(wslay_frame_context_ptr ctx) { + ssize_t r; + if (ctx->ibufmark != ctx->ibuf) { + wslay_shift_ibuf(ctx); + } + r = ctx->callbacks.recv_callback( + ctx->ibuflimit, (size_t)(ctx->ibuf + sizeof(ctx->ibuf) - ctx->ibuflimit), + 0, ctx->user_data); + if (r > 0) { + ctx->ibuflimit += r; + } else { + r = WSLAY_ERR_WANT_READ; + } + return r; +} + +#define WSLAY_AVAIL_IBUF(ctx) ((size_t)(ctx->ibuflimit - ctx->ibufmark)) + +ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx, + struct wslay_frame_iocb *iocb) { + ssize_t r; + if (ctx->istate == RECV_HEADER1) { + uint8_t fin, opcode, rsv, payloadlen; + if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { + if ((r = wslay_recv(ctx)) <= 0) { + return r; + } + } + if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { + return WSLAY_ERR_WANT_READ; + } + fin = (ctx->ibufmark[0] >> 7) & 1; + rsv = (ctx->ibufmark[0] >> 4) & 7; + opcode = ctx->ibufmark[0] & 0xfu; + ctx->iom.opcode = opcode; + ctx->iom.fin = fin; + ctx->iom.rsv = rsv; + ++ctx->ibufmark; + ctx->imask = (ctx->ibufmark[0] >> 7) & 1; + payloadlen = ctx->ibufmark[0] & 0x7fu; + ++ctx->ibufmark; + if (wslay_is_ctrl_frame(opcode) && (payloadlen > 125 || !fin)) { + return WSLAY_ERR_PROTO; + } + if (payloadlen == 126) { + ctx->istate = RECV_EXT_PAYLOADLEN; + ctx->ireqread = 2; + } else if (payloadlen == 127) { + ctx->istate = RECV_EXT_PAYLOADLEN; + ctx->ireqread = 8; + } else { + ctx->ipayloadlen = payloadlen; + ctx->ipayloadoff = 0; + if (ctx->imask) { + ctx->istate = RECV_MASKKEY; + ctx->ireqread = 4; + } else { + ctx->istate = RECV_PAYLOAD; + } + } + } + if (ctx->istate == RECV_EXT_PAYLOADLEN) { + if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { + if ((r = wslay_recv(ctx)) <= 0) { + return r; + } + if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { + return WSLAY_ERR_WANT_READ; + } + } + ctx->ipayloadlen = 0; + ctx->ipayloadoff = 0; + memcpy((uint8_t *)&ctx->ipayloadlen + (8 - ctx->ireqread), ctx->ibufmark, + ctx->ireqread); + ctx->ipayloadlen = ntoh64(ctx->ipayloadlen); + ctx->ibufmark += ctx->ireqread; + if (ctx->ireqread == 8) { + if (ctx->ipayloadlen < (1 << 16) || ctx->ipayloadlen & (1ull << 63)) { + return WSLAY_ERR_PROTO; + } + } else if (ctx->ipayloadlen < 126) { + return WSLAY_ERR_PROTO; + } + if (ctx->imask) { + ctx->istate = RECV_MASKKEY; + ctx->ireqread = 4; + } else { + ctx->istate = RECV_PAYLOAD; + } + } + if (ctx->istate == RECV_MASKKEY) { + if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { + if ((r = wslay_recv(ctx)) <= 0) { + return r; + } + if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { + return WSLAY_ERR_WANT_READ; + } + } + memcpy(ctx->imaskkey, ctx->ibufmark, 4); + ctx->ibufmark += 4; + ctx->istate = RECV_PAYLOAD; + } + if (ctx->istate == RECV_PAYLOAD) { + uint8_t *readlimit, *readmark; + uint64_t rempayloadlen = ctx->ipayloadlen - ctx->ipayloadoff; + if (WSLAY_AVAIL_IBUF(ctx) == 0 && rempayloadlen > 0) { + if ((r = wslay_recv(ctx)) <= 0) { + return r; + } + } + readmark = ctx->ibufmark; + readlimit = WSLAY_AVAIL_IBUF(ctx) < rempayloadlen + ? ctx->ibuflimit + : ctx->ibufmark + rempayloadlen; + if (ctx->imask) { + for (; ctx->ibufmark != readlimit; ++ctx->ibufmark, ++ctx->ipayloadoff) { + ctx->ibufmark[0] ^= ctx->imaskkey[ctx->ipayloadoff % 4]; + } + } else { + ctx->ibufmark = readlimit; + ctx->ipayloadoff += (uint64_t)(readlimit - readmark); + } + iocb->fin = ctx->iom.fin; + iocb->rsv = ctx->iom.rsv; + iocb->opcode = ctx->iom.opcode; + iocb->payload_length = ctx->ipayloadlen; + iocb->mask = ctx->imask; + iocb->data = readmark; + iocb->data_length = (size_t)(ctx->ibufmark - readmark); + if (ctx->ipayloadlen == ctx->ipayloadoff) { + ctx->istate = RECV_HEADER1; + ctx->ireqread = 2; + } + return (ssize_t)iocb->data_length; + } + return WSLAY_ERR_INVALID_ARGUMENT; +} diff --git a/src/spice2x/external/wslay/wslay_frame.h b/src/spice2x/external/wslay/wslay_frame.h new file mode 100644 index 0000000..3ac9811 --- /dev/null +++ b/src/spice2x/external/wslay/wslay_frame.h @@ -0,0 +1,77 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef WSLAY_FRAME_H +#define WSLAY_FRAME_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* HAVE_CONFIG_H */ + +#include + +enum wslay_frame_state { + PREP_HEADER, + PREP_HEADER_NOBUF, + SEND_HEADER, + SEND_PAYLOAD, + RECV_HEADER1, + RECV_PAYLOADLEN, + RECV_EXT_PAYLOADLEN, + RECV_MASKKEY, + RECV_PAYLOAD +}; + +struct wslay_frame_opcode_memo { + uint8_t fin; + uint8_t opcode; + uint8_t rsv; +}; + +struct wslay_frame_context { + uint8_t ibuf[4096]; + uint8_t *ibufmark; + uint8_t *ibuflimit; + struct wslay_frame_opcode_memo iom; + uint64_t ipayloadlen; + uint64_t ipayloadoff; + uint8_t imask; + uint8_t imaskkey[4]; + enum wslay_frame_state istate; + size_t ireqread; + + uint8_t oheader[14]; + uint8_t *oheadermark; + uint8_t *oheaderlimit; + uint64_t opayloadlen; + uint64_t opayloadoff; + uint8_t omask; + uint8_t omaskkey[4]; + enum wslay_frame_state ostate; + + struct wslay_frame_callbacks callbacks; + void *user_data; +}; + +#endif /* WSLAY_FRAME_H */ diff --git a/src/spice2x/external/wslay/wslay_macro.h b/src/spice2x/external/wslay/wslay_macro.h new file mode 100644 index 0000000..a06cff9 --- /dev/null +++ b/src/spice2x/external/wslay/wslay_macro.h @@ -0,0 +1,39 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2020 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef WSLAY_MACRO_H +#define WSLAY_MACRO_H + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include + +#include + +#define wslay_struct_of(ptr, type, member) \ + ((type *)(void *)((char *)(ptr)-offsetof(type, member))) + +#endif /* WSLAY_MACRO_H */ diff --git a/src/spice2x/external/wslay/wslay_net.c b/src/spice2x/external/wslay/wslay_net.c new file mode 100644 index 0000000..e256158 --- /dev/null +++ b/src/spice2x/external/wslay/wslay_net.c @@ -0,0 +1,35 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "wslay_net.h" + +#ifndef WORDS_BIGENDIAN + +uint64_t wslay_byteswap64(uint64_t x) { + uint64_t u = ntohl(x & 0xffffffffllu); + uint64_t l = ntohl((uint32_t)(x >> 32)); + return (u << 32) | l; +} + +#endif /* !WORDS_BIGENDIAN */ diff --git a/src/spice2x/external/wslay/wslay_net.h b/src/spice2x/external/wslay/wslay_net.h new file mode 100644 index 0000000..2310870 --- /dev/null +++ b/src/spice2x/external/wslay/wslay_net.h @@ -0,0 +1,54 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef WSLAY_NET_H +#define WSLAY_NET_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* HAVE_CONFIG_H */ + +#include + +#ifdef HAVE_ARPA_INET_H +# include +#endif /* HAVE_ARPA_INET_H */ +#ifdef HAVE_NETINET_IN_H +# include +#endif /* HAVE_NETINET_IN_H */ +/* For Mingw build */ +#ifdef HAVE_WINSOCK2_H +# include +#endif /* HAVE_WINSOCK2_H */ + +#ifdef WORDS_BIGENDIAN +# define ntoh64(x) (x) +# define hton64(x) (x) +#else /* !WORDS_BIGENDIAN */ +uint64_t wslay_byteswap64(uint64_t x); +# define ntoh64(x) wslay_byteswap64(x) +# define hton64(x) wslay_byteswap64(x) +#endif /* !WORDS_BIGENDIAN */ + +#endif /* WSLAY_NET_H */ diff --git a/src/spice2x/external/wslay/wslay_queue.c b/src/spice2x/external/wslay/wslay_queue.c new file mode 100644 index 0000000..ebde3ba --- /dev/null +++ b/src/spice2x/external/wslay/wslay_queue.c @@ -0,0 +1,77 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "wslay_queue.h" + +#include +#include + +#include "wslay_macro.h" + +void wslay_queue_init(struct wslay_queue *queue) { + queue->top = NULL; + queue->tail = &queue->top; +} + +void wslay_queue_deinit(struct wslay_queue *queue) { (void)queue; } + +void wslay_queue_push(struct wslay_queue *queue, + struct wslay_queue_entry *ent) { + ent->next = NULL; + *queue->tail = ent; + queue->tail = &ent->next; +} + +void wslay_queue_push_front(struct wslay_queue *queue, + struct wslay_queue_entry *ent) { + ent->next = queue->top; + queue->top = ent; + + if (ent->next == NULL) { + queue->tail = &ent->next; + } +} + +void wslay_queue_pop(struct wslay_queue *queue) { + assert(queue->top); + queue->top = queue->top->next; + if (queue->top == NULL) { + queue->tail = &queue->top; + } +} + +struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue) { + assert(queue->top); + return queue->top; +} + +struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue) { + assert(queue->top); + return wslay_struct_of(queue->tail, struct wslay_queue_entry, next); +} + +int wslay_queue_empty(struct wslay_queue *queue) { + assert(queue->top || queue->tail == &queue->top); + return queue->top == NULL; +} diff --git a/src/spice2x/external/wslay/wslay_queue.h b/src/spice2x/external/wslay/wslay_queue.h new file mode 100644 index 0000000..fa16aea --- /dev/null +++ b/src/spice2x/external/wslay/wslay_queue.h @@ -0,0 +1,53 @@ +/* + * Wslay - The WebSocket Library + * + * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef WSLAY_QUEUE_H +#define WSLAY_QUEUE_H + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include + +struct wslay_queue_entry { + struct wslay_queue_entry *next; +}; + +struct wslay_queue { + struct wslay_queue_entry *top; + struct wslay_queue_entry **tail; +}; + +void wslay_queue_init(struct wslay_queue *queue); +void wslay_queue_deinit(struct wslay_queue *queue); +void wslay_queue_push(struct wslay_queue *queue, struct wslay_queue_entry *ent); +void wslay_queue_push_front(struct wslay_queue *queue, + struct wslay_queue_entry *ent); +void wslay_queue_pop(struct wslay_queue *queue); +struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue); +struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue); +int wslay_queue_empty(struct wslay_queue *queue); + +#endif /* WSLAY_QUEUE_H */ diff --git a/src/spice2x/licenses.txt b/src/spice2x/licenses.txt index e552c1f..9021636 100644 --- a/src/spice2x/licenses.txt +++ b/src/spice2x/licenses.txt @@ -1402,6 +1402,52 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Wslay (MIT) +------------------------------------------- +Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Wslay UTF-8 decoder (MIT) +------------------------------------------- +Copyright (c) 2008-2010 Bjoern Hoehrmann +Copyright (c) 2008-2009 Bjoern Hoehrmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + Contributions ------------------------------------------- cardio - Felix - MIT License From 09314f1fd34062386052815312944a12af812624 Mon Sep 17 00:00:00 2001 From: bicarus-dev <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:52:44 -0700 Subject: [PATCH 07/17] fix bad merge --- src/spice2x/licenses.txt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/spice2x/licenses.txt b/src/spice2x/licenses.txt index 9021636..9aabf7a 100644 --- a/src/spice2x/licenses.txt +++ b/src/spice2x/licenses.txt @@ -1428,7 +1428,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Wslay UTF-8 decoder (MIT) ------------------------------------------- Copyright (c) 2008-2010 Bjoern Hoehrmann -Copyright (c) 2008-2009 Bjoern Hoehrmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -1448,11 +1447,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Contributions -------------------------------------------- -cardio - Felix - MIT License -scard - nolm - MIT License - x264 (GPL-2.0-or-later) ------------------------------------------- https://www.videolan.org/developers/x264.html @@ -1471,4 +1465,9 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin -Street, Fifth Floor, Boston, MA 02110-1301, USA. \ No newline at end of file +Street, Fifth Floor, Boston, MA 02110-1301, USA. + +Contributions +------------------------------------------- +cardio - Felix - MIT License +scard - nolm - MIT License From 1cdb7be810f97be4192c4076bd6362cd189ae53e Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 23 Aug 2026 00:34:08 -0700 Subject: [PATCH 08/17] overlay: fix hotkey init race (#888) ## Link to GitHub Issue or related Pull Request, if one exists Regressed by #864 ## Description of change The hotkey sampler thread starts touching `games::io` the moment `enable_raw_input()` is called, and that call sat right after `RI_MGR` was constructed, a few lines before the main thread's own first `games::io` calls. Both threads then hit the unsynchronized lazy `initialize()` at once, crashing at startup with an access violation in `memcmp`. Moved the call down next to `enable_input()`, after the bindings are built. ## Testing --- src/spice2x/launcher/launcher.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index b7f5b58..47339ac 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -2483,7 +2483,6 @@ int main_implementation(int argc, char *argv[]) { // initialize raw input RI_MGR = std::make_unique(); - hotkeys::enable_raw_input(); for (const auto &device : sextet_devices) { RI_MGR->sextet_register(device); } @@ -2507,6 +2506,7 @@ int main_implementation(int argc, char *argv[]) { dump_analog_bindings(); // mappings are ready; begin screenshot and coin polling during late startup + hotkeys::enable_raw_input(); hotkeys::enable_input(); // for certain games, show cursor if no touch is available (must be called after RI_MGR is available) From 21e7d24ed3dba1af0c03eea3defc482d7aaa8816 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:24:41 -0700 Subject: [PATCH 09/17] graphics: take stream captures off the game's present thread to avoid game perf hit (#889) ## Link to GitHub Issue or related Pull Request, if one exists #0 ## Description of change Capturing a frame for the API stream made the game wait for `GetRenderTargetData` in the middle of its present, roughly 1270us per frame at 1080p. A 120Hz cab visibly lost frames for as long as a viewer was connected. The present thread now only issues a `StretchRect` into a render target we own, which is queued rather than waited on, and a pool thread does the readback and the pixel conversion. That takes the present thread cost to 1-4us. Each snapshot is read on the request after the one that took it, so the blit and its transfer have a full frame to land and the read does not stall on the GPU either, at the cost of one frame of stream latency. Only streaming takes this path, and only on a device created with `D3DCREATE_MULTITHREADED`. Screenshots, `capture.get_jpg` and the `THREAD_BAN` models keep the existing inline readback unchanged. Also raises the x264 encoder from `i_threads = 1` to 4, which was holding a 1080p60 stream to 41fps and making a keyframe cost 12.7ms against 6.6ms for an ordinary frame. Capped rather than automatic because this encodes on the same machine it is capturing. ## Testing tested against iidx33, which was the most sensitive to frame drops --- src/spice2x/api/h264_stream.cpp | 8 +- .../graphics/backends/d3d9/d3d9_backend.cpp | 10 +- .../graphics/backends/d3d9/d3d9_device.cpp | 6 + .../graphics/backends/d3d9/d3d9_device.h | 4 + .../graphics/backends/d3d9/d3d9_readback.cpp | 435 +++++++++++++++++- .../graphics/backends/d3d9/d3d9_readback.h | 46 ++ .../backends/d3d9/d3d9_screenshot.cpp | 69 +++ 7 files changed, 551 insertions(+), 27 deletions(-) diff --git a/src/spice2x/api/h264_stream.cpp b/src/spice2x/api/h264_stream.cpp index 7774220..b51c5cb 100644 --- a/src/spice2x/api/h264_stream.cpp +++ b/src/spice2x/api/h264_stream.cpp @@ -118,7 +118,13 @@ namespace api { param.i_height = height; param.i_fps_num = this->fps; param.i_fps_den = 1; - param.i_threads = 1; + + // sliced threading, which zerolatency already selected, so a frame is split + // across workers rather than held back to be reordered. deliberately not the + // automatic count: this shares a machine with the game it is capturing, and + // taking every core to encode would win back frames at the game's expense + param.i_threads = 4; + param.b_annexb = 1; // SPS/PPS ahead of every IDR, so a client can start decoding cold param.b_repeat_headers = 1; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 0ff5452..966f943 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -944,9 +944,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice( } else if (!D3D9_DEVICE_HOOK_DISABLE) { graphics_hook_window(hFocusWindow, pPresentationParameters); - *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9( + auto *wrapped = new WrappedIDirect3DDevice9( hFocusWindow, *ppReturnedDeviceInterface); + + wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0; + *ppReturnedDeviceInterface = wrapped; } // return result @@ -1307,13 +1310,16 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx( } else if (!D3D9_DEVICE_HOOK_DISABLE) { graphics_hook_window(hFocusWindow, pPresentationParameters); - *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9( + auto *wrapped = new WrappedIDirect3DDevice9( hFocusWindow, *ppReturnedDeviceInterface, gfdm_parameters.logical_small_swapchain, gfdm_two_head_exclusive() ? static_cast(this) : nullptr, gfdm_two_head_exclusive() ? pPresentationParameters : nullptr); + wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0; + *ppReturnedDeviceInterface = wrapped; + // initialize sub screen if the game requested a multi-head context if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) && (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp index 26bb855..34a8863 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp @@ -634,6 +634,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset( overlay::OVERLAY->reset_invalidate(); } + // Reset refuses to run while any default pool resource is outstanding + d3d9_readback::discard_snapshot_targets(pReal); + HRESULT res = pReal->Reset(pPresentationParameters); // recreate overlay @@ -2321,6 +2324,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx( overlay::OVERLAY->reset_invalidate(); } + // ResetEx refuses to run while any default pool resource is outstanding + d3d9_readback::discard_snapshot_targets(pReal); + HRESULT res = static_cast(pReal)->ResetEx( gfdm_parameters.presentation_parameters, gfdm_parameters.fullscreen_display_modes); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h index f5b517c..586f3c4 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h @@ -264,6 +264,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { IDirect3DDevice9 *pReal; bool is_d3d9ex = false; + // set from the creation flags; without it the runtime does no locking of its own, so + // nothing but the present thread may touch the device + bool device_multithreaded = false; + std::atomic_ulong refs = 1; WrappedIDirect3DSwapChain9 *main_swapchain = nullptr; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.cpp index 1f36d77..5de25af 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.cpp @@ -1,5 +1,8 @@ #include "d3d9_readback.h" +#include +#include +#include #include #include @@ -10,6 +13,13 @@ namespace d3d9_readback { namespace { +// the snapshot path stamps frames with this to recognise one left behind by a break in the +// request stream +uint64_t now_us() { + return static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); +} + SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) { IDirect3DSurface9 *surface = nullptr; const HRESULT hr = device->CreateOffscreenPlainSurface( @@ -160,21 +170,8 @@ ReadbackPool &pool() { return *instance; } -} // namespace - -void release_device_resources(IDirect3DDevice9 *device) { - pool().clear_device(device); -} - -BackbufferCopy::~BackbufferCopy() { - if (this->pooled && this->surface) { - pool().release(this->device, std::move(this->surface)); - } -} - -std::optional acquire_backbuffer_copy( - IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) { - +// the back buffer plus the checks every caller has to make before copying out of it +SurfacePtr open_backbuffer(IDirect3DSwapChain9 *swap_chain, int screen, D3DSURFACE_DESC &desc) { IDirect3DSurface9 *buffer = nullptr; HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer); if (FAILED(hr) || buffer == nullptr) { @@ -182,17 +179,17 @@ std::optional acquire_backbuffer_copy( "failed to get back buffer for screen {}, hr={}", screen, FMT_HRESULT(hr)); - return std::nullopt; + return nullptr; } - D3DSURFACE_DESC desc {}; - hr = buffer->GetDesc(&desc); + SurfacePtr surface(buffer); + + hr = surface->GetDesc(&desc); if (FAILED(hr)) { log_warning("graphics::d3d9", "failed to acquire back buffer descriptor, hr={}", FMT_HRESULT(hr)); - buffer->Release(); - return std::nullopt; + return nullptr; } // GetRenderTargetData rejects multisampled sources. no supported game has been @@ -204,7 +201,399 @@ std::optional acquire_backbuffer_copy( "back buffer is multisampled ({}), screenshots and capture are unsupported", static_cast(desc.MultiSampleType)); }); - buffer->Release(); + return nullptr; + } + + return surface; +} + +SurfacePtr create_snapshot_target(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) { + IDirect3DSurface9 *surface = nullptr; + + // matching the back buffer keeps the blit a straight copy and leaves the pixels in the + // format the conversion step would have seen without the detour + const HRESULT hr = device->CreateRenderTarget( + desc.Width, desc.Height, desc.Format, + D3DMULTISAMPLE_NONE, 0, FALSE, &surface, nullptr); + + if (FAILED(hr) || surface == nullptr) { + log_warning("graphics::d3d9", + "failed to create snapshot target, hr={}", + FMT_HRESULT(hr)); + return nullptr; + } + + return SurfacePtr(surface); +} + +// two render targets per screen, reused across frames. two because one holds the frame waiting +// out its deferral while the other takes the next one; a read only holds its target until the +// pixels are in system memory, which is far shorter than the gap between requests. +// +// unlike the readback surfaces these live in the default pool, so they have to be gone before a +// Reset and not merely before the device is released +class SnapshotTargets { +public: + SurfacePtr acquire( + IDirect3DDevice9 *device, + int screen, + const D3DSURFACE_DESC &desc, + uint64_t *out_generation) { + + if (screen < 0 || screen >= static_cast(GRAPHICS_CAPTURE_SCREEN_NO)) { + return nullptr; + } + + std::lock_guard lock(this->mutex); + + if (this->device != device) { + this->drop(); + this->device = device; + this->usable = true; + } + + auto &entry = this->screens[screen]; + + Slot *free_slot = nullptr; + for (size_t i = 0; i < SLOTS_PER_SCREEN; i++) { + auto &candidate = entry.slots[(entry.next + i) % SLOTS_PER_SCREEN]; + if (!candidate.busy) { + free_slot = &candidate; + entry.next = (entry.next + i + 1) % SLOTS_PER_SCREEN; + break; + } + } + + if (free_slot == nullptr) { + return nullptr; + } + + auto &slot = *free_slot; + + if (slot.target + && (slot.width != desc.Width + || slot.height != desc.Height + || slot.format != desc.Format)) { + slot.target.reset(); + } + + if (!slot.target) { + slot.target = create_snapshot_target(device, desc); + if (!slot.target) { + // a back buffer format the device will not give us a render target for. the + // inline path can still read it, so stop trying rather than lose the stream + this->usable = false; + return nullptr; + } + + slot.width = desc.Width; + slot.height = desc.Height; + slot.format = desc.Format; + } + + slot.busy = true; + *out_generation = this->current_generation; + + slot.target->AddRef(); + return SurfacePtr(slot.target.get()); + } + + // holds the fresh snapshot back and returns the one from the previous request, which by now + // has had a full frame for its blit and transfer to land + std::optional rotate(int screen, std::optional fresh) { + if (screen < 0 || screen >= static_cast(GRAPHICS_CAPTURE_SCREEN_NO)) { + return std::nullopt; + } + + const uint64_t now = now_us(); + std::optional previous; + + { + std::lock_guard lock(this->mutex); + previous = std::move(this->pending[screen]); + this->pending[screen] = std::move(fresh); + } + + // a frame waits here for the next request rather than for a deadline, so a gap in + // requests, a client reconnecting most obviously, would otherwise hand the new session + // a frame from before the gap. showing a stale frame is worse than showing none + if (previous.has_value() && now - previous->issued_us > MAX_DEFERRAL_US) { + return std::nullopt; + } + + return previous; + } + + // a reader whose target was already thrown away must not free the slot, or it would free + // the snapshot that replaced it while that one is still being read + void finish(int screen, uint64_t generation, IDirect3DSurface9 *target) { + if (screen < 0 || screen >= static_cast(GRAPHICS_CAPTURE_SCREEN_NO)) { + return; + } + + { + std::lock_guard lock(this->mutex); + if (generation != this->current_generation) { + return; + } + + this->free_slot(screen, target); + } + + this->idle.notify_all(); + } + + bool still_current(uint64_t generation) { + std::lock_guard lock(this->mutex); + return generation == this->current_generation; + } + + bool is_usable() { + std::lock_guard lock(this->mutex); + return this->usable; + } + + // Reset fails outright while the device still owns default pool resources, so dropping our + // own references is not enough and any read in flight has to finish first. the wait is + // bounded because a failed Reset is something games retry and a stalled present thread is + // not something they survive + void discard(IDirect3DDevice9 *device) { + // destroyed after the lock is released, since dropping a snapshot calls back in here + std::array, GRAPHICS_CAPTURE_SCREEN_NO> stale; + + { + std::unique_lock lock(this->mutex); + if (this->device != nullptr && this->device != device) { + return; + } + + // deferred frames are abandoned rather than waited for; only a read that is already + // running has to be allowed to finish + for (auto &held : this->pending) { + if (held.has_value() && held->surface) { + this->free_slot(held->screen, held->surface.get()); + } + } + + stale = std::move(this->pending); + this->pending = {}; + + const bool drained = this->idle.wait_for( + lock, + std::chrono::milliseconds(100), + [this] { return !this->any_busy(); }); + + if (!drained) { + log_warning("graphics::d3d9", + "capture snapshot still in flight, discarding its target anyway"); + } + + this->drop(); + this->device = nullptr; + } + } + +private: + static constexpr size_t SLOTS_PER_SCREEN = 2; + + // generous next to the frame interval this is meant to bridge, so that ordinary jitter + // never trips it and only a real break in the request stream does + static constexpr uint64_t MAX_DEFERRAL_US = 250'000; + + struct Slot { + SurfacePtr target; + UINT width = 0; + UINT height = 0; + D3DFORMAT format = D3DFMT_UNKNOWN; + bool busy = false; + }; + + struct Screen { + std::array slots; + size_t next = 0; + }; + + void free_slot(int screen, IDirect3DSurface9 *target) { + for (auto &slot : this->screens[screen].slots) { + if (slot.target.get() == target) { + slot.busy = false; + return; + } + } + } + + bool any_busy() const { + for (const auto &entry : this->screens) { + for (const auto &slot : entry.slots) { + if (slot.busy) { + return true; + } + } + } + + return false; + } + + void drop() { + for (auto &entry : this->screens) { + for (auto &slot : entry.slots) { + slot.target.reset(); + slot.busy = false; + } + + entry.next = 0; + } + + this->current_generation++; + } + + std::mutex mutex; + std::condition_variable idle; + std::array screens; + std::array, GRAPHICS_CAPTURE_SCREEN_NO> pending; + IDirect3DDevice9 *device = nullptr; + uint64_t current_generation = 1; + bool usable = true; +}; + +// never destroyed, for the same reason the readback pool is not +SnapshotTargets &targets() { + static SnapshotTargets *instance = new SnapshotTargets(); + return *instance; +} + +} // namespace + +void release_device_resources(IDirect3DDevice9 *device) { + targets().discard(device); + pool().clear_device(device); +} + +void discard_snapshot_targets(IDirect3DDevice9 *device) { + targets().discard(device); +} + +bool snapshots_supported() { + return targets().is_usable(); +} + +BackbufferCopy::~BackbufferCopy() { + if (this->pooled && this->surface) { + pool().release(this->device, std::move(this->surface)); + } +} + +Snapshot::~Snapshot() { + // still holding the target means the read never ran, and the slot would otherwise stay + // marked busy and take the screen out of capture permanently + if (this->surface) { + targets().finish(this->screen, this->generation, this->surface.get()); + } +} + +namespace { + +std::optional take_snapshot( + IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) { + + const uint64_t started_us = now_us(); + + D3DSURFACE_DESC desc {}; + auto buffer = open_backbuffer(swap_chain, screen, desc); + if (!buffer) { + return std::nullopt; + } + + uint64_t generation = 0; + auto target = targets().acquire(device, screen, desc, &generation); + if (!target) { + return std::nullopt; + } + + // built before the blit so that a failure below hands the slot back through the destructor + Snapshot snapshot; + snapshot.screen = screen; + snapshot.desc = desc; + snapshot.device = device; + snapshot.surface = std::move(target); + snapshot.generation = generation; + snapshot.issued_us = started_us; + + // the point of the whole exercise: this is queued rather than waited on, so the game pays + // for issuing the copy and not for it completing. identical size and format, so there is + // no filtering to ask for + const HRESULT hr = device->StretchRect( + buffer.get(), nullptr, snapshot.surface.get(), nullptr, D3DTEXF_NONE); + + if (FAILED(hr)) { + log_warning("graphics::d3d9", + "failed to snapshot back buffer for screen {}, hr={}", + screen, + FMT_HRESULT(hr)); + return std::nullopt; + } + + return snapshot; +} + +} // namespace + +std::optional snapshot_backbuffer( + IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) { + + return targets().rotate(screen, take_snapshot(device, swap_chain, screen)); +} + +std::optional read_snapshot(Snapshot snapshot) { + if (!snapshot.surface) { + return std::nullopt; + } + + // a Reset between the blit and now means the target no longer holds the captured frame + if (!targets().still_current(snapshot.generation)) { + return std::nullopt; + } + + auto destination = pool().acquire(snapshot.device, snapshot.desc); + if (!destination) { + return std::nullopt; + } + + const HRESULT hr = snapshot.device->GetRenderTargetData( + snapshot.surface.get(), destination.get()); + + // the target is reusable as soon as the pixels are in system memory. dropping the + // reference before freeing the slot keeps the destructor from freeing it twice + const int screen = snapshot.screen; + const uint64_t generation = snapshot.generation; + IDirect3DSurface9 *target = snapshot.surface.get(); + snapshot.surface.reset(); + targets().finish(screen, generation, target); + + if (FAILED(hr)) { + log_warning("graphics::d3d9", + "failed to read snapshot contents, hr={}", + FMT_HRESULT(hr)); + pool().release(snapshot.device, std::move(destination)); + return std::nullopt; + } + + BackbufferCopy copy; + copy.screen = screen; + copy.desc = snapshot.desc; + copy.device = snapshot.device; + copy.surface = std::move(destination); + copy.pooled = true; + + return copy; +} + +std::optional acquire_backbuffer_copy( + IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) { + + D3DSURFACE_DESC desc {}; + auto buffer = open_backbuffer(swap_chain, screen, desc); + if (!buffer) { return std::nullopt; } @@ -212,12 +601,10 @@ std::optional acquire_backbuffer_copy( ? pool().acquire(device, desc) : create_readback_surface(device, desc); if (!destination) { - buffer->Release(); return std::nullopt; } - hr = device->GetRenderTargetData(buffer, destination.get()); - buffer->Release(); + const HRESULT hr = device->GetRenderTargetData(buffer.get(), destination.get()); if (FAILED(hr)) { log_warning("graphics::d3d9", diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.h index d49e595..017f3e4 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -39,6 +40,51 @@ namespace d3d9_readback { int screen, bool pooled); + // GPU side copy of a back buffer, taken while the contents are still the frame that was + // presented, so that reading them into system memory no longer has to happen before it + struct Snapshot { + int screen {}; + D3DSURFACE_DESC desc {}; + IDirect3DDevice9 *device = nullptr; + SurfacePtr surface; + uint64_t generation {}; + + // when the blit was issued, so a frame left behind by a break in the request stream can + // be recognised as stale rather than handed over + uint64_t issued_us {}; + + Snapshot() = default; + Snapshot(Snapshot &&) noexcept = default; + Snapshot &operator=(Snapshot &&) noexcept = default; + Snapshot(const Snapshot &) = delete; + Snapshot &operator=(const Snapshot &) = delete; + ~Snapshot(); + }; + + // for the present thread, between the last EndScene and Present. blits the current frame, + // then returns the snapshot taken on the *previous* call: waiting a frame before reading + // means the blit and its system memory transfer have already happened, so the read does not + // stall on the GPU. costs the stream one frame of latency. + // + // returns nothing on the first call of a stream, and whenever the frame could not be taken, + // which is the caller's cue to skip rather than to wait + std::optional snapshot_backbuffer( + IDirect3DDevice9 *device, + IDirect3DSwapChain9 *swap_chain, + int screen); + + // the expensive half, for a thread that is not the present thread. only legal on a device + // created with D3DCREATE_MULTITHREADED + std::optional read_snapshot(Snapshot snapshot); + + // false once a device has refused to give up a render target matching its back buffer, + // which leaves reading the back buffer directly as the only way to capture it + bool snapshots_supported(); + + // snapshot targets live in the default pool, so unlike the readback surfaces they have to + // be gone before a Reset and not merely before the device is released + void discard_snapshot_targets(IDirect3DDevice9 *device); + // pooled surfaces hold references on the device; call this before releasing it void release_device_resources(IDirect3DDevice9 *device); } diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp index ed8ddfb..0dc90e2 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp @@ -488,6 +488,63 @@ ThreadPool &capture_read_pool() { return *instance; } +// Takes the frame on the present thread as a queued GPU blit and hands the readback to a pool +// thread, so the game waits for neither. Only viable where the whole read can go off thread, +// since the back buffer is overwritten right after Present and a snapshot the present thread +// then had to read itself would cost more than reading the back buffer directly. +// +// Returns false when the frame could not be taken, including the ordinary case of the previous +// snapshot of this screen still being read, which paces capture to what the reader sustains. +static bool snapshot_capture( + IDirect3DDevice9 *device, + WrappedIDirect3DDevice9 *wrapped_device, + int screen) { + + IDirect3DSwapChain9 *swap_chain = nullptr; + const HRESULT hr = wrapped_device->get_screenshot_swap_chain(screen, &swap_chain); + if (FAILED(hr) || swap_chain == nullptr) { + log_warning("graphics::d3d9", + "failed to get swap chain for screen {}, hr={}", + screen, + FMT_HRESULT(hr)); + return false; + } + + auto snapshot = d3d9_readback::snapshot_backbuffer(device, swap_chain, screen); + swap_chain->Release(); + + if (!snapshot.has_value()) { + return false; + } + + try { + capture_read_pool().add([screen, snapshot = std::move(*snapshot)]() mutable { + // an escape from here would cross a thread boundary and terminate + try { + auto copy = d3d9_readback::read_snapshot(std::move(snapshot)); + if (!copy.has_value()) { + graphics_capture_skip(screen); + return; + } + + read_and_dispatch_capture(screen, std::move(*copy)); + } catch (const std::exception &error) { + log_warning("graphics::d3d9", "capture read failed: {}", error.what()); + graphics_capture_skip(screen); + } catch (...) { + log_warning("graphics::d3d9", "capture read failed"); + graphics_capture_skip(screen); + } + }); + } catch (const std::exception &) { + // the snapshot went into the lambda before the queue could fail, so it is already + // destroyed and its target handed back; the client just misses this frame + return false; + } + + return true; +} + // by this point the pixels are plain memory, so none of this needs the device static void dispatch_screenshot_save(std::vector writes, size_t screen_count) { auto screenshot_process = [writes = std::move(writes), screen_count]() mutable { @@ -632,6 +689,18 @@ static void process_image_request( const ImageRequest &request) { const bool screenshot = request.kind == ImageRequestKind::Screenshot; + if (!screenshot + && wrapped_device->device_multithreaded + && capture_read_off_thread(request.screen) + && d3d9_readback::snapshots_supported()) { + + if (!snapshot_capture(device, wrapped_device, request.screen)) { + graphics_capture_skip(request.screen); + } + + return; + } + std::vector screens { request.screen }; if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) { screens.clear(); From 699659d4bfa68ff78d6b15ca575ead5c9d08fe13 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:24:13 -0700 Subject: [PATCH 10/17] cfg: update video options (#890) Move around legacy video options. --- src/spice2x/launcher/options.cpp | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 3bd1e1e..22daf22 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1729,7 +1729,7 @@ static const std::vector OPTION_DEFINITIONS = { "Value must be between 0 (poor quality) and 100 (best quality), inclusive. Default: 70.", .type = OptionType::Integer, .setting_name = "(0-100)", - .category = "Companion & API", + .category = "API Dev", }, { // APIScreenMirrorDivide @@ -1741,21 +1741,17 @@ static const std::vector OPTION_DEFINITIONS = { "Value must be 1 or greater. Default: 1.", .type = OptionType::Integer, .setting_name = "1", - .category = "Companion & API", + .category = "API Dev", }, { // APIStreamEnable .title = "API Video Stream Server Enable (EXPERIMENTAL)", .name = "apistream", - .desc = "Serves the mirrored screen as a video stream, on the API port plus two; " - "alternative to API screen capture. Requires -api.\n\n" - "http://host:apiport+2/stream.mjpg - MJPEG\n\n" - "http://host:apiport+2/stream.h264 - H.264\n\n" - "Parameters: screen (0-3), fps (1-60, default 30), q (1-100, default 70).\n\n" - "Example with -api 1337: http://host:1339/stream.h264?fps=30&q=70\n\n" - "VIEW ONLY - touch input still requires -api. " - "No password protection or encryption of any kind; video sent in the clear!\n\n" - "Streaming is known to cause older games to hang and crash.", + .desc = "Allows companion apps to receive compressed video streams over the API. " + "Companion app must support this feature to take advantage of it.\n\n" + "Video is served unencrypted over the network. " + "May cause older games to hang and crash.\n\n" + "Developers: see the wiki page for more details.", .type = OptionType::Bool, .category = "Companion & API", }, From dad88caf019ed1af60f174138b895ba90cd1a626 Mon Sep 17 00:00:00 2001 From: Jiongjia Lu Date: Tue, 25 Aug 2026 15:49:31 +0800 Subject: [PATCH 11/17] api: add card lookup endpoint (#891) ## Link to GitHub Issue or related Pull Request, if one exists N/A ## Description of change I am developing [spice.nimabe.net](https://spice.nimabe.net), a static frontend for SpiceAPI. It already supports generating and inserting new card IDs, but SpiceAPI does not provide a way to discover cards already configured on the game machine. This makes the frontend inconvenient for existing users who generated and saved their card IDs locally. They currently have to open each card file on the game machine and manually copy its ID into the frontend. This change adds a `card.get_cards` API function that enumerates the current cards for readers supported by the running game. Each entry contains: - reader index - canonical 16-character card ID - `source`, set to `file` or `override` - `file_name` for file-backed cards File-backed cards expose only the save-file basename. Active `-card0` and `-card1` values are returned with `source: "override"` and no `file_name`, instead of representing an override as a file. spicefe derives the useful default import names `card0` and `card1` from the reader index. Reader enumeration uses `eamuse_get_game_keypads()` so games with only one reader do not expose a second card. This allows spicefe and other API clients to offer one-click import for existing cards, without requiring users to manually find and copy their card IDs. Because card IDs are sensitive, `card.get_cards` can only be used when the operator has configured an API password. No full filesystem path is exposed. Missing, unreadable, and invalid cards are omitted. The existing `card.insert` function remains unchanged. To avoid requiring a password for the whole card module, this change adds support for password requirements on individual API functions while preserving existing module-wide password behavior. Access to configured card paths and runtime card overrides is synchronized because API requests may read them concurrently with configuration updates. The Python and Dart API wrappers and README documentation have been updated for the new function and source metadata. ## Testing - Ran the complete `src/spice2x/build_docker.sh` build successfully. - Built all required 32-bit and 64-bit targets. - Built the Windows XP-compatible 32-bit targets. - Passed static import checks. - Passed Windows 7 and Windows XP compatibility checks. - Completed release packaging successfully. - Verified both updated Dart wrappers are correctly formatted. - Ran all 106 spicefe tests successfully. - `git diff --check` passes. *This implementation was prepared with assistance from OpenAI Codex.* --- src/spice2x/README.md | 7 ++ src/spice2x/api/controller.cpp | 8 ++- src/spice2x/api/module.cpp | 8 +++ src/spice2x/api/module.h | 10 +++ src/spice2x/api/modules/card.cpp | 64 +++++++++++++++++++ src/spice2x/api/modules/card.h | 1 + .../spiceapi-websocket/src/wrappers/card.dart | 27 ++++++++ .../dart/spiceapi/src/wrappers/card.dart | 27 ++++++++ .../api/resources/python/spiceapi/card.py | 4 ++ src/spice2x/misc/eamuse.cpp | 40 ++++++++---- src/spice2x/misc/eamuse.h | 2 + 11 files changed, 181 insertions(+), 17 deletions(-) diff --git a/src/spice2x/README.md b/src/spice2x/README.md index 4425487..342ea7e 100644 --- a/src/spice2x/README.md +++ b/src/spice2x/README.md @@ -144,6 +144,13 @@ restriction is that the ID has to be a valid 64-bit unsigned integer. `busy` #### Card +- get_cards() + - returns the current card ID and source for each active card reader + - each entry contains `index`, `card_id`, and `source` + - `source` is `file` or `override` + - file entries also contain `file_name`, without the full configured path + - `-card0` and `-card1` overrides do not contain `file_name` + - this function only works when an API password is configured - insert(index: uint, card_id: hex) - inserts a card which gets read by the emulated card readers for the game - index has to be either 0 (for P1) or 1 (for P2) diff --git a/src/spice2x/api/controller.cpp b/src/spice2x/api/controller.cpp index 1fd41d5..1a6c4dd 100644 --- a/src/spice2x/api/controller.cpp +++ b/src/spice2x/api/controller.cpp @@ -337,9 +337,11 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s if (module->name == request.module) { module_found = true; - // check password force - if (module->password_force && this->password.empty() && request.function != "session_refresh") { - Value err("Module requires the password to be set."); + // check password requirement + if (module->requires_password(request.function) + && this->password.empty() + && request.function != "session_refresh") { + Value err("Function requires the password to be set."); response.add_error(err); break; } diff --git a/src/spice2x/api/module.cpp b/src/spice2x/api/module.cpp index 700a562..af2a0e8 100644 --- a/src/spice2x/api/module.cpp +++ b/src/spice2x/api/module.cpp @@ -16,6 +16,14 @@ namespace api { this->password_force = password_force; } + void Module::require_password(const std::string &function) { + this->password_force_functions.emplace(function); + } + + bool Module::requires_password(const std::string &function) const { + return this->password_force || this->password_force_functions.contains(function); + } + void Module::handle(Request &req, Response &res) { // log module access diff --git a/src/spice2x/api/module.h b/src/spice2x/api/module.h index d41b787..262a893 100644 --- a/src/spice2x/api/module.h +++ b/src/spice2x/api/module.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "response.h" @@ -26,6 +27,13 @@ namespace api { // default constructor explicit Module(std::string name, bool password_force=false); + void require_password(const std::string &function); + + private: + + // functions which expose sensitive data or actions + std::unordered_set password_force_functions; + public: // virtual deconstructor @@ -35,6 +43,8 @@ namespace api { std::string name; bool password_force; + bool requires_password(const std::string &function) const; + // the magic void handle(Request &req, Response &res); diff --git a/src/spice2x/api/modules/card.cpp b/src/spice2x/api/modules/card.cpp index 5eb262a..ac65560 100644 --- a/src/spice2x/api/modules/card.cpp +++ b/src/spice2x/api/modules/card.cpp @@ -1,4 +1,5 @@ #include "card.h" +#include #include #include "external/rapidjson/document.h" #include "util/logging.h" @@ -11,8 +12,71 @@ using namespace rapidjson; namespace api::modules { + static bool normalize_card_id(const std::string &value, std::string &card_id) { + if (value.size() != 16) { + return false; + } + + uint8_t card_bin[8] {}; + if (!hex2bin(value.c_str(), card_bin)) { + return false; + } + + card_id = bin2hex(card_bin, std::size(card_bin)); + return true; + } + + static bool read_card_id(const std::filesystem::path &path, std::string &card_id) { + std::ifstream file(path); + char buffer[16] {}; + if (!file.read(buffer, std::size(buffer))) { + return false; + } + + return normalize_card_id(std::string(buffer, std::size(buffer)), card_id); + } + Card::Card() : Module("card") { + functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2); functions["insert"] = std::bind(&Card::insert, this, _1, _2); + require_password("get_cards"); + } + + /** + * get_cards() + */ + void Card::get_cards(Request &req, Response &res) { + auto &alloc = res.doc()->GetAllocator(); + + for (int index = 0; index < eamuse_get_game_keypads(); index++) { + std::string card_id; + std::string filename; + const auto card_override = eamuse_get_card_override(index); + const bool has_override = !card_override.empty(); + + if (has_override) { + if (!normalize_card_id(card_override, card_id)) { + continue; + } + } else { + const auto path = eamuse_get_card_path(index); + if (!read_card_id(path, card_id)) { + continue; + } + + const auto filename_u8 = path.filename().u8string(); + filename.assign(filename_u8.begin(), filename_u8.end()); + } + + Value card(kObjectType); + card.AddMember("index", index, alloc); + card.AddMember("card_id", Value(card_id.c_str(), alloc), alloc); + card.AddMember("source", Value(has_override ? "override" : "file", alloc), alloc); + if (!has_override) { + card.AddMember("file_name", Value(filename.c_str(), alloc), alloc); + } + res.add_data(card); + } } /** diff --git a/src/spice2x/api/modules/card.h b/src/spice2x/api/modules/card.h index 6012176..e90fbe3 100644 --- a/src/spice2x/api/modules/card.h +++ b/src/spice2x/api/modules/card.h @@ -12,6 +12,7 @@ namespace api::modules { private: // function definitions + void get_cards(Request &req, Response &res); void insert(Request &req, Response &res); }; } diff --git a/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/card.dart b/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/card.dart index 2a42d63..1bd487d 100644 --- a/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/card.dart +++ b/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/card.dart @@ -1,5 +1,32 @@ part of spiceapi; +class CardInfo { + final int index; + final String cardID; + final String source; + final String? fileName; + + CardInfo(this.index, this.cardID, this.source, this.fileName); +} + +Future> cardGetCards(Connection con) { + var req = Request("card", "get_cards"); + return con.request(req).then((res) { + List cards = []; + for (var value in res.getData()) { + cards.add( + CardInfo( + value["index"], + value["card_id"], + value["source"], + value["file_name"], + ), + ); + } + return cards; + }); +} + Future cardInsert(Connection con, int unit, String cardID) { var req = Request("card", "insert"); req.addParam(unit); diff --git a/src/spice2x/api/resources/dart/spiceapi/src/wrappers/card.dart b/src/spice2x/api/resources/dart/spiceapi/src/wrappers/card.dart index 2a42d63..1bd487d 100644 --- a/src/spice2x/api/resources/dart/spiceapi/src/wrappers/card.dart +++ b/src/spice2x/api/resources/dart/spiceapi/src/wrappers/card.dart @@ -1,5 +1,32 @@ part of spiceapi; +class CardInfo { + final int index; + final String cardID; + final String source; + final String? fileName; + + CardInfo(this.index, this.cardID, this.source, this.fileName); +} + +Future> cardGetCards(Connection con) { + var req = Request("card", "get_cards"); + return con.request(req).then((res) { + List cards = []; + for (var value in res.getData()) { + cards.add( + CardInfo( + value["index"], + value["card_id"], + value["source"], + value["file_name"], + ), + ); + } + return cards; + }); +} + Future cardInsert(Connection con, int unit, String cardID) { var req = Request("card", "insert"); req.addParam(unit); diff --git a/src/spice2x/api/resources/python/spiceapi/card.py b/src/spice2x/api/resources/python/spiceapi/card.py index d579ed8..3eff0d2 100644 --- a/src/spice2x/api/resources/python/spiceapi/card.py +++ b/src/spice2x/api/resources/python/spiceapi/card.py @@ -2,6 +2,10 @@ from .connection import Connection from .request import Request +def card_get_cards(con: Connection): + return con.request(Request("card", "get_cards")).get_data() + + def card_insert(con: Connection, unit: int, card_id: str): req = Request("card", "insert") req.add_param(unit) diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp index 7e054f5..8968a00 100644 --- a/src/spice2x/misc/eamuse.cpp +++ b/src/spice2x/misc/eamuse.cpp @@ -34,6 +34,7 @@ static uint16_t KEYPAD_STATE_OVERRIDES_READER[] = {0, 0}; static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0}; static std::string EAMUSE_GAME_NAME; static ConfigKeypadBindings KEYPAD_BINDINGS {}; +static std::mutex KEYPAD_BINDINGS_LOCK; // auto card bool AUTO_INSERT_CARD[2] = {false, false}; @@ -87,23 +88,23 @@ bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) { return true; } - // get file path - std::filesystem::path path; - if (!KEYPAD_BINDINGS.card_paths[index].empty()) { - path = KEYPAD_BINDINGS.card_paths[index]; - } else { - path = index > 0 ? "card1.txt" : "card0.txt"; - } - // call the next function - return eamuse_get_card(path, card, index); + return eamuse_get_card(eamuse_get_card_path(index), card, index); +} + +std::filesystem::path eamuse_get_card_path(size_t index) { + std::lock_guard lock(KEYPAD_BINDINGS_LOCK); + if (index >= std::size(KEYPAD_BINDINGS.card_paths)) { + return {}; + } + if (!KEYPAD_BINDINGS.card_paths[index].empty()) { + return KEYPAD_BINDINGS.card_paths[index]; + } + return index > 0 ? "card1.txt" : "card0.txt"; } bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) { - // do a quick copy under lock - std::unique_lock lock(CARD_OVERRIDES_LOCK); - const auto card_override = CARD_OVERRIDES[index]; - lock.unlock(); + const auto card_override = eamuse_get_card_override(index); // Check if card overrides are present if (!card_override.empty()) { @@ -149,6 +150,14 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index return eamuse_get_card_from_file(path, card, index); } +std::string eamuse_get_card_override(size_t index) { + std::lock_guard lock(CARD_OVERRIDES_LOCK); + if (index >= std::size(CARD_OVERRIDES)) { + return {}; + } + return CARD_OVERRIDES[index]; +} + bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) { // open file @@ -584,6 +593,7 @@ std::string eamuse_get_keypad_state_str(size_t unit) { } bool eamuse_keypad_state_naive() { + std::lock_guard lock(KEYPAD_BINDINGS_LOCK); return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty(); } @@ -595,7 +605,9 @@ void eamuse_set_game(std::string game) { } void eamuse_update_keypad_bindings() { - KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME); + auto bindings = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME); + std::lock_guard lock(KEYPAD_BINDINGS_LOCK); + KEYPAD_BINDINGS = std::move(bindings); } const std::string &eamuse_get_game() { diff --git a/src/spice2x/misc/eamuse.h b/src/spice2x/misc/eamuse.h index d772bb5..366ba10 100644 --- a/src/spice2x/misc/eamuse.h +++ b/src/spice2x/misc/eamuse.h @@ -43,6 +43,8 @@ extern std::string AUTO_PIN_MACRO_TRIGGER[2]; bool eamuse_get_card(int active_count, int unit_id, uint8_t *card); bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id); bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index); +std::filesystem::path eamuse_get_card_path(size_t index); +std::string eamuse_get_card_override(size_t index); void eamuse_card_insert(int unit); void eamuse_card_insert(int unit, const uint8_t *card); From 50b38b856fc8e56fba068658507617d97a1fe1d4 Mon Sep 17 00:00:00 2001 From: James Liu <17558260+JamesLewisLiu@users.noreply.github.com> Date: Fri, 28 Aug 2026 00:05:36 +0800 Subject: [PATCH 12/17] gitadora: fix 32bit gfdm coin button not working (#893) ## Link to GitHub Issue or related Pull Request, if one exists #0 ## Description of change Fix Add Coin/COIN button not actuated after pressing ## Testing *XG2* XG2 *XG3* XG3 --- src/spice2x/misc/device.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/spice2x/misc/device.cpp b/src/spice2x/misc/device.cpp index 54ae9b7..fecf390 100644 --- a/src/spice2x/misc/device.cpp +++ b/src/spice2x/misc/device.cpp @@ -285,8 +285,10 @@ static int __cdecl device_read_secplug(int a1, int a2, int a3) { return 1; } -static int __cdecl device_set_coinblocker_open(char number, char open) { - return 0; +static void __cdecl device_set_coinblocker_open(char slot1_open, char slot2_open) { + // libdevice exposes one open flag per coin slot. Since spice has a shared + // software coin stock, consider it blocked only when both slots are closed. + eamuse_coin_set_block(!slot1_open && !slot2_open); } static void __cdecl device_set_coincounter_merge() { @@ -617,7 +619,9 @@ void spicedevice_attach() { detour::inline_hook((void *) device_finalize, libutils::try_proc_list( DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"})); detour::inline_hook((void *) device_get_coinstock, libutils::try_proc_list( - DEVICE_INSTANCE, {"device_get_coinstock", "?device_get_coinstock@@YAXPEAG0@Z"})); + DEVICE_INSTANCE, {"device_get_coinstock", + "?device_get_coinstock@@YAXPEAG0@Z", + "?device_get_coinstock@@YAXPAG0@Z"})); detour::inline_hook((void *) device_get_coinstock_all, libutils::try_proc_list( DEVICE_INSTANCE, {"device_get_coinstock_all", "?device_get_coinstock_all@@YAXPEAG0@Z", From 8d3685415d1669ed2c1bf0a63caed22506c2b4e3 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 28 Aug 2026 01:42:56 -0700 Subject: [PATCH 13/17] popn: fix LED handling (#894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Link to GitHub Issue or related Pull Request, if one exists #0 ## Description of change Fix the issue with popn lights flickering. `SetTapeLedDataPart()` updates BI3A’s internal LED buffer without immediately sending it to hardware. High Cheers briefly clears this buffer before writing the complete set of lights. Stage pin-0 (button light) updates in the hook and publish only the latest frame during `GetDeviceStatus()`, matching real BI3A behavior. This prevents temporary clear frames from reaching HID controllers. ## Testing Tested with my iidx controller with HID lights. --- src/spice2x/games/popn/bi3a_hook.cpp | 165 ++++++++++++++++++--------- 1 file changed, 110 insertions(+), 55 deletions(-) diff --git a/src/spice2x/games/popn/bi3a_hook.cpp b/src/spice2x/games/popn/bi3a_hook.cpp index cfc3409..fb763dd 100644 --- a/src/spice2x/games/popn/bi3a_hook.cpp +++ b/src/spice2x/games/popn/bi3a_hook.cpp @@ -2,6 +2,8 @@ #if SPICE64 && !SPICE_XP +#include +#include #include #include #include "util/detour.h" @@ -141,10 +143,100 @@ namespace games::popn { static AIO_SCI_COMM *aioSciComm; static AIO_IOB5_BI3A *aioIob5Bi3a; + static constexpr size_t PIKA_BUTTON_COUNT = 9; + static constexpr size_t PIKA_BUTTON_DATA_SIZE = PIKA_BUTTON_COUNT * 3; + static std::array pika_button_staging{}; + static bool pika_button_staging_dirty = false; + static std::mutex pika_button_staging_mutex; + /* * implementations */ + static void publish_pika_button_staging() { + std::array data; + { + std::lock_guard lock(pika_button_staging_mutex); + if (!pika_button_staging_dirty) { + return; + } + data = pika_button_staging; + pika_button_staging_dirty = false; + } + + constexpr Lights::popn_lights_t legacy_button_lights[] = { + Lights::popn_lights_t::Button1, + Lights::popn_lights_t::Button2, + Lights::popn_lights_t::Button3, + Lights::popn_lights_t::Button4, + Lights::popn_lights_t::Button5, + Lights::popn_lights_t::Button6, + Lights::popn_lights_t::Button7, + Lights::popn_lights_t::Button8, + Lights::popn_lights_t::Button9 + }; + constexpr Lights::popn_lights_t button_lights[] = { + Lights::popn_lights_t::PikaButton1_R, + Lights::popn_lights_t::PikaButton1_G, + Lights::popn_lights_t::PikaButton1_B, + Lights::popn_lights_t::PikaButton2_R, + Lights::popn_lights_t::PikaButton2_G, + Lights::popn_lights_t::PikaButton2_B, + Lights::popn_lights_t::PikaButton3_R, + Lights::popn_lights_t::PikaButton3_G, + Lights::popn_lights_t::PikaButton3_B, + Lights::popn_lights_t::PikaButton4_R, + Lights::popn_lights_t::PikaButton4_G, + Lights::popn_lights_t::PikaButton4_B, + Lights::popn_lights_t::PikaButton5_R, + Lights::popn_lights_t::PikaButton5_G, + Lights::popn_lights_t::PikaButton5_B, + Lights::popn_lights_t::PikaButton6_R, + Lights::popn_lights_t::PikaButton6_G, + Lights::popn_lights_t::PikaButton6_B, + Lights::popn_lights_t::PikaButton7_R, + Lights::popn_lights_t::PikaButton7_G, + Lights::popn_lights_t::PikaButton7_B, + Lights::popn_lights_t::PikaButton8_R, + Lights::popn_lights_t::PikaButton8_G, + Lights::popn_lights_t::PikaButton8_B, + Lights::popn_lights_t::PikaButton9_R, + Lights::popn_lights_t::PikaButton9_G, + Lights::popn_lights_t::PikaButton9_B + }; + + static_assert(std::size(legacy_button_lights) == PIKA_BUTTON_COUNT); + static_assert(std::size(button_lights) == PIKA_BUTTON_DATA_SIZE); + + auto &lights = get_lights(); + for (size_t button = 0; button < PIKA_BUTTON_COUNT; button++) { + const auto data_offset = button * 3; + // special handling converting RGB data to non-RGB lights + // take the max(R, G, B) and use it to write the light value + const auto light_value = std::max({ + data[data_offset], + data[data_offset + 1], + data[data_offset + 2] + }); + + // on the new cab, buttons are colorless plastic and rely on RGB to be lit + // at all times, even when "off". When translating to legacy on/off lights, + // treat only values above about 60% brightness as fully on to preserve the + // dim "off" state. + GameAPI::Lights::writeLight( + RI_MGR, + lights.at(legacy_button_lights[button]), + light_value > 150 ? 1.f : 0.f); + } + + for (size_t light = 0; light < data.size(); light++) { + GameAPI::Lights::writeLight( + RI_MGR, + lights.at(button_lights[light]), + data[light] / 255.f); + } + } + // libaio.dll static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) { @@ -260,6 +352,11 @@ namespace games::popn { log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p)); if (i_pNodeMgr == aioNmgrIob5) { + { + std::lock_guard lock(pika_button_staging_mutex); + pika_button_staging.fill(0); + pika_button_staging_dirty = false; + } aioIob5Bi3a = new AIO_IOB5_BI3A; log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a)); return aioIob5Bi3a; @@ -269,11 +366,15 @@ namespace games::popn { } static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) { - RI_MGR->devices_flush_output(); if (i_pNodeCtl != aioIob5Bi3a) { return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus); } + // snapshot the current state of lights and flush the output + // this closely mirrors what the game does with real I/O + publish_pika_button_staging(); + RI_MGR->devices_flush_output(); + memset(o_DevStatus, 0, sizeof(*o_DevStatus)); auto &buttons = get_buttons(); @@ -413,61 +514,15 @@ namespace games::popn { i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data); } - auto &lights = get_lights(); if (i_CnPin == 0 && number_of_leds == 9 * 3) { - // special handling converting to non-RGB lights - // take the max(R, G, B) and use it to write the light value - uint8_t light_value[9] = { 0 }; - for (uint32_t i = 0; i < number_of_leds; i += 3) { - light_value[i / 3] = - std::max({ ((uint8_t *)i_pData)[i], ((uint8_t *)i_pData)[i + 1], ((uint8_t *)i_pData)[i + 2] }); - } - - constexpr Lights::popn_lights_t legacy_button_lights[] = { - Lights::popn_lights_t::Button1, - Lights::popn_lights_t::Button2, - Lights::popn_lights_t::Button3, - Lights::popn_lights_t::Button4, - Lights::popn_lights_t::Button5, - Lights::popn_lights_t::Button6, - Lights::popn_lights_t::Button7, - Lights::popn_lights_t::Button8, - Lights::popn_lights_t::Button9 - }; - - static_assert(std::size(legacy_button_lights) == 9); - - for (size_t light = 0; light < 9; light++) { - // on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off" - // when translating to legacy on/off lights, treat anything above ~60% brightness as fully on, - // otherwise off, to avoid dimly lit "off" state - GameAPI::Lights::writeLight( - RI_MGR, - lights.at(legacy_button_lights[light]), - light_value[light] > 150 ? 1.f : 0.f); - } - - // color buttons - constexpr Lights::popn_lights_t button_lights[] = { - Lights::popn_lights_t::PikaButton1_R, Lights::popn_lights_t::PikaButton1_G, Lights::popn_lights_t::PikaButton1_B, - Lights::popn_lights_t::PikaButton2_R, Lights::popn_lights_t::PikaButton2_G, Lights::popn_lights_t::PikaButton2_B, - Lights::popn_lights_t::PikaButton3_R, Lights::popn_lights_t::PikaButton3_G, Lights::popn_lights_t::PikaButton3_B, - Lights::popn_lights_t::PikaButton4_R, Lights::popn_lights_t::PikaButton4_G, Lights::popn_lights_t::PikaButton4_B, - Lights::popn_lights_t::PikaButton5_R, Lights::popn_lights_t::PikaButton5_G, Lights::popn_lights_t::PikaButton5_B, - Lights::popn_lights_t::PikaButton6_R, Lights::popn_lights_t::PikaButton6_G, Lights::popn_lights_t::PikaButton6_B, - Lights::popn_lights_t::PikaButton7_R, Lights::popn_lights_t::PikaButton7_G, Lights::popn_lights_t::PikaButton7_B, - Lights::popn_lights_t::PikaButton8_R, Lights::popn_lights_t::PikaButton8_G, Lights::popn_lights_t::PikaButton8_B, - Lights::popn_lights_t::PikaButton9_R, Lights::popn_lights_t::PikaButton9_G, Lights::popn_lights_t::PikaButton9_B - }; - - static_assert(std::size(button_lights) == 9 * 3); - - for (size_t light = 0; light < number_of_leds; light++) { - GameAPI::Lights::writeLight( - RI_MGR, - lights.at(button_lights[light]), - ((uint8_t *)i_pData)[light] / 255.f); - } + // SetTapeLedDataPart is meant to be a temporary staging buffer, so the game occasionally + // clears the buffer (set to 0 / lights off) between animations but they are not meant + // to be seen by the I/O + // therefore, instead of writing the lights out directly here, we stage them in a buffer + // and wait for GetDeviceStatus to be called + std::lock_guard lock(pika_button_staging_mutex); + memcpy(pika_button_staging.data(), i_pData, pika_button_staging.size()); + pika_button_staging_dirty = true; } if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) { From 8fb6d940008d0401ca457a9bf8e92306984f1ebe Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:47:15 -0700 Subject: [PATCH 14/17] Include AI-Use Policy in README Added AI-Use Policy section to README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 640d215..7e764ab 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,10 @@ We explicitly do **NOT** have a Discord server for dicussions - we try to do eve Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code. +### AI-Use Policy + +Use AI tools at your discretion, but remember that a human will review every issue and pull request. Please ensure submissions are accurate, relevant, and worth the reviewer’s time. + ## Additional information Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`. From 4009bb8a840fb5ceabeef4ba9610a1eeb2804378 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 28 Aug 2026 03:04:43 -0700 Subject: [PATCH 15/17] sc: bone eater preliminary support (#895) ## Link to GitHub Issue or related Pull Request, if one exists Fixes #114 ## Description of change Initial support for Silent Scope: Bone Eater, enough to be playable; doesn't include QoS work needed to make this a smoother experience. * Game detection * Fix DX11 overlay to work with non-Unity games * Implements sub monitor i/o (only really needed to get past I/O check) * Try to make the game reasonably playable with a mouse * Ignore the game's attempt to move windows, position them in a reasonable place * Fix language hook ## Testing --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/acio/bmpu/bmpu.cpp | 38 ++++- src/spice2x/games/silentscope/projector.cpp | 150 ++++++++++++++++++ src/spice2x/games/silentscope/projector.h | 30 ++++ src/spice2x/games/silentscope/silentscope.cpp | 6 +- .../graphics/backends/d3d11/d3d11_backend.cpp | 10 +- src/spice2x/hooks/graphics/graphics.cpp | 47 ++++++ src/spice2x/hooks/graphics/graphics.h | 1 + src/spice2x/hooks/lang.cpp | 9 +- src/spice2x/misc/eamuse.cpp | 2 + 10 files changed, 278 insertions(+), 16 deletions(-) create mode 100644 src/spice2x/games/silentscope/projector.cpp create mode 100644 src/spice2x/games/silentscope/projector.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 0faaa2b..3ee6c02 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -531,6 +531,7 @@ set(SOURCE_FILES ${SOURCE_FILES} games/otoca/p4io.cpp games/silentscope/silentscope.cpp games/silentscope/io.cpp + games/silentscope/projector.cpp games/pcm/pcm.cpp games/pcm/io.cpp games/onpara/onpara.cpp diff --git a/src/spice2x/acio/bmpu/bmpu.cpp b/src/spice2x/acio/bmpu/bmpu.cpp index e8ccb5f..6e183cf 100644 --- a/src/spice2x/acio/bmpu/bmpu.cpp +++ b/src/spice2x/acio/bmpu/bmpu.cpp @@ -1,5 +1,7 @@ #include "bmpu.h" +#include + #include "acio/icca/icca.h" #include "avs/game.h" #include "cfg/api.h" @@ -9,6 +11,7 @@ #include "games/ftt/io.h" #include "games/museca/io.h" #include "games/silentscope/io.h" +#include "hooks/graphics/graphics.h" #include "launcher/launcher.h" #include "misc/eamuse.h" @@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() { STATUS_BUFFER[4] |= 0x20; } - // joy stick raw input + // gun position, sent big endian; the game keeps the top 12 bits of each pair auto &analogs = games::silentscope::get_analogs(); + auto &gun_x = analogs.at(games::silentscope::Analogs::GUN_X); + auto &gun_y = analogs.at(games::silentscope::Analogs::GUN_Y); unsigned short joy_x = 0x7FFF; unsigned short joy_y = 0x7FFF; - if (analogs.at(games::silentscope::Analogs::GUN_X).isSet()) { - joy_x = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_X)) * USHRT_MAX); - } - if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) { - joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * USHRT_MAX); + + if (gun_x.isSet() || gun_y.isSet()) { + if (gun_x.isSet()) { + joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX); + } + if (gun_y.isSet()) { + joy_y = (unsigned short) (Analogs::getState(RI_MGR, gun_y) * USHRT_MAX); + } + } else { + + // the gun reports where it is aimed, which a relative pointer cannot express - the + // calibration screen asks for the screen centre and two corners, so read the cursor + POINT cursor {}; + RECT client {}; + POINT origin {}; + if (NDD_MAIN_WINDOW != nullptr && + GetCursorPos(&cursor) && + GetClientRect(NDD_MAIN_WINDOW, &client) && + ClientToScreen(NDD_MAIN_WINDOW, &origin) && + client.right > 1 && client.bottom > 1) + { + const LONG x = std::clamp(cursor.x - origin.x, 0L, client.right - 1); + const LONG y = std::clamp(cursor.y - origin.y, 0L, client.bottom - 1); + joy_x = (unsigned short) (x * USHRT_MAX / (client.right - 1)); + joy_y = (unsigned short) (y * USHRT_MAX / (client.bottom - 1)); + } } // invert X axis diff --git a/src/spice2x/games/silentscope/projector.cpp b/src/spice2x/games/silentscope/projector.cpp new file mode 100644 index 0000000..8a4e636 --- /dev/null +++ b/src/spice2x/games/silentscope/projector.cpp @@ -0,0 +1,150 @@ +#include "projector.h" + +#include +#include +#include + +#include "util/logging.h" + +namespace { + + // request/response layout: identifier, command, two zero bytes, payload length, + // the payload itself and a checksum over everything before it + constexpr size_t HEADER_SIZE = 5; + constexpr uint8_t RESPONSE_ID = 0x23; + + constexpr uint8_t COMMAND_COMMON_DATA = 0x8A; + constexpr uint8_t COMMAND_TEMPERATURE = 0x99; + constexpr uint8_t COMMAND_LAMP_CURRENT = 0x9B; + + // shown in the test menu as PROJTIM, PROJHEATIN and PROJHEAT + constexpr uint32_t LAMP_SECONDS = 0; + constexpr uint32_t TEMPERATURE_INTAKE = 25; + constexpr uint32_t TEMPERATURE_EXHAUST = 35; + + // anything below 1000 is treated as a lamp anomaly and raises PROJERROR unless the exact + // value was already recorded in /projecter/current on an earlier boot + constexpr uint32_t LAMP_CURRENT = 1000; + + void put32(uint8_t *dest, uint32_t value) { + dest[0] = (uint8_t) (value & 0xFF); + dest[1] = (uint8_t) ((value >> 8) & 0xFF); + dest[2] = (uint8_t) ((value >> 16) & 0xFF); + dest[3] = (uint8_t) ((value >> 24) & 0xFF); + } +} + +bool games::silentscope::ProjectorHandle::open(LPCWSTR lpFileName) { + if (wcscmp(lpFileName, L"COM2") != 0 && wcscmp(lpFileName, L"\\\\.\\COM2") != 0) { + return false; + } + + log_info("silentscope", "Opened COM2 (projector)"); + + return true; +} + +int games::silentscope::ProjectorHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) { + std::lock_guard lock(this->mutex); + + auto buffer = reinterpret_cast(lpBuffer); + + DWORD bytes_read = 0; + while (bytes_read < nNumberOfBytesToRead && !this->response.empty()) { + buffer[bytes_read++] = this->response.front(); + this->response.pop_front(); + } + + return (int) bytes_read; +} + +int games::silentscope::ProjectorHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) { + std::lock_guard lock(this->mutex); + + auto buffer = reinterpret_cast(lpBuffer); + this->request.insert(this->request.end(), buffer, buffer + nNumberOfBytesToWrite); + + while (this->request.size() >= HEADER_SIZE) { + const size_t packet_size = HEADER_SIZE + this->request[4] + 1; + if (this->request.size() < packet_size) { + break; + } + + this->process_request(this->request.data()); + this->request.erase(this->request.begin(), this->request.begin() + packet_size); + } + + return (int) nNumberOfBytesToWrite; +} + +size_t games::silentscope::ProjectorHandle::bytes_available() { + std::lock_guard lock(this->mutex); + + return this->response.size(); +} + +bool games::silentscope::ProjectorHandle::close() { + std::lock_guard lock(this->mutex); + + this->request.clear(); + this->response.clear(); + + log_info("silentscope", "Closed COM2 (projector)"); + + return true; +} + +void games::silentscope::ProjectorHandle::process_request(const uint8_t *packet) { + const uint8_t command = packet[1]; + const uint8_t length = packet[4]; + const uint8_t *data = packet + HEADER_SIZE; + + switch (command) { + case COMMAND_COMMON_DATA: { + + // the game only looks at the lamp usage time near the end of the block + std::vector payload(98, 0); + put32(&payload[94], LAMP_SECONDS); + this->reply(command, payload); + break; + } + case COMMAND_TEMPERATURE: { + + // the requested sensor is echoed back along with its reading + const uint8_t sensor = length > 0 ? data[0] : 0; + std::vector payload(5, 0); + payload[0] = sensor; + put32(&payload[1], sensor == 0 ? TEMPERATURE_INTAKE : TEMPERATURE_EXHAUST); + this->reply(command, payload); + break; + } + case COMMAND_LAMP_CURRENT: { + + // the three byte item selector is echoed back along with the measurement + std::vector payload(7, 0); + memcpy(payload.data(), data, std::min(length, 3)); + put32(&payload[3], LAMP_CURRENT); + this->reply(command, payload); + break; + } + default: + log_misc("silentscope", "unknown projector command {:#04x}", command); + this->reply(command, {}); + break; + } +} + +void games::silentscope::ProjectorHandle::reply(uint8_t command, const std::vector &data) { + std::vector packet { + RESPONSE_ID, command, 0x00, 0x00, (uint8_t) data.size() + }; + packet.insert(packet.end(), data.begin(), data.end()); + + uint8_t checksum = 0; + for (auto byte : packet) { + checksum += byte; + } + packet.push_back(checksum); + + this->response.insert(this->response.end(), packet.begin(), packet.end()); +} diff --git a/src/spice2x/games/silentscope/projector.h b/src/spice2x/games/silentscope/projector.h new file mode 100644 index 0000000..993ad06 --- /dev/null +++ b/src/spice2x/games/silentscope/projector.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include + +#include "hooks/devicehook.h" + +namespace games::silentscope { + + // The cabinet talks to its projector over COM2. Without an answer the game stops at + // I/O error 5-1560-0004 (IOCOM2, "the projector is not connected correctly"). + class ProjectorHandle : public CustomHandle { + public: + bool open(LPCWSTR lpFileName) override; + int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override; + int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override; + size_t bytes_available() override; + bool close() override; + + private: + std::mutex mutex; + std::vector request; + std::deque response; + + void process_request(const uint8_t *packet); + void reply(uint8_t command, const std::vector &data); + }; +} diff --git a/src/spice2x/games/silentscope/silentscope.cpp b/src/spice2x/games/silentscope/silentscope.cpp index 60a4c50..4586d8c 100644 --- a/src/spice2x/games/silentscope/silentscope.cpp +++ b/src/spice2x/games/silentscope/silentscope.cpp @@ -1,10 +1,11 @@ #include "silentscope.h" -#include "acioemu/handle.h" #include "cfg/configurator.h" #include "hooks/devicehook.h" #include "util/libutils.h" +#include "projector.h" + namespace games::silentscope { SilentScopeGame::SilentScopeGame() : Game("Silent Scope") { @@ -16,9 +17,8 @@ namespace games::silentscope { // load the game DLL so hooks apply libutils::try_library("gamendd.dll"); - // TODO(felix): implement stuff on this port devicehook_init(); - devicehook_add(new acioemu::ACIOHandle(L"COM2")); + devicehook_add(new ProjectorHandle()); } void SilentScopeGame::detach() { diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp index f5d0c8f..b698fd4 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp @@ -30,6 +30,7 @@ void graphics_d3d11_shutdown() {} #include #include +#include "avs/game.h" #include "d3d11_internal.h" #include "util/nt_loader.h" @@ -237,10 +238,11 @@ bool d3dcompiler_available() { } // namespace void graphics_d3d11_init() { - // dx11 titles always run under execexe. skipping on pure-dx9 games keeps - // their startup path completely untouched (no exports patched, no poll - // thread, no LDR callback). - if (!GetModuleHandleW(L"execexe.dll")) { + // dx11 titles run under execexe, except Silent Scope: Bone Eater, whose Aska + // engine LoadLibrary's d3d11 itself. skipping on pure-dx9 games keeps their + // startup path completely untouched (no exports patched, no poll thread, no + // LDR callback). + if (!GetModuleHandleW(L"execexe.dll") && !avs::game::is_model("NDD")) { return; } diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 61c5701..7bb967b 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -52,6 +52,8 @@ static HWND GFDM_RIGHT_WINDOW = nullptr; static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr; static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr; HWND POPN_SUBSCREEN_WINDOW = nullptr; +static HWND NDD_SUBSCREEN_WINDOW = nullptr; +HWND NDD_MAIN_WINDOW = nullptr; bool FAKE_SUBSCREEN_ADAPTER = false; // icon @@ -634,6 +636,9 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen"); bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen"); bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen"); + const bool is_ndd = avs::game::is_model("NDD"); + bool is_ndd_sub_window = is_ndd && window_name.starts_with("Aska MultiDisplay"); + bool is_ndd_main_window = is_ndd && window_name == "ASKA"; const std::string gfdm_window_name = games::gitadora::is_arena_model() ? gitadora_canonical_window_name(effective_window_name) : ""; @@ -777,6 +782,14 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC } } + if (is_ndd_sub_window) { + NDD_SUBSCREEN_WINDOW = result; + } + + if (is_ndd_main_window) { + NDD_MAIN_WINDOW = result; + } + disable_touch_gestures(result); log_misc( "graphics", @@ -902,6 +915,26 @@ static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum, return value; } +// the sub screen renders into a fixed 800x480 buffer, but the game's saved layout asks for rects +// that do not match it, and dxgi stretches the buffer to fill whatever the client area ends up as +static void ndd_subscreen_size(HWND hWnd, int &width, int &height) { + RECT rect {}; + SetRect(&rect, 0, 0, 800, 480); + AdjustWindowRect(&rect, GetWindowLongA(hWnd, GWL_STYLE), 0); + + width = rect.right - rect.left; + height = rect.bottom - rect.top; +} + +// the saved layout drops the sub window wherever it sat on the machine that wrote the file +static void ndd_subscreen_position(int &x, int &y) { + RECT main {}; + if (NDD_MAIN_WINDOW != nullptr && GetWindowRect(NDD_MAIN_WINDOW, &main)) { + x = main.right; + y = main.top; + } +} + static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) { log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})", fmt::ptr(hWnd), @@ -931,6 +964,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei nHeight = rect.bottom - rect.top; } + if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) { + ndd_subscreen_size(hWnd, nWidth, nHeight); + ndd_subscreen_position(X, Y); + } + // iidx windowed TDJ mode if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) { if (GRAPHICS_IIDX_WSUB) { @@ -1073,6 +1111,15 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) { static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags) { + if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) { + if (!(uFlags & SWP_NOSIZE)) { + ndd_subscreen_size(hWnd, cx, cy); + } + if (!(uFlags & SWP_NOMOVE)) { + ndd_subscreen_position(X, Y); + } + } + if (is_gfdm_two_head_small_window(hWnd) && ((uFlags & SWP_HIDEWINDOW) || (uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) { diff --git a/src/spice2x/hooks/graphics/graphics.h b/src/spice2x/hooks/graphics/graphics.h index 72e7d13..fe83955 100644 --- a/src/spice2x/hooks/graphics/graphics.h +++ b/src/spice2x/hooks/graphics/graphics.h @@ -103,6 +103,7 @@ extern HWND TDJ_SUBSCREEN_WINDOW; extern HWND SDVX_SUBSCREEN_WINDOW; extern HWND POPN_SUBSCREEN_WINDOW; extern HWND GFDM_SUBSCREEN_WINDOW; +extern HWND NDD_MAIN_WINDOW; extern bool SUBSCREEN_FORCE_REDRAW; extern bool FAKE_SUBSCREEN_ADAPTER; diff --git a/src/spice2x/hooks/lang.cpp b/src/spice2x/hooks/lang.cpp index 0567dc2..8354712 100644 --- a/src/spice2x/hooks/lang.cpp +++ b/src/spice2x/hooks/lang.cpp @@ -336,9 +336,11 @@ void hooks::lang::early_init() { &GetLocaleInfoA_orig); } - // for TDJ subscreen search keyboard and T44 narrow-string handling + // for TDJ subscreen search keyboard + // T44 narrow-string handling + // NDD text measuring if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) || - avs::game::is_model("T44")) { + avs::game::is_model({ "T44", "NDD" })) { log_info("hooks::lang", "hooking IsDBCSLeadByte"); detour::trampoline_try( "kernel32.dll", @@ -358,8 +360,9 @@ void hooks::lang::early_init() { #endif #ifdef SPICE64 + // NDD renders through GetTextExtentPoint32A, so its wide strings go back through CP_ACP first const auto hook_wide_char_to_multi_byte = - games::gitadora::is_arena_model() || avs::game::is_model("T44"); + games::gitadora::is_arena_model() || avs::game::is_model({ "T44", "NDD" }); #else // XG2 converts UTF-8 property strings through CP_ACP before rendering. const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" }); diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp index 8968a00..bacf85a 100644 --- a/src/spice2x/misc/eamuse.cpp +++ b/src/spice2x/misc/eamuse.cpp @@ -730,6 +730,8 @@ void eamuse_autodetect_game() { eamuse_set_game("Mahjong Fight Girl"); else if (avs::game::is_model("XIF")) eamuse_set_game("Polaris Chord"); + else if (avs::game::is_model("NDD")) + eamuse_set_game("Silent Scope: Bone Eater"); else { log_warning("eamuse", "unknown game model: {}", avs::game::MODEL); eamuse_set_game("unknown"); From 67963389eff644d76585c1b15a058ca2271c0e45 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 29 Aug 2026 17:21:21 -0700 Subject: [PATCH 16/17] drs: hook down movement, adjust touch hooks (#898) When Down Motion button is bound, install hooks into the game that detects down movement via patches. When Disable Touch option is enabled, don't install touch hooks. --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/games/drs/drs.cpp | 78 +++++++++--------- src/spice2x/games/drs/io.cpp | 7 +- src/spice2x/games/drs/io.h | 3 +- src/spice2x/games/drs/motion_cam.cpp | 109 ++++++++++++++++++++++++++ src/spice2x/games/drs/motion_cam.h | 5 ++ src/spice2x/sdk/include/spicesdk_io.h | 1 + 7 files changed, 163 insertions(+), 41 deletions(-) create mode 100644 src/spice2x/games/drs/motion_cam.cpp create mode 100644 src/spice2x/games/drs/motion_cam.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 3ee6c02..9a9f09d 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -520,6 +520,7 @@ set(SOURCE_FILES ${SOURCE_FILES} games/scotto/io.cpp games/drs/drs.cpp games/drs/io.cpp + games/drs/motion_cam.cpp games/drs/rgb_cam.cpp games/we/we.cpp games/we/io.cpp diff --git a/src/spice2x/games/drs/drs.cpp b/src/spice2x/games/drs/drs.cpp index 7d2bf6a..ceac441 100644 --- a/src/spice2x/games/drs/drs.cpp +++ b/src/spice2x/games/drs/drs.cpp @@ -9,6 +9,8 @@ #include "util/logging.h" #include "util/precise_timer.h" #include "util/memutils.h" +#include "io.h" +#include "motion_cam.h" #include "rgb_cam.h" #pragma pack(push) @@ -276,48 +278,50 @@ namespace games::drs { void DRSGame::attach() { Game::attach(); - // TouchSDK hooks - detour::iat("??0TouchSDK@@QEAA@XZ", - (void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE); - detour::iat("?SendData@TouchSDK@@QEAA_NU_DeviceInfo@@QEAEH1HH@Z", - (void *) &TouchSDK_SendData, avs::game::DLL_INSTANCE); - detour::iat("?SetSignalInit@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", - (void *) &TouchSDK_SetSignalInit, avs::game::DLL_INSTANCE); - detour::iat("??1TouchSDK@@QEAA@XZ", - (void *) &TouchSDK_Destructor, avs::game::DLL_INSTANCE); - detour::iat("?GetYLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z", - (void *) &TouchSDK_GetYLedTotal, avs::game::DLL_INSTANCE); - detour::iat("?GetXLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z", - (void *) &TouchSDK_GetXLedTotal, avs::game::DLL_INSTANCE); - detour::iat("?DisableTouch@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", - (void *) &TouchSDK_DisableTouch, avs::game::DLL_INSTANCE); - detour::iat("?DisableDrag@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", - (void *) &TouchSDK_DisableDrag, avs::game::DLL_INSTANCE); - detour::iat("?DisableWheel@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", - (void *) &TouchSDK_DisableWheel, avs::game::DLL_INSTANCE); - detour::iat("?DisableRightClick@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", - (void *) &TouchSDK_DisableRightClick, avs::game::DLL_INSTANCE); - detour::iat("?SetMultiTouchMode@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", - (void *) &TouchSDK_SetMultiTouchMode, avs::game::DLL_INSTANCE); - detour::iat("?EnableTouchWidthData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", - (void *) &TouchSDK_EnableTouchWidthData, avs::game::DLL_INSTANCE); - detour::iat("?EnableRawData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", - (void *) &TouchSDK_EnableRawData, avs::game::DLL_INSTANCE); - detour::iat("?SetAllEnable@TouchSDK@@QEAA_NU_DeviceInfo@@_NH@Z", - (void *) &TouchSDK_SetAllEnable, avs::game::DLL_INSTANCE); - detour::iat("?GetTouchDeviceCount@TouchSDK@@QEAAHXZ", - (void *) &TouchSDK_GetTouchDeviceCount, avs::game::DLL_INSTANCE); - detour::iat("?GetTouchSDKVersion@TouchSDK@@QEAAIXZ", - (void *) &TouchSDK_GetTouchSDKVersion, avs::game::DLL_INSTANCE); - detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z", - (void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE); - if (!DISABLE_TOUCH) { + // TouchSDK hooks + detour::iat("??0TouchSDK@@QEAA@XZ", + (void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE); + detour::iat("?SendData@TouchSDK@@QEAA_NU_DeviceInfo@@QEAEH1HH@Z", + (void *) &TouchSDK_SendData, avs::game::DLL_INSTANCE); + detour::iat("?SetSignalInit@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", + (void *) &TouchSDK_SetSignalInit, avs::game::DLL_INSTANCE); + detour::iat("??1TouchSDK@@QEAA@XZ", + (void *) &TouchSDK_Destructor, avs::game::DLL_INSTANCE); + detour::iat("?GetYLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z", + (void *) &TouchSDK_GetYLedTotal, avs::game::DLL_INSTANCE); + detour::iat("?GetXLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z", + (void *) &TouchSDK_GetXLedTotal, avs::game::DLL_INSTANCE); + detour::iat("?DisableTouch@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", + (void *) &TouchSDK_DisableTouch, avs::game::DLL_INSTANCE); + detour::iat("?DisableDrag@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", + (void *) &TouchSDK_DisableDrag, avs::game::DLL_INSTANCE); + detour::iat("?DisableWheel@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", + (void *) &TouchSDK_DisableWheel, avs::game::DLL_INSTANCE); + detour::iat("?DisableRightClick@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", + (void *) &TouchSDK_DisableRightClick, avs::game::DLL_INSTANCE); + detour::iat("?SetMultiTouchMode@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", + (void *) &TouchSDK_SetMultiTouchMode, avs::game::DLL_INSTANCE); + detour::iat("?EnableTouchWidthData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", + (void *) &TouchSDK_EnableTouchWidthData, avs::game::DLL_INSTANCE); + detour::iat("?EnableRawData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z", + (void *) &TouchSDK_EnableRawData, avs::game::DLL_INSTANCE); + detour::iat("?SetAllEnable@TouchSDK@@QEAA_NU_DeviceInfo@@_NH@Z", + (void *) &TouchSDK_SetAllEnable, avs::game::DLL_INSTANCE); + detour::iat("?GetTouchDeviceCount@TouchSDK@@QEAAHXZ", + (void *) &TouchSDK_GetTouchDeviceCount, avs::game::DLL_INSTANCE); + detour::iat("?GetTouchSDKVersion@TouchSDK@@QEAAIXZ", + (void *) &TouchSDK_GetTouchSDKVersion, avs::game::DLL_INSTANCE); + detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z", + (void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE); + start_touch(); } else { - log_info("drs", "no native input method detected"); + log_info("drs", "touch input for dance floor disabled"); } + init_down_motion_hook(); + if (RGB_CAMERA_HOOK) { init_rgb_camera_hook(); } diff --git a/src/spice2x/games/drs/io.cpp b/src/spice2x/games/drs/io.cpp index 4cf48ee..4979a61 100644 --- a/src/spice2x/games/drs/io.cpp +++ b/src/spice2x/games/drs/io.cpp @@ -20,7 +20,8 @@ std::vector