为什么我的进程没有触发调用进程中的“Exited”事件?

huangapple go评论73阅读模式
英文:

Why is my process not triggering the "Exited" event in a calling process?

问题

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:

  1. My MainView has a Button control, with the Command property bound to a LaunchAppChildCommand.
  2. In my MainViewModel, LaunchAppChildCommand is set equal to new AsyncRelayCommand(async () => await ExecuteAsync(AppChild) (I am using Community Toolkit Mvvm NuGet) (also, note that ExecuteAsync 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).
  3. 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);
    }
}
  1. 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;
}
  1. 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!

英文:

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:

  1. My MainView has a Button control, with the Command property bound to a LaunchAppChildCommand.
  2. In my MainViewModel, LaunchAppChildCommand is set equal to new AsyncRelayCommand(async () =&gt; await ExecuteAsync(AppChild) (I am using Community Toolkit Mvvm NuGet) (also, note that ExecuteAsync 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).
  3. This is what ExecuteAsync looks like:
        private async Task ExecuteAsync(string appFilePath)
        {
            try
            {
                await Task.Run(async () =&gt;
                {
                    await AsyncUtility.RunConverterAsync(appFilePath);
                });
            }
            catch (Exception ex)
            {
                LogEventViewerLog(ex);
            }
        }
  1. I then have a static class, AsyncUtility. AsyncUtility.RunConverterAsync is implemented in this way:
        public static async Task&lt;string&gt; RunConverterAsync(string appFilePath)
        {
            string appSuccessStatus = string.Empty;

            try
            {
                Process process = await RunProcessAsync(appFilePath);

                using (StreamReader streamReader = process.StandardOutput)
                {
                    appSuccessStatus += streamReader.ReadToEnd();

                    if (!appSuccessStatus.Equals(&quot;SUCCESS&quot;, StringComparison.OrdinalIgnoreCase))
                    {
                        DisplayError(appSuccessStatus); // This just displays a generic error message
                    }
                }
            }
            catch (Exception ex)
            {
                appSuccessStatus += &quot;FAILED&quot;;
            }
            return appSuccessStatus;
        }
  1. AsyncUtility.RunProcessAsync is a lengthier method:
        private static Task&lt;Process&gt; RunProcessAsync(string fileName)
        {
            TaskCompletionSource&lt;Process&gt; taskCompletionSource = new TaskCompletionSource&lt;Process&gt;();
            string endOfCmdLineArgument = &quot;\&quot; &quot;;

            if (File.Exists(fileName))
            {
                Process process = new Process()
                {
                    StartInfo = new ProcessStartInfo(fileName)
                    {
                        UseShellExecute = false,
                        RedirectStandardOutput = true
                    },
                    EnableRaisingEvents = true
                };

                process.StartInfo.Arguments += &quot;/parameterOne=\&quot;&quot; + StaticClass.ParameterOne + endOfCmdLineArgument
                    + &quot;/parameterTwo=\&quot;&quot; + StaticClass.ParameterTwo + endOfCmdLineArgument;

                Debug.WriteLine(process.StartInfo.Arguments);

                if (process.StartInfo.Arguments.Length &gt; 0)
                {
                    process.StartInfo.Arguments = process.StartInfo.Arguments.Trim();
                }

                process.Exited += (sender, localEventArgs) =&gt;
                {
                    taskCompletionSource.SetResult(process);
                };

                try
                {
                    process.Start();
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException($&quot;Could not start process: {ex.Message}&quot;);
                }
            }
            else
            {
                throw new ArgumentNullException($&quot;The process does not exist:\n{fileName}&quot;)
                {
                    Source = &quot;RunProcessAsync&quot;
                };
            }
            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!

答案1

得分: 0

经过多个小时的努力,我无法弄清楚为什么会发生这种情况。我花时间尝试在不同的解决方案中复制问题,但毫无结果。在更深入地研究了在 AppChild 中抛出未处理异常时 Process.Exited 事件不会触发的位置之后,我开始认为这与使用 IHost 不太相关,而与覆盖 OnStartup 更相关。作为替代方案,我订阅了 Application.Startup 事件,不仅起作用,而且实际上有文档描述其用途(与 OnStartup 进行比较),表明这是推荐的方法。我之前还遇到过使用 OnStartup 的其他问题。

我还要补充一下,我修改了 ParentApp 中启动 Process 代码的方式。首先,我没有正确处理 Process 的释放,无论是直接还是间接地。所以我把我的 Process 放在了 using 中。其次,在以前的代码中,我两次使用了 new Process - 我对引用类型的内部细节不是很熟悉,但我的直觉告诉我,分配内存两次不是一个好主意。我将 RunProcessAsync 改为扩展方法,传递对象的引用,因此操作影响了相同的底层对象。这是我的异步代码最终的样子:

public static async Task<string> RunAppAsync(string appFilePath, string args)
{
    string appSuccessStatus = string.Empty;
    string endOfCmdLineArgument = "\" ";

    if (File.Exists(appFilePath))
    {
        ProcessStartInfo processStartInfo = new ProcessStartInfo
        {
            FileName = appFilePath,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            Arguments = "/debug=\"" + "true" + endOfCmdLineArgument
                + "/otherParameter=\"" + args + endOfCmdLineArgument
        };

        if (processStartInfo.Arguments.Length > 0)
        {
            processStartInfo.Arguments = processStartInfo.Arguments.Trim();
        }

        using (Process process = new Process { EnableRaisingEvents = true, StartInfo = processStartInfo })
        {
            await process.RunProcessAsync();

            using (StreamReader streamReader = process.StandardOutput)
            {
                appSuccessStatus += streamReader.ReadToEnd();
                appSuccessStatus = appSuccessStatus.Substring(appSuccessStatus.LastIndexOf('\n') + 1).Trim();

                if (!appSuccessStatus.Equals("SUCCESS", StringComparison.OrdinalIgnoreCase))
                {
                    Debug.WriteLine(appSuccessStatus);
                    DisplayError(appSuccessStatus);
                }
            }
        }
    }
    else
    {
        throw new ArgumentNullException("The file for this process does not exist.");
    }

    return appSuccessStatus;
}

private static Task<Process> RunProcessAsync(this Process process)
{
    TaskCompletionSource<Process> taskCompletionSource = new TaskCompletionSource<Process>();

    process.Exited += (sender, localEventArgs) =>
    {
        taskCompletionSource.SetResult(process);
    };

    process.Start();

    return taskCompletionSource.Task;
}

因此,总结一下:尝试订阅 Startup 事件而不是覆盖 OnStartup;确保正确处理您的 Process;并确保处理与您的进程相关的任何代码(如果您的情况与我的类似)都处理相同的底层对象。这些操作似乎已经解决了 Process.Exited 事件不触发的问题。

其他可能有帮助的帖子:

英文:

After spending many hours on this, I couldn't figure out why this was occurring. I spent the time to try and replicate the problem in separate solutions, but to no avail. Playing around more with where the Process.Exited event would not be triggered when an unhandled exception was thrown in AppChild, I started to think it was less related to use of IHost and more related to overriding OnStartup. As an alternative, I subscribed to the Application.Startup event which not only worked, but actually has documentation describing its use (compare OnStartup), suggesting that it is recommended. I have also run into other issues before with the use of OnStartup.

I will also add that I altered how the code in ParentApp is starting the Process. First of all, I wasn't disposing of the Process either directly or indirectly. So I wrapped up my Process in a using. Secondly, I was newing up Process twice in my previous code - I am not expertly familiar with the ins and outs of reference types, but my gut was telling me this wasn't a good idea to allocate memory twice. I changed RunProcessAsync to be an extension method, passing in the reference to my object, and thus the operations were affecting the same underlying object. This is what my asynchronous code ended up looking like:

public static async Task&lt;string&gt; RunAppAsync(string appFilePath, string args)
{
	string appSuccessStatus = string.Empty;
	string endOfCmdLineArgument = &quot;\&quot; &quot;;


	if (File.Exists(appFilePath))
	{
		ProcessStartInfo processStartInfo = new ProcessStartInfo
		{
			FileName = appFilePath,
			UseShellExecute = false,
			RedirectStandardOutput = true,
			Arguments = &quot;/debug=\&quot;&quot; + &quot;true&quot; + endOfCmdLineArgument
				+ &quot;/otherParameter=\&quot;&quot; + args + endOfCmdLineArgument
	};


		if (processStartInfo.Arguments.Length &gt; 0)
		{
			processStartInfo.Arguments = processStartInfo.Arguments.Trim();
		}

		using (Process process = new Process {  EnableRaisingEvents = true, StartInfo = processStartInfo })
		{
			await process.RunProcessAsync();

			using (StreamReader streamReader = process.StandardOutput)
			{
				appSuccessStatus += streamReader.ReadToEnd();
				appSuccessStatus = appSuccessStatus.Substring(appSuccessStatus.LastIndexOf(&#39;\n&#39;) + 1).Trim();

				if (!appSuccessStatus.Equals(&quot;SUCCESS&quot;, StringComparison.OrdinalIgnoreCase))
				{
					Debug.WriteLine(appSuccessStatus);
					DisplayError(appSuccessStatus);
				}
			}
		}
	}
	else
	{
		throw new ArgumentNullException(&quot;The file for this process does not exist.&quot;);
	}

	return appSuccessStatus;
}



private static Task&lt;Process&gt; RunProcessAsync(this Process process)
{
	TaskCompletionSource&lt;Process&gt; taskCompletionSource = new TaskCompletionSource&lt;Process&gt;();

	process.Exited += (sender, localEventArgs) =&gt;
	{
		taskCompletionSource.SetResult(process);
	};

	process.Start();

	return taskCompletionSource.Task;
}

So in summary: try subscribing to Startup event instead of overriding OnStartup; make sure to properly dispose of your Process; and be sure that any code dealing with your process (if your situation is similar to mine) is dealing with the same underlying object. These actions seem to have fixed the issue of the Process.Exited event not getting triggered for me.

Other post(s) that might help:

huangapple
  • 本文由 发表于 2023年8月4日 07:07:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/76832081.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定