@capgo/capacitor-webview-crash 8.0.3 → 8.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/README.md +107 -30
- package/android/src/main/java/app/capgo/webviewcrash/WebViewCrash.java +231 -1
- package/android/src/main/java/app/capgo/webviewcrash/WebViewCrashPlugin.java +99 -18
- package/dist/docs.json +37 -17
- package/dist/esm/definitions.d.ts +71 -16
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +5 -2
- package/dist/esm/web.js +28 -13
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +28 -13
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +28 -13
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/WebViewCrashPlugin/WebViewCrash.swift +256 -2
- package/ios/Sources/WebViewCrashPlugin/WebViewCrashPlugin.swift +101 -11
- package/package.json +4 -2
package/dist/plugin.js
CHANGED
|
@@ -8,48 +8,62 @@ var capacitorWebViewCrash = (function (exports, core) {
|
|
|
8
8
|
class WebViewCrashWeb extends core.WebPlugin {
|
|
9
9
|
constructor() {
|
|
10
10
|
super(...arguments);
|
|
11
|
-
this.
|
|
11
|
+
this.dispatchedPendingEvents = new Set();
|
|
12
12
|
}
|
|
13
13
|
async getPendingCrashInfo() {
|
|
14
14
|
return { value: this.readPendingCrashInfo() };
|
|
15
15
|
}
|
|
16
16
|
async clearPendingCrashInfo() {
|
|
17
17
|
this.removePendingCrashInfo();
|
|
18
|
-
this.
|
|
18
|
+
this.dispatchedPendingEvents.clear();
|
|
19
19
|
}
|
|
20
20
|
async simulateCrashRecovery() {
|
|
21
21
|
const value = this.buildCrashInfo();
|
|
22
22
|
this.writePendingCrashInfo(value);
|
|
23
|
-
this.
|
|
23
|
+
this.dispatchedPendingEvents.clear();
|
|
24
24
|
this.flushPendingCrashEvent();
|
|
25
|
+
this.flushPendingCrashEvent(WebViewCrashWeb.restartEventName);
|
|
26
|
+
return { value };
|
|
27
|
+
}
|
|
28
|
+
async restartWebView() {
|
|
29
|
+
const value = this.buildCrashInfo('manualRestart');
|
|
30
|
+
this.writePendingCrashInfo(value);
|
|
31
|
+
this.dispatchedPendingEvents.clear();
|
|
32
|
+
this.flushPendingCrashEvent(WebViewCrashWeb.restartEventName);
|
|
25
33
|
return { value };
|
|
26
34
|
}
|
|
27
35
|
async addListener(eventName, listenerFunc) {
|
|
28
36
|
const handle = await super.addListener(eventName, listenerFunc);
|
|
29
|
-
if (eventName === WebViewCrashWeb.eventName) {
|
|
30
|
-
this.flushPendingCrashEvent();
|
|
37
|
+
if (eventName === WebViewCrashWeb.crashEventName || eventName === WebViewCrashWeb.restartEventName) {
|
|
38
|
+
this.flushPendingCrashEvent(eventName);
|
|
31
39
|
}
|
|
32
40
|
return handle;
|
|
33
41
|
}
|
|
34
|
-
flushPendingCrashEvent() {
|
|
35
|
-
if (this.
|
|
42
|
+
flushPendingCrashEvent(eventName = WebViewCrashWeb.crashEventName) {
|
|
43
|
+
if (this.dispatchedPendingEvents.has(eventName)) {
|
|
36
44
|
return;
|
|
37
45
|
}
|
|
38
46
|
const value = this.readPendingCrashInfo();
|
|
39
|
-
if (!value) {
|
|
47
|
+
if (!value || !this.shouldDispatchEvent(eventName, value)) {
|
|
40
48
|
return;
|
|
41
49
|
}
|
|
42
|
-
this.
|
|
43
|
-
this.notifyListeners(
|
|
50
|
+
this.dispatchedPendingEvents.add(eventName);
|
|
51
|
+
this.notifyListeners(eventName, value);
|
|
52
|
+
}
|
|
53
|
+
shouldDispatchEvent(eventName, value) {
|
|
54
|
+
if (eventName === WebViewCrashWeb.crashEventName) {
|
|
55
|
+
return value.reason !== 'periodicRestart' && value.reason !== 'manualRestart';
|
|
56
|
+
}
|
|
57
|
+
return eventName === WebViewCrashWeb.restartEventName;
|
|
44
58
|
}
|
|
45
|
-
buildCrashInfo() {
|
|
59
|
+
buildCrashInfo(reason = 'simulated') {
|
|
46
60
|
var _a;
|
|
47
61
|
const timestamp = Date.now();
|
|
48
62
|
return {
|
|
49
63
|
platform: 'web',
|
|
50
64
|
timestamp,
|
|
51
65
|
timestampISO: new Date(timestamp).toISOString(),
|
|
52
|
-
reason
|
|
66
|
+
reason,
|
|
53
67
|
url: (_a = globalThis.location) === null || _a === void 0 ? void 0 : _a.href,
|
|
54
68
|
appState: 'active',
|
|
55
69
|
};
|
|
@@ -76,7 +90,8 @@ var capacitorWebViewCrash = (function (exports, core) {
|
|
|
76
90
|
(_a = globalThis.localStorage) === null || _a === void 0 ? void 0 : _a.removeItem(WebViewCrashWeb.storageKey);
|
|
77
91
|
}
|
|
78
92
|
}
|
|
79
|
-
WebViewCrashWeb.
|
|
93
|
+
WebViewCrashWeb.crashEventName = 'webViewRestoredAfterCrash';
|
|
94
|
+
WebViewCrashWeb.restartEventName = 'webViewRestoredAfterRestart';
|
|
80
95
|
WebViewCrashWeb.storageKey = 'capgo.webview-crash.pending';
|
|
81
96
|
|
|
82
97
|
var web = /*#__PURE__*/Object.freeze({
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst WebViewCrash = registerPlugin('WebViewCrash', {\n web: () => import('./web').then((m) => new m.WebViewCrashWeb()),\n});\nexport * from './definitions';\nexport { WebViewCrash };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class WebViewCrashWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst WebViewCrash = registerPlugin('WebViewCrash', {\n web: () => import('./web').then((m) => new m.WebViewCrashWeb()),\n});\nexport * from './definitions';\nexport { WebViewCrash };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class WebViewCrashWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.dispatchedPendingEvents = new Set();\n }\n async getPendingCrashInfo() {\n return { value: this.readPendingCrashInfo() };\n }\n async clearPendingCrashInfo() {\n this.removePendingCrashInfo();\n this.dispatchedPendingEvents.clear();\n }\n async simulateCrashRecovery() {\n const value = this.buildCrashInfo();\n this.writePendingCrashInfo(value);\n this.dispatchedPendingEvents.clear();\n this.flushPendingCrashEvent();\n this.flushPendingCrashEvent(WebViewCrashWeb.restartEventName);\n return { value };\n }\n async restartWebView() {\n const value = this.buildCrashInfo('manualRestart');\n this.writePendingCrashInfo(value);\n this.dispatchedPendingEvents.clear();\n this.flushPendingCrashEvent(WebViewCrashWeb.restartEventName);\n return { value };\n }\n async addListener(eventName, listenerFunc) {\n const handle = await super.addListener(eventName, listenerFunc);\n if (eventName === WebViewCrashWeb.crashEventName || eventName === WebViewCrashWeb.restartEventName) {\n this.flushPendingCrashEvent(eventName);\n }\n return handle;\n }\n flushPendingCrashEvent(eventName = WebViewCrashWeb.crashEventName) {\n if (this.dispatchedPendingEvents.has(eventName)) {\n return;\n }\n const value = this.readPendingCrashInfo();\n if (!value || !this.shouldDispatchEvent(eventName, value)) {\n return;\n }\n this.dispatchedPendingEvents.add(eventName);\n this.notifyListeners(eventName, value);\n }\n shouldDispatchEvent(eventName, value) {\n if (eventName === WebViewCrashWeb.crashEventName) {\n return value.reason !== 'periodicRestart' && value.reason !== 'manualRestart';\n }\n return eventName === WebViewCrashWeb.restartEventName;\n }\n buildCrashInfo(reason = 'simulated') {\n var _a;\n const timestamp = Date.now();\n return {\n platform: 'web',\n timestamp,\n timestampISO: new Date(timestamp).toISOString(),\n reason,\n url: (_a = globalThis.location) === null || _a === void 0 ? void 0 : _a.href,\n appState: 'active',\n };\n }\n readPendingCrashInfo() {\n var _a;\n const raw = (_a = globalThis.localStorage) === null || _a === void 0 ? void 0 : _a.getItem(WebViewCrashWeb.storageKey);\n if (!raw) {\n return null;\n }\n try {\n return JSON.parse(raw);\n }\n catch (_b) {\n return null;\n }\n }\n writePendingCrashInfo(value) {\n var _a;\n (_a = globalThis.localStorage) === null || _a === void 0 ? void 0 : _a.setItem(WebViewCrashWeb.storageKey, JSON.stringify(value));\n }\n removePendingCrashInfo() {\n var _a;\n (_a = globalThis.localStorage) === null || _a === void 0 ? void 0 : _a.removeItem(WebViewCrashWeb.storageKey);\n }\n}\nWebViewCrashWeb.crashEventName = 'webViewRestoredAfterCrash';\nWebViewCrashWeb.restartEventName = 'webViewRestoredAfterRestart';\nWebViewCrashWeb.storageKey = 'capgo.webview-crash.pending';\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":"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"}
|
|
@@ -5,7 +5,10 @@ import UIKit
|
|
|
5
5
|
import WebKit
|
|
6
6
|
|
|
7
7
|
enum WebViewCrashBridge {
|
|
8
|
-
static let
|
|
8
|
+
static let crashEventName = "webViewRestoredAfterCrash"
|
|
9
|
+
static let restartEventName = "webViewRestoredAfterRestart"
|
|
10
|
+
static let periodicRestartReason = "periodicRestart"
|
|
11
|
+
static let manualRestartReason = "manualRestart"
|
|
9
12
|
|
|
10
13
|
static func pendingResult(_ value: [String: Any]?) -> [String: Any] {
|
|
11
14
|
[
|
|
@@ -14,6 +17,230 @@ enum WebViewCrashBridge {
|
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
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
|
+
|
|
17
244
|
enum WebViewCrashStore {
|
|
18
245
|
private static let pendingCrashKey = "CapgoWebViewCrash.pendingInfo"
|
|
19
246
|
private static let timestampFormatter = ISO8601DateFormatter()
|
|
@@ -50,6 +277,19 @@ enum WebViewCrashStore {
|
|
|
50
277
|
|
|
51
278
|
return value
|
|
52
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
|
+
}
|
|
53
293
|
}
|
|
54
294
|
|
|
55
295
|
enum WebViewCrashSwizzler {
|
|
@@ -85,7 +325,21 @@ private extension WebViewDelegationHandler {
|
|
|
85
325
|
)
|
|
86
326
|
|
|
87
327
|
WebViewCrashStore.write(crashInfo)
|
|
88
|
-
|
|
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
|
+
}
|
|
89
343
|
}
|
|
90
344
|
}
|
|
91
345
|
|
|
@@ -9,20 +9,31 @@ public class WebViewCrashPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
9
9
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
10
10
|
CAPPluginMethod(name: "getPendingCrashInfo", returnType: CAPPluginReturnPromise),
|
|
11
11
|
CAPPluginMethod(name: "clearPendingCrashInfo", returnType: CAPPluginReturnPromise),
|
|
12
|
-
CAPPluginMethod(name: "simulateCrashRecovery", returnType: CAPPluginReturnPromise)
|
|
12
|
+
CAPPluginMethod(name: "simulateCrashRecovery", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "restartWebView", returnType: CAPPluginReturnPromise)
|
|
13
14
|
]
|
|
14
15
|
|
|
15
|
-
private var
|
|
16
|
+
private var dispatchedPendingEvents = Set<String>()
|
|
17
|
+
private var restartOptions = WebViewCrashRestartOptions()
|
|
18
|
+
private var restartTimer: Timer?
|
|
16
19
|
|
|
17
20
|
override public func load() {
|
|
21
|
+
restartOptions = WebViewCrashRestartOptions(config: getConfig())
|
|
22
|
+
WebViewCrashRuntime.update(options: restartOptions)
|
|
18
23
|
WebViewCrashSwizzler.installIfNeeded()
|
|
24
|
+
schedulePeriodicRestart()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
deinit {
|
|
28
|
+
restartTimer?.invalidate()
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
@objc override public func addListener(_ call: CAPPluginCall) {
|
|
22
32
|
super.addListener(call)
|
|
23
33
|
|
|
24
|
-
if call.getString("eventName")
|
|
25
|
-
|
|
34
|
+
if let eventName = call.getString("eventName"),
|
|
35
|
+
eventName == WebViewCrashBridge.crashEventName || eventName == WebViewCrashBridge.restartEventName {
|
|
36
|
+
dispatchPendingCrashIfNeeded(eventName: eventName)
|
|
26
37
|
}
|
|
27
38
|
}
|
|
28
39
|
|
|
@@ -32,7 +43,7 @@ public class WebViewCrashPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
32
43
|
|
|
33
44
|
@objc func clearPendingCrashInfo(_ call: CAPPluginCall) {
|
|
34
45
|
WebViewCrashStore.clear()
|
|
35
|
-
|
|
46
|
+
dispatchedPendingEvents.removeAll()
|
|
36
47
|
call.resolve()
|
|
37
48
|
}
|
|
38
49
|
|
|
@@ -45,18 +56,97 @@ public class WebViewCrashPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
45
56
|
)
|
|
46
57
|
|
|
47
58
|
WebViewCrashStore.write(crashInfo)
|
|
48
|
-
|
|
49
|
-
dispatchPendingCrashIfNeeded()
|
|
59
|
+
dispatchedPendingEvents.removeAll()
|
|
60
|
+
dispatchPendingCrashIfNeeded(eventName: WebViewCrashBridge.crashEventName)
|
|
61
|
+
dispatchPendingCrashIfNeeded(eventName: WebViewCrashBridge.restartEventName)
|
|
50
62
|
|
|
51
63
|
call.resolve(WebViewCrashBridge.pendingResult(crashInfo))
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
guard
|
|
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 {
|
|
56
92
|
return
|
|
57
93
|
}
|
|
58
94
|
|
|
59
|
-
|
|
60
|
-
notifyListeners(
|
|
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
|
|
61
151
|
}
|
|
62
152
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-webview-crash",
|
|
3
|
-
"version": "8.0
|
|
4
|
-
"description": "Capacitor plugin for detecting WebView crash recovery and
|
|
3
|
+
"version": "8.1.0",
|
|
4
|
+
"description": "Capacitor plugin for detecting WebView crash recovery and restarting long-running WebViews natively.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/esm/index.d.ts",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"capacitor",
|
|
30
30
|
"plugin",
|
|
31
31
|
"webview-crash",
|
|
32
|
+
"webview-restart",
|
|
33
|
+
"oom-recovery",
|
|
32
34
|
"crash-recovery",
|
|
33
35
|
"ios",
|
|
34
36
|
"android",
|