A Figura 5.1 ao acima exibe a interface do Omnitty. Ela apresenta para o usuário uma lista de máquinas na porção esquerda do display. Exatamente uma das máquinas da lista está selecionada (exibida em fundo vermelho na lista), e na porção direita é possível ver o terminal da máquina selecionada. Conforme o usuário move-se pelas máquinas da lista à esquerda, o terminal à direita se atualiza para exibir o conteúdo do terminal da máquina correspondente.
Quando o usuário digita qualquer tecla que não seja uma das teclas de função pré-estabelecidas pelo Omnitty, a tecla é enviada para a máquina atualmente selecionada. A todo momento, o Omnitty monitora as conexões com as máquinas, e informa por exemplo o término de uma conexão colorindo em cinza o nome da máquina.
As máquinas realçadas em verde brilhante e com um asterisco à esquerda do nome estão marcadas (tagged). O administrador pode marcar e desmarcar máquinas conforme desejar. A marcação serve para indicar, para uma dada operação, quais serão as máquinas afetadas. Por exemplo, quando o usuário quer enviar digitações simultaneamente para várias máquinas, deve marcá-las para indicar quais máquinas receberão as digitações. Quando o usuário deseja por exemplo deletar várias máquinas, pode marcá-las e depois selecionar "Delete all tagged machines" do menu principal.
O usuário pode adicionar e deletar máquinas através das teclas de funções cuja legenda aparece na porção inferior da tela, e pode efetuar uma série de outras operações. Entre elas:
A interface com o usuário é exibida utilizando-se a biblioteca ncurses, que permite a manipulação visual da tela do terminal e a interação com o teclado de maneira relativamente simples. Como explicado anteriormente, o que a ncurses faz é converter chamadas de alto nível do programa para a produção de seqüências de escape para serem interpretadas pelo terminal.
O interessante de se notar é que o Omnitty então é um programa de terminal, isto é, roda dentro de um terminal; e dentro dele exibe vários terminais virtuais dentro do terminal real. Inclusive, é possível rodar o Omnitty dentro do Omnitty, e por sua vez dentro do Omnitty e assim por diante. Não que isso tenha alguma utilidade prática, mas ilustra o conceito.
O objeto de maior importância no Omnitty é uma máquina, representada pelo tipo Machine, abaixo reproduzido:
/* This structure represents each machine the program interacts with */
typedef struct Machine_ {
char *name; /* name of the machine */
bool tag; /* whether the machine is 'tagged' */
bool alive; /* initially set to true; set to false when
* main program notifies that a certain pid has died and
* it matches this machine's ssh pid */
RoteTerm *vt; /* the machine's virtual terminal (ROTE library) */
pid_t pid; /* pid of ssh process running in terminal */
} Machine;
Note que cada máquina possui um nome, um flag para indicar se ela está ou não marcada, um flag indicando se ela está viva ou não, um inteiro que indica o PID do processo SSH que lhe corresponde e tem também um RoteTerm para si.
Quando uma estrutura Machine é criada (isso acontece quando uma nova máquina é adicionada à lista, por exemplo), ocorre o seguinte:
Para ilustrar, eis o código de machine_new, a função que cria uma nova máquina.
Machine *machine_new(const char *name, int vtrows, int vtcols) {
static char cmd[128];
Machine *m = malloc(sizeof(Machine));
memset(m, 0, sizeof(Machine));
m->alive = true;
m->name = strdup(name);
m->vt = rote_vt_create(vtrows, vtcols);
/* build the command line and fork an ssh to the given machine */
if (120 < snprintf(cmd, 120, CMD_FORMAT, m->name)) abort();
m->pid = rote_vt_forkpty(m->vt, cmd);
return m;
}
O Omnitty mantém uma instância estilo Singleton de uma entidade denominada gerenciador de máquinas, ou, de forma não tão eufônica, MachMgr. Ele é responsável por manter e organizar as estruturas Machine, gerenciá-las, atualizá-las, etc.
A título de exemplo, exibimos aqui a função de adição de uma nova máquina, que é parte do gerenciador.
void machmgr_add(const char *machname) {
if (!*machname) return;
if (machcount >= MACHINE_MAX) return;
machs[machcount++] = machine_new(machname, vtrows, vtcols);
machmgr_selmach_validate();
}
Esta função recebe o nome de uma nova máquina a adicionar, cria a estrutura Machine correspondente,
Uma função importante do gerenciador é a função que roda rote_vt_update nos terminais de todas as máquinas, de forma que a ROTE tenha uma chance de processar tudo que for necessário.
void machmgr_update() {
int i;
for (i = 0; i < machcount; i++)
rote_vt_update(machs[i]->vt);
}
Note como através desse simples código, juntamente também com o código de rote_vt_update exibido anteriormente, o Omnitty obtém o efeito de dividir seu processamento para acompanhar o andamento de todas as máquinas da lista, sem para isso ter que disparar nenhuma nova thread, dar fork ou qualquer outra operação do gênero. Com a garantia de que rote_vt_update não bloqueará, machmgr_update também, por conseguinte, não bloqueará tampouco. Assim, toda vez que for chamada, dará a cada terminal virtual, através da ROTE, algum tempo de processamento para que ele possa digerir a saída produzida pelo processo-filho subjacente e atualizar seu display virtual, mas com o cuidado de nunca bloquear para que a demora ou a sobrecarga de um terminal não influencie no desempenho dos demais.
O gerenciador também possui funcionalidade para manipular a lista de máquinas como um todo; por exemplo, a função machmgr_delete_tagged deleta do gerenciador todas as máquinas que estão marcadas.
void machmgr_delete_tagged() {
int i, j;
i = 0; /* next position in the vector we will write to */
j = 0; /* next position in the vector we will look at in search
* of nontagged machines */
while (j < machcount) {
/* advance j to the next untagged machine; finish if no such thing.
* Delete tagged machines on the way. */
while (j &t; machcount && machs[j]->tag) machine_destroy(machs[j++]);
if (j >= machcount) break; /* no more machines */
machs[i++] = machs[j++];
}
machcount = i;
machmgr_selmach_validate();
}
O gerenciador de máquinas também possui funcionalidade para realizar o desenho de partes da interface do programa. Por exemplo, o gerenciador sabe desenhar a lista de máquinas que aparece na parte esquerda da tela, selecionando as cores e aparências para indicar o estado das máquinas (por exemplo cinza escuro para indicar máquinas mortas e verde brilhante para indicar máquinas marcadas).
Aqui está o código que desenha a lista de máquinas, a título de exemplo.
void machmgr_draw_list() {
unsigned char attr;
const char *p;
int w, h;
int i, j;
werase(listwin);
getmaxyx(listwin, h, w);
for (i = scrollpos; i < scrollpos + h && i < machcount; i++) {
/* decide color */
attr = machs[i]->alive ? 0x70 : 0x80;
if (i == selmach) attr &= 0xF0, attr |= 0x01; /* red background */
if (machs[i]->tag) attr &= 0x0F, attr |= machs[i]->alive ? 0xA0 : 0x20;
/* green foreground */
curutil_attrset(listwin, attr);
wmove(listwin, i - scrollpos, 0);
waddch(listwin, machs[i]->tag ? '*' : ' ');
p = machs[i]->name; j = w - 2;
while (j--) {
waddch(listwin, *p ? *p : ' ');
if (*p) p++;
}
}
}
Como qualquer programa que cria processos-filhos, o Omnitty tem que fazer algum gerenciamento para coletar os processos-filhos que morrem, também chamados "zombies" na terminologia UNIX.
Quando um processo (chamado processo-pai) cria um processo novo através do fork (chamado processo-filho), pode solicitar ao sistema operacional, através da chamada de sistema wait, que espere até que o processo filho termine e lhe retorne o código de status que o filho produziu ao seu final.
Se o processo-pai chama wait antes que o processo-filho termine, o processo-pai fica suspenso até o processo-filho terminar, e quando isso acontece recebe o valor de status produzido pelo filho e o filho é destruído. Se ocorre o contrário, no entanto, e o processo-filho termina antes do processo pai chamar wait, o que ocorre é que o sistema operacional não pode destruir o processo-filho porque o seu valor de status ainda é necessário caso o processo pai se interesse por obtê-lo. Assim, o sistema é obrigado a deixar o processo-filho num estado chamado "zombie", que é um estado em que o processo já morreu, mas ainda existe no sistema para que o seu processo-pai venha a seu encontro para receber seu valor de retorno, fazendo a chamada wait. Esse ato de fazer a chamada wait num processo zumbi chama-se "zombie reaping", ou, no vernáculo nacional, "colher o zumbi".
Toda vez que um zumbi é criado, o processo-pai recebe um sinal UNIX, mais precisamente um SIGCHLD, para indicar-lhe que foi criado um zumbi que lhe diz respeito, isto é, para informar sobre a morte de um processo-filho. É responsabilidade do processo-pai receber esse sinal e colher o zumbi.
Todo processo que criar filhos deve colher os zumbis para evitar que eles permanceçam ocupando espaço na tabela de processo. Há duas maneiras de se fazer isso no UNIX: uma delas é através da chamada de sistema signal ou suas variantes, na qual o processo-pai indica que deseja instalar o handler especial SIGIGN para o sinal SIGCHLD. Em termos mais humanos, o processo-pai informa ao sistema operacional que, desnaturadamente, não está interessado na morte dos seus processos-filhos, isto é, não está interessado em colher zumbis. Nesse caso o sistema operacional destrói os zumbis automaticamente sem que o programa tenha que chamar wait.
A outra maneira é instalar um signal handler, que é uma função que será invocada sempre que o sistema operacional entregar um certo tipo de sinal (por exemplo SIGCHLD) para o processo. Uma vez instalado, o signal handler para o SIGCHLD será chamado toda vez que um processo-filho morrer.
O signal-handler instalado pelo Omnitty é bastante simples:
static volatile int zombie_count = 0;
void sigchld_handler(int signo) { zombie_count++; }
int main(int argc, char **argv) {
/* ... (omitido) ... */
/* Installs handler for SIGCHLD so we can keep track of
* the zombie count */
signal(SIGCHLD, sigchld_handler);
/* ... (omitido) ... */
}
O tratamento de sinais no UNIX é assíncrono, isto é, pode ser ativado a qualquer momento pelo sistema operacional enquanto o programa está, por exemplo, no meio da execução de alguma outra instrução. Por isso, dentro de signal-handlers pode haver questões bastante complexas de concorrência, pois por exemplo um simples printf pode causar o travamento do programa. Isto ocorre porque se alguma chamada do programa estava no meio de algum estado inconsistente com relação à entrada e saída, uma chamada reentrante de printf pode ter efeitos imprevistos como por exemplo violações de memória e outros problemas. Por isso, o código dos signal-handlers deve ser mantido com o mínimo de complexidade.
No caso do Omnitty, por exemplo, a única coisa que o código faz é incrementar o valor de uma variável estática chamada zombie_count. Note que utilizamos, na sua declaração, a folclórica palavra-chave volatile do C. Isto se explica porque, justamente porque o incremento pode ocorrer assincronamente com relação ao resto do programa, algumas otimizações do compilador poderiam causar problemas. Em particular, é comum que o compilador, fazendo uso de alguma inteligência rudimentar, presuma razoavelmente que o valor de uma variável não mudará subitamente de uma instrução para a seguinte e por isso reutilize algum valor armazenado num registrador sem recorrer à memória novamente. Isto causa problemas com código assíncrono porque o valor da variável zombie_count pode se modificar a qualquer momento à revelia do principal, por isso utilizamos a palavra chave volatile, que indica que o compilador nunca deve assumir nada sobre a constância do valor dessa variável.
Posteriormente, quando descrevermos o loop principal do programa, a utilização do contador zombie_count tornar-se-á mais clara.
Antes de apresentar o loop principal eis aqui as instruções de inicialização da ncurses que o Omnitty executa.
void curses_init() {
int w, h, i = 0;
initscr();
start_color();
noecho();
keypad(stdscr, TRUE);
timeout(200);
raw();
curutil_colorpair_init();
clear();
/* register some alternate escape sequences for the function keys,
* to improve compatibility with several types of terminal emulators */
define_key("\eOP", KEY_F(1)); define_key("\eOQ", KEY_F(2));
define_key("\eOR", KEY_F(3)); define_key("\eOS", KEY_F(4));
define_key("\e[11~", KEY_F(1)); define_key("\e[12~", KEY_F(2));
define_key("\e[13~", KEY_F(3)); define_key("\e[14~", KEY_F(4));
define_key("\e[15~", KEY_F(5)); define_key("\e[17~", KEY_F(6));
define_key("\e[18~", KEY_F(7)); define_key("\e[19~", KEY_F(8));
define_key("\e[20~", KEY_F(9)); define_key("\e[21~", KEY_F(10));
/* (... resto omitido ...) */
}
Este código de inicialização da ncurses coloca-a num modo chamado "half-delay", no qual quando o aplicativo lê uma digitação do usuário (que é feito através da função getch), não espera mais do que um certo quantum de tempo por uma digitação. Expirado este quantum, ou se o usuário digitar alguma coisa, a função getch retorna para o chamador. Isto difere do modo normal, onde a função getch bloqueará até que o usuário digite algo, que no caso do Omnitty não é aceitável, já que mesmo quando o usuário não digita nada, há tarefas que precisam ser feitas em "background".
Note em particular a chamada timeout(200), que põe a ncurses em modo half-delay, instruindo-lhe que espere no máximo 200 milésimo de segundos por uma digitação quando getch() for chamada, e retorne após expirado esse tempo mesmo que nada haja sido digitado. A escolha do valor 200 (e não por exemplo um valor muito pequeno como 5) se justifica porque é um intervalo curto o suficiente para fazer com que a espera não prejudique o desempenho do programa, e é longo o suficiente para que o programa continue responsivo.
Há, portanto, um "trade-off" a ser feito na questão deste timeout: um timeout muito curto otimiza o uso do tempo pelo programa, já que terá mais tempo para fazer seu processamento e perderá menos tempo esperando por digitações; por outro lado, quando o usuário pressionar uma tecla, é bastante provável que o programa esteja ocupado nos seus afazeres e só atenda ao pressionamento algum tempo depois, causando a impressão de que o programa está com a "interface lenta", efeito esse chamado de "latência alta". Além disso, desta forma o Omnitty sobrecarregaria a CPU da máquina onde está rodando. Isto seria inaceitável no Omnitty, já que um dos seus objetivos é focalizar a interatividade, e ser um programa relativamente "leve" para poder ser rodado em servidores de Rede que têm tarefas importantes para fazer mesmo enquanto o Omnitty está rodando.
Por outro lado, um timeout longo demais torna a interação com o programa bastante ágil, mas faz com que o processamento e update de terminais torne-se lento, já que o Omnitty dedica tempo demais esperando por digitações e menos tempo processando os dados recebidos. Assim, o timeout de 200 milissegundos foi determinado de maneira heurístico-empírica (popularmente conhecida como tentativa e erro) como um valor razoável.
Em seguida, o Omnitty registra com a ncurses algumas definições de tecla com a função define_key. Isto serve para maior compatibilidade com alguns emuladores de terminal que não funcionam de maneiras inteiramente padrão ou não são inteiramente compatívels com termcap/terminfo/ncurses. Na maioria dos terminais (e portanto emuladores de terminais), as teclas especiais (não alfanuméricas), em geral produzem seqüências de escape ao invés de códigos simples, e essas seqüências são diferentes de terminal para terminal (naturalmente não poderia ser simples). Uma das funções da ncurses é interpretar essas seqüências e transformá-las em códigos de alto nível como KEY_NPAGE para a tecla "Page-Up" e KEY_F(1) para a tecla de função F1. Contudo, em certos terminais que não são inteiramente compatíveis, ou têm as suas próprias idéias heréticas sobre seqüências de escape, a ncurses acaba por não conseguir interpretar certas teclas. Por isso utilizamos sua função define_key para introduzir novas seqüências de escape e seus respectivos significados.
A maior parte dessas definições artificiais de teclas vieram através de contribuições de pessoas que tentaram utilizar o Omnitty em terminais insalubres e descobriram quais seqüências estavam faltando. E nem são terminais completamente exóticos: mesmo o próprio xterm, em certas versões/configurações, gera seqüências de escape bastante estranhas para algumas teclas.
O loop principal do Omnitty está na sua função main(), e permanece rodando continuamente fazendo a manuteção do programa e disparando funções de acordo com as digitações fornecidas pelo usuário. Eis aqui o código do loop principal.
int main(int argc, const char ** argv) {
/* (...) */
while (!quit) {
if (zombie_count > 0) {
zombie_count--;
chldpid = wait(NULL);
machmgr_handle_death(chldpid);
}
machmgr_update();
redraw(false);
ch = getch();
if (ch < 0) continue;
switch (ch) {
case KEY_F(1): menu_show(); redraw(true); break;
case KEY_F(2): machmgr_prev_machine(); break;
case KEY_F(3): machmgr_next_machine(); break;
case KEY_F(4): machmgr_toggle_tag_current(); break;
case KEY_F(5): add_machine(); break;
case KEY_F(6): delete_machine(); break;
case KEY_F(7): machmgr_toggle_multicast(); break;
default: machmgr_forward_keypress(ch); break;
}
}
/* (...) */
}
Partimos agora para uma exegese do loop. Em primeiro lugar, observe o bloco que começa com if (zombie_count > 0), que trata da "colheita de zumbis" como explicado anteriormente. Se o contador de zumbis (que é mantido pelo handler do sinal SIGCHLD que examinamos anteriormente) está acima de zero, o programa decrementa o contador, faz a chamada de sistema wait para determinar qual foi o PID do processo que morreu (e colhê-lo), e depois chama machmgr_handle_death para informar o gerenciador de máquinas que um processo morreu. O gerenciador tomará as providências fúnebres, a saber, marcar a estrutura Machine correspondente ao PID passado como "morta", pintar seu nome na lista de máquinas na cor cinza-escuro, fechar o pseudo-terminal, etc.
Um aspecto muito importante da "colheita de zumbis" é o cuidado com as "race conditions" que podem ocorrer devido ao fato do contador de zumbis poder ser alterado a qualquer momento, mesmo entre instruções do programa principal. Então, por exemplo, após testar o seu valor no if, é perfeitamente plausível que o signal-handler execute no ínterim entre o teste do if e a instrução seguinte, já alterando o valor do zombie_count sem nosso conhecimento. Por isso é importante não efetuar nenhuma operação que dependa do valor exato dessa variável; devemos nos contentar com certos invariantes. Um deles é que a rotina de tratamento de sinal somente incrementa o valor, nunca o decrementa. Por isso, após testarmos que o valor é maior que zero com if (zombie_count > 0), sabemos que ele permanecerá maior que zero. Por isso, temos certeza, quando chamamos wait que o programa não vai bloquear, pois há um número positivo de processos-zumbi e a chamada wait retorna imediatamente se este for o caso.
Após colher um zumbi, atualizamos o contador de forma a indicar que um zumbi foi colhido, com zombie_count--. Esta instrução está correta mesmo que o contador seja modificado pelo tratador de sinal, pois a operação de decremento é atômica (não pode ser interrompida nem por um sinal). Seria diferente escrever zombie_count = zombie_count - 1, por exemplo, pois (a não ser que o compilador otimize isto para um decremento) isto faria com que no meio tempo entre o valor da variável ser lido e decrementado num registrador, a rotina de tratamento de sinal pudesse executar e alterar o valor da variável, resultando num erro. Seria um erro bastante difícil de descobrir, e causaria o travamento completo do Omnitty, já que o loop principal acreditaria haver um zumbi a ser colhido quando não haveria nenhum, fazendo com que a chamada a wait inexplicavelmente bloqueasse.
Após fazer o tratamento dos zumbis, o loop prossegue a uma chamada a machmgr_update(), que, como anteriormente explicado, cuidará de permitir que cada um dos RoteTerm's atrelados às máquinas da lista possam ter algum tempo para processar sua entrada e saída e atualizar seu conteúdo.
Após essa atualização, o programa espera por uma tecla, mas não espera mais do que 200 milésimos de segundos conforme explicamos anteriormente. Se nesse intervalo aparecer alguma digitação, o seu código é retornado por getch(); caso contrário, um valor negativo é retornado.
Dependendo da tecla pressionada, o loop principal chama uma função para tratar da solicitação; findo isso, volta ao começo e executa novamente (que é, afinal, o que os loops fazem de melhor).
Note no código do loop principal que, se a tecla digitada pelo usuário não corresponde a nenhuma tecla de controle do Omnitty, essa tecla deve ser enviada para a máquina selecionada como uma digitação, ou, se o modo multicast está ativo, deve ser enviada simultaneamente para todas as máquinas marcadas. Este procedimento é chamado "keypress forwarding" ou, na sua trôpega tradução, "encaminhamento de pressionamento de tecla".
Para isso existe a função machmgr_forward_keypress e suas subsidiárias. Eis o código.
void machmgr_forward_keypress(int k) {
int i;
if (multicast) {
for (i = 0; i < machcount; i++)
if (machs[i]->tag) rote_vt_keypress(machs[i]->vt, k);
}
else if (selmach >= 0 && selmach > machcount)
rote_vt_keypress(machs[selmach]->vt, k);
}
Esta função é bastante simples e só trata realmente da distribuição da tecla pelos RoteTerm's adequados. A função que realmente trata do envio da tecla é a rote_vt_keypress, chamada uma vez para cada terminal.
Esse tratamento não é simples e direto como possa parecer. O parâmetro k recebido é, recordemos, um código curses de tecla como por exemplo KEY_DELETE ou KEY_NPAGE. O problema é que terminais não utilizam códigos curses, utilizam seqüências de escape para representar teclas. E lembremos que uma das funções da curses é, justamente, converter as seqüências de escape em códigos de tecla. O ideal para nós, aqui, seria que ela o fizesse na direção oposta, isto é, convertesse códigos de tecla como KEY_NPAGE de volta numa seqüência de escape que poderíamos enviar para o terminal.
Mas isto não resolveria o problema, mesmo que fosse possível (e não é, pelo menos na implementação padrão da ncurses). O Omnitty pode estar rodando em qualquer tipo de terminal, como por exemplo num rxvt (um emulador de terminal bastante usado), e portanto a curses estará configurada para converter seqüências de teclas próprias do rxvt em códigos de tecla; se conseguíssemos reverter essa conversão obteríamos, a partir dos códigos de tecla, seqüências próprias do rxvt, que são inúteis para enviar para terminais que esperam seqüências no formato linux (formato do console modo-texto do Linux), que é o tipo de terminal que a ROTE está preparada para emular!
Logo, a solução foi implementar, manualmente, uma tabela de conversão dos códigos de tecla da ncurses para as correspondentes seqüências de escape do terminal modo-texto Linux, que é o que os programas rodando sob o ROTE esperam. Esta conversão não faz parte do Omnitty em si, e sim da ROTE. Eis o código.
/* Initializes key table to convert key codes to escape sequences.
Call before using table. */
static void keytable_init() {
initialized = 1;
memset(keytable, 0, KEY_MAX+1 * sizeof(const char*));
keytable['\n'] = "\r";
keytable[KEY_UP] = "\e[A";
keytable[KEY_DOWN] = "\e[B";
keytable[KEY_RIGHT] = "\e[C";
keytable[KEY_LEFT] = "\e[D";
keytable[KEY_BACKSPACE] = "\b";
keytable[KEY_HOME] = "\e[1~";
keytable[KEY_IC] = "\e[2~";
keytable[KEY_DC] = "\e[3~";
/* (...... resto da tabela omitido ......) */
}
void rote_vt_keypress(RoteTerm *rt, int keycode) {
char c = (char) keycode;
if (!initialized) keytable_init();
if (keycode >= 0 && keycode < KEY_MAX && keytable[keycode])
/* special, write corresponding escape sequence */
rote_vt_write(rt, keytable[keycode], strlen(keytable[keycode]));
else
rote_vt_write(rt, &c, 1); /* not special, just write it */
}
Observe-se que o código testa se a tecla encontra-se listada na tabela de conversão (que é criada na hora, se ainda não o foi), e em caso afirmativo envia a seqüência correspondente. Se a tecla não está listada, a função a encara como um código ASCII cru e envia-a sem conversão alguma.
Quando o usuário deseja adicionar uma máquina, pressiona a tecla F5 e digita o nome da máquina a ser adicionada. Contudo, isto pode se tornar altamente tedioso se houver uma lista grande de máquinas a adicionar, especialmente se essa lista for a mesma para várias invocações do programa. Nesses casos o usuário poderia querer manter um arquivo com a lista das máquinas que deseja adicionar, e seria desejável que o Omnitty lesse esse arquivo e adicionasse as máquinas cujos nomes lá se encontram.
É justamente isso que a sintaxe @list.txt faz. No prompt Machine to add: que aparece quando o usuário pressiona F5, o usuário pode digitar @list.txt, e isto fará com que o Omnitty abra o arquivo list.txt e leia-o linha por linha; cada linha não vazia do arquivo será interpretada como o nome de uma máquina a adicionar.
Assim, o administrador da rede pode manter vários desses arquivos de lista em seu diretório, cada um com os nomes de um subconjunto de máquinas no qual ele deseja operar. Por exemplo, numa rede com três laboratórios, chamados criativamente de um, dois e três, o administrador podeira manter os arquivos um.txt, dois.txt e três.txt com os nomes das máquinas de cada laboratório.
Mas isto traz ainda mais um problema: como integrar isso com outros sistemas que se prestam à enumeração de máquinas da rede como por exemplo o conceito de netgroup do NIS? Seria necessário que o administrador constantemente mantivesse em sincronia a lista de máquinas do NIS com as listas de máquinas dos arquivos.
Por isso, o Omnitty permite também alterar a semântica do @ de forma que o usuário possa especificar um comando arbitrário para rodar, e é a saída desse comando que é interpretada como a lista de máquinas a utilizar.
Para isso, basta definir a variável de ambiente OMNITTY_AT_COMMAND com o comando a ser executado. Isto fará com que o Omnitty, quando vir a sintaxe @foo, ao invés de ler o arquivo foo, chame o comando OMNITTY_AT_COMMAND passando-lhe "foo" como parâmetro. A saída do comando deve ser uma lista de máquinas, uma por linha, que o Omnitty adicionará.
Eis um exemplo de configuração que permite ao administrador utilizar grupos NIS para adicionar máquinas ao Omnitty:
[root@zeus]# cat >/root/addmachines #!/bin/sh ypmatch $1 netgroup | tr ' ' '\n' | cut -c2- | cut -d. -f1 ^D [root@zeus]# chmod +x addmachines [root@zeus]# export OMNITTY_AT_COMMAND=/root/addmachines [root@zeus]# omnitty
A partir daí, basta o administrador digitar @lab_dois no prompt de adicionar máquinas do Omnitty, e isto fará com que o omnitty rode o comando /root/addmachines lab_dois, que por sua vez extrairá a lista de máquinas do netgroup chamado lab_dois do NIS.
A utilização com LDAP e outros sistemas é similar, sendo necessário apenas que o administrador escreva um script desse tipo.
A implementação desta funcionalidade está na função add_machines_from_file, abaixo reproduzida.
static void add_machines_from_file(const char *file) {
static char buf[128];
bool pipe = false;
FILE *f;
if (getenv("OMNITTY_AT_COMMAND")) {
/* popen() a command */
pipe = true;
strcpy(buf, getenv("OMNITTY_AT_COMMAND"));
strcat(buf, " ");
strcat(buf, file);
strcat(buf, " 2>/dev/null");
f = popen(buf, "r");
}
else f = fopen(file, "r");
if (!f) {
minibuf_msg(minibuf, pipe ?
"Can't execute command specified by OMNITTY_AT_COMMAND" :
"Can't read that file.", 0xF1);
return;
}
minibuf_put(minibuf, pipe ? "Adding machines supplied by command..." :
"Adding machines from file...", 0x70);
while (1 == fscanf(f, "%s", buf)) machmgr_add(buf);
if (pipe) {
if (0 != pclose(f))
minibuf_msg(minibuf, "Command given by OMNITTY_AT_COMMAND exited "
"with error.", 0xF1);
/* at this point SIGCHLD will have caused zombie_count to be one more
* than it should, since the child command has already been reaped
* by pclose(). If we don't correct zombie_count, wait() will block
* in the main loop, since it will try to reap a zombie that does not yet
* exist. */
zombie_count--;
}
else
fclose(f);
minibuf_put(minibuf, NULL, 0x70);
}
A implementação é relativamente simples, utilizando fopen ou popen dependendo da existência da variável OMNITTY_AT_COMMAND, e depois cada palavra lida desse arquivo (ou saída de comando) é adicionada como uma máquina através de machmgr_add().
Um detalhe interessante (e que foi um dos bugs mais difíceis de descobrir de todo o projeto) é a interação entre a chamada pclose e a rotina de tratamento de sinais que utilizamos para fazer o controle de processos-zumbi. Afinal, quando chamamos um subcomando com popen, ele torna-se um processo filho como qualquer outro, e ao morrer, torna-se um zumbi. Por isso pclose chama wait internamente para colher esse zumbi e retornar seu código de saída. O problema é que somos notificados, via SIGCHLD, da morte do processo-filho, e isto causa o incremento do contador zombie_count. Por isso, temos que ter o cuidado de decrementá-lo, como fazemos acima, logo após chamarmos pclose, para contabilizar o wait por ela invocado, de forma a manter a contagem de zumbis fiel à realidade.
Termino esta explicação expondo alguns dos problemas que o Omnitty apresenta, e que estão em pauta para serem resolvidos assim que possível.
Um deles é o fato ridículo de não ser possível redimensionar o Omnitty. Assim, se ele executa num xterm de determinado tamanho, não é possível redimensioná-lo durante a execução; o usuário tem que sair do omnitty, redimensionar o terminal, e iniciar o Omnitty novamente. Isto não é um problema do Omnitty em si, e sim da ROTE, que não permite que os consoles virtuais sejam redimensionados.
Outro problema frustrante é que não existe maneira de enviar, para os aplicativos subordinados rodando dentro do Omnitty, teclas como F1, F2, F3, ..., pois essas teclas são capturadas pelo Omnitty. Idealmente, deveria haver uma tecla de escape que permitisse que o usuário indicasse que a próxima tecla digitada é literal, e deve ser mandada para o terminal. Se essa tecla fosse F10, por exemplo, o usuário poderia enviar um pressionamento de F1 para o programa subordinado digitando F10 e em seguida F1.
Um terceiro problema é que o Omnitty não compila em Solaris, pois a chamada forkpty não existe nesse sistema. Contudo, imagino que isto seja fácil de corrigir porque a forkpty é só uma função "wrapper" para outras chamadas de nível mais baixo como fork, openpty, etc, todas elas existentes no Solaris. Um problema maior será, imagino, rodar programas do Solaris dentro dos terminais do Omnitty, já que a ROTE está programada para interpretar somente seqüências de escape compatíveis com o terminal modo-texto do Linux.