tornado

22 09 2009

Logo após o Facebook comprar o FriendFeed por meros U$50 milhões, não demorou pra começarem a publicar software gratuito.

O primeiro da fila foi o servidor e framework web usado pelo FriendFeed, chamado Tornado.

O FriendFeed é famoso por sua integração com Facebook, Twitter e Google. O que ninguém sabia, é que boa parte dessa integração é feita direto no servidor e framework web. Ainda, um dos recursos mais legais é o stream de dados do servidor assíncrono, também conhecido como comet, que permite long polling consumindo o mínimo de recursos.

É interessante como nos últimos anos essas tecnologias avançam no ambiente web. Na década de 90, a coisa mais moderna era desenvolver um CGI isolado, em Perl, que era executado pelo Apache, que por sua vez distribuía as requisições para um de seus processos usando um pool.

Depois, o procedimento de executar um programa externo foi se tornando um pouco ultrapassado, e o CGI foi parar dentro do Apache, com nomes como PHP e Coldfusion. Nessa época, enfiaram até Java dentro do Apache, colocaram o nome de Tomcat nele, e os CGIs passaram a se chamar Servlets.

Anos depois, fizeram o mesmo com o Python, e colocaram lá dentro do Apache o mod_python. No caso específico do Python, foram ainda mais longe: criaram um protocolo padrão para aplicativos web se comunicar com servidores web, chamado WSGI, e também foi parar dentro do Apache com nome de mod_wsgi.

Me lembro de cada uma dessas etapas, das dificuldades e dos benefícios de cada uma dessas adaptações. Programei em todas essas linguagens, nos últimos 15 anos. Meia vida.

Mais recentemente, antigas tecnologias como o FastCGI reabriram o caminho para aplicativos web 2.0. Servidores mais leves que o Apache, como o lighttpd e nginx começaram a ganhar muito espaço em sites com grande volume de acesso, por consumirem menos recursos trabalhando de forma assíncrona.

Mas antes do Apache, já existiam diversos serviços de rede que usavam o conceito de conexão assíncrona. Os servidores de irc, por exemplo, já formavam redes com centenas, e depois milhares, e depois centenas de milhares de usuários simultâneos, no bate-papo infinito.

Quanto mais o ambiente web cresce, mais pessoas acessam sites. Esses sites precisam de mais servidores, e esses servidores precisam de mais recursos. Usar conexões assíncronas permite aceitar mais pessoas simultâneas, consumindo menos recursos que tecnologias baseadas em thread, por exemplo. É uma espécie de redução de custo com otimização de recurso, ao mesmo tempo. Para programadores, é um conceito bem diferente do tradicional, especialmente para web, e exige um pouco de cérebro para se adaptar.

Os sistemas operacionais já vêm evoluindo suas internas para aceitar mais e mais conexões simultâneas, consumindo menos e menos recursos. Nomes como epoll, kqueue e /dev/poll estão cada vez mais no dia-a-dia dos programadores. E facilitadores como a libevent também.

O resultado disso é que os novos frameworks para aplicativos web já possuem servidores web embutidos, como é o caso do Tornado, Twisted Web, Merb, etc etc… Na frente deles, lighttpd e nginx fazem a distribuição de carga, e milhares de conexões web simultâneas, em um único computador físico, começa a se tornar realidade.





atualização pro snow leopard

16 09 2009

Coisas que você precisa saber antes de atualizar pro Snow Leopard, Mac OS X 10.6:

  1. Logo após a atualização, é comum “quebrar” o sistema de autenticação. Aquela janela que costuma aparecer solicitando seu usuário e senha para desbloquear as propriedades de configuração, e também pra atualizar o Mac OS. Comigo aconteceu nos dois Macs, e a solução é remover no diretório do seu usuário, o diretório Library/Preferences/ByHost
  2. O Xcode continua, mas o gcc na linha de comando não. Logo após atualizar pro Snow Leopard, terá que reinstalar o Xcode. Pode apagar manualmente o /Developer e /Library/Developer, pois não servem pra mais nada. A versão mais atual é o 3.2
  3. De fato, MacPorts também vai pro espaço. Tudo que você já tem no port continua funcionando, menos o comando port. É necessário baixar a versão pro Snow Leopard, e depois de instalar, atualizar tudo que você tem: sudo port -v selfupdate; sudo port -v upgrade –force installed
  4. Programas como o Snapz Pro não reconhecem mais sua licença, e a única maneira de resolver é mandando e-mail pro help@ambrosiasw.com. Tentei ir no site, na sessão my account, mas também estava quebrado – dei azar!
  5. VMware Fusion e afins, só funcionam com kernel 32 bits. Já existe um beta pro kernel 64 bits, mas não funciona direito.

Por enquanto, foi isso que rolou por aqui. Assim que encontrar mais alguma zica, publico aqui.





mais sobre crawlers e spiders

9 09 2009

logo_mercadolivreNo mês passado escrevi um artigo com um programa para capturar todos os items da primeira página de cada categoria do MercadoLivre.

Lá, lidava com alguns problemas como:

  • limite de concorrência no download das páginas
  • processamento de html em thread, síncrono
  • manter a maior parte do processo assíncrono, para ganhar tempo e CPU

Depois disso, precisei fazer umas alterações no código e acabei modificando um pouco programa, usando outras técnicas como:

Cada item desta lista corresponde aos itens da lista mais acima, respectivamente.

O esquema de cooperação do twisted é muito melhor que o Controller que havia criado anteriormente. Porém, muito mais complicado para jovens aprendizes. Recomendo este link para mais detalhes.

Sobre o processamento do html, vale a pena verificar o lxml. Antes, havia usado BeautifulSoup, que é muito bom, mas perde violentamente em desempenho e suporte a broken-html.

Por fim, o truque de usar generators para executar alguns callbacks inline é incrível, e absurdamente prático em casos como esse, do programa abaixo.

O resultado é final é o mesmo, mas a melhoria em desempenho é absurda. Fiz alguns testes na minha máquina e obtive o seguinte:

  • esta versão consome, em média, 20% menos de CPU
  • como não há necessidade de gravar os arquivos no disco, não consome disco
  • o processo todo ficou 657% mais rápido, simplesmente
  • ainda, o código é muito menor +_+

Veja ai:

#!/usr/bin/env python
# coding: utf-8

from lxml import html
from twisted.web import client
from twisted.python import log
from twisted.internet import task, defer, reactor

class MercadoLivre:
    def __str__(self):
        return 'http://www.mercadolivre.com.br/jm/ml.allcategs.AllCategsServlet'

    def parse_categories(self, content):
        category = subcategory = ''
        doc = html.fromstring(content)
        for link in doc.iterlinks():
            el, attr, href, offset = link
            try: category = el.find_class('categ')[0].text_content()
            except: pass
            else: continue
            if category:
                try: subcategory = el.find_class('seglnk')[0].text_content()
                except: continue
                else: yield (href, category, subcategory)

    def parse_subcategory(self, content):
        doc = html.fromstring(content)
        for element in doc.find_class('col_titulo'):
            yield element[0].text_content()

class Engine:
    def finish(self, result):
        reactor.stop()

    @defer.inlineCallbacks
    def fetch_categories(self, link, parser):
        try:
            doc = yield client.getPage(link)
            defer.returnValue(parser(doc))
        except Exception, e:
            print e

    def fetch_subcategory(self, links, parser, limit):
        coop = task.Cooperator()
        work = (client.getPage(link[0]).addCallback(parser).addCallback(self.page_items, *link) for link in links)
        result = defer.DeferredList([coop.coiterate(work) for x in xrange(limit)])
        result.addCallback(self.finish)
        result.addErrback(log.err)

    def page_items(self, items, href, category, subcategory):
        print 'Categoria: %s / %s' % (category.encode('utf-8'), subcategory.encode('utf-8'))
        for item in items: print ' -> %s' % item.encode('utf-8')
        print ''

def main(limit, *parsers):
    e = Engine()
    for parser in parsers:
        links = e.fetch_categories(str(parser), parser.parse_categories)
        links.addCallback(e.fetch_subcategory, parser.parse_subcategory, limit)

if __name__ == '__main__':
    reactor.callWhenRunning(main, 150, MercadoLivre())
    reactor.run()




novo serviço de geo localização

1 09 2009

freegeoipnetAos poucos, vou tirando meu serviço de Geo Localização do ar. Apesar de bastante acesso por dia no freegeoip.appspot.com, os problemas com o datastore do GAE me desanimam.

O serviço está no ar há cerca de 1 ano, e a média de acesso é de 10 e 15 requests por segundo. A recente redução das cotas tiram o serviço do ar muitas vezes, e isso porque diminuí ao máximo o nível de processamento, e a maioria das buscas é feita em memória direto no memcache.

freegeoip-quotas
Apesar do sistema ainda funcionar, não tenho mais paciência pra atualizar os dados lá. O datastore não permite deletar os dados antigos de uma maneira simples, e por isso gasta muito processamento quando uso o bulkloader, ou mesmo a remote_api.

Cansado de todos esses problemas, acabei pegando um VPS no slicehost.com e fiz uma versão nova do sistema. Tornei o código fonte disponível pela GPL v2, e daqui um tempo vou redirecionar o serviço do GAE pro slicehost.

Além de rodar mais rápido, é muito mais fácil pra atualizar o banco de dados de IPs, e tenho shell na máquina.

Se você costumava usar o serviço antigo, passe a usar o novo:

http://freegeoip.net

Espero que em breve possa atualizar meu banco de dados usando a Geolocation API que está sendo definida pelo W3C.

Sobre o slicehost: é o melhor serviço de hosting que já encontrei, por um preço ótimo. O formulário de signup é bem simples, e a configuração do ambiente é incrível. Ainda, permitem adicionar seus domínios direto no DNS deles, e apontar pras máquinas virtuais.





skype no iphone 3g

17 08 2009

skype-logoDepois de tanto tempo sem escrever nada, volto logo com uma boa: fazer chamadas de voz por SIP ou Skype, no iPhone, usando a rede 3G !

Aproveitando o assunto (iPhone), vale lembrar que tem apenas duas coisas que me irritam profundamente (até agora) com este telefone:

  1. Não permitir SIP/VoIP/Skype pela rede 3G (olho gordo das operadoras)
  2. Não ter suporte a Flash (idiotice da Apple por causa dos jogos)

De resto, pra quem curte Interweb, é o melhor aparelho que existe.

Voltando ao assunto… a primeira coisa necessária pra ignorar as restrições de SIP/VoIP/Skype apenas pela conexão WiFi é fazer o jailbreak do telefone. O melhor programa que encontrei pra fazer isso, à partir do Mac OS X, é o PwnageTool – não sei o que tem pra Linux nem Windows, não me xinguem.

O jailbreak irá instalar o Cydia automaticamente, que é um front-end pro APT, exatamente igual ao do Debian.

Depois disso é necessário conectar o iPhone a uma rede WiFi, saber o endereço IP dele, e pelo Cydia, instalar os pacotes OpenSSH, APT7 (Strict) e Bigboss Recommended Tools (este irá prover tudo que interessa: ps, vim, etc…).

Com isso feito, você precisa conectar via SSH no iPhone, usando o seu programa preferido: ssh no console, ou PuTTY pros usuários de Windows. Pra conectar no iPhone, o usuário é root e a senha padrão é alpine.

No console, é necessário instalar dois pacotes:

  • mobilesubstrate : resumindo, insere código em um app durante sua execução
  • voipoverip3g: biblioteca do mobilesubstrate pra enganar o iPhone, assim os programar não sabem que estão usando 3G, e pensam que é WiFi (duh!)

Para instalar, o procedimento é padrão de debian/ubuntu, mas no console do iPhone:

apt-get install mobilesubstrate voipoverip3g

Com isso feito, reinicie o iPhone, re-instale o Skype, e seja feliz. Quando abrir o Skype pela primeira vez, ele vai reclamar que “não foi feito pra rodar em iPhoneOS modificado”, mas é “apenas uma mensagem”.

Pros mais interessados: o pacote voipoverip3g instala esse “plugin” pro mobilesubtrate, e sua configuração fica em

/Library/MobileSubstrate/DynamicLibraries/VoIPover3G.plist

Você pode adicionar suporte a qualquer programa que seja restrito ao uso de SIP/VoIP pela rede WiFi, basta editar com vim e adicionar a chave do programa, seguindo o formato do arquivo. O padrão é adicionar suporte ao Fring e Skype.

Este é o conteúdo original do arquivo:

Bozo:~ root# cat /Library/MobileSubstrate/DynamicLibraries/VoIPover3G.plist
Filter = {Bundles = ("com.Fringland.Fring", "com.skype.skype");};

Boa?





paralelismo: python e twisted

4 05 2009

parent-childExecutar algumas tarefas em paralelo no python, usando threads, pode ser problemático, especialmente quando há I/O envolvido.

Apesar do interpretador do python usar as threads nativas do sistema operacional (na maioria dos *nix, pthreads), ele não é 100% thread-safe. Isso não é um bug, mas sim um recurso usado para evitar travamento ou computação incorreta de valores.

Do texto original:

The Python interpreter is not fully thread safe. In order to support multi-threaded Python programs, there’s a global lock, called the global interpreter lock or GIL, that must be held by the current thread before it can safely access Python objects. Without the lock, even the simplest operations could cause problems in a multi-threaded program: for example, when two threads simultaneously increment the reference count of the same object, the reference count could end up being incremented only once instead of twice.

Therefore, the rule exists that only the thread that has acquired the global interpreter lock may operate on Python objects or call Python/C API functions. In order to support multi-threaded Python programs, the interpreter regularly releases and reacquires the lock — by default, every 100 bytecode instructions (this can be changed with sys.setcheckinterval()). The lock is also released and reacquired around potentially blocking I/O operations like reading or writing a file, so that other threads can run while the thread that requests the I/O is waiting for the I/O operation to complete.

Do outro lado, temos o Twisted. Para mim, o Twisted é uma super biblioteca de I/O, com todos os recursos necessários para tratar tarefas de modo assíncrono (non-blocking), considerando ainda aquelas que bloqueiam a execução por determinado tempo (blocking).

Internamente, o Twisted mantém um thread pool para executar as tarefas síncronas, como por exemplo executar um INSERT em banco de dados. Para tal, existem as funções threads.deferToThread e reactor.callInThread.

Porém, se considerarmos o GIL, mencionado acima, temos como resultado um belo problema: enquanto uma tarefa que necessita I/O síncrono está sendo executada em uma thread, o interpretador do python fica bloqueado naquela operação e todo o resto fica parado. Isso causa uma perda de desempenho sem tamanho, e diversos outros efeitos colaterais (como time-out em sockets, etc).

Solucionar o problema de paralelismo no python não é tão complicado, mas também não é tão simples. Existem dois módulos que conheço, que fazem isso: pyprocessing e multiprocessing.

Ambos usam uma API similar à do módulo threading, mas ao invés de threads, criam processos usando fork(), que por sua vez, executam outro interpretador do python e se livram dos efeitos do GIL em um único processo. Mas, quando usados em conjunto com o Twisted, necessitam diversas adaptações para a comunicação entre os processos pai e filhos, pois essa comunicação é feita através de pipes.

Com tudo isso junto, o GIL começa a se tornar um problema e a coisa toda já se parece com uma grande confusão, que para muitos já parece não ter solução decente. Em suma, é o seguinte: escrever programas que necessitam parelelismo em python exige usar pyprocessing ou multiprocessing, mas quando o programa é inteiro assíncrono usando Twisted, tudo fica complicado.

Isso obviamente aconteceu comigo em um sistema relativamente grande, que precisava executar a classificação de alguns dados em paralelo, e quando usava o thread pool do Twisted, o processo inteiro ficava lento devido à grande quantidade de I/O para ler e gravar arquivos no disco. Era o GIL me atrapalhando.

Para solucionar esse problema, escrevi um módulo que usa a função spawnProcess do próprio Twisted, e automaticamente trata do pipe entre os processos pai e filho. Ainda, escrevi um protocolo de comunicação entre eles que permite transmitir e receber dados entre os processos de modo transparente, de maneira assíncrona.

Ainda, esse módulo possui uma classe que cria um pool de processos (não threads) para onde é possível despachar dados para serem processados em paralelo, e aguardar pelo resultado em um deferred, seguindo todo o padrão do Twisted.

Depois de executar todos os testes necessários e deixar o código estável, implementei isso no meu sistema e resolvi todo o problema do paralelismo de maneira simples e elegante, sem nenhuma gambiarra nem esquisitisse.

Denominado Twisted-Parallels, o módulo foi liberado sob a GPL v2 e está disponível no Google Code, com alguns exemplos de utilização.





consulta geográfica de endereço ip

26 03 2009

