Browse Source

fix: unresponsive window when reloading with breakpoint in devtools (#24490)

* fix: unresponsive window when reloading with breakpoint in devtools

Backports https://chromium-review.googlesource.com/c/v8/v8/+/1924366

* update patches

Co-authored-by: Electron Bot <[email protected]>
Robo 4 years ago
parent
commit
49e63948b7

+ 1 - 0
patches/v8/.patches

@@ -13,3 +13,4 @@ use_context_of_then_function_for_promiseresolvethenablejob.patch
 merged_regexp_reserve_space_for_all_registers_in_interpreter.patch
 cherry-pick-d4ddf645c3ca.patch
 turn_some_dchecks_into_checks_in_schedule_methods.patch
+debugger_allow_termination-on-resume_when_paused_at_a_breakpoint.patch

+ 883 - 0
patches/v8/debugger_allow_termination-on-resume_when_paused_at_a_breakpoint.patch

@@ -0,0 +1,883 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Sigurd Schneider <[email protected]>
+Date: Mon, 3 Feb 2020 14:45:06 +0100
+Subject: Allow termination-on-resume when paused at a breakpoint
+
+This CL implements functionality to allow an embedder to mark a
+debug scope as terminate-on-resume. This results in a termination
+exception when that debug scope is left and execution is resumed.
+Execution of JavaScript remains possible after a debug scope is
+marked as terminate-on-resume (but before execution of the paused
+code resumes).
+This is used by blink to correctly prevent resuming JavaScript
+execution upon reload while being paused at a breakpoint.
+
+This is important for handling reloads while paused at a breakpoint
+in blink. The resume command terminates blink's nested message loop
+that is used while to keep the frame responsive while the debugger
+is paused. But if a reload is triggered while execution is paused
+on a breakpoint, but before execution is actually resumed from the
+ breakpoint (that means before returning into the V8 JavaScript
+frames that are paused on the stack below the C++ frames that belong
+to the nested message loop), we re-enter V8 to do tear-down actions
+of the old frame. In this case Runtime.terminateExecution() cannot be
+used before Debugger.resume(), because the tear-down actions that
+re-enter V8 would trigger the termination exception and crash the
+browser (because the browser expected the tear-down to succeed).
+
+Hence we introduce this flag on V8 that says: It is OK if someone
+re-enters V8 (to execute JS), but upon resuming from the breakpoint
+(i.e. returning to the paused frames that are on the stack below),
+generate a termination exception.
+
+We deliberated adding a corresponding logic on the blink side (instead
+of V8) but we think this is the simplest solution.
+
+More details in the design doc:
+
+https://docs.google.com/document/d/1aO9v0YhoKNqKleqfACGUpwrBUayLFGqktz9ltdgKHMk
+
+Bug: chromium:1004038, chromium:1014415
+
+Change-Id: I896692d4c21cb0acae89c1d783d37ce45b73c113
+Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1924366
+Commit-Queue: Sigurd Schneider <[email protected]>
+Reviewed-by: Toon Verwaest <[email protected]>
+Reviewed-by: Dmitry Gozman <[email protected]>
+Reviewed-by: Yang Guo <[email protected]>
+Cr-Commit-Position: refs/heads/master@{#66084}
+
+diff --git a/include/js_protocol.pdl b/include/js_protocol.pdl
+index 427d654cac786b303755d1a805d12e928a454bdc..35b260ab753b429313faa1312644d8e43e32dd40 100644
+--- a/include/js_protocol.pdl
++++ b/include/js_protocol.pdl
+@@ -273,6 +273,13 @@ domain Debugger
+ 
+   # Resumes JavaScript execution.
+   command resume
++    parameters
++      # Set to true to terminate execution upon resuming execution. In contrast
++      # to Runtime.terminateExecution, this will allows to execute further
++      # JavaScript (i.e. via evaluation) until execution of the paused code
++      # is actually resumed, at which point termination is triggered.
++      # If execution is currently not paused, this parameter has no effect.
++      optional boolean terminateOnResume
+ 
+   # Searches for given string in script content.
+   command searchInContent
+diff --git a/include/v8-inspector.h b/include/v8-inspector.h
+index 3ec1325613a1ae2eb53fe10b139e667149dacc8c..6131042024b7622c5578f6944d97bc53a25b5596 100644
+--- a/include/v8-inspector.h
++++ b/include/v8-inspector.h
+@@ -145,7 +145,7 @@ class V8_EXPORT V8InspectorSession {
+   virtual void breakProgram(const StringView& breakReason,
+                             const StringView& breakDetails) = 0;
+   virtual void setSkipAllPauses(bool) = 0;
+-  virtual void resume() = 0;
++  virtual void resume(bool setTerminateOnResume = false) = 0;
+   virtual void stepOver() = 0;
+   virtual std::vector<std::unique_ptr<protocol::Debugger::API::SearchMatch>>
+   searchInTextByLines(const StringView& text, const StringView& query,
+diff --git a/src/api/api.cc b/src/api/api.cc
+index 6228b23b890af55de1c5053d5b3b7b0d5142fa9a..d67366746fed63201a73d159fc759fcb269d5d5d 100644
+--- a/src/api/api.cc
++++ b/src/api/api.cc
+@@ -9396,6 +9396,12 @@ void debug::BreakRightNow(Isolate* v8_isolate) {
+   isolate->debug()->HandleDebugBreak(i::kIgnoreIfAllFramesBlackboxed);
+ }
+ 
++void debug::SetTerminateOnResume(Isolate* v8_isolate) {
++  i::Isolate* isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
++  ENTER_V8_NO_SCRIPT_NO_EXCEPTION(isolate);
++  isolate->debug()->SetTerminateOnResume();
++}
++
+ bool debug::AllFramesOnStackAreBlackboxed(Isolate* v8_isolate) {
+   i::Isolate* isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
+   ENTER_V8_DO_NOT_USE(isolate);
+diff --git a/src/debug/debug-interface.h b/src/debug/debug-interface.h
+index ac8888b5703c335dc6773b885fe0bb16088ce741..a8c85c185952c6544dd670bd3b61e6893375d0fa 100644
+--- a/src/debug/debug-interface.h
++++ b/src/debug/debug-interface.h
+@@ -93,6 +93,13 @@ void PrepareStep(Isolate* isolate, StepAction action);
+ void ClearStepping(Isolate* isolate);
+ V8_EXPORT_PRIVATE void BreakRightNow(Isolate* isolate);
+ 
++// Use `SetTerminateOnResume` to indicate that an TerminateExecution interrupt
++// should be set shortly before resuming, i.e. shortly before returning into
++// the JavaScript stack frames on the stack. In contrast to setting the
++// interrupt with `RequestTerminateExecution` directly, this flag allows
++// the isolate to be entered for further JavaScript execution.
++V8_EXPORT_PRIVATE void SetTerminateOnResume(Isolate* isolate);
++
+ bool AllFramesOnStackAreBlackboxed(Isolate* isolate);
+ 
+ class Script;
+diff --git a/src/debug/debug.cc b/src/debug/debug.cc
+index b31e305a02bc531c1facade1d47c12dea993e178..547decf42ba17e4ac3be081a1186b70cc2dc3cfe 100644
+--- a/src/debug/debug.cc
++++ b/src/debug/debug.cc
+@@ -1718,8 +1718,8 @@ Handle<FixedArray> Debug::GetLoadedScripts() {
+   return FixedArray::ShrinkOrEmpty(isolate_, results, length);
+ }
+ 
+-void Debug::OnThrow(Handle<Object> exception) {
+-  if (in_debug_scope() || ignore_events()) return;
++base::Optional<Object> Debug::OnThrow(Handle<Object> exception) {
++  if (in_debug_scope() || ignore_events()) return {};
+   // Temporarily clear any scheduled_exception to allow evaluating
+   // JavaScript from the debug event handler.
+   HandleScope scope(isolate_);
+@@ -1736,6 +1736,14 @@ void Debug::OnThrow(Handle<Object> exception) {
+     isolate_->thread_local_top()->scheduled_exception_ = *scheduled_exception;
+   }
+   PrepareStepOnThrow();
++  // If the OnException handler requested termination, then indicated this to
++  // our caller Isolate::Throw so it can deal with it immediatelly instead of
++  // throwing the original exception.
++  if (isolate_->stack_guard()->CheckTerminateExecution()) {
++    isolate_->stack_guard()->ClearTerminateExecution();
++    return isolate_->TerminateExecution();
++  }
++  return {};
+ }
+ 
+ void Debug::OnPromiseReject(Handle<Object> promise, Handle<Object> value) {
+@@ -2091,7 +2099,6 @@ DebugScope::DebugScope(Debug* debug)
+   // Link recursive debugger entry.
+   base::Relaxed_Store(&debug_->thread_local_.current_debug_scope_,
+                       reinterpret_cast<base::AtomicWord>(this));
+-
+   // Store the previous frame id and return value.
+   break_frame_id_ = debug_->break_frame_id();
+ 
+@@ -2105,8 +2112,18 @@ DebugScope::DebugScope(Debug* debug)
+   debug_->UpdateState();
+ }
+ 
++void DebugScope::set_terminate_on_resume() { terminate_on_resume_ = true; }
+ 
+ DebugScope::~DebugScope() {
++  // Terminate on resume must have been handled by retrieving it, if this is
++  // the outer scope.
++  if (terminate_on_resume_) {
++    if (!prev_) {
++      debug_->isolate_->stack_guard()->RequestTerminateExecution();
++    } else {
++      prev_->set_terminate_on_resume();
++    }
++  }
+   // Leaving this debugger entry.
+   base::Relaxed_Store(&debug_->thread_local_.current_debug_scope_,
+                       reinterpret_cast<base::AtomicWord>(prev_));
+@@ -2146,6 +2163,13 @@ void Debug::UpdateDebugInfosForExecutionMode() {
+   }
+ }
+ 
++void Debug::SetTerminateOnResume() {
++  DebugScope* scope = reinterpret_cast<DebugScope*>(
++      base::Acquire_Load(&thread_local_.current_debug_scope_));
++  CHECK_NOT_NULL(scope);
++  scope->set_terminate_on_resume();
++}
++
+ void Debug::StartSideEffectCheckMode() {
+   DCHECK(isolate_->debug_execution_mode() != DebugInfo::kSideEffects);
+   isolate_->set_debug_execution_mode(DebugInfo::kSideEffects);
+diff --git a/src/debug/debug.h b/src/debug/debug.h
+index 9100dad871060365232b7219f625aca3d63764d0..3444b39f4241d61f4e6716bfab5fafc195b12ec6 100644
+--- a/src/debug/debug.h
++++ b/src/debug/debug.h
+@@ -217,7 +217,8 @@ class V8_EXPORT_PRIVATE Debug {
+   // Debug event triggers.
+   void OnDebugBreak(Handle<FixedArray> break_points_hit);
+ 
+-  void OnThrow(Handle<Object> exception);
++  base::Optional<Object> OnThrow(Handle<Object> exception)
++      V8_WARN_UNUSED_RESULT;
+   void OnPromiseReject(Handle<Object> promise, Handle<Object> value);
+   void OnCompileError(Handle<Script> script);
+   void OnAfterCompile(Handle<Script> script);
+@@ -238,6 +239,8 @@ class V8_EXPORT_PRIVATE Debug {
+   void ChangeBreakOnException(ExceptionBreakType type, bool enable);
+   bool IsBreakOnException(ExceptionBreakType type);
+ 
++  void SetTerminateOnResume();
++
+   bool SetBreakPointForScript(Handle<Script> script, Handle<String> condition,
+                               int* source_position, int* id);
+   bool SetBreakpointForFunction(Handle<SharedFunctionInfo> shared,
+@@ -565,6 +568,8 @@ class DebugScope {
+   explicit DebugScope(Debug* debug);
+   ~DebugScope();
+ 
++  void set_terminate_on_resume();
++
+  private:
+   Isolate* isolate() { return debug_->isolate_; }
+ 
+@@ -572,6 +577,8 @@ class DebugScope {
+   DebugScope* prev_;               // Previous scope if entered recursively.
+   StackFrameId break_frame_id_;    // Previous break frame id.
+   PostponeInterruptsScope no_interrupts_;
++  // This is used as a boolean.
++  bool terminate_on_resume_ = false;
+ };
+ 
+ // This scope is used to handle return values in nested debug break points.
+diff --git a/src/execution/isolate.cc b/src/execution/isolate.cc
+index ef11e5ad53b357ce2ac7613a08d3a8195d3241e4..17113e7ec11024f42a6b616c3ffcb9f85bc96c63 100644
+--- a/src/execution/isolate.cc
++++ b/src/execution/isolate.cc
+@@ -1563,7 +1563,10 @@ Object Isolate::Throw(Object raw_exception, MessageLocation* location) {
+ 
+   // Notify debugger of exception.
+   if (is_catchable_by_javascript(raw_exception)) {
+-    debug()->OnThrow(exception);
++    base::Optional<Object> maybe_exception = debug()->OnThrow(exception);
++    if (maybe_exception.has_value()) {
++      return *maybe_exception;
++    }
+   }
+ 
+   // Generate the message if required.
+diff --git a/src/inspector/v8-debugger-agent-impl.cc b/src/inspector/v8-debugger-agent-impl.cc
+index eda3297922e2e6660ad5436b02a989e0371f9339..9c6e7bea24fdfd7c8a74089d0a6b97fb50abafc8 100644
+--- a/src/inspector/v8-debugger-agent-impl.cc
++++ b/src/inspector/v8-debugger-agent-impl.cc
+@@ -1029,10 +1029,11 @@ Response V8DebuggerAgentImpl::pause() {
+   return Response::OK();
+ }
+ 
+-Response V8DebuggerAgentImpl::resume() {
++Response V8DebuggerAgentImpl::resume(Maybe<bool> terminateOnResume) {
+   if (!isPaused()) return Response::Error(kDebuggerNotPaused);
+   m_session->releaseObjectGroup(kBacktraceObjectGroup);
+-  m_debugger->continueProgram(m_session->contextGroupId());
++  m_debugger->continueProgram(m_session->contextGroupId(),
++                              terminateOnResume.fromMaybe(false));
+   return Response::OK();
+ }
+ 
+diff --git a/src/inspector/v8-debugger-agent-impl.h b/src/inspector/v8-debugger-agent-impl.h
+index 5b1c7fcdbc333876d0c752edb304475fd01c57af..b7e87a91b0f253ce0cc19e9496c2e2952753b095 100644
+--- a/src/inspector/v8-debugger-agent-impl.h
++++ b/src/inspector/v8-debugger-agent-impl.h
+@@ -98,7 +98,7 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend {
+   Response getWasmBytecode(const String16& scriptId,
+                            protocol::Binary* bytecode) override;
+   Response pause() override;
+-  Response resume() override;
++  Response resume(Maybe<bool> terminateOnResume) override;
+   Response stepOver() override;
+   Response stepInto(Maybe<bool> inBreakOnAsyncCall) override;
+   Response stepOut() override;
+diff --git a/src/inspector/v8-debugger.cc b/src/inspector/v8-debugger.cc
+index e3ed733003d1c4b6ab0598cf8af285c03924ca71..5d37fb4ea708d62c212ce6d41dda018d478c771b 100644
+--- a/src/inspector/v8-debugger.cc
++++ b/src/inspector/v8-debugger.cc
+@@ -247,9 +247,15 @@ void V8Debugger::interruptAndBreak(int targetContextGroupId) {
+       nullptr);
+ }
+ 
+-void V8Debugger::continueProgram(int targetContextGroupId) {
++void V8Debugger::continueProgram(int targetContextGroupId,
++                                 bool terminateOnResume) {
+   if (m_pausedContextGroupId != targetContextGroupId) return;
+-  if (isPaused()) m_inspector->client()->quitMessageLoopOnPause();
++  if (isPaused()) {
++    if (terminateOnResume) {
++      v8::debug::SetTerminateOnResume(m_isolate);
++    }
++    m_inspector->client()->quitMessageLoopOnPause();
++  }
+ }
+ 
+ void V8Debugger::breakProgramOnAssert(int targetContextGroupId) {
+diff --git a/src/inspector/v8-debugger.h b/src/inspector/v8-debugger.h
+index a078d14f3d21137e82ea0a3cef7ba0196876b2d8..2df1f5132a9eabf2696457a864a24ae22a568b1d 100644
+--- a/src/inspector/v8-debugger.h
++++ b/src/inspector/v8-debugger.h
+@@ -78,7 +78,8 @@ class V8Debugger : public v8::debug::DebugDelegate,
+   bool canBreakProgram();
+   void breakProgram(int targetContextGroupId);
+   void interruptAndBreak(int targetContextGroupId);
+-  void continueProgram(int targetContextGroupId);
++  void continueProgram(int targetContextGroupId,
++                       bool terminateOnResume = false);
+   void breakProgramOnAssert(int targetContextGroupId);
+ 
+   void setPauseOnNextCall(bool, int targetContextGroupId);
+diff --git a/src/inspector/v8-inspector-session-impl.cc b/src/inspector/v8-inspector-session-impl.cc
+index ccc587ccb739b85880ad160806a5c0d249066143..be7b57b710274c5e24324c773176efa6bf221ebf 100644
+--- a/src/inspector/v8-inspector-session-impl.cc
++++ b/src/inspector/v8-inspector-session-impl.cc
+@@ -445,7 +445,9 @@ void V8InspectorSessionImpl::setSkipAllPauses(bool skip) {
+   m_debuggerAgent->setSkipAllPauses(skip);
+ }
+ 
+-void V8InspectorSessionImpl::resume() { m_debuggerAgent->resume(); }
++void V8InspectorSessionImpl::resume(bool terminateOnResume) {
++  m_debuggerAgent->resume(terminateOnResume);
++}
+ 
+ void V8InspectorSessionImpl::stepOver() { m_debuggerAgent->stepOver(); }
+ 
+diff --git a/src/inspector/v8-inspector-session-impl.h b/src/inspector/v8-inspector-session-impl.h
+index 786dc2a048b512cf9d57bcd7935d50ee36df1d51..9b9b1dd2d7cf2f01162bdadeac46b916abccb6f3 100644
+--- a/src/inspector/v8-inspector-session-impl.h
++++ b/src/inspector/v8-inspector-session-impl.h
+@@ -76,7 +76,7 @@ class V8InspectorSessionImpl : public V8InspectorSession,
+   void breakProgram(const StringView& breakReason,
+                     const StringView& breakDetails) override;
+   void setSkipAllPauses(bool) override;
+-  void resume() override;
++  void resume(bool terminateOnResume = false) override;
+   void stepOver() override;
+   std::vector<std::unique_ptr<protocol::Debugger::API::SearchMatch>>
+   searchInTextByLines(const StringView& text, const StringView& query,
+diff --git a/test/cctest/test-debug.cc b/test/cctest/test-debug.cc
+index 93a47216f3f9a094c941f0e669548ca264c11392..8cab704e872155c56ee59460fce20d40eda8cc41 100644
+--- a/test/cctest/test-debug.cc
++++ b/test/cctest/test-debug.cc
+@@ -35,6 +35,7 @@
+ #include "src/debug/debug.h"
+ #include "src/deoptimizer/deoptimizer.h"
+ #include "src/execution/frames.h"
++#include "src/execution/microtask-queue.h"
+ #include "src/objects/objects-inl.h"
+ #include "src/snapshot/snapshot.h"
+ #include "src/utils/utils.h"
+@@ -2932,9 +2933,11 @@ TEST(DebugBreak) {
+ 
+ class DebugScopingListener : public v8::debug::DebugDelegate {
+  public:
+-  void BreakProgramRequested(
+-      v8::Local<v8::Context>,
+-      const std::vector<v8::debug::BreakpointId>&) override {
++  void ExceptionThrown(v8::Local<v8::Context> paused_context,
++                       v8::Local<v8::Value> exception,
++                       v8::Local<v8::Value> promise, bool is_uncaught,
++                       v8::debug::ExceptionType exception_type) override {
++    break_count_++;
+     auto stack_traces =
+         v8::debug::StackTraceIterator::Create(CcTest::isolate());
+     v8::debug::Location location = stack_traces->GetSourceLocation();
+@@ -2957,6 +2960,10 @@ class DebugScopingListener : public v8::debug::DebugDelegate {
+     scopes->Advance();
+     CHECK(scopes->Done());
+   }
++  unsigned break_count() const { return break_count_; }
++
++ private:
++  unsigned break_count_ = 0;
+ };
+ 
+ TEST(DebugBreakInWrappedScript) {
+@@ -2996,6 +3003,7 @@ TEST(DebugBreakInWrappedScript) {
+ 
+   // Get rid of the debug event listener.
+   v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CHECK_EQ(1, delegate.break_count());
+   CheckDebuggerUnloaded();
+ }
+ 
+@@ -4803,3 +4811,498 @@ TEST(GetPrivateFields) {
+     CHECK(priv_symbol->is_private_name());
+   }
+ }
++
++namespace {
++class SetTerminateOnResumeDelegate : public v8::debug::DebugDelegate {
++ public:
++  enum Options {
++    kNone,
++    kPerformMicrotaskCheckpointAtBreakpoint,
++    kRunJavaScriptAtBreakpoint
++  };
++  explicit SetTerminateOnResumeDelegate(Options options = kNone)
++      : options_(options) {}
++  void BreakProgramRequested(v8::Local<v8::Context> paused_context,
++                             const std::vector<v8::debug::BreakpointId>&
++                                 inspector_break_points_hit) override {
++    break_count_++;
++    v8::Isolate* isolate = paused_context->GetIsolate();
++    v8::debug::SetTerminateOnResume(isolate);
++    if (options_ == kPerformMicrotaskCheckpointAtBreakpoint) {
++      v8::MicrotasksScope::PerformCheckpoint(isolate);
++    }
++    if (options_ == kRunJavaScriptAtBreakpoint) {
++      CompileRun("globalVariable = globalVariable + 1");
++    }
++  }
++
++  void ExceptionThrown(v8::Local<v8::Context> paused_context,
++                       v8::Local<v8::Value> exception,
++                       v8::Local<v8::Value> promise, bool is_uncaught,
++                       v8::debug::ExceptionType exception_type) override {
++    exception_thrown_count_++;
++    v8::debug::SetTerminateOnResume(paused_context->GetIsolate());
++  }
++
++  int break_count() const { return break_count_; }
++  int exception_thrown_count() const { return exception_thrown_count_; }
++
++ private:
++  int break_count_ = 0;
++  int exception_thrown_count_ = 0;
++  Options options_;
++};
++}  // anonymous namespace
++
++TEST(TerminateOnResumeAtBreakpoint) {
++  break_point_hit_count = 0;
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  SetTerminateOnResumeDelegate delegate;
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  v8::Local<v8::Context> context = env.local();
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    // If the delegate doesn't request termination on resume from breakpoint,
++    // foo diverges.
++    v8::Script::Compile(
++        context,
++        v8_str(env->GetIsolate(), "function foo(){debugger; while(true){}}"))
++        .ToLocalChecked()
++        ->Run(context)
++        .ToLocalChecked();
++    v8::Local<v8::Function> foo = v8::Local<v8::Function>::Cast(
++        env->Global()
++            ->Get(context, v8_str(env->GetIsolate(), "foo"))
++            .ToLocalChecked());
++
++    v8::MaybeLocal<v8::Value> val =
++        foo->Call(context, env->Global(), 0, nullptr);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.break_count(), 1);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++namespace {
++bool microtask_one_ran = false;
++static void MicrotaskOne(const v8::FunctionCallbackInfo<v8::Value>& info) {
++  CHECK(v8::MicrotasksScope::IsRunningMicrotasks(info.GetIsolate()));
++  v8::HandleScope scope(info.GetIsolate());
++  v8::MicrotasksScope microtasks(info.GetIsolate(),
++                                 v8::MicrotasksScope::kDoNotRunMicrotasks);
++  ExpectInt32("1 + 1", 2);
++  microtask_one_ran = true;
++}
++}  // namespace
++
++TEST(TerminateOnResumeRunMicrotaskAtBreakpoint) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  SetTerminateOnResumeDelegate delegate(
++      SetTerminateOnResumeDelegate::kPerformMicrotaskCheckpointAtBreakpoint);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  v8::Local<v8::Context> context = env.local();
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    // Enqueue a microtask that gets run while we are paused at the breakpoint.
++    env->GetIsolate()->EnqueueMicrotask(
++        v8::Function::New(env.local(), MicrotaskOne).ToLocalChecked());
++
++    // If the delegate doesn't request termination on resume from breakpoint,
++    // foo diverges.
++    v8::Script::Compile(
++        context,
++        v8_str(env->GetIsolate(), "function foo(){debugger; while(true){}}"))
++        .ToLocalChecked()
++        ->Run(context)
++        .ToLocalChecked();
++    v8::Local<v8::Function> foo = v8::Local<v8::Function>::Cast(
++        env->Global()
++            ->Get(context, v8_str(env->GetIsolate(), "foo"))
++            .ToLocalChecked());
++
++    v8::MaybeLocal<v8::Value> val =
++        foo->Call(context, env->Global(), 0, nullptr);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.break_count(), 1);
++    CHECK(microtask_one_ran);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++TEST(TerminateOnResumeRunJavaScriptAtBreakpoint) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  CompileRun("var globalVariable = 0;");
++  SetTerminateOnResumeDelegate delegate(
++      SetTerminateOnResumeDelegate::kRunJavaScriptAtBreakpoint);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  v8::Local<v8::Context> context = env.local();
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    // If the delegate doesn't request termination on resume from breakpoint,
++    // foo diverges.
++    v8::Script::Compile(
++        context,
++        v8_str(env->GetIsolate(), "function foo(){debugger; while(true){}}"))
++        .ToLocalChecked()
++        ->Run(context)
++        .ToLocalChecked();
++    v8::Local<v8::Function> foo = v8::Local<v8::Function>::Cast(
++        env->Global()
++            ->Get(context, v8_str(env->GetIsolate(), "foo"))
++            .ToLocalChecked());
++
++    v8::MaybeLocal<v8::Value> val =
++        foo->Call(context, env->Global(), 0, nullptr);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.break_count(), 1);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  ExpectInt32("globalVariable", 1);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++TEST(TerminateOnResumeAtException) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  ChangeBreakOnException(true, true);
++  SetTerminateOnResumeDelegate delegate;
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  v8::Local<v8::Context> context = env.local();
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    const char* source = "throw new Error(); while(true){};";
++
++    v8::ScriptCompiler::Source script_source(v8_str(source));
++    v8::Local<v8::Function> foo =
++        v8::ScriptCompiler::CompileFunctionInContext(
++            env.local(), &script_source, 0, nullptr, 0, nullptr)
++            .ToLocalChecked();
++
++    v8::MaybeLocal<v8::Value> val =
++        foo->Call(context, env->Global(), 0, nullptr);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.break_count(), 0);
++    CHECK_EQ(delegate.exception_thrown_count(), 1);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++TEST(TerminateOnResumeAtBreakOnEntry) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  SetTerminateOnResumeDelegate delegate;
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    v8::Local<v8::Function> builtin =
++        CompileRun("String.prototype.repeat").As<v8::Function>();
++    SetBreakPoint(builtin, 0);
++    v8::Local<v8::Value> val = CompileRun("'b'.repeat(10)");
++    CHECK_EQ(delegate.break_count(), 1);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.exception_thrown_count(), 0);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++TEST(TerminateOnResumeAtBreakOnEntryUserDefinedFunction) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  SetTerminateOnResumeDelegate delegate;
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    v8::Local<v8::Function> foo =
++        CompileFunction(&env, "function foo(b) { while (b > 0) {} }", "foo");
++
++    // Run without breakpoints to compile source to bytecode.
++    CompileRun("foo(-1)");
++    CHECK_EQ(delegate.break_count(), 0);
++
++    SetBreakPoint(foo, 0);
++    v8::Local<v8::Value> val = CompileRun("foo(1)");
++    CHECK_EQ(delegate.break_count(), 1);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.exception_thrown_count(), 0);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++TEST(TerminateOnResumeAtUnhandledRejection) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  ChangeBreakOnException(true, true);
++  SetTerminateOnResumeDelegate delegate;
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  v8::Local<v8::Context> context = env.local();
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    v8::Local<v8::Function> foo = CompileFunction(
++        &env, "async function foo() { Promise.reject(); while(true) {} }",
++        "foo");
++
++    v8::MaybeLocal<v8::Value> val =
++        foo->Call(context, env->Global(), 0, nullptr);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.break_count(), 0);
++    CHECK_EQ(delegate.exception_thrown_count(), 1);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++namespace {
++void RejectPromiseThroughCpp(const v8::FunctionCallbackInfo<v8::Value>& info) {
++  auto data = reinterpret_cast<std::pair<v8::Isolate*, LocalContext*>*>(
++      info.Data().As<v8::External>()->Value());
++
++  v8::Local<v8::String> value1 =
++      v8::String::NewFromUtf8(data->first, "foo", v8::NewStringType::kNormal)
++          .ToLocalChecked();
++
++  v8::Local<v8::Promise::Resolver> resolver =
++      v8::Promise::Resolver::New(data->second->local()).ToLocalChecked();
++  v8::Local<v8::Promise> promise = resolver->GetPromise();
++  CHECK_EQ(promise->State(), v8::Promise::PromiseState::kPending);
++
++  resolver->Reject(data->second->local(), value1).ToChecked();
++  CHECK_EQ(promise->State(), v8::Promise::PromiseState::kRejected);
++  // CHECK_EQ(*v8::Utils::OpenHandle(*promise->Result()),
++  //         i::ReadOnlyRoots(CcTest::i_isolate()).exception());
++}
++}  // namespace
++
++TEST(TerminateOnResumeAtUnhandledRejectionCppImpl) {
++  LocalContext env;
++  v8::Isolate* isolate = env->GetIsolate();
++  v8::HandleScope scope(env->GetIsolate());
++  ChangeBreakOnException(true, true);
++  SetTerminateOnResumeDelegate delegate;
++  auto data = std::make_pair(isolate, &env);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  {
++    // We want to trigger a breapoint upon Promise rejection, but we will only
++    // get the callback if there is at least one JavaScript frame in the stack.
++    v8::Local<v8::Function> func =
++        v8::Function::New(env.local(), RejectPromiseThroughCpp,
++                          v8::External::New(isolate, &data))
++            .ToLocalChecked();
++    CHECK(env->Global()
++              ->Set(env.local(), v8_str("RejectPromiseThroughCpp"), func)
++              .FromJust());
++
++    CompileRun("RejectPromiseThroughCpp(); while (true) {}");
++    CHECK_EQ(delegate.break_count(), 0);
++    CHECK_EQ(delegate.exception_thrown_count(), 1);
++  }
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++namespace {
++static void UnreachableMicrotask(
++    const v8::FunctionCallbackInfo<v8::Value>& info) {
++  UNREACHABLE();
++}
++}  // namespace
++
++TEST(TerminateOnResumeFromMicrotask) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  SetTerminateOnResumeDelegate delegate(
++      SetTerminateOnResumeDelegate::kPerformMicrotaskCheckpointAtBreakpoint);
++  ChangeBreakOnException(true, true);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    // Enqueue a microtask that gets run while we are paused at the breakpoint.
++    v8::Local<v8::Function> foo = CompileFunction(
++        &env, "function foo(){ Promise.reject(); while (true) {} }", "foo");
++    env->GetIsolate()->EnqueueMicrotask(foo);
++    env->GetIsolate()->EnqueueMicrotask(
++        v8::Function::New(env.local(), UnreachableMicrotask).ToLocalChecked());
++
++    CHECK_EQ(2,
++             CcTest::i_isolate()->native_context()->microtask_queue()->size());
++
++    v8::MicrotasksScope::PerformCheckpoint(env->GetIsolate());
++
++    CHECK_EQ(0,
++             CcTest::i_isolate()->native_context()->microtask_queue()->size());
++
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.break_count(), 0);
++    CHECK_EQ(delegate.exception_thrown_count(), 1);
++  }
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++class FutexInterruptionThread : public v8::base::Thread {
++ public:
++  FutexInterruptionThread(v8::Isolate* isolate, v8::base::Semaphore* sem)
++      : Thread(Options("FutexInterruptionThread")),
++        isolate_(isolate),
++        sem_(sem) {}
++
++  void Run() override {
++    // Wait a bit before terminating.
++    v8::base::OS::Sleep(v8::base::TimeDelta::FromMilliseconds(100));
++    sem_->Wait();
++    v8::debug::SetTerminateOnResume(isolate_);
++  }
++
++ private:
++  v8::Isolate* isolate_;
++  v8::base::Semaphore* sem_;
++};
++
++namespace {
++class SemaphoreTriggerOnBreak : public v8::debug::DebugDelegate {
++ public:
++  SemaphoreTriggerOnBreak() : sem_(0) {}
++  void BreakProgramRequested(v8::Local<v8::Context> paused_context,
++                             const std::vector<v8::debug::BreakpointId>&
++                                 inspector_break_points_hit) override {
++    break_count_++;
++    sem_.Signal();
++  }
++
++  v8::base::Semaphore* semaphore() { return &sem_; }
++  int break_count() const { return break_count_; }
++
++ private:
++  v8::base::Semaphore sem_;
++  int break_count_ = 0;
++};
++}  // anonymous namespace
++
++TEST(TerminateOnResumeFromOtherThread) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  ChangeBreakOnException(true, true);
++
++  SemaphoreTriggerOnBreak delegate;
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++
++  FutexInterruptionThread timeout_thread(env->GetIsolate(),
++                                         delegate.semaphore());
++  CHECK(timeout_thread.Start());
++
++  v8::Local<v8::Context> context = env.local();
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    const char* source = "debugger; while(true){};";
++
++    v8::ScriptCompiler::Source script_source(v8_str(source));
++    v8::Local<v8::Function> foo =
++        v8::ScriptCompiler::CompileFunctionInContext(
++            env.local(), &script_source, 0, nullptr, 0, nullptr)
++            .ToLocalChecked();
++
++    v8::MaybeLocal<v8::Value> val =
++        foo->Call(context, env->Global(), 0, nullptr);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.break_count(), 1);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}
++
++namespace {
++class InterruptionBreakRightNow : public v8::base::Thread {
++ public:
++  explicit InterruptionBreakRightNow(v8::Isolate* isolate)
++      : Thread(Options("FutexInterruptionThread")), isolate_(isolate) {}
++
++  void Run() override {
++    // Wait a bit before terminating.
++    v8::base::OS::Sleep(v8::base::TimeDelta::FromMilliseconds(100));
++    isolate_->RequestInterrupt(BreakRightNow, nullptr);
++  }
++
++ private:
++  static void BreakRightNow(v8::Isolate* isolate, void* data) {
++    v8::debug::BreakRightNow(isolate);
++  }
++  v8::Isolate* isolate_;
++};
++
++}  // anonymous namespace
++
++TEST(TerminateOnResumeAtInterruptFromOtherThread) {
++  LocalContext env;
++  v8::HandleScope scope(env->GetIsolate());
++  ChangeBreakOnException(true, true);
++
++  SetTerminateOnResumeDelegate delegate;
++  v8::debug::SetDebugDelegate(env->GetIsolate(), &delegate);
++
++  InterruptionBreakRightNow timeout_thread(env->GetIsolate());
++
++  v8::Local<v8::Context> context = env.local();
++  {
++    v8::TryCatch try_catch(env->GetIsolate());
++    const char* source = "while(true){}";
++
++    v8::ScriptCompiler::Source script_source(v8_str(source));
++    v8::Local<v8::Function> foo =
++        v8::ScriptCompiler::CompileFunctionInContext(
++            env.local(), &script_source, 0, nullptr, 0, nullptr)
++            .ToLocalChecked();
++
++    CHECK(timeout_thread.Start());
++    v8::MaybeLocal<v8::Value> val =
++        foo->Call(context, env->Global(), 0, nullptr);
++    CHECK(val.IsEmpty());
++    CHECK(try_catch.HasTerminated());
++    CHECK_EQ(delegate.break_count(), 1);
++  }
++  // Exiting the TryCatch brought the isolate back to a state where JavaScript
++  // can be executed.
++  ExpectInt32("1 + 1", 2);
++  v8::debug::SetDebugDelegate(env->GetIsolate(), nullptr);
++  CheckDebuggerUnloaded();
++}