fix(cef): follow CEF JS dialog reset semantics and shutdown paths
Ignore the OnResetDialogState echo CEF sends after a suppressed dialog and after every Cont so a spammer or trailing reset cannot cancel the user's dialog. Skip dialogs on a destroyed WebView, cancel on renderer termination, call Cont directly (CEF already posts to its UI thread) and always show Dumber's own beforeunload text.
- commit
- 26ad11fd2962170b24a6d638d9803a4dddcd1283
- author
- brice · · 2026-10-03 06:57 UTC
- committer
- brice · · 2026-10-03 07:50 UTC
- parent
- d5a6841
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +188 -29
M internal/application/port/jsdialog.go +3 -1
| @@ -20,7 +20,9 @@ type JSDialogRequest struct { | |||
| 20 | 20 | Type JSDialogType | |
| 21 | 21 | // Origin is the URL of the frame that requested the dialog (best effort). | |
| 22 | 22 | Origin string | |
| 23 | - | // Message is the page-provided message (may be empty). | |
| 23 | + | // Message is the page-provided message (may be empty). It is empty for | |
| 24 | + | // JSDialogBeforeUnload: CEF always passes a fixed string, so the UI shows | |
| 25 | + | // its own text (like Chrome/Firefox). | |
| 24 | 26 | Message string | |
| 25 | 27 | // DefaultPrompt is the prefilled input for JSDialogPrompt. | |
| 26 | 28 | DefaultPrompt string | |
| @@ -20,7 +20,9 @@ type JSDialogRequest struct { | |||||
| 20 | Type JSDialogType | 20 | Type JSDialogType | ||
| 21 | // Origin is the URL of the frame that requested the dialog (best effort). | 21 | // Origin is the URL of the frame that requested the dialog (best effort). | ||
| 22 | Origin string | 22 | Origin string | ||
| 23 | - | // Message is the page-provided message (may be empty). | 23 | + | // Message is the page-provided message (may be empty). It is empty for |
| 24 | + | // JSDialogBeforeUnload: CEF always passes a fixed string, so the UI shows | |||
| 25 | + | // its own text (like Chrome/Firefox). | |||
| 24 | Message string | 26 | Message string | ||
| 25 | // DefaultPrompt is the prefilled input for JSDialogPrompt. | 27 | // DefaultPrompt is the prefilled input for JSDialogPrompt. | ||
| 26 | DefaultPrompt string | 28 | DefaultPrompt string | ||
M internal/infrastructure/cef/jsdialog_handler.go +65 -26
| @@ -20,9 +20,24 @@ import ( | |||
| 20 | 20 | // Anti-abuse: while a dialog is open for a WebView, further OnJsdialog requests | |
| 21 | 21 | // are suppressed (suppress_message=true) instead of being queued, so a page | |
| 22 | 22 | // cannot stack or spam dialogs. | |
| 23 | + | // | |
| 24 | + | // CEF reset semantics (javascript_dialog_manager.cc): OnResetDialogState is | |
| 25 | + | // also delivered (a) right after a suppressed OnJsdialog and (b) at the end of | |
| 26 | + | // every DialogClosed, i.e. inside/after each callback Cont. Those are not real | |
| 27 | + | // resets and must not cancel the dialog the user is looking at, so | |
| 28 | + | // jsDialogState carries a one-shot ignoreReset flag armed before suppressing, | |
| 29 | + | // rejecting or continuing. Only WebView destruction and renderer termination | |
| 30 | + | // cancel unconditionally. | |
| 23 | 31 | ||
| 24 | 32 | // continueCEFJSDialog resolves a CEF dialog callback. It is a seam for tests. | |
| 25 | - | var continueCEFJSDialog = continueCEFJSDialogOnUIThread | |
| 33 | + | // CEF's callback implementation re-posts to the CEF UI thread by itself. | |
| 34 | + | var continueCEFJSDialog = func(callback purecef.JsdialogCallback, ok bool, input string) { | |
| 35 | + | success := int32(0) | |
| 36 | + | if ok { | |
| 37 | + | success = 1 | |
| 38 | + | } | |
| 39 | + | callback.Cont(success, input) | |
| 40 | + | } | |
| 26 | 41 | ||
| 27 | 42 | var _ purecef.JsdialogHandler = (*handlerSet)(nil) | |
| 28 | 43 | ||
| @@ -34,8 +49,25 @@ type jsDialogCall struct { | |||
| 34 | 49 | ||
| 35 | 50 | // jsDialogState tracks the single pending dialog of a WebView. | |
| 36 | 51 | type jsDialogState struct { | |
| 37 | - | mu sync.Mutex | |
| 38 | - | current *jsDialogCall | |
| 52 | + | mu sync.Mutex | |
| 53 | + | current *jsDialogCall | |
| 54 | + | ignoreReset bool // swallow the next OnResetDialogState (see file comment) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // armIgnoreReset makes the next OnResetDialogState a no-op. | |
| 58 | + | func (s *jsDialogState) armIgnoreReset() { | |
| 59 | + | s.mu.Lock() | |
| 60 | + | s.ignoreReset = true | |
| 61 | + | s.mu.Unlock() | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // consumeIgnoreReset reports (and clears) a pending ignoreReset. | |
| 65 | + | func (s *jsDialogState) consumeIgnoreReset() bool { | |
| 66 | + | s.mu.Lock() | |
| 67 | + | defer s.mu.Unlock() | |
| 68 | + | was := s.ignoreReset | |
| 69 | + | s.ignoreReset = false | |
| 70 | + | return was | |
| 39 | 71 | } | |
| 40 | 72 | ||
| 41 | 73 | // begin registers a new pending dialog; false if one is already open. | |
| @@ -47,6 +79,9 @@ func (s *jsDialogState) begin(cb purecef.JsdialogCallback) (*jsDialogCall, bool) | |||
| 47 | 79 | } | |
| 48 | 80 | call := &jsDialogCall{callback: cb} | |
| 49 | 81 | s.current = call | |
| 82 | + | // A new dialog means any reset owed by the previous one has been delivered | |
| 83 | + | // (the page is blocked until then); never carry a stale flag across. | |
| 84 | + | s.ignoreReset = false | |
| 50 | 85 | return call, true | |
| 51 | 86 | } | |
| 52 | 87 | ||
| @@ -67,6 +102,9 @@ func (s *jsDialogState) resolve(call *jsDialogCall, ok bool, input string) bool | |||
| 67 | 102 | if s.current == call { | |
| 68 | 103 | s.current = nil | |
| 69 | 104 | } | |
| 105 | + | // Armed before Cont: CEF may deliver DialogClosed + OnResetDialogState | |
| 106 | + | // synchronously from inside it. | |
| 107 | + | s.ignoreReset = true | |
| 70 | 108 | s.mu.Unlock() | |
| 71 | 109 | continueCEFJSDialog(call.callback, ok, input) | |
| 72 | 110 | return true | |
| @@ -146,6 +184,10 @@ func (h *handlerSet) OnJsdialog( | |||
| 146 | 184 | if h == nil || h.wv == nil || callback == nil { | |
| 147 | 185 | return 0 | |
| 148 | 186 | } | |
| 187 | + | if h.wv.destroyed.Load() { | |
| 188 | + | // Shutting down: the GTK loop may be blocked. Let CEF dismiss it. | |
| 189 | + | return 0 | |
| 190 | + | } | |
| 149 | 191 | typ, ok := jsDialogTypeFromCEF(dialogType) | |
| 150 | 192 | if !ok { | |
| 151 | 193 | return 0 | |
| @@ -156,7 +198,9 @@ func (h *handlerSet) OnJsdialog( | |||
| 156 | 198 | } | |
| 157 | 199 | call, ok := h.wv.jsDialogs.begin(callback) | |
| 158 | 200 | if !ok { | |
| 159 | - | // A dialog is already open: suppress rather than stack/spam. | |
| 201 | + | // A dialog is already open: suppress rather than stack/spam. CEF will | |
| 202 | + | // follow up with OnResetDialogState, which must not cancel that dialog. | |
| 203 | + | h.wv.jsDialogs.armIgnoreReset() | |
| 160 | 204 | if suppressMessage != nil { | |
| 161 | 205 | *suppressMessage = 1 | |
| 162 | 206 | } | |
| @@ -175,13 +219,18 @@ func (h *handlerSet) OnJsdialog( | |||
| 175 | 219 | // OnBeforeUnloadDialog implements purecef.JsdialogHandler. | |
| 176 | 220 | func (h *handlerSet) OnBeforeUnloadDialog( | |
| 177 | 221 | _ purecef.Browser, | |
| 178 | - | messageText string, | |
| 222 | + | _ string, // CEF always passes a fixed string; the UI shows Dumber's own text. | |
| 179 | 223 | isReload int32, | |
| 180 | 224 | callback purecef.JsdialogCallback, | |
| 181 | 225 | ) bool { | |
| 182 | 226 | if h == nil || h.wv == nil || callback == nil { | |
| 183 | 227 | return false | |
| 184 | 228 | } | |
| 229 | + | if h.wv.destroyed.Load() { | |
| 230 | + | // CloseBrowser dispatches beforeunload; accept it instead of waiting | |
| 231 | + | // on UI that may never run. CEF accepts the unload when we return false. | |
| 232 | + | return false | |
| 233 | + | } | |
| 185 | 234 | ui := h.wv.jsDialogUICallbacks() | |
| 186 | 235 | if ui == nil || ui.OnJSDialog == nil { | |
| 187 | 236 | return false | |
| @@ -189,24 +238,33 @@ func (h *handlerSet) OnBeforeUnloadDialog( | |||
| 189 | 238 | call, ok := h.wv.jsDialogs.begin(callback) | |
| 190 | 239 | if !ok { | |
| 191 | 240 | // Another dialog is open: stay on the page rather than stacking. | |
| 241 | + | // The Cont below makes CEF send a reset that must not cancel it. | |
| 242 | + | h.wv.jsDialogs.armIgnoreReset() | |
| 192 | 243 | continueCEFJSDialog(callback, false, "") | |
| 193 | 244 | return true | |
| 194 | 245 | } | |
| 195 | 246 | h.wv.presentJSDialog(call, port.JSDialogRequest{ | |
| 196 | 247 | Type: port.JSDialogBeforeUnload, | |
| 197 | 248 | Origin: h.wv.URI(), | |
| 198 | - | Message: messageText, | |
| 199 | 249 | IsReload: isReload != 0, | |
| 200 | 250 | }, ui) | |
| 201 | 251 | return true | |
| 202 | 252 | } | |
| 203 | 253 | ||
| 204 | 254 | // OnResetDialogState implements purecef.JsdialogHandler: CEF canceled pending | |
| 205 | - | // dialogs (navigation, close). Close the UI and resolve the callback. | |
| 255 | + | // dialogs (navigation, close). Close the UI and resolve the callback, unless | |
| 256 | + | // this is the echo of a suppress/reject/Cont (see file comment). | |
| 257 | + | // | |
| 258 | + | // The UI reset (OnJSDialogReset) carries no dialog identity: it hides whatever | |
| 259 | + | // the pane shows. That is safe because a pane shows at most one dialog and a | |
| 260 | + | // new one is only presented after the previous call was resolved. | |
| 206 | 261 | func (h *handlerSet) OnResetDialogState(_ purecef.Browser) { | |
| 207 | 262 | if h == nil || h.wv == nil { | |
| 208 | 263 | return | |
| 209 | 264 | } | |
| 265 | + | if h.wv.jsDialogs.consumeIgnoreReset() { | |
| 266 | + | return | |
| 267 | + | } | |
| 210 | 268 | h.wv.cancelJSDialogs() | |
| 211 | 269 | } | |
| 212 | 270 | ||
| @@ -214,22 +272,3 @@ func (h *handlerSet) OnResetDialogState(_ purecef.Browser) { | |||
| 214 | 272 | // nothing: resolve already cleared the pending state, and a late notification | |
| 215 | 273 | // must not clear a newer dialog. | |
| 216 | 274 | func (h *handlerSet) OnDialogClosed(_ purecef.Browser) {} | |
| 217 | - | ||
| 218 | - | // continueCEFJSDialogOnUIThread answers the callback on the CEF UI thread. | |
| 219 | - | func continueCEFJSDialogOnUIThread(callback purecef.JsdialogCallback, ok bool, input string) { | |
| 220 | - | if callback == nil { | |
| 221 | - | return | |
| 222 | - | } | |
| 223 | - | success := int32(0) | |
| 224 | - | if ok { | |
| 225 | - | success = 1 | |
| 226 | - | } | |
| 227 | - | if purecef.CurrentlyOn(purecef.ThreadIDTidUi) == 1 { | |
| 228 | - | callback.Cont(success, input) | |
| 229 | - | return | |
| 230 | - | } | |
| 231 | - | task := cefNewTask(cefTaskFunc(func() { callback.Cont(success, input) })) | |
| 232 | - | if cefPostTask(purecef.ThreadIDTidUi, task) != 1 { | |
| 233 | - | callback.Cont(success, input) | |
| 234 | - | } | |
| 235 | - | } | |
| @@ -20,9 +20,24 @@ import ( | |||||
| 20 | // Anti-abuse: while a dialog is open for a WebView, further OnJsdialog requests | 20 | // Anti-abuse: while a dialog is open for a WebView, further OnJsdialog requests | ||
| 21 | // are suppressed (suppress_message=true) instead of being queued, so a page | 21 | // are suppressed (suppress_message=true) instead of being queued, so a page | ||
| 22 | // cannot stack or spam dialogs. | 22 | // cannot stack or spam dialogs. | ||
| 23 | + | // | |||
| 24 | + | // CEF reset semantics (javascript_dialog_manager.cc): OnResetDialogState is | |||
| 25 | + | // also delivered (a) right after a suppressed OnJsdialog and (b) at the end of | |||
| 26 | + | // every DialogClosed, i.e. inside/after each callback Cont. Those are not real | |||
| 27 | + | // resets and must not cancel the dialog the user is looking at, so | |||
| 28 | + | // jsDialogState carries a one-shot ignoreReset flag armed before suppressing, | |||
| 29 | + | // rejecting or continuing. Only WebView destruction and renderer termination | |||
| 30 | + | // cancel unconditionally. | |||
| 23 | 31 | ||||
| 24 | // continueCEFJSDialog resolves a CEF dialog callback. It is a seam for tests. | 32 | // continueCEFJSDialog resolves a CEF dialog callback. It is a seam for tests. | ||
| 25 | - | var continueCEFJSDialog = continueCEFJSDialogOnUIThread | 33 | + | // CEF's callback implementation re-posts to the CEF UI thread by itself. |
| 34 | + | var continueCEFJSDialog = func(callback purecef.JsdialogCallback, ok bool, input string) { | |||
| 35 | + | success := int32(0) | |||
| 36 | + | if ok { | |||
| 37 | + | success = 1 | |||
| 38 | + | } | |||
| 39 | + | callback.Cont(success, input) | |||
| 40 | + | } | |||
| 26 | 41 | ||||
| 27 | var _ purecef.JsdialogHandler = (*handlerSet)(nil) | 42 | var _ purecef.JsdialogHandler = (*handlerSet)(nil) | ||
| 28 | 43 | ||||
| @@ -34,8 +49,25 @@ type jsDialogCall struct { | |||||
| 34 | 49 | ||||
| 35 | // jsDialogState tracks the single pending dialog of a WebView. | 50 | // jsDialogState tracks the single pending dialog of a WebView. | ||
| 36 | type jsDialogState struct { | 51 | type jsDialogState struct { | ||
| 37 | - | mu sync.Mutex | 52 | + | mu sync.Mutex |
| 38 | - | current *jsDialogCall | 53 | + | current *jsDialogCall |
| 54 | + | ignoreReset bool // swallow the next OnResetDialogState (see file comment) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // armIgnoreReset makes the next OnResetDialogState a no-op. | |||
| 58 | + | func (s *jsDialogState) armIgnoreReset() { | |||
| 59 | + | s.mu.Lock() | |||
| 60 | + | s.ignoreReset = true | |||
| 61 | + | s.mu.Unlock() | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // consumeIgnoreReset reports (and clears) a pending ignoreReset. | |||
| 65 | + | func (s *jsDialogState) consumeIgnoreReset() bool { | |||
| 66 | + | s.mu.Lock() | |||
| 67 | + | defer s.mu.Unlock() | |||
| 68 | + | was := s.ignoreReset | |||
| 69 | + | s.ignoreReset = false | |||
| 70 | + | return was | |||
| 39 | } | 71 | } | ||
| 40 | 72 | ||||
| 41 | // begin registers a new pending dialog; false if one is already open. | 73 | // begin registers a new pending dialog; false if one is already open. | ||
| @@ -47,6 +79,9 @@ func (s *jsDialogState) begin(cb purecef.JsdialogCallback) (*jsDialogCall, bool) | |||||
| 47 | } | 79 | } | ||
| 48 | call := &jsDialogCall{callback: cb} | 80 | call := &jsDialogCall{callback: cb} | ||
| 49 | s.current = call | 81 | s.current = call | ||
| 82 | + | // A new dialog means any reset owed by the previous one has been delivered | |||
| 83 | + | // (the page is blocked until then); never carry a stale flag across. | |||
| 84 | + | s.ignoreReset = false | |||
| 50 | return call, true | 85 | return call, true | ||
| 51 | } | 86 | } | ||
| 52 | 87 | ||||
| @@ -67,6 +102,9 @@ func (s *jsDialogState) resolve(call *jsDialogCall, ok bool, input string) bool | |||||
| 67 | if s.current == call { | 102 | if s.current == call { | ||
| 68 | s.current = nil | 103 | s.current = nil | ||
| 69 | } | 104 | } | ||
| 105 | + | // Armed before Cont: CEF may deliver DialogClosed + OnResetDialogState | |||
| 106 | + | // synchronously from inside it. | |||
| 107 | + | s.ignoreReset = true | |||
| 70 | s.mu.Unlock() | 108 | s.mu.Unlock() | ||
| 71 | continueCEFJSDialog(call.callback, ok, input) | 109 | continueCEFJSDialog(call.callback, ok, input) | ||
| 72 | return true | 110 | return true | ||
| @@ -146,6 +184,10 @@ func (h *handlerSet) OnJsdialog( | |||||
| 146 | if h == nil || h.wv == nil || callback == nil { | 184 | if h == nil || h.wv == nil || callback == nil { | ||
| 147 | return 0 | 185 | return 0 | ||
| 148 | } | 186 | } | ||
| 187 | + | if h.wv.destroyed.Load() { | |||
| 188 | + | // Shutting down: the GTK loop may be blocked. Let CEF dismiss it. | |||
| 189 | + | return 0 | |||
| 190 | + | } | |||
| 149 | typ, ok := jsDialogTypeFromCEF(dialogType) | 191 | typ, ok := jsDialogTypeFromCEF(dialogType) | ||
| 150 | if !ok { | 192 | if !ok { | ||
| 151 | return 0 | 193 | return 0 | ||
| @@ -156,7 +198,9 @@ func (h *handlerSet) OnJsdialog( | |||||
| 156 | } | 198 | } | ||
| 157 | call, ok := h.wv.jsDialogs.begin(callback) | 199 | call, ok := h.wv.jsDialogs.begin(callback) | ||
| 158 | if !ok { | 200 | if !ok { | ||
| 159 | - | // A dialog is already open: suppress rather than stack/spam. | 201 | + | // A dialog is already open: suppress rather than stack/spam. CEF will |
| 202 | + | // follow up with OnResetDialogState, which must not cancel that dialog. | |||
| 203 | + | h.wv.jsDialogs.armIgnoreReset() | |||
| 160 | if suppressMessage != nil { | 204 | if suppressMessage != nil { | ||
| 161 | *suppressMessage = 1 | 205 | *suppressMessage = 1 | ||
| 162 | } | 206 | } | ||
| @@ -175,13 +219,18 @@ func (h *handlerSet) OnJsdialog( | |||||
| 175 | // OnBeforeUnloadDialog implements purecef.JsdialogHandler. | 219 | // OnBeforeUnloadDialog implements purecef.JsdialogHandler. | ||
| 176 | func (h *handlerSet) OnBeforeUnloadDialog( | 220 | func (h *handlerSet) OnBeforeUnloadDialog( | ||
| 177 | _ purecef.Browser, | 221 | _ purecef.Browser, | ||
| 178 | - | messageText string, | 222 | + | _ string, // CEF always passes a fixed string; the UI shows Dumber's own text. |
| 179 | isReload int32, | 223 | isReload int32, | ||
| 180 | callback purecef.JsdialogCallback, | 224 | callback purecef.JsdialogCallback, | ||
| 181 | ) bool { | 225 | ) bool { | ||
| 182 | if h == nil || h.wv == nil || callback == nil { | 226 | if h == nil || h.wv == nil || callback == nil { | ||
| 183 | return false | 227 | return false | ||
| 184 | } | 228 | } | ||
| 229 | + | if h.wv.destroyed.Load() { | |||
| 230 | + | // CloseBrowser dispatches beforeunload; accept it instead of waiting | |||
| 231 | + | // on UI that may never run. CEF accepts the unload when we return false. | |||
| 232 | + | return false | |||
| 233 | + | } | |||
| 185 | ui := h.wv.jsDialogUICallbacks() | 234 | ui := h.wv.jsDialogUICallbacks() | ||
| 186 | if ui == nil || ui.OnJSDialog == nil { | 235 | if ui == nil || ui.OnJSDialog == nil { | ||
| 187 | return false | 236 | return false | ||
| @@ -189,24 +238,33 @@ func (h *handlerSet) OnBeforeUnloadDialog( | |||||
| 189 | call, ok := h.wv.jsDialogs.begin(callback) | 238 | call, ok := h.wv.jsDialogs.begin(callback) | ||
| 190 | if !ok { | 239 | if !ok { | ||
| 191 | // Another dialog is open: stay on the page rather than stacking. | 240 | // Another dialog is open: stay on the page rather than stacking. | ||
| 241 | + | // The Cont below makes CEF send a reset that must not cancel it. | |||
| 242 | + | h.wv.jsDialogs.armIgnoreReset() | |||
| 192 | continueCEFJSDialog(callback, false, "") | 243 | continueCEFJSDialog(callback, false, "") | ||
| 193 | return true | 244 | return true | ||
| 194 | } | 245 | } | ||
| 195 | h.wv.presentJSDialog(call, port.JSDialogRequest{ | 246 | h.wv.presentJSDialog(call, port.JSDialogRequest{ | ||
| 196 | Type: port.JSDialogBeforeUnload, | 247 | Type: port.JSDialogBeforeUnload, | ||
| 197 | Origin: h.wv.URI(), | 248 | Origin: h.wv.URI(), | ||
| 198 | - | Message: messageText, | |||
| 199 | IsReload: isReload != 0, | 249 | IsReload: isReload != 0, | ||
| 200 | }, ui) | 250 | }, ui) | ||
| 201 | return true | 251 | return true | ||
| 202 | } | 252 | } | ||
| 203 | 253 | ||||
| 204 | // OnResetDialogState implements purecef.JsdialogHandler: CEF canceled pending | 254 | // OnResetDialogState implements purecef.JsdialogHandler: CEF canceled pending | ||
| 205 | - | // dialogs (navigation, close). Close the UI and resolve the callback. | 255 | + | // dialogs (navigation, close). Close the UI and resolve the callback, unless |
| 256 | + | // this is the echo of a suppress/reject/Cont (see file comment). | |||
| 257 | + | // | |||
| 258 | + | // The UI reset (OnJSDialogReset) carries no dialog identity: it hides whatever | |||
| 259 | + | // the pane shows. That is safe because a pane shows at most one dialog and a | |||
| 260 | + | // new one is only presented after the previous call was resolved. | |||
| 206 | func (h *handlerSet) OnResetDialogState(_ purecef.Browser) { | 261 | func (h *handlerSet) OnResetDialogState(_ purecef.Browser) { | ||
| 207 | if h == nil || h.wv == nil { | 262 | if h == nil || h.wv == nil { | ||
| 208 | return | 263 | return | ||
| 209 | } | 264 | } | ||
| 265 | + | if h.wv.jsDialogs.consumeIgnoreReset() { | |||
| 266 | + | return | |||
| 267 | + | } | |||
| 210 | h.wv.cancelJSDialogs() | 268 | h.wv.cancelJSDialogs() | ||
| 211 | } | 269 | } | ||
| 212 | 270 | ||||
| @@ -214,22 +272,3 @@ func (h *handlerSet) OnResetDialogState(_ purecef.Browser) { | |||||
| 214 | // nothing: resolve already cleared the pending state, and a late notification | 272 | // nothing: resolve already cleared the pending state, and a late notification | ||
| 215 | // must not clear a newer dialog. | 273 | // must not clear a newer dialog. | ||
| 216 | func (h *handlerSet) OnDialogClosed(_ purecef.Browser) {} | 274 | func (h *handlerSet) OnDialogClosed(_ purecef.Browser) {} | ||
| 217 | - | ||||
| 218 | - | // continueCEFJSDialogOnUIThread answers the callback on the CEF UI thread. | |||
| 219 | - | func continueCEFJSDialogOnUIThread(callback purecef.JsdialogCallback, ok bool, input string) { | |||
| 220 | - | if callback == nil { | |||
| 221 | - | return | |||
| 222 | - | } | |||
| 223 | - | success := int32(0) | |||
| 224 | - | if ok { | |||
| 225 | - | success = 1 | |||
| 226 | - | } | |||
| 227 | - | if purecef.CurrentlyOn(purecef.ThreadIDTidUi) == 1 { | |||
| 228 | - | callback.Cont(success, input) | |||
| 229 | - | return | |||
| 230 | - | } | |||
| 231 | - | task := cefNewTask(cefTaskFunc(func() { callback.Cont(success, input) })) | |||
| 232 | - | if cefPostTask(purecef.ThreadIDTidUi, task) != 1 { | |||
| 233 | - | callback.Cont(success, input) | |||
| 234 | - | } | |||
| 235 | - | } | |||
M internal/infrastructure/cef/jsdialog_handler_test.go +116 -2
| @@ -12,6 +12,9 @@ import ( | |||
| 12 | 12 | ||
| 13 | 13 | type stubJSDialogCallback struct { | |
| 14 | 14 | calls []jsDialogAnswer | |
| 15 | + | // onCont mimics CEF's DialogClosed ordering: OnDialogClosed and | |
| 16 | + | // OnResetDialogState are delivered from inside Cont. | |
| 17 | + | onCont func() | |
| 15 | 18 | } | |
| 16 | 19 | ||
| 17 | 20 | type jsDialogAnswer struct { | |
| @@ -21,6 +24,17 @@ type jsDialogAnswer struct { | |||
| 21 | 24 | ||
| 22 | 25 | func (s *stubJSDialogCallback) Cont(success int32, userInput string) { | |
| 23 | 26 | s.calls = append(s.calls, jsDialogAnswer{ok: success != 0, input: userInput}) | |
| 27 | + | if s.onCont != nil { | |
| 28 | + | s.onCont() | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // echoCEFClose makes cb deliver CEF's DialogClosed + reset echo from Cont. | |
| 33 | + | func echoCEFClose(h *handlerSet, cb *stubJSDialogCallback) { | |
| 34 | + | cb.onCont = func() { | |
| 35 | + | h.OnDialogClosed(nil) | |
| 36 | + | h.OnResetDialogState(nil) | |
| 37 | + | } | |
| 24 | 38 | } | |
| 25 | 39 | ||
| 26 | 40 | // useDirectJSDialogContinue answers stub callbacks inline (no CEF UI thread). | |
| @@ -176,8 +190,9 @@ func TestOnBeforeUnloadDialog(t *testing.T) { | |||
| 176 | 190 | cb := &stubJSDialogCallback{} | |
| 177 | 191 | ||
| 178 | 192 | require.True(t, h.OnBeforeUnloadDialog(nil, "unsaved", 1, cb)) | |
| 193 | + | // CEF's fixed message text is not forwarded. | |
| 179 | 194 | require.Equal(t, []port.JSDialogRequest{{ | |
| 180 | - | Type: port.JSDialogBeforeUnload, Origin: "https://example.com/edit", Message: "unsaved", IsReload: true, | |
| 195 | + | Type: port.JSDialogBeforeUnload, Origin: "https://example.com/edit", IsReload: true, | |
| 181 | 196 | }}, ui.reqs) | |
| 182 | 197 | ||
| 183 | 198 | // A second beforeunload while open is answered "stay" immediately. | |
| @@ -220,7 +235,7 @@ func TestDestroyCancelsPendingJSDialog(t *testing.T) { | |||
| 220 | 235 | cb := &stubJSDialogCallback{} | |
| 221 | 236 | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "m", "", cb, new(int32)) | |
| 222 | 237 | ||
| 223 | - | wv.cancelJSDialogs() | |
| 238 | + | wv.Destroy() | |
| 224 | 239 | ||
| 225 | 240 | require.Equal(t, []jsDialogAnswer{{ok: false}}, cb.calls) | |
| 226 | 241 | require.Equal(t, 1, ui.resets) | |
| @@ -240,3 +255,102 @@ func TestJSDialogCancelledBeforeUIRunsIsNotShown(t *testing.T) { | |||
| 240 | 255 | require.Empty(t, ui.reqs) | |
| 241 | 256 | require.Len(t, cb.calls, 1) | |
| 242 | 257 | } | |
| 258 | + | ||
| 259 | + | func TestResetEchoAfterSuppressDoesNotCancelOpenDialog(t *testing.T) { | |
| 260 | + | useDirectJSDialogContinue(t) | |
| 261 | + | ui := &jsDialogUIRecorder{handled: true} | |
| 262 | + | h := &handlerSet{wv: newJSDialogWebView(ui)} | |
| 263 | + | first := &stubJSDialogCallback{} | |
| 264 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "1", "", first, new(int32)) | |
| 265 | + | ||
| 266 | + | // A spammer is suppressed; CEF then sends OnResetDialogState. | |
| 267 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "2", "", &stubJSDialogCallback{}, new(int32)) | |
| 268 | + | h.OnResetDialogState(nil) | |
| 269 | + | ||
| 270 | + | require.Empty(t, first.calls, "dialog #1 must stay pending") | |
| 271 | + | require.Zero(t, ui.resets) | |
| 272 | + | ||
| 273 | + | // A real reset afterwards still cancels it. | |
| 274 | + | h.OnResetDialogState(nil) | |
| 275 | + | require.Equal(t, []jsDialogAnswer{{ok: false}}, first.calls) | |
| 276 | + | require.Equal(t, 1, ui.resets) | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | func TestResetEchoAfterRejectedBeforeUnloadDoesNotCancelOpenDialog(t *testing.T) { | |
| 280 | + | useDirectJSDialogContinue(t) | |
| 281 | + | ui := &jsDialogUIRecorder{handled: true} | |
| 282 | + | h := &handlerSet{wv: newJSDialogWebView(ui)} | |
| 283 | + | first := &stubJSDialogCallback{} | |
| 284 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeConfirm, "1", "", first, new(int32)) | |
| 285 | + | ||
| 286 | + | other := &stubJSDialogCallback{} | |
| 287 | + | echoCEFClose(h, other) | |
| 288 | + | require.True(t, h.OnBeforeUnloadDialog(nil, "", 0, other)) | |
| 289 | + | ||
| 290 | + | require.Empty(t, first.calls) | |
| 291 | + | require.Zero(t, ui.resets) | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | func TestCEFCloseEchoDoesNotCancelNewerDialog(t *testing.T) { | |
| 295 | + | useDirectJSDialogContinue(t) | |
| 296 | + | ui := &jsDialogUIRecorder{handled: true} | |
| 297 | + | h := &handlerSet{wv: newJSDialogWebView(ui)} | |
| 298 | + | first := &stubJSDialogCallback{} | |
| 299 | + | echoCEFClose(h, first) | |
| 300 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "1", "", first, new(int32)) | |
| 301 | + | ||
| 302 | + | ui.respond[0](true, "") // Cont -> OnDialogClosed + OnResetDialogState (echo) | |
| 303 | + | require.Equal(t, []jsDialogAnswer{{ok: true}}, first.calls) | |
| 304 | + | require.Zero(t, ui.resets, "echo must not hide the UI") | |
| 305 | + | ||
| 306 | + | // A newer dialog is unaffected by a trailing echo that arrives late. | |
| 307 | + | second := &stubJSDialogCallback{} | |
| 308 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "2", "", second, new(int32)) | |
| 309 | + | h.OnResetDialogState(nil) // a genuine reset | |
| 310 | + | require.Equal(t, []jsDialogAnswer{{ok: false}}, second.calls) | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | func TestNoStaleIgnoreAcrossDialogs(t *testing.T) { | |
| 314 | + | useDirectJSDialogContinue(t) | |
| 315 | + | ui := &jsDialogUIRecorder{handled: true} | |
| 316 | + | h := &handlerSet{wv: newJSDialogWebView(ui)} | |
| 317 | + | // Answered without a CEF echo (e.g. reset-then-late-answer path). | |
| 318 | + | first := &stubJSDialogCallback{} | |
| 319 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "1", "", first, new(int32)) | |
| 320 | + | ui.respond[0](true, "") | |
| 321 | + | ||
| 322 | + | second := &stubJSDialogCallback{} | |
| 323 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "2", "", second, new(int32)) | |
| 324 | + | h.OnResetDialogState(nil) | |
| 325 | + | ||
| 326 | + | require.Equal(t, []jsDialogAnswer{{ok: false}}, second.calls, "real reset must cancel the new dialog") | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | func TestDestroyedWebViewDoesNotShowDialogs(t *testing.T) { | |
| 330 | + | ui := &jsDialogUIRecorder{handled: true} | |
| 331 | + | wv := newJSDialogWebView(ui) | |
| 332 | + | wv.destroyed.Store(true) | |
| 333 | + | h := &handlerSet{wv: wv} | |
| 334 | + | cb := &stubJSDialogCallback{} | |
| 335 | + | var suppress int32 | |
| 336 | + | ||
| 337 | + | require.Zero(t, h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "m", "", cb, &suppress)) | |
| 338 | + | require.False(t, h.OnBeforeUnloadDialog(nil, "", 0, cb)) | |
| 339 | + | require.Zero(t, suppress) | |
| 340 | + | require.Empty(t, ui.reqs) | |
| 341 | + | require.Empty(t, cb.calls) | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | func TestRenderProcessTerminatedCancelsPendingJSDialog(t *testing.T) { | |
| 345 | + | useDirectJSDialogContinue(t) | |
| 346 | + | ui := &jsDialogUIRecorder{handled: true} | |
| 347 | + | wv := newJSDialogWebView(ui) | |
| 348 | + | h := &handlerSet{wv: wv} | |
| 349 | + | cb := &stubJSDialogCallback{} | |
| 350 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "m", "", cb, new(int32)) | |
| 351 | + | ||
| 352 | + | h.OnRenderProcessTerminated(nil, purecef.TerminationStatusTsProcessCrashed, 0, "") | |
| 353 | + | ||
| 354 | + | require.Equal(t, []jsDialogAnswer{{ok: false}}, cb.calls) | |
| 355 | + | require.Equal(t, 1, ui.resets) | |
| 356 | + | } | |
| @@ -12,6 +12,9 @@ import ( | |||||
| 12 | 12 | ||||
| 13 | type stubJSDialogCallback struct { | 13 | type stubJSDialogCallback struct { | ||
| 14 | calls []jsDialogAnswer | 14 | calls []jsDialogAnswer | ||
| 15 | + | // onCont mimics CEF's DialogClosed ordering: OnDialogClosed and | |||
| 16 | + | // OnResetDialogState are delivered from inside Cont. | |||
| 17 | + | onCont func() | |||
| 15 | } | 18 | } | ||
| 16 | 19 | ||||
| 17 | type jsDialogAnswer struct { | 20 | type jsDialogAnswer struct { | ||
| @@ -21,6 +24,17 @@ type jsDialogAnswer struct { | |||||
| 21 | 24 | ||||
| 22 | func (s *stubJSDialogCallback) Cont(success int32, userInput string) { | 25 | func (s *stubJSDialogCallback) Cont(success int32, userInput string) { | ||
| 23 | s.calls = append(s.calls, jsDialogAnswer{ok: success != 0, input: userInput}) | 26 | s.calls = append(s.calls, jsDialogAnswer{ok: success != 0, input: userInput}) | ||
| 27 | + | if s.onCont != nil { | |||
| 28 | + | s.onCont() | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // echoCEFClose makes cb deliver CEF's DialogClosed + reset echo from Cont. | |||
| 33 | + | func echoCEFClose(h *handlerSet, cb *stubJSDialogCallback) { | |||
| 34 | + | cb.onCont = func() { | |||
| 35 | + | h.OnDialogClosed(nil) | |||
| 36 | + | h.OnResetDialogState(nil) | |||
| 37 | + | } | |||
| 24 | } | 38 | } | ||
| 25 | 39 | ||||
| 26 | // useDirectJSDialogContinue answers stub callbacks inline (no CEF UI thread). | 40 | // useDirectJSDialogContinue answers stub callbacks inline (no CEF UI thread). | ||
| @@ -176,8 +190,9 @@ func TestOnBeforeUnloadDialog(t *testing.T) { | |||||
| 176 | cb := &stubJSDialogCallback{} | 190 | cb := &stubJSDialogCallback{} | ||
| 177 | 191 | ||||
| 178 | require.True(t, h.OnBeforeUnloadDialog(nil, "unsaved", 1, cb)) | 192 | require.True(t, h.OnBeforeUnloadDialog(nil, "unsaved", 1, cb)) | ||
| 193 | + | // CEF's fixed message text is not forwarded. | |||
| 179 | require.Equal(t, []port.JSDialogRequest{{ | 194 | require.Equal(t, []port.JSDialogRequest{{ | ||
| 180 | - | Type: port.JSDialogBeforeUnload, Origin: "https://example.com/edit", Message: "unsaved", IsReload: true, | 195 | + | Type: port.JSDialogBeforeUnload, Origin: "https://example.com/edit", IsReload: true, |
| 181 | }}, ui.reqs) | 196 | }}, ui.reqs) | ||
| 182 | 197 | ||||
| 183 | // A second beforeunload while open is answered "stay" immediately. | 198 | // A second beforeunload while open is answered "stay" immediately. | ||
| @@ -220,7 +235,7 @@ func TestDestroyCancelsPendingJSDialog(t *testing.T) { | |||||
| 220 | cb := &stubJSDialogCallback{} | 235 | cb := &stubJSDialogCallback{} | ||
| 221 | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "m", "", cb, new(int32)) | 236 | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "m", "", cb, new(int32)) | ||
| 222 | 237 | ||||
| 223 | - | wv.cancelJSDialogs() | 238 | + | wv.Destroy() |
| 224 | 239 | ||||
| 225 | require.Equal(t, []jsDialogAnswer{{ok: false}}, cb.calls) | 240 | require.Equal(t, []jsDialogAnswer{{ok: false}}, cb.calls) | ||
| 226 | require.Equal(t, 1, ui.resets) | 241 | require.Equal(t, 1, ui.resets) | ||
| @@ -240,3 +255,102 @@ func TestJSDialogCancelledBeforeUIRunsIsNotShown(t *testing.T) { | |||||
| 240 | require.Empty(t, ui.reqs) | 255 | require.Empty(t, ui.reqs) | ||
| 241 | require.Len(t, cb.calls, 1) | 256 | require.Len(t, cb.calls, 1) | ||
| 242 | } | 257 | } | ||
| 258 | + | ||||
| 259 | + | func TestResetEchoAfterSuppressDoesNotCancelOpenDialog(t *testing.T) { | |||
| 260 | + | useDirectJSDialogContinue(t) | |||
| 261 | + | ui := &jsDialogUIRecorder{handled: true} | |||
| 262 | + | h := &handlerSet{wv: newJSDialogWebView(ui)} | |||
| 263 | + | first := &stubJSDialogCallback{} | |||
| 264 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "1", "", first, new(int32)) | |||
| 265 | + | ||||
| 266 | + | // A spammer is suppressed; CEF then sends OnResetDialogState. | |||
| 267 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "2", "", &stubJSDialogCallback{}, new(int32)) | |||
| 268 | + | h.OnResetDialogState(nil) | |||
| 269 | + | ||||
| 270 | + | require.Empty(t, first.calls, "dialog #1 must stay pending") | |||
| 271 | + | require.Zero(t, ui.resets) | |||
| 272 | + | ||||
| 273 | + | // A real reset afterwards still cancels it. | |||
| 274 | + | h.OnResetDialogState(nil) | |||
| 275 | + | require.Equal(t, []jsDialogAnswer{{ok: false}}, first.calls) | |||
| 276 | + | require.Equal(t, 1, ui.resets) | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | func TestResetEchoAfterRejectedBeforeUnloadDoesNotCancelOpenDialog(t *testing.T) { | |||
| 280 | + | useDirectJSDialogContinue(t) | |||
| 281 | + | ui := &jsDialogUIRecorder{handled: true} | |||
| 282 | + | h := &handlerSet{wv: newJSDialogWebView(ui)} | |||
| 283 | + | first := &stubJSDialogCallback{} | |||
| 284 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeConfirm, "1", "", first, new(int32)) | |||
| 285 | + | ||||
| 286 | + | other := &stubJSDialogCallback{} | |||
| 287 | + | echoCEFClose(h, other) | |||
| 288 | + | require.True(t, h.OnBeforeUnloadDialog(nil, "", 0, other)) | |||
| 289 | + | ||||
| 290 | + | require.Empty(t, first.calls) | |||
| 291 | + | require.Zero(t, ui.resets) | |||
| 292 | + | } | |||
| 293 | + | ||||
| 294 | + | func TestCEFCloseEchoDoesNotCancelNewerDialog(t *testing.T) { | |||
| 295 | + | useDirectJSDialogContinue(t) | |||
| 296 | + | ui := &jsDialogUIRecorder{handled: true} | |||
| 297 | + | h := &handlerSet{wv: newJSDialogWebView(ui)} | |||
| 298 | + | first := &stubJSDialogCallback{} | |||
| 299 | + | echoCEFClose(h, first) | |||
| 300 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "1", "", first, new(int32)) | |||
| 301 | + | ||||
| 302 | + | ui.respond[0](true, "") // Cont -> OnDialogClosed + OnResetDialogState (echo) | |||
| 303 | + | require.Equal(t, []jsDialogAnswer{{ok: true}}, first.calls) | |||
| 304 | + | require.Zero(t, ui.resets, "echo must not hide the UI") | |||
| 305 | + | ||||
| 306 | + | // A newer dialog is unaffected by a trailing echo that arrives late. | |||
| 307 | + | second := &stubJSDialogCallback{} | |||
| 308 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "2", "", second, new(int32)) | |||
| 309 | + | h.OnResetDialogState(nil) // a genuine reset | |||
| 310 | + | require.Equal(t, []jsDialogAnswer{{ok: false}}, second.calls) | |||
| 311 | + | } | |||
| 312 | + | ||||
| 313 | + | func TestNoStaleIgnoreAcrossDialogs(t *testing.T) { | |||
| 314 | + | useDirectJSDialogContinue(t) | |||
| 315 | + | ui := &jsDialogUIRecorder{handled: true} | |||
| 316 | + | h := &handlerSet{wv: newJSDialogWebView(ui)} | |||
| 317 | + | // Answered without a CEF echo (e.g. reset-then-late-answer path). | |||
| 318 | + | first := &stubJSDialogCallback{} | |||
| 319 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "1", "", first, new(int32)) | |||
| 320 | + | ui.respond[0](true, "") | |||
| 321 | + | ||||
| 322 | + | second := &stubJSDialogCallback{} | |||
| 323 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "2", "", second, new(int32)) | |||
| 324 | + | h.OnResetDialogState(nil) | |||
| 325 | + | ||||
| 326 | + | require.Equal(t, []jsDialogAnswer{{ok: false}}, second.calls, "real reset must cancel the new dialog") | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | func TestDestroyedWebViewDoesNotShowDialogs(t *testing.T) { | |||
| 330 | + | ui := &jsDialogUIRecorder{handled: true} | |||
| 331 | + | wv := newJSDialogWebView(ui) | |||
| 332 | + | wv.destroyed.Store(true) | |||
| 333 | + | h := &handlerSet{wv: wv} | |||
| 334 | + | cb := &stubJSDialogCallback{} | |||
| 335 | + | var suppress int32 | |||
| 336 | + | ||||
| 337 | + | require.Zero(t, h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "m", "", cb, &suppress)) | |||
| 338 | + | require.False(t, h.OnBeforeUnloadDialog(nil, "", 0, cb)) | |||
| 339 | + | require.Zero(t, suppress) | |||
| 340 | + | require.Empty(t, ui.reqs) | |||
| 341 | + | require.Empty(t, cb.calls) | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | func TestRenderProcessTerminatedCancelsPendingJSDialog(t *testing.T) { | |||
| 345 | + | useDirectJSDialogContinue(t) | |||
| 346 | + | ui := &jsDialogUIRecorder{handled: true} | |||
| 347 | + | wv := newJSDialogWebView(ui) | |||
| 348 | + | h := &handlerSet{wv: wv} | |||
| 349 | + | cb := &stubJSDialogCallback{} | |||
| 350 | + | h.OnJsdialog(nil, "", purecef.JsdialogTypeJsdialogtypeAlert, "m", "", cb, new(int32)) | |||
| 351 | + | ||||
| 352 | + | h.OnRenderProcessTerminated(nil, purecef.TerminationStatusTsProcessCrashed, 0, "") | |||
| 353 | + | ||||
| 354 | + | require.Equal(t, []jsDialogAnswer{{ok: false}}, cb.calls) | |||
| 355 | + | require.Equal(t, 1, ui.resets) | |||
| 356 | + | } | |||
M internal/infrastructure/cef/webview_handlers.go +4 -0
| @@ -1298,6 +1298,10 @@ const maxConsecutiveCrashes = 3 | |||
| 1298 | 1298 | ||
| 1299 | 1299 | // OnRenderProcessTerminated fires the OnWebProcessTerminated callback with a mapped reason. | |
| 1300 | 1300 | func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef.TerminationStatus, _ int32, _ string) { | |
| 1301 | + | // CEF never resets dialog state on a renderer crash; don't leave the UI | |
| 1302 | + | // (and the dead page's callback) hanging. | |
| 1303 | + | h.wv.cancelJSDialogs() | |
| 1304 | + | ||
| 1301 | 1305 | if h.wv.crashCount.Add(1) > maxConsecutiveCrashes { | |
| 1302 | 1306 | return // suppress to break the loop | |
| 1303 | 1307 | } | |
| @@ -1298,6 +1298,10 @@ const maxConsecutiveCrashes = 3 | |||||
| 1298 | 1298 | ||||
| 1299 | // OnRenderProcessTerminated fires the OnWebProcessTerminated callback with a mapped reason. | 1299 | // OnRenderProcessTerminated fires the OnWebProcessTerminated callback with a mapped reason. | ||
| 1300 | func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef.TerminationStatus, _ int32, _ string) { | 1300 | func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef.TerminationStatus, _ int32, _ string) { | ||
| 1301 | + | // CEF never resets dialog state on a renderer crash; don't leave the UI | |||
| 1302 | + | // (and the dead page's callback) hanging. | |||
| 1303 | + | h.wv.cancelJSDialogs() | |||
| 1304 | + | ||||
| 1301 | if h.wv.crashCount.Add(1) > maxConsecutiveCrashes { | 1305 | if h.wv.crashCount.Add(1) > maxConsecutiveCrashes { | ||
| 1302 | return // suppress to break the loop | 1306 | return // suppress to break the loop | ||
| 1303 | } | 1307 | } | ||