Elmord's Magic Valley

Computers, languages, and computer languages. Às vezes em Português, sometimes in English.

Posts com a tag: unix

Convertendo arquivos com o LibreOffice pela linha de comando

2013-03-08 02:04 -0300. Tags: comp, unix, mundane, em-portugues

Acabei de descobrir que é possível converter entre os formatos de documento suportados pelo LibreOffice/OpenOffice pela linha de comando. A sintaxe é:

libreoffice --invisible --convert-to extensão[:nome-do-filtro]
            [--outdir diretório-de-saída] arquivo...

Mais de um arquivo pode ser especificado. Se o diretório de saída for omitido, o diretório atual é utilizado. --invisible faz com que a interface gráfica não seja carregada. Por exemplo, para converter uma apresentação em ODP para PDF, você pode usar o comando:

libreoffice --convert-to pdf apresentacao-feliz.odp

Quem me contou foi esse cara (e a manpage do libreoffice). Também existe um programa chamado unoconv, mas não cheguei a experimentá-lo.

Comentários / Comments

Migrando para o XKB

2013-02-23 21:48 -0300. Tags: comp, unix, kbd, em-portugues

(Se você não quer saber de preliminares, clique aqui.)

Depois de muita enrolação, decidi migrar meu keymap do xmodmap para o XKB. Sobrevivi à experiência.

O objetivo deste post é relatar parte do que eu aprendi no processo. Para uma referência mais ou menos completa da configuração do XKB, você pode consultar o mui excelente An Unreliable Guide to XKB Configuration, ou algum outro dos links listados no final do post sobre o xmodmap. Neste post, pretendo cobrir o básico necessário para migrar do xmodmap para o XKB (talvez um pouco mais do que o básico, mas enfim).

Little rant

O XKB é um pequeno monstro. Ele foi feito para resolver todos os problemas que ninguém teve antes dele. Por exemplo: existe uma seção do keymap, xkb_geometry, cujo objetivo é descrever a disposição física das teclas no teclado. Afinal, algum programa pode querer desenhar o teclado na tela (o comando xkbprint faz isso), ou saber que tecla fica do lado de qual. Nunca se sabe. Existem pelo menos três maneiras de descrever um layout pré-pronto (XkbKeymap, XkbRules, ou descrevendo componente por componente do keymap), e pelo menos três meios de ativar uma configuração (setxkbmap, xkbcomp ou pelo arquivo de configuração do X). E como todo programa no Unix, o XKB usa um arquivo de configuração com sua própria sintaxe idiossincrática, com seu próprio parser que emite mensagens de erro extremamente prestativas, como esta diante da falta de um ; na linha 21:

syntax error: line 23 of /root/.xkb/default
last scanned symbol is: modifier_map
Errors encountered in /root/.xkb/default; not compiled.

Alguém podia voltar no tempo e colocar um leitor de S-expressions na libc original.

Dito isso, muito da complexidade do XKB pode ser ignorada para tarefas mais simples de edição de keymap (leia-se: tudo o que era possível com o xmodmap). Além disso, o fato de o povo do X ter conseguido implementar as loucuras do XKB sem quebrar compatibilidade com os programas mais antigos é digno de três estrelinhas (pelo que eu entendo o próprio xmodmap continuou funcionando intacto depois da adição do XKB ao X, ainda que com ocasionais comportamentos estranhos).

But that's enough talk. Have at you!

Conceitos básicos

Antes de mais nada, gostaria de esclarecer que XKB não é um programa, e sim uma infraestrutura de configuração de keymap. O XKB é composto por uma extensão do servidor X e um conjunto de funções da biblioteca Xlib que torna as funcionalidades do XKB acessíveis aos clientes X. Além disso, o X vem com alguns programas utilitários, tais como xkbcomp, setxkbmap e xkbprint, que utilizam a infraestrutura do XKB para ler e manipular o layout. (O xmodmap, por outro lado, é um programa que utiliza a o "core protocol" do X para manipular o teclado; a função do xmodmap é vagamente similar à do xkbcomp e setxkbmap.)

Outra diferença entre o xmodmap/core-protocol e o XKB é que no XKB não existe (aparentemente) a noção de alterar o keybinding de teclas individuais do keymap; é necessário sempre carregar um keymap completo. Por outro lado, os arquivos de configuração do XKB suportam uma diretiva include, que nos permite incluir um keymap pré-pronto em um arquivo de configuração e especificar apenas o que queremos de diferente do default.

O programa principal de controle do XKB é o xkbcomp. A função principal desse programa é ler um arquivo texto descrevendo um keymap e compilá-lo para um formato binário que o X é capaz de digerir. A sintaxe básica é:

xkbcomp [opções] origem destino

Porém, o xkbcomp é uma criatura bastante flexível. Você pode especificar como destino algo como :0, instruindo o xkbcomp a compilar o keymap e ativá-lo no display :0, sem necessidade de criar um arquivo binário intermediário. Além disso, você pode especificar :0 como origem para fazer um dump do keymap atual para um arquivo texto especificado como destino (você pode usar - para imprimir para a stdout). O dump do xkbcomp é enorme (1844 linhas na minha máquina; arquivos de keymap na prática usam includes, o que os torna muito menores), mas é útil para vermos quais são as diretivas de configuração possíveis e qual é a sintaxe de cada uma.

A função primária do setxkbmap é pôr em efeito um keymap pré-pronto. A sintaxe básica é:

setxkbmap [opções] [layout [variant [option...]]]

Onde layout, variant e option são uma das três maneiras de descrever um keymap no X (XkbRules). Exemplos:

setxkbmap br abnt2                 # Layout ABNT-2
setxkbmap us intl                  # Layout US internacional (com acentos)
setxkbmap br abnt2 ctrl:swapcaps   # Layout ABNT-2 com Ctrl e CapsLock invertidos
setxkbmap br abnt2 ctrl:nocaps     # Layout ABNT-2, CapsLock se comporta como Ctrl

Como mencionado anteriormente, você pode atribuir keymaps diferentes a teclados diferentes usando a opção -device id, onde id é o id do dispositivo tal como exibido pelo comando xinput list.

O esquema de rules é muito bonito quando queremos usar um keymap pronto, mas não quando queremos fazer modificações ao keymap. Nesse caso, teremos que criar nosso próprio arquivo de keymap. Como não é possível aplicar um keymap parcial sobre o keymap existente com o XKB, e não queremos ter que descrever todo o keymap só para alterar meia dúzia de teclas, o ideal é usar includes. Felizmente, o setxkbmap suporta uma opção -print, que imprime um keymap do XKB que descreve o keymap atual (ou um especificado na linha de comando) em termos de includes:

# setxkbmap -print -option "" br abnt2
xkb_keymap {
        xkb_keycodes  { include "evdev+aliases(qwerty)" };
        xkb_types     { include "complete"      };
        xkb_compat    { include "complete"      };
        xkb_symbols   { include "pc+br(abnt2)+inet(evdev)"      };
        xkb_geometry  { include "pc(pc105)"     };
};

(O -option "" serve para resetar as options do XKB (tais como ctrl:nocaps e ctrl:swapcaps). Por padrão, o setxkbmap usa as opções ativas atualmente, mesmo que elas não sejam especificadas na linha de comando. Você também pode executar simplesmente setxkbmap -print, sem especificar um keymap, para usar o keymap atual (ou pelo menos o que o setxkbmap pensa que é o keymap atual).)

Eis um keymap completo! Se você salvar esse keymap em um arquivo, você pode usar o comando xkbcomp arquivo :0 para pôr o keymap em efeito.

Componentes e includes

Como podemos ver na saída do setxkbmap -print, um keymap é composto por cinco componentes:

Basicamente a única parte que nos interessa do keymap é a xkb_symbols; podemos ignorar todo o resto e simplesmente copiar a configuração emitida pelo setxkbmap.

A diretiva include instrui o xkbcomp a procurar os componentes especificados em seu caminho de pesquisa. Por padrão, o xkbcomp procurará no diretório /usr/share/X11/xkb. Dentro desse diretório você encontrará subdiretórios para cada componente (keycodes, symbols, etc.). O xkbcomp procurará no subdiretório apropriado dependendo da seção em que a diretiva include ocorrer.

Cada um desses arquivos pode conter mais de uma versão do mesmo componente. Uma diretiva da forma include "br(abnt2)" indica o componente chamado abnt2 dentro do arquivo br no diretório apropriado. Também é possível incluir múltiplos componentes, usando a sintaxe include "componente1+componente2".

Você pode adicionar outros diretórios ao caminho de pesquisa do xkbcomp usando a opção -Idiretório (sem espaço após o -I). O xkbcomp esperará encontrar os componentes a incluir dentro dos subdiretórios apropriados no diretório especificado (e.g., diretório/symbols, etc.).

Símbolos

Vamos ao que interessa: fazer com o XKB o que a essas alturas já teríamos feito com o xmodmap. Eis um arquivo de keymap de exemplo:

xkb_keymap {
    xkb_keycodes  { include "evdev+aliases(qwerty)" };
    xkb_types     { include "complete"      };
    xkb_compat    { include "complete"      };
    xkb_symbols   {
         include "pc+br(abnt2)+inet(evdev)"
         key <LatA> { [ a, A, aacute, Aacute ] };
         key <AE02> { [ 3, numbersign, threesuperior ] };
         key <CAPS> { [ Control_L ] };
         modifier_map Control { <CAPS> };
    };
    xkb_geometry  { include "pc(pc105)"     };
};

O trecho em negrito são os comandos que adicionamos; o resto foi gerado pelo comando setxkbmap -print -option "" br abnt2. Note que não vai ponto-e-vírgula após o include.

O principal comando da seção xkb_symbols é o key. A sintaxe básica é:

key <nome-da-tecla> { [ level1, level2, level3, level4 ] };

Onde <nome-da-tecla> é o nome simbólico da tecla definido na seção xkb_keycodes (uma maneira "prática" de descobrir os nomes das teclas é olhar a seção xkb_symbols do keymap atual com o comando xkbcomp :0 - | less), e level1~level4 são (basicamente) os símbolos emitidos pela tecla pura, Shift+tecla, AltGr+tecla e AltGr+Shift+tecla. Você pode especificar menos que quatro níveis, se desejar.

O comando modifier_map associa teclas a modificadores, e tem basicamente a mesma função do comando add no xmodmap, com a diferença de que no XKB ele funciona direito. A sintaxe é:

modifier_map ModifierName { <nome-da-tecla-1>, <nome-da-tecla-2>, ... };

Onde ModifierName é algo como Control, Shift, Lock, Mod1, etc. (i.e., os mesmos modifiers do xmodmap), e <nome-da-tecla-1> é um nome simbólico de tecla, como <CAPS> ou <LCTL> (left Control) ou <RALT> (right Alt).

É possível especificar mais de um comando modifier_map com o mesmo modificador (o que é ótimo, pois o arquivo em que estamos dando include contém também suas próprias definições de modifier_map). Aparentemente não é possível desassociar um modificador de uma tecla (i.e., aparentemente não existe algo equiparável aos comandos remove e clear do xmodmap), mas o XKB parece ser esperto o suficiente para limpar uma tecla automaticamente quando ela é redefinida pelo comando key (i.e., não é necessário fazer os malabarismos de clear e add que tínhamos que fazer com o xmodmap para remapear Control e CapsLock).

Você pode salvar esse keymap e executar xkbcomp arquivo :0 para pô-lo em vigor.

Simples, hã? Se tudo o que você queria era fazer com o XKB o que você fazia com o xmodmap, você já tem toda a informação de que precisa. (Exceto o comando pointer do xmodmap, cujo equivalente no XKB, se houver, eu não conheço.)

Levels e Groups

Se você fizer um dump do keymap atual com xkbcomp :0 -, verá que a sintaxe que ele usa para o comando key é um pouco mais verbosa:

key <AC01> {
    type= "FOUR_LEVEL_ALPHABETIC",
    symbols[Group1]= [ a, A, aacute, Aacute ]
};

Comecemos pelo fim. A sintaxe symbols[Group1] = [ ... ] indica que os quatro símbolos estão sendo associados aos quatro níveis do grupo Group1 da tecla. Basicamente, o nível de uma tecla é afetado pelas teclas Shift, AltGr e CapsLock. É por isso que, em versões modernas do X, a tecla AltGr é associada ao símbolo ISO_Level3_Shift, como discutido no post sobre o xmodmap.

Um grupo, por sua vez, é uma maneira de atribuir diversos conjuntos de símbolos para uma mesma tecla. Por exemplo, se você costuma escrever em russo, você pode ter um layout ABNT-2 no grupo Group1 e um layout russo no grupo Group2, e atribuir uma tecla para alternar entre os grupos. A vantagem de se usar, por exemplo, dois grupos de quatro níveis ao invés de um grupo de oito níveis, é que cada grupo pode ser definido em um arquivo separado. Se você usar uma diretiva do tipo:

include "pc+br(abnt2)+us(intl):2"

você está instruindo o xkbcomp a incluir o layout br(abnt2) como o primeiro grupo e o layout us(intl) como o segundo*. Você pode então definir uma tecla para alternar entre os dois grupos: o símbolo Mode_switch alterna para o próximo grupo enquanto a tecla é pressionada, e ISO_Next_Group, ISO_Prev_Group e outros alternam permanentemente o grupo atual (como se fosse um CapsLock para grupos).

Ao invés de usar a sintaxe symbols[Group1] = [ ... ], symbols[Group2] = [ ... ], você pode simplesmente especificar mais de um grupo envolvido em [ ... ] dentro do comando key:

key <AC01> { [ a, A, aacute, Aacute ], [ ae, AE, aring, Aring ] };

Agora voltemos ao começo. type= "FOUR_LEVEL_ALPHABETIC" diz que o tipo da tecla A é FOUR_LEVEL_ALPHABETIC. Se nós olharmos o conteúdo da seção xkb_types do dump do xkbcomp, encontraremos a seguinte definição:

