/* Copyright (C) 2014-2019 de4dot@gmail.com This file is part of dnSpy dnSpy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. dnSpy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with dnSpy. If not, see . */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.ExceptionServices; using System.Threading; using Mono.Debugger.Soft; namespace dnSpy.Debugger.DotNet.Mono.Impl { sealed class FuncEvalFactory { internal sealed class FuncEvalState { internal volatile int isEvaluatingCounter; internal volatile int methodInvokeCounter; } public bool IsEvaluating => funcEvalState.isEvaluatingCounter > 0; public int MethodInvokeCounter => funcEvalState.methodInvokeCounter; readonly FuncEvalState funcEvalState; readonly IDebugMessageDispatcher debugMessageDispatcher; public FuncEvalFactory(IDebugMessageDispatcher debugMessageDispatcher) { funcEvalState = new FuncEvalState(); this.debugMessageDispatcher = debugMessageDispatcher; } public FuncEval CreateFuncEval(Action onEvalComplete, ThreadMirror thread, TimeSpan funcEvalTimeout, bool suspendOtherThreads, CancellationToken cancellationToken) => new FuncEvalImpl(debugMessageDispatcher, funcEvalState, onEvalComplete, thread, funcEvalTimeout, suspendOtherThreads, cancellationToken); } [Flags] enum FuncEvalOptions { None = 0, ReturnOutThis = 0x00000001, ReturnOutArgs = 0x00000002, Virtual = 0x00000004, } abstract class FuncEval : IDisposable { public abstract bool EvalTimedOut { get; } public abstract InvokeResult CreateInstance(MethodMirror method, IList arguments, FuncEvalOptions options); public abstract InvokeResult CallMethod(MethodMirror method, Value? obj, IList arguments, FuncEvalOptions options); public abstract void Dispose(); } sealed class FuncEvalImpl : FuncEval { public override bool EvalTimedOut => evalTimedOut; bool evalTimedOut; readonly IDebugMessageDispatcher debugMessageDispatcher; readonly FuncEvalFactory.FuncEvalState funcEvalState; readonly Action onEvalComplete; readonly ThreadMirror thread; readonly bool suspendOtherThreads; readonly CancellationToken cancellationToken; readonly DateTime endTime; public FuncEvalImpl(IDebugMessageDispatcher debugMessageDispatcher, FuncEvalFactory.FuncEvalState funcEvalState, Action onEvalComplete, ThreadMirror thread, TimeSpan funcEvalTimeout, bool suspendOtherThreads, CancellationToken cancellationToken) { this.debugMessageDispatcher = debugMessageDispatcher ?? throw new ArgumentNullException(nameof(debugMessageDispatcher)); this.funcEvalState = funcEvalState ?? throw new ArgumentNullException(nameof(funcEvalState)); this.onEvalComplete = onEvalComplete ?? throw new ArgumentNullException(nameof(onEvalComplete)); this.thread = thread ?? throw new ArgumentNullException(nameof(thread)); endTime = DateTime.UtcNow + funcEvalTimeout; this.suspendOtherThreads = suspendOtherThreads; this.cancellationToken = cancellationToken; } InvokeOptions GetInvokeOptions(FuncEvalOptions funcEvalOptions) { var options = InvokeOptions.DisableBreakpoints; if (suspendOtherThreads) options |= InvokeOptions.SingleThreaded; if ((funcEvalOptions & FuncEvalOptions.ReturnOutThis) != 0) options |= InvokeOptions.ReturnOutThis; if ((funcEvalOptions & FuncEvalOptions.ReturnOutArgs) != 0) options |= InvokeOptions.ReturnOutArgs; if ((funcEvalOptions & FuncEvalOptions.Virtual) != 0) options |= InvokeOptions.Virtual; return options; } public override InvokeResult CreateInstance(MethodMirror method, IList arguments, FuncEvalOptions options) => CallCore(method, null, arguments, options, isNewobj: true); public override InvokeResult CallMethod(MethodMirror method, Value? obj, IList arguments, FuncEvalOptions options) => CallCore(method, obj, arguments, options, isNewobj: false); InvokeResult CallCore(MethodMirror method, Value? obj, IList arguments, FuncEvalOptions options, bool isNewobj) { if (evalTimedOut) throw new TimeoutException(); IInvokeAsyncResult? asyncRes = null; bool done = false; try { funcEvalState.isEvaluatingCounter++; var currTime = DateTime.UtcNow; var timeLeft = endTime - currTime; if (timeLeft >= TimeSpan.Zero) { funcEvalState.methodInvokeCounter++; Debug2.Assert(!isNewobj || obj is null); bool isInvokeInstanceMethod = obj is not null && !isNewobj; AsyncCallback asyncCallback = asyncRes2 => { if (done) return; InvokeResult resTmp; try { if (isInvokeInstanceMethod) resTmp = obj!.EndInvokeMethodWithResult(asyncRes2); else resTmp = method.DeclaringType.EndInvokeMethodWithResult(asyncRes2); debugMessageDispatcher.CancelDispatchQueue(resTmp); } catch (Exception ex) { debugMessageDispatcher.CancelDispatchQueue(ExceptionDispatchInfo.Capture(ex)); } }; if (isInvokeInstanceMethod) asyncRes = obj!.BeginInvokeMethod(thread, method, arguments, GetInvokeOptions(options), asyncCallback, null); else asyncRes = method.DeclaringType.BeginInvokeMethod(thread, method, arguments, GetInvokeOptions(options), asyncCallback, null); var res = debugMessageDispatcher.DispatchQueue(timeLeft, out bool timedOut); if (timedOut) { evalTimedOut = true; try { asyncRes.Abort(); } catch (CommandException ce) when (ce.ErrorCode == ErrorCode.ERR_NO_INVOCATION) { } throw new TimeoutException(); } if (res is ExceptionDispatchInfo exInfo) exInfo.Throw(); Debug.Assert(res is InvokeResult); return res as InvokeResult ?? throw new InvalidOperationException(); } else { evalTimedOut = true; throw new TimeoutException(); } } finally { done = true; funcEvalState.isEvaluatingCounter--; asyncRes?.Dispose(); } } public override void Dispose() => onEvalComplete(this); } }