Usuário com melhor resposta
Executar Shell sem travar aplicação

Pergunta
-
Pessoal,
Eu sou um "mané" em aplicação desktop. Há mais de 8 anos trabalho 100% focado em web.
Estou com um pequeno impasse:
Fiz uma aplicação desktop em C#. Está funcionando sem problemas. Ela acessa o shell para executar algumas coisas, usando o System.Diagnostics.Process.
No entanto, enquanto o processo está sendo executado a aplicação fica travada. Nela existe um textbox onde coloco o retorno apresentado no shell. Porém, esse textbox somente apresenta o resultado quando do final da execução do processo.
Existe como ser iterativo, ou seja, enquanto executa o processo (o shell), a aplicação não travar e as mensagens apresentadas serem mostradas na app?
Valeu!
Respostas
-
Danilo, respire fundo que a resposta vai ser longa
Sua aplicação está travando porque você está lendo o stream de saída (e não o retorno! O retorno é se executou com sucesso ou não
).
Ao invés de ler o stream até o final, você pode ler linha a linha. Assim, ao invés de usar o ReadToEnd, você pode utilizar o ReadLine:
Code Snippetstring linha;
while ((linha = p.StandardOutput.ReadLine()) != null)
{
this.txtMessage.Text += Environment.NewLine + linha;
}
Isso deve fazer sua aplicação travar *menos* do que agora, mas ainda assim não resolve o problema.
A solução definitiva - e muito mais elegante - é executar o processo em uma thread separada. Existem várias formas de fazer isso, e a mais simples no seu caso (minha opinião) é utilizar o BackgroundWorker para fazer esse trabalho.
Com o BackgroundWorker você pode indicar qual método você quer que seja executado em uma thread separada, pode indicar qual método será executado para mostrar o progresso ao usuário, e ainda, indicar qual método será executado quando o processo que estava sendo executado em background terminou.
Então, o primeiro passo: Criar e configurar o Background Worker:
Code Snippetprivate BackgroundWorker compilacaoWorker = null;
private void ClickDoBotaoQueIniciaOProcesso(object sender, EventArgs e)
{
if (this.compilacaoWorker == null)
{
// Instancia um novo Background Worker (BW)
this.compilacaoWorker = new BackgroundWorker();
// Define que o BW permite informar sobre o
// progresso da execução
this.compilacaoWorker.WorkerReportsProgress = true;
// Associa o evento DoWork ao seu método que
// executa o processo
this.compilacaoWorker.DoWork +=
new DoWorkEventHandler(this.IniciarExecucao);
// Associa o evento ProgressChanges ao seu método que
// irá preencher a caixa de texto com as informações
// do processo
this.compilacaoWorker.ProgressChanged +=
new ProgressChangedEventHandler(this.ProgressoMudou);
// Associa o evento RunWorkerCompleted ao seu método
// quer irá avisar o usuário que o processo terminou
this.compilacaoWorker.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(this.ProcessoTerminou);
}
// Inicia a execução em uma nova thread...
this.compilacaoWorker.RunWorkerAsync();
}
Em seguida, implementar os métodos "IniciarExecucao", "ProgressoMudou" e "ProcessoTerminou" - note que estes nomes estão assim apenas por questões didáticas
Code Snippetprivate void IniciarExecucao(object sender, DoWorkEventArgs e)
{
System.Diagnostics.Process p = new System.Diagnostics.Process();
using (p)
{
// Tudo aquilo que você já faz para executar o processo...
p.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
p.StartInfo.FileName = @"compile.bat";
p.StartInfo.Arguments = "...";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
// Informa o usuário que o processo está iniciando...
// A chamada ao ReportProgress fará com que o método
// "ProgressoMudou" seja disparado...
this.compilacaoWorker.ReportProgress(0, ">>> Iniciando Processsamento...");
this.compilacaoWorker.ReportProgress(0, Environment.NewLine + p.StartInfo.FileName + " " + p.StartInfo.Arguments);
// Inicia o processo
p.Start();
// Lê linha por linha, o stream de saída
string linha;
while ((linha = p.StandardOutput.ReadLine()) != null)
{
// Mostra cada linha obtida para o usuário...
this.compilacaoWorker.ReportProgress(0, linha);
}
// Aguarda até que o processo termine sua execução...
p.WaitForExit();
}
}
Code Snippetprivate void ProgressoMudou(object sender, ProgressChangedEventArgs e)
{
// Mostra cada mensagem (linha lida) para o usuário
txtMessage.Text += e.UserState.ToString() + Environment.NewLine;
}
Code Snippetprivate void ProcessoTerminou(object sender, RunWorkerCompletedEventArgs e)
{
// Se esse método foi executado, significa que
// o Background Worker terminou o seu trabalho ;)
txtMessage.Text += ">>> Processo Terminou..." + Environment.NewLine;
}
Acredito que com o código acima dê para entender a idéia... Agora é só adaptar à sua aplicação.
Finalmente, eu te aconselho a trocar essa caixa de texto (txtMessage) por uma ListBox, ListView ou ainda GridView... Dependendo da quantidade de linhas que o seu "compile.bat" retornar, esse monte de concatenações de strings vai custar muito caro!!
Seria melhor ter uma lista e ir adicionando itens
Abraços,
Cao Proiete
-
Caio, show de bola.
Valeu mesmo pelo post.
Eu havia resolvido de uma outra forma, justamente usando threads.
Abaixo o código que inicia o processo. Observe que adicionei um handler para o evento OutputDataReceiver.
Code SnippetSystem.Diagnostics.Process p = new System.Diagnostics.Process();
p.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler(p_OutputDataReceived);
p.StartInfo.FileName = "compilar.bat";
p.StartInfo.Arguments = "\"" + this.txtAppPath.Text + "\" \"" + this.txtAppPathDest.Text + "\"";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.Start();
p.BeginOutputReadLine();
p.Close();
p.Dispose();
Code Snippetvoid p_OutputDataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e)
{
PreencherResultado(Environment.NewLine + e.Data);
}
Code Snippetprivate void PreencherResultado(string text)
{
if (this.txtMessage.InvokeRequired)
{
PreencherResultadoCallback d = new PreencherResultadoCallback(PreencherResultado);
this.Invoke(d, new object[] { text });
}
else
{
this.txtMessage.Text += text;
}
}
Declaração do delegate:
Code Snippetdelegate void PreencherResultadoCallback(string text);
Mas ainda assim, valeu pelo help.
Todas as Respostas
-
Olá Danilo,
Por padrão, quando você inicia um processo via Process.Start, é interativo... A menos que você explicitamente execute o método WaitForExit do processo que instanciou, sua aplicação funcionará normalmente, em paralelo com o processo.
Seria mais fácil te ajudar se você informar o código que utiliza para iniciar o processo e obter o retorno...
Abraços,
Caio Proiete
-
Caio,
O código eh bem simples.. Ele apenas executa uma BAT:
System.Diagnostics.Process p = new System.Diagnostics.Process();
p.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
p.StartInfo.FileName = @"compile.bat";
p.StartInfo.Arguments = "...";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
this.txtMessage.Text = ">>> Iniciando Processsamento...";
this.txtMessage.Text += Environment.NewLine + p.StartInfo.FileName + " " + p.StartInfo.Arguments;
p.Start();
this.txtMessage.Text += Environment.NewLine + ">>> Concluído";
this.txtMessage.Text += Environment.NewLine + "Mensagem do Processo:";
this.txtMessage.Text += Environment.NewLine + p.StandardOutput.ReadToEnd();
p.Close();
p.Dispose();
Não sei como capturar as mensagens que são retornadas a cada passo da execução. Por isso, na linha vermelha, coloquei para retornar toda a mensagem que é devolvida.
Além de "destravar" a aplicação, há como capturar as mensagens à medida que aparecem??
Valeu! -
Danilo, respire fundo que a resposta vai ser longa
Sua aplicação está travando porque você está lendo o stream de saída (e não o retorno! O retorno é se executou com sucesso ou não
).
Ao invés de ler o stream até o final, você pode ler linha a linha. Assim, ao invés de usar o ReadToEnd, você pode utilizar o ReadLine:
Code Snippetstring linha;
while ((linha = p.StandardOutput.ReadLine()) != null)
{
this.txtMessage.Text += Environment.NewLine + linha;
}
Isso deve fazer sua aplicação travar *menos* do que agora, mas ainda assim não resolve o problema.
A solução definitiva - e muito mais elegante - é executar o processo em uma thread separada. Existem várias formas de fazer isso, e a mais simples no seu caso (minha opinião) é utilizar o BackgroundWorker para fazer esse trabalho.
Com o BackgroundWorker você pode indicar qual método você quer que seja executado em uma thread separada, pode indicar qual método será executado para mostrar o progresso ao usuário, e ainda, indicar qual método será executado quando o processo que estava sendo executado em background terminou.
Então, o primeiro passo: Criar e configurar o Background Worker:
Code Snippetprivate BackgroundWorker compilacaoWorker = null;
private void ClickDoBotaoQueIniciaOProcesso(object sender, EventArgs e)
{
if (this.compilacaoWorker == null)
{
// Instancia um novo Background Worker (BW)
this.compilacaoWorker = new BackgroundWorker();
// Define que o BW permite informar sobre o
// progresso da execução
this.compilacaoWorker.WorkerReportsProgress = true;
// Associa o evento DoWork ao seu método que
// executa o processo
this.compilacaoWorker.DoWork +=
new DoWorkEventHandler(this.IniciarExecucao);
// Associa o evento ProgressChanges ao seu método que
// irá preencher a caixa de texto com as informações
// do processo
this.compilacaoWorker.ProgressChanged +=
new ProgressChangedEventHandler(this.ProgressoMudou);
// Associa o evento RunWorkerCompleted ao seu método
// quer irá avisar o usuário que o processo terminou
this.compilacaoWorker.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(this.ProcessoTerminou);
}
// Inicia a execução em uma nova thread...
this.compilacaoWorker.RunWorkerAsync();
}
Em seguida, implementar os métodos "IniciarExecucao", "ProgressoMudou" e "ProcessoTerminou" - note que estes nomes estão assim apenas por questões didáticas
Code Snippetprivate void IniciarExecucao(object sender, DoWorkEventArgs e)
{
System.Diagnostics.Process p = new System.Diagnostics.Process();
using (p)
{
// Tudo aquilo que você já faz para executar o processo...
p.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
p.StartInfo.FileName = @"compile.bat";
p.StartInfo.Arguments = "...";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
// Informa o usuário que o processo está iniciando...
// A chamada ao ReportProgress fará com que o método
// "ProgressoMudou" seja disparado...
this.compilacaoWorker.ReportProgress(0, ">>> Iniciando Processsamento...");
this.compilacaoWorker.ReportProgress(0, Environment.NewLine + p.StartInfo.FileName + " " + p.StartInfo.Arguments);
// Inicia o processo
p.Start();
// Lê linha por linha, o stream de saída
string linha;
while ((linha = p.StandardOutput.ReadLine()) != null)
{
// Mostra cada linha obtida para o usuário...
this.compilacaoWorker.ReportProgress(0, linha);
}
// Aguarda até que o processo termine sua execução...
p.WaitForExit();
}
}
Code Snippetprivate void ProgressoMudou(object sender, ProgressChangedEventArgs e)
{
// Mostra cada mensagem (linha lida) para o usuário
txtMessage.Text += e.UserState.ToString() + Environment.NewLine;
}
Code Snippetprivate void ProcessoTerminou(object sender, RunWorkerCompletedEventArgs e)
{
// Se esse método foi executado, significa que
// o Background Worker terminou o seu trabalho ;)
txtMessage.Text += ">>> Processo Terminou..." + Environment.NewLine;
}
Acredito que com o código acima dê para entender a idéia... Agora é só adaptar à sua aplicação.
Finalmente, eu te aconselho a trocar essa caixa de texto (txtMessage) por uma ListBox, ListView ou ainda GridView... Dependendo da quantidade de linhas que o seu "compile.bat" retornar, esse monte de concatenações de strings vai custar muito caro!!
Seria melhor ter uma lista e ir adicionando itens
Abraços,
Cao Proiete
-
Caio, show de bola.
Valeu mesmo pelo post.
Eu havia resolvido de uma outra forma, justamente usando threads.
Abaixo o código que inicia o processo. Observe que adicionei um handler para o evento OutputDataReceiver.
Code SnippetSystem.Diagnostics.Process p = new System.Diagnostics.Process();
p.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler(p_OutputDataReceived);
p.StartInfo.FileName = "compilar.bat";
p.StartInfo.Arguments = "\"" + this.txtAppPath.Text + "\" \"" + this.txtAppPathDest.Text + "\"";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.Start();
p.BeginOutputReadLine();
p.Close();
p.Dispose();
Code Snippetvoid p_OutputDataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e)
{
PreencherResultado(Environment.NewLine + e.Data);
}
Code Snippetprivate void PreencherResultado(string text)
{
if (this.txtMessage.InvokeRequired)
{
PreencherResultadoCallback d = new PreencherResultadoCallback(PreencherResultado);
this.Invoke(d, new object[] { text });
}
else
{
this.txtMessage.Text += text;
}
}
Declaração do delegate:
Code Snippetdelegate void PreencherResultadoCallback(string text);
Mas ainda assim, valeu pelo help. -
Maravilha, Rodrigo! É uma outra forma de fazer o mesmo.
Como disse acima, tenho preferência pelo BW porque acho mais claro de ler o código, e por que os eventos ProgressChanged e RunWorkerCompleted são garantidamente thread-safe e não preciso me preocupar.
Quando vejo o asynchronous pattern BeginAlgumaCoisa / EndAlgumaCoisa lembro logo do .NET 1.1 e faço cara feia
, mas isso é só preconceito da minha parte
Anyway, a titulo de curiosidade, você poderia economizar esse delegate "PreencherResultadoCallback" com um método anônimo:
O resultado vai ser o mesmo...
Code Snippetprivate void PreencherResultado(string text)
{
if (this.InvokeRequired)
{
this.Invoke((MethodInvoker)delegate { this.PreencherResultado(text); });
}
else
{
this.txtMessage.Text += text;
}
}
É, novamente uma questão de preferência.
Abraços,
Caio Proiete -
-
Sim, e não é um vício ruim.
Eu só falei no delegate para mostrar uma forma alternativa mesmo
.
Abraços,
Caio Proiete
Caio Proiete
http://www.caioproiete.com -
Opa blza..Tentei implementar a sugestão do caio, de usar o readline, to fazendo assim: (vb.net)processo = System.Diagnostics.Process.Start(ArquivoGbak, Parametros)Dim linha As String = String.EmptyDo While (linha = processo.StandardOutput.ReadLine()) """ERRO"""CaixaTexto.Text += Environment.NewLine + linhaLoopmensagem de erro:"ERRO" - StandardOut não foi redirecionado ou o processo ainda não foi iniciado.o que pode estar errado.. ??vlw.
-
Cássio wrote: Mensagem de erro: "ERRO" - StandardOut não foi redirecionado ou o processo ainda não foi iniciado.Olá Cássio,Pela mensagem de erro, parece que você não redirecionou o a saída padrão... Copiando o código lá de cima:Code Snippet// Tudo aquilo que você já faz para executar o processo...
p.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
p.StartInfo.FileName = @"compile.bat";
p.StartInfo.Arguments = "...";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
Abraços,
Caio Proiete
Caio Proiete
http://www.caioproiete.com -
Vlw pelo retorno Caio.Acontece assim,na verdade não estou executando um bat e sim um EXE que passo parametros. É um programa de bkp do firebird.. o gbak.Acontece que consigo executar normalmente, só que ele aparece um prompt de comando com os resultados.... quero dar um feedback meu para o usuário e tirar esse prompt.a maneira que estou vendo que dá pra fazer é usando o process..minha dúvida é se funciona neste caso... porque não tenho um bat e sim um exe...vlw.
-
Cássio wrote: Vlw pelo retorno Caio. na verdade não estou executando um bat e sim um EXE que passo parametros. É um programa de bkp do firebird.. o gbak.Acontece que consigo executar normalmente, só que ele aparece um prompt de comando com os resultados.... quero dar um feedback meu para o usuário e tirar esse prompt.a maneira que estou vendo que dá pra fazer é usando o process..minha dúvida é se funciona neste caso... porque não tenho um bat e sim um exe...Olá Cássio,
Eu não conheço o gbak, mas se esse executável for uma aplicação do tipo Console, irá funcionar sem problemas. Neste tópico existem duas formas de fazer... A forma inicial, que eu adaptei do código do Danilo, e a solução final que o Danilo implementou.
Se você seguir direitinho os passos de uma dessas soluções, você deve conseguir executar sua aplicação e ler o retorno sem problemas.
Se quiser dar uma olhada em outros exemplos, existem muitos outros tópicos aqui no fórum sobre esse assunto. Alguns exemplos:
Rotina para iniciar uma aplicação java
http://forums.microsoft.com/msdn-br/ShowPost.aspx?PostID=1024203&SiteID=21IP Address
http://forums.microsoft.com/msdn-br/ShowPost.aspx?PostID=609745&SiteID=21Pegar Resposta do Prompt de Comando com C#
http://forums.microsoft.com/msdn-br/ShowPost.aspx?PostID=3723793&SiteID=21Executar arquivo bat em gridview
http://forums.microsoft.com/msdn-br/ShowPost.aspx?PostID=3959053&SiteID=21Sugiro que você faça mais algumas tentativas, seguindo os exemplos deste e/ou de outros tópicos, e se ainda assim não conseguir, abra um novo tópico detalhando o código que você desenvolveu, com as devidas mensagens de erro, e problemas encontrados.
Abraços,
Caio Proiete
Caio Proiete
http://www.caioproiete.com -