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.
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.
»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.
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: |
|---|
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 |
|---|
|
|






