From e294999044c91738a335e3c0628e89ab93eee1a7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 8 Jan 2024 10:56:01 +0000 Subject: [PATCH 1/6] ios: fix callkit calls via NSE (#3655) * ios: fix callkit calls via NSE * comments * more reliable NSE start * remove public logs, different RTS parameters for NSE * only suspend NSE if we have chat controller, to avoid crashes if suspension attempted without controller created * comments * fix * simplify --- apps/ios/Shared/Model/NSESubscriber.swift | 6 +- apps/ios/Shared/Model/SimpleXAPI.swift | 2 +- apps/ios/Shared/Model/SuspendChat.swift | 23 ++- apps/ios/Shared/SimpleXApp.swift | 6 +- .../Shared/Views/Call/ActiveCallView.swift | 4 +- .../Shared/Views/Call/CallController.swift | 70 +++++---- .../ios/SimpleX NSE/NotificationService.swift | 143 ++++++++++++------ apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 ++--- .../xcschemes/SimpleX (iOS).xcscheme | 2 +- .../xcschemes/SimpleX NSE.xcscheme | 10 +- apps/ios/SimpleXChat/API.swift | 6 +- apps/ios/SimpleXChat/hs_init.c | 14 ++ apps/ios/SimpleXChat/hs_init.h | 2 + 13 files changed, 213 insertions(+), 115 deletions(-) diff --git a/apps/ios/Shared/Model/NSESubscriber.swift b/apps/ios/Shared/Model/NSESubscriber.swift index f52e72bea..a4a5dc815 100644 --- a/apps/ios/Shared/Model/NSESubscriber.swift +++ b/apps/ios/Shared/Model/NSESubscriber.swift @@ -16,13 +16,13 @@ private var nseSubscribers: [UUID:NSESubscriber] = [:] private let SUSPENDING_TIMEOUT: TimeInterval = 2 // timeout should be larger than SUSPENDING_TIMEOUT -func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = DispatchQueue.main, suspended: @escaping (Bool) -> Void) { +func waitNSESuspended(timeout: TimeInterval, suspended: @escaping (Bool) -> Void) { if timeout <= SUSPENDING_TIMEOUT { logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)") } var state = nseStateGroupDefault.get() if case .suspended = state { - dispatchQueue.async { suspended(true) } + DispatchQueue.main.async { suspended(true) } return } let id = UUID() @@ -45,7 +45,7 @@ func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = Disp logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))") suspendedCalled = true nseSubscribers.removeValue(forKey: id) - dispatchQueue.async { suspended(ok) } + DispatchQueue.main.async { suspended(ok) } } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index eff311096..442ba2079 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -403,7 +403,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) { case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode) case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off) default: - logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)") + logger.debug("apiGetNtfToken response: \(String(describing: r))") return (nil, nil, .off) } } diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index 9b03f38f3..0229bff2b 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -19,11 +19,13 @@ let terminationTimeout: Int = 3 // seconds let activationDelay: TimeInterval = 1.5 +let nseSuspendTimeout: TimeInterval = 5 + private func _suspendChat(timeout: Int) { // this is a redundant check to prevent logical errors, like the one fixed in this PR let state = AppChatState.shared.value if !state.canSuspend { - logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)") + logger.error("_suspendChat called, current state: \(state.rawValue)") } else if ChatModel.ok { AppChatState.shared.set(.suspending) apiSuspendChat(timeoutMicroseconds: timeout * 1000000) @@ -134,20 +136,33 @@ func initChatAndMigrate(refreshInvitations: Bool = true) { } } -func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ completion: @escaping () -> Void) { +func startChatForCall() { + logger.debug("DEBUGGING: startChatForCall") + if ChatModel.shared.chatRunning == true { + ChatReceiver.shared.start() + logger.debug("DEBUGGING: startChatForCall: after ChatReceiver.shared.start") + } + if .active != AppChatState.shared.value { + logger.debug("DEBUGGING: startChatForCall: before activateChat") + activateChat() + logger.debug("DEBUGGING: startChatForCall: after activateChat") + } +} + +func startChatAndActivate(_ completion: @escaping () -> Void) { logger.debug("DEBUGGING: startChatAndActivate") if ChatModel.shared.chatRunning == true { ChatReceiver.shared.start() logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start") } - if .active == AppChatState.shared.value { + if case .active = AppChatState.shared.value { completion() } else if nseStateGroupDefault.get().inactive { activate() } else { // setting app state to "activating" to notify NSE that it should suspend setAppState(.activating) - waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in + waitNSESuspended(timeout: nseSuspendTimeout) { ok in if !ok { // if for some reason NSE failed to suspend, // e.g., it crashed previously without setting its state to "suspended", diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index f72ffcaaa..60d1cf725 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -98,12 +98,12 @@ struct SimpleXApp: App { if legacyDatabase, case .documents = dbContainerGroupDefault.get() { dbContainerGroupDefault.set(.documents) setMigrationState(.offer) - logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db") + logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath())*.db") } else { dbContainerGroupDefault.set(.group) setMigrationState(.ready) - logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db") - logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present") + logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath())*.db") + logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not") present") } } diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index e613476a1..a3be2e900 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -38,13 +38,13 @@ struct ActiveCallView: View { } } .onAppear { - logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)") + logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)") AppDelegate.keepScreenOn(true) createWebRTCClient() dismissAllSheets() } .onChange(of: canConnectCall) { _ in - logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)") + logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)") createWebRTCClient() } .onDisappear { diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index fcd3a8558..6da8294ef 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -130,7 +130,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse // The delay allows to accept the second call before suspending a chat // see `.onChange(of: scenePhase)` in SimpleXApp DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in - logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)") + logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat))") if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true { self?.shouldSuspendChat = false suspendChat() @@ -142,45 +142,57 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse @objc(pushRegistry:didUpdatePushCredentials:forType:) func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { - logger.debug("CallController: didUpdate push credentials for type \(type.rawValue, privacy: .public)") + logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)") } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { - logger.debug("CallController: did receive push with type \(type.rawValue, privacy: .public)") + logger.debug("CallController: did receive push with type \(type.rawValue)") if type != .voIP { completion() return } - logger.debug("CallController: initializing chat") - if (!ChatModel.shared.chatInitialized) { - initChatAndMigrate(refreshInvitations: false) + if AppChatState.shared.value == .stopped { + self.reportExpiredCall(payload: payload, completion) + return } - startChatAndActivate(dispatchQueue: DispatchQueue.global()) { - self.shouldSuspendChat = true - // There are no invitations in the model, as it was processed by NSE - _ = try? justRefreshCallInvitations() - // logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))") - // Extract the call information from the push notification payload - let m = ChatModel.shared - if let contactId = payload.dictionaryPayload["contactId"] as? String, - let invitation = m.callInvitations[contactId] { + if (!ChatModel.shared.chatInitialized) { + logger.debug("CallController: initializing chat") + do { + try initializeChat(start: true, refreshInvitations: false) + } catch let error { + logger.error("CallController: initializing chat error: \(error)") + self.reportExpiredCall(payload: payload, completion) + return + } + } + logger.debug("CallController: initialized chat") + startChatForCall() + logger.debug("CallController: started chat") + self.shouldSuspendChat = true + // There are no invitations in the model, as it was processed by NSE + _ = try? justRefreshCallInvitations() + logger.debug("CallController: updated call invitations chat") + // logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))") + // Extract the call information from the push notification payload + let m = ChatModel.shared + if let contactId = payload.dictionaryPayload["contactId"] as? String, + let invitation = m.callInvitations[contactId] { + let update = self.cxCallUpdate(invitation: invitation) + if let uuid = invitation.callkitUUID { + logger.debug("CallController: report pushkit call via CallKit") let update = self.cxCallUpdate(invitation: invitation) - if let uuid = invitation.callkitUUID { - logger.debug("CallController: report pushkit call via CallKit") - let update = self.cxCallUpdate(invitation: invitation) - self.provider.reportNewIncomingCall(with: uuid, update: update) { error in - if error != nil { - m.callInvitations.removeValue(forKey: contactId) - } - // Tell PushKit that the notification is handled. - completion() + self.provider.reportNewIncomingCall(with: uuid, update: update) { error in + if error != nil { + m.callInvitations.removeValue(forKey: contactId) } - } else { - self.reportExpiredCall(update: update, completion) + // Tell PushKit that the notification is handled. + completion() } } else { - self.reportExpiredCall(payload: payload, completion) + self.reportExpiredCall(update: update, completion) } + } else { + self.reportExpiredCall(payload: payload, completion) } } @@ -211,7 +223,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) { - logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)") + logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))") if CallController.useCallKit(), let uuid = invitation.callkitUUID { if invitation.callTs.timeIntervalSinceNow >= -180 { let update = cxCallUpdate(invitation: invitation) @@ -351,7 +363,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) { controller.request(CXTransaction(action: action)) { error in if let error = error { - logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription, privacy: .public)") + logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)") } else { logger.debug("CallController.requestTransaction requested transaction successfully") onSuccess() diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index f9b4852e5..5b1988e89 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -16,9 +16,11 @@ let logger = Logger() let appSuspendingDelay: UInt64 = 2_500_000_000 -let nseSuspendDelay: TimeInterval = 2 +typealias SuspendSchedule = (delay: TimeInterval, timeout: Int) -let nseSuspendTimeout: Int = 5 +let nseSuspendSchedule: SuspendSchedule = (2, 4) + +let fastNSESuspendSchedule: SuspendSchedule = (1, 1) typealias NtfStream = ConcurrentQueue @@ -32,7 +34,7 @@ actor PendingNtfs { private var ntfStreams: [String: NtfStream] = [:] func createStream(_ id: String) async { - logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)") + logger.debug("NotificationService PendingNtfs.createStream: \(id)") if ntfStreams[id] == nil { ntfStreams[id] = ConcurrentQueue() logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue") @@ -40,14 +42,14 @@ actor PendingNtfs { } func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async { - logger.debug("NotificationService PendingNtfs.readStream: \(id, privacy: .public) \(ntfInfo.ntfMessages.count, privacy: .public)") + logger.debug("NotificationService PendingNtfs.readStream: \(id) \(ntfInfo.ntfMessages.count)") if !ntfInfo.user.showNotifications { nse.setBestAttemptNtf(.empty) } if let s = ntfStreams[id] { logger.debug("NotificationService PendingNtfs.readStream: has stream") var expected = Set(ntfInfo.ntfMessages.map { $0.msgId }) - logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected, privacy: .public)") + logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected)") var readCancelled = false var dequeued: DequeueElement? nse.cancelRead = { @@ -66,7 +68,7 @@ actor PendingNtfs { } else if case let .msgInfo(info) = ntf { let found = expected.remove(info.msgId) if found != nil { - logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty, privacy: .public)") + logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty)") if expected.isEmpty { break } } else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs { logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo") @@ -88,7 +90,7 @@ actor PendingNtfs { } func writeStream(_ id: String, _ ntf: NSENotification) async { - logger.debug("NotificationService PendingNtfs.writeStream: \(id, privacy: .public)") + logger.debug("NotificationService PendingNtfs.writeStream: \(id)") if let s = ntfStreams[id] { logger.debug("NotificationService PendingNtfs.writeStream: writing ntf") s.enqueue(ntf) @@ -208,7 +210,7 @@ class NotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler registerGroupDefaults() let appState = appStateGroupDefault.get() - logger.debug("NotificationService: app is \(appState.rawValue, privacy: .public)") + logger.debug("NotificationService: app is \(appState.rawValue)") switch appState { case .stopped: setBadgeCount() @@ -238,7 +240,7 @@ class NotificationService: UNNotificationServiceExtension { } } } - logger.debug("NotificationService: app state is now \(state.rawValue, privacy: .public)") + logger.debug("NotificationService: app state is now \(state.rawValue)") if state.inactive { receiveNtfMessages(request, contentHandler) } else { @@ -267,7 +269,7 @@ class NotificationService: UNNotificationServiceExtension { let dbStatus = startChat() if case .ok = dbStatus, let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)") + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count))") if let connEntity = ntfInfo.connEntity_ { setBestAttemptNtf( ntfInfo.ntfsEnabled @@ -279,7 +281,7 @@ class NotificationService: UNNotificationServiceExtension { NtfStreamSemaphores.shared.waitForStream(id) if receiveEntityId != nil { Task { - logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)") + logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id)") await PendingNtfs.shared.createStream(id) await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo) deliverBestAttemptNtf() @@ -297,7 +299,7 @@ class NotificationService: UNNotificationServiceExtension { override func serviceExtensionTimeWillExpire() { logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire") - deliverBestAttemptNtf() + deliverBestAttemptNtf(urgent: true) } func setBadgeCount() { @@ -319,7 +321,7 @@ class NotificationService: UNNotificationServiceExtension { } } - private func deliverBestAttemptNtf() { + private func deliverBestAttemptNtf(urgent: Bool = false) { logger.debug("NotificationService.deliverBestAttemptNtf") if let cancel = cancelRead { cancelRead = nil @@ -329,20 +331,55 @@ class NotificationService: UNNotificationServiceExtension { receiveEntityId = nil NtfStreamSemaphores.shared.signalStreamReady(id) } + let suspend: Bool if let t = threadId { threadId = nil - if NSEThreads.shared.endThread(t) { - logger.debug("NotificationService.deliverBestAttemptNtf: will suspend") - // suspension is delayed to allow chat core finalise any processing - // (e.g., send delivery receipts) - DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) { - if NSEThreads.shared.noThreads { - logger.debug("NotificationService.deliverBestAttemptNtf: suspending...") - suspendChat(nseSuspendTimeout) + suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads + } else { + suspend = false + } + deliverCallkitOrNotification(urgent: urgent, suspend: suspend) + } + + private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) { + if case .callkit = bestAttemptNtf { + logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit") + if urgent { + // suspending NSE even though there may be other notifications + // to allow the app to process callkit call + suspendChat(0) + deliverNotification() + } else { + // suspending NSE with delay and delivering after the suspension + // because pushkit notification must be processed without delay + // to avoid app termination + DispatchQueue.global().asyncAfter(deadline: .now() + fastNSESuspendSchedule.delay) { + suspendChat(fastNSESuspendSchedule.timeout) + DispatchQueue.global().asyncAfter(deadline: .now() + Double(fastNSESuspendSchedule.timeout)) { + self.deliverNotification() } } } + } else { + if suspend { + logger.debug("NotificationService.deliverCallkitOrNotification: will suspend") + if urgent { + suspendChat(0) + } else { + // suspension is delayed to allow chat core finalise any processing + // (e.g., send delivery receipts) + DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendSchedule.delay) { + if NSEThreads.shared.noThreads { + suspendChat(nseSuspendSchedule.timeout) + } + } + } + } + deliverNotification() } + } + + private func deliverNotification() { if let handler = contentHandler, let ntf = bestAttemptNtf { contentHandler = nil bestAttemptNtf = nil @@ -357,17 +394,14 @@ class NotificationService: UNNotificationServiceExtension { switch ntf { case let .nse(content): deliver(content) case let .callkit(invitation): + logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)") CXProvider.reportNewIncomingVoIPPushPayload([ "displayName": invitation.contact.displayName, "contactId": invitation.contact.id, "media": invitation.callType.media.rawValue ]) { error in - if error == nil { - deliver(nil) - } else { - logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)") - deliver(createCallInvitationNtf(invitation)) - } + logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") + deliver(error == nil ? nil : createCallInvitationNtf(invitation)) } case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo @@ -402,14 +436,14 @@ var appSubscriber: AppSubscriber = appStateSubscriber { state in logger.debug("NotificationService: appSubscriber") if state.running && NSEChatState.shared.value.canSuspend { logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending") - suspendChat(nseSuspendTimeout) + suspendChat(fastNSESuspendSchedule.timeout) } } func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber { appMessageSubscriber { msg in if case let .state(state) = msg { - logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)") + logger.debug("NotificationService: appStateSubscriber \(state.rawValue)") onState(state) } } @@ -425,23 +459,31 @@ let xftpConfig: XFTPFileConfig? = getXFTPCfg() // Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active func startChat() -> DBMigrationResult? { logger.debug("NotificationService: startChat") - if case .active = NSEChatState.shared.value { return .ok } + // only skip creating if there is chat controller + if case .active = NSEChatState.shared.value, hasChatCtrl() { return .ok } startLock.wait() defer { startLock.signal() } - return switch NSEChatState.shared.value { - case .created: doStartChat() - case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock - case .active: .ok - case .suspending: activateChat() - case .suspended: activateChat() + if hasChatCtrl() { + return switch NSEChatState.shared.value { + case .created: doStartChat() + case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock + case .active: .ok + case .suspending: activateChat() + case .suspended: activateChat() + } + } else { + // Ignore state in preference if there is no chat controller. + // State in preference may have failed to update e.g. because of a crash. + NSEChatState.shared.set(.created) + return doStartChat() } } func doStartChat() -> DBMigrationResult? { logger.debug("NotificationService: doStartChat") - hs_init(0, nil) + haskell_init_nse() let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true) if dbStatus != .ok { resetChatCtrl() @@ -477,7 +519,7 @@ func doStartChat() -> DBMigrationResult? { return .ok } } catch { - logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)") + logger.error("NotificationService startChat error: \(responseError(error))") } } else { logger.debug("NotificationService: no active user") @@ -504,8 +546,10 @@ func suspendChat(_ timeout: Int) { logger.debug("NotificationService: suspendChat") let state = NSEChatState.shared.value if !state.canSuspend { - logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)") - } else { + logger.error("NotificationService suspendChat called, current state: \(state.rawValue)") + } else if hasChatCtrl() { + // only suspend if we have chat controller to avoid crashes when suspension is + // attempted when chat controller was not created suspendLock.wait() defer { suspendLock.signal() } @@ -571,7 +615,7 @@ private let isInChina = SKStorefront().countryCode == "CHN" private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { - logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)") + logger.debug("NotificationService receivedMsgNtf: \(res.responseType)") switch res { case let .contactConnected(user, contact, _): return (contact.id, .nse(createContactConnectedNtf(user, contact))) @@ -613,6 +657,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { case .chatSuspended: chatSuspended() return nil + case let .chatError(_, err): + logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))") + return nil default: logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)") return nil @@ -627,17 +674,22 @@ func updateNetCfg() { try setNetworkConfig(networkConfig) networkConfig = newNetConfig } catch { - logger.error("NotificationService apply changed network config error: \(responseError(error), privacy: .public)") + logger.error("NotificationService apply changed network config error: \(responseError(error))") } } } func apiGetActiveUser() -> User? { let r = sendSimpleXCmd(.showActiveUser) - logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))") + logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)") switch r { case let .activeUser(user): return user - case .chatCmdError(_, .error(.noActiveUser)): return nil + case .chatCmdError(_, .error(.noActiveUser)): + logger.debug("apiGetActiveUser sendSimpleXCmd no active user") + return nil + case let .chatCmdError(_, err): + logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))") + return nil default: logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))") return nil @@ -699,11 +751,12 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { } let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user { + logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessages.count)") return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages) } else if case let .chatCmdError(_, error) = r { logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") } else { - logger.debug("apiGetNtfMessage ignored response: \(r.responseType, privacy: .public) \(String.init(describing: r), privacy: .private)") + logger.debug("apiGetNtfMessage ignored response: \(r.responseType) \(String.init(describing: r))") } return nil } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 801116bf8..c0d4d99a3 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -43,11 +43,11 @@ 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; }; 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; }; 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; }; - 5C4E80DA2B3CCD090080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D52B3CCD090080FAE2 /* libgmp.a */; }; - 5C4E80DB2B3CCD090080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D62B3CCD090080FAE2 /* libffi.a */; }; - 5C4E80DC2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */; }; - 5C4E80DD2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */; }; - 5C4E80DE2B3CCD090080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */; }; + 5C4E80EE2B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E92B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a */; }; + 5C4E80EF2B4991300080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80EA2B4991300080FAE2 /* libgmpxx.a */; }; + 5C4E80F02B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80EB2B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U-ghc9.6.3.a */; }; + 5C4E80F12B4991300080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80EC2B4991300080FAE2 /* libgmp.a */; }; + 5C4E80F22B4991300080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80ED2B4991300080FAE2 /* libffi.a */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; }; 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; @@ -294,11 +294,11 @@ 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = ""; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; 5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = ""; }; - 5C4E80D52B3CCD090080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C4E80D62B3CCD090080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a"; sourceTree = ""; }; - 5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a"; sourceTree = ""; }; - 5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C4E80E92B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a"; sourceTree = ""; }; + 5C4E80EA2B4991300080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C4E80EB2B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U-ghc9.6.3.a"; sourceTree = ""; }; + 5C4E80EC2B4991300080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C4E80ED2B4991300080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = ""; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = ""; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; }; @@ -519,13 +519,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C4E80DD2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a in Frameworks */, + 5C4E80EF2B4991300080FAE2 /* libgmpxx.a in Frameworks */, + 5C4E80EE2B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C4E80DA2B3CCD090080FAE2 /* libgmp.a in Frameworks */, - 5C4E80DC2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C4E80DB2B3CCD090080FAE2 /* libffi.a in Frameworks */, - 5C4E80DE2B3CCD090080FAE2 /* libgmpxx.a in Frameworks */, + 5C4E80F22B4991300080FAE2 /* libffi.a in Frameworks */, + 5C4E80F12B4991300080FAE2 /* libgmp.a in Frameworks */, + 5C4E80F02B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U-ghc9.6.3.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -587,11 +587,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C4E80D62B3CCD090080FAE2 /* libffi.a */, - 5C4E80D52B3CCD090080FAE2 /* libgmp.a */, - 5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */, - 5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */, - 5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */, + 5C4E80ED2B4991300080FAE2 /* libffi.a */, + 5C4E80EC2B4991300080FAE2 /* libgmp.a */, + 5C4E80EA2B4991300080FAE2 /* libgmpxx.a */, + 5C4E80EB2B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U-ghc9.6.3.a */, + 5C4E80E92B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme index 973c30c71..6a1d4192e 100644 --- a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme @@ -41,7 +41,7 @@ + version = "1.3"> @@ -47,16 +47,14 @@ + allowLocationSimulation = "YES"> chat_ctrl { +public func hasChatCtrl() -> Bool { + chatController != nil +} + +public func getChatCtrl() -> chat_ctrl { if let controller = chatController { return controller } fatalError("chat controller not initialized") } diff --git a/apps/ios/SimpleXChat/hs_init.c b/apps/ios/SimpleXChat/hs_init.c index 7a5ea2456..b597453be 100644 --- a/apps/ios/SimpleXChat/hs_init.c +++ b/apps/ios/SimpleXChat/hs_init.c @@ -23,3 +23,17 @@ void haskell_init(void) { char **pargv = argv; hs_init_with_rtsopts(&argc, &pargv); } + +void haskell_init_nse(void) { + int argc = 5; + char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A1m", // chunk size for new allocations + "-H1m", // initial heap size + "-xn", // non-moving GC + 0 + }; + char **pargv = argv; + hs_init_with_rtsopts(&argc, &pargv); +} diff --git a/apps/ios/SimpleXChat/hs_init.h b/apps/ios/SimpleXChat/hs_init.h index 48850e819..a732fd711 100644 --- a/apps/ios/SimpleXChat/hs_init.h +++ b/apps/ios/SimpleXChat/hs_init.h @@ -11,4 +11,6 @@ void haskell_init(void); +void haskell_init_nse(void); + #endif /* hs_init_h */ From 3ccd9903a755f0e592f3903ba65435f921ffa975 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 8 Jan 2024 12:53:16 +0000 Subject: [PATCH 2/6] core: do not start clean up manager in background NSE (#3657) * core: do not start clean up manager in background NSE * update UIs * fix test --- apps/ios/Shared/Model/SimpleXAPI.swift | 2 +- .../ios/SimpleX NSE/NotificationService.swift | 2 +- apps/ios/SimpleXChat/APITypes.swift | 4 ++-- .../chat/simplex/common/model/SimpleXAPI.kt | 6 ++--- src/Simplex/Chat.hs | 22 +++++++++---------- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Core.hs | 2 +- tests/ChatTests/Direct.hs | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 442ba2079..6a5de6b25 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -211,7 +211,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn } func apiStartChat() throws -> Bool { - let r = chatSendCmdSync(.startChat(subscribe: true, expire: true, xftp: true)) + let r = chatSendCmdSync(.startChat(mainApp: true)) switch r { case .chatStarted: return true case .chatRunning: return false diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 5b1988e89..529192423 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -697,7 +697,7 @@ func apiGetActiveUser() -> User? { } func apiStartChat() throws -> Bool { - let r = sendSimpleXCmd(.startChat(subscribe: false, expire: false, xftp: false)) + let r = sendSimpleXCmd(.startChat(mainApp: false)) switch r { case .chatStarted: return true case .chatRunning: return false diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index a199966ba..411a1ab9c 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -25,7 +25,7 @@ public enum ChatCommand { case apiMuteUser(userId: Int64) case apiUnmuteUser(userId: Int64) case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?) - case startChat(subscribe: Bool, expire: Bool, xftp: Bool) + case startChat(mainApp: Bool) case apiStopChat case apiActivateChat(restoreChat: Bool) case apiSuspendChat(timeoutMicroseconds: Int) @@ -154,7 +154,7 @@ public enum ChatCommand { case let .apiMuteUser(userId): return "/_mute user \(userId)" case let .apiUnmuteUser(userId): return "/_unmute user \(userId)" case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))" - case let .startChat(subscribe, expire, xftp): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire)) xftp=\(onOff(xftp))" + case let .startChat(mainApp): return "/_start main=\(onOff(mainApp))" case .apiStopChat: return "/_stop" case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 4af3e3f2e..38f47b8b2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -583,7 +583,7 @@ object ChatController { } suspend fun apiStartChat(): Boolean { - val r = sendCmd(null, CC.StartChat(expire = true)) + val r = sendCmd(null, CC.StartChat(mainApp = true)) when (r) { is CR.ChatStarted -> return true is CR.ChatRunning -> return false @@ -2161,7 +2161,7 @@ sealed class CC { class ApiMuteUser(val userId: Long): CC() class ApiUnmuteUser(val userId: Long): CC() class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC() - class StartChat(val expire: Boolean): CC() + class StartChat(val mainApp: Boolean): CC() class ApiStopChat: CC() class SetTempFolder(val tempFolder: String): CC() class SetFilesFolder(val filesFolder: String): CC() @@ -2288,7 +2288,7 @@ sealed class CC { is ApiMuteUser -> "/_mute user $userId" is ApiUnmuteUser -> "/_unmute user $userId" is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}" - is StartChat -> "/_start subscribe=on expire=${onOff(expire)} xftp=on" + is StartChat -> "/_start main=${onOff(mainApp)}" is ApiStopChat -> "/_stop" is SetTempFolder -> "/_temp_folder $tempFolder" is SetFilesFolder -> "/_files_folder $filesFolder" diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index eb16a492e..2feb51fc1 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -311,10 +311,10 @@ cfgServers p s = case p of SPSMP -> s.smp SPXFTP -> s.xftp -startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ()) -startChatController subConns enableExpireCIs startXFTPWorkers = do +startChatController :: forall m. ChatMonad' m => Bool -> m (Async ()) +startChatController mainApp = do asks smpAgent >>= resumeAgentClient - unless subConns $ + unless mainApp $ chatWriteVar subscriptionMode SMOnlyCreate users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers) restoreCalls @@ -324,15 +324,15 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do start s users = do a1 <- async agentSubscriber a2 <- - if subConns + if mainApp then Just <$> async (subscribeUsers False users) else pure Nothing atomically . writeTVar s $ Just (a1, a2) - when startXFTPWorkers $ do + when mainApp $ do startXFTP void $ forkIO $ startFilesToReceive users - startCleanupManager - when enableExpireCIs $ startExpireCIs users + startCleanupManager + startExpireCIs users pure a1 startXFTP = do tmp <- readTVarIO =<< asks tempDirectory @@ -544,10 +544,10 @@ processChatCommand' vr = \case checkDeleteChatUser user' withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ - StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \_ -> + StartChat mainApp -> withUser' $ \_ -> asks agentAsync >>= readTVarIO >>= \case Just _ -> pure CRChatRunning - _ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted + _ -> checkStoreNotChanged $ startChatController mainApp $> CRChatStarted APIStopChat -> do ask >>= stopChatController pure CRChatStopped @@ -6153,8 +6153,8 @@ chatCommandP = "/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)), "/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)), ("/user" <|> "/u") $> ShowActiveUser, - "/_start subscribe=" *> (StartChat <$> onOffP <* " expire=" <*> onOffP <* " xftp=" <*> onOffP), - "/_start" $> StartChat True True True, + "/_start main=" *> (StartChat <$> onOffP), + "/_start" $> StartChat True, "/_stop" $> APIStopChat, "/_app activate restore=" *> (APIActivateChat <$> onOffP), "/_app activate" $> APIActivateChat True, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b198cccbf..52181b3c9 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -233,7 +233,7 @@ data ChatCommand | UnmuteUser | APIDeleteUser UserId Bool (Maybe UserPwd) | DeleteUser UserName Bool (Maybe UserPwd) - | StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool, startXFTPWorkers :: Bool} + | StartChat {mainApp :: Bool} | APIStopChat | APIActivateChat {restoreChat :: Bool} | APISuspendChat {suspendTimeout :: Int} diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 1d870bf38..0444723de 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -35,7 +35,7 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController runSimplexChat ChatOpts {maintenance} u cc chat | maintenance = wait =<< async (chat u cc) | otherwise = do - a1 <- runReaderT (startChatController True True True) cc + a1 <- runReaderT (startChatController True) cc a2 <- async $ chat u cc waitEither_ a1 a2 diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 64fa6ff3b..a53bbff3d 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1149,7 +1149,7 @@ testSubscribeAppNSE tmp = alice ##> "/_app suspend 1" alice <## "ok" alice <## "chat suspended" - nseAlice ##> "/_start subscribe=off expire=off xftp=off" + nseAlice ##> "/_start main=off" nseAlice <## "chat started" nseAlice ##> "/ad" cLink <- getContactLink nseAlice True From 58ad97fe6d9f83a9c53fff412e405ee91b2de36e Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:28:01 +0400 Subject: [PATCH 3/6] core: pause cleanup when chat is suspended (#3658) --- src/Simplex/Chat.hs | 25 +++++++++++++++++++------ src/Simplex/Chat/Controller.hs | 1 + 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2feb51fc1..8fbd978c5 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -234,6 +234,7 @@ newChatController expireCIFlags <- newTVarIO M.empty cleanupManagerAsync <- newTVarIO Nothing timedItemThreads <- atomically TM.empty + chatActivated <- newTVarIO (not backgroundMode) showLiveItems <- newTVarIO False encryptLocalFiles <- newTVarIO False userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg @@ -269,6 +270,7 @@ newChatController expireCIFlags, cleanupManagerAsync, timedItemThreads, + chatActivated, showLiveItems, encryptLocalFiles, userXFTPFileConfig, @@ -554,6 +556,7 @@ processChatCommand' vr = \case APIActivateChat restoreChat -> withUser $ \_ -> do when restoreChat restoreCalls withAgent foregroundAgent + chatWriteVar chatActivated True when restoreChat $ do users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers void . forkIO $ subscribeUsers True users @@ -561,6 +564,7 @@ processChatCommand' vr = \case setAllExpireCIFlags True ok_ APISuspendChat t -> do + chatWriteVar chatActivated False setAllExpireCIFlags False stopRemoteCtrl withAgent (`suspendAgent` t) @@ -2479,6 +2483,7 @@ startExpireCIThread user@User {userId} = do flip catchChatError (toView . CRChatError (Just user)) $ do expireFlags <- asks expireCIFlags atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry + waitChatStartedAndActivated ttl <- withStoreCtx' (Just "startExpireCIThread, getChatItemTTL") (`getChatItemTTL` user) forM_ ttl $ \t -> expireChatItems user t False liftIO $ threadDelay' interval @@ -2972,7 +2977,7 @@ cleanupManager = do stepDelay <- asks (cleanupManagerStepDelay . config) forever $ do flip catchChatError (toView . CRChatError Nothing) $ do - waitChatStarted + waitChatStartedAndActivated users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers let (us, us') = partition activeUser users forM_ us $ cleanupUser interval stepDelay @@ -2982,7 +2987,7 @@ cleanupManager = do liftIO $ threadDelay' $ diffToMicroseconds interval where runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do - waitChatStarted + waitChatStartedAndActivated users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers let (us, us') = partition activeUser users forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u)) @@ -3037,7 +3042,7 @@ deleteTimedItem :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do ts <- liftIO getCurrentTime liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts - waitChatStarted + waitChatStartedAndActivated vr <- chatVersionRange case cType of CTDirect -> do @@ -3063,8 +3068,10 @@ expireChatItems user@User {userId} ttl sync = do let expirationDate = addUTCTime (-1 * fromIntegral ttl) currentTs -- this is to keep group messages created during last 12 hours even if they're expired according to item_ts createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs + waitChatStartedAndActivated contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user) loop contacts $ processContact expirationDate + waitChatStartedAndActivated groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (\db -> getUserGroupDetails db vr user Nothing Nothing) loop groups $ processGroup expirationDate createdAtCutoff where @@ -3083,11 +3090,13 @@ expireChatItems user@User {userId} ttl sync = do when (expire == Just True) $ threadDelay 100000 >> a processContact :: UTCTime -> Contact -> m () processContact expirationDate ct = do + waitChatStartedAndActivated filesInfo <- withStoreCtx' (Just "processContact, getContactExpiredFileInfo") $ \db -> getContactExpiredFileInfo db user ct expirationDate deleteFilesAndConns user filesInfo withStoreCtx' (Just "processContact, deleteContactExpiredCIs") $ \db -> deleteContactExpiredCIs db user ct expirationDate processGroup :: UTCTime -> UTCTime -> GroupInfo -> m () processGroup expirationDate createdAtCutoff gInfo = do + waitChatStartedAndActivated filesInfo <- withStoreCtx' (Just "processGroup, getGroupExpiredFileInfo") $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff deleteFilesAndConns user filesInfo withStoreCtx' (Just "processGroup, deleteGroupExpiredCIs") $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff @@ -6113,10 +6122,14 @@ checkSameUser userId User {userId = activeUserId} = when (userId /= activeUserId chatStarted :: ChatMonad m => m Bool chatStarted = fmap isJust . readTVarIO =<< asks agentAsync -waitChatStarted :: ChatMonad m => m () -waitChatStarted = do +waitChatStartedAndActivated :: ChatMonad m => m () +waitChatStartedAndActivated = do agentStarted <- asks agentAsync - atomically $ readTVar agentStarted >>= \a -> unless (isJust a) retry + chatActivated <- asks chatActivated + atomically $ do + started <- readTVar agentStarted + activated <- readTVar chatActivated + unless (isJust started && activated) retry chatVersionRange :: ChatMonad' m => m VersionRange chatVersionRange = do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 52181b3c9..faf3a21a0 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -200,6 +200,7 @@ data ChatController = ChatController expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIFlags :: TMap UserId Bool, cleanupManagerAsync :: TVar (Maybe (Async ())), + chatActivated :: TVar Bool, timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), showLiveItems :: TVar Bool, encryptLocalFiles :: TVar Bool, From fadce0c1408379d94b49398718afaefd3c8d7ed4 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:34:10 +0400 Subject: [PATCH 4/6] core: create new chat controller with chatActivated set to true --- src/Simplex/Chat.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 8fbd978c5..3d25ba7cf 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -234,7 +234,7 @@ newChatController expireCIFlags <- newTVarIO M.empty cleanupManagerAsync <- newTVarIO Nothing timedItemThreads <- atomically TM.empty - chatActivated <- newTVarIO (not backgroundMode) + chatActivated <- newTVarIO True showLiveItems <- newTVarIO False encryptLocalFiles <- newTVarIO False userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg From 267178dddb07d1da477a6372d5390aec07dcf31a Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 9 Jan 2024 01:20:52 +0700 Subject: [PATCH 5/6] android, desktop: show alerts on critical and internal errors (#3653) * android, desktop: show alerts on critical and internal errors * test * don't stop chat if it's stopped already * show notification * restart chat or app * Revert "test" This reverts commit 5b78bbae5bbe6076ab96bcc4ff937d07d61b2c73. * update strings * strings2 * refactoring * refactoring2 * refactoring3 --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../main/java/chat/simplex/app/SimplexApp.kt | 1 + .../simplex/app/model/NtfManager.android.kt | 32 ++++++++++ .../views/database/DatabaseView.android.kt | 7 ++ .../usersettings/SettingsView.android.kt | 2 +- .../chat/simplex/common/model/ChatModel.kt | 3 + .../chat/simplex/common/model/SimpleXAPI.kt | 12 ++++ .../simplex/common/platform/NtfManager.kt | 1 + .../common/views/database/DatabaseView.kt | 14 ++-- .../common/views/helpers/ProcessedErrors.kt | 64 +++++++++++++++++++ .../views/usersettings/DeveloperView.kt | 4 +- .../commonMain/resources/MR/base/strings.xml | 8 +++ .../resources/MR/images/ic_report.svg | 1 + .../common/model/NtfManager.desktop.kt | 4 ++ .../common/platform/AppCommon.desktop.kt | 1 + .../views/database/DatabaseView.desktop.kt | 23 +++++++ 15 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index ee43da5d4..1c99b7377 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -171,6 +171,7 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() override fun cancelCallNotification() = NtfManager.cancelCallNotification() override fun cancelAllNotifications() = NtfManager.cancelAllNotifications() + override fun showMessage(title: String, text: String) = NtfManager.showMessage(title, text) } platform = object : PlatformInterface { override suspend fun androidServiceStart() { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index 916f40df1..7158b82ea 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -208,6 +208,38 @@ object NtfManager { } } + fun showMessage(title: String, text: String) { + val builder = NotificationCompat.Builder(context, MessageChannel) + .setContentTitle(title) + .setContentText(text) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setGroup(MessageGroup) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setSmallIcon(R.drawable.ntf_icon) + .setLargeIcon(null) + .setColor(0x88FFFF) + .setAutoCancel(true) + .setVibrate(null) + .setContentIntent(chatPendingIntent(ShowChatsAction, null, null)) + .setSilent(false) + + val summary = NotificationCompat.Builder(context, MessageChannel) + .setSmallIcon(R.drawable.ntf_icon) + .setColor(0x88FFFF) + .setGroup(MessageGroup) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setGroupSummary(true) + .setContentIntent(chatPendingIntent(ShowChatsAction, null)) + .build() + + with(NotificationManagerCompat.from(context)) { + if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + notify("MESSAGE".hashCode(), builder.build()) + notify(0, summary) + } + } + } + fun cancelCallNotification() { manager.cancel(CallNotificationId) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt new file mode 100644 index 000000000..e392c0999 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt @@ -0,0 +1,7 @@ +package chat.simplex.common.views.database + +import chat.simplex.common.views.usersettings.restartApp + +actual fun restartChatOrApp() { + restartApp() +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt index 49a29cf14..9b376bb9d 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -28,7 +28,7 @@ actual fun SettingsSectionApp( } -private fun restartApp() { +fun restartApp() { ProcessPhoenix.triggerRebirth(androidAppContext) shutdownApp() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 708bbb907..5f1cf5783 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -122,6 +122,9 @@ object ChatModel { val remoteHostPairing = mutableStateOf?>(null) val remoteCtrlSession = mutableStateOf(null) + val processedCriticalError: ProcessedErrors = ProcessedErrors(60_000) + val processedInternalError: ProcessedErrors = ProcessedErrors(20_000) + fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 38f47b8b2..012fce705 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -108,6 +108,7 @@ class AppPreferences { val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) + val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false) val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050") @@ -276,6 +277,7 @@ class AppPreferences { private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" + private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors" private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" @@ -1920,6 +1922,14 @@ object ChatController { } } } + is CR.ChatCmdError -> when { + r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> { + chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart) + } + r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.INTERNAL && appPrefs.showInternalErrors.get() -> { + chatModel.processedInternalError.newError(r.chatError.agentError, false) + } + } else -> Log.d(TAG , "unsupported event: ${r.responseType}") } @@ -4710,6 +4720,7 @@ sealed class AgentErrorType { is AGENT -> "AGENT ${agentErr.string}" is INTERNAL -> "INTERNAL $internalErr" is INACTIVE -> "INACTIVE" + is CRITICAL -> "CRITICAL $offerRestart $criticalErr" } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() @@ -4721,6 +4732,7 @@ sealed class AgentErrorType { @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() + @Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType() } @Serializable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 6ca065086..5c57a48c8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -99,6 +99,7 @@ abstract class NtfManager { abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List Unit>> = emptyList()) abstract fun cancelCallNotification() abstract fun cancelAllNotifications() + abstract fun showMessage(title: String, text: String) // Android only abstract fun androidCreateNtfChannelsMaybeShowAlert() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 224317f94..2251d890f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -4,7 +4,6 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionTextFooter import SectionItemView -import SectionSpacer import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* @@ -367,7 +366,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive) } -private fun startChat(m: ChatModel, chatLastStart: MutableState, chatDbChanged: MutableState) { +fun startChat(m: ChatModel, chatLastStart: MutableState, chatDbChanged: MutableState) { withApi { try { if (chatDbChanged.value) { @@ -407,6 +406,8 @@ private fun stopChatAlert(m: ChatModel) { ) } +expect fun restartChatOrApp() + private fun exportProhibitedAlert() { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.set_password_to_export), @@ -414,7 +415,7 @@ private fun exportProhibitedAlert() { ) } -private fun authStopChat(m: ChatModel) { +fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) { if (m.controller.appPrefs.performLA.get()) { authenticate( generalGetString(MR.strings.auth_stop_chat), @@ -422,7 +423,7 @@ private fun authStopChat(m: ChatModel) { completed = { laResult -> when (laResult) { LAResult.Success, is LAResult.Unavailable -> { - stopChat(m) + stopChat(m, onStop) } is LAResult.Error -> { m.chatRunning.value = true @@ -434,15 +435,16 @@ private fun authStopChat(m: ChatModel) { } ) } else { - stopChat(m) + stopChat(m, onStop) } } -private fun stopChat(m: ChatModel) { +private fun stopChat(m: ChatModel, onStop: (() -> Unit)? = null) { withApi { try { stopChatAsync(m) platform.androidChatStopped() + onStop?.invoke() } catch (e: Error) { m.chatRunning.value = true AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt new file mode 100644 index 000000000..4b44777a3 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt @@ -0,0 +1,64 @@ +package chat.simplex.common.views.helpers + +import chat.simplex.common.model.AgentErrorType +import chat.simplex.common.platform.Log +import chat.simplex.common.platform.TAG +import chat.simplex.common.platform.ntfManager +import chat.simplex.common.views.database.restartChatOrApp +import chat.simplex.res.MR +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay + +class ProcessedErrors (val interval: Long) { + private var lastShownTimestamp: Long = -1 + private var lastShownOfferRestart: Boolean = false + private var timer: Job = Job() + + fun newError(error: T, offerRestart: Boolean) { + timer.cancel() + timer = withBGApi { + val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis() + if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) { + delay(delayBeforeNext) + } + lastShownTimestamp = System.currentTimeMillis() + lastShownOfferRestart = offerRestart + AlertManager.shared.hideAllAlerts() + showMessage(error, offerRestart) + } + } + + private fun showMessage(error: T, offerRestart: Boolean) { + when (error) { + is AgentErrorType.CRITICAL -> { + val title = generalGetString(MR.strings.agent_critical_error_title) + val text = generalGetString(MR.strings.agent_critical_error_desc).format(error.criticalErr) + try { + ntfManager.showMessage(title, text) + } catch (e: Throwable) { + Log.e(TAG, e.stackTraceToString()) + } + if (offerRestart) { + AlertManager.shared.showAlertDialog( + title = title, + text = text, + confirmText = generalGetString(MR.strings.restart_chat_button), + onConfirm = { + withApi { restartChatOrApp() } + }) + } else { + AlertManager.shared.showAlertMsg( + title = title, + text = text, + ) + } + } + is AgentErrorType.INTERNAL -> { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.agent_internal_error_title), + text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr), + ) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index a6ac8c14e..969a6d9d9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -10,10 +10,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import chat.simplex.common.model.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.appPreferences import chat.simplex.common.views.TerminalView import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -44,6 +45,7 @@ fun DeveloperView( m.controller.appPrefs.terminalAlwaysVisible.set(false) } } + SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors) } } SectionTextFooter( diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 4ad40d7a6..d13a7c65f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -662,6 +662,7 @@ Hide: Show developer options Database IDs and Transport isolation option. + Show internal errors Shutdown? Notifications will stop working until you re-launch the app @@ -1724,4 +1725,11 @@ You are already joining the group via this link. You are already in group %1$s. Connect via link? + + + Critical error + Please report it to the developers: \n%s\n\nIt is recommended to restart the app. + Internal error + Please report it to the developers: \n%s + Restart chat \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg new file mode 100644 index 000000000..8695857b9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt index cb34bdb3b..1892b0c7f 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -47,6 +47,10 @@ object NtfManager { } } + fun showMessage(title: String, text: String) { + displayNotificationViaLib("MESSAGE", title, text, null, emptyList()) {} + } + fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId } fun cancelNotificationsForChat(chatId: ChatId) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt index 92111f162..c4dc974da 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -23,6 +23,7 @@ fun initApp() { override fun androidCreateNtfChannelsMaybeShowAlert() {} override fun cancelCallNotification() {} override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications() + override fun showMessage(title: String, text: String) = chat.simplex.common.model.NtfManager.showMessage(title, text) } applyAppLocale() withBGApi { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt new file mode 100644 index 000000000..10e9f9f3c --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt @@ -0,0 +1,23 @@ +package chat.simplex.common.views.database + +import androidx.compose.runtime.mutableStateOf +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.helpers.withApi +import kotlinx.coroutines.delay +import kotlinx.datetime.Instant + +actual fun restartChatOrApp() { + if (chatModel.chatRunning.value == false) { + chatModel.chatDbChanged.value = true + startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged) + } else { + authStopChat(chatModel) { + withApi { + // adding delay in order to prevent locked database by previous initialization + delay(1000) + chatModel.chatDbChanged.value = true + startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged) + } + } + } +} From a2f190a6c6e44fca4324e2e80726f748e2e55b49 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:15:35 +0000 Subject: [PATCH 6/6] core: update simplexmq (better batching) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 3d3002c9a..4f0866c0d 100644 --- a/cabal.project +++ b/cabal.project @@ -14,7 +14,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 55808b0c829fbb13876e99021cf8e6c5cce3ee78 + tag: ca527b4d6cb83d24abdc9cbefcf56c870f694a63 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 4d02fa422..a41ca5b3e 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."55808b0c829fbb13876e99021cf8e6c5cce3ee78" = "02zm73f57lhsmy1wn08hv0mb21al718nk27q6q01vwxg4hhhknhi"; + "https://github.com/simplex-chat/simplexmq.git"."ca527b4d6cb83d24abdc9cbefcf56c870f694a63" = "06547v4n30xbk49c87frnvfbj6pihvxh4nx8rq9idpd8x2kxpyb1"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";