Aus Linux-Magazin 01/2006

Automatische Versionierung von Dateien mit Perl-Daemon

© photocase.com

Ein Perl-Daemon beschert gelöschten Files ein zweites Leben und kann ungewollte Änderungen ungeschehen machen. Dafür überwacht er mit Hilfe der Kernelschnittstelle Dnotify eine Verzeichnishierarchie und integriert alle darin gefundenen Dateien transparent in ein eigenes Versionskontrollsystem.

Während früher Phasen eines Projekts probieren Entwickler gerne mehrere Möglichkeiten aus, selten sind sie gut beraten, schon die ersten Ergebnisse der zentralen Versionskontrolle zu übergeben. Existiert zum Beispiel das Repository noch nicht oder steht dessen Struktur noch nicht fest, arbeiten alle praktisch ohne Sicherheitsnetz. Manches Stück guter Code fällt so schnell einem übereilten »rm *« oder einem großflächigen Delete im Editor zum Opfer.

Das Perl-Skript »noworries« kann in einer solchen Situation eine eigene automatische Versionskontrolle übernehmen. Bei jedem Sichern einer Datei im Editor und bei jeder Dateimanipulation in der Shell (wie »rm« oder »mv«) erhält ein unsichtbar im Hintergrund laufender Daemon eine Nachricht. Daraufhin schnappt er sich die erzeugte oder geänderte Datei und versioniert sie mit RCS.

Für den Benutzer läuft der Vorgang unsichtbar ab. Abbildung 1 zeigt, wie ein Benutzer zunächst in der Shell eine neue Datei erzeugt und dann löscht. Ohne Perl-Hexerei wäre das File »Datei« damit endgültig verschwunden, doch ein Aufruf von »noworries -l Datei« zeigt an, dass der Versionierer 17 Sekunden zuvor eine Sicherungskopie angelegt hat. »noworries -r 1.1 Datei« holt sie hervor und schreibt sie nach STDOUT.

Kein Trick

Das Skript benutzt keineswegs manipulierte Shellfunktionen, alles geht mit rechten Dingen zu. Allerdings läuft im Hintergrund eine Instanz des Skripts mit der Option »-w« (watch) und bespitzelt den File Alteration Monitor (FAM), der wiederum an der Dnotify-Schnittstelle des Betriebssystemkernels lauscht. Immer wenn das Dateisystem ein neues Verzeichnis oder eine Datei erzeugt, verschiebt, löscht oder deren Inhalt manipuliert, kommt eine Nachricht über den Vorfall im Kernel an. Der File Alteration Monitor dockt sich an Dnotify an und bringt sein Interesse an Ereignissen in bestimmten Dateiverzeichnissen zum Ausdruck.

Im CPAN gibt es als passendes Perl-Modul SGI::FAM, das die C-Schnittstelle nach Perl verlagert. Ein Aufruf der Methode »next_event()« blockt den Daemon dann so lange, bis ein Ereignis eingetroffen ist. CPU-intensives Pollen ist daher nicht mehr erforderlich.

Abbildung 2 zeigt ein weiteres Beispiel. Es erzeugt zunächst eine Datei, die es dann zweimal hintereinander modifiziert. Im Hintergrund hat der Daemon deswegen drei Versionen angelegt (1.1, 1.2 und 1.3). Der Aufruf »noworries -l Datei« zeigt sie anschließend an. Daran ändert sich auch nichts, wenn ein anderer Prozess die Datei zwischenzeitlich gelöscht hat.

Mit der Option »-r 1.2« wählt der Benutzer die mittlere Version aus und leitet die Ausgabe zurück in die Datei »Datei«, die der Daemon natürlich sofort erneut versioniert. Abbildung 3 zeigt dessen Aktionen, die er der Ordnung halber in der Logdatei »/tmp/noworries.log« protokolliert.Das Skript »noworries« kümmert sich um alle Dateien und Verzeichnisse in beliebiger Tiefe unter »~/noworries« im Homeverzeichnis des Benutzers. Das ist die Spielwiese, auf der er gerne neue Verzeichnisse anlegen oder Tarbälle extrahieren kann.

