diff --git a/FptnLib/fptn b/FptnLib/fptn index 13acece..0ad95dc 160000 --- a/FptnLib/fptn +++ b/FptnLib/fptn @@ -1 +1 @@ -Subproject commit 13acecee048f2f9d80ffede884db16e8d51982d8 +Subproject commit 0ad95dc889ef7311f22dd9973390fca6fb839b4e diff --git a/FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp b/FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp index d1eb539..0dc96af 100644 --- a/FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp +++ b/FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp @@ -44,6 +44,8 @@ struct WebsocketClientWrapper { std::atomic running{false}; std::mutex mutex; int idle_timeout_seconds; + // PR1C: terminal reason stored after Run() returns. + std::uint32_t final_terminal_reason{0}; std::string server_ip; int server_port; @@ -274,6 +276,12 @@ void initialize_logger_once() { void client_run_thread(WebsocketClientWrapper* wrapper) { initialize_logger_once(); + // PR1C: keep a local strong pointer for the entire thread lifetime. + // The wrapper's stop() may reset wrapper->client before join, but + // this local keeps the client alive until Run() returns and we've + // read the terminal reason. + std::shared_ptr client; + try { { std::unique_lock lock(wrapper->mutex); @@ -297,10 +305,7 @@ void client_run_thread(WebsocketClientWrapper* wrapper) { packet_callback_adapter(std::move(packet), wrapper); }; - // PR1A: io_context concurrency hint of 1 on iOS. The wrapper - // already drives the context from a single native thread; the - // hint only affects internal data-structure sizing. - wrapper->client = std::make_shared( + client = std::make_shared( std::move(client_config), #if defined(__APPLE__) && TARGET_OS_IOS 1 @@ -308,10 +313,25 @@ void client_run_thread(WebsocketClientWrapper* wrapper) { 4 #endif ); + wrapper->client = client; } - if (wrapper->running && wrapper->client) { - wrapper->client->Run(); + // PR1C: always drive the client once constructed. A pre-Run + // stop is represented by stop_requested_; Run() processes the + // posted cleanup and exits through the barrier. Skipping Run() + // would leave the posted lambda (and its captured shared_ptr) + // stranded in the io_context forever. + if (client) { + client->Run(); + } + + const auto reason = client ? client->GetTerminalReason() : 0; + { + std::unique_lock lock(wrapper->mutex); + if (wrapper->client == client) { + wrapper->client.reset(); + } + wrapper->final_terminal_reason = reason; } disconnected_callback_adapter(true, "WebSocket client stopped", wrapper); } catch (const std::exception& ex) { @@ -319,6 +339,9 @@ void client_run_thread(WebsocketClientWrapper* wrapper) { std::unique_lock lock(wrapper->mutex); wrapper->last_error = std::string("WebSocket wrapper exception: ") + ex.what(); wrapper->last_disconnect_reason = wrapper->last_error; + if (wrapper->client == client) { + wrapper->client.reset(); + } } disconnected_callback_adapter(false, std::string("WebSocket wrapper exception: ") + ex.what(), @@ -328,6 +351,9 @@ void client_run_thread(WebsocketClientWrapper* wrapper) { std::unique_lock lock(wrapper->mutex); wrapper->last_error = "WebSocket wrapper unknown exception"; wrapper->last_disconnect_reason = wrapper->last_error; + if (wrapper->client == client) { + wrapper->client.reset(); + } } disconnected_callback_adapter(false, "WebSocket wrapper unknown exception", wrapper); } @@ -420,7 +446,9 @@ bool WebsocketSwiftBridge::start() { } } -bool WebsocketSwiftBridge::stop() { +// PR1C: stop accepts an origin so the caller can distinguish +// tunnel shutdown, reconnect, and bridge destruction. +bool WebsocketSwiftBridge::stop(std::uint16_t origin) { if (!wrapper_) { return false; } @@ -435,7 +463,7 @@ bool WebsocketSwiftBridge::stop() { } if (active_client) { - active_client->Stop(); + active_client->Stop(static_cast(origin)); } if (wrapper_->client_thread.joinable()) { @@ -447,34 +475,47 @@ bool WebsocketSwiftBridge::stop() { // PR1B: returns typed send result as uint8_t. // 0=accepted, 1=queue_full, 2=transport_stopped, 3=invalid_packet. +// PR1C: takes a local copy of the client under the mutex to avoid +// racing with stop() resetting wrapper_->client. std::uint8_t WebsocketSwiftBridge::sendPacket(const uint8_t* packet_data, uint32_t length) { if (!wrapper_ || !wrapper_->running) { return static_cast(fptn::protocol::https::SendResult::transport_stopped); } - if (!wrapper_->client) { - return static_cast(fptn::protocol::https::SendResult::transport_stopped); - } if (!packet_data || length == 0) { return static_cast(fptn::protocol::https::SendResult::invalid_packet); } + std::shared_ptr client; + { + std::lock_guard lock(wrapper_->mutex); + client = wrapper_->client; + } + if (!client) { + return static_cast(fptn::protocol::https::SendResult::transport_stopped); + } try { fptn::common::network::IPPacketData buffer(packet_data, packet_data + length); auto packet = fptn::common::network::IPPacket::Parse(std::move(buffer)); if (!packet) { return static_cast(fptn::protocol::https::SendResult::invalid_packet); } - return static_cast(wrapper_->client->Send(std::move(packet))); + return static_cast(client->Send(std::move(packet))); } catch (...) { return static_cast(fptn::protocol::https::SendResult::invalid_packet); } } bool WebsocketSwiftBridge::isStarted() const { - return wrapper_ && wrapper_->client && wrapper_->client->IsStarted(); + if (!wrapper_) return false; + std::shared_ptr client; + { + std::lock_guard lock(wrapper_->mutex); + client = wrapper_->client; + } + return client && client->IsStarted(); } WebsocketClientBridgeStatus WebsocketSwiftBridge::getStatus() const { - WebsocketClientBridgeStatus status = {false, false, 0, "", "", 0, 0, 0, 0, 0, 0, 0, false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + WebsocketClientBridgeStatus status = {false, false, 0, "", "", 0, 0, 0, 0, 0, 0, 0, false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 0}; if (!wrapper_) { status.last_error = "Invalid handle"; return status; @@ -505,6 +546,18 @@ WebsocketClientBridgeStatus WebsocketSwiftBridge::getStatus() const { status.queued_bytes = wrapper_->client->GetQueuedBytes(); status.queued_bytes_peak = wrapper_->client->GetQueuedBytesPeak(); status.queue_full_count = wrapper_->client->GetQueueFullCount(); + const auto reason = wrapper_->client->GetTerminalReason(); + status.disconnect_code = static_cast(fptn::protocol::https::unpackDisconnectCode(reason)); + status.stop_origin = static_cast(fptn::protocol::https::unpackStopOrigin(reason)); + status.stop_cleanup_completed = wrapper_->client->IsStopCleanupCompleted(); + status.active_operations = wrapper_->client->GetActiveOperations(); + } else { + // PR1C: client was reset (after stop); use stored terminal reason. + const auto reason = wrapper_->final_terminal_reason; + status.disconnect_code = static_cast(fptn::protocol::https::unpackDisconnectCode(reason)); + status.stop_origin = static_cast(fptn::protocol::https::unpackStopOrigin(reason)); + status.stop_cleanup_completed = true; + status.active_operations = 0; } status.live_clients = fptn::protocol::https::WebsocketClient::GetLiveClients(); status.active_reader_coroutines = fptn::protocol::https::WebsocketClient::GetActiveReaderCoroutines(); diff --git a/FptnLib/src/websocket/WrapperWebsocketClientBridge.h b/FptnLib/src/websocket/WrapperWebsocketClientBridge.h index d00eecf..2c30c96 100644 --- a/FptnLib/src/websocket/WrapperWebsocketClientBridge.h +++ b/FptnLib/src/websocket/WrapperWebsocketClientBridge.h @@ -44,6 +44,11 @@ struct WebsocketClientBridgeStatus { uint64_t queued_bytes; uint64_t queued_bytes_peak; uint64_t queue_full_count; + // PR1C: teardown diagnostics. + uint16_t disconnect_code; + uint16_t stop_origin; + bool stop_cleanup_completed; + uint32_t active_operations; }; // PR0: SWIFT_NONCOPYABLE makes Swift import this as ~Copyable, @@ -78,7 +83,9 @@ class SWIFT_NONCOPYABLE WebsocketSwiftBridge { WebsocketSwiftBridge& operator=(WebsocketSwiftBridge&& other) noexcept; bool start(); - bool stop(); + // PR1C: stop accepts an origin (0=none, 1=swift_tunnel_stop, + // 2=swift_reconnect, 3=native_failure, 4=peer, 255=unknown). + bool stop(std::uint16_t origin = 1); // PR1B: returns 0=accepted, 1=queue_full, 2=transport_stopped, 3=invalid_packet. std::uint8_t sendPacket(const std::uint8_t* packet_data, std::uint32_t length); bool isStarted() const; diff --git a/FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift b/FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift index 7085550..27d9f55 100644 --- a/FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift +++ b/FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift @@ -34,6 +34,11 @@ struct WebsocketClientStatus: Sendable { let queuedBytes: UInt64 let queuedBytesPeak: UInt64 let queueFullCount: UInt64 + // PR1C: teardown diagnostics. + let disconnectCode: WebsocketDisconnectCode + let stopOrigin: WebsocketStopOrigin + let stopCleanupCompleted: Bool + let activeOperations: UInt32 } final class WebsocketClientBridge { @@ -127,10 +132,11 @@ final class WebsocketClientBridge { return ok } + // PR1C: stop accepts an origin for disconnect classification. @discardableResult - func stop() -> Bool { - let ok = clientBridge.stop() - logger.debug("WebSocket stop → \(ok)") + func stop(origin: WebsocketStopOrigin = .swiftTunnelStop) -> Bool { + let ok = clientBridge.stop(origin.rawValue) + logger.debug("WebSocket stop → \(ok) origin=\(origin)") return ok } @@ -180,7 +186,11 @@ final class WebsocketClientBridge { queuedPackets: raw.queued_packets, queuedBytes: raw.queued_bytes, queuedBytesPeak: raw.queued_bytes_peak, - queueFullCount: raw.queue_full_count + queueFullCount: raw.queue_full_count, + disconnectCode: WebsocketDisconnectCode(bridgeValue: raw.disconnect_code), + stopOrigin: WebsocketStopOrigin(bridgeValue: raw.stop_origin), + stopCleanupCompleted: raw.stop_cleanup_completed, + activeOperations: raw.active_operations ) } } diff --git a/FptnVPNTunnel/Cpp/WebScoketClientBridge.swift b/FptnVPNTunnel/Cpp/WebScoketClientBridge.swift index 24029d9..7a16292 100644 --- a/FptnVPNTunnel/Cpp/WebScoketClientBridge.swift +++ b/FptnVPNTunnel/Cpp/WebScoketClientBridge.swift @@ -34,6 +34,11 @@ struct WebsocketClientStatus: Sendable { let queuedBytes: UInt64 let queuedBytesPeak: UInt64 let queueFullCount: UInt64 + // PR1C: teardown diagnostics. + let disconnectCode: WebsocketDisconnectCode + let stopOrigin: WebsocketStopOrigin + let stopCleanupCompleted: Bool + let activeOperations: UInt32 } final class WebsocketClientBridge { @@ -139,11 +144,12 @@ final class WebsocketClientBridge { return ok } + // PR1C: stop accepts an origin for disconnect classification. @discardableResult - func stop() -> Bool { - let ok = clientBridge.stop() - logger.debug("WebSocket stop → \(ok)") - TunnelDiagnosticsStore.shared.recordProviderEvent(category: "bridge", message: "stop id=\(diagnosticsID) ok=\(ok)") + func stop(origin: WebsocketStopOrigin = .swiftTunnelStop) -> Bool { + let ok = clientBridge.stop(origin.rawValue) + logger.debug("WebSocket stop → \(ok) origin=\(origin)") + TunnelDiagnosticsStore.shared.recordProviderEvent(category: "bridge", message: "stop id=\(diagnosticsID) ok=\(ok) origin=\(origin)") return ok } @@ -193,7 +199,11 @@ final class WebsocketClientBridge { queuedPackets: raw.queued_packets, queuedBytes: raw.queued_bytes, queuedBytesPeak: raw.queued_bytes_peak, - queueFullCount: raw.queue_full_count + queueFullCount: raw.queue_full_count, + disconnectCode: WebsocketDisconnectCode(bridgeValue: raw.disconnect_code), + stopOrigin: WebsocketStopOrigin(bridgeValue: raw.stop_origin), + stopCleanupCompleted: raw.stop_cleanup_completed, + activeOperations: raw.active_operations ) } } diff --git a/SharedTunnelRuntime/TunnelLifecycleRuntime.swift b/SharedTunnelRuntime/TunnelLifecycleRuntime.swift index f89944a..e2752fd 100644 --- a/SharedTunnelRuntime/TunnelLifecycleRuntime.swift +++ b/SharedTunnelRuntime/TunnelLifecycleRuntime.swift @@ -206,9 +206,39 @@ enum WebsocketSendResult: UInt8, Sendable { } } +// PR1C: numeric disconnect diagnostics matching C++ ABI values. +enum WebsocketDisconnectCode: UInt16, Sendable { + case none = 0 + case peerClosed = 1 + case tcpError = 2 + case tlsError = 3 + case websocketError = 4 + case watchdog = 5 + case localStop = 6 + case queueFailure = 7 + case unknown = 255 + + init(bridgeValue: UInt16) { + self = Self(rawValue: bridgeValue) ?? .unknown + } +} + +enum WebsocketStopOrigin: UInt16, Sendable { + case none = 0 + case swiftTunnelStop = 1 + case swiftReconnect = 2 + case nativeFailure = 3 + case peer = 4 + case unknown = 255 + + init(bridgeValue: UInt16) { + self = Self(rawValue: bridgeValue) ?? .unknown + } +} + protocol TunnelWebSocketTransport: AnyObject { func start() -> Bool - func stop() -> Bool + func stop(origin: WebsocketStopOrigin) -> Bool func sendPacket(_ data: Data) -> WebsocketSendResult }