Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ struct WebsocketClientWrapper {
std::atomic<bool> 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;
Expand Down Expand Up @@ -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<fptn::protocol::https::WebsocketClient> client;

try {
{
std::unique_lock<std::mutex> lock(wrapper->mutex);
Expand All @@ -297,28 +305,43 @@ 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<fptn::protocol::https::WebsocketClient>(
client = std::make_shared<fptn::protocol::https::WebsocketClient>(
std::move(client_config),
#if defined(__APPLE__) && TARGET_OS_IOS
1
#else
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<std::mutex> 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) {
{
std::unique_lock<std::mutex> 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(),
Expand All @@ -328,6 +351,9 @@ void client_run_thread(WebsocketClientWrapper* wrapper) {
std::unique_lock<std::mutex> 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);
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -435,7 +463,7 @@ bool WebsocketSwiftBridge::stop() {
}

if (active_client) {
active_client->Stop();
active_client->Stop(static_cast<fptn::protocol::https::StopOrigin>(origin));
}

if (wrapper_->client_thread.joinable()) {
Expand All @@ -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<std::uint8_t>(fptn::protocol::https::SendResult::transport_stopped);
}
if (!wrapper_->client) {
return static_cast<std::uint8_t>(fptn::protocol::https::SendResult::transport_stopped);
}
if (!packet_data || length == 0) {
return static_cast<std::uint8_t>(fptn::protocol::https::SendResult::invalid_packet);
}
std::shared_ptr<fptn::protocol::https::WebsocketClient> client;
{
std::lock_guard<std::mutex> lock(wrapper_->mutex);
client = wrapper_->client;
}
if (!client) {
return static_cast<std::uint8_t>(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<std::uint8_t>(fptn::protocol::https::SendResult::invalid_packet);
}
return static_cast<std::uint8_t>(wrapper_->client->Send(std::move(packet)));
return static_cast<std::uint8_t>(client->Send(std::move(packet)));
} catch (...) {
return static_cast<std::uint8_t>(fptn::protocol::https::SendResult::invalid_packet);
}
}

bool WebsocketSwiftBridge::isStarted() const {
return wrapper_ && wrapper_->client && wrapper_->client->IsStarted();
if (!wrapper_) return false;
std::shared_ptr<fptn::protocol::https::WebsocketClient> client;
{
std::lock_guard<std::mutex> 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;
Expand Down Expand Up @@ -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<uint16_t>(fptn::protocol::https::unpackDisconnectCode(reason));
status.stop_origin = static_cast<uint16_t>(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<uint16_t>(fptn::protocol::https::unpackDisconnectCode(reason));
status.stop_origin = static_cast<uint16_t>(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();
Expand Down
9 changes: 8 additions & 1 deletion FptnLib/src/websocket/WrapperWebsocketClientBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 14 additions & 4 deletions FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
)
}
}
20 changes: 15 additions & 5 deletions FptnVPNTunnel/Cpp/WebScoketClientBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
)
}
}
Expand Down
32 changes: 31 additions & 1 deletion SharedTunnelRuntime/TunnelLifecycleRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading