Página principal

Entendendo e Usando D-Bus, parte 1

From CInLUG

Conteúdo

Introdução

Nesses dias de web 2.0 em que você consegue ter as músicas que escuta enviadas para serviços como o last.fm, essa lista de músicas exibida em seu blog via RSS, que por sua vez é sindicado num planet via Atom. Nomes demais, não? Mas os envolvidos se comunicam muito bem, e é esse o ponto. Então porque essas coisas legais só podem acontecer fora do seu computador?

Entra em cena o D-Bus, um sistema de comunicação entre processos numa mesma máquina. Todo sistema operacional que se preze dispõe de recursos de IPC (inter-process comunication), contudo D-Bus foi criado visando facilitar e estabelecer um padrão para a comunicação entre aplicações do desktop. Diferente de outros métodos de IPC ou das conexões TCP, que transportam dados como fluxos indistintos de bits, D-Bus transporta mensagens contendo dados de tipos bem definidos.

Um uso interessante do D-Bus é um plugin para o Rhythmbox que sempre que uma música começa a tocar, ele atualiza minha mensagem de status no GAJIM para uma frase como "Ouvindo Banda Tal, Música Tal". Isso funciona porque o GAJIM disponibiliza serviços de mudança de mensagem de status na SessionBus, que você saberá o que é se continuar lendo.

Arquitetura

Diferente da comunicação via sockets TCP, que acontece como um fluxo de dados, no D-Bus as aplicações se comunicam através de mensagens contendo dados de tipos bem definidos. Resumidamente o sistema D-Bus consiste da especificação do padrão, implementada na libdbus, e do daemon roteador de mensagens.

Normalmente as aplicações acessam a libdbus via um binding, neste artigo usaremos o binding para python pela praticidade (se você tiver um desktop GNU/Linux moderno não vai precisar instalar nada para este artigo), mas vários outros estão disponíveis.

Imagem:Dbus_daemon.png

Como se vê no diagrama acima, as aplicações não "conversam" diretamente, tudo passa pelo daemon roteador, que recebe a mensagem do remetente e repassa para o destinatário, ou destinatários, pois pode haver mais de um deles. Outra coisa sobre D-Bus é que ele é orientado a objetos, uma aplicação disponibiliza seus serviços na bus na forma de objetos que podem ser encontrados e instanciados por outra aplicação. Bem, não exatamente instanciados, a instância do objeto é uma só e fica na aplicação, mas os clientes ao requisitarem o tal objeto recebem do binding/implementação d-bus um objeto proxy representando o objeto remoto. Ao chamar os métodos neste proxy, a libdbus transforma as chamdas em mensagens que trafegam até o objeto real que as processa.

Imagem:Dbus_proxyobjects.png

Além do tráfego 1 pra 1 da chamada métodos de objetos na bus, as mensagens também podem fluir de 1 para muitos, através dos sinais. De forma semelhante à orientação a eventos da Glib (base da GTK+), aplicações interessadas se subscrevem nos objetos da bus (ou na própria bus) para receberem mensagens sobre determinados eventos dentre os emitidos pelo objeto. Quando a aplicação sinalizar a ocorrência do tal evento, o daemon roteador encaminhará a mensagem para os "assinantes".

Imagem:Dbus_signaling.png

Uma última informação para esta parte. Mais de uma bus pode estar disponível ao mesmo tempo, e num sistema comum costumam ser duas:

  • SessionBus: responsável pelo tráfego de mensagens entre as aplicações do usuário, durante uma sessão de uso;
  • SystemBus: por onde trafegam mensagens do kernel, sobre hardware (pendrive colocada, bateria fraca, etc.), e outras coisas sérias desse tipo.

Usando

Para pegar um objeto da bus, e efetivamente usá-lo como proxy object o sujeito precisa saber de três coisas:

bus name
nome distinto do serviço na bus, parecido com nomes de pacote em Java, nos exemplos usaremos br.org.cinlug.
object path
cada serviço pode disponibilizar vários objetos, o caminho para cada objeto é dado como um path num sistema de arquivos unix. Exemplos: /org/freedesktop/Hal/Manager ou /MeuObjeto. A primeira forma é a mais comum, mas não é obrigatória.
interface
especificam os métodos que podem ser chamados no objeto e os sinais que ele pode enviar. O objeto pode implementar mais de uma interface, o conjunto dessas interfaces define o tipo do objeto.

