/* 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.Linq; using System.Threading; using dndbg.COM.CorDebug; namespace dndbg.Engine { [Serializable] class EvalException : Exception { public int HR { get; } public EvalException() : this(-1, null, null) { } public EvalException(int hr) : this(hr, null, null) { } public EvalException(int hr, string msg) : this(hr, msg, null) { } public EvalException(int hr, string? msg, Exception? ex) : base(msg, ex) { HResult = hr; HR = hr; } } struct EvalResult { public bool NormalResult => !WasException && !WasCustomNotification && !WasCancelled; public bool WasException { get; } public bool WasCustomNotification { get; } public bool WasCancelled { get; } public CorValue? ResultOrException { get; } public EvalResult(bool wasException, bool wasCustomNotification, bool wasCancelled, CorValue? resultOrException) { WasException = wasException; WasCustomNotification = wasCustomNotification; WasCancelled = wasCancelled; ResultOrException = resultOrException; } } class EvalEventArgs : EventArgs { } sealed class DnEval : IDisposable { readonly DnDebugger debugger; readonly IDebugMessageDispatcher debugMessageDispatcher; readonly List<(DnModule module, CorClass cls)> customNotificationList; readonly CancellationToken cancellationToken; DnThread thread; CorEval eval; DateTime? startTime; DateTime endTime; TimeSpan initialTimeOut; const int ABORT_TIMEOUT_MS = 3000; const int RUDE_ABORT_TIMEOUT_MS = 1000; public bool EvalTimedOut { get; private set; } public bool SuspendOtherThreads { get; } public event EventHandler? EvalEvent; internal DnEval(DnDebugger debugger, IDebugMessageDispatcher debugMessageDispatcher, bool suspendOtherThreads, List<(DnModule module, CorClass cls)> customNotificationList, CancellationToken cancellationToken) { thread = null!; eval = null!; this.debugger = debugger; this.debugMessageDispatcher = debugMessageDispatcher; this.customNotificationList = customNotificationList; SuspendOtherThreads = suspendOtherThreads; useTotalTimeout = true; initialTimeOut = TimeSpan.FromMilliseconds(1000); this.cancellationToken = cancellationToken; // This is only enabled during func-eval. If it's always enabled, everything gets slower. // It took about 50% longer to start VS. foreach (var info in customNotificationList) info.module.Process.CorProcess.SetEnableCustomNotification(info.cls, enable: true); } public void SetNoTotalTimeout() => useTotalTimeout = false; bool useTotalTimeout; public void SetTimeout(TimeSpan timeout) => initialTimeOut = timeout; public void SetThread(DnThread thread) { if (thread is null) throw new InvalidOperationException(); int hr = thread.CorThread.RawObject.CreateEval(out var ce); if (hr < 0 || ce is null) throw new EvalException(hr, $"Could not create an evaluator, HR=0x{hr:X8}"); this.thread = thread; eval = new CorEval(ce); } public CorValue CreateNull() => eval.CreateValue(CorElementType.Class) ?? throw new InvalidOperationException(); public CorValue? Box(CorValue value, CorType? valueType = null) { var et = valueType ?? value?.ExactType; if (et is null) return null; if (value is null || !value.IsGeneric || value.IsBox || value.IsHeap) return value; var cls = et?.Class; if (cls is null) return null; if (valueType is null) return null; var res = WaitForResult(eval.NewParameterizedObjectNoConstructor(cls, valueType.TypeParameters.ToArray())); if (res is null || !res.Value.NormalResult) { res?.ResultOrException?.DisposeHandle(); return null; } var newObj = res.Value.ResultOrException!; var r = newObj.GetDereferencedValue(out int hr); var vb = r?.GetBoxedValue(out hr); if (vb is null) { newObj.DisposeHandle(); return null; } hr = vb.WriteGenericValue(value.ReadGenericValue(), thread.CorThread.Process); if (hr < 0) return null; return newObj; } public EvalResult? CreateDontCallConstructor(CorType type, out int hr) { if (!type.HasClass) { hr = -1; return null; } return WaitForResult(hr = eval.NewParameterizedObjectNoConstructor(type.Class!, type.TypeParameters.ToArray())); } public EvalResult? CallConstructor(CorFunction func, CorType[] typeArgs, CorValue[] args, out int hr) => WaitForResult(hr = eval.NewParameterizedObject(func, typeArgs, args)); public EvalResult? Call(CorFunction func, CorType[] typeArgs, CorValue[] args, out int hr) => WaitForResult(hr = eval.CallParameterizedFunction(func, typeArgs, args)); public EvalResult? CreateString(string s, out int hr) => WaitForResult(hr = eval.NewString(s)); public EvalResult? CreateSZArray(CorType type, int numElems, out int hr) => WaitForResult(hr = eval.NewParameterizedArray(type, new uint[1] { (uint)numElems })); EvalResult? WaitForResult(int hr) { if (hr < 0) return null; InitializeStartTime(); return SyncWait(); } void InitializeStartTime() { if (startTime is not null) return; startTime = DateTime.UtcNow; endTime = startTime.Value + initialTimeOut; } struct ThreadInfo { public readonly CorThread Thread; public readonly CorDebugThreadState State; public ThreadInfo(CorThread thread) { Thread = thread; State = thread.State; } } struct ThreadInfos { readonly CorThread thread; readonly List list; readonly bool suspendOtherThreads; public ThreadInfos(CorThread thread, bool suspendOtherThreads) { this.thread = thread; list = GetThreadInfos(thread); this.suspendOtherThreads = suspendOtherThreads; } static List GetThreadInfos(CorThread thread) { var process = thread.Process; var list = new List(); if (process is null) { list.Add(new ThreadInfo(thread)); return list; } foreach (var t in process.Threads) list.Add(new ThreadInfo(t)); return list; } public void EnableThread() { foreach (var info in list) { CorDebugThreadState newState; if (info.Thread.Equals(thread)) newState = CorDebugThreadState.THREAD_RUN; else if (suspendOtherThreads) newState = CorDebugThreadState.THREAD_SUSPEND; else continue; if (info.State != newState) info.Thread.State = newState; } } public void RestoreThreads() { foreach (var info in list) info.Thread.State = info.State; } } EvalResult SyncWait() { Debug2.Assert(startTime is not null); var now = DateTime.UtcNow; if (now >= endTime) now = endTime; var timeLeft = endTime - now; if (!useTotalTimeout) timeLeft = initialTimeOut; var infos = new ThreadInfos(thread.CorThread, SuspendOtherThreads); EvalResultKind dispResult; debugger.DebugCallbackEvent += Debugger_DebugCallbackEvent; try { infos.EnableThread(); debugger.EvalStarted(); var res = debugMessageDispatcher.DispatchQueue(timeLeft, out bool timedOut); if (timedOut) { AbortEval(timedOut); throw new TimeoutException(); } Debug2.Assert(res is not null); dispResult = (EvalResultKind)res; if (dispResult == EvalResultKind.CustomNotification) { if (!AbortEval(false)) throw new TimeoutException(); if (debugger.ProcessState != DebuggerProcessState.Paused) debugger.TryBreakProcesses(); } } finally { debugger.DebugCallbackEvent -= Debugger_DebugCallbackEvent; infos.RestoreThreads(); debugger.EvalStopped(); } bool wasException = dispResult == EvalResultKind.Exception; bool wasCustomNotification = dispResult == EvalResultKind.CustomNotification; bool wasCancelled = dispResult == EvalResultKind.Cancelled; return new EvalResult(wasException, wasCustomNotification, wasCancelled, wasCustomNotification ? null : eval.Result); } enum EvalResultKind { Normal, Exception, CustomNotification, Cancelled, } bool AbortEval(bool forceBreakProcesses) { bool timedOut = false; int hr = eval.Abort(); if (hr >= 0) { debugMessageDispatcher.DispatchQueue(TimeSpan.FromMilliseconds(ABORT_TIMEOUT_MS), out timedOut); if (timedOut) { hr = eval.RudeAbort(); if (hr >= 0) debugMessageDispatcher.DispatchQueue(TimeSpan.FromMilliseconds(RUDE_ABORT_TIMEOUT_MS), out _); } } if (timedOut || forceBreakProcesses) { hr = debugger.TryBreakProcesses(); Debug.WriteLineIf(hr != 0, $"Eval timed out and TryBreakProcesses() failed: hr=0x{hr:X8}"); EvalTimedOut = true; } return !timedOut; } void Debugger_DebugCallbackEvent(DnDebugger dbg, DebugCallbackEventArgs e) { switch (e.Kind) { case DebugCallbackKind.EvalComplete: case DebugCallbackKind.EvalException: var ee = (EvalDebugCallbackEventArgs)e; if (ee.Eval == eval.RawObject) { debugger.DebugCallbackEvent -= Debugger_DebugCallbackEvent; e.AddPauseReason(DebuggerPauseReason.Eval); debugMessageDispatcher.CancelDispatchQueue(ee.WasException ? EvalResultKind.Exception : EvalResultKind.Normal); return; } break; case DebugCallbackKind.CustomNotification: if (!SuspendOtherThreads) break; var cne = (CustomNotificationDebugCallbackEventArgs)e; var value = cne.CorThread?.GetCurrentCustomDebuggerNotification(); if (value is not null) { debugMessageDispatcher.CancelDispatchQueue(EvalResultKind.CustomNotification); debugger.DisposeHandle(value); return; } debugger.DisposeHandle(value); break; } if (cancellationToken.IsCancellationRequested) debugMessageDispatcher.CancelDispatchQueue(EvalResultKind.Cancelled); } public void SignalEvalComplete() => EvalEvent?.Invoke(this, new EvalEventArgs()); public void Dispose() { foreach (var info in customNotificationList) info.module.Process.CorProcess.SetEnableCustomNotification(info.cls, enable: false); SignalEvalComplete(); } } }