Udostępnij za pośrednictwem


Async methods failures can be hard to diagnose

Using asynchronous programming can make your application be more responsive and seem faster. However, beware of possible failures that are hard to diagnose. I’ve just been analyzing some of these crash reports that are sent to Microsoft from the Problem Reporting Settings (below).

A big part of writing programs is calling other programs, or APIs, to do various things. Some of these may throw exceptions if passed invalid parameters. These parameters might come from user input or even the user OS version (the valid characters in a file system can vary). An API might be called to operate on something that no longer exists (System.Diagnostics.Process.GetProcessById) , perhaps due to a race condition. An application might be running in an operating system locale where the Date/Time formats or numeric separators might be different, causing exceptions.

When these exceptions occur in an asynchronous method, the call stacks of the crashes can be hard to decipher.

Many of these failures come from code that doesn’t come from Visual Studio, but from a VS extension running in the VS process, perhaps from a 3rd party.

The sample below produces the same call stack as these crashes.

Run the application below:

1. Start Visual Studio

2. File->New->Project->C# WPF Application

3. Paste the code below into MainWindow.xaml.cs

4. Hit Ctrl F5 to run the program without debugging it.

5. Click the Doit Button.

The button click code queues an asynchronous work item that fetches some web content. After the content is fetched, “DoThePossibleCrash” is called. It just calls Path.GetFileName on a passed in string. If the checkbox is unchecked, the passed in string is valid and the web content is returned as a string and displayed in the textbox. If the crash checkbox is checked, the string passed contains some invalid characters, like “<>”, which causes the GetFileName call to throw an exception, which is uncaught and thus crashes the program.

clip_image001[4]

Cancel or Close the program.

If your program just disappears instead of showing the crash dialog, check your settings and this registry key:

HKEY_CURRENT_USER\Software\Microsoft\Windows\Windows Error Reporting “DontShowUI”

clip_image002[4]

Now, in VS, hit F5 to run the code under the VS debugger:

Make sure the CLR Exceptions are not caught in the debugger:

clip_image004[4]

Also, disable JustMyCode in Settings->Debugging.

The call stack from the VS debugger shows:

mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ThrowAsync.AnonymousMethod__5(object state) Line 1010 C#

mscorlib.dll!System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(object state) Line 1283 C#

mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 581 C#

mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 531 C#

mscorlib.dll!System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Line 1261 C#

mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() Line 829 C#

> mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Line 1170 C#

Using WinDBG sometimes I get a more complete callstack showing the code that causes the exception.

EXCEPTION_MESSAGE: Illegal characters in path.

MANAGED_OBJECT_NAME: SYSTEM.ARGUMENTEXCEPTION

MANAGED_STACK_COMMAND: !pe 245c584

LAST_CONTROL_TRANSFER: from 720121a7 to 764f2c1a

PRIMARY_PROBLEM_CLASS: WRONG_SYMBOLS

BUGCHECK_STR: APPLICATION_FAULT_WRONG_SYMBOLS_CLR_EXCEPTION

STACK_TEXT:

0802e9f4 71b9755f mscorlib_ni!System.IO.Path.CheckInvalidPathChars+0xa995bf

0802ea04 71112cb6 mscorlib_ni!System.IO.Path.GetFileName+0x16

0802ea1c 00801887 wpfcrashasync!WpfApplication1.MainWindow.DoThePossibleCrash+0x11f

0802ea64 00800e74 wpfcrashasync!WpfApplication1.MainWindow+_AccessTheWebAsync_d__e.MoveNext+0x20c

0802e73c 7117d17a mscorlib_ni!System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess+0x5e

0802e74c 7117d115 mscorlib_ni!System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification+0x35

0802e758 718f7cf9 mscorlib_ni!System.Runtime.CompilerServices.TaskAwaiter_1[[System.__Canon,_mscorlib]].GetResult+0x19

0802e760 008009df wpfcrashasync!WpfApplication1.MainWindow+_doTheWeb_d__b.MoveNext+0x1ff

07d8f698 718f4633 mscorlib_ni!System.Runtime.CompilerServices.AsyncMethodBuilderCore._ThrowAsync_b__5+0x33

07d8f6a0 7114c6a6 mscorlib_ni!System.Threading.QueueUserWorkItemCallback.WaitCallback_Context+0x3e

