【.net 深呼吸】启动一个进程并实时获取状态信息
地球人和火星人都知道,Process类既可以获取正在运行的进程,也可以启动一个新的进程。在79.77%应用场合,我们只需要让目标进程顺利启动就完事了,至于它执行了啥,有没有出错,啥时候退出就不管了。
但是,在某些情况下,启动新进程后,还希望能向目标进程传送数据,或者实时读取来自新进程的信息。比如,启动一个安装程序,安装程序会向标准流写入安装进度,然后调用方可以从标准流中读取进度,以达到实时监控安装进度的目的。
Process类公开三个标准流属性:
StandardInput——输入流。类型是Writer,为啥是writer呢,因为这个标准流是相对于被启动的进程而言的,流动方向是从调用方流向目标进程,所以是写入数据,即将内容发送到目标进程。
StandardOutput——输出流。即目标进程对外输出的内容,流动方向是从目标进程流向调用方,因此,对调用者来说,是读取,故其类型为Reader。
StandardError——和输入流差不多,只是它专用于输出错误。错误信息是目标进程输出的,所以,对调用者来说还是读取者。
综上所述,只要启动新进程后,从StandardOutput属性在得到一个StreamReader对象,然后建立一个循环,不断地从流中读取内容,就能够实时获得最新状态了。
其实,还有更好办的方法,Process类有个BeginOutputReadLine方法,调用后,会自动异步读取数据,一旦收到目标进程传回的数据,就会引发OutputDataReceived事。所以,我们在代码中只要处理这个事件就可以接收实时信息了。
咱们来做个例子吧。假设我弄一个程序,只负责在后台安装,每处理完一个进度,就会向标准流写入进度信息,这样调用者就能实时监控安装进度了。
首先完成被调用的项目,项目类型为Windows应用程序项目。
不管它,反正就是一个标准的.exe文件,这个项目我是先建个空白项目,然后手动设置的。
每个可执行程序都必须至少有一个Main方法。主要的处理代码都在这里完成,只要Main方法执行完,进程就会退出了。
static void Main() { StreamWriter writer = null; Stream outStream = Console.OpenStandardOutput(); writer = new StreamWriter(outStream); int n = 0; while (n <= 100) { Thread.Sleep(30); writer.WriteLine(n); writer.Flush(); n++; } writer.Close(); Environment.ExitCode = 0; }
这里用到了Console类,别以为它只能耍控制台应用程序,其实Console类还包括标准输入输出的操作。要调用OpenStandardOutput方法获取标准输出流,然后就可以写入内容了。
由于Process类的StandarOutput属性或OutputDataReceived事件,都是用字符串来传递的,所以上面代码中,咱们也用StreamWriter来写数据。
不过要注意一定,每写一回都要记得Flush一次,这样写入的内容才会让调用方及时收到。如果不Flush的话,写入的内容会放在缓冲区中,直接流关闭或执行Flush时才会真正发送到标准流上,所以,每写完一次都调用一下Flush方法,确保调用方能够实时收到信息。
最后那一行Environment.ExitCode = 0 表示进程退出时返回退出码0,即正常退出。因为我这个Main是返回void的,所以要用Enviroment类的ExitCode来设置。当然了,你还可以把Main方法改为返回int类型的值,然后直接 return 0 就行了。
好,被调用进程项目完成,现在做调用者项目,它是一个WPF项目。在这个时代,写Windows桌面应用都应优先用WPF,因为WPF是牛逼层面的东东。
XAML代码就不贴了,直接讲核心代码。
我用了个进度条来实时显示进度,而Process类的OutputDataReceived事件是异步引发的,要在事件处理中更新进度条,需要借助Dispatcher来代理调用。
不过,在这个例子中并不需要,因为有一个很NX的类,专门用来处理进度的,就是Progress<T>,这个类可以绑定一个回调的委托,用它来更新UI是不需要Dispatcher来调度的,只要Progress<T>实例是在UI线程上创建的即可(忘了说明这句,多谢网友在评论中补充)。
IProgress<int> progress; progress = new Progress<int>(p => { pb.Value = p; });
在声明变量的时候,应用 IProcess<T> 接口来声明,T是表示进度的类型,Progress类是显示实现了IProgress接口的,为了能够直接调用Report方法报告进度,应当用IProgress接口来声明变量。
下面代码启动刚刚写的那个进程,并监视状态信息。
Process p = new Process(); p.StartInfo.FileName = "..\\..\\..\\Demo\\bin\\Debug\\Demo.exe"; p.StartInfo.UseShellExecute = false; //必须为false p.StartInfo.RedirectStandardOutput = true; p.StartInfo.CreateNoWindow = true; p.EnableRaisingEvents = true; //必须为true p.OutputDataReceived += OnDataReceived; p.Exited += OnExited; p.Start(); p.BeginOutputReadLine();
实例化Process后,要设置StartInfo相关参数,FileName是要执行进程的exe文件。
注意:
UseShellExecute必须为false,不然无法在代码中读标准。
RedirectStandardOutput必须为true,这样我们才能在代码中访问标准流。
EnableRaisingEvents必须为true,这样才会引发OutputDataReceived和Exited
事件,否则事件不会发生。
CreateNoWindow表示不显示目标程序的窗口,这个你自己看着办,这里我不让它显示窗口,因为这个程序本来就没有窗口。
一定要在Start方法之后调用BeginOutputReadLine方法,一定要在 Start 和 BeginOutputReadLine方法调用前处理OutputDataReceived事件。为啥,自己想吧,这太简单了,不脱裤子怎么拉屎,除非你不穿裤子。
在OnDataReceived方法中读出数据,并转化为int类型,因为刚才上面的那个项目中,是把一个int值写入流的,所以这里读出来的值是可以转换为int类型的。
private void OnDataReceived(object sender, DataReceivedEventArgs e) { int p = Convert.ToInt32(e.Data); progress.Report(p); }
直接调用IProgress<T>的Report方法就能报告进度了。
本来,是可以调用 System.Diagnostics.Process.WaitForExit()方法来等待进程执行完的,但是,由于这个方法是同步调用的,它会让UI线程塞车,导致UI无法即时响应,体验不好。所以改为处理Exited事件,这个事件会在进程退出后异步调用,不会让UI线程塞车,所以处理它较好。
现在,运行例子,会看以下效果。