diff --git a/Yattee/Helpers/ObjCExceptionHandler.h b/Yattee/Helpers/ObjCExceptionHandler.h new file mode 100644 index 00000000..a4d201fe --- /dev/null +++ b/Yattee/Helpers/ObjCExceptionHandler.h @@ -0,0 +1,17 @@ +// +// ObjCExceptionHandler.h +// Yattee +// +// Catches ObjC NSExceptions that Swift cannot handle natively. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Executes a block and catches any NSException thrown. +/// Returns YES if the block executed without throwing, NO if an exception was caught. +/// If an exception is caught, it is returned via the outException parameter. +BOOL tryCatchObjCException(void (NS_NOESCAPE ^block)(void), NSException *_Nullable *_Nullable outException); + +NS_ASSUME_NONNULL_END diff --git a/Yattee/Helpers/ObjCExceptionHandler.m b/Yattee/Helpers/ObjCExceptionHandler.m new file mode 100644 index 00000000..dac39604 --- /dev/null +++ b/Yattee/Helpers/ObjCExceptionHandler.m @@ -0,0 +1,20 @@ +// +// ObjCExceptionHandler.m +// Yattee +// +// Catches ObjC NSExceptions that Swift cannot handle natively. +// + +#import "ObjCExceptionHandler.h" + +BOOL tryCatchObjCException(void (NS_NOESCAPE ^block)(void), NSException *_Nullable *_Nullable outException) { + @try { + block(); + return YES; + } @catch (NSException *exception) { + if (outException) { + *outException = exception; + } + return NO; + } +} diff --git a/Yattee/Services/Downloads/DownloadManager+Execution.swift b/Yattee/Services/Downloads/DownloadManager+Execution.swift index 17644557..104e3498 100644 --- a/Yattee/Services/Downloads/DownloadManager+Execution.swift +++ b/Yattee/Services/Downloads/DownloadManager+Execution.swift @@ -99,10 +99,41 @@ extension DownloadManager { resumeData: Data?, httpHeaders: [String: String]? = nil ) { + guard urlSession != nil else { + LoggingService.shared.logDownloadError( + "[Downloads] URLSession is nil in startStreamDownload (\(phase))", + error: DownloadError.downloadFailed("URLSession not available - session may be invalidated") + ) + handleDownloadError( + downloadID: downloadID, + phase: phase, + error: DownloadError.downloadFailed("URLSession not available") + ) + return + } + let task: URLSessionDownloadTask if let resumeData { - task = urlSession.downloadTask(withResumeData: resumeData) + var caughtException: NSException? + var resumeTask: URLSessionDownloadTask? + let success = tryCatchObjCException({ + resumeTask = self.urlSession.downloadTask(withResumeData: resumeData) + }, &caughtException) + + guard success, let resumeTask else { + LoggingService.shared.logDownloadError( + "[Downloads] NSException creating resume task (\(phase))", + error: DownloadError.downloadFailed("CFNetwork exception: \(caughtException?.reason ?? "unknown")") + ) + handleDownloadError( + downloadID: downloadID, + phase: phase, + error: DownloadError.downloadFailed("Failed to create download task: \(caughtException?.reason ?? "unknown")") + ) + return + } + task = resumeTask } else { // Starting fresh without resumeData - reset progress for this phase // to avoid jumping when saved progress conflicts with new URLSession progress @@ -128,7 +159,26 @@ extension DownloadManager { request.setValue(value, forHTTPHeaderField: key) } } - task = urlSession.downloadTask(with: request) + + var caughtException: NSException? + var newTask: URLSessionDownloadTask? + let success = tryCatchObjCException({ + newTask = self.urlSession.downloadTask(with: request) + }, &caughtException) + + guard success, let newTask else { + LoggingService.shared.logDownloadError( + "[Downloads] NSException creating download task (\(phase))", + error: DownloadError.downloadFailed("CFNetwork exception: \(caughtException?.reason ?? "unknown")") + ) + handleDownloadError( + downloadID: downloadID, + phase: phase, + error: DownloadError.downloadFailed("Failed to create download task: \(caughtException?.reason ?? "unknown")") + ) + return + } + task = newTask } task.taskDescription = "\(downloadID.uuidString):\(phase.rawValue)" diff --git a/Yattee/Services/Downloads/DownloadManager.swift b/Yattee/Services/Downloads/DownloadManager.swift index 634fa34b..b49aed0d 100644 --- a/Yattee/Services/Downloads/DownloadManager.swift +++ b/Yattee/Services/Downloads/DownloadManager.swift @@ -148,8 +148,9 @@ final class DownloadManager: NSObject { self.downloadSettings = settings // Invalidate old session before creating new one with correct settings urlSession?.invalidateAndCancel() + urlSession = nil setupSession() - + // Resume interrupted downloads only on initial setup if isInitialSetup { resumeInterruptedDownloads() @@ -186,8 +187,9 @@ final class DownloadManager: NSObject { } } - // 3. Invalidate the old session - urlSession.invalidateAndCancel() + // 3. Invalidate the old session (use finishTasksAndInvalidate since downloads are already paused) + urlSession?.finishTasksAndInvalidate() + urlSession = nil // 4. Create new session with updated cellular config setupSession() diff --git a/Yattee/Yattee-Bridging-Header.h b/Yattee/Yattee-Bridging-Header.h index 5d592292..5e984a0c 100644 --- a/Yattee/Yattee-Bridging-Header.h +++ b/Yattee/Yattee-Bridging-Header.h @@ -8,4 +8,5 @@ #ifndef Yattee_Bridging_Header_h #define Yattee_Bridging_Header_h #import "SMBBridge.h" +#import "ObjCExceptionHandler.h" #endif /* Yattee_Bridging_Header_h */