using Jint.Native; using Jint.Runtime.Interpreter; namespace Jint.Runtime.Debugger { public enum PauseType { Skip, Step, Break, DebuggerStatement } public class DebugHandler { public delegate void BeforeEvaluateEventHandler(object sender, Program ast); public delegate StepMode DebugEventHandler(object sender, DebugInformation e); private readonly Engine _engine; private bool _paused; private int _steppingDepth; /// /// Triggered before the engine executes/evaluates the parsed AST of a script or module. /// public event BeforeEvaluateEventHandler? BeforeEvaluate; /// /// The Step event is triggered before the engine executes a step-eligible execution point. /// /// /// 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; /// /// The Skip event is triggered for each execution point, when the point doesn't trigger a /// or event. /// public event DebugEventHandler? Skip; internal DebugHandler(Engine engine, StepMode initialStepMode) { _engine = engine; HandleNewStepMode(initialStepMode); } private bool IsStepping => _engine.CallStack.Count <= _steppingDepth; /// /// 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 SourceLocation? 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"); } var 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(); var parser = new Parser(options); try { var script = parser.ParseScript(source, "evaluation"); return Evaluate(script); } catch (ParseErrorException ex) { throw new DebugEvaluationException("An error occurred during debugger expression parsing", ex); } } internal void OnBeforeEvaluate(Program ast) { if (ast != null) { BeforeEvaluate?.Invoke(_engine, ast); } } 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(node, node.Location); } 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 = SourceLocation.From(functionBodyEnd, functionBodyEnd, bodyLocation.Source); CheckBreakPointAndPause(node: null, location, returnValue); } private void CheckBreakPointAndPause( Node? node, in SourceLocation location, JsValue? returnValue = null) { CurrentLocation = location; // 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. var breakLocation = new BreakLocation(location.Source, location.Start); var breakPoint = BreakPoints.FindMatch(this, breakLocation); PauseType pauseType; if (IsStepping) { pauseType = PauseType.Step; } else if (breakPoint != null) { pauseType = PauseType.Break; } else if (node?.Type == NodeType.DebuggerStatement && _engine.Options.Debugger.StatementHandling == DebuggerStatementHandling.Script) { pauseType = PauseType.DebuggerStatement; } else { pauseType = PauseType.Skip; } Pause(pauseType, node, location, returnValue, breakPoint); _paused = false; } private void Pause( PauseType type, Node? node, in SourceLocation location, JsValue? returnValue = null, BreakPoint? breakPoint = null) { var info = new DebugInformation( engine: _engine, currentNode: node, currentLocation: 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.Skip => Skip?.Invoke(_engine, info), 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 }; } } } }