ios: mach messages to coordinate database acceess between app & NSE (#717)

This commit is contained in:
Evgeny Poberezkin
2022-06-02 13:16:22 +01:00
committed by GitHub
parent b435c0145f
commit 949fb17406
6 changed files with 206 additions and 46 deletions

View File

@@ -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
}

View File

@@ -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? {

View File

@@ -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 = "<group>"; };
5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX NSE.entitlements"; sourceTree = "<group>"; };
5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDefaults.swift; sourceTree = "<group>"; };
5CDCAD5228186F9500503DA2 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
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 = "<group>"; };
@@ -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 = "<group>";
};
5CDCAD7128188CEB00503DA2 /* Shared */ = {
isa = PBXGroup;
children = (
);
path = Shared;
sourceTree = "<group>";
};
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 */,
);

View File

@@ -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<String?, SendError> {
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<CallbackInfo>.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<String?, SendError> {
if let data = msg.data(using: .utf8) {
let msgData = data as CFData
var respData: Unmanaged<CFData>? = 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)
}
}

View File

@@ -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 {

View File

@@ -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
}