From 142e96d6415e4d5f8bcf8b217993cd99e2764659 Mon Sep 17 00:00:00 2001 From: Joey Pender Date: Tue, 16 Apr 2024 15:31:19 -0500 Subject: [PATCH] fix(iOS): KV, Context fixes (#83) --- .changeset/late-peas-scream.md | 8 ++ apps/example-app/ios/App/Podfile.lock | 2 +- .../ios/Plugin/BackgroundRunner.swift | 122 ++++++++---------- .../ios/Plugin/BackgroundRunnerErrors.swift | 1 + .../ios/Plugin/BackgroundRunnerPlugin.swift | 29 ++++- .../ios/Plugin/CapacitorAPI/KV.swift | 6 +- .../ios/Plugin/RunnerConfig.swift | 4 +- .../Sources/RunnerEngine/Runner.swift | 17 +-- 8 files changed, 94 insertions(+), 95 deletions(-) create mode 100644 .changeset/late-peas-scream.md diff --git a/.changeset/late-peas-scream.md b/.changeset/late-peas-scream.md new file mode 100644 index 0000000..178c1fb --- /dev/null +++ b/.changeset/late-peas-scream.md @@ -0,0 +1,8 @@ +--- +"@capacitor/background-runner": minor +"ios-engine": minor +--- + +(iOS) KV: calling `get` on an non-existent key returns null instead of empty object +(iOS) Fixed an issue within Runner that could potentially cause EXC_BAD_ACCESS crashes + diff --git a/apps/example-app/ios/App/Podfile.lock b/apps/example-app/ios/App/Podfile.lock index b445168..2734b85 100644 --- a/apps/example-app/ios/App/Podfile.lock +++ b/apps/example-app/ios/App/Podfile.lock @@ -49,4 +49,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 023978344a0d202e5020371cd007ff62c16044ac -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/packages/capacitor-plugin/ios/Plugin/BackgroundRunner.swift b/packages/capacitor-plugin/ios/Plugin/BackgroundRunner.swift index d63c77e..ae33994 100644 --- a/packages/capacitor-plugin/ios/Plugin/BackgroundRunner.swift +++ b/packages/capacitor-plugin/ios/Plugin/BackgroundRunner.swift @@ -5,8 +5,8 @@ import JavaScriptCore public class BackgroundRunner { public static let shared = BackgroundRunner() - - private var config: RunnerConfig? + public var config: RunnerConfig? + private var runner = Runner() public init() { @@ -17,93 +17,51 @@ public class BackgroundRunner { print("could not initialize BackgroundRunner: \(error)") } } - - public func registerBackgroundTask() { + + public func scheduleBackgroundTasks() throws { guard let config = config else { - print("no runner to register") - return + throw BackgroundRunnerPluginError.noRunnerConfig } - - if config.event == nil { - print("cannot register background task without a event to call") - return - } - - BGTaskScheduler.shared.register(forTaskWithIdentifier: config.label, using: nil) { task in - do { - task.expirationHandler = { - print("task timed out") - } - - guard let event = config.event else { - throw BackgroundRunnerPluginError.invalidRunnerConfig(reason: "runner event is missing or invalid") - } - - _ = try BackgroundRunner.shared.execute(config: config, event: event) - - task.setTaskCompleted(success: true) - } catch { - print("background task error: \(error)") - task.setTaskCompleted(success: false) - } - } - } - - public func scheduleBackgroundTasks() { - guard let config = config else { + + if !config.autoSchedule { return } guard let interval = config.interval else { - print("cannot register background task without a configured interval") - return + throw BackgroundRunnerPluginError.invalidRunnerConfig(reason: "cannot register background task without a configured interval") } let request = BGAppRefreshTaskRequest(identifier: config.label) request.earliestBeginDate = Date(timeIntervalSinceNow: Double(interval) * 60) - do { - print("Scheduling \(config.label)") + print("Scheduling \(config.label)") - try BGTaskScheduler.shared.submit(request) - } catch { - print("Could not schedule app refresh: \(error)") - } - } - - public func getConfig() -> RunnerConfig? { - return config + try BGTaskScheduler.shared.submit(request) } - public func dispatchEvent(event: String, inputArgs: [String: Any]?, callbackId: String? = nil) throws { - if let config = config { - let waitGroup = DispatchGroup() - waitGroup.enter() - - var err: Error? + public func registerBackgroundTask() throws { + guard let config = config else { + throw BackgroundRunnerPluginError.noRunnerConfig + } - // swiftlint:disable:next unowned_variable_capture - DispatchQueue.global(qos: .userInitiated).async { [unowned self] in - do { - _ = try self.execute(config: config, event: event, inputArgs: inputArgs, callbackId: callbackId) - } catch { - err = error - print("[\(config.label)]: \(error)") + BGTaskScheduler.shared.register(forTaskWithIdentifier: config.label, using: nil) { task in + do { + task.expirationHandler = { + print("task timed out") } - waitGroup.leave() - } + _ = try BackgroundRunner.shared.execute(config: config) - waitGroup.wait() - - if let err = err { - throw err + task.setTaskCompleted(success: true) + } catch { + print("background task error: \(error)") + task.setTaskCompleted(success: false) } } } - + // swiftlint:disable:next cyclomatic_complexity function_body_length - public func execute(config: RunnerConfig, event: String, inputArgs: [String: Any]? = nil, callbackId: String? = nil) throws -> [String: Any]? { + public func execute(config: RunnerConfig, inputArgs: [String: Any]? = nil, callbackId: String? = nil) throws -> [String: Any]? { do { let context = try initContext(config: config, callbackId: callbackId) @@ -170,9 +128,10 @@ public class BackgroundRunner { waitGroup.enter() - try context.dispatchEvent(event: event, details: args) + try context.dispatchEvent(event: config.event, details: args) waitGroup.wait() + if let rejection = rejectionErr { throw EngineError.jsException(details: rejection.message) @@ -185,6 +144,33 @@ public class BackgroundRunner { } } + public func dispatchEvent(event: String, inputArgs: [String: Any]?, callbackId: String? = nil) throws { + if let config = config { + let waitGroup = DispatchGroup() + waitGroup.enter() + + var err: Error? + + // swiftlint:disable:next unowned_variable_capture + DispatchQueue.global(qos: .userInitiated).async { [unowned self] in + do { + _ = try self.execute(config: config, inputArgs: inputArgs, callbackId: callbackId) + } catch { + err = error + print("[\(config.label)]: \(error)") + } + + waitGroup.leave() + } + + waitGroup.wait() + + if let err = err { + throw err + } + } + } + private func loadRunnerConfig() throws -> RunnerConfig? { guard let capacitorConfigFileURL = Bundle.main.url(forResource: "capacitor.config.json", withExtension: nil) else { throw BackgroundRunnerPluginError.runnerError(reason: "capacitor.config.json file not found") diff --git a/packages/capacitor-plugin/ios/Plugin/BackgroundRunnerErrors.swift b/packages/capacitor-plugin/ios/Plugin/BackgroundRunnerErrors.swift index 1fc6826..317249f 100644 --- a/packages/capacitor-plugin/ios/Plugin/BackgroundRunnerErrors.swift +++ b/packages/capacitor-plugin/ios/Plugin/BackgroundRunnerErrors.swift @@ -1,6 +1,7 @@ import Foundation public enum BackgroundRunnerPluginError: Error, Equatable { + case noRunnerConfig case invalidRunnerConfig(reason: String) case invalidArgument(reason: String) case runnerError(reason: String) diff --git a/packages/capacitor-plugin/ios/Plugin/BackgroundRunnerPlugin.swift b/packages/capacitor-plugin/ios/Plugin/BackgroundRunnerPlugin.swift index 27e1f69..832a6a3 100644 --- a/packages/capacitor-plugin/ios/Plugin/BackgroundRunnerPlugin.swift +++ b/packages/capacitor-plugin/ios/Plugin/BackgroundRunnerPlugin.swift @@ -13,6 +13,7 @@ public class BackgroundRunnerPlugin: CAPPlugin { name: UIApplication.didEnterBackgroundNotification, object: nil ) + initWatchConnectivity() } @@ -54,22 +55,23 @@ public class BackgroundRunnerPlugin: CAPPlugin { @objc func dispatchEvent(_ call: CAPPluginCall) { do { - guard let runnerEvent = call.getString("event") else { + guard let runnerEvent = call.getString("event"), !runnerEvent.isEmpty else { throw BackgroundRunnerPluginError.invalidArgument(reason: "event is missing or invalid") } let details = call.getObject("details", JSObject()) - guard let config = impl.getConfig() else { - throw BackgroundRunnerPluginError.runnerError(reason: "no runner config loaded") + guard var config = impl.config else { + throw BackgroundRunnerPluginError.noRunnerConfig } + + config.event = runnerEvent // swiftlint:disable:next unowned_variable_capture DispatchQueue.global(qos: .userInitiated).async { [unowned self] in do { let result = try self.impl.execute( config: config, - event: runnerEvent, inputArgs: details as [String: Any], callbackId: call.callbackId ) @@ -94,14 +96,27 @@ public class BackgroundRunnerPlugin: CAPPlugin { } @objc private func didEnterBackground() { - impl.scheduleBackgroundTasks() + do { + try impl.scheduleBackgroundTasks() + } catch { + print("could not schedule background task: \(error)") + } } public static func registerBackgroundTask() { - BackgroundRunner.shared.registerBackgroundTask() + do { + try BackgroundRunner.shared.registerBackgroundTask() + } catch { + print("could not register background task: \(error)") + } } public static func dispatchEvent(event: String, eventArgs: [AnyHashable: Any], completionHandler: ((Result) -> Void)) { + if event.isEmpty { + completionHandler(.failure(BackgroundRunnerPluginError.invalidArgument(reason: "event is missing or invalid"))) + return + } + var args: [String: Any] = [:] eventArgs.forEach { (key: AnyHashable, value: Any) in @@ -147,7 +162,7 @@ public class BackgroundRunnerPlugin: CAPPlugin { return } - guard let config = impl.getConfig() else { + guard let config = impl.config else { return } diff --git a/packages/capacitor-plugin/ios/Plugin/CapacitorAPI/KV.swift b/packages/capacitor-plugin/ios/Plugin/CapacitorAPI/KV.swift index 6d441b3..395b306 100644 --- a/packages/capacitor-plugin/ios/Plugin/CapacitorAPI/KV.swift +++ b/packages/capacitor-plugin/ios/Plugin/CapacitorAPI/KV.swift @@ -13,9 +13,11 @@ class CapacitorKVStore: NSObject, CapacitorKVStoreExports { } static func get(_ key: String) -> JSValue { + guard let value = UserDefaults.standard.string(forKey: key) else { + return JSValue(nullIn: JSContext.current()) + } + var valueWrapper: [String: String] = [:] - - let value = UserDefaults.standard.string(forKey: key) valueWrapper["value"] = value return JSValue(object: valueWrapper, in: JSContext.current()) diff --git a/packages/capacitor-plugin/ios/Plugin/RunnerConfig.swift b/packages/capacitor-plugin/ios/Plugin/RunnerConfig.swift index 25345a9..fc44061 100644 --- a/packages/capacitor-plugin/ios/Plugin/RunnerConfig.swift +++ b/packages/capacitor-plugin/ios/Plugin/RunnerConfig.swift @@ -5,7 +5,7 @@ public struct RunnerConfig { let label: String let src: String var autoSchedule: Bool - let event: String? + var event: String let repeats: Bool? let enableWatchConnectivity: Bool @@ -20,7 +20,7 @@ public struct RunnerConfig { throw BackgroundRunnerPluginError.invalidRunnerConfig(reason: "runner source file path is missing or invalid") } - let event = jsObject["event"] as? String + let event = jsObject["event"] as? String ?? "" let repeats = jsObject["repeat"] as? Bool let interval = jsObject["interval"] as? Int let autoStart = jsObject["autoStart"] as? Bool diff --git a/packages/ios-engine/Sources/RunnerEngine/Runner.swift b/packages/ios-engine/Sources/RunnerEngine/Runner.swift index a0505a2..9b8f040 100644 --- a/packages/ios-engine/Sources/RunnerEngine/Runner.swift +++ b/packages/ios-engine/Sources/RunnerEngine/Runner.swift @@ -3,31 +3,17 @@ import JavaScriptCore public class Runner { var machine: JSVirtualMachine - var contexts: [String: Context] private let runLoop: RunLoop private var thread: Thread? public init() { machine = JSVirtualMachine() - contexts = [:] runLoop = RunLoop.current } func createContext(name: String) throws -> Context { - let context = try Context(vm: machine, contextName: name, runLoop: runLoop) - - if contexts.keys.contains(name) { - throw EngineError.runnerError(details: "context with name \(name) already exists") - } - - contexts[name] = context - - return context - } - - func destroyContext(name: String) { - contexts.removeValue(forKey: name) + return try Context(vm: machine, contextName: name, runLoop: runLoop) } func start() { @@ -54,6 +40,7 @@ public class Runner { } thread.cancel() + while !thread.isFinished { Thread.sleep(forTimeInterval: 0.05) }