none
Executar Shell sem travar aplicação RRS feed

  • 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!
    terça-feira, 9 de setembro de 2008 14:45

Respostas

  • Danilo, respire fundo que a resposta vai ser longa Smile

     

    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 Wink).

     

    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 Snippet

     

    string 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 Snippet

     

    private 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 Smile Smile

     

    Code Snippet

     

    private 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 Snippet

     

    private void ProgressoMudou(object sender, ProgressChangedEventArgs e)

    {

    // Mostra cada mensagem (linha lida) para o usuário

    txtMessage.Text += e.UserState.ToString() + Environment.NewLine;

    }

     

     

     

    Code Snippet

     

    private 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 Wink

     

    Abraços,

    Cao Proiete

    quarta-feira, 10 de setembro de 2008 10:47
    Moderador
  • 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 Snippet

    System.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();


    Agora a implementação do Handler:

    Code Snippet

    void p_OutputDataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e)
    {
        PreencherResultado(Environment.NewLine + e.Data);
    }


    Como a comunicação é entre threads não era possível que a thread aberta pelo "Process" acessasse o componente do Form. Sendo assim, foi criado um Workaround.

    Code Snippet

    private void PreencherResultado(string text)
    {
        if (this.txtMessage.InvokeRequired)
        {
            PreencherResultadoCallback d = new PreencherResultadoCallback(PreencherResultado);
            this.Invoke(d, new object[] { text });
        }
        else
        {
            this.txtMessage.Text += text;
        }
    }


    O método PreencherResultado implementa um Thread Safe. No primeiro momento verifica se existe necessidade de "invocar" o controle "txtMessage". Em caso positivo, é porque ele está em thread diferente. Então instacia-se o delegate (descrito logo abaixo) e invoca o formulário passando o delegate instanciado e os valores a serem preenchidos. No segundo momento, é verificado que não há necessidade de "invocar" a threrad (porque já está na original) então apenas atribui o valor ao controle.

    Declaração do delegate:

    Code Snippet

    delegate void PreencherResultadoCallback(string text);


    Acho que ficou legal...

    Mas ainda assim, valeu pelo help.
    quarta-feira, 10 de setembro de 2008 12:36

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

    terça-feira, 9 de setembro de 2008 15:29
    Moderador
  • 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!
    terça-feira, 9 de setembro de 2008 18:29
  • Danilo, respire fundo que a resposta vai ser longa Smile

     

    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 Wink).

     

    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 Snippet

     

    string 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 Snippet

     

    private 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 Smile Smile

     

    Code Snippet

     

    private 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 Snippet

     

    private void ProgressoMudou(object sender, ProgressChangedEventArgs e)

    {

    // Mostra cada mensagem (linha lida) para o usuário

    txtMessage.Text += e.UserState.ToString() + Environment.NewLine;

    }

     

     

     

    Code Snippet

     

    private 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 Wink

     

    Abraços,

    Cao Proiete

    quarta-feira, 10 de setembro de 2008 10:47
    Moderador
  • 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 Snippet

    System.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();


    Agora a implementação do Handler:

    Code Snippet

    void p_OutputDataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e)
    {
        PreencherResultado(Environment.NewLine + e.Data);
    }


    Como a comunicação é entre threads não era possível que a thread aberta pelo "Process" acessasse o componente do Form. Sendo assim, foi criado um Workaround.

    Code Snippet

    private void PreencherResultado(string text)
    {
        if (this.txtMessage.InvokeRequired)
        {
            PreencherResultadoCallback d = new PreencherResultadoCallback(PreencherResultado);
            this.Invoke(d, new object[] { text });
        }
        else
        {
            this.txtMessage.Text += text;
        }
    }


    O método PreencherResultado implementa um Thread Safe. No primeiro momento verifica se existe necessidade de "invocar" o controle "txtMessage". Em caso positivo, é porque ele está em thread diferente. Então instacia-se o delegate (descrito logo abaixo) e invoca o formulário passando o delegate instanciado e os valores a serem preenchidos. No segundo momento, é verificado que não há necessidade de "invocar" a threrad (porque já está na original) então apenas atribui o valor ao controle.

    Declaração do delegate:

    Code Snippet

    delegate void PreencherResultadoCallback(string text);


    Acho que ficou legal...

    Mas ainda assim, valeu pelo help.
    quarta-feira, 10 de setembro de 2008 12:36
  • 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 Smile, mas isso é só preconceito da minha parte Smile Smile

     

    Anyway, a titulo de curiosidade, você poderia economizar esse delegate "PreencherResultadoCallback" com um método anônimo:

     

    O resultado vai ser o mesmo...

    Code Snippet

     

    private 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

    quarta-feira, 10 de setembro de 2008 13:53
    Moderador


  • O delegate foi só pra padronizar mesmo.
    Estou acostumado a desenvolver componentes, daí o "vício".

    Valeu Caio.
    quarta-feira, 24 de setembro de 2008 14:03
  • Sim, e não é um vício ruim.

     

    Eu só falei no delegate para mostrar uma forma alternativa mesmo Wink.

     

    Abraços,

    Caio Proiete




    Caio Proiete
    http://www.caioproiete.com
    quarta-feira, 24 de setembro de 2008 14:24
    Moderador
  • 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.Empty         

                Do While (linha = processo.StandardOutput.ReadLine()) """ERRO"""
                    CaixaTexto.Text += Environment.NewLine + linha
                Loop


    mensagem de erro:
    "ERRO" - StandardOut não foi redirecionado ou o processo ainda não foi iniciado.

    o que pode estar errado.. ??

    vlw.
    segunda-feira, 29 de dezembro de 2008 16:53
  •  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
    segunda-feira, 29 de dezembro de 2008 17:43
    Moderador
  • 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.
    segunda-feira, 29 de dezembro de 2008 19:44
  •  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=21

     

    IP Address
    http://forums.microsoft.com/msdn-br/ShowPost.aspx?PostID=609745&SiteID=21

     

    Pegar Resposta do Prompt de Comando com C#
    http://forums.microsoft.com/msdn-br/ShowPost.aspx?PostID=3723793&SiteID=21

     

    Executar arquivo bat em gridview
    http://forums.microsoft.com/msdn-br/ShowPost.aspx?PostID=3959053&SiteID=21

     

     

    Sugiro 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
    segunda-feira, 29 de dezembro de 2008 22:38
    Moderador
  • Oi Caio.
    Vou tentar aqui. Muito obrigado pelo retorno cara.

    Abraço!

    terça-feira, 30 de dezembro de 2008 13:04