Auto-Atualização de Aplicativos em Delphi

By: Ricardo Carvalho

Abstract: Desenvolvimento de um componente para atualização e distribuição automáticas de aplicações

Cenário inicial

Creio que muitos desenvolvedores Delphi já passaram pela situação de ter aplicativos instalados em diversas máquinas em um ambiente corporativo. Normalmente o programa começa com uma pequena base de usuários e, com o tempo, mais pessoas passam a utilizá-lo. Se por um lado isso é um sinal do sucesso do software, por outro, com o aumento da base de usuários começam a surgir novos requisitos e descobertas de bugs que culminam com a necessidade de gerar novas versões do aplicativo. E uma tarefa das mais desagradáveis, além de tomar muito tempo, é sair de máquina em máquina atualizando aplicativos. Em grandes corporações, aplicativos podem estar instalados inclusive em outras cidades ou estados. Deixar esta tarefa para os usuários é boa receita para dores de cabeça. Com certeza, com esta política, não dá pra se considerar que todos farão as atualizações.

A solução para este problema, em um ambiente de rede, é fazer com que o software se auto-atualize. No entanto esta tarefa não é das mais triviais. A primeira solução que desenvolvi neste sentido me custou uns dois meses para se tornar estável (lógico que não fiquei este tempo só por conta dessa atividade). Há questões um tanto complicadas envolvidas como, por exemplo, a dificuldade de fazer debug em equipamentos remotos, as diferenças de comportamento entre versões diferentes do Windows, a estratégia para fazer com que um software possa substituir seu próprio arquivo, entre outras. Se isso vale a pena para um aplicativo importante, é desanimador quando pensamos em softwares mais triviais.

Assim, após desenvolver uma solução de auto-update para um aplicativo, foi natural concluir que esta funcionalidade deveria idealmente ficar em um componente para que pudesse ser facilmente reaproveitada. Afinal trata-se de uma necessidade comum à maioria dos programas desenvolvidos em um ambiente corporativo. Portanto, investi algum tempo na elaboração do componente TAutoUpdate. O resultado prático foi tão satisfatório, para mim assim como para diversos colegas, que me motivou a escrever o presente artigo.


Pressupostos

O pressuposto básico, é óbvio, é a disponibilidade de um ambiente de rede. No entanto, é bom esclarecer que a versão apresentada do componente foi projetada para uma rede LAN de bom desempenho. Ainda não é uma boa solução para conexões de baixa velocidade ou em ambiente Web, pois o programa principal congela durante o download. Para redes de baixa velocidade o download deveria ser realizado em uma thread distinta, o que implicaria em uma série de alterações no projeto.

Considera-se, ainda, que o software será distribuído a partir de um servidor FTP. Uma vez que o arquivo executável esteja disponível no servidor, sua distribuição será automática. Por ser muito oportuno, o componente também é capaz de fazer o upload da aplicação para o servidor quando o desenvolvedor libera uma nova versão. Para tanto, o servidor deverá permitir operações de escrita ao usuário configurado no componente.

Na implementação proposta, entende-se que a atualização do programa consiste na simples substituição do executável pelo arquivo da nova versão, de nome idêntico, não sendo realizado nenhum procedimento adicional de instalação. Não é difícil, entretanto, fazer alterações para a inclusão de funcionalidades semelhantes.

Embora o componente não faça nenhuma pressuposição a respeito da aplicação (não impõe requisitos à aplicação), é necessário que haja algum mecanismo de identificação e comparação das versões do executável. Assim, esta tarefa pode ser personalizada pelo aplicativo, mas se as informações de versão forem habilitadas (em Project/Options), o componente será capaz de realizá-la sem necessidade de codificação via aplicação. Nesse caso, o esquema de identificação de versões será o tradicional de quatro números separados por pontos.

O componente

Sem mais delongas, passemos então ao componente. Para facilitar a exposição, irei apresentar recortes de código sem a implementação; basicamente para apresentar a interface. As propriedades estão no ponto em que teclaríamos Ctrl+Shift+C para que o Delphi completasse o código. Ao final do artigo, disponibilizo um link para o código fonte completo.

Faz sentido manter o Client de FTP no componente, já que a utilização do protocolo, a princípio, não diz respeito à aplicação e sim à tarefa de auto-atualização. Portanto, iremos encapsular um TIdFTP. Optamos por utilizar a versão 10 dos componentes Indy. Para a versão anterior, há uma ligeira diferença na codificação, mas não iremos abordá-la aqui, por não ser nada substancial.


  TAutoUpdate = class(TComponent)
  private
    Client: TIdFTP;
    AntiFreeze: TIdAntiFreeze;
  public
  published
    property FTPHost: string;
    property FTPUser: string;
    property FTPPassword: string;
    property FTPDir: string;
    property FTPPassive: Boolean;
  end;

