Nicht immer vertrauen Entwickler ihre Source-Dateien früh genug einem Revision-Control-System an. Ein Perl-Dämon überwacht eine Verzeichnishierarchie, wird von der dnotify-Schnittstelle des Kernels über Änderungen informiert und versioniert alle Dateien transparent mit RCS.
Während früher Phasen eines Projekts probieren Entwickler gerne
verschiedene Möglichkeiten aus, und nicht immer ist es ratsam, die
ersten Ergebnisse schon der Versionskontrolle zu übergeben. Existiert
das Repository noch nicht oder man konnte sich noch nicht über die
Struktur einigen, wird ohne Sicherheitsnetz gearbeitet und manches
Stück guter Code fällt einem übereilten rm *
oder einem
großflächigem Löschvorgang im Editor zum Opfer.
Mit dem Perlskript noworries
erfolgt Versionskontrolle automatisch.
Bei jedem Sichern einer Datei im Editor oder Shell-Manipulationen wie
rm
und mv
erhält ein unsichtbar im Hintergrund laufender Dämon
eine Nachricht, auf die er sich die neu erzeugte oder geänderte Datei
schnappt und mit RCS versioniert. Für den Benutzer läuft der Vorgang
magisch ab, wie Abbildung 1 zeigt. In der Shell wird zunächst eine
neue Datei erzeugt und diese dann mit rm
gelöscht. Ohne Perl-
Hexerei wäre das das Ende der Datei datei
, doch ein Aufruf von
noworries -l datei
im früheren Verzeichnis der Datei zeigt an, dass
der Versionierer vor 17 Sekunden eine Sicherungskopie angelegt hat.
Mit noworries -r 1.1 datei
wird diese hervorgeholt und nach STDOUT
geschrieben. Wie ist das möglich?
Abbildung 1: Eine neu angelegte Datei wird gelöscht und ihr Inhalt anschließend mit noworries gerettet. |
Der Trick liegt keineswegs in manipulierten Shell-Funktionen, 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, 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 (FAM) dockt sich an dnotify
an
und bringt sein Interesse an Ereignissen in bestimmten
Dateiverzeichnissen zum Ausdruck. Auf dem CPAN gibt es ein passendes
Perl-Modul (SGI::FAM), das die C-Schnittstelle nach Perl verlagert.
Ein Aufruf der Methode next_event()
blockt den Dämon dann so lange,
bis ein Ergeignis vorliegt. Ein CPU-intensives Pollen ist nicht
erforderlich.
Abbildung 2 zeigt ein weiteres Beispiel. Dort wird eine Datei zunächst
erzeugt und dann zweimal hintereinander modifiziert. Im Hintergrund
hat der Dämon deswegen drei Versionen angelegt (1.1, 1.2 und 1.3). Der
Aufruf noworries -l datei
zeigt sie anschließend an, auch wenn
zwischenzeitlich die Datei gelöscht wurde.
Mit der Option -r 1.2
wählt der Benutzer anschließend die
mittlere Version aus und leitet die Ausgabe zurück in die Datei
datei
, die der Dämon natürlich sofort wieder versioniert. Abbildung
3 zeigt dessen Aktionen, die er der Ordnung halber in der Logdatei
/tmp/noworries.log
mitprotokolliert.
Abbildung 2: An eine neu angelegte Datei wird in zwei Einzelschritten jeweils eine Zeile angehängt. noworries holt anschließend auf Anfrage wieder Version 2 hervor. |
Abbildung 3: ... denn der Dämon überwacht hinter den Kulissen das Dateisystem und legt versionierte Sicherungskopien an, sobald sich etwas in den überwachten Verzeichnissen verändert. |
noworries
kümmert sich um alle Dateien und Verzeichnisse in
beliebiger Tiefe unter ~/noworries
im Home-Verzeichnis des
Benutzers. Das ist die Spielwiese, in der auch gerne neue
Verzeichnisse angelegt oder Tarbälle extrahiert werden dürfen.
Parallel dazu baut der Dämon mit jeder neuen Datei eine Struktur
unter ~/.noworries.rcs
auf. Jedes Unterverzeichnis enthält ein
Verzeichnis RCS
, in der die versionierten Dateien lagern. rcs
ist ein Unix-Urgestein und wird auch heute noch von Versions-
Kontrollsystemen wie CVS oder Perforce verwendet. Um eine Datei
datei
'einzuchecken', ist folgende Kommandosequenz erforderlich:
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 aus-checken mit co
mit der
Option -l
(für lock) holt die aktuelle 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 Versionen anzusehen.
Die Namen dieser Hilfsprogramme definiert Listing noworries
in
den Zeilen 18 bis 20. So wie angegeben müssen sie im PATH
liegen, damit noworries
sie aufrufen kann. Falls notwendig,
lassen sich die vollständigen Pfade einkodieren.
noworries
nutzt die von Sysadm::Install
exportierten
Funktionen mkd
(Verzeichnis erzeugen), cp
(Datei kopieren),
cd
(Verzeichnis wechseln),
cdback
(zurück zum alten Verzeichnis)
und tap
(Programme ausführen und Ausgaben aufsammeln),
die alten Snapshot-Hasen natürlich schon aus [4] bekannt sind.
Bevor SGI::FAM
Nachrichten über modifizierte Dateien in einem
Verzeichnis erhält, muss FAM erst einmal Interesse daran beim
Kernel bekunden.
Der Aufruf $fam->monitor(...)
mit dem Verzeichnis
~/noworries
als Argument lässt Events eintrudeln, falls
direkt in ~/noworries
ein neues Verzeichnis angelegt oder
eine Datei erzeugt wird. Allerdings gilt das nicht für
Unterverzeichnisse, für diese ruft SGI::FAM
sofort einen eigenen
Monitor auf, sobald es von ihrer Erzeugung erfährt.
Mit der Option -w
aufgerufen, ist noworries
im Dämon-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.
Die Art des Events gibt die Methode type()
an.
Für noworries
interessante Werte sind "create"
und "change"
. Neu erzeugte Verzeichnisse werden mit
der Methode monitor()
gleich in den Überwachungsstaat
assimiliert, während neu erzeugte oder geänderte Dateien
an die ab Zeile 133 definierte Funktion check_in()
weitergereicht werden. Ähnliches gilt für Verzeichnisse, die
der Dämon 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 sie aus.
Geht es daran, 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 wird mit einem Pfadnamen
relativ zu ~/noworries
aufgerufen, da watcher()
in Zeile
67 dort hin gesprungen ist. Zeile 148 kopiert
die Originaldatei in den RCS-Baum und 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
check-out 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.
Um abzufragen, welche Versionen für eine Datei verfügbar sind,
ruft noworries
die rcs
-Funktion rlog
auf. Diese liefert
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. Die erste Version hat
naturgemäß nichts dergleichen, aber wenn Version 1.2 ``lines: +10 -0''
aufweist, heißt das, dass gegenüber Version 1.1 ganze 10 Zeilen
hinzukamen, während keine Zeile gelöscht wurde.
Mit Datumsberechnungen hilft das Modul DateTime
vom CPAN.
Die RCS-Datumsangaben
werden mit DateTime::Format::Strptime
geparst und in
Sekunden-seit-1970 umgerechnet. Der Konstruktor
nimmt hierzu einen Formatstring der Form
"%Y/%m/%d %H:%M:%S"
entgegen und 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.
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 $fullpath\n"; 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 =~ /^revision\s(\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 # C 213 214 # List all revisions 215 noworries -l file 216 217 noworries -w # Start the watcher
Leider ist dnotify
nicht in der Lage, große Dateimengen zu verwalten.
Nach etwa zweihundert Unterverzeichnissen ist typischerweise Schluss.
In neueren Kerneln wurde dnotify
deswegen durch inotify
ersetzt,
das sparsamer mit Ressourcen umgeht und besser skaliert. Auch FAM hat
ausgedient, Gamin [3] soll der Nachfolger werden.
Der dnotify
-Mechanismus des Kernels arbeitet nicht etwa mit den
Inodes des Dateisystems, sondern tatsächlich mit Dateinamen, so dass
ein mv datei1 datei2
zwei Events auslöst: Einen vom Typ "delete"
und einen weiteren vom Typ "create"
. noworries
stört das nicht,
denn "delete"
-Events ignoriert es, und falls die gleiche Datei
später wieder auftaucht, wird sie einfach als neueste Version eingecheckt.
Das Skript sollte man nur auf der lokalen Platte und nicht unter
NFS verwenden, da FAM
nur dann effizient arbeitet, wenn NFS-Gegenseite
auch ein FAM läuft. Falls nicht, pollt es die andere Seite in regelmäßigen
Abständen, was das Verfahren ad absurdum führt.
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, sollte Zeile 813 in FAM.c
von enum FAMCodes RETVAL;
in FAMCodes RETVAL;
umgeändert werden,
dann führt ein neuerliches make
zum Erfolg.
Damit der Dämon immer aktiv ist, sollte er mit
x777:3:respawn:su mschilli -c "/home/mschilli/bin/noworries -w"
in die /etc/inittab
aufgenommen und der init
-Dämon anschließend
mit init q
darauf hingewiesen werden. Der Prozess muss unter der ID des
richtigen Benutzers laufen, damit $ENV{HOME}
im Skript auf das richtige
Home-Verzeichnis zeigt. init
sorgt dafür, dass der Dämon noworries
beim Systemstart hochfährt und die respawn
-Option stellt sicher, dass
ein eventuell versehentlich terminierter Prozess sofort wieder gestartet
wird. Den Aufruf des Dämons sollte man vor der Installation aber
auf jeden Fall zunächst von der Kommandozeile versuchen. Bei Problemen
hilft die Option -d
für debug weiter, die detaillierte
Statusausgaben statt
in der Datei /tmp/noworries.log
in die Standardausgabe
leitet.
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. |