07d8f6a8 71113207 mscorlib_ni!System.Threading.ExecutionContext.RunInternal+0xa7

07d8f714 71113156 mscorlib_ni!System.Threading.ExecutionContext.Run+0x16

07d8f728 7116b910 mscorlib_ni!System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem+0x60

07d8f73c 7116b10a mscorlib_ni!System.Threading.ThreadPoolWorkQueue.Dispatch+0x10a

07d8f78c 7116aff5 mscorlib_ni!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback+0x5

https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

see also:

Scan the Windows Event Log for your application crashes and hangs

<code>

 //#define UseTryCatch // uncomment this line to try with Exception handling
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    TextBox _txtBoxResults;
    public MainWindow()
    {
      InitializeComponent();
      this.Loaded += (o, e) =>
      {
        this.Width = 1000;
        this.Height = 800;
        var sp = new StackPanel() { Orientation = Orientation.Vertical };
        var chkbox = new CheckBox() { Content = "Cause Crash" };
        var btn = new Button() { Content = "_DoIt" };
        sp.Children.Add(chkbox);
        sp.Children.Add(btn);
        _txtBoxResults = new TextBox();
        sp.Children.Add(_txtBoxResults);
        this.Content = sp;
        btn.Click += (ob, eb) =>
        {
          ThreadPool.QueueUserWorkItem(doTheWeb, chkbox.IsChecked);
          //var res = DoThePossibleCrash(@"c:\SomeDirName\SomeFile<badchars>.txt");
          //_txtBox.Text = res;
        };
      };
    }
    async void doTheWeb(object o)// the param we pass is the checkbox.IsChecked
    {
      var urlContent = await AccessTheWebAsync((bool)o);
      _txtBoxResults.Dispatcher.Invoke(() => _txtBoxResults.Text = urlContent);
    }

    // Three things to note in the signature: 
    // - The method has an async modifier. 
    // - The return type is Task or Task<T>. (See "Return Types" section.)
    // Here, it is Task<int> because the return statement returns an integer. 
    // - The method name ends in "Async."
    async Task<string> AccessTheWebAsync(bool fCauseACrash)
    {
      var urlContents = string.Empty;

#if UseTryCatch

            try
            {
#endif
      // You need to add a reference to System.Net.Http to declare client.
      HttpClient client = new HttpClient();
      Task<string> getWebContentTask = 
        client.GetStringAsync("https://blogs.msdn.com/b/calvin_hsia/archive/2013/07/31/10438571.aspx");

      // The await operator suspends AccessTheWebAsync. 
      // - AccessTheWebAsync can't continue until getStringTask is complete. 
      // - Meanwhile, control returns to the caller of AccessTheWebAsync. 
      // - Control resumes here when getStringTask is complete. 
      // - The await operator then retrieves the string result from getStringTask. 
      urlContents = await getWebContentTask;

      var testString = @"\foobar";
      if (fCauseACrash)
      {
        testString += "<temp>";  //illegal chars
      }

      // Call stacks can contain strings 
      //that should not be treated with invalid chars like "<"
      // f:\dd\internalapis\atlmfc\inc\atlcom.h(2973) : msenv.dll!ATL::CComObject<CTaskList>::CreateInstance + 143 bytes
      DoThePossibleCrash(testString, fValidateString: false);

      // another System.Argument exception crash:
      //  get the ProcessId of a non-existent process: 
      //var x = System.Diagnostics.Process.GetProcessById(222222); 

      // yet another System.Argument exception crash: An item with the same key has already been added
      //var dict = new Dictionary<int, int>();
      //dict[1] = 1;
      //dict.Add(1, 1);
#if UseTryCatch
            }
            catch (System.Exception ex)
            {
                // handle the exception!
                urlContents = ex.ToString();
            }
#endif


      return urlContents;
    }
    string DoThePossibleCrash(string testString, bool fValidateString)
    {
      var retVal = string.Empty;
      var isValid = true;
      if (fValidateString)
      {
        var invalidChars = Path.GetInvalidPathChars();
        //caution:  MSDN says even this set of chars is not guaranteed to be complete
        if (testString.Any(c => invalidChars.Contains(c)))
        {
          retVal = "Invalid characters in path";
          isValid = false;
        }
      }
      if (isValid)
      {
        retVal = Path.GetFileName(testString); // cause an argument exception: Invalid Chars in path
      }
      return retVal;

    }
  }
}

</code>