Avner Shahar-Kashtan
Avner Shahar-Kashtan

Reputation: 14700

Start new process under the current debugger session

I'm writing a simple test application (a WPF application in this case, if it matters), which attempts to launch a second application from within it (in this case, a second instance of the same app, but that should really matter). If the first program is running inside a debugger (in VS2013, in my case), I want the secondary instance launched to be automatically attached to the first instance's debug session.

Right now, I'm using Process.Start to launch the second process, but if I try calling Debugger.Launch within it, it will show the "choose a debugger" window where the current session is explicitly excluded from the list.

Is there a way that I can, from the first process, explicitly launch a second process in the current debugging session, or (failing that) get a handle to the current debugging session and call code to attach to a process? Or, alternately, a way to get the second process to call a specific debugger session to attach to it?

(I am familiar with various macros or shortcuts within VS to quickly attach to the second process, and I'm using them already. Just wondering if there's a way to have it happen automatically).

Upvotes: 2

Views: 1950

Answers (1)

Erwin Mayer
Erwin Mayer

Reputation: 18670

The Visual Studio team has released a Visual Studio extension that allows automatically attaching child processes to the current debugger: Introducing the Child Process Debugging Power Tool.

It is available on the Gallery for Visual Studio 2013 and above.

I have personally come up with the following code to attach a new process manually to the current debugger:

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Schedulers;
using EnvDTE;
using EnvDTE80;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Test {
    public static class Debugging {
        private static _DTE Dte;
        private static readonly object DteLock = new object();
        private static bool Initialized;

        public static void AttachCurrentDebuggerToProcess(int processId) {
            lock (DteLock) {
                using (var sta = new StaTaskScheduler(numberOfThreads: 1)) {
                    Task.Factory.StartNew(() => {
                        if (System.Threading.Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) throw new NotSupportedException("Thread should be in STA appartment state.");

                        // Register the IOleMessageFilter to handle any threading errors.
                        MessageFilter.Register();
                        if (!Initialized) {
                            using (var currentProcess = System.Diagnostics.Process.GetCurrentProcess())
                            using (var vsInstances = System.Diagnostics.Process.GetProcessesByName("devenv").AsDisposable()) {
                                foreach (var p in vsInstances.Enumerable) {
                                    _DTE dte;
                                    if (TryGetVSInstance(p.Id, out dte)) {
                                        //Will return null if target process doesn't have the same elevated rights as current process.
                                        Utils.Retry(() => {
                                            var debugger = dte?.Debugger;
                                            if (debugger != null) {
                                                foreach (Process2 process in debugger.DebuggedProcesses) {
                                                    if (process.ProcessID == currentProcess.Id) {
                                                        Dte = dte;
                                                        break;
                                                    }
                                                }
                                            }
                                        }, nbRetries: int.MaxValue, msInterval: 1000, retryOnlyOnExceptionTypes: typeof(COMException).InArray());
                                        if (Dte != null) break;
                                    }
                                }
                            }
                            Initialized = true;
                        }
                        if (Dte != null) {
                            foreach (Process2 process in Dte.Debugger.LocalProcesses) {
                                if (process.ProcessID == processId) {
                                    process.Attach2();
                                    Dte.Debugger.CurrentProcess = process;
                                }
                            }
                        }
                        //turn off the IOleMessageFilter.
                        MessageFilter.Revoke();
                    }, CancellationToken.None, TaskCreationOptions.None, sta).Wait();
                }
            }
        }

        private static bool TryGetVSInstance(int processId, out _DTE instance) {
            IntPtr numFetched = IntPtr.Zero;
            IRunningObjectTable runningObjectTable;
            IEnumMoniker monikerEnumerator;
            IMoniker[] monikers = new IMoniker[1];

            GetRunningObjectTable(0, out runningObjectTable);
            runningObjectTable.EnumRunning(out monikerEnumerator);
            monikerEnumerator.Reset();

            while (monikerEnumerator.Next(1, monikers, numFetched) == 0) {
                IBindCtx ctx;
                CreateBindCtx(0, out ctx);

                string runningObjectName;
                monikers[0].GetDisplayName(ctx, null, out runningObjectName);

                object runningObjectVal;
                runningObjectTable.GetObject(monikers[0], out runningObjectVal);

                if (runningObjectVal is _DTE && runningObjectName.StartsWith("!VisualStudio")) {
                    int currentProcessId = int.Parse(runningObjectName.Split(':')[1]);

                    if (currentProcessId == processId) {
                        instance = (_DTE)runningObjectVal;
                        return true;
                    }
                }
            }

            instance = null;
            return false;
        }

        [DllImport("ole32.dll")]
        private static extern void CreateBindCtx(int reserved, out IBindCtx ppbc);

        [DllImport("ole32.dll")]
        private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
    }
}

