From 25a272be6291d1691c9d02b622f49269a49dca94 Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Tue, 22 Dec 2020 12:45:20 +0000 Subject: [PATCH] Resolve various iOS crashes relating to bad access from multiple threads --- .../flutter_uploader_test.dart | 41 ++++++++++++++++++- example/ios/Runner.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- example/ios/Runner/Info.plist | 2 +- example/lib/upload_screen.dart | 2 +- ios/Classes/CachingStreamHandler.swift | 10 ++++- ios/Classes/EngineManager.swift | 8 ++++ ios/Classes/URLSessionUploader.swift | 28 +++++++++++++ 8 files changed, 90 insertions(+), 7 deletions(-) diff --git a/example/integration_test/flutter_uploader_test.dart b/example/integration_test/flutter_uploader_test.dart index c95a298..d29a2c5 100644 --- a/example/integration_test/flutter_uploader_test.dart +++ b/example/integration_test/flutter_uploader_test.dart @@ -65,6 +65,27 @@ void main() { expect(res.status, UploadTaskStatus.complete); }); + testWidgets('multiple uploads stresstest', (WidgetTester tester) async { + final taskIds = []; + for (var i = 0; i < 10; i++) { + taskIds.add(await uploader.enqueue( + MultipartFormDataUpload(url: url.toString(), files: [ + FileItem(path: await _tmpFile(), field: 'file'), + ]), + )); + } + + final res = await Future.wait( + taskIds.map( + (taskId) => uploader.result.firstWhere(isCompleted(taskId)), + ), + ); + + for (var i = 0; i < res.length; i++) { + expect(res[i].taskId, taskIds[i]); + } + }); + testWidgets('can submit custom data', (tester) async { var fileItem = FileItem(path: await _tmpFile(), field: 'file'); @@ -85,7 +106,6 @@ void main() { final res = await uploader.result.firstWhere(isCompleted(taskId)); final json = jsonDecode(res.response); - print(json); expect(json['request']['fields']['simpleKey'], 'simpleValue'); expect(jsonDecode(json['request']['fields']['listOf']), @@ -192,6 +212,25 @@ void main() { expect(res.status, UploadTaskStatus.complete); }); + testWidgets('multiple uploads stresstest', (WidgetTester tester) async { + final taskIds = []; + for (var i = 0; i < 10; i++) { + taskIds.add(await uploader.enqueue( + RawUpload(url: url.toString(), path: await _tmpFile()), + )); + } + + final res = await Future.wait( + taskIds.map( + (taskId) => uploader.result.firstWhere(isCompleted(taskId)), + ), + ); + + for (var i = 0; i < res.length; i++) { + expect(res[i].taskId, taskIds[i]); + } + }); + testWidgets("can overwrite 'Accept' header", (WidgetTester tester) async { final taskId = await uploader.enqueue(RawUpload( url: url.toString(), diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index a08f012..245ed66 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -293,11 +293,11 @@ "${PODS_ROOT}/../Flutter/Flutter.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", - "${BUILT_PRODUCTS_DIR}/e2e/e2e.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_uploader/flutter_uploader.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", + "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", ); @@ -309,11 +309,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/e2e.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_uploader.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", ); diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 31d8d1f..16a1037 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -61,7 +61,7 @@ FUMaximumConnectionsPerHost 5 FUMaximumUploadOperation - 1 + 3 LSRequiresIPhoneOS NSCameraUsageDescription diff --git a/example/lib/upload_screen.dart b/example/lib/upload_screen.dart index db2cc94..786e748 100644 --- a/example/lib/upload_screen.dart +++ b/example/lib/upload_screen.dart @@ -181,7 +181,7 @@ class _UploadScreenState extends State { allowCompression: false, allowMultiple: true, ); - if (files.count > 0) { + if (files != null && files.count > 0) { if (binary) { for (var file in files.files) { _handleFileUpload([file.path]); diff --git a/ios/Classes/CachingStreamHandler.swift b/ios/Classes/CachingStreamHandler.swift index 8edde23..fdc0e56 100644 --- a/ios/Classes/CachingStreamHandler.swift +++ b/ios/Classes/CachingStreamHandler.swift @@ -11,23 +11,31 @@ class CachingStreamHandler: NSObject, FlutterStreamHandler { var cache: [String:T] = [:] var eventSink: FlutterEventSink? + + private let cacheSemaphore = DispatchSemaphore(value: 1) func add(_ id: String, _ value: T) { + cacheSemaphore.wait() cache[id] = value - + cacheSemaphore.signal() + if let sink = eventSink { sink(value) } } func clear() { + cacheSemaphore.wait() cache.removeAll() + cacheSemaphore.signal() } func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + cacheSemaphore.wait() for cacheEntry in cache { events(cacheEntry.value) } + cacheSemaphore.signal() self.eventSink = events diff --git a/ios/Classes/EngineManager.swift b/ios/Classes/EngineManager.swift index a2307b0..8100946 100644 --- a/ios/Classes/EngineManager.swift +++ b/ios/Classes/EngineManager.swift @@ -10,8 +10,16 @@ import Foundation class EngineManager { private var headlessRunner: FlutterEngine? public var registerPlugins: FlutterPluginRegistrantCallback? + + private let semaphore = DispatchSemaphore(value: 1) private func startEngineIfNeeded() { + semaphore.wait() + + defer { + semaphore.signal() + } + guard let callbackHandle = UploaderDefaults.shared.callbackHandle else { if let runner = headlessRunner { runner.destroyContext() diff --git a/ios/Classes/URLSessionUploader.swift b/ios/Classes/URLSessionUploader.swift index 6cd0d78..6648f17 100644 --- a/ios/Classes/URLSessionUploader.swift +++ b/ios/Classes/URLSessionUploader.swift @@ -18,6 +18,9 @@ class URLSessionUploader: NSObject { var session: URLSession? let queue = OperationQueue() + + // Accessing uploadedData & runningTaskById will require exclusive access + private let semaphore = DispatchSemaphore(value: 1) // Reference for uploaded data. var uploadedData = [String: Data]() @@ -51,7 +54,10 @@ class URLSessionUploader: NSObject { delegates.uploadEnqueued(taskId: taskId) uploadTask.resume() + + semaphore.wait() self.runningTaskById[taskId] = UploadTask(taskId: taskId, status: .enqueue, progress: 0) + semaphore.signal() return uploadTask } @@ -153,6 +159,11 @@ extension URLSessionUploader: URLSessionDelegate, URLSessionDataDelegate, URLSes } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + semaphore.wait() + defer { + semaphore.signal() + } + NSLog("URLSessionDidReceiveData:") guard let uploadTask = dataTask as? URLSessionUploadTask else { @@ -179,6 +190,11 @@ extension URLSessionUploader: URLSessionDelegate, URLSessionDataDelegate, URLSes } public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + semaphore.wait() + defer { + semaphore.signal() + } + if totalBytesExpectedToSend == NSURLSessionTransferSizeUnknown { NSLog("Unknown transfer size") } else { @@ -191,6 +207,7 @@ extension URLSessionUploader: URLSessionDelegate, URLSessionDataDelegate, URLSes let bytesExpectedToSend = Double(integerLiteral: totalBytesExpectedToSend) let tBytesSent = Double(integerLiteral: totalBytesSent) let progress = round(Double(tBytesSent / bytesExpectedToSend * 100)) + let runningTask = self.runningTaskById[taskId] NSLog("URLSessionDidSendBodyData: taskId: \(taskId), byteSent: \(bytesSent), totalBytesSent: \(totalBytesSent), totalBytesExpectedToSend: \(totalBytesExpectedToSend), progress:\(progress)") @@ -211,7 +228,13 @@ extension URLSessionUploader: URLSessionDelegate, URLSessionDataDelegate, URLSes public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { NSLog("URLSessionDidFinishEvents:") + session.getTasksWithCompletionHandler { (_, uploadTasks, _) in + self.semaphore.wait() + defer { + self.semaphore.signal() + } + if uploadTasks.isEmpty { NSLog("all upload tasks have been completed") @@ -222,6 +245,11 @@ extension URLSessionUploader: URLSessionDelegate, URLSessionDataDelegate, URLSes } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + semaphore.wait() + defer { + semaphore.signal() + } + guard let uploadTask = task as? URLSessionUploadTask else { NSLog("URLSessionDidCompleteWithError: not an uplaod task") return