Parallel dazu baut der Daemon mit jeder neuen Datei eine gleichartige Verzeichnisstruktur unter »~/.noworries.rcs« auf. Jedes Unterverzeichnis darin enthält ein Verzeichnis »RCS«, in dem die versionierten Dateien lagern. RCS ist ein Unix-Urgestein, Versionskontrollsysteme wie CVS oder Perforce verwenden es noch heute.

Abbildung 1: Hier bekommt eine Datei wieder Leben eingehaucht, nachdem der Benutzer sie zuerst löschte. Retter ist der Perl-Daemon im Hintergrund.

Abbildung 1: Hier bekommt eine Datei wieder Leben eingehaucht, nachdem der Benutzer sie zuerst löschte. Retter ist der Perl-Daemon im Hintergrund.

Für das Einchecken einer Datei »datei« ist die folgende kurze Kommandosequenz zuständig:

echo "Daten!" >datei
mkdir RCS
ci datei
co -l datei

Der Befehl »ci« aus dem RCS-Fundus erzeugt eine Versionsdatei »RCS/datei,v«. Das anschließende Auschecken über »co« mit der Option »-l« (für lock) holt die letzte Version wieder zurück ins aktuelle Verzeichnis. Verändert man anschließend »datei«, gefolgt von einer weiteren »ci/co«-Kommandosequenz, liegen schon zwei Versionen vor, die »co« separat hervorholen kann. Das ebenfalls aus dem RCS-Fundus stammende Programm »rlog« erlaubt es, Meta-Informationen über die eingecheckten Dateiversionen anzusehen.

Funktionsimport

Die Namen dieser Hilfsprogramme definiert Listing 1 in den Zeilen 18 bis 20. Sie müssen allerdings im »PATH« liegen, damit »noworries« sie findet, ohne den Pfad zu kennen. Falls nötig, lassen sich die vollständigen Pfade einkodieren.

Abbildung 2: An eine neu angelegte Datei wird in zwei Einzelschritten jeweils eine Zeile angehängt. Anschließend holt »noworries« auf Anfrage wieder die ältere Version 2 hervor.

Abbildung 2: An eine neu angelegte Datei wird in zwei Einzelschritten jeweils eine Zeile angehängt. Anschließend holt »noworries« auf Anfrage wieder die ältere Version 2 hervor.

»noworries« nutzt die von Sysadm::Install exportierten Funktionen »mkd« (Verzeichnis erzeugen), »cp« (Datei kopieren), »cd« (Verzeichnis wechseln) oder »cdback« (zurück zum alten Verzeichnis) und »tap« (Programme ausführen und Ausgaben aufsammeln), die alten Snapshot-Hasen natürlich allesamt schon aus [4] bekannt sind.

Überwachungsstaat

Bevor »SGI::FAM« Nachrichten über modifizierte Dateien in einem Verzeichnis erhält, muss FAM erst einmal Interesse daran beim Kernel anmelden. Der Aufruf »->monitor()« mit dem Verzeichnis »~/noworries« als Argument lässt Events eintrudeln, falls jemand direkt in »~/noworries« ein neues Verzeichnis anlegt oder eine Datei erzeugt. Allerdings gilt das nicht für Unterverzeichnisse, für diese startet »SGI::FAM« sofort einen eigenen Monitor, sobald es von ihrer Erzeugung erfährt.

Mit der Option »-w« aufgerufen startet »noworries« im Daemon-Mode und führt die Endlosschleife in der ab Zeile 64 definierten Funktion »watcher« aus. Der Methodenaufruf »next_event()« in Zeile 74 blockiert, bis eines der vielen Ereignisse eintritt, die FAM verwaltet. Um herauszufinden, welcher der aktiven Verzeichnismonitore angeschlagen hat, liefert die in Zeile 76 aufgerufene Methode »which()« des »SGI::FAM«-Objekts das auslösende Verzeichnis. Die Methode »filename()« des Events gibt den Namen des neuen, existierenden, modifizierten oder gelöschten Objekts zurück. Dabei kann es sich sowohl um ein Verzeichnis als auch um eine Datei handeln.

Abbildung 3: Der Daemon überwacht hinter den Kulissen das Dateisystem und legt versionierte Sicherungskopien an, sobald sich etwas in den überwachten Verzeichnissen ändert.

Abbildung 3: Der Daemon überwacht hinter den Kulissen das Dateisystem und legt versionierte Sicherungskopien an, sobald sich etwas in den überwachten Verzeichnissen ändert.

