Ein Perl-Dämon startet einen automatischen Backup mit Progressanzeige auf dem Desktop, sobald der D-Bus einen eingesteckten USB-Stick meldet.
Ich gebe nur ungern zu, dass mein Ubuntu-Laptop zusehens verstaubt, seit mir in der Arbeit vor einem halben Jahr ein Macbook aufgedrängt wurde. Zwei Gründe möchte ich nennen: Ein schlafen gelegtes Macbook wacht in 99 von 100 Fällen wieder korrekt auf und ist in 5 Sekunden samt Wireless betriebsbereit. Und um ein Backup zu ziehen, stöpselt der User einfach die vorkonfigurierte Backup-USB-Platte ein und der Reigen beginnt, ohne dass man auch nur einen Finger rühren oder einen Gedanken an den weiteren Ablauf verschwenden muss.
Man könnte dies als Schnickschnack abtun, aber derartige Zuckerln versüßen den Alltag und deren Entzug bestraft der Körper sofort mit allergischen Reaktionen. Spontane langezogene "Waruuum?"-Rufe sind häufig die Folge, sobald es statt Torte wieder nur Diätgerichte gibt.
Abbildung 1: Auf dem Macbook springt sofort die Backup-Utility "Time Machine" an, sobald der User das USB-Backup-Drive einstöpselt. |
Auch moderne Linux-Desktops lösen solche Aktionen aus. Der von Gnome und mittlerweile auch von KDE verwendete D-Bus stellt einen praktischen Kommunikationskanal zwischen verschiedenen Applikationen dar, ohne dass diese direkt voneinander wissen müssten. Bekommt zum Beispiel der Hardware Abstraction Layer (HAL) mit, dass der User einen USB-Stick einstöpselt, verbreitet er diese Nachricht auf dem D-Bus. Andere Applikationen wie der Gnome-Desktop schnappen sie dort auf, mounten zum Beispiel den Stick wie unter Ubuntu im Verzeichnis /media ins Filesystem und werfen ein Fenster mit dem "File Browser" auf den Desktop.
Das Einklinken in den D-Bus ist selbst in einer Skriptsprache
wie Perl dank dem CPAN-Modul Net::DBus sehr einfach möglich.
Das Skript in Listing 1 wählt in Zeile 5 den System-Bus aus, der
unabhängig von der gerade laufenden Desktop-Session systemweite Meldungen
sammelt und weitergibt. Alternativ betreibt D-Bus den Session-Bus,
der die Daten der aktuellen User-Session bereithält. Die Methode
get_service()
befragt das Busobjekt nach dem Service "org.freedesktop.Hal",
der die HAL-Daten in der Hierarchie freedesktop.org
, dem
D-Bus-Mutterschiff, beherbergt. Die verdrehte Notation dient der
hierarchischen Ordnung und ist aus der Java-Welt bekannt.
Über diesen Service versucht nun
Zeile 8 mit get_object()
ein Objekt der Klasse des HAL-Managers
einzuholen. In Hochsprachen bietet D-Bus seine Dienste oft als Objekte an,
deren Methoden die Busdaten schicken oder empfangen. Der Hal-Manager verfügt
über die Methode GetAllDevices()
, der für jedes angeschlossene und
von HAL erkannte Gerät einen beschreibenden String zurückgibt.
Abbildung 2 zeigt eine von der for-Schleife ausgegebene Auswahl.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use Net::DBus; 04 05 my $bus = Net::DBus->system(); 06 my $hal = $bus->get_service( "org.freedesktop.Hal" ); 07 08 my $manager = $hal->get_object( 09 "/org/freedesktop/Hal/Manager", 10 "org.freedesktop.Hal.Manager" ); 11 12 my $devices = $manager->GetAllDevices(); 13 14 for my $device ( @$devices ) { 15 print "$device\n"; 16 }
Abbildung 2: Net::DBus verbindet sich mit dem HAL-Manager-Objekt und gibt alle bisher erkannten Hardware-Teile aus. |
Während Listing 1 das Remote-Objekt des HAL-Managers anspricht, um die bislang registrierten Geräte aufzuzählen, erfordert ein nach dem Einstöpseln automatisch startender Backup einen aktiv lauschenden Client, den der D-Bus verzögerungslos benachrichtigt, sobald ein vorher definierter Zustand eingetreten ist.
Da die Dokumentation der Bestandteile ausgesandter D-Bus-Nachrichten oft
stark zu wünschen übrig lässt, ist es oft einfacher, ein Tool wie
dbus-monitor
zu bemühen, das dem Paket dbus
von Haus aus beiliegt.
Es abonniert nach dem Start von der Kommandozeile
kurzerhand alle D-Bus-Nachrichten und gibt alle eintreffenden
Messages aus, sobald diese eintrudeln. So zeigt Abbildung 3, dass
dbus-monitor
unter anderem einen MountAdded
-Event zugesteckt bekommt,
nachdem der User einen USB-Stick aktiviert hat.
Für die Nachricht verantwortlich
zeichnet der Service org.gtk.Private.GduVolumeMonitor
, der über das
Interface org.gtk.Private.RemoteVolumeMonitor
das Objekt
/org/gtk/Private/RemoteVolumeMonitor
anbietet.
Abbildung 3: Der frisch eingestöpselte USB-Stick erscheint als neuer Mount auf dem D-Bus. |
Dieser Event auf dem Session-Bus rührt zweifelsfrei von einer Applikation aus der Gnome-Welt, die den USB-Stick unter /media mountet und dies interessierten Lauschern auf dem D-Bus mitteilt.
Um diese Events abzufangen, ohne in einer Flut
irrelevantem Bus-Geplappers
zu ertrinken, meldet sich das Backup-Dämon-Skript dbus-mount-watcher
in Listing 2 auf dem DBus an,
fängt ausschließlich
"MountAdded"-Nachrichten ab und untersucht anhand der voreingestellten
UUID A840-E2B3, ob der User den vorher angemeldeten Backup-Stick eingesteckt
hat. Ist dies der Fall und der Event stammt nicht etwa von einem
anderen soeben angeschlossenen Gerät, startet das Dämon-Skript die
Backup-Applikation gtk2-backup
in Listing 3 mit einer Gtk-Oberfläche,
die den aktuellen Status des Backups auf dem Desktop mit einem
Progressbalken anzeigt (Abbildung 6).
Da das Dämon-Skript mit Hilfe des CPAN-Moduls App::Daemon im Hintergrund
läuft und kein Terminal oder X-Server-Display kennt, gibt das Kommando
in Zeile 39 den Wert der DISPLAY-Variablen als :0.0
vor, also das erste
Display des X-Servers auf dem aktiven Rechner. Der Dämon wird
mit dbus-mount-watcher start
gestartet und schiebt sich dank der von
App::Daemon exportierten Methode daemonize()
in den Hintergrund,
so dass der User kurz darauf wieder den Kommandozeilenprompt sieht.
In der Datei /tmp/dbus-mount-watcher.log
loggt der Daemon seine
Aktivitäten (Abbildung 4).
Das Kommando dbus-mountwatcher stop
fährt den Dämon wieder herunter,
mit -X
kann der Entwickler ihn im Vordergrund starten (Logdaten
wandern allerdings immer noch in die Logdatei) und mit status
lässt sich
der Status des Dämons abfragen.
Abbildung 4: Die Logdatei des Dämons offenbart, dass 20 Sekunden nach dem Skriptstart der Backup-USB-Stick erkannt und der Backup-Prozess eingeleitet wurde. |
01 #!/usr/local/bin/perl -w 02 use strict; 03 use Net::DBus; 04 use Net::DBus::Reactor; 05 use App::Daemon; 06 use FindBin qw($Bin); 07 use Log::Log4perl qw(:easy); 08 09 use App::Daemon qw( daemonize ); 10 daemonize(); 11 12 INFO "Starting up"; 13 14 my $BACKUP_STICK = 15 "file:///media/A840-E2B3"; 16 my $BACKUP_PROCESS = "$Bin/gtk2-backup"; 17 18 my $notifications = Net::DBus->session 19 ->get_service( 20 "org.gtk.Private.GduVolumeMonitor" ) 21 ->get_object( 22 "/org/gtk/Private/RemoteVolumeMonitor", 23 "org.gtk.Private.RemoteVolumeMonitor", 24 ); 25 26 INFO "Subscribing to signal"; 27 28 $notifications->connect_to_signal( 29 'MountAdded', \&mount_added ); 30 31 ########################################### 32 sub mount_added { 33 ########################################### 34 my( $service, $addr, $data ) = @_; 35 36 INFO "Found mount point $data->[4] "; 37 38 if( $data->[4] eq $BACKUP_STICK ) { 39 my $cmd = "DISPLAY=:0.0 " . 40 "$BACKUP_PROCESS $data->[4] &"; 41 INFO "Launching $cmd"; 42 system( $cmd ); 43 } 44 } 45 46 my $reactor = Net::DBus::Reactor->main(); 47 $reactor->run();
Die Methode connect_to_signal()
in Zeile 28 weist dem Event "MountAdded"
auf dem Session-Bus den ab Zeile 32 definierten Callback mount_added()
zu. Das Net::DBus-Framework sorgt im Falle eines aufgeschnappten Events
dafür, dass dem Callback alle aus dem Bus stammenden und in der Ausgabe
von dbus-monitor
in Abbildung 3 aufgelisteten Paramter übergeben werden.
Als dritter Parameter liegt demnach eine Referenz auf einen Array vor,
dessen fünftes Element der Mount-Point des USB-Sticks unter dem
/media
-Verzeichnis ist (Abbildung 5).
Abbildung 5: Net::Dbus ruft den Callback für das Signal "MountAdd" mit diesen Parametern auf. |
Diese URI der Form file:///media/XXX
übergibt der system()
-Aufruf
in Zeile 42 von Listing 2 dem eigentlichen Backup-Skript gtk2-backup
aus Listing 3, das unaufgefordert direkt nach dem Start einen dicken
roten Progressbalken auf den Bildschirm zaubert und den Backup-Prozess
beginnt (Abbildung 6).
Damit der Dämon nach dem Registrieren mit dem D-Bus nicht abrupt abbricht,
sonder ewig weiterläuft und gleichzeitig D-Bus-Events bearbeitet, definiert
Zeile 46 einen sogenannten "Reactor". Dieses Objekt verfügt über eine
Methode run()
, die den Dämon auf ewig mit dem D-Bus verschweißt.
Abbildung 6: Der automatische Backup startet auf dem Ubuntu-Desktop, sofort nachdem der USB-Stick eingesteckt wurde. |
Listing 3 nimmt den Mount-Point des erkannten USB-Sticks entgegen,
entfernt dessen file://
-Vorspann in Zeile 18 und definiert mit dem
Kommando in Zeile 26 das simple Backup-Verfahren: Das tar
-Kommando
sammelt alle unter dem Verzeichnis $src_dir
(Zeile 10) liegenen
Dateien ein und schreibt das daraus erzeugte Tar-Archiv auf den
USB-Stick. Um Überschreiber zu verhindern, erzeugt das Skript eine
nach dem aktuellen Datum benannte Datei im Format YYYYMMDD.tgz
. Wer
den USB-Stick mehrmals am Tag einlegt, muss noch Stunden und Minuten
einbeziehen.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use File::Finder; 04 use Glib qw/TRUE FALSE/; 05 use Gtk2 '-init'; 06 use DateTime; 07 08 my $PID; 09 my $tar = "tar"; 10 my $src_dir = "/home/mschilli/test"; 11 my $ymd = DateTime->now->ymd(''); 12 13 my($stick_dir) = @ARGV; 14 15 if(! defined $stick_dir ) { 16 die "usage: $0 stick_dir"; 17 } 18 $stick_dir =~ s#^file://##; 19 20 my $dst_tarball = "$stick_dir/$ymd.tgz"; 21 22 my $NOF_FILES = scalar File::Finder 23 -> type( "f" ) 24 -> in( $src_dir ); 25 26 my $CMD = 27 "$tar zcfv $dst_tarball $src_dir"; 28 29 my $window = Gtk2::Window->new('toplevel'); 30 $window->set_border_width(10); 31 $window->set_size_request( 500, 100 ); 32 33 my $vbox = Gtk2::VBox->new( TRUE, 10 ); 34 $window->add( $vbox ); 35 36 my $pbar = Gtk2::ProgressBar->new(); 37 $pbar->set_fraction(0); 38 $pbar->set_text("Progress"); 39 $vbox->pack_start( $pbar, TRUE, TRUE, 0 ); 40 41 my $cancel = Gtk2::Button->new('Cancel'); 42 $vbox->pack_end( $cancel, 43 FALSE, FALSE, 0 ); 44 $cancel->signal_connect( clicked => 45 sub { kill 2, $PID if defined $PID; 46 Gtk2->main_quit; } ); 47 48 $window->show_all(); 49 50 my $timer = Glib::Timeout->add ( 51 10, \&start, $pbar, 52 Glib::G_PRIORITY_LOW ); 53 54 Gtk2->main; 55 56 ########################################### 57 sub start { 58 ########################################### 59 my( $pbar ) = @_; 60 61 $PID = open my $fh, "$CMD |"; 62 63 my $count = 1; 64 while( <$fh> ) { 65 chomp; 66 next if m#/$#; # skip dirs 67 68 $pbar->set_text( "Backup Progress " . 69 "($count/$NOF_FILES)" ); 70 $pbar->set_fraction($count/$NOF_FILES); 71 72 Gtk2->main_iteration while 73 Gtk2->events_pending; 74 75 $count++; 76 } 77 78 close $fh or die "$CMD failed ($!)"; 79 80 $cancel->set_label( "Success. Hooray!" ); 81 undef $PID; 82 83 return Glib::SOURCE_REMOVE; 84 }
Das Layout der GUI besteht aus einem Oberteil mit dem Progressbalken
und einem Unterteil mit einem Button, der während des Backup-Laufs als
"Cancel" erscheint und nach dessen Beendigung eine Erfolgsmeldung trägt.
Da Widgets wie Progressbalken oder Buttons nicht direkt in einem
Fenster der Klasse Gtk2::Window liegen können, muss ein
Gtk2::VBox-Container herhalten, der die in ihm liegenden Elemente
(Progressbalken und Button) mit pack_start()
untereinander darstellt.
Falls der Backup zu langsam vor sich hin zuckelt, kann der ungeduldige User ihn
mit einem Druck auf den "Cancel"-Button abbrechen. Das Skript schickt in diesem
Fall ein Sigterm-Signal (Nummer 2) an den Tar-Prozess, der sich vorzeitig
beendet, was wiederum einen Fehler im close()
in Zeile 78 auslöst und die GUI
abbricht.
Damit der Fortschrittsbalken auch einigermaßen die Wirklichkeit
widerspiegelt, sammelt das CPAN-Modul File::Finder ab Zeile 22
alle Dateien (type "f") unter dem Verzeichnis $src_dir und allen
Unterverzeichnissen ein und ermittelt ihre Anzahl mit dem scalar
-Operator
auf den resultierenden Array. Während der tar
-Prozess im Verbose-Modus
läuft, schnappt sich die while
-Schleife ab Zeile 64 jeweils neu ausgegebene
Zeilen und ist so gut darüber informiert, wie viele Dateien tar
bereits
bearbeitet hat. Das Verhältnis von bereits erledigten Dateien zu der
bekannten Gesamtzahl meldet Zeile 70 an den Progressbalken und Zeile 68
schreibt den entsprechenden Text "Backup Progress (XX/YY)" dazu. Das
Gtk2-Konstrukt mit der Methode main_iteration
in Zeile 76 frischt
die Oberfläche bei jedem Balkenruckler auf, andernfalls wäre wegen
Pufferung kein Fortschritt zu erkennen.
Nach Abschluss des tar
-Kommandos schreibt Zeile 87 eine Erfolgsmeldung in
den Button unterhalb des Balkens und ein Mausklick darauf (oder das
Betätigen der Enter-Taste) bricht das Programm ab.
Abbildung 7: Der Backup lief erfolgreich und der Tarball liegt auf dem USB-Stick. |
Damit die von tar
geschriebenen Daten auch auf dem USB-Stick landen
und nicht etwa im Betriebssystem zwischengespeichert werden, empfiehlt sich
vor dem gewaltsamen Entfernen des USB-Sticks
ein "umount", entweder von der Kommandozeile oder aus dem Dateimanager.
Nach dem Eintritt in die Haupteventschleife mit Gtk2->main
in Zeile 56 nimmt die GUI ihren Lauf und wartet auf User-Eingaben.
Da das Backup-Programm aber selbständig zu laufen beginnen soll sobald die
Oberfläche steht, ohne dass ein Mausklick des Users dies einleitet, setzt
Zeile 52 einen Timer. Dieser ruft die ab Zeile 59 definierte Funktion
start()
als Task mit der niedrigsten Priorität Glib::G_PRIORITY_LOW auf,
die der Glib-Kern erst dann startet, wenn keine GUI-Aufbau-Tasks mehr anliegen.
Als einzigen Parameter übergibt der Timer start()
das Widget des
Progressbalkens $pbar
. Wichtig ist dann noch, dass start()
nach
getaner Arbeit den Wert Glib::SOURCE_REMOVE zurückliefert, sonst ruft
der Timer den Callback nach dem Timeout erneut auf und der Backup
begänne von vorne.
Das Paket dbus
liegt bereits allen gängigen Linux-Distributionen bei.
Das Tool gdbusviewer
, ein weiteres Diagnosetool neben dbus-monitor
,
wird mit der Sammlung gq4-dev-tools
auf Ubuntu installiert. Die
benötigten Perl-Module liegen als libdatetime-perl, libfile-finder-perl,
libgtk2-perl, libglib-perl, libapp-daemon-perl, liblog-log4perl-perl
und libnet-dbus-perl in den Ubuntu-Repositories vor.
Der Dämon wird anschließend mit dbus-mount-watcher start
hochgefahren,
und wer möchte, dass dies beim einem Reboot des Rechners automatisch
geschieht, sollte den Dämon unter /etc/init.d/
einhängen und mit
update-rc.d
registriereren. Das grafische Backup-Skript sollte im gleichen
Verzeichnis wie der Dämon landen oder mittels absolutem Pfad aus dem
Dämon heraus aufgerufen werden.
Die Möglichkeiten mit D-Bus gehen noch weit über die hier vorgestellten Tricks hinaus. Applikationen wie der Instant-Messenger-Client Pidgin oder der Musikspieler Rhythmbox sind eng mit D-Bus integriert und lassen sich damit nicht nur überwachen, sondern regelrecht fernsteuern [4].
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/04/Perl
"Introduction To DBus", Sprach-agnostische Einführung in D-Bus, http://www.freedesktop.org/wiki/IntroductionToDBus
Emmanuel Rodriquez, "D-Bus with Perl", http://bratislava.pm.org/presentation/dbus/
Pidgin-Integration mit D-Bus, http://developer.pidgin.im/wiki/DbusHowto
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |