Luxusbus (Linux-Magazin, April 2011)

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.

Süchtig nach Schnickschnack

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.

Fahr mit im Systembus

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.

Listing 1: hal-status

    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.

Lauscher an der Wand

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.

Backup auf Kommando

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).

Bildschirmloser Dämon

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.

Listing 2: dbus-mount-watcher

    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.

Boxen als Baumaterial

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.

Listing 3: gtk2-backup

    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.

Fortschritt mit Trick

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.

Installieren

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].

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/04/Perl

[2]

"Introduction To DBus", Sprach-agnostische Einführung in D-Bus, http://www.freedesktop.org/wiki/IntroductionToDBus

[3]

Emmanuel Rodriquez, "D-Bus with Perl", http://bratislava.pm.org/presentation/dbus/

[4]

Pidgin-Integration mit D-Bus, http://developer.pidgin.im/wiki/DbusHowto

Michael Schilli

arbeitet 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.