Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve various iOS crashes relating to bad access from multiple threads #129

Merged
merged 1 commit into from
Dec 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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