Skip to content

Commit

Permalink
Resolve various iOS crashes relating to bad access from multiple thre…
Browse files Browse the repository at this point in the history
…ads (#129)
  • Loading branch information
Sebastian Roth authored Dec 22, 2020
1 parent 9dd202a commit cdd295f
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 7 deletions.
41 changes: 40 additions & 1 deletion example/integration_test/flutter_uploader_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ void main() {
expect(res.status, UploadTaskStatus.complete);
});

testWidgets('multiple uploads stresstest', (WidgetTester tester) async {
final taskIds = <String>[];
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');

Expand All @@ -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']),
Expand Down Expand Up @@ -192,6 +212,25 @@ void main() {
expect(res.status, UploadTaskStatus.complete);
});

testWidgets('multiple uploads stresstest', (WidgetTester tester) async {
final taskIds = <String>[];
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(),
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Expand All @@ -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",
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
buildConfiguration = "Debug"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<key>FUMaximumConnectionsPerHost</key>
<integer>5</integer>
<key>FUMaximumUploadOperation</key>
<integer>1</integer>
<integer>3</integer>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
Expand Down
2 changes: 1 addition & 1 deletion example/lib/upload_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class _UploadScreenState extends State<UploadScreen> {
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]);
Expand Down
10 changes: 9 additions & 1 deletion ios/Classes/CachingStreamHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,31 @@ class CachingStreamHandler<T>: 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

Expand Down
8 changes: 8 additions & 0 deletions ios/Classes/EngineManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
28 changes: 28 additions & 0 deletions ios/Classes/URLSessionUploader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)")

Expand All @@ -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")

Expand All @@ -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
Expand Down

0 comments on commit cdd295f

Please sign in to comment.