@capgo/capacitor-webview-crash 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoCapacitorWebViewCrash.podspec +17 -0
- package/LICENSE +373 -0
- package/Package.swift +28 -0
- package/README.md +293 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/app/capgo/webviewcrash/WebViewCrash.java +288 -0
- package/android/src/main/java/app/capgo/webviewcrash/WebViewCrashPlugin.java +185 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +271 -0
- package/dist/esm/definitions.d.ts +139 -0
- package/dist/esm/definitions.js +3 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +20 -0
- package/dist/esm/web.js +90 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +104 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +107 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/WebViewCrashPlugin/WebViewCrash.swift +359 -0
- package/ios/Sources/WebViewCrashPlugin/WebViewCrashPlugin.swift +152 -0
- package/ios/Tests/WebViewCrashPluginTests/WebViewCrashPluginTests.swift +12 -0
- package/package.json +94 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import Foundation
|
|
3
|
+
import ObjectiveC.runtime
|
|
4
|
+
import UIKit
|
|
5
|
+
import WebKit
|
|
6
|
+
|
|
7
|
+
enum WebViewCrashBridge {
|
|
8
|
+
static let crashEventName = "webViewRestoredAfterCrash"
|
|
9
|
+
static let restartEventName = "webViewRestoredAfterRestart"
|
|
10
|
+
static let periodicRestartReason = "periodicRestart"
|
|
11
|
+
static let manualRestartReason = "manualRestart"
|
|
12
|
+
|
|
13
|
+
static func pendingResult(_ value: [String: Any]?) -> [String: Any] {
|
|
14
|
+
[
|
|
15
|
+
"value": value ?? NSNull()
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct WebViewCrashRestartOptions {
|
|
21
|
+
let restartOnCrash: Bool
|
|
22
|
+
let restartIntervalMs: Int
|
|
23
|
+
let restartCron: WebViewCrashCronSchedule?
|
|
24
|
+
let restartAfterCrashDelayMs: Int
|
|
25
|
+
|
|
26
|
+
init(config: PluginConfig? = nil) {
|
|
27
|
+
let intervalMs = max(0, config?.getInt("restartIntervalMs", 0) ?? 0)
|
|
28
|
+
let cronExpression = config?.getString("restartCron", "") ?? ""
|
|
29
|
+
|
|
30
|
+
if intervalMs > 0 && !cronExpression.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
31
|
+
fatalError("Invalid WebViewCrash config: set either restartIntervalMs or restartCron, not both.")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
restartOnCrash = config?.getBoolean("restartOnCrash", true) ?? true
|
|
35
|
+
restartIntervalMs = intervalMs
|
|
36
|
+
restartCron = WebViewCrashCronSchedule(cronExpression)
|
|
37
|
+
restartAfterCrashDelayMs = max(0, config?.getInt("restartAfterCrashDelayMs", 0) ?? 0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var restartIntervalSeconds: TimeInterval {
|
|
41
|
+
TimeInterval(restartIntervalMs) / 1_000
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var nextRestartDelaySeconds: TimeInterval? {
|
|
45
|
+
if let restartCron {
|
|
46
|
+
return restartCron.nextDelaySeconds()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return restartIntervalMs > 0 ? restartIntervalSeconds : nil
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var restartAfterCrashDelaySeconds: TimeInterval {
|
|
53
|
+
TimeInterval(restartAfterCrashDelayMs) / 1_000
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
struct WebViewCrashCronSchedule {
|
|
58
|
+
private static let searchLimitMinutes = 366 * 24 * 60 * 5
|
|
59
|
+
|
|
60
|
+
private let minutes: CronField
|
|
61
|
+
private let hours: CronField
|
|
62
|
+
private let daysOfMonth: CronField
|
|
63
|
+
private let months: CronField
|
|
64
|
+
private let daysOfWeek: CronField
|
|
65
|
+
|
|
66
|
+
init?(_ expression: String?) {
|
|
67
|
+
guard let expression else {
|
|
68
|
+
return nil
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let parts = expression
|
|
72
|
+
.split(whereSeparator: { $0 == " " || $0 == "\t" || $0 == "\n" })
|
|
73
|
+
.map(String.init)
|
|
74
|
+
|
|
75
|
+
guard parts.count == 5,
|
|
76
|
+
let minutes = CronField(parts[0], min: 0, max: 59),
|
|
77
|
+
let hours = CronField(parts[1], min: 0, max: 23),
|
|
78
|
+
let daysOfMonth = CronField(parts[2], min: 1, max: 31),
|
|
79
|
+
let months = CronField(parts[3], min: 1, max: 12),
|
|
80
|
+
let daysOfWeek = CronField(parts[4], min: 0, max: 7, normalizeSunday: true) else {
|
|
81
|
+
return nil
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
self.minutes = minutes
|
|
85
|
+
self.hours = hours
|
|
86
|
+
self.daysOfMonth = daysOfMonth
|
|
87
|
+
self.months = months
|
|
88
|
+
self.daysOfWeek = daysOfWeek
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func nextDelaySeconds(from date: Date = Date(), calendar sourceCalendar: Calendar = .current) -> TimeInterval? {
|
|
92
|
+
var calendar = sourceCalendar
|
|
93
|
+
calendar.timeZone = .current
|
|
94
|
+
|
|
95
|
+
guard var candidate = calendar.date(byAdding: .minute, value: 1, to: date) else {
|
|
96
|
+
return nil
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: candidate)
|
|
100
|
+
components.second = 0
|
|
101
|
+
components.nanosecond = 0
|
|
102
|
+
guard let roundedCandidate = calendar.date(from: components) else {
|
|
103
|
+
return nil
|
|
104
|
+
}
|
|
105
|
+
candidate = roundedCandidate
|
|
106
|
+
|
|
107
|
+
for _ in 0..<Self.searchLimitMinutes {
|
|
108
|
+
if matches(candidate, calendar: calendar) {
|
|
109
|
+
return max(0, candidate.timeIntervalSince(date))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
guard let nextCandidate = calendar.date(byAdding: .minute, value: 1, to: candidate) else {
|
|
113
|
+
return nil
|
|
114
|
+
}
|
|
115
|
+
candidate = nextCandidate
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private func matches(_ date: Date, calendar: Calendar) -> Bool {
|
|
122
|
+
let components = calendar.dateComponents([.minute, .hour, .day, .month, .weekday], from: date)
|
|
123
|
+
|
|
124
|
+
guard let minute = components.minute,
|
|
125
|
+
let hour = components.hour,
|
|
126
|
+
let day = components.day,
|
|
127
|
+
let month = components.month,
|
|
128
|
+
let weekday = components.weekday else {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
guard minutes.matches(minute), hours.matches(hour), months.matches(month) else {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let cronWeekday = (weekday - 1) % 7
|
|
137
|
+
let dayOfMonthMatches = daysOfMonth.matches(day)
|
|
138
|
+
let dayOfWeekMatches = daysOfWeek.matches(cronWeekday)
|
|
139
|
+
|
|
140
|
+
if daysOfMonth.restricted && daysOfWeek.restricted {
|
|
141
|
+
return dayOfMonthMatches || dayOfWeekMatches
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return dayOfMonthMatches && dayOfWeekMatches
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private struct CronField {
|
|
148
|
+
let values: Set<Int>
|
|
149
|
+
let restricted: Bool
|
|
150
|
+
|
|
151
|
+
init?(_ expression: String, min: Int, max: Int, normalizeSunday: Bool = false) {
|
|
152
|
+
var values = Set<Int>()
|
|
153
|
+
|
|
154
|
+
for part in expression.split(separator: ",", omittingEmptySubsequences: false) {
|
|
155
|
+
guard Self.apply(String(part), to: &values, min: min, max: max, normalizeSunday: normalizeSunday) else {
|
|
156
|
+
return nil
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
guard !values.isEmpty else {
|
|
161
|
+
return nil
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let allValueCount = normalizeSunday ? 7 : max - min + 1
|
|
165
|
+
self.values = values
|
|
166
|
+
self.restricted = values.count != allValueCount
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func matches(_ value: Int) -> Bool {
|
|
170
|
+
values.contains(value)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private static func apply(_ part: String, to values: inout Set<Int>, min: Int, max: Int, normalizeSunday: Bool) -> Bool {
|
|
174
|
+
let stepParts = part.split(separator: "/", omittingEmptySubsequences: false)
|
|
175
|
+
guard stepParts.count <= 2 else {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var step = 1
|
|
180
|
+
if stepParts.count == 2 {
|
|
181
|
+
guard let parsedStep = Int(stepParts[1]), parsedStep > 0 else {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
step = parsedStep
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let rangePart = String(stepParts[0])
|
|
188
|
+
let start: Int
|
|
189
|
+
let end: Int
|
|
190
|
+
|
|
191
|
+
if rangePart == "*" {
|
|
192
|
+
start = min
|
|
193
|
+
end = max
|
|
194
|
+
} else if rangePart.contains("-") {
|
|
195
|
+
let range = rangePart.split(separator: "-", omittingEmptySubsequences: false)
|
|
196
|
+
guard range.count == 2, let rangeStart = Int(range[0]), let rangeEnd = Int(range[1]) else {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
start = rangeStart
|
|
200
|
+
end = rangeEnd
|
|
201
|
+
} else {
|
|
202
|
+
guard let value = Int(rangePart) else {
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
start = value
|
|
206
|
+
end = value
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
guard start >= min, end <= max, start <= end else {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
var value = start
|
|
214
|
+
while value <= end {
|
|
215
|
+
values.insert(normalize(value, normalizeSunday: normalizeSunday))
|
|
216
|
+
value += step
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private static func normalize(_ value: Int, normalizeSunday: Bool) -> Int {
|
|
223
|
+
normalizeSunday && value == 7 ? 0 : value
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
enum WebViewCrashRuntime {
|
|
229
|
+
private static var options = WebViewCrashRestartOptions()
|
|
230
|
+
|
|
231
|
+
static func update(options newOptions: WebViewCrashRestartOptions) {
|
|
232
|
+
options = newOptions
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static var restartOnCrash: Bool {
|
|
236
|
+
options.restartOnCrash
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static var restartAfterCrashDelaySeconds: TimeInterval {
|
|
240
|
+
options.restartAfterCrashDelaySeconds
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
enum WebViewCrashStore {
|
|
245
|
+
private static let pendingCrashKey = "CapgoWebViewCrash.pendingInfo"
|
|
246
|
+
private static let timestampFormatter = ISO8601DateFormatter()
|
|
247
|
+
|
|
248
|
+
static func read() -> [String: Any]? {
|
|
249
|
+
UserDefaults.standard.dictionary(forKey: pendingCrashKey)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static func write(_ value: [String: Any]) {
|
|
253
|
+
UserDefaults.standard.set(value, forKey: pendingCrashKey)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static func clear() {
|
|
257
|
+
UserDefaults.standard.removeObject(forKey: pendingCrashKey)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
static func buildCrashInfo(platform: String, reason: String, url: String?, appState: String? = nil) -> [String: Any] {
|
|
261
|
+
let date = Date()
|
|
262
|
+
let timestamp = Int(date.timeIntervalSince1970 * 1000)
|
|
263
|
+
var value: [String: Any] = [
|
|
264
|
+
"platform": platform,
|
|
265
|
+
"timestamp": timestamp,
|
|
266
|
+
"timestampISO": timestampFormatter.string(from: date),
|
|
267
|
+
"reason": reason
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
if let url, !url.isEmpty {
|
|
271
|
+
value["url"] = url
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if let appState {
|
|
275
|
+
value["appState"] = appState
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return value
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static func shouldDispatch(eventName: String, crashInfo: [String: Any]) -> Bool {
|
|
282
|
+
if eventName == WebViewCrashBridge.restartEventName {
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
guard eventName == WebViewCrashBridge.crashEventName else {
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let reason = crashInfo["reason"] as? String
|
|
291
|
+
return reason != WebViewCrashBridge.periodicRestartReason && reason != WebViewCrashBridge.manualRestartReason
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
enum WebViewCrashSwizzler {
|
|
296
|
+
private static var didInstall = false
|
|
297
|
+
|
|
298
|
+
static func installIfNeeded() {
|
|
299
|
+
guard !didInstall else {
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let originalSelector = #selector(WebViewDelegationHandler.webViewWebContentProcessDidTerminate(_:))
|
|
304
|
+
let swizzledSelector = #selector(WebViewDelegationHandler.capgo_webViewCrash_webViewWebContentProcessDidTerminate(_:))
|
|
305
|
+
|
|
306
|
+
guard
|
|
307
|
+
let originalMethod = class_getInstanceMethod(WebViewDelegationHandler.self, originalSelector),
|
|
308
|
+
let swizzledMethod = class_getInstanceMethod(WebViewDelegationHandler.self, swizzledSelector)
|
|
309
|
+
else {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
method_exchangeImplementations(originalMethod, swizzledMethod)
|
|
314
|
+
didInstall = true
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private extension WebViewDelegationHandler {
|
|
319
|
+
@objc func capgo_webViewCrash_webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
320
|
+
let crashInfo = WebViewCrashStore.buildCrashInfo(
|
|
321
|
+
platform: "ios",
|
|
322
|
+
reason: "webContentProcessDidTerminate",
|
|
323
|
+
url: webView.url?.absoluteString,
|
|
324
|
+
appState: UIApplication.shared.applicationState.capgoWebViewCrashValue
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
WebViewCrashStore.write(crashInfo)
|
|
328
|
+
|
|
329
|
+
guard WebViewCrashRuntime.restartOnCrash else {
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let restart = {
|
|
334
|
+
self.capgo_webViewCrash_webViewWebContentProcessDidTerminate(webView)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let delay = WebViewCrashRuntime.restartAfterCrashDelaySeconds
|
|
338
|
+
if delay > 0 {
|
|
339
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: restart)
|
|
340
|
+
} else {
|
|
341
|
+
restart()
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
extension UIApplication.State {
|
|
347
|
+
var capgoWebViewCrashValue: String {
|
|
348
|
+
switch self {
|
|
349
|
+
case .active:
|
|
350
|
+
return "active"
|
|
351
|
+
case .inactive:
|
|
352
|
+
return "inactive"
|
|
353
|
+
case .background:
|
|
354
|
+
return "background"
|
|
355
|
+
@unknown default:
|
|
356
|
+
return "unknown"
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import Foundation
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
@objc(WebViewCrashPlugin)
|
|
6
|
+
public class WebViewCrashPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
7
|
+
public let identifier = "WebViewCrashPlugin"
|
|
8
|
+
public let jsName = "WebViewCrash"
|
|
9
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
10
|
+
CAPPluginMethod(name: "getPendingCrashInfo", returnType: CAPPluginReturnPromise),
|
|
11
|
+
CAPPluginMethod(name: "clearPendingCrashInfo", returnType: CAPPluginReturnPromise),
|
|
12
|
+
CAPPluginMethod(name: "simulateCrashRecovery", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "restartWebView", returnType: CAPPluginReturnPromise)
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
private var dispatchedPendingEvents = Set<String>()
|
|
17
|
+
private var restartOptions = WebViewCrashRestartOptions()
|
|
18
|
+
private var restartTimer: Timer?
|
|
19
|
+
|
|
20
|
+
override public func load() {
|
|
21
|
+
restartOptions = WebViewCrashRestartOptions(config: getConfig())
|
|
22
|
+
WebViewCrashRuntime.update(options: restartOptions)
|
|
23
|
+
WebViewCrashSwizzler.installIfNeeded()
|
|
24
|
+
schedulePeriodicRestart()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
deinit {
|
|
28
|
+
restartTimer?.invalidate()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@objc override public func addListener(_ call: CAPPluginCall) {
|
|
32
|
+
super.addListener(call)
|
|
33
|
+
|
|
34
|
+
if let eventName = call.getString("eventName"),
|
|
35
|
+
eventName == WebViewCrashBridge.crashEventName || eventName == WebViewCrashBridge.restartEventName {
|
|
36
|
+
dispatchPendingCrashIfNeeded(eventName: eventName)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc func getPendingCrashInfo(_ call: CAPPluginCall) {
|
|
41
|
+
call.resolve(WebViewCrashBridge.pendingResult(WebViewCrashStore.read()))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@objc func clearPendingCrashInfo(_ call: CAPPluginCall) {
|
|
45
|
+
WebViewCrashStore.clear()
|
|
46
|
+
dispatchedPendingEvents.removeAll()
|
|
47
|
+
call.resolve()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@objc func simulateCrashRecovery(_ call: CAPPluginCall) {
|
|
51
|
+
let crashInfo = WebViewCrashStore.buildCrashInfo(
|
|
52
|
+
platform: "ios",
|
|
53
|
+
reason: "simulated",
|
|
54
|
+
url: bridge?.webView?.url?.absoluteString,
|
|
55
|
+
appState: UIApplication.shared.applicationState.capgoWebViewCrashValue
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
WebViewCrashStore.write(crashInfo)
|
|
59
|
+
dispatchedPendingEvents.removeAll()
|
|
60
|
+
dispatchPendingCrashIfNeeded(eventName: WebViewCrashBridge.crashEventName)
|
|
61
|
+
dispatchPendingCrashIfNeeded(eventName: WebViewCrashBridge.restartEventName)
|
|
62
|
+
|
|
63
|
+
call.resolve(WebViewCrashBridge.pendingResult(crashInfo))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@objc func restartWebView(_ call: CAPPluginCall) {
|
|
67
|
+
guard bridge?.viewController is CAPBridgeViewController else {
|
|
68
|
+
call.reject("Unable to restart WebView because the bridge view controller is unavailable.")
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let restartInfo = WebViewCrashStore.buildCrashInfo(
|
|
73
|
+
platform: "ios",
|
|
74
|
+
reason: WebViewCrashBridge.manualRestartReason,
|
|
75
|
+
url: bridge?.webView?.url?.absoluteString,
|
|
76
|
+
appState: UIApplication.shared.applicationState.capgoWebViewCrashValue
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
WebViewCrashStore.write(restartInfo)
|
|
80
|
+
dispatchedPendingEvents.removeAll()
|
|
81
|
+
call.resolve(WebViewCrashBridge.pendingResult(restartInfo))
|
|
82
|
+
|
|
83
|
+
DispatchQueue.main.async { [weak self] in
|
|
84
|
+
_ = self?.recreateBridgeWebView()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private func dispatchPendingCrashIfNeeded(eventName: String) {
|
|
89
|
+
guard !dispatchedPendingEvents.contains(eventName),
|
|
90
|
+
let crashInfo = WebViewCrashStore.read(),
|
|
91
|
+
WebViewCrashStore.shouldDispatch(eventName: eventName, crashInfo: crashInfo) else {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
dispatchedPendingEvents.insert(eventName)
|
|
96
|
+
notifyListeners(eventName, data: crashInfo)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private func schedulePeriodicRestart() {
|
|
100
|
+
restartTimer?.invalidate()
|
|
101
|
+
|
|
102
|
+
guard let restartDelaySeconds = restartOptions.nextRestartDelaySeconds, restartDelaySeconds > 0 else {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
DispatchQueue.main.async { [weak self] in
|
|
107
|
+
guard let self else {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
restartTimer = Timer.scheduledTimer(withTimeInterval: restartDelaySeconds, repeats: false) { [weak self] _ in
|
|
112
|
+
self?.restartWebView(reason: WebViewCrashBridge.periodicRestartReason)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func restartWebView(reason: String) {
|
|
118
|
+
DispatchQueue.main.async { [weak self] in
|
|
119
|
+
guard let self, let webView = bridge?.webView else {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let restartInfo = WebViewCrashStore.buildCrashInfo(
|
|
124
|
+
platform: "ios",
|
|
125
|
+
reason: reason,
|
|
126
|
+
url: webView.url?.absoluteString,
|
|
127
|
+
appState: UIApplication.shared.applicationState.capgoWebViewCrashValue
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
WebViewCrashStore.write(restartInfo)
|
|
131
|
+
dispatchedPendingEvents.removeAll()
|
|
132
|
+
if !recreateBridgeWebView() {
|
|
133
|
+
webView.reload()
|
|
134
|
+
schedulePeriodicRestart()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func recreateBridgeWebView() -> Bool {
|
|
140
|
+
guard let viewController = bridge?.viewController as? CAPBridgeViewController else {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
restartTimer?.invalidate()
|
|
145
|
+
viewController.webView?.stopLoading()
|
|
146
|
+
viewController.webView?.navigationDelegate = nil
|
|
147
|
+
viewController.webView?.uiDelegate = nil
|
|
148
|
+
viewController.loadView()
|
|
149
|
+
viewController.loadWebView()
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import WebViewCrashPlugin
|
|
3
|
+
|
|
4
|
+
final class WebViewCrashPluginTests: XCTestCase {
|
|
5
|
+
func testPluginMetadata() {
|
|
6
|
+
let plugin = WebViewCrashPlugin()
|
|
7
|
+
|
|
8
|
+
XCTAssertEqual(plugin.identifier, "WebViewCrashPlugin")
|
|
9
|
+
XCTAssertEqual(plugin.jsName, "WebViewCrash")
|
|
10
|
+
XCTAssertEqual(plugin.pluginMethods.count, 3)
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capgo/capacitor-webview-crash",
|
|
3
|
+
"version": "7.1.0",
|
|
4
|
+
"description": "Capacitor plugin for detecting WebView crash recovery and restarting long-running WebViews natively.",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"android/src/main/",
|
|
11
|
+
"android/build.gradle",
|
|
12
|
+
"dist/",
|
|
13
|
+
"ios/Sources",
|
|
14
|
+
"ios/Tests",
|
|
15
|
+
"Package.swift",
|
|
16
|
+
"CapgoCapacitorWebViewCrash.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "Martin Donadieu <martin@capgo.app>",
|
|
19
|
+
"license": "MPL-2.0",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Cap-go/capacitor-webview-crash.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Cap-go/capacitor-webview-crash/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://capgo.app/docs/plugins/webview-crash/",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"capacitor",
|
|
30
|
+
"plugin",
|
|
31
|
+
"webview-crash",
|
|
32
|
+
"webview-restart",
|
|
33
|
+
"oom-recovery",
|
|
34
|
+
"crash-recovery",
|
|
35
|
+
"ios",
|
|
36
|
+
"android",
|
|
37
|
+
"capgo"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"verify": "bun run verify:ios && bun run verify:android && bun run verify:web",
|
|
41
|
+
"verify:ios": "xcodebuild -scheme CapgoCapacitorWebviewCrash -destination generic/platform=iOS",
|
|
42
|
+
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
43
|
+
"verify:web": "bun run build && bun test",
|
|
44
|
+
"test": "bun test",
|
|
45
|
+
"lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
|
|
46
|
+
"fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
|
|
47
|
+
"eslint": "eslint . --ext .ts",
|
|
48
|
+
"prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java,json,md}\" --plugin=prettier-plugin-java",
|
|
49
|
+
"swiftlint": "node-swiftlint",
|
|
50
|
+
"docgen": "docgen --api WebViewCrashPlugin --output-readme README.md --output-json dist/docs.json && prettier --write README.md",
|
|
51
|
+
"build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs",
|
|
52
|
+
"clean": "rimraf ./dist",
|
|
53
|
+
"watch": "tsc --watch",
|
|
54
|
+
"prepublishOnly": "bun run build",
|
|
55
|
+
"check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@capacitor/android": "^7.0.0",
|
|
59
|
+
"@capacitor/cli": "^7.0.0",
|
|
60
|
+
"@capacitor/core": "^7.0.0",
|
|
61
|
+
"@capacitor/docgen": "^0.3.1",
|
|
62
|
+
"@capacitor/ios": "^7.0.0",
|
|
63
|
+
"@ionic/eslint-config": "^0.4.0",
|
|
64
|
+
"@ionic/prettier-config": "^4.0.0",
|
|
65
|
+
"@ionic/swiftlint-config": "^2.0.0",
|
|
66
|
+
"@types/node": "^24.10.1",
|
|
67
|
+
"eslint": "^8.57.1",
|
|
68
|
+
"eslint-plugin-import": "^2.31.0",
|
|
69
|
+
"husky": "^9.1.7",
|
|
70
|
+
"prettier": "^3.6.2",
|
|
71
|
+
"prettier-pretty-check": "^0.2.0",
|
|
72
|
+
"prettier-plugin-java": "^2.7.7",
|
|
73
|
+
"rimraf": "^6.1.0",
|
|
74
|
+
"rollup": "^4.53.2",
|
|
75
|
+
"swiftlint": "^2.0.0",
|
|
76
|
+
"typescript": "^5.9.3"
|
|
77
|
+
},
|
|
78
|
+
"peerDependencies": {
|
|
79
|
+
"@capacitor/core": ">=7.0.0 <8.0.0"
|
|
80
|
+
},
|
|
81
|
+
"prettier": "@ionic/prettier-config",
|
|
82
|
+
"swiftlint": "@ionic/swiftlint-config",
|
|
83
|
+
"eslintConfig": {
|
|
84
|
+
"extends": "@ionic/eslint-config/recommended"
|
|
85
|
+
},
|
|
86
|
+
"capacitor": {
|
|
87
|
+
"ios": {
|
|
88
|
+
"src": "ios"
|
|
89
|
+
},
|
|
90
|
+
"android": {
|
|
91
|
+
"src": "android"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|