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
This commit is contained in:
Evgeny Poberezkin 2024-01-08 10:56:01 +00:00 committed by GitHub
parent 2bbc687f4a
commit e294999044
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 213 additions and 115 deletions

View File

@ -16,13 +16,13 @@ private var nseSubscribers: [UUID:NSESubscriber] = [:]
private let SUSPENDING_TIMEOUT: TimeInterval = 2 private let SUSPENDING_TIMEOUT: TimeInterval = 2
// timeout should be larger than SUSPENDING_TIMEOUT // 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 { if timeout <= SUSPENDING_TIMEOUT {
logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)") logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)")
} }
var state = nseStateGroupDefault.get() var state = nseStateGroupDefault.get()
if case .suspended = state { if case .suspended = state {
dispatchQueue.async { suspended(true) } DispatchQueue.main.async { suspended(true) }
return return
} }
let id = UUID() let id = UUID()
@ -45,7 +45,7 @@ func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = Disp
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))") logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
suspendedCalled = true suspendedCalled = true
nseSubscribers.removeValue(forKey: id) nseSubscribers.removeValue(forKey: id)
dispatchQueue.async { suspended(ok) } DispatchQueue.main.async { suspended(ok) }
} }
} }

View File

@ -403,7 +403,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode) case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off) case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
default: default:
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)") logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off) return (nil, nil, .off)
} }
} }

View File

