2
0

DebugHandler.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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(in Prepared<Script> preparedScript)
  71. {
  72. if (!preparedScript.IsValid)
  73. {
  74. ExceptionHelper.ThrowInvalidPreparedScriptArgumentException(nameof(preparedScript));
  75. }
  76. var context = _engine._activeEvaluationContext;
  77. if (context == null)
  78. {
  79. throw new DebugEvaluationException("Jint has no active evaluation context");
  80. }
  81. var callStackSize = _engine.CallStack.Count;
  82. var list = new JintStatementList(null, preparedScript.Program.Body);
  83. Completion result;
  84. try
  85. {
  86. result = list.Execute(context);
  87. }
  88. catch (Exception ex)
  89. {
  90. // An error in the evaluation may return a Throw Completion, or it may throw an exception:
  91. throw new DebugEvaluationException("An error occurred during debugger evaluation", ex);
  92. }
  93. finally
  94. {
  95. // Restore call stack
  96. while (_engine.CallStack.Count > callStackSize)
  97. {
  98. _engine.CallStack.Pop();
  99. }
  100. }
  101. if (result.Type == CompletionType.Throw)
  102. {
  103. // TODO: Should we return an error here? (avoid exception overhead, since e.g. breakpoint
  104. // evaluation may be high volume.
  105. var error = result.GetValueOrDefault();
  106. var ex = new JavaScriptException(error).SetJavaScriptCallstack(_engine, result.Location);
  107. throw new DebugEvaluationException("An error occurred during debugger evaluation", ex);
  108. }
  109. return result.GetValueOrDefault();
  110. }
  111. /// <inheritdoc cref="Evaluate(in Prepared{Script})" />
  112. public JsValue Evaluate(string sourceText, ScriptParsingOptions? parsingOptions = null)
  113. {
  114. var parserOptions = parsingOptions?.GetParserOptions() ?? _engine.GetActiveParserOptions();
  115. var parser = _engine.GetParserFor(parserOptions);
  116. try
  117. {
  118. var script = parser.ParseScript(sourceText, "evaluation");
  119. return Evaluate(new Prepared<Script>(script, parserOptions));
  120. }
  121. catch (ParseErrorException ex)
  122. {
  123. throw new DebugEvaluationException("An error occurred during debugger expression parsing", ex);
  124. }
  125. }
  126. internal void OnBeforeEvaluate(Program ast)
  127. {
  128. if (ast != null)
  129. {
  130. BeforeEvaluate?.Invoke(_engine, ast);
  131. }
  132. }
  133. internal void OnStep(Node node)
  134. {
  135. // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
  136. if (_paused)
  137. {
  138. return;
  139. }
  140. _paused = true;
  141. CheckBreakPointAndPause(node, node.Location);
  142. }
  143. internal void OnReturnPoint(Node functionBody, JsValue returnValue)
  144. {
  145. // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
  146. if (_paused)
  147. {
  148. return;
  149. }
  150. _paused = true;
  151. var bodyLocation = functionBody.Location;
  152. var functionBodyEnd = bodyLocation.End;
  153. var location = SourceLocation.From(functionBodyEnd, functionBodyEnd, bodyLocation.SourceFile);
  154. CheckBreakPointAndPause(node: null, location, returnValue);
  155. }
  156. private void CheckBreakPointAndPause(
  157. Node? node,
  158. in SourceLocation location,
  159. JsValue? returnValue = null)
  160. {
  161. CurrentLocation = location;
  162. // Even if we matched a breakpoint, if we're stepping, the reason we're pausing is the step.
  163. // Still, we need to include the breakpoint at this location, in case the debugger UI needs to update
  164. // e.g. a hit count.
  165. var breakLocation = new BreakLocation(location.SourceFile, location.Start);
  166. var breakPoint = BreakPoints.FindMatch(this, breakLocation);
  167. PauseType pauseType;
  168. if (IsStepping)
  169. {
  170. pauseType = PauseType.Step;
  171. }
  172. else if (breakPoint != null)
  173. {
  174. pauseType = PauseType.Break;
  175. }
  176. else if (node?.Type == NodeType.DebuggerStatement &&
  177. _engine.Options.Debugger.StatementHandling == DebuggerStatementHandling.Script)
  178. {
  179. pauseType = PauseType.DebuggerStatement;
  180. }
  181. else
  182. {
  183. pauseType = PauseType.Skip;
  184. }
  185. Pause(pauseType, node, location, returnValue, breakPoint);
  186. _paused = false;
  187. }
  188. private void Pause(
  189. PauseType type,
  190. Node? node,
  191. in SourceLocation location,
  192. JsValue? returnValue = null,
  193. BreakPoint? breakPoint = null)
  194. {
  195. var info = new DebugInformation(
  196. engine: _engine,
  197. currentNode: node,
  198. currentLocation: location,
  199. returnValue: returnValue,
  200. currentMemoryUsage: _engine.CurrentMemoryUsage,
  201. pauseType: type,
  202. breakPoint: breakPoint
  203. );
  204. StepMode? result = type switch
  205. {
  206. // Conventionally, sender should be DebugHandler - but Engine is more useful
  207. PauseType.Skip => Skip?.Invoke(_engine, info),
  208. PauseType.Step => Step?.Invoke(_engine, info),
  209. PauseType.Break => Break?.Invoke(_engine, info),
  210. PauseType.DebuggerStatement => Break?.Invoke(_engine, info),
  211. _ => throw new ArgumentException("Invalid pause type", nameof(type))
  212. };
  213. HandleNewStepMode(result);
  214. }
  215. private void HandleNewStepMode(StepMode? newStepMode)
  216. {
  217. if (newStepMode != null)
  218. {
  219. _steppingDepth = newStepMode switch
  220. {
  221. StepMode.Over => _engine.CallStack.Count,// Resume stepping when back at this level of the stack
  222. StepMode.Out => _engine.CallStack.Count - 1,// Resume stepping when we've popped the stack
  223. StepMode.None => int.MinValue,// Never step
  224. _ => int.MaxValue,// Always step
  225. };
  226. }
  227. }
  228. }
  229. }