Search code examples
c#multithreadingthread-safetyremote-debuggingvisual-studio-debugging

C# suspending all threads


I have a problem that may be fairly unique. I have an application that runs on a headless box for long hours when I am not present, but is not critical. I would like to be able to debug this application remotely using Visual Studio. In order to do so, I have code that looks like this:

// Suspend all other threads to prevent loss
// of state while we investigate the issue.
SuspendAllButCurrentThread();
var remoteDebuggerProcess = new Process
    {
        StartInfo =
            {
                UseShellExecute = true,
                FileName = MsVsMonPath;
            }
    };
// Exception handling and early return removed here for brevity.
remoteDebuggerProcess.Start();

// Wait for a debugger attach.
while (!Debugger.IsAttached)
{
    Thread.Sleep(500);
}
Debugger.Break();

// Once we get here, we've hit continue in the debugger. Restore all of our threads,
// then get rid of the remote debugging tools.
ResumeAllButCurrentThread();

remoteDebuggerProcess.CloseMainWindow();
remoteDebuggerProcess.WaitForExit();

The idea being that this way, I hit an error while I am away, and the application effectively pauses itself and waits for a remote debugger attach, which after the first continue automatically gets the right context thanks to the Debugger.Break call.

Here is the problem: Implementing SuspendAllButCurrentThread turns out to be nontrivial. Thread.Suspend is deprecated, and I can't P/Invoke down to SuspendThread because there's no one-to-one mapping between managed threads and native threads (since I need to keep the current thread alive). I don't want to install Visual Studio on the machine in question if it can possibly be avoided. How can I make this work?


Solution

  • I can't P/Invoke down to SuspendThread because there's no one-to-one mapping between managed threads and native threads

    You can't enumerate managed threads either, only unmanaged threads. There actually is a one-to-one mapping between them, they just made it hard to find it. The original intent was to allow creating a custom CLR host that didn't use operating system threads to implement Thread, a request by the SQL Server group that wanted to use fibers instead. That never worked out, they could not get it reliable enough. No actual CLR host exists that doesn't use real operating system threads.

    So you can actually use Process.GetCurrentProcess().Threads to enumerate all your threads. And avoid suspending your own by pinvoking GetCurrentThreadId(), comparing it to ProcessThread.Id

    How reliable that's going to be is a guess, don't try to do anything drastic like sending an alert to remind you that it is time to attach the debugger. You may have well suspended a thread that was executing code inside Windows and acquired a global lock. As well as a CLR worker thread, like the finalizer thread or the background GC thread.

    The better approach is to use a separate guard process that does all this, just like a debugger will. Use a named EventWaitHandle that you create in the guard program and OpenExisting() in your main program. The guard program needs to WaitAny() on that wait handle as well as the process. Your main program can now simply call Set() to wake up the guard program. Which can now safely suspend all threads.