Skip to content

Commit b27e628

Browse files
committed
Fix up application handing when shutting down
1 parent 5f6d236 commit b27e628

File tree

9 files changed

+60
-4
lines changed

9 files changed

+60
-4
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).
55
## [staging]
66

77

8+
## [0.1.1] - 2026-03-17
9+
### Fixed
10+
- Fix REAPER hanging on exit due to client TCP sockets not being shut down during server stop, causing blocking `recv()` calls in client threads to never return
11+
- Unregister timer callback (`-timer`) during plugin unload to prevent use-after-free of destroyed globals
12+
- Drain pending command queue on shutdown so in-flight futures resolve instead of blocking indefinitely
13+
814
## [0.1.0] - 2026-03-17
915
### Added
1016
- Initial Build. Artefacts are built for Linux, Windows, and macOS and published to GitHub releases.

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
cmake_minimum_required(VERSION 3.15)
2-
project(reaserve VERSION 0.1.0 LANGUAGES CXX)
2+
project(reaserve VERSION 0.1.1 LANGUAGES CXX)
33

44
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)

DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Triggered by pushing a `v*` tag. Builds and tests all three platforms, then crea
8181

8282
### Making a Release
8383

84-
```bash
84+
```
8585
# 1. Bump version in CMakeLists.txt
8686
# project(reaserve VERSION 0.2.0 LANGUAGES CXX)
8787

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ A standalone C++ REAPER extension plugin that exposes common REAPER operations o
44

55
Control REAPER from any language — Python, Go, Node.js, Rust, or anything that can open a TCP socket.
66

7+
This project ahs been built to help facilitate AI integration with REAPER, but can be used for any remote control use case. This plugin does not require a "file based bridge" or for the http server to be enabled. It can execute commands and return results entirely over TCP.
8+
9+
710
## Installation
811

912
1. Download `reaper_reaserve` for your platform from [Releases](../../releases)
@@ -36,15 +39,21 @@ python examples/python_client.py
3639
go run examples/go_client.go
3740
```
3841

39-
If you have installed hte plugin correctly, both should print the JSON-RPC response:
42+
If you have installed hte plugin correctly, the example scripts will print the JSON-RPC responses for the example commands sent in:
4043

4144
```
4245
$ go run go_client.go
4346
4447
Ping: {"pong":true,"version":"0.1.0"}
4548
4649
Project: 120 BPM, 4 tracks
50+
Track 0: Melody (0.0 dB)
51+
Track 1: Bass (-1.9 dB)
52+
Track 2: Chords (-4.4 dB)
53+
Track 3: Drums (-0.9 dB)
4754
55+
Transport: stopped at 15.00s
56+
4857
Added track: {"index":4,"success":true,"track_count":5}
4958
```
5059

@@ -86,6 +95,11 @@ The plugin binary will be at `build/reaper_reaserve.so` (Linux), `.dylib` (macOS
8695

8796
## Architecture
8897

98+
Note: The timer callback processes up to 5 commands per tick
99+
100+
![reaserve diagram](files/reaserve.png)
101+
102+
89103
```mermaid
90104
graph TD
91105
Client([TCP Client]) <-->|JSON-RPC over TCP| Server

files/reaserve.png

2.39 MB
Loading

src/main.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,22 @@ REAPER_PLUGIN_DLL_EXPORT int ReaperPluginEntry(
8181
(void)hInstance;
8282

8383
if (!rec) {
84-
// Unloading
84+
// Unloading — order matters to avoid deadlock:
85+
// 1. Unregister timer first so no new commands are processed
86+
reaserve::api::plugin_register("-timer", (void*)timer_callback);
87+
88+
// 2. Stop server (closes sockets, joins threads)
8589
if (g_server) {
8690
g_server->stop();
8791
g_server.reset();
8892
}
93+
94+
// 3. Drain any remaining commands so their futures don't block
95+
while (auto cmd = g_queue.try_pop()) {
96+
cmd->response.set_value(
97+
reaserve::make_error(cmd->id, reaserve::ERR_TIMEOUT,
98+
"Plugin shutting down"));
99+
}
89100
return 0;
90101
}
91102

src/platform.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <ws2tcpip.h>
66
using socket_t = SOCKET;
77
constexpr socket_t INVALID_SOCK = INVALID_SOCKET;
8+
constexpr int SHUT_RDWR_COMPAT = SD_BOTH;
89
inline int close_socket(socket_t s) { return closesocket(s); }
910
inline bool init_sockets() {
1011
WSADATA wsa;
@@ -19,6 +20,7 @@
1920
#include <cerrno>
2021
using socket_t = int;
2122
constexpr socket_t INVALID_SOCK = -1;
23+
constexpr int SHUT_RDWR_COMPAT = SHUT_RDWR;
2224
inline int close_socket(socket_t s) { return close(s); }
2325
inline bool init_sockets() { return true; }
2426
inline void cleanup_sockets() {}

src/tcp_server.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,22 @@ bool TcpServer::start() {
9595
void TcpServer::stop() {
9696
running_ = false;
9797
if (listen_sock_ != INVALID_SOCK) {
98+
shutdown(listen_sock_, SHUT_RDWR_COMPAT);
9899
close_socket(listen_sock_);
99100
listen_sock_ = INVALID_SOCK;
100101
}
102+
103+
// Shutdown all active client sockets so their blocking recv() calls
104+
// return immediately instead of hanging until the remote end closes.
105+
{
106+
std::lock_guard<std::mutex> lock(clients_mutex_);
107+
for (auto sock : active_client_sockets_) {
108+
shutdown(sock, SHUT_RDWR_COMPAT);
109+
close_socket(sock);
110+
}
111+
active_client_sockets_.clear();
112+
}
113+
101114
if (accept_thread_.joinable()) {
102115
accept_thread_.join();
103116
}
@@ -133,6 +146,7 @@ void TcpServer::accept_loop() {
133146
}),
134147
client_threads_.end()
135148
);
149+
active_client_sockets_.push_back(client);
136150
client_threads_.emplace_back(&TcpServer::client_loop, this, client);
137151
}
138152
}
@@ -146,6 +160,14 @@ void TcpServer::client_loop(socket_t client_sock) {
146160
if (!framing::write_message(client_sock, response)) break;
147161
}
148162
close_socket(client_sock);
163+
164+
// Remove from active set
165+
{
166+
std::lock_guard<std::mutex> lock(clients_mutex_);
167+
active_client_sockets_.erase(
168+
std::remove(active_client_sockets_.begin(), active_client_sockets_.end(), client_sock),
169+
active_client_sockets_.end());
170+
}
149171
}
150172

151173
} // namespace reaserve

src/tcp_server.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class TcpServer {
4444
socket_t listen_sock_ = INVALID_SOCK;
4545
std::thread accept_thread_;
4646
std::vector<std::thread> client_threads_;
47+
std::vector<socket_t> active_client_sockets_;
4748
std::mutex clients_mutex_;
4849
};
4950

0 commit comments

Comments
 (0)