Seria natural, nesse ponto, imaginar que deveríamos sobrepor o construtor para instanciar o Client FTP. No entanto, há uma particularidade: espera-se que os aplicativos sejam atualizados apenas ocasionalmente e, portanto, a instanciação seria inútil à maioria das vezes. Vamos então, deixar para instanciar o componente interno quando concluirmos pela sua necessidade. O que podemos adiantar é o método privado para executar esta tarefa, assim como o destrutor que fará a limpeza de contrapartida.

procedure TAutoUpdate.CreateClient;
begin
  if Client = nil then
  begin
    AntiFreeze := TIdAntiFreeze.Create(Self);
    Client     := TIdFTP.Create(Self);
  end;
end;


destructor TAutoUpdate.Destroy;
begin
  if Client <> nil then
  begin
    Client.Free;
    AntiFreeze.Free;
  end;
  inherited;
end;

Quando a atualização for iniciar é conveniente apresentar uma mensagem informando a ocorrência ao usuário. Vamos criar uma propriedade para conter o texto dessa mensagem. Seu valor inicial pode ser configurado no construtor da classe. Vamos ainda, considerar que a atualização poderá ser opcional ou obrigatória, e criar uma propriedade para ajuste desse comportamento.

…
published
  property UpdateMessage: string;
  property OptionalUpdate: Boolean; 
…
end;
…
constructor TAutoUpdate.Create(AOwner: TComponent);
begin
  inherited;
  UpdateMessage :=
    'Há uma nova versão do aplicativo disponível.'#13 +
    'A atualização automática será iniciada.';
end;

Uma questão central no que diz respeito a este componente, é a questão do estilo de identificação das versões. Isso é importante porque é comparando a versão do executável corrente com o disponível é que será possível saber se a operação a realizar deve ser de atualização, de distribuição ou nenhuma. Uma boa alternativa é adotar o estilo padrão com quatro números separados por pontos. No entanto, há desenvolvedores que preferem trabalhar com um ou dois números, outros com uma data, outros com textos (personal, professional, entreprise) etc. Se, por um lado, não queremos impor um estilo em particular, por outro, achamos que o componente deve oferecer algum tratamento com essa finalidade. Assim, optamos por adotar o estilo padrão, mas com a possibilidade de personalização. Como há um momento em que as versões devem ser comparadas, criamos um evento que o desenvolvedor pode ignorar, assumindo assim o tratamento padrão, ou escrever um manipulador para impor uma regra de comparação personalizada.

type
  TCompareVersions = procedure(Sender: TObject; ExeVersion,
    DeployVersion: string; var DeployIsLatest: Integer) of object;

TAutoUpdate = class(TComponent)
…
Published
  property OnCompareVersions: TCompareVersions;
…
end;

Se o parametro var DeployIsLatest for maior que zero, a aplicação deve ser atualizada, se for menor que zero deve ser distribuída (é o caso em que o desenvolvedor está liberando uma versão para deploy e quer fazer o upload para o servidor FTP) e, se for igual a zero nada deve ser feito (as versões são as mesmas).

Ainda temos outra questão com relação à identificação da versão. Precisamos conhecer a versão disponível no servidor para fazer a comparação. Claro que não faz sentido baixar o arquivo só pra ver a versão. Então optei por deixar esta verificação a cargo da aplicação. Usualmente, basta gravar esta informação no banco de dados, o que torna a tarefa muito simples para o aplicativo. Fazer isso no componente exigiria protocolos adicionais ou acesso ao banco, o que não é (nem deveria ser) do conhecimento desta camada. Para isso criamos mais um evento.

Type
  TNeedVersion = procedure (
    Sender: TObject; var DeployVersion: string) of object;

TAutoUpdate = class(TComponent)
…
Published
  property OnNeedVersion: TNeedVersion;
…
end;

Para facilitar a eventual gravação da versão atual no banco de dados ou outra utilização que o desenvolvedor possa ter, disponibilizamos, ainda, uma propriedade de só leitura.

property ExeVersion: string read GetExeVersion;

O último evento disponibilizado pelo componente é disparado quando o componente conclui que a versão corrente aplicação é mais recente que a versão do servidor de FTP. Talvez pareça um pouco estranho, afinal como a aplicação poderia ser mais nova que a do servidor? Na verdade é simples: quando o desenvolvedor gera uma nova versão. Assim, esse evento é um ótimo facilitador para programar a distribuição do novo arquivo. Basta alterar o número da versão e o programa pode fazer automaticamente o upload e, se for o caso, gravar no banco de dados a identificação de versão do arquivo.

  property OnNeedDeploy: TNotifyEvent;

