diff --git a/Extensions/dnSpy.Debugger/dnSpy.Debugger/Attach/AppCommandLineArgsHandler.cs b/Extensions/dnSpy.Debugger/dnSpy.Debugger/Attach/AppCommandLineArgsHandler.cs index b04adddd74..945ebc1a9d 100644 --- a/Extensions/dnSpy.Debugger/dnSpy.Debugger/Attach/AppCommandLineArgsHandler.cs +++ b/Extensions/dnSpy.Debugger/dnSpy.Debugger/Attach/AppCommandLineArgsHandler.cs @@ -20,32 +20,101 @@ You should have received a copy of the GNU General Public License using System; using System.ComponentModel.Composition; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using dnSpy.Contracts.App; +using dnSpy.Contracts.Debugger; using dnSpy.Contracts.Debugger.Attach; namespace dnSpy.Debugger.Attach { [Export(typeof(IAppCommandLineArgsHandler))] sealed class AppCommandLineArgsHandler : IAppCommandLineArgsHandler { readonly Lazy attachableProcessesService; + readonly Lazy dbgManager; [ImportingConstructor] - AppCommandLineArgsHandler(Lazy attachableProcessesService) => + AppCommandLineArgsHandler(Lazy attachableProcessesService, Lazy dbgManager) { this.attachableProcessesService = attachableProcessesService; + this.dbgManager = dbgManager; + } public double Order => 0; + [DllImport("kernel32.dll")] + private static extern bool SetEvent(IntPtr hEvent); + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle(IntPtr hObject); + + private async Task HandleEventOnAttach(AttachableProcess process, IntPtr jitDebugStartEventHandle, bool BreakOnStart) { + var mgr = dbgManager.Value; + var tsc = new TaskCompletionSource(); + Func? SendSignalEvent = jitDebugStartEventHandle != IntPtr.Zero ? async () => { + var hdl = jitDebugStartEventHandle; + jitDebugStartEventHandle = IntPtr.Zero;//we might get called multiple times + if (hdl != IntPtr.Zero) { + await Task.Delay(500); //without the delay we will be 15-40 ticks late on attaching, there is no maximum here other than responsiveness for user. Most of the time without the resume event sent we will not fully attach + SetEvent(hdl); + CloseHandle(hdl); + } + } + : null; + + EventHandler? threadCreatedHandler = null; + + threadCreatedHandler = async (_, e) => { + mgr.MessageThreadCreated -= threadCreatedHandler; + if (BreakOnStart) + e.Pause = true; + tsc.TrySetResult(default); + + }; + EventHandler? runningDebuggingHandler = null; + runningDebuggingHandler = (_, _) => { + if (!mgr.IsDebugging || mgr.IsRunning != true) + return; + SendSignalEvent?.Invoke(); + mgr.IsRunningChanged -= runningDebuggingHandler; + mgr.IsDebuggingChanged -= runningDebuggingHandler; + }; + + if (jitDebugStartEventHandle != IntPtr.Zero) { + mgr.IsRunningChanged += runningDebuggingHandler; + mgr.IsDebuggingChanged += runningDebuggingHandler; + } + + mgr.MessageThreadCreated += threadCreatedHandler; // even if we are not BreakOnStart we will use this to complete the wait event + + process.Attach(); + + /* + We want to do cleanup here for a few just in cases: + - If we don't attach we want to remove the event listeners so we don't randomly pause a future session. + - if the debug manager status events don't go off as expected and we haven't already sent the event handle signal the process is suspended until the signal comes (or we exit) so it acts as a backup to firing that event. + */ + if (await Task.WhenAny(Task.Delay(TimeSpan.FromSeconds(10)), tsc.Task) != tsc.Task) { + SendSignalEvent?.Invoke(); + mgr.MessageThreadCreated -= threadCreatedHandler; + mgr.IsRunningChanged -= runningDebuggingHandler; + mgr.IsDebuggingChanged -= runningDebuggingHandler; + } + } public async void OnNewArgs(IAppCommandLineArgs args) { + AttachableProcess? process = null; if (args.DebugAttachPid is int pid && pid != 0) { var processes = await attachableProcessesService.Value.GetAttachableProcessesAsync(null, new[] { pid }, null, CancellationToken.None).ConfigureAwait(false); - var process = processes.FirstOrDefault(p => p.ProcessId == pid); - process?.Attach(); + process = processes.FirstOrDefault(p => p.ProcessId == pid); } else if (args.DebugAttachProcess is string processName && !string.IsNullOrEmpty(processName)) { var processes = await attachableProcessesService.Value.GetAttachableProcessesAsync(processName, CancellationToken.None).ConfigureAwait(false); - var process = processes.FirstOrDefault(); - process?.Attach(); + process = processes.FirstOrDefault(); } + if ((args.DebugBreakOnAttach || args.DebugEvent != 0) && process is not null) + await HandleEventOnAttach(process, new IntPtr(args.DebugEvent), args.DebugBreakOnAttach); + else + process?.Attach(); } + } } diff --git a/dnSpy/dnSpy.Contracts.DnSpy/App/IAppCommandLineArgs.cs b/dnSpy/dnSpy.Contracts.DnSpy/App/IAppCommandLineArgs.cs index f4e044d430..e90f2230ca 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/App/IAppCommandLineArgs.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/App/IAppCommandLineArgs.cs @@ -81,6 +81,9 @@ public interface IAppCommandLineArgs { /// Attach to this process, unless it's 0 int DebugAttachPid { get; } + /// If attaching on startup break right away + bool DebugBreakOnAttach { get; } + /// Event handle duplicated into the postmortem debugger process uint DebugEvent { get; } diff --git a/dnSpy/dnSpy/MainApp/AppCommandLineArgs.cs b/dnSpy/dnSpy/MainApp/AppCommandLineArgs.cs index 7559cf2828..fd4658ecd0 100644 --- a/dnSpy/dnSpy/MainApp/AppCommandLineArgs.cs +++ b/dnSpy/dnSpy/MainApp/AppCommandLineArgs.cs @@ -46,6 +46,7 @@ sealed class AppCommandLineArgs : IAppCommandLineArgs { public string HideToolWindow { get; } public bool ShowStartupTime { get; } public int DebugAttachPid { get; } + public bool DebugBreakOnAttach { get; } public uint DebugEvent { get; } public ulong JitDebugInfo { get; } public string DebugAttachProcess { get; } @@ -76,6 +77,7 @@ public AppCommandLineArgs(string[] args) { HideToolWindow = string.Empty; ShowStartupTime = false; DebugAttachPid = 0; + DebugBreakOnAttach = false; DebugAttachProcess = string.Empty; ExtraExtensionDirectory = string.Empty; @@ -178,6 +180,10 @@ public AppCommandLineArgs(string[] args) { i++; break; + case "--break": + DebugBreakOnAttach = true; + break; + case "-e": if (TryParseUInt32(next, out uint debugEvent)) DebugEvent = debugEvent;