6

I've been troubleshooting all day. After doing some research and a lot of trial and error, it seems I've been able to narrow down the issue to the fact that my call to process.Start() doesn't work on a timer thread. The code below works when running on the main thread. Put that exact same code in a timer callback, and it hangs. Why? How do I get it to work with a timer?

private static void RunProcess()
{
    var process = new Process();

    process.StartInfo.FileName = "cmd";
    process.StartInfo.Arguments = "/c exit";
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.RedirectStandardInput = true;
    process.StartInfo.RedirectStandardOutput = true;

    process.Start();  // code hangs here, when running on background thread

    process.StandardOutput.ReadToEnd();

    process.WaitForExit();
}

EDIT

As a test, I used this exact same code on another laptop, and I experienced the same problem. This is complete code that can be pasted into a console app. process.Start() hangs, but as soon as I hit any key to end, process.Start() completes before the program ends.

private static System.Timers.Timer _timer;
private static readonly object _locker = new object();

static void Main(string[] args)
{
    ProcessTest();

    Console.WriteLine("Press any key to end.");
    Console.ReadKey();
}
private static void ProcessTest()
{
    Initialize();
}
private static void Initialize()
{
    int timerInterval = 2000;
    _timer = new System.Timers.Timer(timerInterval);
    _timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
    _timer.Start();
}
private static void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
    if (!Monitor.TryEnter(_locker)) { return; }  // Don't let  multiple threads in here at the same time.
    try
    {
        RunProcess();
    }
    finally
    {
        Monitor.Exit(_locker);
    }
}
private static void RunProcess()
{
    var process = new Process();
    process.StartInfo.FileName = "cmd";
    process.StartInfo.Arguments = "/c exit";
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.RedirectStandardInput = true;
    process.StartInfo.RedirectStandardOutput = true;
    process.Start();  // ** HANGS HERE **
    process.StandardOutput.ReadToEnd();
    process.WaitForExit();
}
Bob Horn
  • 33,387
  • 34
  • 113
  • 219
  • Works fine, as expected and as it should. You'll need try this is another machine or a VM without any crudware loaded. – Hans Passant Apr 25 '13 at 02:13
  • In what way can crudware cause this? Any idea how to identify the culprit? – Bob Horn Apr 25 '13 at 02:24
  • @HansPassant I've duplicated this issue on another laptop. I've posted complete code that can be run as a test. That would lead me to believe that the issue isn't crudware. (Unless I have the same crudware on both systems.) – Bob Horn Apr 25 '13 at 11:33
  • 1
    Your problem is getting to be a bit infamous, it is specific to .NET 4.5. Which modified the Console class, the ReadKey() method acquires a lock to prevent other threads from messing with the console. You can readily see this in the worker thread's call stack, it deadlocks on the Console.InputEncoding property getter which also tries to acquire the same lock. Lots of questions about this deadlock already, I'll try to dig one up. The workaround is to just not call ReadKey(). Something else to block the thread, like a WaitOne() call that says the worker is done. – Hans Passant Apr 25 '13 at 13:07
  • That would explain why this would be working when running as a service, but not in my console app. One wrinkle: my console app targets .NET 4.0. – Bob Horn Apr 25 '13 at 13:35
  • The .NET 4.5 install replaces .NET 4.0 assemblies, it is not a side-by-side version. – Hans Passant Apr 25 '13 at 14:45
  • @HansPassant Did you want to post your comment as an answer? Seems like the appropriate thing to do. – Bob Horn Apr 25 '13 at 14:46

1 Answers1

10

There are a lot of duplicate questions about this problem, none that exactly fits your case. You can see the problem by using the debugger's Debug + Windows + Threads window. Locate the timer thread and double-click it. Look at the Call Stack window to see:

mscorlib.dll!System.Console.InputEncoding.get() + 0x66 bytes    
System.dll!System.Diagnostics.Process.StartWithCreateProcess(System.Diagnostics.ProcessStartInfo startInfo) + 0x7f5 bytes   
System.dll!System.Diagnostics.Process.Start() + 0x88 bytes  
ConsoleApplication70.exe!Program.RunProcess() Line 43 + 0xa bytes   C#
ConsoleApplication70.exe!Program.OnTimerElapsed(object sender, System.Timers.ElapsedEventArgs e) Line 28 + 0x5 bytes    C#
    // etc...

The thread is deadlocked on the Console.InputEncoding property getter. Which is used by the Process class to figure out what encoding needs to be used to translate the redirected output of the process into strings.

This is specific to .NET 4.5, it will also affect apps that target 4.0 on a machine that has 4.5 installed since it is not a side-by-side version of .NET. The deadlock is caused by the Console.ReadKey() method call in your main thread. Which now acquires a lock that prevents other threads from messing with the console. This has been a fairly global change across Microsoft software, the CRT that is used in C/C++ apps created by VS2012 also added this lock. The exact reason isn't that clear to me, but surely has to do something with console output not getting intermingled with console input while your program is asking for input. Exactly why the InputEncoding property needs to take that lock as well is, well, a bit hard to explain but fits the pattern of serializing access to console input. This of course comes as a big surprise to many programmers, especially the ones that write little test apps that test threaded code, like you did. Bit of a setback to TDD.

The workaround is a bit unpleasant, TDD wise, you do have to stop using Console.ReadKey() to avoid the deadlock. Real programs would use the WaitOne() method of an AutoResetEvent to know that the worker thread finished executing. Or CountDownEvent.Wait(), more in keeping with trying out code a couple of times. Etcetera.


UPDATE: this deadlock scenario was resolved in a service update for .NET 4.5. Enable Windows Update on your machine to get it.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Not suitable for all applications but if you just wait on the main thread for `Console.ReadKey()` to exit, you can swap it for `Console.In.Peek()`. "Works on my machine" – Jonno May 21 '13 at 13:16