Skip to content

Improve network stack#876

Open
jcelerier wants to merge 15 commits into
masterfrom
feature/remove-websocketpp
Open

Improve network stack#876
jcelerier wants to merge 15 commits into
masterfrom
feature/remove-websocketpp

Conversation

@jcelerier

@jcelerier jcelerier commented Mar 24, 2026

Copy link
Copy Markdown
Member
  • make sure all tests pass.
  • ipv6
  • TLS support
  • emscripten WS back-end
  • fix boost.json requiring linking on windows ?

OSCQuery publication / exploration examples seem to work fine and are compatible with the previous version.

@jcelerier jcelerier changed the title feature/remove websocketpp Improve network stack Mar 24, 2026
@jcelerier

jcelerier commented Mar 24, 2026

Copy link
Copy Markdown
Member Author

@x37v if you want to review / test this version with your stack. I need to do a bit more testing but we now have:

  • a much more modern (and consistently maintained) websocket backend, unlike websocketpp which has been abandoned for years: one commit in the last five years.
  • I tried adding ipv6 support wherever it was relevant - things work locally but so far I only tested this part under linux, need to test on windows and mac, also with various clients such as ESP32
  • the websocket implementation for oscquery is more flexible and also now supports using [socket.io](https://socket.io/ as backend) (@ogauthiersat @blueyeti) ; this opens interesting use cases with the "rooms" concept of socket.io.

@ogauthiersat

Copy link
Copy Markdown

@jcelerier I managed to build this branch into my score dev environment. However, I had to comment out all calls to boost::asio::ip::v6_only and disable vst/vst3/clap plugins that were not finding websocket_simple_client.

/home/ogauthier/git/github.com/ossia/score/3rdparty/libossia/src/ossia/protocols/socketio/socketio_server.cpp:472:42: error: no member named 'v6_only' in namespace 'boost::asio::ip'
  472 |   m_acceptor.set_option(boost::asio::ip::v6_only(false), ec);
/home/ogauthier/git/github.com/ossia/score/src/clappuppet/clappuppet.cpp:190:15: error: no type named 'websocket_simple_client' in namespace 'ossia::net'; did you mean 'websocket_simple_client_beast'?
  190 |   ossia::net::websocket_simple_client socket{{.url = "ws://127.0.0.1:37589"}, ctx};

I tested OSCQuery device, gives the same results under this branch or with release 3.8.1 :

  • Add OSCQuery device pointing at host ws://127.0.0.1:9999
  • OSCQuery device appears empty
  • Right-click QSCQuery device, Refresh namespace
  • OSCQuery device is populated normally
  • Add String process
  • String process does not appear in QSCQuery device tree
  • Right-click QSCQuery device, Refresh namespace
  • String process is available in OSCQuery tree, and modify value works

I confirm that user agent is different in the websocket setup header :

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: WHdzoMgsjebbUf6MTfY4iCsB6aQ=
Server: Boost.Beast/359

3.8.1 :

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: RNBbeBhCRzVA8/FAE9I48Lo7LWA=
Server: WebSocket++/0.8.3-dev
Upgrade: websocket

Is there a way to test socket.io from Score at the moment ?

@x37v

x37v commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

@jcelerier sorry for the slow response, pretty busy with the latest release and various tasks around that but I will get to this.

@jcelerier

Copy link
Copy Markdown
Member Author

thanks for the review @ogauthiersat ! and no worries @x37v, the branch still needs a fair bit of polishing ::)

@jcelerier jcelerier temporarily deployed to Apple Certificate June 1, 2026 23:57 — with GitHub Actions Inactive
@jcelerier jcelerier temporarily deployed to Apple Certificate June 1, 2026 23:57 — with GitHub Actions Inactive
@jcelerier jcelerier force-pushed the feature/remove-websocketpp branch from 4337901 to 450ea74 Compare June 2, 2026 00:33
jcelerier and others added 15 commits June 21, 2026 01:10
…removal

- websocket_simple_beast: the duck-typed simple client stored its member as
  unique_ptr<websocket_client_beast>, but make_websocket_client() returns the
  websocket_client_interface base (and the beast header is not included here).
  This broke every libossia build that compiles osc_factory.cpp (i.e. all
  platforms). Store the interface type instead.

