diff --git a/docs/site/package-lock.json b/docs/site/package-lock.json index 37ca23691..cf2c3678b 100644 --- a/docs/site/package-lock.json +++ b/docs/site/package-lock.json @@ -3442,9 +3442,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { diff --git a/tests/app/net_plugin.cpp b/tests/app/net_plugin.cpp index 6eb53f4a3..e89307883 100644 --- a/tests/app/net_plugin.cpp +++ b/tests/app/net_plugin.cpp @@ -37,6 +37,33 @@ void wait_for(predicate ready, pump update) { require(ready(), "Timed out waiting for owned loopback peer"); } +arknet::error_code listener_probe(asio::io_context&, const asio::ip::tcp::endpoint& endpoint) { + asio::io_context context; + asio::ip::tcp::socket probe(context); + asio::steady_timer deadline(context, 5s); + arknet::error_code result = asio::error::operation_aborted; + bool completed = false; + bool timed_out = false; + deadline.async_wait([&](const arknet::error_code& error) { + if (!error && !completed) + { + timed_out = true; + arknet::error_code ignored; + probe.cancel(ignored); + probe.close(ignored); + } + }); + probe.async_connect(endpoint, [&](const arknet::error_code& error) { + completed = true; + result = timed_out ? arknet::error_code(asio::error::timed_out) : error; + deadline.cancel(); + }); + // Drain both the connect completion and the cancelled timer before their + // stack captures go out of scope. A deadline is not a refused connection. + context.run(); + return result; +} + struct receiver { void on_message(const ark::net_msg* message, ark::conv_id_t session) { @@ -86,8 +113,17 @@ int main(int argc, char* argv[]) { receiver received; require(server->reg_msg_callback(45000, &received, &receiver::on_message), "Net message callback rejected"); std::weak_ptr endpoint = server->get_net(); + const asio::ip::tcp::endpoint listener_endpoint(asio::ip::make_address("127.0.0.1"), port); + const auto live_probe = listener_probe(context, listener_endpoint); + require(!live_probe, "Live Net listener negative control failed: " + live_probe.message()); + std::atomic peer_closed{}; + arknet::error_code peer_close_error; arknet::tcp_client client; client.set_auto_reconnect(false); + client.bind_disconnect([&] { + peer_close_error = arknet::get_last_error(); + peer_closed.store(true); + }); require(client.start("127.0.0.1", port), "Owned TCP peer failed to start"); ark::s_msg_header header; header.set_message_id(45000); @@ -143,16 +179,27 @@ int main(int argc, char* argv[]) { const auto cancelled = pending.get(); require(!cancelled.first && cancelled.second == arknet::error_code(asio::error::operation_aborted).message(), "HTTP DSO shutdown changed pending-request cancellation"); - client.stop(); peer.stop(); + require(client.is_started() && !peer_closed.load(), "Owned TCP peer must remain live across Net shutdown"); server.reset(); require(manager.stop(++tick), "Net DSO shutdown did not complete"); require(endpoint.expired(), "Net listener survived normal module unregistration"); require(manager.get_shutdown_state() == ark::plugin_manager::shutdown_state::stopped, "Net resources were retained instead of normally stopped"); + // Keep the peer open until the server closes its accepted socket. Stopping + // the client first would hide a retained server-side connection. + wait_for([&] { return peer_closed.load(); }, [] {}); + require(peer_close_error == asio::error::eof || peer_close_error == asio::error::connection_reset, + "Net shutdown did not remotely close the owned TCP peer: " + peer_close_error.message()); + client.stop(); require(manager.stop(++tick), "Repeated Net shutdown failed"); - asio::ip::tcp::acceptor rebound(context, {asio::ip::make_address("127.0.0.1"), port}); - require(rebound.is_open(), "Net shutdown did not release its listener"); - std::cout << "Net actual DSOs: interface RTTI lookup, manager-owned TCP/HTTP callbacks, listener release and " + // Binding the same port also tests unrelated kernel port-reuse state. A + // refused new connection checks listener closure directly, with no retry. + // The successful probe above is the negative control for a live listener. + const auto closed_probe = listener_probe(context, listener_endpoint); + require(closed_probe == asio::error::connection_refused, + "Net listener closure probe must be refused: " + closed_probe.message()); + std::cout << "Net actual DSOs: interface RTTI lookup, manager-owned TCP/HTTP callbacks, listener refusal, remote " + "peer closure and " "normal repeated shutdown passed\n"; }