Bom, até agora está muito bonito, mas como o processo é iniciado? Precisamos de um método de tempo de execução que acione a verificação e orquestre todo o processo. Este método, chamamos de Execute. Criamos ainda us métodos Update e Deploy, que fazem respectivamente o download e o upload a partir do servidor de FTP.

…
protected
  procedure Update;
public
  procedure Execute;
  procedure Deploy;
…

Vejamos a procedure Execute:

procedure TAutoUpdate.Execute;
var
  VersaoExecutavel, VersaoDisponivel: string;
  i: integer;
  botoes: TMsgDlgButtons;
begin
  if not Assigned(FOnNeedVersion) then
    raise Exception.Create(
      'O manipulador do evento OnNeedVersion é obrigatório.'
    );
  VersaoDisponivel := '';
  FOnNeedVersion(Self, VersaoDisponivel);
  if VersaoDisponivel = '' then
    raise Exception.Create('Versão disponível inválida (vazia).');
  VersaoExecutavel := VersaoExe;
  if VersaoExecutavel = '' then
    VersaoExecutavel := '1.0.0.0';

  i := CompareVersion(VersaoDisponivel, VersaoExecutavel);
  if Assigned(FOnCompareVersions) then
    FOnCompareVersions(Self, VersaoExecutavel, VersaoDisponivel, i);
  if i > 0 then
  begin
    botoes := [mbOK];
    if OptionalUpdate then
      Include(botoes, mbCancel);
    if MessageDlg(UpdateMessage, mtInformation, botoes, 0) = mrOk then
      Update;
  end
  else if (i < 0) and Assigned(FOnNeedDeploy) then
    FOnNeedDeploy(Self);
end;

Inicialmente nos certificamos que exista um manipulador para o evento OnNeedVersion, levantando uma exceção em caso negativo. Este manipulador deve fornecer o valor da versão disponível no servidor e, portanto, é obrigatório.

A seguir obtemos a versão atual através da função VersaoExe (link para fontes completos ao final do artigo) e a comparamos com a versão disponível. Se houver um manipulador para o evento OnCompareVersions, este poderá alterar o resultado da comparação padrão.

Finalmente, se a versão disponível for a mais recente iniciamos o procedimento de Update. Para isso, enviamos uma mensagem para o usuário avisando que a versão será atualizada. Esta mensagem terá um botão OK e, se OptionalUpdate for verdadeiro, um botão Cancel para que o usuário possa cancelar o procedimento. Se a versão atual for mais recente que a disponível, disparamos o evento OnNeedDeploy. Optamos por deixar a decisão quanto a realizar a distribuição da nova versão a cargo do manipulador de evento. Além disso, disponibilizamos um método Deploy que pode ser chamado neste manipulador.

O próximo método a comentar é o Update, entendo que o mais importante do componente, pois realiza a tarefa para o qual o mesmo foi desenvolvido.

procedure TAutoUpdate.Update;
var
  tempFile, NomeExe, batchName, NomeDos: string;
  lista: TStringList;
  existe: Boolean;
begin
  if FTPHost = '' then
    raise Exception.Create('FTPHost não definido');
  CreateClient;
  Client.Host     := FTPHost;
  Client.Username := FTPUser;
  Client.Password := FTPPassword;
  Client.Passive  := FTPPassive;

  if not Client.Connected then
    Client.Connect;
  if not Client.Connected then
    raise Exception.Create('Erro na conexão com o servidor de FTP');
  Client.ChangeDir(FTPDir);

  // verificar disponibilidade do arquivo no servidor
  NomeExe := ExtractFileName(Application.ExeName);
  lista := TStringList.Create;
  frmAtualizando := TfrmAtualizando.Create(Self);
  try
    Client.TransferType := ftASCII;
    Client.List(lista, NomeExe, False);
    existe := (lista.Count > 0) and
              (UpperCase(lista[0]) = UpperCase(NomeExe));
    if not existe then
      raise Exception.Create('Arquivo não disponível no servidor FTP.');

    // Exibir transferência para o usuário
    Client.TransferType := ftBinary;
    BytesToTransfer := Client.Size(NomeExe);
    frmAtualizando.Show;

    // baixar arquivo temporário
    tempFile := GetTmpDir + ChangeFileExt(NomeExe, '.tmp');
    Client.Get(NomeExe, tempFile, True);
    Client.Disconnect;
    if not FileExists(tempFile) then
      exit;

    // criar bath e sobrepor exe
    NomeDos := ExtractShortPathName(ParamStr(0));
    lista.Clear;
    batchname := GetTmpFileName('.bat');
    FileSetAttr(ParamStr(0), 0);
    lista.Add(':Label1');
    lista.Add('@echo off');
    lista.Add('del ' + NomeDos);
    lista.Add('if Exist ' + NomeDos + ' goto Label1');
    lista.Add('Move ' + tempFile + ' ' + NomeDos);
    lista.Add('Call ' + NomeDos);
    lista.Add('del ' + batchname);
    lista.SaveToFile(batchname);
    ChDir(GetTmpDir);
    WinExec(PChar(batchname), SW_HIDE);
  finally
    lista.Free;
    FreeAndNil(frmAtualizando);
    Application.Terminate;
  end;
