首页 文章

如何关闭cmd.exe窗口作为后台工作程序C#中的进程运行?

提问于
浏览
1

我对C#很新,并试图了解后台工作人员 .

目前,当我运行此代码时,它将停止重定向并在单击StopButton后从命令提示符读取输出并留下“已取消”消息,但之后不执行任何操作 . 我目前可能实现这一切都错了,但我通过单击调用CancelAsync()的停止按钮来设置e.Cancel,这会取消CancellationPending = true . 任何人都有任何想法我应该怎么做?

非常感谢!!我很感激帮助!

public Form2()
    {
        InitializeComponent();

        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerCompleted += new   
        RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);

    }

    private void StopButton_Click(object sender, EventArgs e)
    {
        if (bw.WorkerSupportsCancellation == true)
        {
            bw.CancelAsync();
            bw.Dispose();

            Invoke(new ToDoDelegate(() => this.textBox2.Text += Environment.NewLine + "Cancelled " + Environment.NewLine));
        }
    }

    private void Submit_Click(object sender, EventArgs e)
    {
            if(bw.IsBusy != true)
            {
                bw.RunWorkerAsync();
                Invoke(new ToDoDelegate(() => this.textBox2.Text += "Starting Download " + Environment.NewLine));
            }

    }

    public bool DoSVNCheckout(String KeyUrl, DoWorkEventArgs e)
    {
        SVNProcess = new Process
        {
            StartInfo = new ProcessStartInfo
            {

                FileName = "cmd.exe",
                Arguments = "/C plink download files using svn"
                Verb = "runas",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = false,
            }
        };

        SVNProcess.Start();
        while(!SVNProcess.StandardOutput.EndOfStream & bw.CancellationPending == false)
        {
            string output = SVNProcess.StandardOutput.ReadLine();
            Invoke(new ToDoDelegate(() => this.textBox2.Text += output));
        }
        while (!SVNProcess.StandardError.EndOfStream & bw.CancellationPending == false)
        {
            string Erroutput = SVNProcess.StandardError.ReadLine();
            Invoke(new ToDoDelegate(() => this.textBox2.Text += Erroutput));
        }
        if(SVNProcess.StandardError.EndOfStream & bw.CancellationPending == false)
        {
            string Erroutput = SVNProcess.StandardError.ReadLine();
            Invoke(new ToDoDelegate(() => this.textBox2.Text += Erroutput));
        }

        //if I manually close the cmd.exe window by clicking X
        //in the top right corner the program runs correctly
        //at these lines of code right here

        if(bw.CancellationPending == true)
        {
            e.Cancel = true;
            return true;
        }
        return false;
    }

    private delegate void ToDoDelegate();
    private void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;


            if(bw.CancellationPending == true)
            {
                e.Cancel = true;
                return;
            }
            e.Cancel = DoSVNCheckout(URL, e);

    }
    private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {

        if ((e.Cancelled == true))
        {
            this.textBox2.Text = "Canceled!";
        }
        else{
            this.textBox2.Text = "Done!";
        }
    }

