DebugHandler.cs 9.1 KB

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