From 949fb17406954ca666123272c5d057e8e6701ead Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 2 Jun 2022 13:16:22 +0100 Subject: [PATCH] ios: mach messages to coordinate database acceess between app & NSE (#717) --- apps/ios/Shared/SimpleXApp.swift | 12 ++ .../ios/SimpleX NSE/NotificationService.swift | 13 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +- apps/ios/SimpleXChat/AppGroup.swift | 177 ++++++++++++++++++ apps/ios/SimpleXChat/FileUtils.swift | 2 +- apps/ios/SimpleXChat/GroupDefaults.swift | 32 ---- 6 files changed, 206 insertions(+), 46 deletions(-) create mode 100644 apps/ios/SimpleXChat/AppGroup.swift delete mode 100644 apps/ios/SimpleXChat/GroupDefaults.swift diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 26ec3490c..2c2540eab 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -11,6 +11,13 @@ import SimpleXChat let logger = Logger() +let machMessenger = MachMessenger(APP_MACH_PORT, callback: receivedNSEMachMessage) + +func receivedNSEMachMessage(msgId: Int32, msg: String) -> String? { + logger.debug("MachMessenger: receivedNSEMachMessage \"\(msg)\" from NSE, replying") + return "reply from App to: \(msg)" +} + @main struct SimpleXApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @@ -27,6 +34,7 @@ struct SimpleXApp: App { UserDefaults.standard.register(defaults: appDefaults) BGManager.shared.register() NtfManager.shared.registerCategories() + machMessenger.start() } var body: some Scene { @@ -42,14 +50,18 @@ struct SimpleXApp: App { } .onChange(of: scenePhase) { phase in logger.debug("scenePhase \(String(describing: scenePhase))") + let res = machMessenger.sendMessageWithReply(NSE_MACH_PORT, msg: "App scenePhase changed to \(String(describing: scenePhase))") + logger.debug("MachMessenger \(String(describing: res), privacy: .public)") setAppState(phase) switch (phase) { case .background: BGManager.shared.schedule() doAuthenticate = false enteredBackground = ProcessInfo.processInfo.systemUptime + machMessenger.stop() case .active: doAuthenticate = true + machMessenger.start() default: break } diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index c0a06a123..41668e954 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -12,15 +12,20 @@ import SimpleXChat let logger = Logger() -class NotificationService: UNNotificationServiceExtension { +let machMessenger = MachMessenger(NSE_MACH_PORT, callback: receivedAppMachMessage) +class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("NotificationService.didReceive") + machMessenger.start() + let res = machMessenger.sendMessageWithReply(APP_MACH_PORT, msg: "starting NSE didReceive") + logger.debug("MachMessenger \(String(describing: res), privacy: .public)") if getAppState() != .background { contentHandler(request.content) + machMessenger.stop() return } logger.debug("NotificationService: app is in the background") @@ -29,6 +34,7 @@ class NotificationService: UNNotificationServiceExtension { if let _ = startChat() { let content = receiveMessages() contentHandler (content) + machMessenger.stop() return } @@ -38,6 +44,7 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } + machMessenger.stop() } override func serviceExtensionTimeWillExpire() { @@ -48,7 +55,11 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } } +} +func receivedAppMachMessage(msgId: Int32, msg: String) -> String? { + logger.debug("MachMessenger: receivedAppMachMessage \"\(msg)\" from App, replying") + return "reply from NSE to: \(msg)" } func startChat() -> User? { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index def572caa..a0d4de258 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -85,7 +85,7 @@ 5CE2BA88284532AD00EC33A6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907F28376BB90076573F /* libgmp.a */; }; 5CE2BA89284532AD00EC33A6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907E28376BB90076573F /* libgmpxx.a */; }; 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; }; - 5CE2BA8C284533A300EC33A6 /* GroupDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */; }; + 5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* AppGroup.swift */; }; 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; }; 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7D2818941F00503DA2 /* API.swift */; }; 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7428188D2900503DA2 /* APITypes.swift */; }; @@ -250,7 +250,7 @@ 5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX NSE.entitlements"; sourceTree = ""; }; - 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDefaults.swift; sourceTree = ""; }; + 5CDCAD5228186F9500503DA2 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; }; 5CDCAD6028187D7900503DA2 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTypes.swift; sourceTree = ""; }; @@ -398,7 +398,6 @@ 5C764E87279CBC8E000C6508 /* Model */ = { isa = PBXGroup; children = ( - 5CDCAD7128188CEB00503DA2 /* Shared */, 5C764E88279CBCB3000C6508 /* ChatModel.swift */, 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, @@ -540,17 +539,10 @@ path = "SimpleX NSE"; sourceTree = ""; }; - 5CDCAD7128188CEB00503DA2 /* Shared */ = { - isa = PBXGroup; - children = ( - ); - path = Shared; - sourceTree = ""; - }; 5CE2BA692845308900EC33A6 /* SimpleXChat */ = { isa = PBXGroup; children = ( - 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */, + 5CDCAD5228186F9500503DA2 /* AppGroup.swift */, 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */, 5CDCAD7428188D2900503DA2 /* APITypes.swift */, 5C5E5D3C282447AB00B0488A /* CallTypes.swift */, @@ -887,7 +879,7 @@ 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */, 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */, 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */, - 5CE2BA8C284533A300EC33A6 /* GroupDefaults.swift in Sources */, + 5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */, 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */, 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */, ); diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift new file mode 100644 index 000000000..92573e035 --- /dev/null +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -0,0 +1,177 @@ +// +// GroupDefaults.swift +// SimpleX (iOS) +// +// Created by Evgeny on 26/04/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +let GROUP_DEFAULT_APP_IN_BACKGROUND = "appInBackground" + +let APP_GROUP_NAME = "group.chat.simplex.app" + +public let NSE_MACH_PORT = "\(APP_GROUP_NAME).nse" as CFString + +public let APP_MACH_PORT = "\(APP_GROUP_NAME).app" as CFString + +func getGroupDefaults() -> UserDefaults? { + UserDefaults(suiteName: APP_GROUP_NAME) +} + +public func setAppState(_ phase: ScenePhase) { + if let defaults = getGroupDefaults() { + defaults.set(phase == .background, forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) + defaults.synchronize() + } +} + +public func getAppState() -> ScenePhase { + if let defaults = getGroupDefaults() { + if defaults.bool(forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) { + return .background + } + } + return .active +} + +let MACH_SEND_TIMEOUT: CFTimeInterval = 1.0 +let MACH_REPLY_TIMEOUT: CFTimeInterval = 1.0 + +public class MachMessenger { + public init(_ localPortName: CFString, callback: @escaping Callback) { + self.localPortName = localPortName + self.callback = callback + self.localPort = nil + } + + var localPortName: CFString + var callback: Callback + var localPort: CFMessagePort? + + public enum SendError: Error { + case sndTimeout + case rcvTimeout + case portInvalid + case sendError(Int32) + case msgError + } + + public typealias Callback = (_ msgId: Int32, _ msg: String) -> String? + + class CallbackInfo { + internal init(callback: @escaping MachMessenger.Callback) { + self.callback = callback + } + + var callback: Callback + } + + public static func sendError(_ code: Int32) -> SendError? { + switch code { + case kCFMessagePortSuccess: return nil + case kCFMessagePortSendTimeout: return .sndTimeout + case kCFMessagePortReceiveTimeout: return .rcvTimeout + case kCFMessagePortIsInvalid: return .portInvalid + case kCFMessagePortBecameInvalidError: return .portInvalid + default: return .sendError(code) + } + } + + public func start() { + logger.debug("MachMessenger.start") + localPort = createLocalPort(localPortName, callback: callback) + } + + public func stop() { + if let port = localPort { + logger.debug("MachMessenger.stop") + CFMessagePortInvalidate(port) + localPort = nil + } + } + + public func sendMessage(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> SendError? { + logger.debug("MachMessenger.sendMessage") + if let port = createRemotePort(remotePortName) { + logger.debug("MachMessenger.sendMessage: sending...") + return sendMessage(port, msgId: msgId, msg: msg) + } else { + logger.debug("MachMessenger.sendMessage: no remote port") + return .portInvalid + } + } + + public func sendMessageWithReply(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> Result { + logger.debug("MachMessenger.sendMessageWithReply") + if let port = createRemotePort(remotePortName) { + logger.debug("MachMessenger.sendMessageWithReply: sending...") + return sendMessageWithReply(port, msgId: msgId, msg: msg) + } else { + logger.debug("MachMessenger.sendMessageWithReply: no remote port") + return .failure(.portInvalid) + } + } + + private func createLocalPort(_ portName: CFString, callback: @escaping Callback) -> CFMessagePort? { + logger.debug("MachMessenger.createLocalPort") + if let port = localPort { return port } + logger.debug("MachMessenger.createLocalPort: creating...") + var context = CFMessagePortContext() + context.version = 0 + context.info = Unmanaged.passRetained(CallbackInfo(callback: callback)).toOpaque() + let callout: CFMessagePortCallBack = { port, msgId, msgData, info in + logger.debug("MachMessenger CFMessagePortCallBack called") + if let data = msgData, + let msg = String(data: data as Data, encoding: .utf8), + let info = info, + let resp = Unmanaged.fromOpaque(info).takeUnretainedValue().callback(msgId, msg), + let respData = resp.data(using: .utf8) { + return Unmanaged.passRetained(respData as CFData) + } + return nil + } + return withUnsafeMutablePointer(to: &context) { cxt in + let port = CFMessagePortCreateLocal(kCFAllocatorDefault, portName, callout, cxt, nil) + CFMessagePortSetDispatchQueue(port, DispatchQueue.main); + localPort = port + logger.debug("MachMessenger.createLocalPort created: \(portName)") + return port + } + } + + private func createRemotePort(_ portName: CFString) -> CFMessagePort? { + CFMessagePortCreateRemote(kCFAllocatorDefault, portName) + } + + private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> SendError? { + if let data = msg.data(using: .utf8) { + logger.debug("MachMessenger sendMessage") + let msgData = data as CFData + let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, 0, nil, nil) + logger.debug("MachMessenger sendMessage \(code)") + return MachMessenger.sendError(code) + } + return .msgError + } + + private func sendMessageWithReply(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> Result { + if let data = msg.data(using: .utf8) { + let msgData = data as CFData + var respData: Unmanaged? = nil + let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, MACH_REPLY_TIMEOUT, CFRunLoopMode.defaultMode.rawValue, &respData) + if let err = MachMessenger.sendError(code) { + return .failure(err) + } else if let data = respData?.takeUnretainedValue(), + let resp = String(data: data as Data, encoding: .utf8) { + return .success(resp) + } else { + return .success(nil) + } + + } + return .failure(.msgError) + } +} diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 6cf037a5c..039303931 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -19,7 +19,7 @@ public let maxFileSize: Int64 = 8000000 func getDocumentsDirectory() -> URL { // FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.chat.simplex.app")! + FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)! } public func getAppFilesDirectory() -> URL { diff --git a/apps/ios/SimpleXChat/GroupDefaults.swift b/apps/ios/SimpleXChat/GroupDefaults.swift deleted file mode 100644 index f7fc3fa26..000000000 --- a/apps/ios/SimpleXChat/GroupDefaults.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// GroupDefaults.swift -// SimpleX (iOS) -// -// Created by Evgeny on 26/04/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import Foundation -import SwiftUI - -let GROUP_DEFAULT_APP_IN_BACKGROUND = "appInBackground" - -func getGroupDefaults() -> UserDefaults? { - UserDefaults(suiteName: "group.chat.simplex.app") -} - -public func setAppState(_ phase: ScenePhase) { - if let defaults = getGroupDefaults() { - defaults.set(phase == .background, forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) - defaults.synchronize() - } -} - -public func getAppState() -> ScenePhase { - if let defaults = getGroupDefaults() { - if defaults.bool(forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) { - return .background - } - } - return .active -}