【.net 深呼吸】启动一个进程并实时获取状态信息

时间:2022-09-27 09:46:48

地球人和火星人都知道,Process类既可以获取正在运行的进程,也可以启动一个新的进程。在79.77%应用场合,我们只需要让目标进程顺利启动就完事了,至于它执行了啥,有没有出错,啥时候退出就不管了。

但是,在某些情况下,启动新进程后,还希望能向目标进程传送数据,或者实时读取来自新进程的信息。比如,启动一个安装程序,安装程序会向标准流写入安装进度,然后调用方可以从标准流中读取进度,以达到实时监控安装进度的目的。

Process类公开三个标准流属性:

StandardInput——输入流。类型是Writer,为啥是writer呢,因为这个标准流是相对于被启动的进程而言的,流动方向是从调用方流向目标进程,所以是写入数据,即将内容发送到目标进程。

StandardOutput——输出流。即目标进程对外输出的内容,流动方向是从目标进程流向调用方,因此,对调用者来说,是读取,故其类型为Reader。

StandardError——和输入流差不多,只是它专用于输出错误。错误信息是目标进程输出的,所以,对调用者来说还是读取者。

综上所述,只要启动新进程后,从StandardOutput属性在得到一个StreamReader对象,然后建立一个循环,不断地从流中读取内容,就能够实时获得最新状态了。

 

其实,还有更好办的方法,Process类有个BeginOutputReadLine方法,调用后,会自动异步读取数据,一旦收到目标进程传回的数据,就会引发OutputDataReceived事。所以,我们在代码中只要处理这个事件就可以接收实时信息了。

 

咱们来做个例子吧。假设我弄一个程序,只负责在后台安装,每处理完一个进度,就会向标准流写入进度信息,这样调用者就能实时监控安装进度了。

首先完成被调用的项目,项目类型为Windows应用程序项目。

【.net 深呼吸】启动一个进程并实时获取状态信息

不管它,反正就是一个标准的.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线程塞车,所以处理它较好。

 

现在,运行例子,会看以下效果。

【.net 深呼吸】启动一个进程并实时获取状态信息

 

示例代码下载地址