@ -19,11 +19,13 @@ let terminationTimeout: Int = 3 // seconds
let activationDelay: TimeInterval = 1.5 let activationDelay: TimeInterval = 1.5
let nseSuspendTimeout: TimeInterval = 5
private func _suspendChat(timeout: Int) { private func _suspendChat(timeout: Int) {
// this is a redundant check to prevent logical errors, like the one fixed in this PR // this is a redundant check to prevent logical errors, like the one fixed in this PR
let state = AppChatState.shared.value let state = AppChatState.shared.value
if !state.canSuspend { 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 { } else if ChatModel.ok {
AppChatState.shared.set(.suspending) AppChatState.shared.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000) 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") logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true { if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start() ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start") logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
} }
if .active == AppChatState.shared.value { if case .active = AppChatState.shared.value {
completion() completion()
} else if nseStateGroupDefault.get().inactive { } else if nseStateGroupDefault.get().inactive {
activate() activate()
} else { } else {
// setting app state to "activating" to notify NSE that it should suspend // setting app state to "activating" to notify NSE that it should suspend
setAppState(.activating) setAppState(.activating)
waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in waitNSESuspended(timeout: nseSuspendTimeout) { ok in
if !ok { if !ok {
// if for some reason NSE failed to suspend, // if for some reason NSE failed to suspend,
// e.g., it crashed previously without setting its state to "suspended", // e.g., it crashed previously without setting its state to "suspended",

View File

@ -98,12 +98,12 @@ struct SimpleXApp: App {
if legacyDatabase, case .documents = dbContainerGroupDefault.get() { if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
dbContainerGroupDefault.set(.documents) dbContainerGroupDefault.set(.documents)
setMigrationState(.offer) 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 { } else {
dbContainerGroupDefault.set(.group) dbContainerGroupDefault.set(.group)
setMigrationState(.ready) setMigrationState(.ready)
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db") logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath())*.db")
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present") logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not") present")
} }
} }

View File

@ -38,13 +38,13 @@ struct ActiveCallView: View {
} }
} }
.onAppear { .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) AppDelegate.keepScreenOn(true)
createWebRTCClient() createWebRTCClient()
dismissAllSheets() dismissAllSheets()
} }
.onChange(of: canConnectCall) { _ in .onChange(of: canConnectCall) { _ in
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)") logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
createWebRTCClient() createWebRTCClient()
} }
.onDisappear { .onDisappear {

View File

@ -130,7 +130,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
// The delay allows to accept the second call before suspending a chat // The delay allows to accept the second call before suspending a chat
// see `.onChange(of: scenePhase)` in SimpleXApp // see `.onChange(of: scenePhase)` in SimpleXApp
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in 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 { if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true {
self?.shouldSuspendChat = false self?.shouldSuspendChat = false
suspendChat() suspendChat()
@ -142,45 +142,57 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
@objc(pushRegistry:didUpdatePushCredentials:forType:) @objc(pushRegistry:didUpdatePushCredentials:forType:)
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { 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) { 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 { if type != .voIP {
completion() completion()
return return
} }
logger.debug("CallController: initializing chat") if AppChatState.shared.value == .stopped {
if (!ChatModel.shared.chatInitialized) { self.reportExpiredCall(payload: payload, completion)
initChatAndMigrate(refreshInvitations: false) return
} }
startChatAndActivate(dispatchQueue: DispatchQueue.global()) { if (!ChatModel.shared.chatInitialized) {
self.shouldSuspendChat = true logger.debug("CallController: initializing chat")
// There are no invitations in the model, as it was processed by NSE do {
_ = try? justRefreshCallInvitations() try initializeChat(start: true, refreshInvitations: false)
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))") } catch let error {
// Extract the call information from the push notification payload logger.error("CallController: initializing chat error: \(error)")
let m = ChatModel.shared self.reportExpiredCall(payload: payload, completion)
if let contactId = payload.dictionaryPayload["contactId"] as? String, return
let invitation = m.callInvitations[contactId] { }
}
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) let update = self.cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID { self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
logger.debug("CallController: report pushkit call via CallKit") if error != nil {
let update = self.cxCallUpdate(invitation: invitation) m.callInvitations.removeValue(forKey: contactId)
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()
} }
} else { // Tell PushKit that the notification is handled.
self.reportExpiredCall(update: update, completion) completion()
} }
} else { } 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) { 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 CallController.useCallKit(), let uuid = invitation.callkitUUID {
if invitation.callTs.timeIntervalSinceNow >= -180 { if invitation.callTs.timeIntervalSinceNow >= -180 {
let update = cxCallUpdate(invitation: invitation) let update = cxCallUpdate(invitation: invitation)
@ -351,7 +363,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) { private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) {
controller.request(CXTransaction(action: action)) { error in controller.request(CXTransaction(action: action)) { error in
if let error = error { 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 { } else {
logger.debug("CallController.requestTransaction requested transaction successfully") logger.debug("CallController.requestTransaction requested transaction successfully")
onSuccess() onSuccess()

View File

@ -16,9 +16,11 @@ let logger = Logger()
let appSuspendingDelay: UInt64 = 2_500_000_000 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<NSENotification> typealias NtfStream = ConcurrentQueue<NSENotification>
@ -32,7 +34,7 @@ actor PendingNtfs {
private var ntfStreams: [String: NtfStream] = [:] private var ntfStreams: [String: NtfStream] = [:]
func createStream(_ id: String) async { func createStream(_ id: String) async {
logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)") logger.debug("NotificationService PendingNtfs.createStream: \(id)")
if ntfStreams[id] == nil { if ntfStreams[id] == nil {
ntfStreams[id] = ConcurrentQueue() ntfStreams[id] = ConcurrentQueue()
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue") logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
@ -40,14 +42,14 @@ actor PendingNtfs {
} }
func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async { 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 { if !ntfInfo.user.showNotifications {
nse.setBestAttemptNtf(.empty) nse.setBestAttemptNtf(.empty)
} }
if let s = ntfStreams[id] { if let s = ntfStreams[id] {
logger.debug("NotificationService PendingNtfs.readStream: has stream") logger.debug("NotificationService PendingNtfs.readStream: has stream")
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId }) 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 readCancelled = false
var dequeued: DequeueElement<NSENotification>? var dequeued: DequeueElement<NSENotification>?
nse.cancelRead = { nse.cancelRead = {
@ -66,7 +68,7 @@ actor PendingNtfs {
} else if case let .msgInfo(info) = ntf { } else if case let .msgInfo(info) = ntf {
let found = expected.remove(info.msgId) let found = expected.remove(info.msgId)
if found != nil { 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 } if expected.isEmpty { break }
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs { } else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo") logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
@ -88,7 +90,7 @@ actor PendingNtfs {
} }
func writeStream(_ id: String, _ ntf: NSENotification) async { 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] { if let s = ntfStreams[id] {
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf") logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
s.enqueue(ntf) s.enqueue(ntf)
@ -208,7 +210,7 @@ class NotificationService: UNNotificationServiceExtension {
self.contentHandler = contentHandler self.contentHandler = contentHandler
registerGroupDefaults() registerGroupDefaults()
let appState = appStateGroupDefault.get() let appState = appStateGroupDefault.get()
logger.debug("NotificationService: app is \(appState.rawValue, privacy: .public)") logger.debug("NotificationService: app is \(appState.rawValue)")
switch appState { switch appState {
case .stopped: case .stopped:
setBadgeCount() 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 { if state.inactive {
receiveNtfMessages(request, contentHandler) receiveNtfMessages(request, contentHandler)
} else { } else {
@ -267,7 +269,7 @@ class NotificationService: UNNotificationServiceExtension {
let dbStatus = startChat() let dbStatus = startChat()
if case .ok = dbStatus, if case .ok = dbStatus,
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { 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_ { if let connEntity = ntfInfo.connEntity_ {
setBestAttemptNtf( setBestAttemptNtf(
ntfInfo.ntfsEnabled ntfInfo.ntfsEnabled
@ -279,7 +281,7 @@ class NotificationService: UNNotificationServiceExtension {
NtfStreamSemaphores.shared.waitForStream(id) NtfStreamSemaphores.shared.waitForStream(id)
if receiveEntityId != nil { if receiveEntityId != nil {
Task { 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.createStream(id)
await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo) await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo)
deliverBestAttemptNtf() deliverBestAttemptNtf()
@ -297,7 +299,7 @@ class NotificationService: UNNotificationServiceExtension {
override func serviceExtensionTimeWillExpire() { override func serviceExtensionTimeWillExpire() {
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire") logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
deliverBestAttemptNtf() deliverBestAttemptNtf(urgent: true)
} }
func setBadgeCount() { func setBadgeCount() {
@ -319,7 +321,7 @@ class NotificationService: UNNotificationServiceExtension {
} }
} }
private func deliverBestAttemptNtf() { private func deliverBestAttemptNtf(urgent: Bool = false) {
logger.debug("NotificationService.deliverBestAttemptNtf") logger.debug("NotificationService.deliverBestAttemptNtf")
if let cancel = cancelRead { if let cancel = cancelRead {
cancelRead = nil cancelRead = nil
@ -329,20 +331,55 @@ class NotificationService: UNNotificationServiceExtension {
receiveEntityId = nil receiveEntityId = nil
NtfStreamSemaphores.shared.signalStreamReady(id) NtfStreamSemaphores.shared.signalStreamReady(id)
} }
let suspend: Bool
if let t = threadId { if let t = threadId {
threadId = nil threadId = nil
if NSEThreads.shared.endThread(t) { suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads
logger.debug("NotificationService.deliverBestAttemptNtf: will suspend") } else {
// suspension is delayed to allow chat core finalise any processing suspend = false
// (e.g., send delivery receipts) }
DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) { deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
if NSEThreads.shared.noThreads { }
logger.debug("NotificationService.deliverBestAttemptNtf: suspending...")
suspendChat(nseSuspendTimeout) 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 { if let handler = contentHandler, let ntf = bestAttemptNtf {
contentHandler = nil contentHandler = nil
bestAttemptNtf = nil bestAttemptNtf = nil
@ -357,17 +394,14 @@ class NotificationService: UNNotificationServiceExtension {
switch ntf { switch ntf {
case let .nse(content): deliver(content) case let .nse(content): deliver(content)
case let .callkit(invitation): case let .callkit(invitation):
logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)")
CXProvider.reportNewIncomingVoIPPushPayload([ CXProvider.reportNewIncomingVoIPPushPayload([
"displayName": invitation.contact.displayName, "displayName": invitation.contact.displayName,
"contactId": invitation.contact.id, "contactId": invitation.contact.id,
"media": invitation.callType.media.rawValue "media": invitation.callType.media.rawValue
]) { error in ]) { error in
if error == nil { logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
deliver(nil) deliver(error == nil ? nil : createCallInvitationNtf(invitation))
} else {
logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
deliver(createCallInvitationNtf(invitation))
}
} }
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet 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 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") logger.debug("NotificationService: appSubscriber")
if state.running && NSEChatState.shared.value.canSuspend { if state.running && NSEChatState.shared.value.canSuspend {
logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending") logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending")
suspendChat(nseSuspendTimeout) suspendChat(fastNSESuspendSchedule.timeout)
} }
} }
func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber { func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber {
appMessageSubscriber { msg in appMessageSubscriber { msg in
if case let .state(state) = msg { if case let .state(state) = msg {
logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)") logger.debug("NotificationService: appStateSubscriber \(state.rawValue)")
onState(state) 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 // Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
func startChat() -> DBMigrationResult? { func startChat() -> DBMigrationResult? {
logger.debug("NotificationService: startChat") 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() startLock.wait()
defer { startLock.signal() } defer { startLock.signal() }
return switch NSEChatState.shared.value { if hasChatCtrl() {
case .created: doStartChat() return switch NSEChatState.shared.value {
case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock case .created: doStartChat()
case .active: .ok case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock
case .suspending: activateChat() case .active: .ok
case .suspended: activateChat() 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? { func doStartChat() -> DBMigrationResult? {
logger.debug("NotificationService: doStartChat") logger.debug("NotificationService: doStartChat")
hs_init(0, nil) haskell_init_nse()
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true) let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
if dbStatus != .ok { if dbStatus != .ok {
resetChatCtrl() resetChatCtrl()
@ -477,7 +519,7 @@ func doStartChat() -> DBMigrationResult? {
return .ok return .ok
} }
} catch { } catch {
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)") logger.error("NotificationService startChat error: \(responseError(error))")
} }
} else { } else {
logger.debug("NotificationService: no active user") logger.debug("NotificationService: no active user")
@ -504,8 +546,10 @@ func suspendChat(_ timeout: Int) {
logger.debug("NotificationService: suspendChat") logger.debug("NotificationService: suspendChat")
let state = NSEChatState.shared.value let state = NSEChatState.shared.value
if !state.canSuspend { if !state.canSuspend {
logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)") logger.error("NotificationService suspendChat called, current state: \(state.rawValue)")
} else { } 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() suspendLock.wait()
defer { suspendLock.signal() } defer { suspendLock.signal() }
@ -571,7 +615,7 @@ private let isInChina = SKStorefront().countryCode == "CHN"
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)") logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
switch res { switch res {
case let .contactConnected(user, contact, _): case let .contactConnected(user, contact, _):
return (contact.id, .nse(createContactConnectedNtf(user, contact))) return (contact.id, .nse(createContactConnectedNtf(user, contact)))
@ -613,6 +657,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
case .chatSuspended: case .chatSuspended:
chatSuspended() chatSuspended()
return nil return nil
case let .chatError(_, err):
logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))")
return nil
default: default:
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)") logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
return nil return nil
@ -627,17 +674,22 @@ func updateNetCfg() {
try setNetworkConfig(networkConfig) try setNetworkConfig(networkConfig)
networkConfig = newNetConfig networkConfig = newNetConfig
} catch { } 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? { func apiGetActiveUser() -> User? {
let r = sendSimpleXCmd(.showActiveUser) let r = sendSimpleXCmd(.showActiveUser)
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))") logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)")
switch r { switch r {
case let .activeUser(user): return user 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: default:
logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))") logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))")
return nil return nil
@ -699,11 +751,12 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
} }
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user { 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) return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages)
} else if case let .chatCmdError(_, error) = r { } else if case let .chatCmdError(_, error) = r {
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
} else { } 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 return nil
} }

View File

@ -43,11 +43,11 @@
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; }; 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; }; 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; }; 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
5C4E80DA2B3CCD090080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D52B3CCD090080FAE2 /* libgmp.a */; }; 5C4E80EE2B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E92B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a */; };
5C4E80DB2B3CCD090080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D62B3CCD090080FAE2 /* libffi.a */; }; 5C4E80EF2B4991300080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80EA2B4991300080FAE2 /* libgmpxx.a */; };
5C4E80DC2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.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 */; };
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 */; }; 5C4E80F12B4991300080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80EC2B4991300080FAE2 /* libgmp.a */; };
5C4E80DE2B3CCD090080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */; }; 5C4E80F22B4991300080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80ED2B4991300080FAE2 /* libffi.a */; };
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; }; 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.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 = "<group>"; }; 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; }; 5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
5C4E80D52B3CCD090080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; }; 5C4E80E92B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a"; sourceTree = "<group>"; };
5C4E80D62B3CCD090080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; }; 5C4E80EA2B4991300080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a"; sourceTree = "<group>"; }; 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 = "<group>"; };
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 = "<group>"; }; 5C4E80EC2B4991300080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; }; 5C4E80ED2B4991300080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
@ -519,13 +519,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( 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 */, 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 */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C4E80DB2B3CCD090080FAE2 /* libffi.a in Frameworks */, 5C4E80F22B4991300080FAE2 /* libffi.a in Frameworks */,
5C4E80DE2B3CCD090080FAE2 /* libgmpxx.a in Frameworks */, 5C4E80F12B4991300080FAE2 /* libgmp.a in Frameworks */,
5C4E80F02B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U-ghc9.6.3.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -587,11 +587,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C4E80D62B3CCD090080FAE2 /* libffi.a */, 5C4E80ED2B4991300080FAE2 /* libffi.a */,
5C4E80D52B3CCD090080FAE2 /* libgmp.a */, 5C4E80EC2B4991300080FAE2 /* libgmp.a */,
5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */, 5C4E80EA2B4991300080FAE2 /* libgmpxx.a */,
5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */, 5C4E80EB2B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U-ghc9.6.3.a */,
5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */, 5C4E80E92B4991300080FAE2 /* libHSsimplex-chat-5.4.2.1-6ax7BlvjLCrBLFyG7Bue3U.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";

View File

@ -41,7 +41,7 @@
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"

View File

@ -2,7 +2,7 @@
<Scheme <Scheme
LastUpgradeVersion = "1400" LastUpgradeVersion = "1400"
wasCreatedForAppExtension = "YES" wasCreatedForAppExtension = "YES"
version = "2.0"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
@ -47,16 +47,14 @@
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
allowLocationSimulation = "YES" allowLocationSimulation = "YES">
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference

View File

@ -12,7 +12,11 @@ private var chatController: chat_ctrl?
private var migrationResult: (Bool, DBMigrationResult)? private var migrationResult: (Bool, DBMigrationResult)?
public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl { public func hasChatCtrl() -> Bool {
chatController != nil
}
public func getChatCtrl() -> chat_ctrl {
if let controller = chatController { return controller } if let controller = chatController { return controller }
fatalError("chat controller not initialized") fatalError("chat controller not initialized")
} }

View File

@ -23,3 +23,17 @@ void haskell_init(void) {
char **pargv = argv; char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv); 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);
}

View File

@ -11,4 +11,6 @@
void haskell_init(void); void haskell_init(void);
void haskell_init_nse(void);
#endif /* hs_init_h */ #endif /* hs_init_h */