mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 19:05:20 +00:00
Fix thread safety crashes in BluetoothMeshService
Fixed multiple threading issues that were causing crashes when users sent messages: - Added NSLock to synchronize access to recentlySentMessages Set - Fixed thread-unsafe access to activePeers Set using existing lock - Removed force unwrapping that could cause crashes - Added defensive input validation for message sending methods These fixes address crashes reported in TestFlight where the app would crash when typing and sending messages.
This commit is contained in:
@@ -74,6 +74,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
private var cachedMessagesSentToPeer: Set<String> = [] // Track which peers have already received cached messages
|
private var cachedMessagesSentToPeer: Set<String> = [] // Track which peers have already received cached messages
|
||||||
private var receivedMessageTimestamps: [String: Date] = [:] // Track timestamps of received messages for debugging
|
private var receivedMessageTimestamps: [String: Date] = [:] // Track timestamps of received messages for debugging
|
||||||
private var recentlySentMessages: Set<String> = [] // Short-term cache to prevent any duplicate sends
|
private var recentlySentMessages: Set<String> = [] // Short-term cache to prevent any duplicate sends
|
||||||
|
private let recentlySentMessagesLock = NSLock() // Thread safety for recentlySentMessages
|
||||||
private var lastMessageFromPeer: [String: Date] = [:] // Track last message time from each peer for connection prioritization
|
private var lastMessageFromPeer: [String: Date] = [:] // Track last message time from each peer for connection prioritization
|
||||||
|
|
||||||
// Battery and range optimizations
|
// Battery and range optimizations
|
||||||
@@ -490,6 +491,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendMessage(_ content: String, mentions: [String] = [], channel: String? = nil, to recipientID: String? = nil) {
|
func sendMessage(_ content: String, mentions: [String] = [], channel: String? = nil, to recipientID: String? = nil) {
|
||||||
|
// Defensive check for empty content
|
||||||
|
guard !content.isEmpty else { return }
|
||||||
|
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
@@ -532,12 +536,21 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Track this message to prevent duplicate sends
|
// Track this message to prevent duplicate sends
|
||||||
let msgID = "\(packet.timestamp)-\(self.myPeerID)-\(packet.payload.prefix(32).hashValue)"
|
let msgID = "\(packet.timestamp)-\(self.myPeerID)-\(packet.payload.prefix(32).hashValue)"
|
||||||
if !self.recentlySentMessages.contains(msgID) {
|
|
||||||
|
self.recentlySentMessagesLock.lock()
|
||||||
|
let shouldSend = !self.recentlySentMessages.contains(msgID)
|
||||||
|
if shouldSend {
|
||||||
self.recentlySentMessages.insert(msgID)
|
self.recentlySentMessages.insert(msgID)
|
||||||
|
}
|
||||||
|
self.recentlySentMessagesLock.unlock()
|
||||||
|
|
||||||
|
if shouldSend {
|
||||||
// Clean up old entries after 10 seconds
|
// Clean up old entries after 10 seconds
|
||||||
self.messageQueue.asyncAfter(deadline: .now() + 10.0) { [weak self] in
|
self.messageQueue.asyncAfter(deadline: .now() + 10.0) { [weak self] in
|
||||||
self?.recentlySentMessages.remove(msgID)
|
guard let self = self else { return }
|
||||||
|
self.recentlySentMessagesLock.lock()
|
||||||
|
self.recentlySentMessages.remove(msgID)
|
||||||
|
self.recentlySentMessagesLock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add random delay before initial send
|
// Add random delay before initial send
|
||||||
@@ -559,6 +572,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
|
|
||||||
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
|
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
|
||||||
|
// Defensive checks
|
||||||
|
guard !content.isEmpty, !recipientPeerID.isEmpty, !recipientNickname.isEmpty else { return }
|
||||||
|
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
@@ -617,7 +633,11 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
|
|
||||||
// Check if recipient is offline and cache if they're a favorite
|
// Check if recipient is offline and cache if they're a favorite
|
||||||
if !self.activePeers.contains(recipientPeerID) {
|
self.activePeersLock.lock()
|
||||||
|
let isRecipientOffline = !self.activePeers.contains(recipientPeerID)
|
||||||
|
self.activePeersLock.unlock()
|
||||||
|
|
||||||
|
if isRecipientOffline {
|
||||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) {
|
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) {
|
||||||
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
||||||
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
||||||
@@ -630,12 +650,21 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Track to prevent duplicate sends
|
// Track to prevent duplicate sends
|
||||||
let msgID = "\(packet.timestamp)-\(self.myPeerID)-\(packet.payload.prefix(32).hashValue)"
|
let msgID = "\(packet.timestamp)-\(self.myPeerID)-\(packet.payload.prefix(32).hashValue)"
|
||||||
if !self.recentlySentMessages.contains(msgID) {
|
|
||||||
|
self.recentlySentMessagesLock.lock()
|
||||||
|
let shouldSend = !self.recentlySentMessages.contains(msgID)
|
||||||
|
if shouldSend {
|
||||||
self.recentlySentMessages.insert(msgID)
|
self.recentlySentMessages.insert(msgID)
|
||||||
|
}
|
||||||
|
self.recentlySentMessagesLock.unlock()
|
||||||
|
|
||||||
|
if shouldSend {
|
||||||
// Clean up after 10 seconds
|
// Clean up after 10 seconds
|
||||||
self.messageQueue.asyncAfter(deadline: .now() + 10.0) { [weak self] in
|
self.messageQueue.asyncAfter(deadline: .now() + 10.0) { [weak self] in
|
||||||
self?.recentlySentMessages.remove(msgID)
|
guard let self = self else { return }
|
||||||
|
self.recentlySentMessagesLock.lock()
|
||||||
|
self.recentlySentMessages.remove(msgID)
|
||||||
|
self.recentlySentMessagesLock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message tracking is now done in ChatViewModel to ensure consistent message IDs
|
// Message tracking is now done in ChatViewModel to ensure consistent message IDs
|
||||||
@@ -798,7 +827,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let sealedBox = try AES.GCM.seal(contentData, using: channelKey)
|
let sealedBox = try AES.GCM.seal(contentData, using: channelKey)
|
||||||
let encryptedData = sealedBox.combined!
|
guard let encryptedData = sealedBox.combined else {
|
||||||
|
// Encryption failed to produce combined data
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Create message with encrypted content
|
// Create message with encrypted content
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ class MessageRetentionService {
|
|||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
// Get documents directory
|
// Get documents directory
|
||||||
documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
guard let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||||
|
fatalError("Unable to access documents directory")
|
||||||
|
}
|
||||||
|
documentsDirectory = docsDir
|
||||||
messagesDirectory = documentsDirectory.appendingPathComponent("Messages", isDirectory: true)
|
messagesDirectory = documentsDirectory.appendingPathComponent("Messages", isDirectory: true)
|
||||||
|
|
||||||
// Create messages directory if it doesn't exist
|
// Create messages directory if it doesn't exist
|
||||||
|
|||||||
Reference in New Issue
Block a user