Google AppEngineQuem nunca precisou nem procurou por isso, é porque não sabe que é possível. Um tempo atrás, precisei desse tipo de consulta mas não encontrei nada gratuito, que fosse “ilimitado”. Normalmente, os sites que possuem esse serviço, gratuito, colocam cotas diárias e oferecem um plano pago.

Esses dias o Gleicon me passou um link, de um cara que teve a mesma necessidade, o mesmo problema, mas arrumou a solução: sabe-se lá como, ele “criou” uma base com todas as informações. No blog dele, tornou disponível gratuitamente, em dois formatos.

Um deles, é um dump do MySQL que qualquer um pode importar; o outro, é um arquivo ZIP contendo diversos arquivos em formato similar ao CSV (ele usa ponto-e-vírgula como delimitador, e coloca o conteúdo de cada campo entre aspas duplas).

Além de publicar essa base, cara (Marc) também provê um serviço gratuito para executar essas consultas; porém, quando fui usá-lo, estava fora do ar.

Pensei que isso pudesse ser interessante, se rodasse no App Engine do Google, que não custa nada, e é “ilimitado” pra qualquer um.  Fiz o seguinte:

  1. Criei o app lá, com nome free geoip
  2. Juntei os dados de IP por cidade, lista de países e regiões
  3. Importei tudo pro datastore, usando o bulk loader que vem no SDK
  4. O iWeb criou um frontend “bonito”

O resultado foi esse aqui:

http://freegeoip.appspot.com

Se alguém quiser fazer um favicon.ico, me avise!





postgresql: fast full text search

12 03 2009

Atendendo a pedidos por comentários e e-mails que recebi sobre este assunto, decidi colocar aqui um esquema simples para demonstrar o que tenho usado para realizar busca em texto com postgresql.

O resultado é impressionante! Quando há muitos dados na base, é necessário particionar a tabela para diminuir o tamanho dos índices e manter a velocidade na busca – porém, este é um outro assunto que vou guardar para outro artigo.

O lance agora é apenas mostrar de maneira bem básica a construção de uma tabela, função plpgsql e trigger para usar o fast full text search.

Primeiro, alguns dados: crie um arquivo chamado data.txt com apenas duas colunas, separadas por ponto-e-vírgula, contendo nomes e endereços. Exemplo:

maria josé;rua um s/n
josé maria;rua dois s/n
pedro silva;rua um 5782
joaquim silva;rua dois 3030
maria pereira;rua 5, número 10
josé pereira;rua 5, número 15

Depois, é necessário criar uma nova base no postgresql e já importar esses dados nela. O arquivo que preparei pro exemplo já faz todo o trabalho, e provavelmente deverá ser ajustado para as suas necessidades.

Ai vai:


-- zz.sql 20090310 AF
-- fast full text search

-- create table for the data
DROP TABLE IF EXISTS data CASCADE;
CREATE TABLE data (
    id      SERIAL PRIMARY KEY,
    name    VARCHAR(32),
    addr    VARCHAR(64),

    name_ts TSVECTOR,
    addr_ts TSVECTOR
);
CREATE INDEX name_ts_idx ON data USING gin(name_ts);
CREATE INDEX addr_ts_idx ON data USING gin(addr_ts);

-- create function and trigger for setting tsvectors
DROP LANGUAGE IF EXISTS plpgsql CASCADE;
CREATE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION tsv_update()
RETURNS TRIGGER AS $$
BEGIN
    NEW.name_ts := to_tsvector('portuguese', NEW.name);
    NEW.addr_ts := to_tsvector('portuguese', NEW.addr);
    RETURN NEW;
END
$$
LANGUAGE plpgsql;
CREATE TRIGGER tsv_update_trigger BEFORE INSERT OR UPDATE ON data
    FOR EACH ROW EXECUTE PROCEDURE tsv_update();

-- import data from csv-like into the database
\COPY data (name, addr) FROM 'data.txt' WITH DELIMITER AS E';'

Ao executar este arquivo pelo postgresql, toda a estrutura será automaticamente criada e os dados devidamente importados, inclusive preenchendo as colunas name_ts e addr_ts, que são do tipo tsvector e possibilitam o fast full text search.

Depois disso, basta se conectar no banco e executar a busca. Veja aí (ubuntu):

$ sudo su postgres -c psql
Welcome to psql 8.3.5, the PostgreSQL interactive terminal.

Type:  \copyright for distribution terms
\h for help with SQL commands
\? for help with psql commands
\g or terminate with semicolon to execute query
\q to quit

