Impedindo duplicatas do mesmo programa

Um problema comum para ser resolvidos em desktops é que evitar que o usuário carregue o mesmo programa diversas vezes. Neste artigo, vamos entender melhor este tipo de problema.

ENTENDENDO O PROBLEMA

Quando um usuário não se apercebe, mas carrega mais uma vez um programa que já tem na memória isso é ruim em vários aspectos: (1) é mais consumo de memória e recursos desnecessariamente carregados; (2) isso é um ruim especialmente quando uma conexão com o banco de dados está envolvida, pois cada programa trará um contexto transacional que concorreá com a outra instância do mesmo programa e se houver operações bloqueantes, outros usuários e si mesmo poderão ficar esperando indefinidamente pelo recurso ser liberado.

Então não há duvidas de que bloquear uma segunda instância do mesmo programa é o correto a ser feito. Há vários métodos para isso, mas qual deles usar?

Muitos programadores usam um método bastante simples, observam se já há um programe.exe carregado e se positivo então decide abortar a carga. Um problema nessa abordagem é que se o sistema operacional que estamos executando o programa for multi-terminal ou multi-usuário então outros usuários não conseguirão carregar o mesmo programa. O que desejamos bloquear é a duplicata advindo do mesmo usuário e não de usuários diferentes.

Se a palavra “multi-terminal” lhe soou estranha, basta entender que computadores atuais podem permitir que vários usuários remotos possam acessar um computador central e dalí rodar seus programas. O Windows Terminal Server(RDS), TS Plus ou GoGlobal são exemplos de softwares populares que permitem transformar um Windows num ambiente multi-terminal para acessos locais ou remotos.

Qualquer que seja a solução, precisaremos lidar com o fato em que o mesmo programa.exe pode estar carregado em multiplas instâncias, apenas não devem ser do mesmo usuário. Isso é imprescindivel para que seu programa possa funcionar em ambientes multi-usuário e multi-terminal.

Uma outra abordagem que programadores costumam usar é uma função da WinAPI intitulada FindWindow, ela basicamente procura por uma janela(classe) com um certo nome. Ela irá resolver o problema, mas tem dois problemas (1) custo da operação, pois o Windows carrega muitos programas e logo tem muitas classes e o usuário pode vir a carregar muitos outros programas e ao submeter um FindWindow na raiz do desktop dele então levará tempo considerável encontrar uma em específico e outros problema (2) é que uma janela(classe) de um programa que não tem nada a ver com o seu tenha o mesmo nome e isso fará com que o seu programa nunca seja executado sob esta situação.

LIDANDO COM O PROBLEMA

Usando com o Delphi e o FreePascal podemos lidar com o problema usando MUTEX. MUTEX para efeito prático é similar a criar constantes no formato texto visiveis para outros programas enxergaem na mesma sessão(desktop) do usuário, mas que apenas existe enquanto o programa que os criou ainda existir, quando o programa morre, estas constantes que o programa criou vão embora também.

A solução portanto é testar a existencia de um MUTEX com um nome de referencia – e em nosso exemplo vamos usar como referencia o titulo do formulário principal – e se ela existir então não prosseguiremos porque consideraremos que já existe uma instancia do programa e opcionalmente talvez apresentemos uma mensagem de erro.

Diferentemente no FindWindow, o MUTEX será instantaneo. Isso resolverá o problema porque cada execução seguinte encontrará o nome de referencia. O bom dessa solução é que ela funcionará em ambientes multi-terminais e muulti-terminal por causa do comportamento do MUTEX.

Mas é possível acrescentar um bonus, caso o MUTEX seja detectado podemos usar o FindWindow para encontrar a instancia previamente carregada e restaurá-la para a tela, afinal talvez esteja minimizado e o usuário não conseguiu notá-lo. Aqui vai o alerta, lembre-se de que o FindWindow pode demorar então só use-o no exemplo abaixo se a performance não for um quesito importante comparado a facilidade do programa se auto-expor.

O codigo a seguir ficará no arquivo .dpr no caso de usarmos o Delphi ouu no arquivo .lpr no caso de estarmos programando em Lazarus+FPC, vamos ao código:

program Programa;

(...)

uses
  (...)  
  Forms,
  Windows;

{$R *.res}

var
  MutexHnd: Cardinal;
  Handle: HWND;
  sTitle:String;
begin
  // unit Windows é requerida por causa do FindWindow, IsWindowVisible
  sTitle:='Meu Titulo'
  RequireDerivedFormResource:=True;  // Lazarus+FPC only
  Application.Scaled:=True; // Lazarus+FPC Only

  MutexHnd := CreateMutex(nil, False, pChar(sTitle));
  If GetLastError = ERROR_ALREADY_EXISTS Then
  begin
    //TfmPrincipal é a classe do form principal e
    // 'Meu Titulo' é o caption do form principal
    // Não funcionará em tempo de design porque a janela(caption) que procuro
    // para todas as hipoteses testadas esta visivel por causa da IDE.
    Handle := FindWindow(nil, pChar(sTitle));
    if Handle <> 0 then
    begin
      //Application.MessageBox(
      //  pChar(sTitle+' já está em execução, observe a bandeja do sistema.'+sLineBreak+
      //  'Se fez o logout nos ultimos instantes, '+
      //  'aguarde uns 60 segundos para carregar-me novamente.'),
      //  pChar('Ops, já estou carregado:')
      //  );
      if not IsWindowVisible(Handle) then
        ShowWindow(Handle, SW_RESTORE);
      SetForegroundWindow(Handle);
      Application.Terminate;
    end
  end
  else
  begin
    Application.Initialize;
    Application.Title:=sTitle;
    Application.ShowMainForm:=True;  // Delphi only
    Application.MainFormOnTaskbar := True;  // Delphi only
    Application.CreateForm(TfmPrincipal, fmPrincipal);
    fmPrincipal.Caption:=sTitle;  // não mude o caption depois
    Application.Run;
  end;
end.  

O código acima é justamente colocado no carregamento do projeto, isto é, no arquivo .dpr no caso do Delphi ou .lpr no caso do Lazarus+FPC.

As linhas marcadas como ‘Delphi only’ deverão ser removidas caso você esteja usando o Lazarus e as linhas comentadas como ‘Lazarus+fpc’ você as remove se estiver usando Delphi.

CONCLUSÃO

Assim temos um código plenamente funcional. Existem por aí outros códigos diferentes, mas alguuns deles esbarram no problema comum já citado, não sabem lidar com ambientes multi-tarefas ou multi-usuários e o nosso codigo foi testado neste contexto.