Proxy Objects & Interfaces

Vamos praticar um pouco, coisa simples. O Notification Daemon, responsável por aquelas mensagens legais que aparecem quando muda música ou chega mensagem via IM, disponibiliza seus serviços via d-bus. O que vamos fazer é pegar o objeto Notification e mandar ele escrever uma informação qualquer.

As informações necessárias:

  • bus name: org.freedesktop.Notifications
  • object path: /org/freedesktop/Notifications
  • interface: org.freedesktop.Notifications

Abra seu terminal python e coloque isso aqui:

  1. >>> import dbus
  2. >>> my_bus = dbus.SessionBus()
  3. >>> proxy = my_bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
  4. >>> notifier = dbus.Interface(proxy, 'org.freedesktop.Notifications')
  5. >>> notifier.Notify('meuscript', 0, '/usr/share/icons/Tango/scalable/devices/harddrive.svg', \
  6. ... 'Harddisk em Chamas!', 'Parece que seu hardisk queimou de vez...', '', {}, 5000)
  7.  

Imagem:Dbus_notification.png

Depois de importar o módulo d-bus, a primeira coisa é pegar a bus que se pretende usar, nesse caso a SessionBus. Para obter o proxy, chamamos get_object com o bus name do serviço seguido do object path do objeto dentro do serviço. Depois disso criamos outro objeto com o método Interface, passando o proxy e o nome da interface como parâmetros. Como este novo objeto está associado à interface, não precisamos explicitá-la em cada chamada de método, o que seria o caso se usássemos apenas o proxy_object. Algo assim:

  1. >>> proxy.Notify('meuscript', 0, '/usr/share/icons/Tango/scalable/devices/harddrive.svg', \
  2. ... 'Harddisk em Chamas!', 'Parece que seu hardisk queimou de vez...', '', {}, 5000, \
  3. ... dbus_interface='org.freedesktop.Notifications')
  4.  

A definição do método Notify explica os parâmetros detalhadamente.

Chamadas com valor de retorno

Agora vamos pedir ao HAL (ou Hardware Abstraction Layer)

  1. >>> import dbus
  2. >>> bus = dbus.SystemBus()
  3. >>> hal_manager = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager')
  4. >>> devices = hal_manager.GetAllDevices(dbus_interface='org.freedesktop.Hal.Manager')
  5. >>> for dev in devices:
  6. ... print dev
  7.  

Diversos dispositivos serão listados, um deles é o

/org/freedesktop/Hal/devices/computer

Que também é um object path no serviço do HAL, então podemos pegá-lo pelo nome e usar seus métodos.

  1. >>> computer = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/devices/computer')
  2. >>> print computer.GetAllProperties(dbus_interface='org.freedesktop.Hal.Device')
  3. >>> print computer.GetProperty('smbios.bios.vendor')
  4.  

Note que dessa vez usamos a SystemBus que é onde encontramos o HAL, o que é natural, já que hardware é assunto do sistema inteiro. Com o objeto manager conseguimos uma lista de todos os dispositivos dos sistema, mais tarde pegamos um único dispositivo e lemos uma de suas propriedades. As respostas as essas chamadas vêm tão rápido quanto se elas fossem feitas a objetos locais.

Sinais

Como foi dito, um cliente pode registrar seu interesse em eventos dos objetos numa bus (ou eventos da própria bus), basta localizar o objeto e registrar um método callback que será chamado, de forma análoga à conectar sinais de widgets em GTK+. E assim como na GTK+, precisaremos entrar num main loop para receber os eventos.

  1. #!/usr/bin/python
  2. # hardware_monitor.py
  3.  
  4. import dbus
  5. import dbus.glib
  6. import gobject
  7.  
  8. class HardwareMonitor:
  9.  
  10. def __init__(self):
  11. bus = dbus.SystemBus()
  12. obj = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager')
  13. hal = dbus.Interface(obj, 'org.freedesktop.Hal.Manager')
  14.  
  15. hal.connect_to_signal('DeviceAdded', self.device_added)
  16. hal.connect_to_signal('DeviceRemoved', self.device_removed)
  17.  
  18. def device_added(self, device):
  19. print '= dispositivo adicionado ='
  20. print device
  21.  
  22. def device_removed(self, device):
  23. print '= dispositivo removido ='
  24. print device
  25.  
  26. if __name__ == '__main__':
  27. hm = HardwareMonitor()
  28.  
  29. mainloop = gobject.MainLoop()
  30. mainloop.run()
  31.  