namespace System.Threading.Tasks.Schedulers {
    /// <summary>Provides a scheduler that uses STA threads. From ParallelExtensionsExtras https://code.msdn.microsoft.com/Samples-for-Parallel-b4b76364/sourcecode?fileId=44488&pathId=574018573</summary> 
    public sealed class StaTaskScheduler : TaskScheduler, IDisposable {
        /// <summary>Stores the queued tasks to be executed by our pool of STA threads.</summary> 
        private BlockingCollection<Task> _tasks;
        /// <summary>The STA threads used by the scheduler.</summary> 
        private readonly List<Thread> _threads;

        /// <summary>Initializes a new instance of the StaTaskScheduler class with the specified concurrency level.</summary> 
        /// <param name="numberOfThreads">The number of threads that should be created and used by this scheduler.</param> 
        public StaTaskScheduler(int numberOfThreads) {
            // Validate arguments 
            if (numberOfThreads < 1) throw new ArgumentOutOfRangeException(nameof(numberOfThreads));

            // Initialize the tasks collection 
            _tasks = new BlockingCollection<Task>();

            // Create the threads to be used by this scheduler 
            _threads = Enumerable.Range(0, numberOfThreads).Select(i =>
            {
                var thread = new Thread(() => {
                    // Continually get the next task and try to execute it. 
                    // This will continue until the scheduler is disposed and no more tasks remain. 
                    foreach (var t in _tasks.GetConsumingEnumerable()) {
                        TryExecuteTask(t);
                    }
                }) { IsBackground = true };
                thread.SetApartmentState(ApartmentState.STA);
                return thread;
            }).ToList();

            // Start all of the threads 
            _threads.ForEach(t => t.Start());
        }

        /// <summary>Queues a Task to be executed by this scheduler.</summary> 
        /// <param name="task">The task to be executed.</param> 
        protected override void QueueTask(Task task) {
            // Push it into the blocking collection of tasks 
            _tasks.Add(task);
        }

        /// <summary>Provides a list of the scheduled tasks for the debugger to consume.</summary> 
        /// <returns>An enumerable of all tasks currently scheduled.</returns> 
        protected override IEnumerable<Task> GetScheduledTasks() {
            // Serialize the contents of the blocking collection of tasks for the debugger 
            return _tasks.ToArray();
        }

        /// <summary>Determines whether a Task may be inlined.</summary> 
        /// <param name="task">The task to be executed.</param> 
        /// <param name="taskWasPreviouslyQueued">Whether the task was previously queued.</param> 
        /// <returns>true if the task was successfully inlined; otherwise, false.</returns> 
        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) {
            // Try to inline if the current thread is STA 
            return
                Thread.CurrentThread.GetApartmentState() == ApartmentState.STA &&
                TryExecuteTask(task);
        }

        /// <summary>Gets the maximum concurrency level supported by this scheduler.</summary> 
        public override int MaximumConcurrencyLevel => this._threads.Count;

        /// <summary> 
        /// Cleans up the scheduler by indicating that no more tasks will be queued. 
        /// This method blocks until all threads successfully shutdown. 
        /// </summary> 
        public void Dispose() {
            if (_tasks != null) {
                // Indicate that no new tasks will be coming in 
                _tasks.CompleteAdding();

                // Wait for all threads to finish processing tasks 
                foreach (var thread in _threads) thread.Join();

                // Cleanup 
                _tasks.Dispose();
                _tasks = null;
            }
        }
    }
}

Usage:

AttachCurrentDebuggerToProcess(1234); //where 1234 is your pid

Upvotes: 3

Related Questions