Die Art des Events gibt die Methode »type()« an. Für »noworries« interessante Werte sind »create« und »change«. Neu erzeugte Verzeichnisse schickt die Methode »monitor()« gleich in den Überwachungsstaat, während neu erzeugte oder geänderte Dateien bei der ab Zeile 133 definierten Funktion »check_in()« landen. Ähnliches gilt für Verzeichnisse, die der Daemon mit »find« findet, wenn er hochfährt und »~/noworries« schon existiert. Die Hilfsfunktion »subdir()« ab Zeile 117 schraubt sich hierzu tiefer und tiefer in eine Verzeichnisstruktur und liefert alle darunter liegenden Verzeichnisse in beliebiger Tiefe.

Der ab Zeile 205 folgende Dokumentationsabschnitt dient nicht nur der Illustration, falls jemand »perldoc noworries« aufruft, sondern wird auch von der Funktion »pod2usage()« ausgegeben, falls jemand vergisst, die richtigen Parameter anzugeben. Temporäre »vi«- oder »emacs«-Dateien zu versionieren ergäbe keinen Sinn. Deshalb filtern die Zeilen 81 und 83 solche Files aus.

Geht es darum, dem Versionskontrollsystem eine Datei einzuverleiben, untersucht »check_in« ab Zeile 133 zunächst, ob es sich um eine Textdatei handelt. Binärdateien weist »check_in« ab Zeile 138 zurück. Die Funktion benutzt einen Pfadnamen relativ zu »~/noworries«, da »watcher()« in Zeile 67 dorthin gesprungen ist. Zeile 148 kopiert die Originaldatei in den RCS-Baum, Zeile 153 ruft das Programm »ci« mit den Optionen »-t« und »-m« auf.

Beiden übergibt es den Wert »-«, denn sowohl der erste als auch alle folgenden Check-in-Kommentare sind bedeutungslos. Es ist aber wichtig, zumindest irgendetwas anzugeben, da »ci« sonst interaktiv nachfragt. Zeile 158 führt den oben beschriebenen Checkout aus und damit ist die Datei im Kasten. Die ausgecheckte Kopie wird bei der nächsten Änderung überschrieben und die neue Version mit »ci« eingecheckt.

Der Wievielte ist heute?

Um abzufragen, welche Versionen für eine Datei verfügbar sind, ruft »noworries« die RCS-Funktion »rlog« auf. Sie liefert die Versionsnummern mit Datumsangaben im Format »yyyy/mm/dd hh:mm:ss« und einer Angabe über die veränderten Zeilen gegenüber der letzten Version. Wenn Version 1.2 »lines: +10 -0« ausgibt, heißt das: Gegenüber Version 1.1 kamen ganze zehn Zeilen hinzu, nichts wurde gelöscht.

Bei Datumsberechnungen hilft das Modul »DateTime« vom CPAN. Die RCS-Datumsangaben parst das Modul »DateTime::Format::Strptime« und rechnet sie in Sekunden seit 1970 um. Der Konstruktor nimmt hierzu einen Formatstring der Form »”%Y/%m/%d %H:%M:%S”« entgegen, der anschließende Aufruf von »parse_datetime()« liefert im Erfolgsfall ein komplett initialisiertes »DateTime«-Objekt zurück.

Durch die etwas unübersichtliche Ausgabe des Hilfsprogramms »rlog« hangelt sich die »while«-Schleife ab Zeile 187 mit einem mehrzeiligen regulären Ausdruck. Die Funktion »time_diff()« ab Zeile 163 nimmt ein »DateTime«-Objekt entgegen und rechnet aus, wie alt eine Version in Sekunden, Minuten, Stunden, Tagen oder Wochen ist.

Leider ist »dnotify« nicht in der Lage, große Dateimengen zu verwalten. Bereits nach etwa zweihundert Unterverzeichnissen ist in der Regel Schluss. Neuere Kernel ersetzen »dnotify« daher durch »inotify«, das besser skaliert. Auch FAM hat ausgedient, Gamin [3] ist der designierte Nachfolger.