postgres=# CREATE USER zz WITH PASSWORD 'zz';
postgres=# CREATE DATABASE zz OWNER zz;
postgres=# \c zz
You are now connected to database "zz".
zz=# \i zz.sql
...

zz=# SELECT * FROM data WHERE plainto_tsquery('portuguese', 'maria') @@ name_ts;
id |     name      |       addr       |      name_ts       |            addr_ts
----+---------------+------------------+--------------------+--------------------------------
1 | maria josé    | rua um s/n       | 'jos':2 'mar':1    | 'rua':1 's/n':3
2 | josé maria    | rua dois s/n     | 'jos':1 'mar':2    | 'rua':1 's/n':3 'dois':2
5 | maria pereira | rua 5, número 10 | 'mar':1 'pereir':2 | '5':2 '10':4 'rua':1 'númer':3
(3 rows)

zz=# \q

O grande lance é que na busca comum por texto (full text search), o tsvector seria criado em tempo de execução usando a função to_tsvector. Nesse exemplo, já deixei uma função e um trigger para executar esse trabalho e armazenar o resultado na própria tabela, nos campos previamente mencionados, name_ts e addr_ts, que são indexados por um algoritmo diferente, especial para busca em texto, chamado gin.

Assim, a busca é feita direto neles, o que possibilita o fast full text search.





unix time na sexta feira 13

6 02 2009

friday13th1Direto do Wikipedia:

“A Era UNIX teve início no dia 1 de janeiro de 1970. O nome se deve ao fato de esta data, 01/01/1970 00:00:00 UTC, ser o marco zero do sistema de calendário usado pelo sistema operacional UNIX. Também pode ser chamada de era POSIX.”

E finalmente, o unix time 1234567890 está chegando. Isso ocorrerá na próxima sexta-feira 13, às 21:31:30 (GMT -2 pelo horário de verão em São Paulo).

Para confirmar, pode usar o console do python:

>>> import time
>>> time.asctime(time.localtime(1234567890))
‘Fri Feb 13 21:31:30 2009′

fonte: br-linux.org





unix shell

18 01 2009

O interpretador de comandos dos sistemas operacionais Unix e Unix-like é a interface tradicional para operar o computador. Por meio de comandos, o usuário direciona o sistema para tarefas específicas.

O interpretador, conhecido como shell, normalmente suporta uma linguagem de script para que os comandos possam ser executados de maneira sequencial, permitindo automatizar tarefas rotineiras.

Mas, internamente, o que faz o shell? Bem, as etapas básicas são as seguintes:

  1. imprime o prompt de comando: $ para usuário ou # para root
  2. lê a linha digitada pelo usuário
  3. remove CRLF (\r\n) e depois separa a linha em strings, onde houver espaço
  4. fork(): duplica o conteúdo do próprio processo (parent) no sistema, criando um processo filho (child)
    • no child: execvp() executa o comando digtado – tipo ls -l
    • no parent: wait(NULL) aguarda o processo filho terminar

Embora os shells mais avançados como bash e csh possuam uma enorme quantidade de funções, tais como as da biblioteca readline, um shell apenas com as etapas mencionadas acima funciona perfeitamente.

Aqui está o exemplo:

/* 20090118 - AF */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

static char *stripcrlf(char *line)
{
    char *p;
    for(p = line; *p; p++)
        if(*p == '\r' || *p == '\n') { *p = '\'; break; }
    return line;
}

static void split(char *line, char **args)
{
    while(*line) {
        while(*line && *line == ' ') *line++ = '\';
        *args++ = line;
        while(*line && *line != ' ') line++;
    }
}

int main()
{
    char *p, line[4096], *args[sizeof line/2];

    for(;;) {
        fprintf(stdout, getuid() ? "$ " : "# ");
        fflush(stdout);

        memset(line, 0, sizeof line);
        if(!fgets(line, sizeof line, stdin)) {
            fprintf(stdout, "\n");
            clearerr(stdin);
            continue;
        }

        p = stripcrlf(line);
        if(*p) split(p, args);
        else continue;

        if(!fork()) {
            if(execvp(args[0], args) == -1) perror("exec");
        } else wait(NULL);
    }
    return 0;
}

NOTA: a imagem no topo foi criada por Paul Bourke em 1989, encontrada neste site.