Фикс: имя файла в пересланных сообщениях, потеря фоток/файлов при пересылке forwarded-сообщений, Фоллбэк при unwrap forwarded-сообщения, защита БД от перезаписи синком
This commit is contained in:
@@ -57,6 +57,23 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
private var handshakeComplete = false
|
||||
private var heartbeatTask: Task<Void, Never>?
|
||||
private var handshakeTimeoutTask: Task<Void, Never>?
|
||||
private var pingTimeoutTask: Task<Void, Never>?
|
||||
/// Guards against overlapping ping-first verifications on foreground.
|
||||
private var pingVerificationInProgress = false
|
||||
|
||||
/// Android parity: sync batch flag set SYNCHRONOUSLY on receive queue.
|
||||
/// Prevents race where MainActor Task for BATCH_START runs after message Task.
|
||||
/// Written on URLSession delegate queue, read on MainActor — protected by lock.
|
||||
private let syncBatchLock = NSLock()
|
||||
private var _syncBatchActive = false
|
||||
|
||||
/// Thread-safe read for SessionManager to check sync state without MainActor race.
|
||||
var isSyncBatchActive: Bool {
|
||||
syncBatchLock.lock()
|
||||
let val = _syncBatchActive
|
||||
syncBatchLock.unlock()
|
||||
return val
|
||||
}
|
||||
private let searchHandlersLock = NSLock()
|
||||
private let resultHandlersLock = NSLock()
|
||||
private let packetQueueLock = NSLock()
|
||||
@@ -94,6 +111,9 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
Self.logger.info("Disconnecting")
|
||||
heartbeatTask?.cancel()
|
||||
handshakeTimeoutTask?.cancel()
|
||||
pingTimeoutTask?.cancel()
|
||||
pingTimeoutTask = nil
|
||||
pingVerificationInProgress = false
|
||||
handshakeComplete = false
|
||||
client.disconnect()
|
||||
connectionState = .disconnected
|
||||
@@ -109,12 +129,13 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
func reconnectIfNeeded() {
|
||||
guard savedPublicKey != nil, savedPrivateHash != nil else { return }
|
||||
|
||||
// Android parity: skip if already in any active state.
|
||||
// Android parity (Protocol.kt:651-658): skip if already in any active state.
|
||||
switch connectionState {
|
||||
case .authenticated, .handshaking, .deviceVerificationRequired, .connected:
|
||||
return
|
||||
case .connecting:
|
||||
if client.isConnected { return }
|
||||
// Android parity: `(CONNECTING && isConnecting)` — skip if connect() is in progress.
|
||||
if client.isConnecting { return }
|
||||
case .disconnected:
|
||||
break
|
||||
}
|
||||
@@ -127,13 +148,63 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
client.forceReconnect()
|
||||
}
|
||||
|
||||
/// Ping-first zombie socket detection for foreground resume.
|
||||
/// iOS suspends the process in background — server RSTs TCP, but `didCloseWith`
|
||||
/// delegate never fires. `connectionState` stays `.authenticated` (stale).
|
||||
/// This method sends a WebSocket ping: pong → alive, no pong → force reconnect.
|
||||
func verifyConnectionOrReconnect() {
|
||||
guard savedPublicKey != nil, savedPrivateHash != nil else { return }
|
||||
guard connectionState == .authenticated || connectionState == .connected else { return }
|
||||
guard !pingVerificationInProgress else { return }
|
||||
|
||||
pingVerificationInProgress = true
|
||||
Self.logger.info("🏓 Verifying connection with ping after foreground...")
|
||||
|
||||
client.sendPing { [weak self] error in
|
||||
guard let self, self.pingVerificationInProgress else { return }
|
||||
self.pingVerificationInProgress = false
|
||||
self.pingTimeoutTask?.cancel()
|
||||
self.pingTimeoutTask = nil
|
||||
|
||||
if let error {
|
||||
Self.logger.warning("🏓 Ping failed — zombie socket: \(error.localizedDescription)")
|
||||
self.handlePingFailure()
|
||||
} else {
|
||||
Self.logger.info("🏓 Pong received — connection alive")
|
||||
}
|
||||
}
|
||||
|
||||
// Safety timeout: if sendPing never calls back (completely dead socket), force reconnect.
|
||||
pingTimeoutTask?.cancel()
|
||||
pingTimeoutTask = Task { [weak self] in
|
||||
try? await Task.sleep(for: .seconds(3))
|
||||
guard let self, !Task.isCancelled, self.pingVerificationInProgress else { return }
|
||||
self.pingVerificationInProgress = false
|
||||
Self.logger.warning("🏓 Ping timeout (3s) — zombie socket, forcing reconnect")
|
||||
self.handlePingFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePingFailure() {
|
||||
pingTimeoutTask?.cancel()
|
||||
pingTimeoutTask = nil
|
||||
handshakeComplete = false
|
||||
heartbeatTask?.cancel()
|
||||
Task { @MainActor in
|
||||
self.connectionState = .connecting
|
||||
}
|
||||
client.forceReconnect()
|
||||
}
|
||||
|
||||
// MARK: - Sending
|
||||
|
||||
func sendPacket(_ packet: any Packet) {
|
||||
PerformanceLogger.shared.track("protocol.sendPacket")
|
||||
let id = String(type(of: packet).packetId, radix: 16)
|
||||
if (!handshakeComplete && !(packet is PacketHandshake)) || !client.isConnected {
|
||||
Self.logger.info("⏳ Queueing packet 0x\(id) — connected=\(self.client.isConnected), handshake=\(self.handshakeComplete)")
|
||||
// Android parity (Protocol.kt:436-448): triple check — handshakeComplete + socket alive + authenticated.
|
||||
let isAuth = connectionState == .authenticated
|
||||
if (!handshakeComplete && !(packet is PacketHandshake)) || !client.isConnected || !isAuth {
|
||||
Self.logger.info("⏳ Queueing packet 0x\(id) — connected=\(self.client.isConnected), handshake=\(self.handshakeComplete), auth=\(isAuth)")
|
||||
enqueuePacket(packet)
|
||||
return
|
||||
}
|
||||
@@ -200,6 +271,9 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
}
|
||||
heartbeatTask?.cancel()
|
||||
handshakeComplete = false
|
||||
pingVerificationInProgress = false
|
||||
pingTimeoutTask?.cancel()
|
||||
pingTimeoutTask = nil
|
||||
|
||||
Task { @MainActor in
|
||||
self.connectionState = .disconnected
|
||||
@@ -348,6 +422,18 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
}
|
||||
case 0x19:
|
||||
if let p = packet as? PacketSync {
|
||||
// Android parity: set sync flag SYNCHRONOUSLY on receive queue
|
||||
// BEFORE dispatching to MainActor callback. This prevents the race
|
||||
// where a 0x06 message Task runs on MainActor before BATCH_START Task.
|
||||
if p.status == .batchStart {
|
||||
syncBatchLock.lock()
|
||||
_syncBatchActive = true
|
||||
syncBatchLock.unlock()
|
||||
} else if p.status == .notNeeded {
|
||||
syncBatchLock.lock()
|
||||
_syncBatchActive = false
|
||||
syncBatchLock.unlock()
|
||||
}
|
||||
onSyncReceived?(p)
|
||||
}
|
||||
default:
|
||||
@@ -412,8 +498,8 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
self.connectionState = .deviceVerificationRequired
|
||||
}
|
||||
|
||||
// Keep packet queue: messages will be flushed when the other device
|
||||
// approves this login and the server re-sends handshake with .completed
|
||||
// Android parity (Protocol.kt:163): clear packet queue on device verification.
|
||||
clearPacketQueue()
|
||||
startHeartbeat(interval: packet.heartbeatInterval)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user