type "FOUR_LEVEL_ALPHABETIC" {
    modifiers= Shift+Lock+LevelThree;
    map[Shift]= Level2;
    map[Lock]= Level2;
    map[LevelThree]= Level3;
    map[Shift+LevelThree]= Level4;
    map[Lock+LevelThree]= Level4;
    map[Shift+Lock+LevelThree]= Level3;
    level_name[Level1]= "Base";
    level_name[Level2]= "Shift";
    level_name[Level3]= "Alt Base";
    level_name[Level4]= "Shift Alt";
};

O que essa definição nos diz é que uma tecla do tipo FOUR_LEVEL_ALPHABETIC é afetada pelos modificadores Shift, Lock e LevelThree (i.e., AltGr), e que diferentes combinações desses modificadores ativam diferentes níveis da tecla. As teclas de pontuação, por outro lado, são do tipo FOUR_LEVEL. A definição de FOUR_LEVEL é similar à de FOUR_LEVEL_ALPHABETIC, mas não inclui o Lock entre os modificadores que afetam a tecla. As teclas do keypad numérico, por sua vez, são do tipo FOUR_LEVEL_KEYPAD, que é análogo ao FOUR_LEVEL_ALPHABETIC, mas usa o modificador NumLock ao invés de Lock para escolher os níveis superiores. Há diversos outros tipos de teclas, definidos de maneira similar.

End of Game

Por hoje ficamos por aqui. O XKB possui inúmeras outras features, sobre as quais talvez eu venha escrever no futuro. Para mais informações, consulte os links já referidos, e dê uma olhada na saída do xkbcomp :0 -.

____

* Três estrelinhas para o primeiro que achar essa informação documentada em algum lugar; eu descobri olhando o /usr/share/X11/xkb/rules/xorg, chutando o significado do :n e testando. By the way, aparentemente simplesmente não existe documentação oficial, que dirá uma especificação, da sintaxe desses arquivos. Para mais informações, consulte o código-fonte da libxkbfile. (E não, a gramática não é especificada por um arquivo yacc da vida. Vire-se no parser escrito à mão em C, se sua paciência for capaz.)

2 comentários / comments

Múltiplos keymaps no X

2013-02-09 03:50 -0200. Tags: comp, unix, mundane, kbd, em-portugues

(O script desenvolvido neste post pode ser encontrado aqui.)

Em um episódio anterior deste blog, vimos como usar o xmodmap para alterar o keymap do X. Também vimos que embora o xmodmap seja muito mais simples de usar do que o XKB, ele está semi-obsoleto e não lida muito bem com certos recursos mais modernos do XKB. Recentemente eu descobri mais um: o xmodmap não funciona direito com múltiplos teclados (e.g., um teclado builtin de notebook e um teclado externo). Especificamente:

  1. O xmodmap altera os layouts de todos os teclados conectados; não é possível atribuir um layout US para um teclado e um ABNT-2 para outro, por exemplo;
  2. O xmodmap altera apenas os layouts dos teclados conectados; se você ligar um novo teclado, ele assumirá o layout default, e você terá que rodar o xmodmap novamente para afetá-lo.

Sendo assim, temos dois problemas a resolver: detectar quando um teclado é conectado (o que independe de usarmos xmodmap ou XKB), e atribuir um layout a ele (o que exige o XKB se quisermos usar um layout específico para cada teclado; se quisermos apenas propagar o layout corrente para o novo teclado, podemos usar o xmodmap).

Detectando o teclado

A maneira mais palatável que eu encontrei de detectar o teclado* é monitorando o arquivo de log do X (/var/log/Xorg.n.log, onde n é o número do display). As linhas relevantes do arquivo têm a forma:

[ 10848.216] (II) XINPUT: Adding extended input device "GASIA GASIA USB KB Pro" (type: KEYBOARD)

Podemos fazer um script que monitora esse arquivo com o comando tail -f, filtra apenas os dados que nos interessam, e fica em um laço aguardando que novas linhas sejam escritas no arquivo:

#!/bin/bash

[[ $DISPLAY == :* ]] || echo "$0: Oops, remote display?" >&2
display="${DISPLAY##*:}"
display="${display%%.*}"

logfile="/var/log/Xorg.$display.log"

tail -n +1 -f "$logfile" |
    sed -une 's/.*XINPUT: Adding extended input device "\([^"]*\)" (type: \([^)]*\)).*/\2\t\1/p' |
    while IFS=$'\t' read type name; do

        echo "Dispositivo detectado: $name"
        # comandos...

    done

Salve esse script em algum local apropriado (e.g., ~/bin/xinput-monitor) e dê-lhe permissão de execução (chmod +x ~/bin/xinput-monitor). Você pode executá-lo como está para testar se os dispositivos estão sendo realmente detectados (experimente conectar um teclado externo com o script rodando).

A opção -n +1 faz com que o tail imprima o conteúdo do arquivo desde a primeira linha antes de começar a monitorá-lo; isso faz com que o script "detecte" mesmo os dispositivos que já estavam conectados antes de ele rodar. Se você não desejar esse comportamento, pode trocar o -n +1 por -n 0.

Se tudo o que você quer é rodar o xmodmap (ou qualquer outro comando) quando um dispositivo é conectado, basta colocar o comando no trecho indicado no while. Feito isso, basta pôr o script para rodar na inicialização de seu ambiente gráfico (colocando uma linha do tipo ~/bin/xinput-monitor & no seu ~/.xinitrc ou ~/.xsession, ou em algum arquivo de configuração do seu ambiente gráfico favorito).

Note que nem todos os dispositivos listados são teclados (nem mesmo todos os dispositivos com type: KEYBOARD são teclados); se você não quiser executar o xmodmap uma vez para cada um desses dispositivos (o que em tese é inofensivo, mas nunca se sabe), você pode usar um if ou case para ignorar os dispositivos que não lhe interessam. Por exemplo:

    ...
    while IFS=$'\t' read type name; do
        echo "Dispositivo detectado: $name"

        case "$name" in
            'Power Button'|'Video Bus'|'Integrated Webcam'|*'Synaptics'*)
                # Não faz nada.
                ;;
            *)
                # Os comandos que desejamos executar quando um teclado é conectado.
                xmodmap ~/.xmodmaprc
                xset r rate 250
                xkbset exp =m
                ;;
        esac
    done

Setando um layout específico para cada teclado

Para atribuir um layout individual para cada teclado conectado, teremos que abandonar o bom[citation needed] e velho xmodmap e entrar no maravilhoso[dubious – discuss] mundo do XKB. Felizmente, usar um layout pronto com o XKB é bem simples: basta executar um comando como:

# Layout ABNT-2.
setxkbmap -device device_id br abnt2

# Layout US internacional (com acentos).
setxkbmap -device device_id us intl

A opção -device device_id indica qual teclado o comando deve afetar. Se ela não for especificada, o comando afeta todos os teclados.

O grande problema é determinar o device_id de cada teclado; ele pode variar dependendo dos dispositivos presentes e da ordem em que o X os encontra. Aparentemente o id não aparece no arquivo de log do X. A solução é usar o xinput (que vem no pacote xinput no Debian, Ubuntu e companhia), um comando que permite ver e modificar diversas configurações de dispositivos de entrada no X (tais como os dez mil parâmetros que controlam velocidade, aceleração, gestures e outras firulas de touchpads), entre outras coisas. O comando xinput list produz uma listagem dos dispositivos presentes e respectivos ids, com a seguinte cara:

# xinput list
⎡ Virtual core pointer                          \tid=2\t[master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer                \tid=4\t[slave  pointer  (2)]
⎜   ↳ SynPS/2 Synaptics TouchPad                \tid=13\t[slave  pointer  (2)]
⎜   ↳ GASIA GASIA USB KB Pro                    \tid=10\t[slave  pointer  (2)]
⎣ Virtual core keyboard                         \tid=3\t[master keyboard (2)]
    ↳ Virtual core XTEST keyboard               \tid=5\t[slave  keyboard (3)]
    ↳ Power Button                              \tid=6\t[slave  keyboard (3)]
    ↳ Video Bus                                 \tid=7\t[slave  keyboard (3)]
    ↳ Power Button                              \tid=8\t[slave  keyboard (3)]
    ↳ Integrated Webcam                         \tid=11\t[slave  keyboard (3)]
    ↳ AT Translated Set 2 keyboard              \tid=12\t[slave  keyboard (3)]
    ↳ GASIA GASIA USB KB Pro                    \tid=9\t[slave  keyboard (3)]

(Os \t representam TABs.)

Um problema visível nessa saída é que pode haver mais de um dispositivo com o mesmo nome (temos dois Power Button e dois GASIA GASIA USB KB Pro (sendo que um deles é um mouse (!))). Como chamar o setxkbmap sobre um não-teclado parece não ter efeito algum, podemos simplesmente tentar os dois ids e nos darmos por satisfeitos.

O primeiro passo é obter os ids a partir dos nomes. Para isso, podemos recorrer ao sed:

        device_ids="$(xinput list | sed -n "s|.*↳ $name *\tid=\([^\t]*\)\t.*|\1|p")"

Com os ids à mão, basta executar o setxkbmap para cada id, usando os parâmetros apropriados dependendo do nome do dispositivo detectado (altere as cláusulas do case para "configurar" o script para os seus teclados e layouts):

        for id in $device_ids; do
            case "$name" in
                'AT Translated Set 2 keyboard') setxkbmap -device "$id" us intl ;;
                'Some weird keyboard')          setxkbmap -device "$id" is ;;
                *)                              setxkbmap -device "$id" br abnt2 ;;
            esac
        done

'Tis over

E era isso. Eis uma versão completa do script. Ficou faltando resolver o problema de portar um xmodmaprc personalizado (caso você tenha um) para o XKB, mas isso fica para um próximo post.

(Este foi o 100-ésimo post do blog, por sinal.)

____

* Outra maneira freqüentemente citada pelo povo da Internet é alterar as configurações do udev para rodar um script quando o teclado é conectado. Além de exigir direitos de root, o script vai ter que descobrir o nome que o X atribui ao teclado por vodu, vai rodar como root (a menos que você tome a precaução de fazer o script trocar para o usuário "dono" da sessão X, o que implica descobrir quem é o dono), e você vai ter que tomar precauções para o caso de haver mais de um display X rodando (por exemplo, se você estiver usando a função "trocar de usuário" de certos ambientes gráficos). Just say no.

Comentários / Comments

The shell is completely wrong

2013-01-04 13:56 -0200. Tags: comp, prog, bash, unix, wrong, rant, em-portugues

Já faz algum tempo que eu estou para escrever uma série de posts sobre como os ambientes computacionais modernos estão completamente errados. Este não é um dos posts previstos. Ao contrário da maior parte das idéias que eu pretendia/pretendo expor na série, este post trata de um tópico dentro do âmbito do tranqüilamente implementável, sem envolver mudanças de paradigma. Considere este o zero-ésimo post da série. Considere este também o proto-manifesto de um shell que eu posso vir a escrever ou não durante as férias. Comentários são bem-vindos.