Der »dnotify«-Mechanismus des Kernels arbeitet nicht etwa mit den Inodes des Dateisystems, sondern tatsächlich mit Dateinamen, sodass ein »mv Datei1 Datei2« zwei Events auslöst: einen vom Typ Delete und einen weiteren vom Typ Create. Das stört »noworries« nicht, denn Delete-Events ignoriert es.

Das Skript sollte man nur auf der lokalen Platte und nicht unter NFS verwenden, da FAM nur dann effizient arbeitet, wenn auf der NFS-Gegenseite auch ein FAM läuft. Ansonsten pollt es die andere Seite in regelmäßigen Abständen, was aber die Idee des Skripts ad absurdum führen würde.

Listing 1:
»noworries«

001 #!/usr/bin/perl -w
002 use strict;
003 use Sysadm::Install qw(:all);
004 use File::Find;
005 use SGI::FAM;
006 use Log::Log4perl qw(:easy);
007 use File::Basename;
008 use Getopt::Std;
009 use File::Spec::Functions qw(rel2abs
010                              abs2rel);
011 use DateTime;
012 use DateTime::Format::Strptime;
013 use Pod::Usage;
014
015 my $RCS_DIR = "$ENV{HOME}/.noworries.rcs";
016 my $SAFE_DIR = "$ENV{HOME}/noworries";
017
018 my $CI   = "ci";
019 my $CO   = "co";
020 my $RLOG = "rlog";
021
022 getopts("dr:wl", my %opts);
023
024 mkd $RCS_DIR unless -d $RCS_DIR;
025
026 Log::Log4perl->easy_init({
027   category => 'main',
028   level    => $opts{d} ? $DEBUG : $INFO,
029   file     => $opts{w} && !$opts{d} ?
030       "/tmp/noworries.log" : "stdout",
031   layout   => "%d %p %m%n" });
032
033 if($opts{w}) {
034   INFO "$0 starting up";
035   watcher();
036
037 } elsif($opts{r} or $opts{l}) {
038   my($file) = @ARGV;
039   pod2usage("No file given")
040       unless defined $file;
041
042   my $filename = basename $file;
043
044   my $absfile = rel2abs($file);
045   my $relfile = abs2rel($absfile,
046                         $SAFE_DIR);
047
048   my $reldir = dirname($relfile);
049   cd "$RCS_DIR/$reldir";
050
051   if($opts{l}) {
052     rlog($filename);
053   } else {
054     sysrun("$CO", "-r$opts{r}",
055            "-p", $filename);
056   }
057   cdback;
058
059 } else {
060     pod2usage("No valid option given");
061 }
062
063 ###########################################
064 sub watcher {
065 ###########################################
066
067   cd $SAFE_DIR;
068
069   my $fam = SGI::FAM->new();
070   watch_subdirs(".", $fam);
071
072   while (1) {
073       # Block until next event
074     my $event=$fam->next_event();
075
076     my $dir = $fam->which($event);
077     my $fullpath = $dir . "/" .
078                    $event->filename();
079
080       # Emacs temp files
081     next if $fullpath =~ /~$/;
082       # Vi temp files
083     next if $fullpath =~ /.sw[px]x?$/;
084
085     DEBUG "Event: ", $event->type,
086           "(", $event->filename, ")";
087
088     if($event->type eq "create" and
089        -d $fullpath) {
090       DEBUG "Dynamically adding monitor ",
091             "for directory $fullpathn";
092       $fam->monitor($fullpath);
093
094     } elsif($event->type =~ /create|change/
095             and -f $fullpath) {
096       check_in($fullpath);
097     }
098   }
099 }
100
101 ###########################################
102 sub watch_subdirs {
103 ###########################################
104     my($start_dir, $fam) = @_;
105
106     $fam->monitor($start_dir);
107
108     for my $dir (subdirs($start_dir)) {
109         DEBUG "Adding monitor for $dir";
110         $fam->monitor($dir);
111     }
112
113     return $fam;
114 }
115
116 ###########################################
117 sub subdirs {
118 ###########################################
119     my($dir) = @_;
120
121     my @dirs = ();
122
123     find sub {
124         return unless -d;
125         return if /^..?$/;
126         push @dirs, $File::Find::name;
127     }, $dir;
128
129     return @dirs;
130 }
131
132 ###########################################
133 sub check_in {
134 ###########################################
135   my ($file) = @_;
136
137   if(! -T $file) {
138       DEBUG "Skipping non-text file $file";
139       return;
140   }
141
142   my $rel_dir = dirname($file);
143   my $rcs_dir = "$RCS_DIR/$rel_dir/RCS";
144
145   mkd $rcs_dir unless -d $rcs_dir;
146
147   cd "$RCS_DIR/$rel_dir";
148   cp "$SAFE_DIR/$file", ".";
149   my $filename = basename($file);
150
151   INFO "Checking $filename into RCS";
152   my ($stdout, $stderr, $exit_code) =
153       tap($CI, "-t-", "-m-", $filename);
154   INFO "Check-in result: ",
155        "rc=$exit_code $stdout $stderr";
156
157   ($stdout, $stderr, $exit_code) =
158                  tap($CO, "-l", $filename);
159   cdback;
160 }
161
162 ###########################################
163 sub time_diff {
164 ###########################################
165   my ($dt) = @_;
166
167   my $dur = DateTime->now() - $dt;
168
169   for(qw(weeks days hours
170          minutes seconds)) {
171       my $u = $dur->in_units($_);
172       return "$u $_" if $u;
173   }
174 }
175
176 ###########################################
177 sub rlog {
178 ###########################################
179   my ($file) = @_;
180
181   my ($stdout, $stderr, $exit_code) =
182                         tap($RLOG, $file);
183
184   my $p = DateTime::Format::Strptime->new(
185            pattern => '%Y/%m/%d %H:%M:%S');
186
187   while($stdout =~ /^revisions(S+).*?
188                     date:s(.*?);
189                     (.*?)$/gmxs) {
190     my($rev, $date, $rest) = ($1, $2, $3);
191
192       (my $lines) =
193                ($rest =~ /lines:s+(.*)/);
194       $lines ||= "first version";
195
196       my $dt = $p->parse_datetime($date);
197
198       print "$rev ", time_diff($dt),
199             " ago ($lines)n";
200   }
201 }
202
203 __END__
204
205 =head1 NAME
206
207     noworries - Developing with a safety net
208
209 =head1 SYNOPSIS
210
211                   # Print previous version
212     noworries -r revision file
213
214                   # List all revisions
215     noworries -l file
216
217     noworries -w  # Start the watcher

Installation

Vom CPAN sind die Module SGI::FAM, Sysadm::Install, DateTime, DateTime::Format::Strptime und Pod::Usage zu installieren, ihre Abhängigkeiten löst eine CPAN-Shell schnell auf. Wenn SGI::FAM beim Übersetzen mit der Fehlermeldung »FAM.c:813: error: storage size of \’RETVAL\’ isn\’t known« stoppt, empfiehlt es sich, Zeile 813 in »FAM.c« von »enum FAMCodes RETVAL;« in »FAMCodes RETVAL;« zu ändern. Dann führt ein neuerliches »make« zum Erfolg.

Damit der Daemon immer aktiv ist, sollte Root ihn mit einer Zeile wie zum Beispiel »x777:3:respawn:su mschilli -c “/home/mschilli/bin/noworries -w”« in die »/etc/inittab« aufnehmen und den Init-Daemon anschließend mit »init q« darauf hinweisen. Der Prozess muss allerdings unter der ID des richtigen Benutzers laufen, damit »{HOME}« im Skript auf das richtige Homeverzeichnis zeigt. Dann sorgt »init« dafür, dass der Daemon »noworries« automatisch bei jedem Systemstart hochfährt. Die »respawn«-Option stellt sicher, dass ein eventuell versehentlich terminierter Prozess sofort wieder startet. Der Aufruf des Daemon ist vor der Installation aber auf jeden Fall zunächst von der Kommandozeile aus zu testen.

Bei Problemen hilft die Option »-d« für »debug« weiter, die detaillierte Statusausgaben statt in die Datei »/tmp/noworries.log« auf die Standardausgabe umleitet. (jcb)

Infos

[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2006/01/Perl]

[2] FAM-Homepage: [http://oss.sgi.com/projects/fam/]

[3] Gamin-Homepage: [http://www.gnome.org/~veillard/gamin/]

[4] Michael Schilli, “Muschelperle”: [https://www.linux-magazin.de/Artikel/ausgabe/2005/02/perl/perl.html]

Der Autor


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]

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben