From 0c7dce57f71fc9fcba5d393296559109218aa616 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 3 Nov 2021 16:21:45 +0300 Subject: [PATCH 1/4] Core class should be converted to actor --- FlowCrypt/Core/Core.swift | 117 ++++++++---------- FlowCrypt/Core/CoreHost.swift | 4 +- .../Functionality/Services/AppStartup.swift | 12 +- .../Core/FlowCryptCoreTests.swift | 6 +- .../Key Services/KeyServiceTests.swift | 3 +- 5 files changed, 60 insertions(+), 82 deletions(-) diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 2ec387773..221297104 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -53,20 +53,22 @@ protocol KeyParser { func parseKeys(armoredOrBinary: Data) async throws -> CoreRes.ParseKeys } -final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { +actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { static let shared = Core() + private typealias CallbackResult = (String, [UInt8]) + private var jsEndpointListener: JSValue? private var cb_catcher: JSValue? - private var cb_last_value: (String, [UInt8])? private var vm = JSVirtualMachine()! private var context: JSContext? - private dynamic var started = false - private dynamic var ready = false - - private let queue = DispatchQueue(label: "com.flowcrypt.core", qos: .default) // todo - try also with .userInitiated + + private var cb_last_value = [CallbackResult]() + private var started = false + private var ready = false private lazy var logger = Logger.nested(in: Self.self, with: "Js") + private lazy var resultSemaphore = DispatchSemaphore(value: 0) private init() {} @@ -188,31 +190,28 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { return try r.json.decodeJson(as: CoreRes.ZxcvbnStrengthBar.self) } - func startInBackgroundIfNotAlreadyRunning(_ completion: @escaping (() -> Void)) { - if self.ready { - completion() - } + func startInBackgroundIfNotAlreadyRunning() async { + guard !ready else { return } + if !started { started = true - DispatchQueue.global(qos: .default).async { [weak self] in - guard let self = self else { return } - let trace = Trace(id: "Start in background") - let jsFile = Bundle(for: Core.self).path(forResource: "flowcrypt-ios-prod.js.txt", ofType: nil)! - let jsFileSrc = try? String(contentsOfFile: jsFile) - self.context = JSContext(virtualMachine: self.vm)! - self.context?.setObject(CoreHost(), forKeyedSubscript: "coreHost" as (NSCopying & NSObjectProtocol)) - self.context!.exceptionHandler = { [weak self] _, exception in - guard let exception = exception else { return } - self?.logger.logWarning("\(exception)") - } - self.context!.evaluateScript("const APP_VERSION = 'iOS 0.2';") - self.context!.evaluateScript(jsFileSrc) - self.jsEndpointListener = self.context!.objectForKeyedSubscript("handleRequestFromHost") - self.cb_catcher = self.context!.objectForKeyedSubscript("engine_host_cb_value_formatter") - self.ready = true - self.logger.logInfo("JsContext took \(trace.finish()) to start") - completion() + + let trace = Trace(id: "Start in background") + let jsFile = Bundle(for: Self.self).path(forResource: "flowcrypt-ios-prod.js.txt", ofType: nil)! + let jsFileSrc = try? String(contentsOfFile: jsFile) + context = JSContext(virtualMachine: vm)! + context?.setObject(CoreHost(), forKeyedSubscript: "coreHost" as (NSCopying & NSObjectProtocol)) + context!.exceptionHandler = { _, exception in + guard let exception = exception else { return } + let logger = Logger.nested(in: Self.self, with: "Js") + logger.logWarning("\(exception)") } + context!.evaluateScript("const APP_VERSION = 'iOS 0.2';") + context!.evaluateScript(jsFileSrc) + jsEndpointListener = context!.objectForKeyedSubscript("handleRequestFromHost") + cb_catcher = context!.objectForKeyedSubscript("engine_host_cb_value_formatter") + ready = true + logger.logInfo("JsContext took \(trace.finish()) to start") } } @@ -223,7 +222,7 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { } func handleCallbackResult(json: String, data: [UInt8]) { - cb_last_value = (json, data) + cb_last_value.append((json, data)) } // MARK: Private calls @@ -236,47 +235,29 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { } private func call(_ endpoint: String, jsonData: Data, data: Data) async throws -> RawRes { - try await sleepUntilReadyOrThrow() - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - // tom - todo - currently there is only one callback storage variable "cb_last_value" - // for all JavaScript calls, and so we have to synchronize - // all calls into JavaScript to happen serially, else - // the return values would be undefined when used concurrently - // see https://github.com/FlowCrypt/flowcrypt-ios/issues/852 - // A possible solution would be to only synchronize returning o fthe callbac values into some dict. But I'm unsure if JavaScript is otherwise safe to call concurrently, so for now we'll do the safer thing. - queue.async { - self.cb_last_value = nil - self.jsEndpointListener!.call(withArguments: [endpoint, String(data: jsonData, encoding: .utf8)!, Array(data), self.cb_catcher!]) - guard - let resJsonData = self.cb_last_value?.0.data(using: .utf8), - let rawResponse = self.cb_last_value?.1 - else { - self.logger.logError("could not see callback response, got cb_last_value: \(String(describing: self.cb_last_value))") - continuation.resume(throwing: CoreError.format("JavaScript callback response not available")) - return - } - let error = try? resJsonData.decodeJson(as: CoreRes.Error.self) - if let error = error { - let errMsg = "------ js err -------\nCore \(endpoint):\n\(error.error.message)\n\(error.error.stack ?? "no stack")\n------- end js err -----" - self.logger.logError(errMsg) - continuation.resume(throwing: CoreError(coreError: error)) - return - } - continuation.resume(returning: RawRes(json: resJsonData, data: Data(rawResponse))) - } - } - } - - private func sleepUntilReadyOrThrow() async throws { - // This will block the task for up to 1000ms if the app was just started and Core method was called before JSContext is ready - // It should only affect the user if Core method was called within 500-800ms of starting the app - let start = DispatchTime.now() - while !ready { - if start.millisecondsSince > 1000 { // already waited for 1000 ms, give up - throw CoreError.notReady("App Core not ready yet") - } + jsEndpointListener!.call(withArguments: [endpoint, String(data: jsonData, encoding: .utf8)!, Array(data), cb_catcher!]) + + while cb_last_value.isEmpty { await Task.sleep(50 * 1_000_000) // 50ms } + + let lastValue = cb_last_value.removeFirst() + let rawResponse = lastValue.1 + guard + let resJsonData = lastValue.0.data(using: .utf8) + else { + logger.logError("could not see callback response, got cb_last_value: \(String(describing: lastValue))") + throw CoreError.format("JavaScript callback response not available") + } + + let error = try? resJsonData.decodeJson(as: CoreRes.Error.self) + if let error = error { + let errMsg = "------ js err -------\nCore \(endpoint):\n\(error.error.message)\n\(error.error.stack ?? "no stack")\n------- end js err -----" + logger.logError(errMsg) + throw CoreError(coreError: error) + } + + return RawRes(json: resJsonData, data: Data(rawResponse)) } private struct RawRes { diff --git a/FlowCrypt/Core/CoreHost.swift b/FlowCrypt/Core/CoreHost.swift index 4762b2e56..024310318 100644 --- a/FlowCrypt/Core/CoreHost.swift +++ b/FlowCrypt/Core/CoreHost.swift @@ -120,7 +120,9 @@ final class CoreHost: NSObject, CoreHostExports { } func handleCallback(_ string: String, _ data: [UInt8]) { - Core.shared.handleCallbackResult(json: string, data: data) + Task { + await Core.shared.handleCallbackResult(json: string, data: data) + } } @objc func callJsCb(_ timer: Timer) { diff --git a/FlowCrypt/Functionality/Services/AppStartup.swift b/FlowCrypt/Functionality/Services/AppStartup.swift index 99c97d9e3..77631747e 100644 --- a/FlowCrypt/Functionality/Services/AppStartup.swift +++ b/FlowCrypt/Functionality/Services/AppStartup.swift @@ -26,7 +26,7 @@ struct AppStartup { Task { do { - try awaitPromise(setupCore()) + await setupCore() try setupMigrationIfNeeded() try setupSession() try await getUserOrgRulesIfNeeded() @@ -37,13 +37,9 @@ struct AppStartup { } } - private func setupCore() -> Promise { - Promise { resolve, _ in - logger.logInfo("Setup Core") - Core.shared.startInBackgroundIfNotAlreadyRunning { - resolve(()) - } - } + private func setupCore() async { + logger.logInfo("Setup Core") + await Core.shared.startInBackgroundIfNotAlreadyRunning() } private func setupMigrationIfNeeded() throws { diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index 16af131b2..5b8c9c12a 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -15,7 +15,8 @@ final class FlowCryptCoreTests: XCTestCase { override func setUp() { let expectation = XCTestExpectation() - core.startInBackgroundIfNotAlreadyRunning { + Task { + await core.startInBackgroundIfNotAlreadyRunning() expectation.fulfill() } wait(for: [expectation], timeout: 20) @@ -387,9 +388,6 @@ final class FlowCryptCoreTests: XCTestCase { } } - // this test is only meaningful on a real device - // it passes on simulator even if implementation is broken - // maybe there's a way to run simulator with more cores? (on a mac that has them) which would simulate real device better func testCoreResponseCorrectnessUnderConcurrency() async throws { // given: a bunch of keys let pp = "this particular pass phrase is long enough" diff --git a/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift b/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift index 6e5db9971..3a8850260 100644 --- a/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift @@ -15,7 +15,8 @@ final class KeyServiceTests: XCTestCase { super.setUp() let expectation = XCTestExpectation() - Core.shared.startInBackgroundIfNotAlreadyRunning { + Task { + await Core.shared.startInBackgroundIfNotAlreadyRunning() expectation.fulfill() } wait(for: [expectation], timeout: 10) From 0570a0cd414b3c77f82cbcdcdba3346fe2de32a7 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 3 Nov 2021 16:33:45 +0300 Subject: [PATCH 2/4] Core class should be converted to actor --- FlowCrypt/Core/Core.swift | 49 ++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 221297104..922ee7f21 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -63,8 +63,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { private var vm = JSVirtualMachine()! private var context: JSContext? - private var cb_last_value = [CallbackResult]() - private var started = false + private var callbackResults = [CallbackResult]() private var ready = false private lazy var logger = Logger.nested(in: Self.self, with: "Js") @@ -193,26 +192,22 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { func startInBackgroundIfNotAlreadyRunning() async { guard !ready else { return } - if !started { - started = true - - let trace = Trace(id: "Start in background") - let jsFile = Bundle(for: Self.self).path(forResource: "flowcrypt-ios-prod.js.txt", ofType: nil)! - let jsFileSrc = try? String(contentsOfFile: jsFile) - context = JSContext(virtualMachine: vm)! - context?.setObject(CoreHost(), forKeyedSubscript: "coreHost" as (NSCopying & NSObjectProtocol)) - context!.exceptionHandler = { _, exception in - guard let exception = exception else { return } - let logger = Logger.nested(in: Self.self, with: "Js") - logger.logWarning("\(exception)") - } - context!.evaluateScript("const APP_VERSION = 'iOS 0.2';") - context!.evaluateScript(jsFileSrc) - jsEndpointListener = context!.objectForKeyedSubscript("handleRequestFromHost") - cb_catcher = context!.objectForKeyedSubscript("engine_host_cb_value_formatter") - ready = true - logger.logInfo("JsContext took \(trace.finish()) to start") + let trace = Trace(id: "Start in background") + let jsFile = Bundle(for: Self.self).path(forResource: "flowcrypt-ios-prod.js.txt", ofType: nil)! + let jsFileSrc = try? String(contentsOfFile: jsFile) + context = JSContext(virtualMachine: vm)! + context?.setObject(CoreHost(), forKeyedSubscript: "coreHost" as (NSCopying & NSObjectProtocol)) + context!.exceptionHandler = { _, exception in + guard let exception = exception else { return } + let logger = Logger.nested(in: Self.self, with: "Js") + logger.logWarning("\(exception)") } + context!.evaluateScript("const APP_VERSION = 'iOS 0.2';") + context!.evaluateScript(jsFileSrc) + jsEndpointListener = context!.objectForKeyedSubscript("handleRequestFromHost") + cb_catcher = context!.objectForKeyedSubscript("engine_host_cb_value_formatter") + ready = true + logger.logInfo("JsContext took \(trace.finish()) to start") } func gmailBackupSearch(for email: String) async throws -> String { @@ -222,7 +217,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { } func handleCallbackResult(json: String, data: [UInt8]) { - cb_last_value.append((json, data)) + callbackResults.append((json, data)) } // MARK: Private calls @@ -237,16 +232,16 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { private func call(_ endpoint: String, jsonData: Data, data: Data) async throws -> RawRes { jsEndpointListener!.call(withArguments: [endpoint, String(data: jsonData, encoding: .utf8)!, Array(data), cb_catcher!]) - while cb_last_value.isEmpty { + while callbackResults.isEmpty { await Task.sleep(50 * 1_000_000) // 50ms } - let lastValue = cb_last_value.removeFirst() - let rawResponse = lastValue.1 + let result = callbackResults.removeFirst() + let rawResponse = result.1 guard - let resJsonData = lastValue.0.data(using: .utf8) + let resJsonData = result.0.data(using: .utf8) else { - logger.logError("could not see callback response, got cb_last_value: \(String(describing: lastValue))") + logger.logError("could not see callback response, got cb_last_value: \(String(describing: result))") throw CoreError.format("JavaScript callback response not available") } From 19ea418a35f42d295cd03ba02d9a660ca3ba4cf3 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 3 Nov 2021 20:04:57 +0300 Subject: [PATCH 3/4] Core class should be converted to actor --- Core/source/assets/flowcrypt-ios-begin.js | 4 +- Core/source/entrypoint-bare.ts | 10 ++--- FlowCrypt/Core/Core.swift | 39 +++++++++++-------- FlowCrypt/Core/CoreHost.swift | 6 +-- .../Functionality/Services/AppStartup.swift | 2 +- FlowCrypt/Resources/flowcrypt-ios-prod.js.txt | 16 ++++---- .../Core/FlowCryptCoreTests.swift | 4 +- .../Key Services/KeyServiceTests.swift | 2 +- 8 files changed, 46 insertions(+), 37 deletions(-) diff --git a/Core/source/assets/flowcrypt-ios-begin.js b/Core/source/assets/flowcrypt-ios-begin.js index 1c3ace8d5..fa149c045 100644 --- a/Core/source/assets/flowcrypt-ios-begin.js +++ b/Core/source/assets/flowcrypt-ios-begin.js @@ -35,8 +35,8 @@ const window = { navigator, crypto, setTimeout, clearTimeout }; const self = { window, navigator, crypto }; global.window = window; -var engine_host_cb_value_formatter = function(val) { - coreHost.handleCallback(val.json, val.data); +var engine_host_cb_value_formatter = function(key, val) { + coreHost.handleCallback(key, val.json, val.data); }; (function() { diff --git a/Core/source/entrypoint-bare.ts b/Core/source/entrypoint-bare.ts index ca0d66bc2..bb6ea27c7 100644 --- a/Core/source/entrypoint-bare.ts +++ b/Core/source/entrypoint-bare.ts @@ -11,17 +11,17 @@ declare const global: any; const endpoints = new Endpoints(); -global.handleRequestFromHost = (endpointName: string, request: string, data: Uint8Array, cb: (response: EndpointRes) => void): void => { +global.handleRequestFromHost = (endpointName: string, endpointKey: string, request: string, data: Uint8Array, cb: (key: string, response: EndpointRes) => void): void => { try { const handler = endpoints[endpointName]; if (!handler) { - cb(fmtErr(new Error(`Unknown endpoint: ${endpointName}`))); + cb(endpointKey, fmtErr(new Error(`Unknown endpoint: ${endpointName}`))); } else { handler(JSON.parse(request), [data]) - .then(res => cb(res)) - .catch(err => cb(fmtErr(err))); + .then(res => cb(endpointKey, res)) + .catch(err => cb(endpointKey, fmtErr(err))); } } catch (err) { - cb(fmtErr(err)); + cb(endpointKey, fmtErr(err)); } }; diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 922ee7f21..16843cdfa 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -63,11 +63,10 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { private var vm = JSVirtualMachine()! private var context: JSContext? - private var callbackResults = [CallbackResult]() + private var callbackResults: [String: CallbackResult] = [:] private var ready = false private lazy var logger = Logger.nested(in: Self.self, with: "Js") - private lazy var resultSemaphore = DispatchSemaphore(value: 0) private init() {} @@ -189,7 +188,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { return try r.json.decodeJson(as: CoreRes.ZxcvbnStrengthBar.self) } - func startInBackgroundIfNotAlreadyRunning() async { + func startIfNotAlreadyRunning() async { guard !ready else { return } let trace = Trace(id: "Start in background") @@ -216,8 +215,8 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { return result.query } - func handleCallbackResult(json: String, data: [UInt8]) { - callbackResults.append((json, data)) + func handleCallbackResult(endpointKey: String, json: String, data: [UInt8]) { + callbackResults[endpointKey] = (json, data) } // MARK: Private calls @@ -230,18 +229,26 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { } private func call(_ endpoint: String, jsonData: Data, data: Data) async throws -> RawRes { - jsEndpointListener!.call(withArguments: [endpoint, String(data: jsonData, encoding: .utf8)!, Array(data), cb_catcher!]) + guard ready else { + throw CoreError.exception("Core is not ready yet. Most likeyly startIfNotAlreadyRunning wasn't called first") + } + + let endpointKey = NSUUID().uuidString + jsEndpointListener!.call(withArguments: [ + endpoint, + endpointKey, + String(data: jsonData, encoding: .utf8)!, + Array(data), cb_catcher! + ]) - while callbackResults.isEmpty { - await Task.sleep(50 * 1_000_000) // 50ms + while callbackResults[endpointKey] == nil { + await Task.sleep(1_000_000) // 1ms } - let result = callbackResults.removeFirst() - let rawResponse = result.1 guard + let result = callbackResults.removeValue(forKey: endpointKey), let resJsonData = result.0.data(using: .utf8) else { - logger.logError("could not see callback response, got cb_last_value: \(String(describing: result))") throw CoreError.format("JavaScript callback response not available") } @@ -252,13 +259,13 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { throw CoreError(coreError: error) } - return RawRes(json: resJsonData, data: Data(rawResponse)) + return RawRes(json: resJsonData, data: Data(result.1)) } +} - private struct RawRes { - let json: Data - let data: Data - } +private struct RawRes { + let json: Data + let data: Data } private struct GmailBackupSearchResponse: Decodable { diff --git a/FlowCrypt/Core/CoreHost.swift b/FlowCrypt/Core/CoreHost.swift index 024310318..a1ef7bf8f 100644 --- a/FlowCrypt/Core/CoreHost.swift +++ b/FlowCrypt/Core/CoreHost.swift @@ -22,7 +22,7 @@ import SwiftyRSA // for rsa func setTimeout(_ callback: JSValue, _ ms: Double) -> String func clearTimeout(_ identifier: String) - func handleCallback(_ string: String, _ data: [UInt8]) + func handleCallback(_ endpointKey: String, _ string: String, _ data: [UInt8]) } var timers = [String: Timer]() @@ -119,9 +119,9 @@ final class CoreHost: NSObject, CoreHostExports { return uuid } - func handleCallback(_ string: String, _ data: [UInt8]) { + func handleCallback(_ endpointKey: String, _ string: String, _ data: [UInt8]) { Task { - await Core.shared.handleCallbackResult(json: string, data: data) + await Core.shared.handleCallbackResult(endpointKey: endpointKey, json: string, data: data) } } diff --git a/FlowCrypt/Functionality/Services/AppStartup.swift b/FlowCrypt/Functionality/Services/AppStartup.swift index 77631747e..e322801cf 100644 --- a/FlowCrypt/Functionality/Services/AppStartup.swift +++ b/FlowCrypt/Functionality/Services/AppStartup.swift @@ -39,7 +39,7 @@ struct AppStartup { private func setupCore() async { logger.logInfo("Setup Core") - await Core.shared.startInBackgroundIfNotAlreadyRunning() + await Core.shared.startIfNotAlreadyRunning() } private func setupMigrationIfNeeded() throws { diff --git a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt index 531313b35..b704f2e9b 100644 --- a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt +++ b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt @@ -40,8 +40,8 @@ const window = { navigator, crypto, setTimeout, clearTimeout }; const self = { window, navigator, crypto }; global.window = window; -var engine_host_cb_value_formatter = function(val) { - coreHost.handleCallback(val.json, val.data); +var engine_host_cb_value_formatter = function(key, val) { + coreHost.handleCallback(key, val.json, val.data); }; (function() { @@ -86405,20 +86405,20 @@ Object.defineProperty(exports, "__esModule", { value: true }); const endpoints_1 = __webpack_require__(2); const format_output_1 = __webpack_require__(3); const endpoints = new endpoints_1.Endpoints(); -global.handleRequestFromHost = (endpointName, request, data, cb) => { +global.handleRequestFromHost = (endpointName, endpointKey, request, data, cb) => { try { const handler = endpoints[endpointName]; if (!handler) { - cb((0, format_output_1.fmtErr)(new Error(`Unknown endpoint: ${endpointName}`))); + cb(endpointKey, (0, format_output_1.fmtErr)(new Error(`Unknown endpoint: ${endpointName}`))); } else { handler(JSON.parse(request), [data]) - .then(res => cb(res)) - .catch(err => cb((0, format_output_1.fmtErr)(err))); + .then(res => cb(endpointKey, res)) + .catch(err => cb(endpointKey, (0, format_output_1.fmtErr)(err))); } } catch (err) { - cb((0, format_output_1.fmtErr)(err)); + cb(endpointKey, (0, format_output_1.fmtErr)(err)); } }; @@ -111770,7 +111770,7 @@ elliptic.eddsa = __webpack_require__(170); /* 144 */ /***/ (function(module) { -module.exports = JSON.parse("{\"_from\":\"elliptic@^6.5.3\",\"_id\":\"elliptic@6.5.4\",\"_inBundle\":false,\"_integrity\":\"sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==\",\"_location\":\"/elliptic\",\"_phantomChildren\":{},\"_requested\":{\"type\":\"range\",\"registry\":true,\"raw\":\"elliptic@^6.5.3\",\"name\":\"elliptic\",\"escapedName\":\"elliptic\",\"rawSpec\":\"^6.5.3\",\"saveSpec\":null,\"fetchSpec\":\"^6.5.3\"},\"_requiredBy\":[\"/browserify-sign\",\"/create-ecdh\"],\"_resolved\":\"https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz\",\"_shasum\":\"da37cebd31e79a1367e941b592ed1fbebd58abbb\",\"_spec\":\"elliptic@^6.5.3\",\"_where\":\"/home/ivan/pro/flowcrypt/flowcrypt-ios/Core/node_modules/browserify-sign\",\"author\":{\"name\":\"Fedor Indutny\",\"email\":\"fedor@indutny.com\"},\"bugs\":{\"url\":\"https://github.com/indutny/elliptic/issues\"},\"bundleDependencies\":false,\"dependencies\":{\"bn.js\":\"^4.11.9\",\"brorand\":\"^1.1.0\",\"hash.js\":\"^1.0.0\",\"hmac-drbg\":\"^1.0.1\",\"inherits\":\"^2.0.4\",\"minimalistic-assert\":\"^1.0.1\",\"minimalistic-crypto-utils\":\"^1.0.1\"},\"deprecated\":false,\"description\":\"EC cryptography\",\"devDependencies\":{\"brfs\":\"^2.0.2\",\"coveralls\":\"^3.1.0\",\"eslint\":\"^7.6.0\",\"grunt\":\"^1.2.1\",\"grunt-browserify\":\"^5.3.0\",\"grunt-cli\":\"^1.3.2\",\"grunt-contrib-connect\":\"^3.0.0\",\"grunt-contrib-copy\":\"^1.0.0\",\"grunt-contrib-uglify\":\"^5.0.0\",\"grunt-mocha-istanbul\":\"^5.0.2\",\"grunt-saucelabs\":\"^9.0.1\",\"istanbul\":\"^0.4.5\",\"mocha\":\"^8.0.1\"},\"files\":[\"lib\"],\"homepage\":\"https://github.com/indutny/elliptic\",\"keywords\":[\"EC\",\"Elliptic\",\"curve\",\"Cryptography\"],\"license\":\"MIT\",\"main\":\"lib/elliptic.js\",\"name\":\"elliptic\",\"repository\":{\"type\":\"git\",\"url\":\"git+ssh://git@github.com/indutny/elliptic.git\"},\"scripts\":{\"lint\":\"eslint lib test\",\"lint:fix\":\"npm run lint -- --fix\",\"test\":\"npm run lint && npm run unit\",\"unit\":\"istanbul test _mocha --reporter=spec test/index.js\",\"version\":\"grunt dist && git add dist/\"},\"version\":\"6.5.4\"}"); +module.exports = JSON.parse("{\"_args\":[[\"elliptic@6.5.4\",\"/Users/ivan/Documents/Projects/FlowCrypt/flowcrypt-ios/Core\"]],\"_development\":true,\"_from\":\"elliptic@6.5.4\",\"_id\":\"elliptic@6.5.4\",\"_inBundle\":false,\"_integrity\":\"sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==\",\"_location\":\"/elliptic\",\"_phantomChildren\":{},\"_requested\":{\"type\":\"version\",\"registry\":true,\"raw\":\"elliptic@6.5.4\",\"name\":\"elliptic\",\"escapedName\":\"elliptic\",\"rawSpec\":\"6.5.4\",\"saveSpec\":null,\"fetchSpec\":\"6.5.4\"},\"_requiredBy\":[\"/browserify-sign\",\"/create-ecdh\"],\"_resolved\":\"https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz\",\"_spec\":\"6.5.4\",\"_where\":\"/Users/ivan/Documents/Projects/FlowCrypt/flowcrypt-ios/Core\",\"author\":{\"name\":\"Fedor Indutny\",\"email\":\"fedor@indutny.com\"},\"bugs\":{\"url\":\"https://github.com/indutny/elliptic/issues\"},\"dependencies\":{\"bn.js\":\"^4.11.9\",\"brorand\":\"^1.1.0\",\"hash.js\":\"^1.0.0\",\"hmac-drbg\":\"^1.0.1\",\"inherits\":\"^2.0.4\",\"minimalistic-assert\":\"^1.0.1\",\"minimalistic-crypto-utils\":\"^1.0.1\"},\"description\":\"EC cryptography\",\"devDependencies\":{\"brfs\":\"^2.0.2\",\"coveralls\":\"^3.1.0\",\"eslint\":\"^7.6.0\",\"grunt\":\"^1.2.1\",\"grunt-browserify\":\"^5.3.0\",\"grunt-cli\":\"^1.3.2\",\"grunt-contrib-connect\":\"^3.0.0\",\"grunt-contrib-copy\":\"^1.0.0\",\"grunt-contrib-uglify\":\"^5.0.0\",\"grunt-mocha-istanbul\":\"^5.0.2\",\"grunt-saucelabs\":\"^9.0.1\",\"istanbul\":\"^0.4.5\",\"mocha\":\"^8.0.1\"},\"files\":[\"lib\"],\"homepage\":\"https://github.com/indutny/elliptic\",\"keywords\":[\"EC\",\"Elliptic\",\"curve\",\"Cryptography\"],\"license\":\"MIT\",\"main\":\"lib/elliptic.js\",\"name\":\"elliptic\",\"repository\":{\"type\":\"git\",\"url\":\"git+ssh://git@github.com/indutny/elliptic.git\"},\"scripts\":{\"lint\":\"eslint lib test\",\"lint:fix\":\"npm run lint -- --fix\",\"test\":\"npm run lint && npm run unit\",\"unit\":\"istanbul test _mocha --reporter=spec test/index.js\",\"version\":\"grunt dist && git add dist/\"},\"version\":\"6.5.4\"}"); /***/ }), /* 145 */ diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index 5b8c9c12a..1b3b95758 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -16,7 +16,7 @@ final class FlowCryptCoreTests: XCTestCase { override func setUp() { let expectation = XCTestExpectation() Task { - await core.startInBackgroundIfNotAlreadyRunning() + await core.startIfNotAlreadyRunning() expectation.fulfill() } wait(for: [expectation], timeout: 20) @@ -388,6 +388,8 @@ final class FlowCryptCoreTests: XCTestCase { } } + // This test always passes, even wrongly, on simulators running on a mac with 2 or fewer cores. + // Behaves meaningfully on real iPhone or simulator on a mac with many cores func testCoreResponseCorrectnessUnderConcurrency() async throws { // given: a bunch of keys let pp = "this particular pass phrase is long enough" diff --git a/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift b/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift index 3a8850260..e3bb5292f 100644 --- a/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift @@ -16,7 +16,7 @@ final class KeyServiceTests: XCTestCase { let expectation = XCTestExpectation() Task { - await Core.shared.startInBackgroundIfNotAlreadyRunning() + await Core.shared.startIfNotAlreadyRunning() expectation.fulfill() } wait(for: [expectation], timeout: 10) From 786239ebfbccc0d479cec94eaefb56b0da2a6f0a Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 3 Nov 2021 21:07:10 +0300 Subject: [PATCH 4/4] Core class should be converted to actor --- Core/source/assets/flowcrypt-ios-begin.js | 4 ++-- Core/source/entrypoint-bare.ts | 10 +++++----- FlowCrypt/Core/Core.swift | 12 ++++++------ FlowCrypt/Core/CoreHost.swift | 4 ++-- FlowCrypt/Resources/flowcrypt-ios-prod.js.txt | 14 +++++++------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Core/source/assets/flowcrypt-ios-begin.js b/Core/source/assets/flowcrypt-ios-begin.js index fa149c045..fac5e2833 100644 --- a/Core/source/assets/flowcrypt-ios-begin.js +++ b/Core/source/assets/flowcrypt-ios-begin.js @@ -35,8 +35,8 @@ const window = { navigator, crypto, setTimeout, clearTimeout }; const self = { window, navigator, crypto }; global.window = window; -var engine_host_cb_value_formatter = function(key, val) { - coreHost.handleCallback(key, val.json, val.data); +var engine_host_cb_value_formatter = function(callbackId, val) { + coreHost.handleCallback(callbackId, val.json, val.data); }; (function() { diff --git a/Core/source/entrypoint-bare.ts b/Core/source/entrypoint-bare.ts index bb6ea27c7..7e15dbc5c 100644 --- a/Core/source/entrypoint-bare.ts +++ b/Core/source/entrypoint-bare.ts @@ -11,17 +11,17 @@ declare const global: any; const endpoints = new Endpoints(); -global.handleRequestFromHost = (endpointName: string, endpointKey: string, request: string, data: Uint8Array, cb: (key: string, response: EndpointRes) => void): void => { +global.handleRequestFromHost = (endpointName: string, callbackId: string, request: string, data: Uint8Array, cb: (key: string, response: EndpointRes) => void): void => { try { const handler = endpoints[endpointName]; if (!handler) { - cb(endpointKey, fmtErr(new Error(`Unknown endpoint: ${endpointName}`))); + cb(callbackId, fmtErr(new Error(`Unknown endpoint: ${endpointName}`))); } else { handler(JSON.parse(request), [data]) - .then(res => cb(endpointKey, res)) - .catch(err => cb(endpointKey, fmtErr(err))); + .then(res => cb(callbackId, res)) + .catch(err => cb(callbackId, fmtErr(err))); } } catch (err) { - cb(endpointKey, fmtErr(err)); + cb(callbackId, fmtErr(err)); } }; diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 16843cdfa..041ee537c 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -215,8 +215,8 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { return result.query } - func handleCallbackResult(endpointKey: String, json: String, data: [UInt8]) { - callbackResults[endpointKey] = (json, data) + func handleCallbackResult(callbackId: String, json: String, data: [UInt8]) { + callbackResults[callbackId] = (json, data) } // MARK: Private calls @@ -233,20 +233,20 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { throw CoreError.exception("Core is not ready yet. Most likeyly startIfNotAlreadyRunning wasn't called first") } - let endpointKey = NSUUID().uuidString + let callbackId = NSUUID().uuidString jsEndpointListener!.call(withArguments: [ endpoint, - endpointKey, + callbackId, String(data: jsonData, encoding: .utf8)!, Array(data), cb_catcher! ]) - while callbackResults[endpointKey] == nil { + while callbackResults[callbackId] == nil { await Task.sleep(1_000_000) // 1ms } guard - let result = callbackResults.removeValue(forKey: endpointKey), + let result = callbackResults.removeValue(forKey: callbackId), let resJsonData = result.0.data(using: .utf8) else { throw CoreError.format("JavaScript callback response not available") diff --git a/FlowCrypt/Core/CoreHost.swift b/FlowCrypt/Core/CoreHost.swift index a1ef7bf8f..19daf5bb7 100644 --- a/FlowCrypt/Core/CoreHost.swift +++ b/FlowCrypt/Core/CoreHost.swift @@ -119,9 +119,9 @@ final class CoreHost: NSObject, CoreHostExports { return uuid } - func handleCallback(_ endpointKey: String, _ string: String, _ data: [UInt8]) { + func handleCallback(_ callbackId: String, _ string: String, _ data: [UInt8]) { Task { - await Core.shared.handleCallbackResult(endpointKey: endpointKey, json: string, data: data) + await Core.shared.handleCallbackResult(callbackId: callbackId, json: string, data: data) } } diff --git a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt index b704f2e9b..ce0b14a96 100644 --- a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt +++ b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt @@ -40,8 +40,8 @@ const window = { navigator, crypto, setTimeout, clearTimeout }; const self = { window, navigator, crypto }; global.window = window; -var engine_host_cb_value_formatter = function(key, val) { - coreHost.handleCallback(key, val.json, val.data); +var engine_host_cb_value_formatter = function(callbackId, val) { + coreHost.handleCallback(callbackId, val.json, val.data); }; (function() { @@ -86405,20 +86405,20 @@ Object.defineProperty(exports, "__esModule", { value: true }); const endpoints_1 = __webpack_require__(2); const format_output_1 = __webpack_require__(3); const endpoints = new endpoints_1.Endpoints(); -global.handleRequestFromHost = (endpointName, endpointKey, request, data, cb) => { +global.handleRequestFromHost = (endpointName, callbackId, request, data, cb) => { try { const handler = endpoints[endpointName]; if (!handler) { - cb(endpointKey, (0, format_output_1.fmtErr)(new Error(`Unknown endpoint: ${endpointName}`))); + cb(callbackId, (0, format_output_1.fmtErr)(new Error(`Unknown endpoint: ${endpointName}`))); } else { handler(JSON.parse(request), [data]) - .then(res => cb(endpointKey, res)) - .catch(err => cb(endpointKey, (0, format_output_1.fmtErr)(err))); + .then(res => cb(callbackId, res)) + .catch(err => cb(callbackId, (0, format_output_1.fmtErr)(err))); } } catch (err) { - cb(endpointKey, (0, format_output_1.fmtErr)(err)); + cb(callbackId, (0, format_output_1.fmtErr)(err)); } };