- http_client_request: a response to a HEAD request never carries a body even
  when it advertises Content-Length. Tell the beast parser to skip the body
  for HEAD, otherwise it waits for bytes that never arrive and fails with
  "partial message". Fixes the http_client_head test.

- HttpClientRequestTest: include the boost.asio headers it uses directly
  (streambuf, read, read_until, buffers_iterator, write) instead of relying on
  transitive includes that no longer pull them in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRveXCy9GX7Kt7xD2ydd7v
The websocketpp->beast port performed synchronous beast writes from the
producer (device/score) thread while an async_read was in flight on the
io thread. A beast websocket::stream is not thread-safe; the old
websocketpp send() was. ThreadSanitizer confirms the data race; this
commit makes every send marshal onto the connection strand through an
async write queue (one write in flight, payload copied so it outlives the
op), restoring the thread-safe-send semantics.

Server (websocket_server_beast):
- per-connection async write queue dispatched on the strand; close() also
  dispatched (it shares the stream with the in-flight read).
- find_connection() returns a shared_ptr (was a raw pointer freed under a
  TOCTOU window by the io thread).
- on_accept() re-arms on transient errors instead of killing the listener.

Client (websocket_client_beast):
- same strand-dispatched async write queue.
- remote_endpoint() now uses the non-throwing overload (a throw here ran
  inside io_context::run and would stop all networking).
- wss:// is rejected loudly instead of silently connecting in plaintext
  (TLS is not implemented in the beast backend yet); factory docs fixed.

OSCQuery:
- open_osc_sender() sets up client.sender under m_clientsMutex; it was
  written by the io thread while push() read it under the lock (TSAN race).
- query_parser query-string slice off-by-one (read one byte past the end).

Socket.IO:
- do_accept_ws() passed the request by value; async_accept borrowed the
  destroyed local -> use the member (use-after-free).
- same strand write queue; ping/probe go through it; close() on the strand;
  parse_* use the non-throwing boost::json::parse; Emscripten build guards.

websocket_simple_beast: guard the listener list with a mutex.
examples/Web: usleep -> std::this_thread::sleep_for (Windows build).

New: tests/Network/NetworkThreadSafetyTest validates the WS send race, the
teardown path, and concurrent OSCQuery pushes over both transports. Verified
under TSAN (0 races), ASAN/UBSAN (0 errors), and the full suite (59/59) on
gcc-14 and clang-20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRveXCy9GX7Kt7xD2ydd7v
…build

A parent project that embeds libossia with CMAKE_INCLUDE_CURRENT_DIR ON
(score does, globally) leaks that setting into the vendored Abseil
add_subdirectory. Each Abseil target then gets its own source/binary dir
on the include path, so `absl/time/time.h` shadows the system <time.h>,
which breaks libstdc++'s <ctime>/<chrono> and fails the abseil build under
clang. Force the flag off inside the existing block() (scoped, so the
parent's setting is restored afterwards). No-op for standalone libossia,
where the flag is already off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRveXCy9GX7Kt7xD2ydd7v
websocket_client_beast derives from the OSSIA_EXPORT websocket_client_interface
whose vtable lives in a .cpp, so any use of it (or the make_websocket_client
factory) requires linking libossia. Tiny standalone tools that consume ossia
headers only — e.g. score's vst/vst3/clap puppets, which link
$<COMPILE_ONLY:ossia> and must not depend on libossia.so — therefore can't use
it.

Add websocket_simple_header_client: a fully inline beast-based client (reusing
parse_websocket_uri and the common ws types) that connects, fires
on_open/on_fail/on_close, sends text/binary frames and closes. Driven by a
single-threaded io_context, so sends are synchronous.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRveXCy9GX7Kt7xD2ydd7v
score builds with CMAKE_UNITY_BUILD ON, which leaks into the vendored Abseil
add_subdirectory. Abseil's cctz sources are not unity-safe: time_zone_posix.cc
and time_zone_fixed.cc each define a namespace-scope `kDigits`, so the merged
unity TU fails with "redefinition of 'kDigits'". Force unity off for Abseil
inside the existing scoped block(); the parent setting is unchanged. No-op for
standalone libossia (unity already off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRveXCy9GX7Kt7xD2ydd7v
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants