using Esprima; using Esprima.Ast; using Jint.Native; using Jint.Runtime.Interpreter; namespace Jint.Runtime.Debugger { public enum PauseType { Step, Break, DebuggerStatement } public class DebugHandler { public delegate StepMode DebugEventHandler(object sender, DebugInformation e); private readonly Engine _engine; private bool _paused; private int _steppingDepth; /// /// The Step event is triggered before the engine executes a step-eligible AST node. /// /// /// If the current step mode is , this event is never triggered. The script may /// still be paused by a debugger statement or breakpoint, but these will trigger the /// event. /// public event DebugEventHandler? Step; /// /// The Break event is triggered when a breakpoint or debugger statement is hit. /// /// /// This is event is not triggered if the current script location was reached by stepping. In that case, only /// the event is triggered. /// public event DebugEventHandler? Break; internal DebugHandler(Engine engine, StepMode initialStepMode) { _engine = engine; HandleNewStepMode(initialStepMode); } /// /// The location of the current (step-eligible) AST node being executed. /// /// /// The location is available as long as DebugMode is enabled - i.e. even when not stepping /// or hitting a breakpoint. /// public Location? CurrentLocation { get; private set; } /// /// Collection of active breakpoints for the engine. /// public BreakPointCollection BreakPoints { get; } = new BreakPointCollection(); /// /// Evaluates a script (expression) within the current execution context. /// /// /// Internally, this is used for evaluating breakpoint conditions, but may also be used for e.g. watch lists /// in a debugger. /// public JsValue Evaluate(Script script) { var context = _engine._activeEvaluationContext; if (context == null) { throw new DebugEvaluationException("Jint has no active evaluation context"); } int callStackSize = _engine.CallStack.Count; var list = new JintStatementList(null, script.Body); Completion result; try { result = list.Execute(context); } catch (Exception ex) { // An error in the evaluation may return a Throw Completion, or it may throw an exception: throw new DebugEvaluationException("An error occurred during debugger evaluation", ex); } finally { // Restore call stack while (_engine.CallStack.Count > callStackSize) { _engine.CallStack.Pop(); } } if (result.Type == CompletionType.Throw) { // TODO: Should we return an error here? (avoid exception overhead, since e.g. breakpoint // evaluation may be high volume. var error = result.GetValueOrDefault(); var ex = new JavaScriptException(error).SetJavaScriptCallstack(_engine, result.Location); throw new DebugEvaluationException("An error occurred during debugger evaluation", ex); } return result.GetValueOrDefault(); } /// public JsValue Evaluate(string source, ParserOptions? options = null) { options ??= new ParserOptions("evaluation"); var parser = new JavaScriptParser(source, options); try { var script = parser.ParseScript(); return Evaluate(script); } catch (ParserException ex) { throw new DebugEvaluationException("An error occurred during debugger expression parsing", ex); } } internal void OnStep(Node node) { // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler) if (_paused) { return; } _paused = true; CheckBreakPointAndPause( new BreakLocation(node.Location.Source!, node.Location.Start), node: node, location: null, returnValue: null); } internal void OnReturnPoint(Node functionBody, JsValue returnValue) { // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler) if (_paused) { return; } _paused = true; var bodyLocation = functionBody.Location; var functionBodyEnd = bodyLocation.End; var location = Location.From(functionBodyEnd, functionBodyEnd, bodyLocation.Source); CheckBreakPointAndPause( new BreakLocation(bodyLocation.Source!, bodyLocation.End), node: null, location: location, returnValue: returnValue); } internal void OnDebuggerStatement(Statement statement) { // Don't reenter if we're already paused if (_paused) { return; } _paused = true; bool isStepping = _engine.CallStack.Count <= _steppingDepth; // Even though we're at a debugger statement, if we're stepping, ignore the statement. OnStep already // takes care of pausing. if (!isStepping) { Pause(PauseType.DebuggerStatement, statement); } _paused = false; } private void CheckBreakPointAndPause( BreakLocation breakLocation, Node? node, Location? location = null, JsValue? returnValue = null) { CurrentLocation = location ?? node?.Location; var breakpoint = BreakPoints.FindMatch(this, breakLocation); bool isStepping = _engine.CallStack.Count <= _steppingDepth; if (breakpoint != null || isStepping) { // Even if we matched a breakpoint, if we're stepping, the reason we're pausing is the step. // Still, we need to include the breakpoint at this location, in case the debugger UI needs to update // e.g. a hit count. Pause(isStepping ? PauseType.Step : PauseType.Break, node!, location, returnValue, breakpoint); } _paused = false; } private void Pause( PauseType type, Node node, Location? location = null, JsValue? returnValue = null, BreakPoint? breakPoint = null) { var info = new DebugInformation( engine: _engine, currentNode: node, currentLocation: location ?? node.Location, returnValue: returnValue, currentMemoryUsage: _engine.CurrentMemoryUsage, pauseType: type, breakPoint: breakPoint ); StepMode? result = type switch { // Conventionally, sender should be DebugHandler - but Engine is more useful PauseType.Step => Step?.Invoke(_engine, info), PauseType.Break => Break?.Invoke(_engine, info), PauseType.DebuggerStatement => Break?.Invoke(_engine, info), _ => throw new ArgumentException("Invalid pause type", nameof(type)) }; HandleNewStepMode(result); } private void HandleNewStepMode(StepMode? newStepMode) { if (newStepMode != null) { _steppingDepth = newStepMode switch { StepMode.Over => _engine.CallStack.Count,// Resume stepping when back at this level of the stack StepMode.Out => _engine.CallStack.Count - 1,// Resume stepping when we've popped the stack StepMode.None => int.MinValue,// Never step _ => int.MaxValue,// Always step }; } } } }