From ef1612a8a7bdfc71a77bbb5bc01dc3b970392db7 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:18:50 +0530 Subject: [PATCH] debug: add native file logger --- mobile/ios/Runner.xcodeproj/project.pbxproj | 4 +++ .../Runner/Background/BackgroundWorker.swift | 29 ++++++++++++---- .../Background/BackgroundWorkerApiImpl.swift | 24 ++++++++++---- mobile/ios/Runner/Background/FileLogger.swift | 33 +++++++++++++++++++ mobile/ios/Runner/Info.plist | 4 ++- 5 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 mobile/ios/Runner/Background/FileLogger.swift diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index fbffa69ba5..3ef4556033 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01DD6982F7F43B40049AB63 /* ImageRequest.swift */; }; B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; }; B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; }; + B21E34B02E5B09190031FDB9 /* FileLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34B12E5B09100031FDB9 /* FileLogger.swift */; }; B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; }; B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; }; B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; }; @@ -103,6 +104,7 @@ B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = ""; }; B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = ""; }; + B21E34B12E5B09100031FDB9 /* FileLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogger.swift; sourceTree = ""; }; B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = ""; }; B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = ""; }; B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = ""; }; @@ -304,6 +306,7 @@ B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */, B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */, B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */, + B21E34B12E5B09100031FDB9 /* FileLogger.swift */, ); path = Background; sourceTree = ""; @@ -614,6 +617,7 @@ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, + B21E34B02E5B09190031FDB9 /* FileLogger.swift in Sources */, FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */, FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */, FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */, diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index c5b5e1778a..311365a028 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -80,29 +80,34 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * starts the engine, and sets up a timeout timer if specified. */ func run() { + FileLogger.log("BackgroundWorker:run Starting Flutter engine for taskType=\(taskType) maxSeconds=\(maxSeconds.map(String.init) ?? "nil")") // Start the Flutter engine with the specified callback as the entry point let isRunning = engine.run( withEntrypoint: "backgroundSyncNativeEntrypoint", libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart" ) - + // Verify that the Flutter engine started successfully if !isRunning { + FileLogger.log("BackgroundWorker:run Flutter engine failed to start, completing with success=false") complete(success: false) return } - + FileLogger.log("BackgroundWorker:run Flutter engine started") + // Register plugins in the new engine GeneratedPluginRegistrant.register(with: engine) // Register custom plugins AppDelegate.registerPlugins(with: engine, messenger: engine.binaryMessenger) flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger) BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self) - + FileLogger.log("BackgroundWorker:run Plugins registered, waiting for Flutter onInitialized") + // Set up a timeout timer if maxSeconds was specified to prevent runaway background tasks if maxSeconds != nil { // Schedule a timer to cancel the task after the specified timeout period Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in + FileLogger.log("BackgroundWorker:run maxSeconds=\(self.maxSeconds!) timer fired, closing task") self.close() } } @@ -114,6 +119,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * This method acts as a bridge between the native iOS background task system and Flutter. */ func onInitialized() throws { + FileLogger.log("BackgroundWorker:onInitialized Flutter ready, calling onIosUpload isRefresh=\(self.taskType == .refresh)") flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in self.handleHostResult(result: result) }) @@ -126,16 +132,22 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { */ func close() { if isComplete { + FileLogger.log("BackgroundWorker:close Already complete, ignoring close()") return } + FileLogger.log("BackgroundWorker:close Cancel requested, signaling Flutter (taskType=\(taskType))") flutterApi?.cancel { result in + FileLogger.log("BackgroundWorker:close Flutter cancel acknowledged") self.complete(success: false) } // Fallback safety mechanism: ensure completion is called within 2 seconds // This prevents the background task from hanging indefinitely if Flutter doesn't respond Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in + if !self.isComplete { + FileLogger.log("BackgroundWorker:close 2s fallback fired, Flutter did not acknowledge cancel") + } self.complete(success: false) } } @@ -149,8 +161,12 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { */ private func handleHostResult(result: Result) { switch result { - case .success(): self.complete(success: true) - case .failure(_): self.close() + case .success(): + FileLogger.log("BackgroundWorker:handleHostResult Flutter onIosUpload succeeded (taskType=\(taskType))") + self.complete(success: true) + case .failure(let error): + FileLogger.log("BackgroundWorker:handleHostResult Flutter onIosUpload failed: \(error.localizedDescription) (taskType=\(taskType))") + self.close() } } @@ -166,7 +182,8 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { if(isComplete) { return } - + + FileLogger.log("BackgroundWorker:complete Tearing down engine, success=\(success) (taskType=\(taskType))") isComplete = true AppDelegate.cancelPlugins(with: engine) engine.destroyContext() diff --git a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift index b6272042ce..706bfc8aae 100644 --- a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift +++ b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift @@ -5,7 +5,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { func enable() throws { BackgroundWorkerApiImpl.scheduleRefreshWorker() BackgroundWorkerApiImpl.scheduleProcessingWorker() - print("BackgroundWorkerApiImpl:enable Background worker scheduled") + FileLogger.log("BackgroundWorkerApiImpl:enable Background worker scheduled") } func configure(settings: BackgroundWorkerSettings) throws { @@ -19,7 +19,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { func disable() throws { BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID); BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID); - print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers") + FileLogger.log("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers") } private static let refreshTaskID = "app.alextran.immich.background.refreshUpload" @@ -30,6 +30,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { BGTaskScheduler.shared.register( forTaskWithIdentifier: processingTaskID, using: nil) { task in if task is BGProcessingTask { + FileLogger.log("BackgroundWorkerApiImpl:BGProcessingTask Background Processing task received") handleBackgroundProcessing(task: task as! BGProcessingTask) } } @@ -37,9 +38,11 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { BGTaskScheduler.shared.register( forTaskWithIdentifier: refreshTaskID, using: nil) { task in if task is BGAppRefreshTask { + FileLogger.log("BackgroundWorkerApiImpl:BGAppRefreshTask Background Refresh task received") handleBackgroundRefresh(task: task as! BGAppRefreshTask) } } + FileLogger.log("BackgroundWorkerApiImpl:registerBackgroundWorkers Background workers registered") } private static func scheduleRefreshWorker() { @@ -48,8 +51,9 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { do { try BGTaskScheduler.shared.submit(backgroundRefresh) + FileLogger.log("BackgroundWorkerApiImpl:scheduleRefreshWorker Scheduled Refresh task") } catch { - print("Could not schedule the refresh upload task \(error.localizedDescription)") + FileLogger.log("BackgroundWorkerApiImpl:scheduleRefreshWorker Could not schedule the refresh upload task \(error.localizedDescription)") } } @@ -61,25 +65,32 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { do { try BGTaskScheduler.shared.submit(backgroundProcessing) + FileLogger.log("BackgroundWorkerApiImpl:scheduleProcessingWorker Scheduled Processing task") } catch { - print("Could not schedule the processing upload task \(error.localizedDescription)") + FileLogger.log("BackgroundWorkerApiImpl:scheduleProcessingWorker Could not schedule the processing upload task \(error.localizedDescription)") } } private static func handleBackgroundRefresh(task: BGAppRefreshTask) { + FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Entered, re-queuing next refresh task") scheduleRefreshWorker() // If another task is running, cede the background time back to the OS if taskSemaphore.wait(timeout: .now()) == .success { + FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Starting background worker") // Restrict the refresh task to run only for a maximum of (maxSeconds) seconds runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20) } else { + FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Processing task is in progress") task.setTaskCompleted(success: true) } } private static func handleBackgroundProcessing(task: BGProcessingTask) { + FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Entered, re-queuing next processing task") scheduleProcessingWorker() + FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Waiting for taskSemaphore") taskSemaphore.wait() + FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Semaphore acquired, starting background worker") // There are no restrictions for processing tasks. Although, the OS could signal expiration at any time runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil) } @@ -105,11 +116,12 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { } task.expirationHandler = { + FileLogger.log("BackgroundWorkerApiImpl:runBackgroundWorker iOS signaled expiration (taskType=\(taskType)), closing worker") DispatchQueue.main.async { backgroundWorker.close() } isSuccess = false - + // Schedule a timer to signal the semaphore after 2 seconds Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in semaphore.signal() @@ -122,6 +134,6 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { semaphore.wait() task.setTaskCompleted(success: isSuccess) - print("Background task completed with success: \(isSuccess)") + FileLogger.log("BackgroundWorkerApiImpl:runBackgroundWorker Background task completed with success: \(isSuccess)") } } diff --git a/mobile/ios/Runner/Background/FileLogger.swift b/mobile/ios/Runner/Background/FileLogger.swift new file mode 100644 index 0000000000..4c24801107 --- /dev/null +++ b/mobile/ios/Runner/Background/FileLogger.swift @@ -0,0 +1,33 @@ +import Foundation + +enum FileLogger { + private static let queue = DispatchQueue(label: "app.alextran.immich.FileLogger") + private static let isoFormatter: ISO8601DateFormatter = { + let f = ISO8601DateFormatter() + f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return f + }() + + private static var logFileURL: URL? { + guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + else { return nil } + return docs.appendingPathComponent("background_log.txt") + } + + static func log(_ message: String) { + let line = "[\(isoFormatter.string(from: Date()))] \(message)\n" + print(line, terminator: "") + queue.async { + guard let url = logFileURL, let data = line.data(using: .utf8) else { return } + if FileManager.default.fileExists(atPath: url.path) { + if let handle = try? FileHandle(forWritingTo: url) { + defer { try? handle.close() } + try? handle.seekToEnd() + try? handle.write(contentsOf: data) + } + } else { + try? data.write(to: url, options: .atomic) + } + } + } +} diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 3b030e4f86..a1fe633c5e 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -115,7 +115,9 @@ LSRequiresIPhoneOS LSSupportsOpeningDocumentsInPlace - No + + UIFileSharingEnabled + MGLMapboxMetricsEnabledSettingShownInApp NSAppTransportSecurity