[Foto de John McCarthy com a legenda 'Programming: You're doing it completely wrong']

It's a fucking programming language

Although most users think of the shell as an interactive command interpreter, it is really a programming language in which each statement runs a command. Because it must satisfy both the interactive and programming aspects of command execution, it is a strange language, shaped as much by history as by design.

The Unix Programming Environment

Embora desde os primórdios o shell do Unix tenha sido reconhecido como uma linguagem de programação, as pessoas tendem a não encará-lo como uma "linguagem de verdade"; o shell serve para rodar "scripts", ao invés de "programas". Não só os usuários de shells têm essa visão, mas também os próprios desenvolvedores de shells: embora bash, zsh e companhia tenham acumulado diversas features ao longo dos anos, faltam nos shells features básicas que se espera encontrar em qualquer linguagem de programação razoável. Por exemplo, extensões do bash em relação ao Bourne shell (sh) original incluem:

  • Variáveis array (que só podem ser arrays de string);
  • Arrays associativos, i.e., arrays indexados por strings;
  • Um comando mapfile para ler o conteúdo de um arquivo para dentro de um array (mas não mapeia coisa nenhuma: alterações no array não se refletem no arquivo);
  • Sintaxe para permitir fall-through das cláusulas de um case (i.e., permitir o comportamento de um case sem break em C);

E no entanto:

  • Arrays só podem conter strings; não é possível criar estruturas de dados aninhadas;
  • Arrays não são elementos de primeira classe: não é possível passar um array como argumento para uma função, ou retornar um array; by the way...
  • Não existe um mecanismo decente para retornar valores de uma função; o jeito é usar uma variável global, ou imprimir o valor a retornar para a stdout e ler usando a sintaxe $(função args...), que cria um subprocesso só para executar a função e captura a saída.

Além disso, a sintaxe e a semântica de certas features são bastante facão. Isso em parte se deve ao fato de que o bash tentou acrescentar features novas sem quebrar compatibilidade com a sintaxe do sh, mas também por falta de princípios de design decentes. Um mecanismo para retornar valores de funções já seria um bom começo, e ajudaria a limpar outros aspectos do shell, já que seria possível forncecer mais funcionalidades através de funções, ao invés de usar sintaxe abstrusa (por exemplo, uma função lowercase string, ao invés da nova novidade, a substituição ${varname,,pattern}, onde pattern é opcional e indica quais caracteres devem ser transformados (sim, o padrão casa com cada caractere; se o padrão tiver mais de um caractere ele não casa com nada); ou uma função substr string start length, ao invés da substituição ${varname:start:length}, cujo uso de : conflita com a substituição ${varname:-string}, o que impede que start comece com -).

Se tanto desenvolvedores quanto usuários do shell tendem a concordar que o shell não foi feito para ser uma "linguagem de verdade", poder-se-ia argumentar que eu que sou o perdido e que o propósito do shell é de fato ser uma linguagem "de brincadeira". Mas que sentido faz isso? Para que termos uma linguagem de programação pela metade, se podemos ter uma linguagem mais decente adicionando umas poucas funcionalidades básicas?

É possível argumentar alguns motivos técnicos para a resistência a estruturas de dados. Um deles é que o mecanismo de invocação de programas é totalmente baseado em strings: não é possível chamar um programa externo passando um array como argumento, por exemplo, e isso não é culpa do shell, e sim da maneira como programas são chamados no Unix. (O que por sinal é lamentável; seria ótimo poder passar estruturas complexas entre os programas. Voltaremos a esse assunto mais adiante.) Isso não é um problema insuperável; só porque comandos externos não podem receber dados estruturados não quer dizer que não possamos passar dados estruturados internamente entre as funções do shell. O caso em que o usuário tenta chamar um comando externo com um array exige um tratamento especial (transformar o array em string, ou em múltiplos argumentos, ou emitir um erro), mas isso não é motivo para eliminar estruturas de dados complexas da linguagem.

(Outro possível motivo é medinho de pôr um garbage collector dentro do shell. Há quem ache ainda hoje que garbage collection é coisa do demônio. Mas dada a popularidade de linguagens garbage-collected hoje em dia (Java, Perl, Python, Ruby, C#, etc.) e dada a existência de bibliotecas de garbage collection para C, esse também não é um motivo forte.)

Shells alternativos

Houve e há diversos shells que tentam escapar da tradição do Bourne shell. Um deles (que serviu de inspiração para outros) é o rc, o shell do Plan 9, que possui versões para Unix. O rc tem uma sintaxe um pouco mais limpa (ou não) do que o sh, mas não apresenta grandes avanços em termos de features. Uma diferença não diretamente relacionada com o shell é que no Plan 9 o exit status de um programa é uma string, e não um inteiro entre 0 e 255 como no Unix, o que possibilita usar o exit status como meio de retornar valores de funções. Porém, o shell não apresenta nenhum recurso sintático para executar uma função e substituir a chamada pelo exit status.

Inspirado no rc surgiu o es, um shell com funções de primeira classe, variáveis com escopo léxico, closures e exceptions. Uma característica interessante do es é que boa parte dos internals do shell são expostos ao usuário. Por exemplo, o operador pipe (|) é apenas açúcar sintático para uma chamada a uma função %pipe, que pode ser substituída pelo usuário de modo a modificar o comportamento do pipeline (um exemplo dado no artigo linkado é calcular o tempo de execução de cada programa da pipeline). O es possui listas/arrays, mas não permite listas aninhadas. (O motivo oferecido é fazer com que passagem de arrays para comandos externos e para funções tenham a mesma semântica; porém, dado que o shell tem que lidar com a possibilidade de o usuário tentar passar uma função para um programa externo, esse argumento não é tão convincente. Não sei o que o shell faz nessa situação; pelo artigo eu suponho que ele passe o corpo da função como uma string, e de fato parece que o shell armazena as funções internamente como strings.) O es também não possui outros tipos de estruturas de dados complexas, embora seja possível implementá-las (ainda que de maneira convoluta) através de closures. O es também permite retornar valores complexos a partir de funções, com uma sintaxe para chamar a função e recuperar o valor. Um ponto levantado no artigo é que esse mecanismo de retorno e o mecanismo de exceptions não interage bem com comandos externos: um script externo não tem como retornar valores ou propagar uma exception para o script que o chamou. Voltaremos a esse tópico mais adiante.

Um shell posterior inspirado no rc e no es é o shell do Inferno. Esse shell não tem grandes novidades comparado com o es (embora o fato de ele rodar no Inferno lhe confira alguns poderes mágicos, como por exemplo realizar comunicação pela rede atavés do sistema de arquivos, e embora ele tenha alguns módulos interessantes, tais como uma interface gráfica). Entretanto, um ponto que chama a atenção é a sintaxe: comandos como if, for e while não são tratados de maneira especial sintaticamente. Ao invés disso, eles são comandos normais que recebem blocos de código como argumentos. A idéia é similar ao método each do Ruby: ao invés de se usar uma estrutura de controle especial para iterar sobre os itens de uma lista, chama-se um método que recebe um bloco de código e o chama sobre cada elemento:

# Ruby.
a = [1, 2, 3, 4, 5]
a.each {|i|
    puts i
}

Com isso, o usuário pode definir novas estruturas de controle que se comportam de maneira similar às estruturas padrão da linguagem.

Outros shells alternativos incluem o fish, um shell com diversos recursos interativos, mas (na minha humilde opinião) sem grandes avanços em termos de programação, e o xmlsh, um shell escrito em Java cujo objetivo é permitir a manipulação de estruturas de dados baseadas em XML, e que conspira em última instância substituir a comunicação baseada em texto dos programas atuais do Unix por um modelo de comunicação estruturada baseada em XML. (Voltaremos a esse assunto mais adiante (ainda).)

O que eu faria diferente

Um shell tem dois casos de uso distintos: como um interpretador de comandos interativo, e como um interpretador de programas. O fato de que o shell deve ser conveniente de usar interativamente se reflete na sintaxe de sua linguagem: strings literais geralmente não precisam ser colocadas entre aspas; valores literais são mais freqüentes do que variáveis, e portanto nomes por si sós representam strings (o caso mais freqüente), e variáveis são indicadas com um símbolo especial ($); execução de comandos externos usa uma sintaxe simples; há uma sintaxe conveniente para gerar listas de arquivos (*.txt, ao invés de uma chamada de função), combinar entrada e saída de comandos (|), redirecionar entrada e saída para arquivos, etc. Essa moldagem da sintaxe ao redor do uso interativo limita as possibilidades sintáticas das features voltadas à programabilidade (e.g., sintaxe para chamadas de função, estruturas de dados, operações aritméticas).

Conveniências lingüísticas à parte, a minha opinião é de que o núcleo do shell deva ser a linguagem de programação em si, e não as facilidades de uso interativo. Coisas como edição de linha de comando, histórico e completamento automático de nomes de arquivos e comandos não deveriam ser internas ao shell; ao invés disso, a linguagem do shell deveria ser suficientemente capaz para que essas coisas todas pudessem ser implementadas como scripts.

Em termos de features da linguagem, é possível tornar o shell mais hábil sem quebrar (muito) a compatibilidade com sh e bash. O primeiro passo é criar um mecanismo para permitir retornar valores de funções sem criar um subshell. Para isso, é necessário definir um comando para retornar valores, e uma sintaxe para chamar uma função e recuperar o valor. Eu usaria reply valor (já que return N já está em uso, para sair da função com um exit status N) e $[função args...] para isso. (Atualmente $[...] causa avaliação aritmética em bash. A sintaxe $[...] é considerada obsoleta, em favor da sintaxe do padrão POSIX, $((...)).)

O segundo passo é tornar arrays elementos de primeira classe, permitindo passá-los para funções e retorná-los, e permitindo armazená-los onde quer que se possa armazenar um valor (por exemplo, dentro de outros arrays). Acaba-se com a noção de "array variable" do bash: uma variável contém um array, não é um array. $array não retorna mais o primeiro valor do array, e sim o array inteiro. É possível passar um array literal para uma função:

função arg1 (1 2 3) arg3

Convém criar uma sintaxe para arrays associativos, possivelmente %(chave valor chave valor ...). Também convém usar um operador diferente para indexar arrays associativos e não-associativos, já que em bash, índices de arrays não-associativos sofrem avaliação aritmética:

i=0
echo ${array_comum[i]}   # Elemento da i-ésima posição
echo ${array_assoc{i}}   # Elemento cuja chave é a string "i"

(Outra alternativa seria nunca fazer avaliação aritmética sem que o programador mande explicitamente, mas isso não só quebra a compatibilidade como é inconveniente na prática.)

Word splitting implícita sobre a os valores de variáveis fora de aspas teria a morte horrível que merece. Pathname expansion sobre valores de variáveis teria as três mortes horríveis que merece.

Em bash, os elementos de uma pipeline são executados em subshells (com exceção do primeiro (ou do último se a opção lastpipe estiver ativa, outra novidade do bash 4)), o que significa que uma função usada em um pipeline não tem como alterar os valores das variáveis globais, pois as alterações que fizer serão isoladas em seu subprocesso, o que freqüemente é inconveniente. Por exemplo, código desse tipo não funciona em bash (embora seja possível contornar o problema em muitos casos):

files=()
find . -name '*.txt' | while read file; do
    files+=("$file")
done

echo "${files[@]}"   # Array continua vazio

Uma feature desejável seria que todos os itens de uma pipeline que possam ser executados internamente ao shell o sejam. Uma solução é, ao invés de criar novos processos, criar threads, e simular os redirecionamentos de entrada e saída do pipe internamente (e.g., se um comando função1 | função2 é executado, o shell executa cada função em uma thread, e faz mágica internamente para dar à função1 a ilusão de que o que ela imprime para a stdout vai para a stdout (file descriptor 1), quando no entanto os dados vão parar em um outro file descriptor usado para fazer a comunicação entre as funções (ou mesmo em uma string, evitando chamadas de sistema para leitura e escrita em file descriptors)). O mesmo mecanismo pode ser usado para executar $(função args...) sem gerar um subprocesso.

Oops! Mas o ponto de inventar a sintaxe $[...] e reply ... era justamente evitar criar um subprocesso! Uma diferença, entretanto, é que a sintaxe $(...) só serve para retornar strings (pela sua definição), enquanto $[...] serve também para retornar estruturas de dados complexas.

Dado o paralelo entre $(...) e as pipelines, ocorre a idéia interessante de termos um equivalente do pipeline para $[...], i.e., um pipeline capaz de passar dados estruturados. Com isso, poderíamos escrever "generators", a la Python e companhia. Algo do tipo:

generate_factorials() {
    local i=1 val=1
    while :; do
        yield $val # Entrega um valor para a próxima função; aqui usamos um valor simples,
                   # mas poderíamos passar qualquer estrutura de dados.
        ((i++, val*=i))
    done
}

consume() {
    local val
    while val=$[take]; do
        echo "Recebi $val"
    done
}

generate_factorials ^ consume   # Sintaxe hipotética para o "pipeline de objetos"

Hmmrgh, agora temos dois conceitos parecidíssimos mas diferentes no shell. Quem sabe se ao invés de usar um tipo especial de pipeline, usássemos o mesmo mecanismo de pipeline para as duas coisas? Conceitualmente o comando yield então escreveria o valor para a fake stdout, e take o leria da fake stdin, enquanto internamente o shell transferiria o objeto de uma função para a outra. Da mesma forma, por consistência, o comando reply escreveria o valor de retorno para a fake stdout, e $[...] o leria da fake stdin. (Não temos como unificar $(...) e $[...] porque a semântica das duas expressões é diferente: uma retorna uma string, não importa o que o subcomando faça, enquanto a outra pode retornar qualquer tipo de valor. $[...] é análogo a um take, enquanto $(...) é análogo a um read.)

A questão é: se yield escreve na fake stdout, o que é que ele escreve? A princípio, não precisaria escrever nada "real": contanto que haja um take na outra ponta, poderíamos transferir o valor do yield para o take por mágica internamente, e apenas conceitualmente escrever no file descriptor correspondente à pipeline. Porém, se ela escrevesse alguma coisa, e essa coisa representasse o valor a ser transferido, as duas pontas do pipeline não precisariam mais estar no mesmo processo! Quer dizer, da mesma forma que o nosso pipeline comum sabe distinguir se as duas pontas estão em processos diferentes ou não, e usa file descriptors de verdade ou fake stdin/stdout dependendo do caso, o nosso pipeline de objetos também poderia fazer o mesmo. Se as duas pontas estão no mesmo processo, transferimos o objeto puro e simples. Mas se as duas pontas estão em processos distintos, podemos serializar o objeto de um lado e des-serializar do outro, de modo que é possível passar uma stream de objetos para um script externo de maneira transparente. Da mesma maneira, $[...] poderia funcionar com scripts externos, lendo o valor de retorno serializado da stdout do script e des-serializando-o novamente. Assim, resolvemos parte do problema mencionado no artigo do shell es: conseguimos fazer nossos valores complexos conviverem com o mundo texto-puro do Unix.

Em parte: falta dar um jeito de podermos passar argumentos complexos para os programas externos. A princípio poderíamos passar uma representação serializada dos argumentos. Porém, precisamos arranjar um meio de permitir ao programa chamado distinguir o array (1 2 3) da string composta pelos sete caracteres (1 2 3). Uma idéia seria sempre passar as strings com aspas em volta. Mas não podemos fazer isso porque não sabemos se o programa chamado é um script ou não, e portanto não sabemos se ele está preparado para entender as aspas (e.g., o comando ls não entende a opção "-la", com aspas em volta). Somos obrigados a passar as strings literalmente. Uma solução é passar uma variável de ambiente para o programa dizendo o tipo de cada argumento; se o subprocesso for um script, ele saberá interpretar a variável e distinguir strings de representações serializadas, e se for um outro programa qualquer, ele não dará bola para a variável*, interpretará as strings como strings, e as representações serializadas como strings também; não faz sentido passar outros objetos para não-scripts, de qualquer forma.

A semântica da passagem por serialização pode ser um pouco diferente da passagem direta dentro de um mesmo processo. Se as estruturas de dados são mutáveis, as mudanças em um processo diferente não se refletirão (a princípio) no processo original (pois o subprocesso tem uma cópia, não uma referência ao objeto original). Porém, isso não há de ser problema.

Um problema com passar valores pela stdout é que o valor de uma chamada de função não é ignorado por padrão. Isto é, enquanto na maior parte das linguagens de programação o valor de retorno de uma função é descartado por padrão se não for usado, no nosso caso a chamada imprime seu valor de retorno para a stdout, e para descartá-lo temos que tomar alguma ação explícita (um >/dev/null, por exemplo). Não sei até que ponto isso é um problema.

It's text all way down (and up)

A idéia de passar objetos estruturados entre os programas não é novidade. A grande glória das pipelines é permitir que combinemos diversos programas simples de modo a realizar uma tarefa mais complexa. No Unix, grande parte dos programas lêem e emitem texto em um formato simples (campos separados por algum caractere como TAB ou :, um registro por linha). A universalidade desse formato e a grande quantidade de ferramentas para manipulá-lo permite que combinemos programas que não foram necessariamente feitos para se comunicar um com o outro. Por exemplo, se quiséssemos contar quantos usuários locais usam cada shell, poderíamos usar um comando do tipo:

# cat /etc/passwd | cut -d: -f7 | sort | uniq -c | sort -rn
     17 /bin/sh
      5 /bin/false
      2 /bin/bash
      1 /usr/sbin/nologin
      1 /bin/sync

No entanto, texto puro por vezes deixa um pouco a desejar. E se quisermos trabalhar com dados hierárquicos, ao invés de tabelas? E se quisermos usar o separador dentro de um dos campos? Esse último problema é freqüente em programas que manipulam nomes de arquivo. No Unix, um nome de arquivo pode conter qualquer caractere, com exceção do ASCII NUL (a.k.a. \0) e da /, que é usada para separar nomes de diretórios. Isso significa que nomes de arquivo podem conter espaços, asteriscos, tabs e quebras de linha, entre outros, o que atrapalha a vida de muitos scripts. Por exemplo, se você usar um comando do tipo:

find / -size +4G >lista-de-arquivos-maiores-que-4-giga.txt

você corre o risco de uma alma perversa ter criado um arquivo com um \n nome, o que vai fazer com que esse nome pareça duas entradas distintas na lista de arquivos. A maior parte das ferramentas não é preparada para lidar com terminadores de linha diferentes de \n (embora diversas ferramentas do GNU tenham opções para lidar com \0). E se ao invés de passar texto puro entre os programas pudéssemos passar dados estruturados, em um formato compreendido por todas as ferramentas?

Como disse, essa idéia não é novidade. Talvez o projeto mais famoso a explorar essa possibilidade no Unix seja o TermKit, um projeto que objetiva liberar o shell do mundo do texto puro e dos emuladores de terminal. As ferramentas do TermKit se comunicam usando JSON e headers baseados em HTTP. A idéia é que, ao invés de transmitir bytes brutos, os dados que entram e saem dos processos carreguem consigo uma indicação de seu tipo (no header), de modo que as ferramentas saibam como lidar com o conteúdo que recebem. O TermKit foca na parte de interação com o usuário, e não provê um shell completo (programável).

Há uma thread longa sobre o assunto nos fóruns do xkcd.

Outro projeto nesse sentido é o xmlsh mencionado na última seção, que utiliza XML para a comunicação entre os processos.

No mundo não-Unix, um programa interessante é o PowerShell do Windows. No caso do PowerShell, os comandos realmente passam objetos entre si (e não representações serializadas). Isso é feito com uma pequena "trapaça": os comandos capazes de lidar com objetos não executam em processos externos, mas sim são instâncias de "cmdlets", objetos que fornecem uma interface para o shell, e que são instanciados dentro do próprio shell. Um exemplo retirado do artigo é o comando:

get-childitem | sort-object extension | select extension | where { $_.extension.length -eq 4 }

get-childitem é uma versão generalizada do ls, que funciona sobre diretórios e outras estruturas hierárquicas. sort-object recebe uma stream de objetos como entrada pelo pipeline, e os ordena pelo campo passado como argumento. select é similar ao cut do Unix, mas trabalha sobre objetos. where é um comando de filtro que recebe como argumento um predicado, i.e., uma função que é executada sobre cada elemento da entrada e decide se o elemento permanece na tabela final ou não.

O que eu acharia realmente ideal seria passar objetos entre processos, não apenas como um mecanismo interno do shell. Isso era possível na Lisp Machine, primariamente porque todos processos eram executados no mesmo espaço de endereçamento (i.e., os processos na verdade são threads). (Além disso, o fato de todos os programas do sistema terem uma linguagem comum (Lisp) garante que os objetos serão entendidos por todos os programas.) Permitir isso em um ambiente com processos isolados é uma tarefa mais complicada.

Mas estamos divergindo do ponto original e começando a sair do âmbito do tranqüilamente implementável. Por enquanto eu só quero criar um shell decentemente programável. Revolucionar a maneira como os programas se comunicam é tópico para outros posts.

O que mais eu faria diferente

Até agora adicionamos features de maneira semi-compatível com o sh. Mas podemos aproveitar a oportunidade para fazer uma limpeza geral no shell. Poucos são os fãs da sintaxe do sh [citation needed], então creio que poucos se objetarão terminantemente a um shell com sintaxe diferente.

Uma coisa que eu gostaria é de seguir o exemplo do shell do Inferno e usar comandos comuns e blocos de código ao invés de estruturas de controle com sintaxe especial. Assim, o usuário poderia definir estruturas de controle novas sem necessidade de alterar a sintaxe do shell. Exemplo hipotético:

function foreach (list body) {
    local i=0
    local length=$[length $list]

    while { $i < $length } {
        $body ${list[i]}
        i += 1
    }
}

# Exemplo de uso. (Sintaxe de bloco roubada do Ruby.)
foreach (*.txt) {|file|
    echo "Eis o arquivo $file:"
    cat $file
}

Tenho outras idéias para um shell, mas acho que esse post já está ficando longo demais. Talvez eu escreva sobre isso em outra ocasião (ou se alguém demonstrar interesse). Por ora ficamos por aqui.

Conclusão

Os pontos principais deste post são:

  • O shell é uma linguagem de programação; não existe um bom motivo para ele ser uma linguagem de programação pela metade;
  • É possível adicionar capacidades que favoreçam a programabilidade do shell sem prejudicar seu uso interativo (ainda que isso force o shell a usar uma sintaxe estranha para algumas coisas, de modo a acomodar mais naturalmente o uso interativo);
  • Todos são a favor da moção.

_____

* Mas o subprocesso pode passar a variável intacta para outros processos, o que eventualmente pode fazer a variável cair em outro shell, que ficará confuso achando que a variável se refere aos argumentos que recebeu. Uma solução é incluir no valor da variável o PID do processo a quem a variável se destina. Mas ainda há o risco de o processo fazer um exec() e executar um shell com o mesmo PID atual, ou de um PID se repetir. Hmmrrgh...

1 comentário / comment

Kindle hacks

2012-11-08 02:44 -0200. Tags: comp, unix, kindle, em-portugues

Comprei um Kindle 3 Wi-Fi usado. Assim como o seu celular, seu terminal de banco e sua cafeteira, o Kindle roda um GNU/Linux, o que torna o dispositivo (depois de rooteado) bastante versátil.

[Foto de um Kindle mostrando uma sessão X remota por VNC]
Dizem que ele até lê eBooks!

Rooteando a criança

Tudo o que você precisa saber para rootear o Kindle pode ser encontrado neste artigo totalmente excelente e posts do fórum MobileRead linkados pelo mesmo. Segue um resumo das instruções para o Kindle 3 (para o Kindle 4 as coisas são um pouco diferentes, aparentemente).

O INEVITÁVEL DISCLAIMER: Obviamente ninguém (além de você) se responsabiliza por qualquer dano causado ao Kindle caso alguma coisa dê errado ao seguir estas instruções. Use por sua conta e risco. Em todo caso, o procedimento parece seguro. (Funcionou para mim sem nenhum evento inesperado, pelo menos. Eu inclusive tentei instalar os hacks na ordem errada e o procedimento simplesmente falhou sem qualquer dano ao funcionamento do dispositivo. Mas a gente nunca sabe.)

Instale o Jailbreak Hack

  1. Baixe o kindle-jailbreak-0.11.N.zip.
  2. Dentro do zip há diversos arquivos com nomes do tipo update_jailbreak_0.11.N_k3w_install.bin, onde k3w é um identificador da versão do firmware do Kindle (no caso, k3w é o Kindle 3 Wi-Fi). Conecte o Kindle na USB e copie o arquivo apropriado para o raiz da unidade do Kindle.
  3. Desmonte/ejete a unidade do Kindle e ligue a criança.
  4. Pressione Menu, escolha Settings, pressione Menu novamente, selecione Update Your Kindle. O dispositivo reiniciará.

Pronto! Na verdade tudo o que esse "hack" faz é adicionar uma chave na lista de chaves que o Kindle aceita como válidas na hora de instalar updates. Feito isso, você poderá instalar os hacks que fazem alguma coisa útil. Para mais informações, dê uma olhada no fórum do MobileRead.

Instale o Launchpad

O Launchpad é um aplicativo que permite associar certas seqüências de teclas a comandos no Kindle. Como a interface padrão do Kindle não provê nenhum meio de executar programas de terceiros, é necessário instalar algum programa que permita chamar esses outros programas.

O procedimento de instalação é análogo. Baixe o zip com o Launchpad, ache o arquivo apropriado dentro do zip (e.g., update_launchpad_0.0.1c_k3w_install.bin), copie-o para o raiz do Kindle e use a opção Update Your Kindle.

Após a instalação, haverá no raiz da unidade do Kindle um diretório launchpad, com diversos arquivos *.ini. Esses arquivos configuram, entre outras coisas, as associações de teclas a comandos. Instruções quanto ao formato desses arquivos podem ser encontradas no arquivo README nesse diretório, ou nos comentários do launchpad.ini.

Instale o usbNetwork (opcional)

O usbNetwork é um hack que faz com que o Kindle se comporte como um dispositivo de rede USB, ao invés de um dispositivo de armazenamento. (Essa funcionalidade existe builtin no Kindle 2, mas foi removida no Kindle 3.) Além disso, o pacote vem com o Dropbear (um servidor SSH), entre outras coisas. Além de permitir acessar o sistema que roda no Kindle por SSH, isso também permite copiar arquivos de/para o Kindle por scp/sftp/sshfs, tanto pela USB quanto por Wi-Fi.

Para fazer a instalação (guess what), baixe o zip com o usbNetwork e proceda de maneira análoga aos outros pacotes.

Feito isso, é necessário ativar o usbNetwork. Para tal, a partir da tela inicial do Kindle, pressione Del (para abrir o campo de pesquisa), e dê os comandos (i.e., "pesquise" pelas seguintes strings):

Feito isso, ao ser conectado em uma máquina com GNU/Linux, o Kindle se apresentará como a interface de rede usb0. Dê um ifconfig usb0 192.168.2.1/24 (como root). O Kindle deverá estar acessível com o IP 192.168.2.2 (experimente dar um ping para ver se tudo está funcionando). Agora você pode dar um ssh 192.168.2.2 e divertir-se nos internals do seu Kindle.

Para fazer com que o Kindle volte a se comportar como um dispositivo de armazenamento, basta executar o comando ~usbNetwork novamente (em modo debug).

Para convencer o Dropbear do usbNetwork a aceitar conexões por Wi-Fi, você deverá ajustar a opção K3_WIFI para "true" no arquivo usbnet/etc/config na unidade do Kindle, e reiniciar o usbNetwork. Quando o acesso é feito por Wi-Fi, o Dropbear requisita autenticação (por chave pública ou senha). Para alterar a senha de root do Kindle (recomendável), dê os seguintes comandos em um terminal:

mntroot rw
passwd
mntroot ro

Se você quer brincar de chave pública, consulte o artigo linkado no início deste post.

E agora?

A partir de agora, a princípio, você não precisará mais instalar updates para instalar programas no Kindle; basta copiar os programas para a unidade do Kindle (ou para o /mnt/us do sistema de arquivos do Kindle, caso você o esteja acessando diretamente por ssh/scp/sftp/sshfs, e não como dispositivo de armazenamento). Até o momento eu instalei dois aplicativos no Kindle: o myts (um emulador de terminal) e o kindlevncviewer (um cliente de VNC).

myts

Para instalar o myts, basta descompactar o zip com o binário diretamente na unidade do Kindle (i.e., os diretórios myts e launchpad do zip devem ser copiados diretamente para o raiz da unidade; o conteúdo do diretório launchpad deve ir parar dentro do diretório launchpad que já existe no Kindle).

Feito isso, vá para a tela inicial do Kindle e digite Shift, Shift, Espaço (cada tecla deve ser pressionada individualmente). Isso atualizará as configurações do Launchpad; uma mensagem de Success deve ser exibida na parte inferior da tela. Agora você pode digitar Shift, T, T para abrir um terminal.

O terminal utiliza combinações bizarras das teclas Menu e Back com letras para permitir a digitação dos caracteres que não existem no teclado do Kindle (e.g., Back+v = /). Para ver uma tabela com as combinações, mantenha pressionada uma das teclas modificadoras e pressione o botão volta-página da esquerda. A tecla Aa funciona como Ctrl, e a tecla volta-página da esquerda sozinha sai do terminal.

kindlevncviewer

A instalação do kindlevncviewer é similar (eis o zip). Com as configurações padrão (vide launchpad/kindlevncviewer.ini), a seqüência Shift, V, U inicia uma sessão VNC usando a tela 1 (porta 5901) da máquina 192.168.2.1.

Quanto ao servidor VNC, você pode usar um servidor como o tightvncserver e iniciar uma sessão X independente (com um comando do tipo Xvnc :1), ou usar o x11vnc e tornar sua sessão X atual remotamente acessível (com um comando do tipo x11vnc -rfbport 5901). Note que ambos os comandos iniciam servidores VNC remotamente acessíveis sem senha; configurar uma senha de acesso é sugerido como exercício para o leitor (até eu possivelmente atualizar este post, em um momento de maior disposição).

Escaping Amazon

Uma precaução tomada pelo autor do artigo linkado é desabilitar as atualizações automáticas de firmware da Amazon. Para isso, basta abrir um shell (por SSH ou usando o emulador de terminal) e executar:

mv /etc/uks /etc/uks.disabled

Isso remove as chaves da própria Amazon do conjunto de chaves conhecidas, o que faz com que o Kindle rejeite as atualizações. Um comando grep http /opt/amazon/ebook/config/framework.fiona.conf também revela diversas URLs que o software do Kindle usa para contactar a Amazon, em particular para obter as TodoLists que instruem o dispositivo a baixar atualizações de firmware (ou apagar seus livros). Você pode se divertir alterando essas URLs (possivelmente apontando-as para uma outra máquina para analisar as requisições que o Kindle faz).

(Ainda não tive a oportunidade de analisar o tráfego gerado pelo Kindle, embora eu desconfie de que o meu Kindle não esteja tentando baixar atualizações, pois eu nunca registrei o dispositivo.)

[Update (1/1/2013): Mesmo com essas configurações, o Kindle continua se comunicando com a Amazon. Uma solução para evitar isso é proibir no iptables qualquer conexão de saída que não tenha sido explicitamente habilitada. Para isso, você pode substituir o arquivo /etc/sysconfig/iptables do Kindle pelo conteúdo deste arquivo. Infelizmente, para que o sistema de conexão por wireless do Kindle funcione corretamente, é necessário deixar o DNS desbloqueado (caso contrário ele considera que a conexão wireless não foi estabelecida), o que deixa passar as requisições de DNS perguntando por servidores da Amazon (que o Kindle aparentemente emite a cada dois segundos enquanto não consegue se conectar com a Amazon). Fora o tráfego extra, essas requisições são a princípio inofensivas. Se alguém souber um meio de bloquear essas requisições sem impedir o funcionamento da wireless, queira deixar um comentário.]

E assim termina a saga.

Comentários / Comments

15 graus a leste

2012-10-15 00:27 -0300. Tags: comp, unix, mundane, random, em-portugues

No próximo domingo, dia 21 de outubro de 2012, diversas cidades brasileiras sofrerão um deslocamento de 15 graus para leste, e por conseguinte estarão sob o fuso-horário -2. Habitantes dessas cidades deverão ajustar seus relógios de acordo. O fenômeno geológico ocorre todo ano por volta da terceira semana de outubro, e encerra-se por volta da terceira semana de fevereiro do ano seguinte, quando um deslocamento reverso restaura as posições originais das cidades.

Graças à regularidade do fenômeno, muitos sistemas operacionais já vêm preparados para realizar a mudança de fuso-horário automaticamente. A usuários que ajustam seus relógios manualmente, recomenda-se ajustar o fuso-horário do sistema (com um comando do tipo ln -sf /usr/share/zoneinfo/Etc/GMT+2 /etc/localtime em sistemas GNU/Linux), ao invés de adiantar o relógio em uma hora, já que o relógio do sistema é mantido em UTC, que não é afetado pelo deslocamento.

(Os nomes dos fusos-horários em /usr/share/zoneinfo/Etc possuem sinais contrários aos dos nomes convencionais dos fusos (i.e., o fuso-horário -2 corresponde ao arquivo GMT+2). A convenção de sinais anti-intuitivos vem de uma longa tradição na comunidade científica enraizada na nomenclatura das cargas elétricas.)

Recomenda-se fortemente manter-se sentado em uma cadeira firme durante o deslocamento, por razões de segurança.

1 comentário / comment

SSH passivo/reverso

2012-10-03 15:31 -0300. Tags: comp, unix, network, em-portugues

Problema: queremos acessar uma máquina por SSH, mas a máquina "alvo" está atrás de um roteador que faz NAT e não é viável reconfigurar o roteador para redirecionar uma porta, ou a máquina está por trás de um firewall que não permite conexões de entrada.

Solução: o SSH tem uma feature chamada remote forwarding. Se você executa o comando:

ssh -R porta_remota:endereço_local:porta_local usuário@máquina_remota

Isso serve para encaminhar qualquer porta da máquina remota para a máquina local, mas em particular serve para encaminhar uma porta qualquer da máquina remota para a porta 22 (ssh) da máquina local. Na máquina que queremos acessar por SSH, executamos:

ssh -R 9000:127.0.0.1:22 usuário@máquina_a_partir_da_qual_queremos_fazer_o_acesso

Na máquina de onde queremos fazer o acesso, executamos:

ssh -p 9000 127.0.0.1

E pronto! Quando acessarmos a porta 9000 da máquina de origem, o SSH encaminhará a conexão para a porta 22 da máquina alvo, onde o servidor SSH da máquina alvo atenderá a conexão.

[* É possível ouvir em outras interfaces, ao invés de apenas a interface loopback. Basta usar -R bind_address:porta_remota:endereço_local:porta_local, especificando em que endereços se quer ouvir através do bind_address. Um bind_address vazio (i.e., :porta_remota:endereço_local:porta_local (note o : inicial)) ouve em todas as interfaces.]

Comentários / Comments

Laptop mode, ACPI, fsync, e a arte de interceptar syscalls

2012-09-27 13:46 -0300. Tags: comp, unix, mundane, prog, em-portugues

Recentemente eu comprei um Dell Inspiron Mini 10 usado. Essa máquina tem uma propriedade peculiar: ela não tem fan! O processador, um Intel Atom N270 1.6GHz, esquenta suficientemente pouco para que a máquina não precise de um. Conseqüentemente, a máquina é dead-silent – ou quase. Infelizmente esse modelo vem com um HD, ao invés de um SSD, e portanto a máquina faz algum barulho, ainda que isso só seja significativo em ambientes realmente silenciosos (e.g., à noite).

Felizmente, esse tal de Linux possui uma feature especial de primeira chamada laptop mode. Quando o laptop mode está ativo e ocorre uma leitura ou escrita física ao disco, o kernel aproveita para escrever em disco todos os blocos sujos (modificados) que estejam em buffers de memória. A idéia é que o disco fique desligado a maior parte do tempo, e nas poucas ocasiões em que ele é "acordado", realizam-se todas as operações de I/O pendentes, de modo que o disco não tenha que ser reativado tão cedo. Para que o laptop mode seja efetivo, é necessário ajustar o kernel para manter as coisas o maior tempo possível em buffers, através de certas configurações em /proc/sys/vm e outros locais. O laptop mode foi concebido para economizar bateria em laptops, mas também serve para deixá-lo mais silencioso.

Easy mode

A maneira mais fácil de usar o laptop mode é através das Laptop Mode Tools (pacote laptop-mode-tools no Debian e derivados), um conjunto de scripts que dão conta de ativar o laptop mode quando a máquina está fora da tomada, e desativá-lo quando a máquina é ligada na tomada e quando a bateria está fraca (evitando que se perca todas as mil coisas que estão em cache caso a bateria acabe). O laptop-mode-tools é um pacote bastante bloated completo, com mais de oito mil opções de configuração e módulos que controlam não somente o HD como diversas outras features de economia de energia, tais como brilho do display, redução da potência de certas placas wireless, USB auto-suspend, entre outras coisas. Basta um apt-get install laptop-mode-tools, e ser feliz para sempre.

Obviamente, tudo isso é insuportável.

True mode

Mais instrutivo é fazer o serviço na mão. O objetivo é ativar o disco apenas esporadicamente, tipicamente a cada 10 minutos. A idéia é que os buffers sejam gravados com uma freqüência suficiente para que não se perca muita coisa caso o sistema sofra uma queda (falta de energia ou whatever), mas se mantenha o disco desligado o maior tempo possível. São três os pontos que exigem configuração.

Arquivos em /proc/sys/vm

/proc/sys é uma interface para diversas variáveis que controlam o comportamento do kernel. /proc/sys/vm, em particular, contém as variáveis que controlam o gerenciamento de memória (Virtual Memory) do kernel. Essas variáveis podem ser manipuladas lendo e escrevendo nos arquivos, assim:

# cat /proc/sys/vm/laptop_mode 
0
# echo 2 >/proc/sys/vm/laptop_mode 
# cat /proc/sys/vm/laptop_mode 
2
As variáveis relevantes para o funcionamento do laptop mode são:
Arquivo Descrição Valor
padrão
Valor
laptop
laptop_mode Define quantos segundos o kernel espera quando ocorre um acesso ao disco para começar a despejar os buffers. 0 desativa o laptop mode. 0 2
dirty_writeback_centisecs Intervalo (em centésimos de segundo) entre ativações do daemon pdflush, que despeja os buffers sujos para o disco. 0 desativa o despejo periódico. 500 60000
dirty_expire_centisecs Tempo (em centésimos de segundo) depois do qual uma página suja é considerada velha o suficiente para ser elegível a ser despejada em disco na próxima ativação do pdflush. 3000 60000
dirty_ratio Percentual de páginas (da memória total) máximo que um processo pode sujar sem forçar um despejo em disco. 40 60
dirty_background_ratio Sem laptop mode: Percentual de páginas sujas (da memória total) que provoca a ativação do pdflush.

Com laptop mode: Percentual de páginas sujas máximo permitido depois de um despejo em disco causado por um processo que tenha excedido a dirty_ratio.

10 1

Os itens da coluna "Valor laptop" indicam valores típicos usados em laptop mode (roubados dos padrões do laptop-mode-tools e de alguma página que já não encontro mais).

Tempo de commit dos sistemas de arquivo

ext3 e outros sistemas de arquivo com journaling realizam um commit periódico (dos dados? dos metadados? do journal? de tudo? respostas são bem-vindas). No caso do ext3, o padrão é fazer um commit a cada 5 segundos. O intervalo de commit de um filesystem pode ser alterado com um comando do tipo:

mount filesystem -o remount,commit=600

onde filesystem pode ser um nome de dispositivo (e.g., /dev/sda1) ou um ponto de montagem (e.g., /).

Tempo de standby do disco e modo de economia de energia

Por fim, agora que evitamos que o disco seja acessado, podemos mandá-lo dormir quando não há acessos. Para isso, usamos o comando hdparm. O hdparm permite configurar dúzias de parâmetros do disco. Os que nos interessam no momento são as seguintes opções:

A sintaxe do hdparm é hdparm opções /dev/dispositivo.

Tudo de uma vez

Agora podemos fazer um pequeno script para ativar e desativar o laptop mode, no que diz respeito ao HD. Você pode colocar esse script em /usr/local/bin/set-laptop-mode, por exemplo. (O script segue a metodologia Facão-Driven Development™, e não se preocupa muito com flexibilidade de configuração. Afinal, basta alterar o fonte!)

#!/bin/bash

# Status atual do laptop mode. Evitaremos executar os comandos
# se o sistema já estiver no modo que queremos.
current_status="$(</proc/sys/vm/laptop_mode)"

# Mas se a primeira opção for -f, fazemos mesmo assim.
[[ $1 = -f ]] && { shift; current_status=-1; }

set_commit_time() {
    # set_commit_time DISK SECONDS
    # Ajusta o commit time de todos os filesystems do dispositivo DISK.
    for filesystem in $(mount | cut -d' ' | grep "^$DISK"); do
        mount "$filesystem" -o remount,commit="$SECONDS"
    done
}

case "$1" in
    on)
        [[ $current_status -gt 0 ]] && exit
        echo 60000 >/proc/sys/vm/dirty_expire_centisecs
        echo 60000 >/proc/sys/vm/dirty_writeback_centisecs
        echo 1 >/proc/sys/vm/dirty_background_ratio
        echo 60 >/proc/sys/vm/dirty_ratio
        echo 2 >/proc/sys/vm/laptop_mode
        set_commit_time sda 600
        hdparm -B 1 -S 4 /dev/sda
        ;;
    off)
        [[ $current_status -eq 0 ]] && exit
        echo 3000 >/proc/sys/vm/dirty_expire_centisecs
        echo 500 >/proc/sys/vm/dirty_writeback_centisecs
        echo 10 >/proc/sys/vm/dirty_background_ratio
        echo 40 >/proc/sys/vm/dirty_ratio
        echo 0 >/proc/sys/vm/laptop_mode
        set_commit_time sda 5
        hdparm -B 128 -S 0 /dev/sda
        ;;
    *)
        echo "Usage: ${0#*/} [-f] on|off" >&2
        exit 1
        ;;
