3 de setembro de 2010

Criando Serviços que permitam várias instalações num mesmo computador

Como parte de sua solução de ERP Omega, a ABC71 entrega a seus Clientes um Serviço do Windows para execução de processos de forma agendada. Isto é, o Cliente dita a frequência e o horário mais apropriado e o Serviço, que pode ser instalado em outro computador, se encarrega da execução na data e hora indicada.

A maioria de nossos Clientes precisa gerenciar apenas um banco de dados, mesmo quando há mais de uma planta envolvida já que cada uma controla seu próprio banco. Essa característica permitiu ao Serviço descrito no parágrafo anterior atender muito bem a necessidade de agendamento desses Clientes. A situação mudou quando em alguns Clientes se fez necessário monitorar de forma centralizada as execuções em mais de um banco simultaneamente, cada um deles representando uma filial distinta do mesmo Cliente.

Um solução óbvia é fazer com que o mesmo Serviço do Windows seja instalado mais de uma vez e, então, cada instalação cuida de um banco de dados diferente.

Implementar essa solução num programa feito em C++ Builder ou Delphi é relativamente simples, exigindo duas alterações na forma de se registrar o serviço. Uma vez que o Windows não aceita a existência simultânea de serviços com um mesmo nome interno, a primeira modificação é fazer com que cada instalação seja feita com um nome diferente. A segunda modificação no programa diz respeito a como informar ao Serviço qual é o banco de dados que ele deve usar. A resposta curta para isso é passar a informação como um parâmetro na linha de comando registrada para o Serviço.

A linha de comando nada mais é que o nome do programa (com o caminho e a extensão EXE) seguido de valores separados por espaços em branco. Tais valores são repassados para o programa quando ele é executado, de forma que é possível lê-los e utilizá-los conforme a necessidade. A linha de comando de um serviço pode ser alterada após sua instalação através da função ChangeServiceConfig da API do Windows. Então, nós a chamaremos no evento AfterInstall do TService:
void __fastcall TSchedService::ServiceAfterInstall(TService *Sender)
{
SC_HANDLE mh = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (mh != NULL)
{
{ Abre o serviço com o Nome calculado para distinguir instâncias que apontem para bancos de dados diferentes. }
SC_HANDLE sh = OpenService(mh, Name.c_str(), SC_MANAGER_ALL_ACCESS);
if (sh != NULL)
{
{ Incluir como parâmetro de execução do serviço o nome da configuração que deve ser usada, conforme definido pelo Configurador do Scheduler }
AnsiString lParam = "USE_ALIAS=\"" + _CfgName + "\"";
AnsiString lPath = System::ParamStr (0) + " " + lParam;

ChangeServiceConfig(sh, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
SERVICE_NO_CHANGE, lPath.c_str(), NULL, NULL,
NULL, NULL, NULL, NULL);

CloseServiceHandle (sh);
}
CloseServiceHandle (mh);
}
};

A expressão System::ParamStr (0) dá acesso ao caminho completo do executável para o Serviço, isto é, do programa que está atualmente em execução. Veja que foi criado um par de valores no formato NOME=VALOR, que é acrescentado ao executável. Durante a execução do serviço eu consigo obter essa informação e extrair o valor. Segue o código para realizar essa extração em C++ Builder - no Delphi, varia apenas a sintaxe:
AnsiString __fastcall TSchedService::ExtractCfgName (void)
{
bool lOk = false;
AnsiString lNomeParam ("USE_ALIAS="), ret;
int i = 1, lCompr = lNomeParam.Length();

{ Percorre os parâmetros da linha de comando para encontra o que nos interessa }
while ((i <= System::ParamCount()) && (lOk == false))
{
ret = System::ParamStr (i);

if (ret.SubString (1, lCompr) == lNomeParam)
{
lOk = true;
{ Extrai apenas o valor após o sinal de igual }
ret = ret.SubString (lCompr+1, ret.Length ());
}
i ++;
}

if (! lOk)
ret = "";
return (ret);
};

No meu caso, o valor extraído representa uma chave do Registry onde gravei com antecedência toda a configuração para acessar o banco de dados correto mas poderia representar um nome de arquivo ou uma tag num XML, por exemplo. Em última análise, eu poderia ter passado todas as informações necessárias para a conexão com essa técnica mas isso tornaria mais complicado dar manutenção no programa, exigindo atenção especial à passagem dos parâmetros e ao limite de tamanho da linha de comando (normalmente, 256 caracteres).

Serviços do Windows têm um nome interno usado pelo sistema operacional como identificador único e um nome externo usado como uma descrição a ser apresentada ao usuário. Tanto o C++ Builder quanto o Delphi usam como nome interno do Serviço o valor da propriedade Name do componente TService. Por isso, é preciso modificar esse valor antes do programa ter a oportunidade de registrar o Serviço ou realizar qualquer outra operação que dependa do nome. Um bom lugar é o próprio construtor do TService.

Uma implicação importante ao se usar esse método é que alterar o nome de um componente nos força a respeitar as regras de nomeação do ambiente. Ou seja, o nome não pode ser iniciado por um número, não pode apresentar espaços em branco nem caracteres que não existam na língua inglesa, como caracteres acentuados (agudo, circunflexo, til, trema, crase) ou o cê cidilha. Podemos usar o próprio nome da configuração obtido anteriormente para diferenciar cada instância instalada. Para que isso funcione, teremos que preparar o nome obtido para garantir sua validade, substituindo os caracateres que não forem permitidos. Veja um exemplo:
char __fastcall TWConfigSched::PrepChar (char AChar)
{
char lCh;
switch (Byte (AChar) )
{
case 192: case 193: case 194: case 195:
case 196: case 197:
lCh = 'A';break;
case 199:
lCh = 'C';break;
case 200: case 201: case 202: case 203:
lCh = 'E';break;
case 204: case 205: case 206: case 207:
lCh = 'I';break;
case 210: case 211: case 212: case 213:
case 214: case 216:
lCh = 'O';break;
case 217: case 218: case 219: case 220:
lCh = 'U';break;
}

{ Outros caracteres inválidos são substituídos pelo underscore }
if ( ! ( (lCh >= '0' && lCh <= '9') ||
(lCh >= 'a' && lCh <= 'z') ||
(lCh >= 'A' && lCh <= 'Z') ) )
{
lCh = '_';
}

return (lCh);
};

Como esta função faz a troca de um único caracter, é preciso percorrer todo o nome do Serviço e trocá-los um a um para compor o novo nome. O conjunto de substituições reproduzido acima não está completo - falta tratar caracteres minúsculos e aqueles usados por línguas como o espanhol e o alemão (N com til, por exemplo). No entanto, as letras que não se encaixarem na substituição direta são trocadas por um underscore.

Mais Informações API de Serviços do Windows, Criação de Serviços (parte 1 e parte 2)

Nenhum comentário :

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.