Таинственное зависание при выполнении пакетных скриптов в процессе C # и отображении вывода в текстовом поле - PullRequest
0 голосов
/ 29 октября 2018

Я отлаживаю библиотеку классов (на самом деле это расширение Visual Studio), которая отвечает за выполнение пользовательских пакетных сценариев с использованием Windows cmd.exe и отображение результатов. Он настроен так, что использует C # Process, который вызывает cmd.exe, выполняет команды одну за другой и отображает результаты. Вот интересная часть: результаты отображаются в форме Windows Textbox! Textbox должен находиться сверху во время выполнения команд (по независящим от меня причинам), а кнопка закрытия активируется, когда все команды выполняются. Только тогда пользователь может закрыть эту форму. Проблема заключается в том, что при нажатии кнопки закрытия приложение (в данном случае Visual Studio) зависает. Я получаю следующее исключение при отладке программы:

 System.ComponentModel.InvalidAsynchronousStateException
 HResult=0x80070057
  Message=An error occurred invoking the method.  The destination thread no longer exists.
  Source=System.Windows.Forms
  StackTrace:
   at System.Windows.Forms.Control.WaitForWaitHandle(WaitHandle waitHandle)
   at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
   at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
   at System.Windows.Forms.Control.Invoke(Delegate method)
   at VSPlugin.Ft.ProgressForm.AppendOutputLine(String line) in T:\...\ProgressForm.cs:line 99
   at VSPlugin.Ft.SystemShell.<>c__DisplayClass35_0.<Execute>b__0(Object sender, DataReceivedEventArgs e) in T:\..\SystemShell.cs:line 117
   at System.Diagnostics.Process.OutputReadNotifyUser(String data)
   at System.Diagnostics.AsyncStreamReader.FlushMessageQueue()
   at System.Diagnostics.AsyncStreamReader.GetLinesFromStringBuilder()
   at System.Diagnostics.AsyncStreamReader.ReadBuffer(IAsyncResult ar)
   at System.IO.Stream.ReadWriteTask.InvokeAsyncCallback(Object completedTask)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.IO.Stream.ReadWriteTask.System.Threading.Tasks.ITaskCompletionAction.Invoke(Task completingTask)
   at System.Threading.Tasks.Task.FinishContinuations()
   at System.Threading.Tasks.Task.FinishStageThree()
   at System.Threading.Tasks.Task.FinishStageTwo()
   at System.Threading.Tasks.Task.Finish(Boolean bUserDelegateExecuted)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
   at System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
   at System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

Это класс, который должен выполнять пакетные сценарии. Сценарии хранятся в виде строки в объекте Script. ExecuteAsTask() - это метод ввода этого класса, который вызывается при нажатии кнопки где-то в плагине.

class SystemShell
{
    protected Ft.System System { get; private set; }
    public List<string> Script { get; protected set; }
    public List<string> Output { get; private set; }

    public CommonParams Param { get; private set; }

    private Process OutputConsole { get; set; }
    private bool IncludeProgress { get; set; }
    private bool IgnorePreview { get; set; }
    public SystemShell(CommonParams fcparam, bool ignorePreview, bool includeProgress)
    {
        System = new Ft.System();
        Param = fcparam;
        Output = new List<string>();
        IncludeProgress = includeProgress;
        IgnorePreview = ignorePreview;
    }



    public Task ExecuteAsTask()
    {
        return Task.Run(() => this.Execute());
    }