end;

Inicialmente, procedemos à criação, configuração e conexão do client FTP. Em seguida verificamos a existência no servidor de um arquivo com o mesmo nome da aplicação. Se tudo estiver certo até aqui, podemos iniciar o download. Para dar um feedback ao usuário, utilizamos um formulário a parte (frmAtualizando), que encapsula um Gauge e será atualizado pelo evento FTPWork do IdFTP. Para isso, alteramos o método CreateCliente descrito anteriormente adicionando os manipuladores de evento:

…
    Client.OnWorkBegin := FTPWorkBegin;
    Client.OnWork      := FTPWork;
…

procedure TAutoUpdate.FTPWorkBegin(Sender: TObject; AWorkMode: TWorkMode;
  AWorkCountMax: Integer);
begin
  if AWorkCountMax > 0 then
    frmAtualizando.Max := AWorkCountMax
  else
    frmAtualizando.Max := BytesToTransfer;
end;

procedure TAutoUpdate.FTPWork(Sender: TObject; AWorkMode: TWorkMode;
  AWorkCount: Integer);
begin
  frmAtualizando.Position := AWorkCount;
end;

Em seguida, fazemos o download salvando o arquivo no diretório temporário do sistema. Neste momento surge a questão mais complicada do procedimento. Nós teríamos, neste ponto, que sobrepor o executával da aplicação. Mas como fazer isso se a mesma está rodando? O Sistema Operacional não permitiria sobrepor um arquivo em uso. A solução, um tanto tortuosa, é verdade, foi criar um arquivo batch com esta finalidade. O componente cria e aciona o script que irá entrar em looping até conseguir apagar o arquivo executável. Como a aplicação terminará em seguida, o script sairá do looping e realizará as tarefas subsequentes que são mover o arquivo temporário, executá-lo e, por fim deletar seu próprio arquivo (sim, arquivos batches podem fazer isso, provavelmente porque são inteiramente carregados para a memória antes da execução).

O proximo método, o Deploy, é similar ao anterior, porém faz um updolad em vez de um download, e não precisa fazer toda essa ginástica para sobrepor o arquivo.

procedure TAutoUpdate.Deploy;
var
  NomeExe: string;
begin
  if FTPHost = '' then
    raise Exception.Create('FTPHost não definido');
  CreateClient;
  Client.Host     := FTPHost;
  Client.Username := FTPUser;
  Client.Password := FTPPassword;
  Client.Passive  := FTPPassive;

  if not Client.Connected then
    Client.Connect;
  if not Client.Connected then
    raise Exception.Create('Erro na conexão com o servidor de FTP');
  Client.ChangeDir(FTPDir);

  NomeExe := Application.ExeName;
  frmAtualizando := TfrmAtualizando.Create(Self);
  try
    Client.TransferType := ftBinary;
    BytesToTransfer := FileLength(NomeExe);
    Client.Put(NomeExe, ExtractFileName(NomeExe));
    frmAtualizando.Show;
    Client.Disconnect;
  finally
    FreeAndNil(frmAtualizando);
    Screen.Cursor := crDefault;
  end;
  ShowMessage('Deploy finalizado.');
end;

Conclusão

O componente TAutoUpdate tem se revelado muito útil no dia-a-dia. Com ele, em questão de instantes conseguimos configurar uma aplicação que rode em um ambiente de rede para que seja auto-autualizável e auto-distribuível.

Como sugestão de desenvolvimento, aponto a melhoria do componente para que o mesmo possa operar adequadamente no ambiente Internet. Outra melhoria seria ter a opção de, após o download, perguntar ao usuário se a aplicação deve ser atualizada imediatamente ou apenas na próxima execução. Isso seria importante em aplicações que possam ter dados não salvos.

Claro, gostaria de receber qualquer melhoria porventura implementada.


Fontes

Os fontes completos do componente estão disponíveis em:

http://www.dbquester.com/files/artigos/autoupdate/autoupdate.rar


Server Response from: ETNASC04