esac

Agora basta dar permissão de executável para o camadada (chmod +x /usr/local/bin/set-laptop-mode), e ativar e desativar o laptop mode com os comandos set-laptop-mode on e set-laptop-mode off.

Mas eu quero automático

Bom, falta fazer o sistema chamar o script sozinho quando o laptop é ligado ou desligado da tomada. Para tal, podemos usar o acpid, um daemonzinho feliz que espera por eventos ACPI e executa comandos de acordo com os eventos que observa. ACPI trata-se do Advanced Configuration and Power Interface, um mecanismo para gerenciamento de energia e controle de certos dispositivos (notavelmente o botão Power da maior parte das máquinas e os botões multimídia de certos notebooks).

O acpid lê os arquivos de configuração contidos em /etc/acpi/events. Cada arquivo tem a forma:

event=regex
action=comando

O acpid identifica os eventos gerados pelo ACPI através de strings (tais como ac_adapter ACPI0003:00 00000080 00000000). Quando um evento é observado, o acpid procura por todos os arquivos cuja regex case com a string do evento, e executa os comandos associados. Na string do comando, o valor %e é substituído pela string do evento, e %% por um % literal. Como o comando é processado pelo shell, deve-se usar aspas em torno do %e caso não se queira que o shell splite a string de evento em palavras ou faça algum outro tipo de expansão.

Podemos então criar um arquivinho /etc/acpi/auto-laptop-mode:

event=ac_adapter.*
action=/usr/local/bin/auto-laptop-mode

Isso fará com que, toda vez que a máquina seja ligada ou desligada da tomada, o script /usr/local/bin/auto-laptop-mode seja chamado. Falta escrever o tal script. Ao invés de usarmos os números místicos da string de evento para determinar se a máquina foi ligada ou desligada, vamos ao invés disso usar o comando acpi para determinar o status da bateria (que é Discharging caso a máquina não esteja na tomada). Uma vantagem disso é que poderemos chamar o script por fora do ACPI (por exemplo, na inicialização da máquina, ou na cron).

O comando acpi imprime uma string do tipo:

Battery 0: Discharging, 97%, 01:56:46 remaining

Podemos extrair os dados que nos interessam com um sed:

# acpi | sed -r 's/^[^:]*: ([^,]*), ([0-9]+)%.*/\1 \2/'
Discharging 97

Nosso script ativará o laptop mode se a máquina não estiver na tomada e houver pelo menos 5% de carga, e o desativará caso contrário.

#!/bin/bash

# Coloca o status e o percentual de carga em $1 e $2.
set -- $(acpi | sed -r 's/^[^:]*: ([^,]*), ([0-9]+)%.*/\1 \2/')

if [[ $1 = Discharging && $2 -gt 5 ]]; then
    set-laptop-mode on
else
    set-laptop-mode off
fi

