DebugHandler.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. using Esprima;
  2. using Esprima.Ast;
  3. using Jint.Native;
  4. using Jint.Runtime.Interpreter;
  5. namespace Jint.Runtime.Debugger
  6. {
  7. public enum PauseType
  8. {
  9. Skip,
  10. Step,
  11. Break,
  12. DebuggerStatement
  13. }
  14. public class DebugHandler
  15. {
  16. public delegate void BeforeEvaluateEventHandler(object sender, Program ast);
  17. public delegate StepMode DebugEventHandler(object sender, DebugInformation e);
  18. private readonly Engine _engine;
  19. private bool _paused;
  20. private int _steppingDepth;
  21. /// <summary>
  22. /// Triggered before the engine executes/evaluates the parsed AST of a script or module.
  23. /// </summary>
  24. public event BeforeEvaluateEventHandler? BeforeEvaluate;
  25. /// <summary>
  26. /// The Step event is triggered before the engine executes a step-eligible execution point.
  27. /// </summary>
  28. /// <remarks>
  29. /// If the current step mode is <see cref="StepMode.None"/>, this event is never triggered. The script may
  30. /// still be paused by a debugger statement or breakpoint, but these will trigger the
  31. /// <see cref="Break"/> event.
  32. /// </remarks>
  33. public event DebugEventHandler? Step;
  34. /// <summary>
  35. /// The Break event is triggered when a breakpoint or debugger statement is hit.
  36. /// </summary>
  37. /// <remarks>
  38. /// This is event is not triggered if the current script location was reached by stepping. In that case, only
  39. /// the <see cref="Step"/> event is triggered.
  40. /// </remarks>
  41. public event DebugEventHandler? Break;
  42. /// <summary>
  43. /// The Skip event is triggered for each execution point, when the point doesn't trigger a <see cref="Step"/>
  44. /// or <see cref="Break"/> event.
  45. /// </summary>
  46. public event DebugEventHandler? Skip;
  47. internal DebugHandler(Engine engine, StepMode initialStepMode)
  48. {
  49. _engine = engine;
  50. HandleNewStepMode(initialStepMode);
  51. }
  52. private bool IsStepping => _engine.CallStack.Count <= _steppingDepth;
  53. /// <summary>
  54. /// The location of the current (step-eligible) AST node being executed.
  55. /// </summary>
  56. /// <remarks>
  57. /// The location is available as long as DebugMode is enabled - i.e. even when not stepping
  58. /// or hitting a breakpoint.
  59. /// </remarks>
  60. public Location? CurrentLocation { get; private set; }
  61. /// <summary>
  62. /// Collection of active breakpoints for the engine.
  63. /// </summary>
  64. public BreakPointCollection BreakPoints { get; } = new BreakPointCollection();
  65. /// <summary>
  66. /// Evaluates a script (expression) within the current execution context.
  67. /// </summary>
  68. /// <remarks>
  69. /// Internally, this is used for evaluating breakpoint conditions, but may also be used for e.g. watch lists
  70. /// in a debugger.
  71. /// </remarks>
  72. public JsValue Evaluate(Script script)
  73. {
  74. var context = _engine._activeEvaluationContext;
  75. if (context == null)
  76. {
  77. throw new DebugEvaluationException("Jint has no active evaluation context");
  78. }
  79. var callStackSize = _engine.CallStack.Count;
  80. var list = new JintStatementList(null, script.Body);
  81. Completion result;
  82. try
  83. {
  84. result = list.Execute(context);
  85. }
  86. catch (Exception ex)
  87. {
  88. // An error in the evaluation may return a Throw Completion, or it may throw an exception:
  89. throw new DebugEvaluationException("An error occurred during debugger evaluation", ex);
  90. }
  91. finally
  92. {
  93. // Restore call stack
  94. while (_engine.CallStack.Count > callStackSize)
  95. {
  96. _engine.CallStack.Pop();
  97. }
  98. }
  99. if (result.Type == CompletionType.Throw)
  100. {
  101. // TODO: Should we return an error here? (avoid exception overhead, since e.g. breakpoint
  102. // evaluation may be high volume.
  103. var error = result.GetValueOrDefault();
  104. var ex = new JavaScriptException(error).SetJavaScriptCallstack(_engine, result.Location);
  105. throw new DebugEvaluationException("An error occurred during debugger evaluation", ex);
  106. }
  107. return result.GetValueOrDefault();
  108. }
  109. /// <inheritdoc cref="Evaluate(Script)" />
  110. public JsValue Evaluate(string source, ParserOptions? options = null)
  111. {
  112. options ??= new ParserOptions();
  113. var parser = new JavaScriptParser(options);
  114. try
  115. {
  116. var script = parser.ParseScript(source, "evaluation");
  117. return Evaluate(script);
  118. }
  119. catch (ParserException ex)
  120. {
  121. throw new DebugEvaluationException("An error occurred during debugger expression parsing", ex);
  122. }
  123. }
  124. internal void OnBeforeEvaluate(Program ast)
  125. {
  126. if (ast != null)
  127. {
  128. BeforeEvaluate?.Invoke(_engine, ast);
  129. }
  130. }
  131. internal void OnStep(Node node)
  132. {
  133. // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
  134. if (_paused)
  135. {
  136. return;
  137. }
  138. _paused = true;
  139. CheckBreakPointAndPause(node, node.Location);
  140. }
  141. internal void OnReturnPoint(Node functionBody, JsValue returnValue)
  142. {
  143. // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
  144. if (_paused)
  145. {
  146. return;
  147. }
  148. _paused = true;
  149. var bodyLocation = functionBody.Location;
  150. var functionBodyEnd = bodyLocation.End;
  151. var location = Location.From(functionBodyEnd, functionBodyEnd, bodyLocation.Source);
  152. CheckBreakPointAndPause(node: null, location, returnValue);
  153. }
  154. private void CheckBreakPointAndPause(
  155. Node? node,
  156. Location location,
  157. JsValue? returnValue = null)
  158. {
  159. CurrentLocation = location;
  160. // Even if we matched a breakpoint, if we're stepping, the reason we're pausing is the step.
  161. // Still, we need to include the breakpoint at this location, in case the debugger UI needs to update
  162. // e.g. a hit count.
  163. var breakLocation = new BreakLocation(location.Source, location.Start);
  164. var breakPoint = BreakPoints.FindMatch(this, breakLocation);
  165. PauseType pauseType;
  166. if (IsStepping)
  167. {
  168. pauseType = PauseType.Step;
  169. }
  170. else if (breakPoint != null)
  171. {
  172. pauseType = PauseType.Break;
  173. }
  174. else if (node?.Type == Nodes.DebuggerStatement &&
  175. _engine.Options.Debugger.StatementHandling == DebuggerStatementHandling.Script)
  176. {
  177. pauseType = PauseType.DebuggerStatement;
  178. }
  179. else
  180. {
  181. pauseType = PauseType.Skip;
  182. }
  183. Pause(pauseType, node, location, returnValue, breakPoint);
  184. _paused = false;
  185. }
  186. private void Pause(
  187. PauseType type,
  188. Node? node,
  189. Location location,
  190. JsValue? returnValue = null,
  191. BreakPoint? breakPoint = null)
  192. {
  193. var info = new DebugInformation(
  194. engine: _engine,
  195. currentNode: node,
  196. currentLocation: location,
  197. returnValue: returnValue,
  198. currentMemoryUsage: _engine.CurrentMemoryUsage,
  199. pauseType: type,
  200. breakPoint: breakPoint
  201. );
  202. StepMode? result = type switch
  203. {
  204. // Conventionally, sender should be DebugHandler - but Engine is more useful
  205. PauseType.Skip => Skip?.Invoke(_engine, info),
  206. PauseType.Step => Step?.Invoke(_engine, info),
  207. PauseType.Break => Break?.Invoke(_engine, info),
  208. PauseType.DebuggerStatement => Break?.Invoke(_engine, info),
  209. _ => throw new ArgumentException("Invalid pause type", nameof(type))
  210. };
  211. HandleNewStepMode(result);
  212. }
  213. private void HandleNewStepMode(StepMode? newStepMode)
  214. {
  215. if (newStepMode != null)
  216. {
  217. _steppingDepth = newStepMode switch
  218. {
  219. StepMode.Over => _engine.CallStack.Count,// Resume stepping when back at this level of the stack
  220. StepMode.Out => _engine.CallStack.Count - 1,// Resume stepping when we've popped the stack
  221. StepMode.None => int.MinValue,// Never step
  222. _ => int.MaxValue,// Always step
  223. };
  224. }
  225. }
  226. }
  227. }