    public void Execute()
    {            

        ProgressForm pf = null;            
        pf = new ProgressForm(Script.Count);
        pf.Start();            

        Process process = new Process()
        {
            StartInfo = new ProcessStartInfo()
            {
                CreateNoWindow = true,
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                RedirectStandardInput = true,
                UseShellExecute = false,
                WorkingDirectory = Param.ControlPath,
                FileName = @"cmd.exe",
                Verb = @"runas", // to self elevate to administrative privilege...
                Arguments = @"/k"                    
            },                
            EnableRaisingEvents = true // enable raising events because Process does not raise events by default
        };

        DataReceivedEventHandler outputHandler = new DataReceivedEventHandler // attach the event handler for OutputDataReceived before starting the process
        (
            delegate (object sender, DataReceivedEventArgs e)
            {
                // append the new data to the data already read-in
                if (IncludeProgress)
                {
                    pf.AppendOutputLine(e.Data);
                }
                Output.Add(e.Data);
            }
        );

        process.OutputDataReceived += outputHandler;
        process.ErrorDataReceived += outputHandler;


        // start the process
        // then begin asynchronously reading the output
        // then wait for the process to exit
        // then cancel asynchronously reading the output
        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        foreach (string command in Script)
        {
            process.StandardInput.WriteLine(command);
            process.StandardInput.Flush();
        }

        process.StandardInput.Close();
        process.WaitForExit();

        process.CancelOutputRead();
        process.CancelErrorRead();

        if (IncludeProgress)
        {
            pf.Stop();
        }

    }
}

И это класс, который должен создать форму вывода:

public class ProgressForm : IWin32Window
{
    [DllImport("user32.dll")]
    private static extern IntPtr GetForegroundWindow();
    IntPtr IWin32Window.Handle { get { return GetForegroundWindow(); } }

    private Form m_form;
    private TextBox m_textbox;
    private Button m_closeButton;
    private bool m_processingComplete;

    private Thread m_formThread;
    public ProgressForm(int maxProgress)
    {
        m_processingComplete = false;

        m_form = new Form()
        {
            Text = "Shell Operations",
            FormBorderStyle = FormBorderStyle.FixedSingle,
            MinimizeBox = false,
            MaximizeBox = false,
            ControlBox = true                
        };

        m_textbox = new TextBox()
        {
            Multiline = true,
            ReadOnly = true,
            ScrollBars = ScrollBars.Vertical,
            Font = new Font(@"Lucida Console", 9),
            BackColor = Color.Black,
            ForeColor = Color.GreenYellow,
        };
        m_textbox.SetBounds(0, 0, 800, 330);

        m_closeButton = new Button()
        {
            Text = @"Close",
            Enabled = false,
            TextAlign = ContentAlignment.MiddleCenter                
        };
        m_closeButton.Click += new EventHandler(this.OnCloseButtonClick);
        m_closeButton.SetBounds(0, 335, 800, 25);

        // Set the client area of the form equal to the size of the Text Box
        m_form.ClientSize = new Size(800, 360);
        // Add the Textbox to the form
        m_form.Controls.Add(m_textbox);
        m_form.Controls.Add(m_closeButton);
    }

    private void OnCloseButtonClick(object sender, EventArgs args)
    {
        if (!m_processingComplete)
        {
            return;
        }
        m_form.Close();
        m_form = null;
    }

    public void Start()
    {
        m_formThread = new Thread(this.RunForm);
        m_formThread.Start();
        // yes I hate myself - but just in case of paranoid delusions
        Thread.Sleep(10);
    }

    private void UpdateCloseButtonEnabled(bool isEnabled)
    {
        m_closeButton.Invoke((MethodInvoker)delegate
        {
            m_closeButton.Enabled = isEnabled;
            // m_form.Refresh();
        });
    }

    public void AppendOutput(string line)
    {
        m_textbox.Invoke((MethodInvoker)delegate
        {
            m_textbox.AppendText(line);
            //m_form.Refresh();
        });
    }

    public void AppendOutputLine(string line)
    {            
        m_textbox.Invoke((MethodInvoker)delegate
        {
            m_textbox.AppendText(line + "\n");
            //m_form.Refresh();
        });
    }

    private void RunForm()
    {
        m_form.ShowDialog(this);
    }

    public void Stop()
    {
        m_processingComplete = true;
        UpdateCloseButtonEnabled(true);
    }
}

Таким образом, когда все сценарии запущены, кнопка закрытия активируется. Нажатие на кнопку «Закрыть» приводит к зависанию Visual Studio на неопределенный срок. Любая идея или мысли, что вызывает это?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...