E está feito. Só temos um problema: quando a bateria ficar fraca, o evento ac_adapter não será gerado, e portanto o laptop mode não será desativado automaticamente. Há duas soluções para esse problema:

  1. Se a sua máquina gera eventos do tipo battery quando é (des)ligada na tomada e quando a bateria está fraca (o que não é o meu caso), você pode substituir a expressão regular do evento em /etc/acpi/events/auto-laptop-mode por battery.*.
  2. Se essa solução não lhe serve, você pode colocar uma tarefa na cron para executar o script uma vez por minuto. É por essa razão que o set-laptop-mode evita executar comandos caso seja executado múltiplas vezes: se colocado na cron, o script será chamado inúmeras vezes sem que tenha que fazer nada. Basta a seguinte linha no /etc/crontab:
    * * * * * root /usr/local/bin/auto-laptop-mode

    Você pode substituir o primeiro * por */5 para executar o script a cada 5 minutos, por exemplo.

Com isso, fizemos em cinqüenta linhas o que o laptop-mode-tools faz em umas mil, mais os módulos. Nada como o raciocínio categorial.

fsync

Tanto laptop-mode-tools quanto a nossa solução nos deixa um problema. Alguns programas fazem uso das chamadas de sistema fsync e fdatasync. Essas chamadas forçam o kernel a despejar os buffers de um arquivo para disco, e são usadas para garantir a consistência desses arquivos em caso de um crash do sistema. O kernel sempre obedece a essas chamadas, mesmo com o laptop mode ativo, fazendo com que o disco seja ativado. Se algum programa faz uso freqüente dessas chamadas, o laptop mode se torna inútil.

Três são os vilões mais comuns: o syslog, editores de texto como Vim e Emacs, e o Firefox.

