I have two .NET Framework 4.8 WPF applications. Let's call them "AppParent" and "AppChild". I cannot actually share the code, but here is the situation.
AppParent is an app that runs other applications, and in this case, is running AppChild. And it does so asynchronously. The way I accomplish this is as follows:
- My MainView has a Button control, with the Command property bound to a
LaunchAppChildCommand
. - In my MainViewModel,
LaunchAppChildCommand
is set equal tonew AsyncRelayCommand(async () => await ExecuteAsync(AppChild)
(I am using Community Toolkit Mvvm NuGet) (also, note thatExecuteAsync
actually takes a custom type I created, but what matters is that it includes the fully-qualified file path of AppChild or whatever the process is that I want to run). - This is what
ExecuteAsync
looks like:
private async Task ExecuteAsync(string appFilePath)
{
try
{
await Task.Run(async () =>
{
await AsyncUtility.RunConverterAsync(appFilePath);
});
}
catch (Exception ex)
{
LogEventViewerLog(ex);
}
}
- I then have a static class,
AsyncUtility
.AsyncUtility.RunConverterAsync
is implemented in this way:
public static async Task<string> RunConverterAsync(string appFilePath)
{
string appSuccessStatus = string.Empty;
try
{
Process process = await RunProcessAsync(appFilePath);
using (StreamReader streamReader = process.StandardOutput)
{
appSuccessStatus += streamReader.ReadToEnd();
if (!appSuccessStatus.Equals("SUCCESS", StringComparison.OrdinalIgnoreCase))
{
DisplayError(appSuccessStatus); // This just displays a generic error message
}
}
}
catch (Exception ex)
{
appSuccessStatus += "FAILED";
}
return appSuccessStatus;
}
AsyncUtility.RunProcessAsync
is a lengthier method:
private static Task<Process> RunProcessAsync(string fileName)
{
TaskCompletionSource<Process> taskCompletionSource = new TaskCompletionSource<Process>();
string endOfCmdLineArgument = "\" ";
if (File.Exists(fileName))
{
Process process = new Process()
{
StartInfo = new ProcessStartInfo(fileName)
{
UseShellExecute = false,
RedirectStandardOutput = true
},
EnableRaisingEvents = true
};
process.StartInfo.Arguments += "/parameterOne=\"" + StaticClass.ParameterOne + endOfCmdLineArgument
+ "/parameterTwo=\"" + StaticClass.ParameterTwo + endOfCmdLineArgument;
Debug.WriteLine(process.StartInfo.Arguments);
if (process.StartInfo.Arguments.Length > 0)
{
process.StartInfo.Arguments = process.StartInfo.Arguments.Trim();
}
process.Exited += (sender, localEventArgs) =>
{
taskCompletionSource.SetResult(process);
};
try
{
process.Start();
}
catch (Exception ex)
{
throw new InvalidOperationException($"Could not start process: {ex.Message}");
}
}
else
{
throw new ArgumentNullException($"The process does not exist:\n{fileName}")
{
Source = "RunProcessAsync"
};
}
return taskCompletionSource.Task;
}
While the above is not precisely relevant to my problem, the fundamental issue here appears related to processes and threads, which I am spawning from the above code, so I think it is relevant context. (Note that there may be some details in this code that are not entirely consistent or best practice, but this is what ultimately got me a good result.)
Now, the issue actually seems to be occurring in AppChild. When started by process.Start()
it'll start going. I've set up my environment to launch the debugger for the AppChild process, and I see confounding behavior. In the App
constructor (recall it's also a WPF app) I subscribe to unhandled exceptions like this: AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
. When an exception occurs, I go into my handler:
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
try
{
Exception ex = (Exception)e.ExceptionObject;
LogEventViewerLog(ex);
}
// There is a catch clause here, but it has never been reached during any of my sessions
}
The debugger goes all the way through, but then back over in AppParent, process.Exited
never gets hit! (I have a breakpoint there too, that I know works in some cases.) If I look in my task manager, I see the pesky process for AppChild running and so far I resolve the exited event by terminating that process manually.
The exact exception I am getting that started all this (in AppChild) is System.IO.DirectoryNotFoundException. I started testing by throwing a contrived exception in AppChild: throw new Exception()
after my Debugger.Launch()
. Lo! The debugger hit my exception handler. The debugger then jumps momentarily back up to my thrown exception and then to the exited event over in AppParent.
Then I went back to testing the original exception (System.IO.DirectoryNotFoundException). I noticed that these methods did not resolve the exited event (i.e. they did not cause the process to end properly): Application.Current.Shutdown()
, Application.Current.Dispatcher.InvokeShutdown()
, Application.Current.Dispatcher.Invoke(Application.Current.Shutdown)
. However, the following two did work: Environment.Exit(0)
and Process.GetCurrentProcess().Kill()
. But these methods are very "abrupt" to my understanding, and can result in other issues. I would think that a process should exit in an appropriate fashion upon an unhandled exception. Am I misunderstanding what the docs mean when they say "In most cases, this means that the unhandled exception causes the application to terminate"?
I then reviewed this article. As I played around with various events, I also tried throwing exceptions around in other places. I then started to notice that the behavior appears related to the point at which the exception occurs in AppChild. As mentioned before, when I throw an exception immediately after Debugger.Launch()
, the exited event resolves as expected. The original exception that results in the exited event not triggering comes much later, before I Show()
the MainView, but after initializing an IHost
instance. Interestingly, when I put my contrived throw new Exception
right before the same point that the other exception is being thrown, I see the same behavior. I tried this in various spots, and the relationship seems to hold.
After trying a variety of things, and looking at the documentation I could find, what I am coming up with is that it's related to the IHost
- and probably the fact that this is being initiated by AppParent. But I am at a loss as to how to resolve this and perhaps more importantly, why this is happening!