1 回答

  • 0

    您编写的代码存在许多问题 . 在我看来,两个主要问题是:

    • 首先,您似乎将 BackgroundWorker 操作与您已启动的单独运行的进程混淆 . 这两者绝不是相同的,甚至是相互关联的 . 取消 BackgroundWorker 赢了't have any direct effect on the process you start. Your question is not clear on what the actual desired behavior is here, but you don' t做任何实际终止外部进程的事情 . 充其量,如果你没有阻止 DoWork 方法等待进程产生输出,你就放弃了这个过程 . 实际上,在没有终止进程的情况下,你的 DoWork 永远无法达到它注意到 ReadLine() 被卡在 ReadLine() 呼叫上的程度 .

    • 您正在以串行方式使用 StandardOutputStandardError 流,即一个接一个地使用 . 文档明确警告不要这样做,因为这是一种非常可靠的方法来解决您的代码 . 每个流的缓冲区相对较小,如果缓冲区已满时尝试写入其中一个流,则整个外部进程将挂起 . 这将导致不再将输出写入任何流 . 在您的代码示例中,如果 StandardError 流缓冲区在您能够完全读取 StandardOutput 流之前已满,则外部进程将挂起,您自己的进程也会挂起 .

    另一个小问题是您没有利用 BackgroundWorker.ProgressChanged 事件,您可以使用该事件将您从输出中读取的文本和错误字符串传递回UI线程,您可以将该文本添加到文本框中 . 这里使用 Control.Invoke() 并不是绝对必要的,并且无法充分利用 BackgroundWorker 中的功能 .

    有一些方法可以修改您编写的代码,这样您仍然可以使用 BackgroundWorker 来实现目标 . 您可以做出的一个明显改进是将 Process 对象引用存储在实例字段中,以便 StopButton_Click() 方法可以访问它 . 在该方法中,您可以调用 Process.Kill() 方法来实际终止该进程 .

    但即便如此,您仍需要修复现在已经遇到的死锁问题 . 这可以通过多种方式完成:使用 Process.OutputDataReceivedProcess.ErrorDataReceived 事件;创建第二个 BackgroundWorker 任务来处理其中一个流;使用基于 Task 的习语来读取流 .

    最后一个选择是我的偏好 . 第二个选项不必要地创建长时间运行的线程,而基于事件的模式(第一个选项)使用起来很笨拙(并且是基于行的,因此在处理在操作过程中写入部分行的进程时具有有限的值) . 但是如果你打算使用基于 Task 的习惯用法来阅读流,那么在我看来你应该升级整个实现来这样做 .

    BackgroundWorker 仍然是一个可行的类,如果有人想要使用,但新的 Task 功能与 async / await 关键字一起提供了什么是IMHO处理异步操作更简单,更简洁的方法 . 其中一个最大的优点是它不依赖于显式使用的线程(例如,在线程池线程中运行 DoWork 事件处理程序) . 异步I / O操作(例如构成整个场景的内容)通过API隐式处理,允许您编写的所有代码在您想要的UI线程中执行 .

    以下是您的示例的一个版本:

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    
        private TaskCompletionSource<bool> _cancelTask;
    
        private async void button1_Click(object sender, EventArgs e)
        {
            button1.Enabled = false;
            button2.Enabled = true;
            _cancelTask = new TaskCompletionSource<bool>();
    
            try
            {
                await RunProcess();
            }
            catch (TaskCanceledException)
            {
                MessageBox.Show("The operation was cancelled");
            }
            finally
            {
                _cancelTask = null;
                button1.Enabled = true;
                button2.Enabled = false;
            }
        }
    
        private void button2_Click(object sender, EventArgs e)
        {
            _cancelTask.SetCanceled();
        }
    
        private async Task RunProcess()
        {
            Process process = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    FileName = "cmd.exe",
                    Arguments = "/C pause",
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = false,
                }
            };
    
            process.Start();
    
            Task readerTasks = Task.WhenAll(
                ConsumeReader(process.StandardError),
                ConsumeReader(process.StandardOutput));
    
            Task completedTask = await Task.WhenAny(readerTasks, _cancelTask.Task);
    
            if (completedTask == _cancelTask.Task)
            {
                process.Kill();
                await readerTasks;
                throw new TaskCanceledException(_cancelTask.Task);
            }
        }
    
        private async Task ConsumeReader(TextReader reader)
        {
            char[] text = new char[512];
            int cch;
    
            while ((cch = await reader.ReadAsync(text, 0, text.Length)) > 0)
            {
                textBox1.AppendText(new string(text, 0, cch));
            }
        }
    }
    

    笔记:

    • 首先,正如您所看到的,根本不再需要 BackgroundWorker . async / await 模式隐式执行 BackgroundWorker 将为您提供的所有相同工作,但没有设置和管理它所需的所有额外样板代码 .

    • 有一个新的实例字段 _cancelTask ,它表示一个简单的 Task 对象,可以完成 . 它不是严格要求的......你会注意到监视任务完成的 await 语句实际上并不关心任务是如何结束的 . 就是这样 . 在更复杂的场景中,实际上可能希望将 Result 用于此类 Task 对象,调用 SetResult() 以使用值完成任务,并使用 SetCanceled() 实际取消正在表示的操作 . 这一切都取决于具体的背景 .

    • button1_Click() 方法(相当于您的 Submit_Click() 方法)被写成好像一切都是同步发生的 . 通过 await 语句的"magic",该方法实际上分两部分执行 . 单击按钮时,将执行 await 语句之前的所有操作;在 await ,一旦 RunProcess() 方法返回, button1_Click() 方法返回 . 当 RunProcess() 返回的 Task 对象完成时,它将在稍后恢复执行,这反过来将在该方法到达其结束时发生(即不是第一次返回) .

    • button1_Click() 方法中,UI会更新以反映当前的操作状态:禁用启动按钮并启用取消按钮 . 在返回之前,按钮将返回其原始状态 .

    • button1_Click() 方法也是 _cancelTask 对象的创建位置,后来被丢弃 . 如果 RunProcess() 抛出它, await RunProcess() 语句将看到 TaskCanceledException ;这用于向用户显示 MessageBox 报告操作已被取消 . 你当然可以回应这样的例外但是你认为合适 .

    • 这样, button2_Click() 方法(相当于您的 StopButton_Click() 方法)只需要将 _cancelTask 对象设置为已完成状态,在这种情况下,通过调用 SetCanceled() .

    • RunProcess() 方法是进程的主要处理 . 它启动该过程,然后等待相关任务完成 . 表示输出和错误流的两个任务包含在对 Task.WhenAll() 的调用中 . 这将创建一个新的 Task 对象,该对象仅在完成所有包装任务时才会完成 . 然后该方法通过 Task.WhenAny() 等待该包装器任务和 _cancelTask 对象 . 如果完成,则该方法将完成执行 . 如果完成的任务是 _cancelTask 对象,则该方法通过终止已启动的进程(在其正在执行的任务中间中断它)来等待进程实际退出(这可以通过完成包装器任务...当达到输出和错误流的结束时,这些完成,当进程退出时反过来发生,然后抛出 TaskCanceledException .

    • ConsumeReader() 方法是一个帮助方法,它简单地从给定的 TextReader 对象中读取文本,并将输出附加到文本框 . 它使用 TextReader.ReadAsync() ;这种类型的方法也可以使用 TextReader.ReadLineAsync() 编写,但在这种情况下,您只能看到每行末尾的输出 . 使用 ReadAsync() 可确保在输出可用时立即检索输出,而无需等待换行符 .

    • 请注意, RunProcess()ConsumeReader() 方法也是 async ,并且还包含 await 语句 . 与 button1_Click() 一样,这些方法最初在到达 await 语句时从执行返回,稍后等待 Task 完成时继续执行 . 在 ConsumeReader() 示例中,您还会注意到 await 解包 int 值,该值是它正在等待的 Task<int>Result 属性值 . await 语句形成一个表达式,其值为 TaskResult 值 .

    • 在每种情况下使用 await 的一个非常重要的特征是框架在UI线程上恢复执行该方法 . 这就是为什么 button1_Click() 方法仍然可以在 await 之后访问UI对象 button1button2 ,以及为什么 textBox1 可以访问 textBox1 对象,以便每次 ReadAsync() 方法返回一些文本时附加文本 .

    我意识到以上可能需要消化很多 . 特别是当它大部分涉及从使用 BackgroundWorker 到基于_1084114的API的完全变化时,而不是解决我在开始时提到的两个主要问题 . 但我希望你能看到这些变化以及其他变化如何隐含地解决这些问题通过使用现代 async / await 模式,可以以更简单,更易于阅读的方式满足代码的要求 .

    为了完整性,以下是与上述 Form1 类一起使用的Designer生成的代码:

    partial class Form1
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;
    
        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }
    
        #region Windows Form Designer generated code
    
        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.button1 = new System.Windows.Forms.Button();
            this.button2 = new System.Windows.Forms.Button();
            this.textBox1 = new System.Windows.Forms.TextBox();
            this.SuspendLayout();
            // 
            // button1
            // 
            this.button1.Location = new System.Drawing.Point(12, 12);
            this.button1.Name = "button1";
            this.button1.Size = new System.Drawing.Size(75, 23);
            this.button1.TabIndex = 0;
            this.button1.Text = "Start";
            this.button1.UseVisualStyleBackColor = true;
            this.button1.Click += new System.EventHandler(this.button1_Click);
            // 
            // button2
            // 
            this.button2.Enabled = false;
            this.button2.Location = new System.Drawing.Point(93, 12);
            this.button2.Name = "button2";
            this.button2.Size = new System.Drawing.Size(75, 23);
            this.button2.TabIndex = 0;
            this.button2.Text = "Stop";
            this.button2.UseVisualStyleBackColor = true;
            this.button2.Click += new System.EventHandler(this.button2_Click);
            // 
            // textBox1
            // 
            this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
            | System.Windows.Forms.AnchorStyles.Left) 
            | System.Windows.Forms.AnchorStyles.Right)));
            this.textBox1.Location = new System.Drawing.Point(13, 42);
            this.textBox1.Multiline = true;
            this.textBox1.Name = "textBox1";
            this.textBox1.ReadOnly = true;
            this.textBox1.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
            this.textBox1.Size = new System.Drawing.Size(488, 258);
            this.textBox1.TabIndex = 1;
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(513, 312);
            this.Controls.Add(this.textBox1);
            this.Controls.Add(this.button2);
            this.Controls.Add(this.button1);
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);
            this.PerformLayout();
    
        }
    
        #endregion
    
        private System.Windows.Forms.Button button1;
        private System.Windows.Forms.Button button2;
        private System.Windows.Forms.TextBox textBox1;
    }
    

相关问题