O syslog pode ser configurado para não forçar o despejo dos logs adicionando-se um - antes dos nomes dos arquivos para os quais se deseja evitar o fsync nos arquivos de configuração (/etc/rsyslog.conf e /etc/rsyslog.d/*).

O Vim faz o fsync dos arquivos quando são salvos. Isso pode ser controlado pela opção fsync: :set fsync ativa o despejo automático (o padrão), e :set nofsync desativa. Além disso, o Vim faz o fsync dos swapfiles, arquivos temporários que são usados para recuperação de modificações caso haja um crash durante a edição antes de o arquivo ser salvo. Isso é controlado pela opção swapsync: se seu valor for fsync (o padrão), é usada a chamada a fsync na gravação do swapfile; se seu valor for sync, uma chamada a sync é feita; se for a string vazia, nada é feito.

Uma conseqüência de não fsyncar os arquivos é que se houver um crash seu trabalho pode ser perdido. Uma alternativa seria não sincronizar os arquivos quando salvos, mas sincronizar os arquivos de swap, e colocá-los em uma outra unidade (um pendrive ou um SD-card, por exemplo). Assim, se houver um crash os swapfiles são capazes de recuperar o arquivo, mas se evita escrever no HD. Note que normalmente, no momento em que o Vim salva um arquivo, ele considera que a informação atual do swapfile não é mais necessária, já que todas as modificações já foram para o disco, o que é falso caso desativemos o fsync. Para fazer com que o swapfile contenha toda a informação necessária para a recuperação do arquivo, pode-se usar o comando :preserve.

Uma pequena dose de Vimscript no ~/.vimrc dá conta de fazer tudo isso automaticamente:

function LaptopModeWrite()
    if readfile("/proc/sys/vm/laptop_mode")[0] == 0
        set fsync swapsync=fsync directory-=/media/seu-pendrive-ou-sdcard
    else
        set nofsync swapsync= directory^=/media/seu-pendrivre-ou-sdcard
        preserve
    fi
endfunction

call LaptopModeWrite()
autocmd BufWritePre * call LaptopModeWrite()

O caso do Firefox é mais complicado. O Firefox usa a biblioteca SQLite para guardar o histórico e favoritos, e a SQLite usa fsyncs para manter a consistência dos bancos de dados. Além disso, o Firefox fsynca um arquivinho chamado sessionrestore.js, que ele usa para recuperar as tabs abertas no caso de um crash do browser. Aparentemente não há uma opção para desativar o uso de fsync no Firefox. As únicas alternativas que nos restam são mover os arquivos em questão para outra unidade, ou alterar o fonte do Firefox e da SQLite para não chamar fsync.

Ou alterar a fsync para não fazer nada.

LD_PRELOAD

Que me conste, não há uma maneira "normal" de substituir uma syscall sem modificar o kernel. Entretanto, a maior parte dos programas não chama syscalls diretamente, mas sim por intermédio de funções da biblioteca padrão C (libc), que se encarregam de chamar as syscalls. Acontece que a libc, como a maior parte das bibliotecas, é uma biblioteca dinamicamente linkada. Isso significa que as chamadas para funções da biblioteca no programa não saltam para endereços fixos, mas sim para endereços contidos em uma tabela de indireção que é preenchida pelo linker quando o programa é iniciado. Isto é, o linker, quando carrega o programa, olha os nomes das funções que o programa procura na biblioteca, obtém seus endereços, e preenche a tabela, de modo que quando o programa chamar as funções, as chamadas apontarão para os lugares apropriados.

Acontece que o ld.so (o linker) permite sacanear esse processo. Se a variável de ambiente LD_PRELOAD existe, o linker a interpreta como uma lista (separada por espaços) de bibliotecas a serem carregadas antes de quaisquer outras. Se uma dessas bibliotecas redefinir um símbolo (por exemplo, a função fsync), a versão redefinida é a que vai parar na tabela de indireção do programa, e conseqüentemente chamadas à tal função caem na nossa função, e não na original. Isso quer dizer que podemos interceptar as chamadas a fsync e fdatasync que um programa qualquer faça, e repassá-las ou não para a fsync "legítima" segundo queiramos ou não.

A coisa mais simples que podemos fazer é simplesmente descartar todas as chamadas a fsync. Criemos um arquivo libnofsync.c:

#include <stdio.h>

int fsync(int fd) {
    fprintf(stderr, "Chamada a fsync interceptada.\n");
    return 0;
}

Agora compilemo-lo com gcc -Wall -shared -o libnofsync.so libnofsync.c. Está feito! Agora, se executarmos um export LD_PRELOAD=caminho-do-arquivo/libnofsync.so, todo programa que for executado a partir de agora nesse shell utilizará a fsync modificada. Experimente abrir o Firefox.

Podemos elaborar mais sobre o tema, entretanto. Idealmente, queremos interceptar fsyncs e fdatasyncs apenas quando o laptop mode estiver ativado, e apenas quando o arquivo a ser despejado se encontra no disco. Caso decidamos que o fsync é permitido, precisamos chamar a fsync legítima; para isso, utilizaremos a função dlsym, da biblioteca dl (dynamic linker), que permite procurar um símbolo por nome em uma biblioteca. Para mais detalhes, dê uma olhada na manpage da dlopen.

#include <stdio.h>
#include <dlfcn.h>  //define dlsym() e RTLD_NEXT

#include <sys/types.h>     // para uso da fstat()
#include <sys/stat.h>
#include <sys/unistd.h>

// Variáveis globais onde armazenaremos ponteiros para as versões legítimas
// das funções. 'static' faz com que as variáveis não sejam exportadas
// (visíveis a partir do programa ou de outras bibliotecas.)
static int (*sys_fsync)(int);
static int (*sys_fdatasync)(int);

// Função de inicialização da biblioteca. Qualquer função marcada por
// __attribute__((constructor)) é considerada pelo GCC/linker como uma função
// que deve ser chamada quando a biblioteca é carregada.

__attribute__((constructor)) void libnofsync_init() {
    // Procura por 'fsync' e 'fdatasync' nas bibliotecas que vêm depois da
    // atual na ordem de pesquisa de símbolos.
    sys_fsync = dlsym(RTLD_NEXT, "fsync");
    sys_fdatasync = dlsym(RTLD_NEXT, "fdatasync");
}

static int laptop_mode_enabled() {
    FILE *f = fopen("/proc/sys/vm/laptop_mode", "r");
    if (!f) return 1;
    int status = 0;
    fscanf(f, "%d", &status);
    fclose(f);
    return status;
}

static int fsync_allowed(int fd) {
    // Se não estamos em laptop mode, sempre permite.
    if (!laptop_mode_enabled())
        return 1;

    // Se o "major number" do dispositivo do arquivo for 0x08, trata-se do
    // dispositivo /dev/sda. Você pode descobrir o major number do seu disco
    // executando um 'stat -c %D /' e desprezando os dois últimos dígitos.
    struct stat info;
    if (fstat(fd, &info) < 0) return 1;
    if (major(info.st_dev) == 0x08) return 0;
    return 1;
}

static int handle_fsync(int fd, int (*sysfun)(int)) {
    if (fsync_allowed(fd))
        return sysfun(fd);
    else
        return 0;
}

int fsync(int fd) { return handle_fsync(fd, sys_fsync); }
int fdatasync(int fd) { return handle_fsync(fd, sys_fdatasync); }

E aí está. Para compilá-la, é necessário incluir a opção -ldl na linha de comando do GCC, para linkar a biblioteca com a biblioteca dl.

Caveat

Essa história de desligar e ligar o HD consome sua vida útil. Segundo alguém na Internet, HDs de laptops são feitos para durar cerca de 300 mil ciclos de liga-desliga. HDs de desktops duram ainda menos, por volta de 50 mil ciclos. Por isso, o ideal é evitar que o disco seja ligado e desligado com muita freqüência; por exemplo, enquanto se está em laptop mode, ficar lendo mil arquivos diferentes a intervalos espaçados (suficientes para que o HD tenha desligado) não é uma boa idéia, pois os arquivos não estarão em cache quando forem lidos. O laptop-mode-tools vem com uma ferramenta chamada lm-profiler, que é capaz de lhe dizer que programas estão produzindo acessos a disco. O que essa ferramenta faz é basicamente dar um echo 1 >/proc/sys/vm/block_dump, o que faz com que o kernel reporte todos os acessos a disco pelo log do kernel, acessível pelo comando dmesg, ou por cat /proc/kmsg, caso o daemon de logging não esteja ativo. (Aliás, é bom que ele não esteja, pois escrever nos logs pode gerar acessos extra ao disco.) Não tenho dados exatos de quanta bateria se economiza com o laptop mode, embora reze a lenda que é "bastante". Use com cautela.

1 comentário / comment

Diversão com keymaps no X

2012-08-20 04:35 -0300. Tags: comp, unix, mundane, kbd, em-portugues

Em que o poeta compartilha seus nem tão vastos conhecimentos sobre alteração de keymaps no X. Em um post futuro, pretendo falar sobre as modificações que fiz em meu keymap e que achei úteis.

Introdução ao caos

Keymaps no X funcionam da seguinte maneira: A cada tecla está associado um keycode, um número único para cada tecla. Ao pressionar (ou soltar) uma tecla, o X envia o keycode ao cliente que tem o foco do teclado. O cliente, então, pode fazer o que bem entender com esse keycode. Na maior parte dos casos, entretanto, o cliente utiliza-se da keymap table que o servidor X armazena para converter um keycode em um keysym, que dá o significado da tecla em questão. Por exemplo, ao pressionar a tecla Enter, o X repassa para o cliente o keycode 36 (em um teclado comum de PC), que normalmente está associado a um keysym Return na tabela de keymap. Você pode alterar a keymap table usando um programinha chamado xmodmap. Alternativamente, você pode usar alguma outra interface para realizar essa tarefa, como o programa gráfico XKeyCaps (que altera a keymap table em tempo real e gera um arquivo de configuração para o xmodmap).

A cada keycode está associado mais de um keysym; o keysym apropriado depende dos modificadores que estão ativos no momento em que a tecla é pressionada. Em geral, as teclas Shift e AltGr estão associadas a modificadores que influenciam na seleção do keysym (e.g., em um teclado ABNT2, o keycode 10 está associado aos keysyms 1, exclam (Shift+1) e onesuperior (AltGr+1)). [O Caps_Lock também controla um modificador, que não é o mesmo do Shift; mais informação adiante.]

Insatisfeitos com os poderes do xmodmap, o povo do X inventou o Xkb, um mecanismo mais flexível para a especificação de keymaps. Em tese, a criação do Xkb torna o xmodmap obsoleto, mas como disse um sábio:

In principle, Xkb is supposed to take over. In practice, there are only three people in the known universe who understand Xkb, and nobody is quite sure who they are.

O xmodmap, embora apresente o ocasional bug, é bem mais simples de usar do que o Xkb. (Os relatos de experiências com a migração do xmodmap para o Xkb que eu vejo por aí não são muito animadores.) Ainda não estudei o Xkb direito para poder dar instruções; talvez eu poste algo sobre ele no futuro. Para mais informações, vide links. Neste post, focarei no xmodmap.

Fundamentos de xmodmap

Usualmente, o xmodmap lê de um arquivo uma série de comandos, um por linha, que indicam as alterações que se deseja realizar no keymap. O tipo de comando mais simples é da forma:

keycode número = keysym1 keysym2 ...

Esse comando associa os keysyms ao keycode especificado. Até 8 keysyms podem ser especificados em um comando keycode. O primeiro keysym é usado quando a tecla é pressionada sozinha. O segundo é usado quando a tecla é pressionada enquanto o Shift está ativo. O terceiro é usado quando Mode_switch está ativo (já chegamos lá); o quarto com Mode_switch+Shift; o quinto com ISO_Level3_Shift; o sexto com ISO_Level3_Shift+Shift.

Agora, que diabos são Mode_switch e ISO_Level3_Shift?

No começo dos tempos, Mode_switch correspondia ao AltGr. Os quatro primeiros keysyms correspondiam à tecla pura, Shift, AltGr e AltGr+Shift. E o mundo era feliz.

Por algum diabo, em algum ponto da história o AltGr passou a ser mapeado como ISO_Level3_Shift na maior parte dos keymaps padrão do X. A conseqüência mais visível disso é que o terceiro e quarto keysym da tabela do xmodmap não fazem nada no keymap padrão. Conseqüentemente, se você pretende atribuir um keysym diferente para AltGr+alguma-coisa, você deve preencher as colunas 3 e 4 com algum valor tapa-buraco (e.g., NoSymbol, ou uma cópia das colunas 5 e 6, ou qualquer coisa).

Se o problema fosse só esse, seria apenas um pequeno inconveniente. O problema é que o tal ISO_Level3_Shift é uma invenção do Xkb que não interage muito bem com o xmodmap. Se uma tecla não possui um valor associado a ISO_Level3_Shift+tecla no keymap padrão, não é possível associar um valor a essa combinação via xmodmap; o X simplesmente ignorará o valor das colunas 5 e 6. Por exemplo, se você está usando um keymap us-intl (US Internacional) com AltGr mapeado para ISO_Level3_Shift, você conseguirá alterar o keysym da combinação AltGr+a, pois essa combinação existe no keymap original, mas você não conseguirá atribuir efetivamente um keysym a AltGr+j, pois essa combinação não possui mapeamento no us-intl. Da mesma forma, você não conseguirá mapear AltGr-↑.

A solução mais simples para esse problema é simplesmente remapear o AltGr para Mode_switch e ser feliz para sempre. O problema é que nesse caso você perde todos os mapeamentos com AltGr que possam existir originalmente no seu keymap padrão, pois eles estão associados ao ISO_Level3_Shift. Por exemplo, se você remapear o AltGr para Mode_switch em um layout ABNT2, você perderá o mapeamento padrão de AltGr+1 para ¹ e terá que remapeá-lo manualmente (ou com um script; vejamos mais adiante) se desejá-lo.

Evidentemente, isso só é um problema se você deseja usar o AltGr com alguma tecla não prevista no layout original.

A solução de remapear o AltGr não resolve o problema se você deseja ter tanto o Mode_switch quanto o ISO_Level3_Shift no mesmo teclado (permitindo associar seis keysyms por tecla). Nesse caso, você vai ter que arranjar um keymap do Xkb que mapeie todas as combinações desejadas com ISO_Level3_Shift. O keymap br(abnt2) parece servir bem para esse propósito. Em último caso, você pode experimentar alterar os arquivos /usr/share/X11/xkb/symbols/us e companhia e adicionar o que falta (mas nesse caso não teria mais por que usar o xmodmap, em tese (exceto pelo fato de que é mais prático alterar o teclado posteriormente via xmodmap do que alterando os arquivos de sistema)).

Quanto às colunas 7 e 8, elas aparentemente não servem para nada. Dizem as más línguas que elas deveriam conter o keysym associado à combinação Mode_switch+ISO_Level3_Shift+Shift+tecla, mas nunca consegui fazer funcionar. Tentar criar um modificador ISO_Level4_Shift ou ISO_Level5_Shift também não foi eficaz. (Por sinal, o keysym ISO_Level4_Shift não existe, mas ISO_Level5_Shift sim. Go figure.)

E como eu descubro os keycodes e keysyms?

O X vem com um programinha supimpa chamado xev. Esse programa cria uma janela e reporta pela stdout (o terminal por onde você abriu o xev) todos os eventos que essa janela recebe. Assim, ao pressionar uma tecla, o xev imprimirá algo do tipo:

KeyPress event, serial 33, synthetic NO, window 0x2200001,
root 0x69, subw 0x0, time 39264530, (38,371), root:(1137,799),
state 0x0, keycode 98 (keysym 0xff52, Up), same_screen YES,
XLookupString gives 0 bytes: 
XmbLookupString gives 0 bytes: 
XFilterEvent returns: False

Com isso, você consegue descobrir o keycode da tecla (para poder atribuir keysyms) e o keysym que ela gera atualmente (para poder atribuí-lo a outra tecla).

Se você deseja atribuir a uma tecla um keysym que ainda não existe no seu keymap e que você não sabe o nome (e.g., se você quiser atribuir o caractere ĉ à combinação Mode_switch+c), você pode:

  1. usar Uxxxx, onde xxxx é o código Unicode hexadecimal do caractere desejado (no caso de o símbolo desejado ser um caractere, e não um XF86AudioStop da vida); ou
  2. olhar uma tabela de keysyms, tal como a tabela incompleta em /usr/share/X11/XKeysymDB (que normalmente acompanha o X) ou a tabela completa em /usr/include/X11/keysymdef.h (descartando o prefixo XK_; esse arquivo vem no pacote x11proto-core-dev no Debian); ou
  3. adivinhar o nome do símbolo. Nomes típicos incluem:
    • dead_acute, dead_diaresis, dead_circumflex, dead_tilde, dead_macron, dead_breve, dead_abovedot, dead_cedilla, dead_caron, etc.: acentos e diacríticos de todos os sabores (uma deadkey é uma tecla que não faz nada até que seja digitado o caractere seguinte);
    • aacute, Aacute, ccedilla, Ccedilla, etc.: caracteres acentuados prontos;
    • XF86AudioMute, XF86AudioLowerVolume, XF86AudioRaiseVolume, XF86AudioPlay, XF86AudioStop, XF86AudioPrev, XF86AudioNext, XF86PowerOff, etc.: teclas "multimídia" e firulas relacionadas;

O comando keysym

Em alguns casos você não precisa saber o keycode de uma tecla para mapeá-la. O xmodmap reconhece o comando:

keysym keysym_velho = keysym1 keysym2 ...

Nesse caso, o xmodmap procura por todos keycodes que atualmente estão associados ao keysym_velho, e lhes atribui os valores novos. Isso é particularmente útil quando se deseja apenas associar valores novos às combinações com AltGr, sem alterar o valor puro da tecla:

! O primeiro par ccedilla/Ccedilla para o Mode_switch, o segundo para o ISO_Level3_Shift.
keysym c = c C ccedilla Ccedilla ccedilla Ccedilla

Para modificações que alteram o valor puro da tecla, entretanto, usar o comando keysym pode não ser uma boa idéia. Suponha que você deseja trocar os parênteses e os colchetes de lugar no teclado:

keysym 9 = 9 bracketleft
keysym 0 = 0 bracketright
keysym bracketleft = parenleft braceleft
keysym bracketright = parenright braceright

A aplicação dessas modificações no keymap original funciona (o xmodmap é esperto o suficiente para primeiro avaliar todos os lados esquerdos do =, e depois efetuar as mudanças, de modo que a alteração das duas primeiras linhas não interfere no funcionamento das duas últimas numa mesma execução). Porém, se esse arquivo de keymap for carregado uma segunda vez, catástrofes acontecem: as duas primeiras linhas fazem o que deveriam, mas as duas últimas associam parênteses a todas as teclas que atualmente estão mapeadas para colchetes, ou seja, as teclas 9 e 0! Como os comandos são avaliados na ordem, as duas últimas linhas sobrepõem o mapeamento de 9 e 0, de modo que tanto 9 e 0 quanto as teclas de colchetes agora contêm parênteses!

Evidentemente, o problema só surge se você carregar o arquivo duas vezes. Porém, enquanto você está experimentando com keymaps, você provavelmente vai querer fazer alterações e recarregar o arquivo de keymap diversas vezes sem ter que reiniciar o X. Em qualquer caso, parece melhor escrever um keymap que faz a coisa certa quando é carregado múltiplas vezes do que encontrar surpresas no futuro e levar mil instantes para entender o que está acontecendo.

Moral da história: só use o keysym para alterar valores secundários das teclas, e isso só se você tiver certeza de que keysym_velho só ocorre uma vez no keymap, ou que você queira realmente alterar todas as teclas associadas ao keysym_velho.

Modifiers

Remapear modificadores (Ctrl, Alt, Shift, Caps_Lock, JanelinhaFeliz, etc.) envolve alguns detalhes mais.

O X mantém uma tabela de modificadores; se você executar xmodmap sem parâmetros, ele imprimirá essa tabela. Essa tabela associa keycodes a modificadores. Os modificadores reconhecidos são control, shift (auto-explicativos), lock (correspondente ao Caps_Lock), e mod1 até mod5 (que não têm significado pré-definido). Sempre que você altera o mapeamento de uma tecla associada a modificadores, você deve lembrar de reajustar a tabela de modificadores também. Para controlar essa tabela, o xmodmap provê os comandos add, remove e clear.

add modifier_name = keysym ... adiciona as teclas associadas aos keysyms especificados à lista de teclas que ativam o modificador. Se você executar um comando do tipo add shift = a, o efeito será que a tecla A, além de funcionar como um A normal, também passará a funcionar como um Shift: se você segurar a tecla A enquanto digita outras teclas, o efeito será o mesmo de segurar Shift ao mesmo tempo que as outras teclas.

remove modifier_name = keysym ... desassocia todas as teclas associadas aos keysyms especificados do modificador em questão. clear modifier_name desassocia todas as teclas do modificador. O fato de que os comandos trabalham com keysyms mas a tabela trabalha com keycodes gera todo tipo de bizarrice na manipulação de keymaps. No geral, o mais simples, prático e indolor é, sempre que se remapeia teclas associadas a modificadores, limpar os modificadores em questão, fazer o remapeamento e adicionar as teclas novamente.

Exemplo: para inverter a posição usual do Ctrl da esquerda e do Caps_Lock:

clear lock
clear control
! Keycodes do Ctrl e Caps_Lock obtidos com o xev. Os valores podem ser diferentes no
! seu teclado / versão do X.
keycode 66 = Control_L
keycode 37 = Caps_Lock
add lock = Caps_Lock
! Não esqueça de readicionar o Ctrl da direita, que foi apagado no clear.
add control = Control_L Control_R

Opções de linha de comando do xmodmap

Além do xmodmap arquivo, para carregar um arquivo com comandos, o xmodmap suporta algumas outras opções. Em primeiro lugar, usando - como arquivo, o xmodmap lê da stdin. Isso é primariamente útil se por alguma razão você quiser gerar um keymap por script e alimentá-lo ao xmodmap. Além disso, o xmodmap suporta uma opção -e comando, que permite executar um comando de keymap diretamente a partir da linha de comando. Por exemplo:

xmodmap -e 'keysym a = a A ae AE'

Cada comando consiste de apenas um comando de keymap, mas a opção -e pode ser repetida.

Outro comando útil é xmodmap -pke, que imprime o keymap atual no formato que o xmodmap lê. Assim, você pode salvar o keymap atual com xmodmap -pke >.xmodmaprc e carregá-lo posteriormente (em outra sessão do X) com xmodmap .xmodmaprc. Isso é útil se você fez diversas modificações no layout pela linha de comando e deseja salvar o estado atual sem ter que encontrar todos os comandos dados e agrupá-los em um arquivo. (O xmodmap -pke não imprime linhas add, remove e clear, entretanto.) Isso também é útil para salvar o keymap atual antes de começar a bagunçá-lo, para poder retornar a um estado consistente sem ter que reiniciar o X. Para isso, é bom criar um meio de invocar o xmodmap seu-teclado-original sem ter que usar o teclado (e.g., com um atalho no seu ambiente gráfico favorito).

Carregando o keymap na inicialização

Todas as modificações feitas na keymap table duram apenas até o servidor X morrer; na próxima inicialização, a tabela volta a ser o que era. Para tornar as modificações permanentes, você precisa carregá-las toda vez que sua sessão X inicia. Para isso, você deverá colocar o comando xmodmap ~/seu-arquivo-com-comandos-de-keymap em algum lugar que seja executado pelo seu ambiente gráfico na inicialização. Se você usa o xinit para carregar a interface gráfica, o arquivo é o .xinitrc, no seu home. Alguns ambientes gráficos carregam o .xsessionrc. Alguns permitem que você adicione comandos para serem executados na inicialização em alguma configuração do ambiente. Finalmente, se não estou enganado, o GNOME detecta por conta arquivos de nome .xmodmaprc e assemelhados na inicialização e pergunta se você deseja carregá-los na inicialização da primeira vez que os encontra.

Notas soltas

Na transição do driver de teclado kbd para evdev (i.e., da época do Debian 4 para o Debian 5, embora ainda seja possível usar o driver kbd hoje em dia, desde que seja instalado o pacote xserver-xorg-input-kbd), algumas teclas mudaram de keycode. Em particular, o AltGr mudou de 113 para 108, e as setas e algumas outras teclas especiais também mudaram. Isso pode causar algum problema na hora de migrar de uma versão velha do X para uma mais recente.

Eu prometi links com informações sobre o Xkb. Eis o que eu encontrei (e ainda não li, mostly):

EOF.

3 comentários / comments

Transferindo file descriptors entre processos

2012-08-04 04:35 -0300. Tags: comp, prog, unix, em-portugues

Nos últimos tempos descobri diversas features interessantes desse tal de Unix, entre elas a possibilidade de transferir um file descriptor entre dois processos. Há mil conceitos envolvidos; basicamente o que você precisa é abrir um Unix domain socket (vide man unix) entre os dois processos e usar um código tipo este para transferir o file descriptor. O objetivo deste post, entretanto, é fazer um passeio pelos mil conceitos em questão. E lá vamos nós!

File descriptions e file descriptors

Quando um processo solicita a abertura de um arquivo, o sistema cria uma descrição de arquivo correspondente: uma entrada em uma tabela global de arquivos abertos que contém a posição do "cursor" dentro do arquivo e as flags com que o arquivo foi aberto, bem como quaisquer informações internas que o sistema operacional precise para manipular o arquivo. Além disso, o processo fica de posse de um descritor de arquivo, uma referência a uma descrição de arquivo. Ao processo está associada uma tabela, acessível apenas pelo kernel, dos descritores de arquivo que o processo possui; o processo em si, ao abrir o arquivo, recebe como resultado um índice nessa tabela. (O índice também é chamado de "descritor de arquivo".) Como o processo trabalha apenas com índices na tabela de descritores de arquivo, mas não tem acesso direto nem à tabela nem ao conteúdo dos descritores/descrições, é impossível falsificar um descritor de arquivo; um arquivo só é acessível a um processo se o sistema operacional lhe entregar um descritor de arquivo correspondente, e o sistema só o fará se o processo tiver permissão para obter o descritor. Assim, um descritor de arquivo tem características de uma capability: a posse de um descritor é prova suficiente de que o processo tem o direito de acessar o arquivo.

A maneira mais básica de se obter um descritor de arquivo é por meio da chamada de sistema open:

int fd = open("/etc/passwd", O_RDWR)

Essa chamada recebe um caminho para um arquivo e um conjunto de flags (unidas por bitwise OR), e retorna um (índice para um) descritor de arquivo correspondente. Caso o arquivo não possa ser aberto, a chamada (como a maior parte das chamadas de sistema do Unix) retorna -1 e indica o erro ocorrido setando a variável global errno. Em um Unix, a função fopen do C padrão chama open internamente, e associa um descritor de arquivo à estrutura FILE * que retorna. Dada uma estrutura FILE *, você pode obter o descritor de arquivo correspondente usando a função fileno(handler). Você também pode criar um FILE * a partir de um descritor de arquivo usando a função fdopen(fd, modo), onde modo é um argumento do mesmo tipo que se passa para a função fopen.

Há diversas outras chamadas que criam e retornam descritores de arquivo. Algumas dessas chamadas associam descritores de arquivos a coisas que não são exatamente arquivos, tais como pipe (que cria dois descritores que não correspondem a nenhum arquivo físico; tudo o que é escrito no primeiro descritor pode ser lido através do segundo) e as chamadas para criação de sockets. Talvez "resource descriptor" fosse um nome mais apropriado do que "file descriptor" para essas criaturas. Enfim, não fui eu que fiz.

(Um ponto interessante dessa história é que como um socket está associado a um descritor de arquivo, você pode usar fdopen para criar um FILE * correspondente, e a partir daí usar as funções comuns para leitura e escrita em arquivos em C (fgets, fprintf, etc.) para enviar e receber dados pelo socket. Genial, não?)

Herança

Um processo pode criar novos processos através da chamada fork(). Essa chamada cria uma cópia do processo atual, apenas com o PID e algumas outras informações modificadas. A chamada retorna 0 para o novo processo (dito processo filho), e o PID do processo recém criado para o processo pai. A execução do código continua do mesmíssimo ponto (o retorno da chamada a fork) em ambos os processos; o valor retornado pode ser usado para determinar quem é quem:

pid_t pid;

// Cria um novo processo.
if (pid = fork()) {
    // Código que será executado pelo processo pai.
    ...
}
else {
    // Código que será executado pelo processo filho.
    ...
}

O processo filho recebe uma cópia da tabela de descritores de arquivo do processo pai, i.e., ele continua com os mesmos arquivos abertos. Um ponto importante é que embora a tabela de descritores seja uma cópia (i.e., se um dos processos fechar um arquivo, o outro continuará com o mesmo aberto), as entradas dessas tabelas apontam para as mesmas descrições (i.e., se um processo muda a posição do cursor ou as flags de um arquivo, o outro verá as mudanças).

É possível substituir o programa que está sendo executado por um processo, através das funções da família exec* (que são wrappers para a chamada execve). Os detalhes variam para cada função, mas basicamente elas recebem um nome de arquivo correspondente a um programa e os argumentos a serem passados para o programa, e substituem o programa atual pelo novo programa. É como se tivéssemos simplesmente chamado o outro programa, com a diferença de que o programa é executado com o mesmo PID do processo atual. O novo programa também herda os descritores de arquivo abertos (desde que a flag FD_CLOEXEC (close-on-exec) não esteja ativa no descritor; vide open e fcntl).

De fato, "chamar" um novo programa no Unix consiste em duas etapas: criar um subprocesso através de uma chamada a fork, e substituir o executável pelo programa que se deseja chamar. É isso que a famosa função system faz por baixo dos panos, basicamente.

Lembre-se de que a posse de um descritor é suficiente para garantir o acesso do processo ao arquivo correspondente. Um processo que herde um descritor de arquivo carrega com ele as mesmas permissões de acesso ao arquivo, mesmo que o usuário dono do processo seja outro. Por exemplo, um processo rodando como root pode abrir um arquivo A acessível apenas pelo root e criar um subprocesso que passa a executar com um usuário comum; o subprocesso continuará podendo acessar o arquivo A, mesmo que em circunstâncias normais ele não pudesse abrir o arquivo. (Afinal, o que ele não pode é abrir o arquivo; usar um arquivo que ele já recebeu aberto é outra história.) Isso permite, por exemplo, que um processo comece executando como root, obtenha certos recursos que só podem ser obtidos como root (e.g., um socket ouvindo em uma porta menor que 1024, ou acesso ao buffer de uma placa de vídeo), e passe a executar com um usuário com menos privilégios. Assim, o processo só tem acesso aos recursos de que necessita, sem receber mais permissões do que o necessário; isso reduz o potencial de danos caso o programa sofra um ataque.

Pipes e sockets

Um pipe é um canal de comunicação unidirecional orientado a bytes. Um pipe possui duas pontas: o que se escreve em uma das pontas pode ser lido pela outra. Você deve conhecer as pipelines do shell:

ls | grep foo

A função da pipeline é fazer com que a saída de um processo seja alimentada como entrada de outro. Pois bem, o que o shell faz ao se deparar com a pipeline acima é:

  1. Criar um pipe;
  2. Criar um subprocesso, substituir a stdout do processo por uma ponta do pipe, e substituir o programa do processo atual pelo ls;
  3. Criar outro subprocesso, substituir a stdin do processo pela outra ponta do pipe, e substituir o programa do processo atual pelo grep foo;
  4. Deixar a galera executar.

Assim, o ls herda como stdout uma ponta do pipe, e o grep herda a outra ponta como stdin; conseqüentemente, tudo que o ls imprimir para a stdout vai parar na stdin do grep.

Pipes são criados pela chamada pipe: ela recebe um vetor de duas posições, que serão preenchidas com dois descritores de arquivo correspondentes às duas pontas do pipe. Essa chamada cria um pipe anônimo: os descritores retornados não correspondem a nenhum arquivo fisicamente presente no sistema de arquivos. Também é possível criar um pipe nomeado, de modo que é possível se referir ao pipe como um arquivo comum. Um pipe nomeado pode ser criado com a função mkfifo, e pode ser aberto, lido e escrito como um arquivo comum.

Um pipe é um canal unidirecional: se escreve apenas por um lado, e se lê apenas pelo outro. Não há como o processo que recebeu a segunda ponta do pipe transferir informações de volta para o processo que está de posse da primeira ponta. Um socket, por outro lado, é uma criatura bidirecional: pode-se ler e escrever a partir de ambos os lados do socket. Sockets são a API genérica para criação de canais de comunicação no Unix. Existem diversas famílias de sockets (e.g., AF_INET (sockets TCP/IPv4), AF_INET6 (sockets TCP/IPv6), AF_UNIX (Unix domain sockets)), e diversos tipos de socket (e.g., SOCK_STREAM (socket orientado a bytes), SOCK_DGRAM (socket orientado a mensagens com limites bem-definidos)). Os detalhes do funcionamento de um socket variam de família para família.

Normalmente quando se fala de sockets, tem-se em mente os sockets da família TCP/IP. Nesse caso, cada ponta do socket é identificada por um IP e uma porta. No caso do TCP (por oposição a UDP), há um lado cliente e um servidor. O servidor:

  1. Cria um socket:
    int sock = socket(AF_INET, SOCK_STREAM, 0);
  2. Associa um endereço e uma porta local ao socket:
    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(5000);
    server_addr.sin_addr = INADDR_ANY;
    bind(sock, &server_addr, sizeof(server_addr));
    
  3. Indica que o socket é passivo, i.e., servirá para esperar por conexões de clientes:
    listen(sock, 8);
  4. Aguarda por conexões:
    while (1) {
        struct sockaddr_in client_addr = {0};
        int len = sizeof(client_addr);
        int clientfd = accept(sock, &client_addr, &len);
    
        // A partir daqui, é possível ler e escrever em clientfd para
        // trocar dados com o cliente, assumindo que não tenha ocorrido
        // nenhum erro.
    
        close(clientfd);
    }
    

O cliente, por sua vez:

  1. Cria um socket:
    int sock = socket(AF_INET, SOCK_STREAM, 0);
  2. Abre uma conexão com o servidor:
    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(5000);
    server_addr.sin_addr = inet_addr("127.0.0.1");
    connect(sock, &server_addr, sizeof(server_addr));
    
    // A partir daqui, é possível escrever em sock para
    // trocar dados com o servidor, assumindo que não tenha ocorrido
    // nenhum erro.
    
    close(sock);
    

Um socket, porém, não é necessariamente da família TCP/IP. Um outro tipo de socket particularmente interessante são os Unix domain sockets, os sockets da família AF_UNIX. Esses sockets foram concebidos para permitir a comunicação entre processos da mesma máquina. Um Unix domain socket basicamente é uma generalização de pipe que permite a comunicação em ambas as direções. Assim como um pipe, um Unix domain socket pode ser anônimo ou nomeado. Um socket anônimo pode ser criado por uma chamada do tipo:

int endpoints[2];
socketpair(AF_UNIX, SOCK_DGRAM, 0, endpoints);

Assim como pipe, socketpair preenche um vetor de duas posições com descritores correspondentes às duas pontas do socket. Esse socket já vem pronto para uso: não é necessário usar nenhuma chamada a listen, connect, etc. para que ele possa ser usado. Como esperado, subprocessos criados posteriormente herdarão os descritores, e assim o processo pai e o filho podem trocar dados por meio do socket, cada um usando uma ponta do mesmo.

Um socket nomeado funciona de maneira similar a um socket TCP/IP: o servidor faz uma chamada a bind, utilizando como endereço uma estrutura sockaddr_un, que contém o nome do arquivo de socket a ser criado/usado, marca o socket como passivo usando listen, e espera conexões com accept. Analogamente, o cliente usa connect para conectar-se ao servidor, usando como endereço uma estrutura sockaddr_un.

Transferindo file descriptors

Sockets possuem uma maluquice que permite que informações de controle sejam enviadas juntamente com os dados propriamente ditos. No caso de Unix domain sockets, um dos tipos de informação de controle que se pode enviar consiste em um vetor de descritores de arquivo que se deseja compartilhar com o processo na outra ponta do socket. Quando o processo recebe a mensagem, o kernel cria uma cópia dos descritores de arquivo do processo de origem no processo destino. Essas mensagens especiais são enviadas e recebidas por meio de chamadas a sendmsg e recvmsg. (No caso de sockets TCP/IP, é possível usar esse mecanismo para enviar pacotes IP com as (nem tão) famosas "opções" que o IP suporta.)

A interface para especificação das mensagens especiais é bastante lamentável; se você pretende transferir descritores por esse meio, o mais prático é roubar o código de alguém ou usar uma biblioteca. Vamos, entretanto, tentar entender como funciona esse caos.

A função sendmsg recebe um socket, uma estrutura do tipo msghdr representando a mensagem a ser enviada, e um conjunto de flags. Essa estrutura msghdr contém os seguintes campos:

O buffer apontado por msg_control contém uma seqüência de "mensagens de controle". Cada uma das mensagens de controle é uma estrutura do tipo cmsghdr, que contém os seguintes campos:

Após esses campos vai o conteúdo da mensagem de controle, no nosso caso os números de um ou mais descritores de arquivo que desejamos transferir. A questão toda é como inicializar esses campos:

  1. Criamos um buffer capaz de conter todas as mensagens de controle (no caso só queremos enviar uma). O tamanho de cada entrada desse buffer pode ser calculado com a macro CMSG_SPACE: ela recebe o tamanho do dado que desejamos transferir e devolve o tamanho total da mensagem, headers inclusos. No caso, queremos enviar um inteiro (o descritor de arquivo):
    char buf[CMSG_SPACE(sizeof(int))];
  2. Inicializamos a mensagem (estrutura msghdr) para apontar para o buffer:
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);
    
  3. Obtemos um ponteiro para a primeira mensagem no buffer, e inicializamos os campos do header:
    cmsghdr *cmsg = CMSG_FIRSTHDR(msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    
  4. Copiamos os dados (no caso o nosso inteiro) para dentro do conteúdo da mensagem:
    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
    memmove(CMSG_DATA(cmsg), &fd, sizeof(int));
    
  5. Atribuimos a msg.msg_controllen a soma do CMSG_SPACE de todas as mensagens contidas no buffer. Isso seria necessário se tivéssemos usado um buffer maior do que o necessário para armazenar as mensagens. No nosso caso, o buffer já tinha o tamanho correspondente ao CMSG_SPACE de todas as (uma) mensagens, e msg.msg_controllen foi inicializado com esse valor no passo 2.

No recebimento, os passos 1 e 2 são idênticos. Após a chamada a recvmsg, o valor de msg.msg_controllen terá sido atualizado com o tamanho real do buffer de controle da mensagem.

Falta inicializar a mensagem com os buffers de dados não-controle. No nosso caso, só queremos transferir o descritor de arquivo, que vai nos headers de controle, mas o Unix exige que seja transferido pelo menos um byte de dados junto com os headers de controle. You know, worse is better. Sendo assim, devemos:

  1. Criar um buffer de envio/recebimento de pelo menos um byte:
    char dummy[1] = {0};
  2. Criar um par iovec descrevendo esse buffer:
    struct iovec iov;
    iov.iov_base = dummy;
    iov.iov_len = sizeof(dummy);
    
  3. Fazer a mensagem apontar para o par:
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    

E graças a Odin, acabou.

Comentários / Comments

Main menu

Recent posts

Recent comments

Tags

em-portugues (213) comp (148) prog (71) in-english (62) life (49) unix (38) pldesign (37) lang (32) random (28) about (28) mind (26) lisp (25) fenius (22) mundane (22) web (20) ramble (18) img (13) rant (12) hel (12) scheme (10) privacy (10) freedom (8) esperanto (7) music (7) lash (7) bash (7) academia (7) copyright (7) home (6) mestrado (6) shell (6) android (5) conlang (5) misc (5) emacs (5) latex (4) editor (4) etymology (4) php (4) worldly (4) book (4) politics (4) network (3) c (3) tour-de-scheme (3) security (3) kbd (3) film (3) wrong (3) cook (2) treta (2) poem (2) physics (2) x11 (2) audio (2) comic (2) lows (2) llvm (2) wm (2) philosophy (2) perl (1) wayland (1) ai (1) german (1) en-esperanto (1) golang (1) translation (1) kindle (1) pointless (1) old-chinese (1)

Elsewhere

Quod vide


Copyright © 2010-2024 Vítor De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.