Я отлаживаю библиотеку классов (на самом деле это расширение 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 на неопределенный срок. Любая идея или мысли, что вызывает это?