hardware_monitor.py

O "main loop" do qual estamos falando é uma espécie de loop infinito (while True) que a cada iteração realiza tarefas como verificar as entradas do usuário, tratá-las como eventos ou sinais, entregar eventos/sinais às partes interessadas, entre outras coisas.

O main loop é fornecido pelo módulo gobject, mas o módulo dbus.glib também precisa ser importado para que o main loop da GLib seja configurado como o loop usado pelo DBus para entrega dos sinais.

Captura genérica de sinais

Também é possível escutar sinais de maneira genérica usando o método add_signal_receiver em um objeto dbus.Bus.Esse método verifica todos os sinais enviados e caso algum dos sinais tenha as caracterísicas desejadas (definidas pelos argumentos abaixo), a função é chamada.

  • handler_function - Um objeto "callable" - Função a ser executada caso as características do sinal lançado combinem com as desejadas.
  • signal_name - Nome do sinal. Caso seja None (padrão), aceita todos os sinais.
  • dbus_interface - Interface DBus que enviou o sinal. Funciona como signal_name (None aceita todos as interfaces).
  • Sender named_service - Nome do serviço que enviou o sinal. Funciona como signal_name (None aceita todos os serviços).
  • path - Caminho do objeto DBus que enviou. Funciona como signal_name (None aceita todos os objetos).

Na realidade, uma chamada a connect_signal se resume a uma chamada a add_signal_receiver com os parâmetros do objeto/interface em que foi chamado.

Chamadas assíncronas e tratamento de erros

Algumas vezes as respostas às requisições não podem vir imediatamente, como é o caso de muitas operações envolvendo leitura e escrita de grandes volumes de dados em dispositivos, ou ainda dependência de fatores como velocidade da rede. Para esses casos podemos usar as chamadas assíncronas do D-Bus, basta chamar o método assíncrono passando métodos callback para receber a resposta e eventuais erros.

No código a seguir chamamos novamente o método remoto GetAllDevices de um dos exemplos anteriores, mas passando os parâmetros reply_handler e error_handler. Lembre-se que ao fazer chamadas assíncronas um error handler sempre deve ser fornecido.

  1. #!/usr/bin/python
  2. # async_test.py
  3.  
  4. import dbus
  5. import dbus.glib
  6. import gobject
  7.  
  8. class AsyncTest:
  9.  
  10. def __init__(self):
  11. bus = dbus.SystemBus()
  12. obj = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager')
  13. hal = dbus.Interface(obj, 'org.freedesktop.Hal.Manager')
  14.  
  15. hal.GetAllDevices(reply_handler=self.get_all_devices,
  16. error_handler=self.catch_error)
  17.  
  18.  
  19. def get_all_devices(self, devices):
  20. print 'Resultado de GetAllDevices():'
  21. for device in devices:
  22. print device
  23.  
  24. def catch_error(self, error):
  25. print 'Erro!'
  26. print error
  27.  
  28. if __name__ == '__main__':
  29. hm = AsyncTest()
  30.  
  31. mainloop = gobject.MainLoop()
  32. mainloop.run()
  33.  

async_test.py

Conclusão

Nesta primeira parte do artigo vimos o que é D-Bus e como usá-lo como consumidores. Na próxima parte veremos como escrever serviços e disponibilizá-los na bus.

Referências

P.S.: Agradecimentos à Maíra que me deixou foto-copiar um tutorial (o do segundo link) que ela tinha acabado de imprimir. Foi bem útil. :)

Autor: Marcelo Lira

Imagem:Cc-small.png : Imagem:Cc-by.png Imagem:Cc-sa.png
Atribuição-Compatilhamento pela mesma licença 2.5

Ferramentas pessoais
Vistas