From 837b775c61d37077baa900097d68bb176b981754 Mon Sep 17 00:00:00 2001 From: Shoaib Akhtar Date: Wed, 14 Oct 2026 14:11:05 +0400 Subject: [PATCH 1/3] test(ios): add RunnerTests showing Talsec.start blocks the main thread Adds an XCTest target to the example app and a seam (`talsecStarter`) so the plugin's call to Talsec.start can be observed without running TalsecRuntime. The two threading tests fail against the current bridge: start runs inline on the platform thread and blocks handle(_:result:). Refs #231 --- example/ios/Podfile | 3 + example/ios/Podfile.lock | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 182 ++++++++++++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 10 + example/ios/RunnerTests/RunnerTests.swift | 100 ++++++++++ .../Sources/SwiftFreeraspPlugin.swift | 5 +- 6 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 example/ios/RunnerTests/RunnerTests.swift diff --git a/example/ios/Podfile b/example/ios/Podfile index e72e0b4..e51a31d 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -32,6 +32,9 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ae93778..961ebe1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -23,6 +23,6 @@ SPEC CHECKSUMS: freerasp: bb827d80b926abcfb8f4ca4ff4557c2fe4a5ae21 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 -PODFILE CHECKSUM: 0dbd5a87e0ace00c9610d2037ac22083a01f861d +PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694 COCOAPODS: 1.15.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 8285e4b..4f59c2c 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,8 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 0BD4F69B9FF969126588DDC0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 708D3446324AE10BC008699A /* Foundation.framework */; }; + 0D0441A20FC5887DD6AFEB6C /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF37A6CE1BE2F1C80B1B1D8 /* RunnerTests.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 51C2B8803DC8DF7D7A96F6E9 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96471C26EC1C9CEDE502CB82 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -16,6 +19,16 @@ AED47D1AFE1542C7BFB86C77 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D43E6C8CBA8A2CDFE487D91B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 9E9CD00D053E44FC5B080BC3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -30,14 +43,17 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 01723E74BF5296F76A02530D /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 08482B952564A020241C06D0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 708D3446324AE10BC008699A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 86022E44343332330DC011D6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 96471C26EC1C9CEDE502CB82 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -45,11 +61,24 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A8416C15D45FB5A093756EE4 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + BFF37A6CE1BE2F1C80B1B1D8 /* RunnerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + C32B0AA845ED0CB64AEAF8B0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; D43E6C8CBA8A2CDFE487D91B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E529EABA8026DE77401F8953 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; F1E8D858DBFD1D1C73D64F39 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 0CBE11AB630712EE7A017C0C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0BD4F69B9FF969126588DDC0 /* Foundation.framework in Frameworks */, + 51C2B8803DC8DF7D7A96F6E9 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -65,10 +94,29 @@ isa = PBXGroup; children = ( D43E6C8CBA8A2CDFE487D91B /* Pods_Runner.framework */, + 1A4E2ED85FD2E7FDEBC714F9 /* iOS */, + 96471C26EC1C9CEDE502CB82 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; + 1A4E2ED85FD2E7FDEBC714F9 /* iOS */ = { + isa = PBXGroup; + children = ( + 708D3446324AE10BC008699A /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 2E316B12ADE3E40CBF0903B4 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + BFF37A6CE1BE2F1C80B1B1D8 /* RunnerTests.swift */, + ); + name = RunnerTests; + path = RunnerTests; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -88,6 +136,7 @@ 97C146EF1CF9000F007C117D /* Products */, 9A81FCBD5939B4091B538CD4 /* Pods */, 085AFCA140DB14AFA3E72543 /* Frameworks */, + 2E316B12ADE3E40CBF0903B4 /* RunnerTests */, ); sourceTree = ""; }; @@ -95,6 +144,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 01723E74BF5296F76A02530D /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -120,6 +170,9 @@ 08482B952564A020241C06D0 /* Pods-Runner.debug.xcconfig */, 86022E44343332330DC011D6 /* Pods-Runner.release.xcconfig */, F1E8D858DBFD1D1C73D64F39 /* Pods-Runner.profile.xcconfig */, + C32B0AA845ED0CB64AEAF8B0 /* Pods-RunnerTests.release.xcconfig */, + A8416C15D45FB5A093756EE4 /* Pods-RunnerTests.debug.xcconfig */, + E529EABA8026DE77401F8953 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -150,6 +203,25 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + B682B928862323CFBAE9F112 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 86AA4C37BE984905C5A1FB3B /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 15F5EEBBA15A816971501329 /* [CP] Check Pods Manifest.lock */, + 33734A4A14E5CB7A7867E82B /* Sources */, + 0CBE11AB630712EE7A017C0C /* Frameworks */, + A00CEE5D09983E3CA316B7FA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D76DC28796526461049238EC /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 01723E74BF5296F76A02530D /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -179,6 +251,7 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + B682B928862323CFBAE9F112 /* RunnerTests */, ); }; /* End PBXProject section */ @@ -195,9 +268,38 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A00CEE5D09983E3CA316B7FA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 15F5EEBBA15A816971501329 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -288,6 +390,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 33734A4A14E5CB7A7867E82B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0D0441A20FC5887DD6AFEB6C /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -299,6 +409,15 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + D76DC28796526461049238EC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Runner; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 9E9CD00D053E44FC5B080BC3 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -393,6 +512,41 @@ }; name = Profile; }; + 4644A9E29E4394F4ABDBA157 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A8416C15D45FB5A093756EE4 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGNING_ALLOWED = NO; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + PRODUCT_BUNDLE_IDENTIFIER = app.talsec.freeraspExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 5F16D39CE559EF7AC151A295 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E529EABA8026DE77401F8953 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGNING_ALLOWED = NO; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + PRODUCT_BUNDLE_IDENTIFIER = app.talsec.freeraspExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -549,9 +703,37 @@ }; name = Release; }; + FE96407EF9F075013AF42371 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C32B0AA845ED0CB64AEAF8B0 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGNING_ALLOWED = NO; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + PRODUCT_BUNDLE_IDENTIFIER = app.talsec.freeraspExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 86AA4C37BE984905C5A1FB3B /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FE96407EF9F075013AF42371 /* Release */, + 4644A9E29E4394F4ABDBA157 /* Debug */, + 5F16D39CE559EF7AC151A295 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 183fe67..e27925a 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -38,6 +38,16 @@ + + + + Void)! + + private let validConfig = """ + {"watcherMail":"security@example.com","isProd":false,\ + "iosConfig":{"bundleIds":["app.talsec.freeraspExample"],"teamId":"ABCDE12345"}} + """ + + override func setUp() { + super.setUp() + originalStarter = plugin.talsecStarter + } + + override func tearDown() { + plugin.talsecStarter = originalStarter + super.tearDown() + } + + private func startCall(config: String?) -> FlutterMethodCall { + FlutterMethodCall(methodName: "start", arguments: config.map { ["config": $0] }) + } + + func testStartRunsTalsecOffTheMainThread() { + let started = expectation(description: "Talsec started") + var startedOnMainThread: Bool? + plugin.talsecStarter = { _ in + startedOnMainThread = Thread.isMainThread + started.fulfill() + } + + plugin.handle(startCall(config: validConfig)) { _ in } + + wait(for: [started], timeout: 2) + XCTAssertEqual(startedOnMainThread, false) + } + + func testStartReturnsBeforeTalsecFinishes() { + // The starter blocks until the test releases it. If start ran inline on the main + // thread, handle(_:result:) could not return to let the test release it. + let release = DispatchSemaphore(value: 0) + let finished = expectation(description: "Talsec finished") + var waitResult: DispatchTimeoutResult? + plugin.talsecStarter = { _ in + waitResult = release.wait(timeout: .now() + 1) + finished.fulfill() + } + + plugin.handle(startCall(config: validConfig)) { _ in } + release.signal() + + wait(for: [finished], timeout: 3) + XCTAssertEqual(waitResult, .success) + } + + func testStartRepliesOnTheMainThreadAfterTalsecStarts() { + let replied = expectation(description: "result delivered") + var talsecStarted = false + var repliedOnMainThread: Bool? + var reply: Any? = "unset" + plugin.talsecStarter = { _ in talsecStarted = true } + + plugin.handle(startCall(config: validConfig)) { value in + XCTAssertTrue(talsecStarted, "result must only be sent once Talsec.start has returned") + repliedOnMainThread = Thread.isMainThread + reply = value + replied.fulfill() + } + + wait(for: [replied], timeout: 2) + XCTAssertEqual(repliedOnMainThread, true) + XCTAssertNil(reply) + } + + func testStartWithInvalidConfigReturnsErrorWithoutStartingTalsec() { + var talsecStarted = false + plugin.talsecStarter = { _ in talsecStarted = true } + let replied = expectation(description: "error delivered") + var reply: Any? + + plugin.handle(startCall(config: "not json")) { value in + reply = value + replied.fulfill() + } + + wait(for: [replied], timeout: 2) + XCTAssertEqual((reply as? FlutterError)?.code, "configuration-exception") + XCTAssertFalse(talsecStarted) + } +} diff --git a/ios/freerasp/Sources/SwiftFreeraspPlugin.swift b/ios/freerasp/Sources/SwiftFreeraspPlugin.swift index f7414ff..9540401 100644 --- a/ios/freerasp/Sources/SwiftFreeraspPlugin.swift +++ b/ios/freerasp/Sources/SwiftFreeraspPlugin.swift @@ -8,6 +8,9 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler /// The singleton instance of `SwiftTalsecPlugin`. static let instance = SwiftFreeraspPlugin() + /// Starts the native SDK. Replaceable in tests so the call can be observed without running TalsecRuntime. + var talsecStarter: (TalsecConfig) -> Void = { Talsec.start(config: $0) } + private override init() {} /// Registers this plugin with the given `FlutterPluginRegistrar`. @@ -66,7 +69,7 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler return } - Talsec.start(config: flutterConfig.toNativeConfig()) + talsecStarter(flutterConfig.toNativeConfig()) // Flutter expects *some* result to be returned even if it's void result(nil) From b915dc6f217570915db043b4498767f43d399b06 Mon Sep 17 00:00:00 2001 From: Shoaib Akhtar Date: Wed, 14 Oct 2026 14:11:05 +0400 Subject: [PATCH 2/3] fix(ios): run Talsec.start off the platform thread Talsec.start generates a key over a synchronous keychain/XPC call. Called from the method channel handler it ran on the main thread, which on iOS 26 hung the UI for 30+ s and led to watchdog terminations. Start now runs on a serial background queue and the Flutter result is delivered on the main thread once it returns, so `await Talsec.instance.start(...)` keeps its meaning. Fixes #231 --- CHANGELOG.md | 8 ++++++++ ios/freerasp/Sources/SwiftFreeraspPlugin.swift | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6549274..b33bfdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### iOS + +#### Fixed + +- `Talsec.start` no longer runs on the platform (main) thread. Its keychain work could hang the UI for tens of seconds and get the app killed by the launch watchdog on iOS 26 ([#231](https://github.com/talsec/Free-RASP-Flutter/issues/231)). + ## [8.3.0] - 2026-11-09 - Android SDK version: 19.3.1 diff --git a/ios/freerasp/Sources/SwiftFreeraspPlugin.swift b/ios/freerasp/Sources/SwiftFreeraspPlugin.swift index 9540401..ddebe20 100644 --- a/ios/freerasp/Sources/SwiftFreeraspPlugin.swift +++ b/ios/freerasp/Sources/SwiftFreeraspPlugin.swift @@ -11,6 +11,10 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler /// Starts the native SDK. Replaceable in tests so the call can be observed without running TalsecRuntime. var talsecStarter: (TalsecConfig) -> Void = { Talsec.start(config: $0) } + /// Serial queue for `Talsec.start`, which performs keychain work over a synchronous XPC call + /// and must not block the platform thread. + private let startQueue = DispatchQueue(label: "talsec.app/freerasp/start", qos: .userInitiated) + private override init() {} /// Registers this plugin with the given `FlutterPluginRegistrar`. @@ -56,7 +60,7 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler } } - /// Runs Talsec with given configuration + /// Runs Talsec with given configuration on a background queue and replies once it has started. /// /// - Parameters: /// - args: The arguments received from Flutter which contains configuration @@ -69,10 +73,15 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler return } - talsecStarter(flutterConfig.toNativeConfig()) + let config = flutterConfig.toNativeConfig() + startQueue.async { [talsecStarter] in + talsecStarter(config) - // Flutter expects *some* result to be returned even if it's void - result(nil) + // Flutter expects *some* result to be returned even if it's void + DispatchQueue.main.async { + result(nil) + } + } } /// Blocks screen capture for the current UIWindow. From d2af1e50e549fc7d0a89edfa9bd36f37a3bdfa4e Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Mon, 9 Nov 2026 17:45:14 +0100 Subject: [PATCH 3/3] chore(example): drop hardcoded Foundation.framework link from RunnerTests The RunnerTests target linked Foundation through a path pinned to the iPhoneOS18.0 SDK inside the developer directory, which no longer exists with Xcode 26. Swift links Foundation automatically, so the reference, its build file and its group are removed. Co-Authored-By: Claude Opus 5.5 --- example/ios/Runner.xcodeproj/project.pbxproj | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4f59c2c..17f924f 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 0BD4F69B9FF969126588DDC0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 708D3446324AE10BC008699A /* Foundation.framework */; }; 0D0441A20FC5887DD6AFEB6C /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF37A6CE1BE2F1C80B1B1D8 /* RunnerTests.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -48,7 +47,6 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 708D3446324AE10BC008699A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -74,7 +72,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0BD4F69B9FF969126588DDC0 /* Foundation.framework in Frameworks */, 51C2B8803DC8DF7D7A96F6E9 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -94,20 +91,11 @@ isa = PBXGroup; children = ( D43E6C8CBA8A2CDFE487D91B /* Pods_Runner.framework */, - 1A4E2ED85FD2E7FDEBC714F9 /* iOS */, 96471C26EC1C9CEDE502CB82 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; - 1A4E2ED85FD2E7FDEBC714F9 /* iOS */ = { - isa = PBXGroup; - children = ( - 708D3446324AE10BC008699A /* Foundation.framework */, - ); - name = iOS; - sourceTree = ""; - }; 2E316B12ADE3E40CBF0903B4 /* RunnerTests */ = { isa = PBXGroup; children = (