/[echolot]/branches/snapshot-2003-02-17-branch/pingd
ViewVC logotype

Contents of /branches/snapshot-2003-02-17-branch/pingd

Parent Directory Parent Directory | Revision Log Revision Log


Revision 209 - (hide annotations) (download)
Wed Jul 17 19:16:06 2002 UTC (10 years, 10 months ago) by weasel
Original Path: trunk/pingd
File size: 16189 byte(s)
Prepare 2.0beta16
1 weasel 158 #!/usr/bin/perl -w
2 weasel 1
3 weasel 138 $| = 1;
4    
5 weasel 1 # (c) 2002 Peter Palfrader <peter@palfrader.org>
6 weasel 209 # $Id: pingd,v 1.46 2002/07/17 19:16:06 weasel Exp $
7 weasel 1 #
8    
9 weasel 54 =pod
10    
11     =head1 NAME
12    
13     pingd - echolot ping daemon
14    
15     =head1 SYNOPSIS
16    
17     =over
18    
19     =item B<pingd> B<start>
20    
21     =item B<pingd> B<stop>
22    
23 weasel 88 =item B<pingd> B<process>
24    
25 weasel 75 =item B<pingd> B<add> I<address> [I<address> ...]
26 weasel 54
27 weasel 94 =item B<pingd> B<delete> I<address> [I<address> ...]
28    
29 weasel 75 =item B<pingd> B<set> option=value [option=value..] I<address> [I<address> ...]
30    
31 weasel 103 =item B<pingd> B<setremailercaps> I<capsstring>
32    
33     =item B<pingd> B<deleteremailercaps> I<address>
34    
35 weasel 206 =item B<pingd> B<getkeyconf> [I<address> [I<address> ...]]
36 weasel 94
37 weasel 172 =item B<pingd> B<buildstats>
38    
39     =item B<pingd> B<buildkeys>
40    
41     =item B<pingd> B<buildthesaurus>
42    
43 weasel 81 =item B<pingd> B<dumpconf>
44    
45 weasel 54 =back
46    
47     =head1 DESCRIPTION
48    
49     pingd is a the heart of echolot. Echolot is a pinger for anonymous remailers.
50    
51     A Pinger in the context of anonymous remailers is a program that regularily
52     sends messages through remailers to check their reliability. It then calculates
53     reliability statistics which are used by remailer clients to choose the chain
54     of remailers to use.
55    
56     Additionally it collects configuration parameters and keys of all remailers and
57     offers them in a format readable by remailer clients.
58    
59     When called without parameters pingd schedules tasks like sending pings,
60     processing incoming mail and requesting remailer-xxx data and runs them in
61     configurable intervalls.
62    
63     =head1 COMMANDS
64    
65     =over
66    
67     =item B<start>
68    
69     Start the ping daemon.
70    
71     =item B<stop>
72    
73     Send the running pingd process a SIGTERM.
74    
75 weasel 88 =item B<process>
76    
77     Sends a HUP signal to the daemon which instructs it to process the commands.
78    
79     For other affects of sending the HUP Signal see the SIGNALS section below.
80    
81 weasel 75 =item B<add> I<address> [I<address> ...]
82 weasel 54
83     Add I<address> to the list of remailers to query for
84     keys and confs.
85    
86 weasel 94 =item B<delete> I<address> [I<address> ...]
87    
88     Delete I<address> from the list of remailers to query for
89 weasel 128 keys and confs. Delete all statistics and keys for that remailer.
90 weasel 94
91 weasel 75 =item B<set> option=value [option=value..] I<address> [I<address> ...]
92    
93     Possible options and values:
94    
95     =over
96    
97     =item B<showit=>{B<on>,B<off>}
98    
99     Set B<showit> (show remailer in mlist, rlist etc.) for remailer I<address> to
100     either B<on> or B<off>.
101    
102     =item B<pingit=>{B<on>,B<off>}
103    
104     Set B<pingit> (send out pings to that remailer) for remailer I<address> to
105     either B<on> or B<off>.
106    
107     =item B<fetch=>{B<on>,B<off>}
108    
109     Set B<fetch> (fetch remailer-key and remailer-conf) for remailer I<address> to
110     either B<on> or B<off>.
111    
112     =back
113    
114 weasel 103 =item B<setremailercaps> I<capsstring>
115    
116     Some remailers (Mixmaster V2 - currently lcs and passthru2) don't return a
117     useable remailer-conf message. For such remailers you need to set it manually.
118    
119     For instance:
120    
121     ./pingd setremailercaps '$remailer{"passthru2"} = "<mixer@immd1.informatik.uni-erlangen.de> mix middle";'
122     ./pingd setremailercaps '$remailer{"lcs"} = "<mix@anon.lcs.mit.edu> mix klen1000";'
123    
124     =item B<deleteremailercaps> I<address>
125    
126     Delete remailer-conf data for I<address>. The config data will be reset from
127     the next valid remailer-conf reply by the remailer.
128    
129 weasel 206 =item B<getkeyconf> [I<address> [I<address> ...]]
130 weasel 94
131     Send a command to immediatly request keys and configuration from remailers.
132 weasel 206 If no addresses are given, then requests will be sent to all remailers.
133 weasel 94
134 weasel 172 =item B<buildstats>
135    
136     Send a command to immediatly rebuild stats.
137    
138     =item B<buildkeys>
139    
140     Send a command to immediatly rebuild the keyrings.
141    
142     =item B<buildthesaurus>
143    
144     Send a command to immediatly rebuild the Thesaurus.
145    
146 weasel 81 =item B<dumpconf>
147    
148     Dumps the current configuration to standard output.
149    
150 weasel 103 =back
151    
152 weasel 54 =head1 OPTIONS
153    
154 weasel 81 =over
155 weasel 54
156 weasel 81 =item --verbose
157    
158     Verbose mode. Causes B<pingd> to print debugging messages about its progress.
159    
160     =item --help
161    
162     Print a short help and exit sucessfully.
163    
164 weasel 117 =item --version
165    
166     Print version number and exit sucessfully.
167    
168 weasel 94 =item --nohup
169 weasel 88
170 weasel 128 Usefull only with the B<add>, B<set>, B<setremailercaps>,
171 weasel 172 B<deleteremailercaps>, B<getkeyconf>, B<buildstats>, B<buildkeys>,
172     or B<buildthesaurus> command.
173 weasel 88
174 weasel 94 Don't send a HUP signal to the daemon which instructs it to process the
175     commands after adding the command to the task list.
176 weasel 88
177 weasel 94 Per default such a signal is sent.
178    
179 weasel 127 =item --process
180    
181 weasel 128 Usefull only with the B<start> command.
182 weasel 127
183     Read and process the commands file on startup.
184    
185 weasel 88 =item --detach
186    
187 weasel 128 Usefull only with the B<start> command.
188 weasel 88
189     Tell B<pingd> to detach.
190    
191 weasel 54 =back
192    
193     =head1 FILES
194    
195 weasel 115 The configuration file is searched in those places in that order:
196 weasel 54
197 weasel 115 =over
198    
199     =item the file pointed to by the B<ECHOLOT_CONF> environment variable
200    
201     =item `pwd`/pingd.conf
202    
203     =item $HOME/echolot/pingd.conf
204    
205     =item $HOME/pingd.conf
206    
207     =item $HOME/.pingd.conf
208    
209     =item /etc/pingd.conf
210    
211     =back
212    
213 weasel 88 =head1 SIGNALS
214    
215 weasel 103 On B<SIGINT>, B<SIGQUIT>, and B<SIGTERM> B<pingd> will schedule a shutdown
216     for as soon as the current actions are finished or immediatly if no actions are
217     currently beeing processed. It will then write all metadata and pingdata to
218 weasel 88 disk and close all files cleanly before exiting.
219    
220     On B<SIGHUP> <pingd> will execute any pending commands from the commands file
221     (B<commands.txt> per default). It also closes and reopens the file 'output'
222     which is used for stdout and stderr in case the daemon was told to detach.
223     This can be used if you want to rotate that file.
224    
225 weasel 54 =head1 AUTHOR
226    
227 weasel 103 Peter Palfrader E<lt>peter@palfrader.orgE<gt>
228 weasel 54
229     =head1 BUGS
230    
231 weasel 103 Please report them at E<lt>URL:http://savannah.gnu.org/bugs/?group=echolotE<gt>
232 weasel 54
233     =cut
234    
235 weasel 1 use strict;
236     use Getopt::Long;
237 weasel 54 use English;
238 weasel 91 use Carp;
239 weasel 34 use lib qw{ . lib };
240 weasel 1 use Echolot::Config;
241     use Echolot::Globals;
242     use Echolot::Storage::File;
243 weasel 15 use Echolot::Scheduler;
244 weasel 1 use Echolot::Conf;
245     use Echolot::Mailin;
246 weasel 15 use Echolot::Pinger;
247 weasel 34 use Echolot::Stats;
248 weasel 54 use Echolot::Commands;
249 weasel 106 use Echolot::Thesaurus;
250 weasel 1
251     delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
252    
253 weasel 54
254 weasel 209 my $VERSION = '2.0beta16';
255 weasel 117
256    
257 weasel 88 my $redirected_stdio = 0;
258 weasel 54
259     sub setSigHandlers() {
260     $SIG{'HUP'} = sub {
261 weasel 115 print "Got SIGHUP. scheduling readcommands\n";
262 weasel 94 Echolot::Globals::get()->{'scheduler'}->schedule('readcommands', time() );
263 weasel 88 if ($redirected_stdio) {
264     close STDOUT;
265     close STDERR;
266     open (STDOUT, ">>output") or die ("Cannot open 'output' as STDOUT\n");
267     open (STDERR, ">&STDOUT") or die ("Cannot dup STDOUT as STDERR\n");
268     };
269 weasel 54 };
270     $SIG{'INT'} = sub {
271     print "Got SIGINT. scheduling exit\n";
272 weasel 94 Echolot::Globals::get()->{'scheduler'}->schedule('exit', time() );
273 weasel 54 };
274     $SIG{'QUIT'} = sub {
275     print "Got SIGQUIT. scheduling exit\n";
276 weasel 94 Echolot::Globals::get()->{'scheduler'}->schedule('exit', time() );
277 weasel 54 };
278     $SIG{'TERM'} = sub {
279     print "Got SIGTERM. scheduling exit\n";
280 weasel 94 Echolot::Globals::get()->{'scheduler'}->schedule('exit', time() );
281 weasel 54 };
282 weasel 34 };
283 weasel 54
284 weasel 88
285    
286 weasel 54 sub commit_prospective_address() {
287     Echolot::Globals::get()->{'storage'}->commit_prospective_address();
288 weasel 34 };
289 weasel 60 sub expire() {
290     Echolot::Globals::get()->{'storage'}->expire();
291     };
292 weasel 54
293    
294    
295    
296    
297 weasel 94 sub command_adddelete(@) {
298     my $command = shift @_;
299 weasel 88 my @argv = @_;
300 weasel 54
301 weasel 94 die ("command_adddelete requires command\n") unless defined $command;
302 weasel 88 die ("add requires argument <address>\n") unless scalar @argv;
303 weasel 75 my @addresses;
304 weasel 88 for my $address (@argv) {
305 weasel 75 die ("argument <address> is not a valid email address\n") unless ($address =~ /^[a-zA-Z0-9+._-]+\@[a-zA-Z0-9+.-]+$/ );
306     push @addresses, $address;
307     };
308     for my $address (@addresses) {
309 weasel 94 Echolot::Commands::addCommand("$command $address");
310 weasel 75 };
311 weasel 88 };
312    
313     sub command_set(@) {
314     my @argv = @_;
315    
316 weasel 75 my @settings;
317 weasel 88 while (scalar @argv && $argv[0] =~ /^(showit|pingit|fetch)=(on|off)$/) {
318     push @settings, $argv[0];
319     shift @argv;
320 weasel 75 };
321    
322     my @addresses;
323 weasel 88 for my $address (@argv) {
324 weasel 75 die ("argument $address is not a valid email address\n") unless ($address =~ /^[a-zA-Z0-9+._-]+\@[a-zA-Z0-9+.-]+$/ );
325     push @addresses, $address;
326     };
327    
328 weasel 88 for my $address (@argv) {
329 weasel 75 for my $setting (@settings) {
330     Echolot::Commands::addCommand("set $address $setting");
331     };
332     };
333 weasel 88 };
334    
335 weasel 103 sub command_setremailercaps(@) {
336     my @argv = @_;
337 weasel 94
338 weasel 103 my @caps;
339     for my $caps (@argv) {
340     my ($remailer_nick, $remailer_address) = ($caps =~ /^\s* \$remailer{"(.*)"} \s*=\s* "<(.*@.*)>.*"; \s*$/ix);
341     die ("caps '$caps' is not a valid remailer caps line\n") unless (defined $remailer_nick && defined $remailer_address);
342     push @caps, {
343     address => $remailer_address,
344     caps => $caps };
345     };
346     for my $caps (@caps) {
347     Echolot::Commands::addCommand("setremailercaps ".$caps->{'address'}." ".$caps->{'caps'});
348     };
349     };
350 weasel 94
351 weasel 103 sub command_deleteremailercaps(@) {
352     my @argv = @_;
353    
354     my @addresses;
355     for my $address (@argv) {
356     die ("argument $address is not a valid email address\n") unless ($address =~ /^[a-zA-Z0-9+._-]+\@[a-zA-Z0-9+.-]+$/ );
357     push @addresses, $address;
358     };
359    
360 weasel 206 for my $address (@addresses) {
361 weasel 103 Echolot::Commands::addCommand("deleteremailercaps $address");
362     };
363     };
364    
365 weasel 206 sub command_getkeyconf(@) {
366     my @argv = @_;
367 weasel 103
368 weasel 206 my @addresses;
369     for my $address (@argv) {
370     die ("argument $address is not a valid email address\n") unless ($address =~ /^[a-zA-Z0-9+._-]+\@[a-zA-Z0-9+.-]+$/ );
371     push @addresses, $address;
372     };
373    
374     push @addresses, 'all' unless (scalar @addresses);
375    
376     for my $address (@addresses) {
377     Echolot::Commands::addCommand("getkeyconf $address");
378     };
379     };
380    
381    
382 weasel 88 sub pid_exists() {
383     return (-e Echolot::Config::get()->{'pidfile'});
384     };
385    
386 weasel 129 sub daemon_run($) {
387     my ($process) = @_;
388    
389 weasel 88 die ("Pidfile '".Echolot::Config::get()->{'pidfile'}."' exists\n")
390     if pid_exists();
391     open (PIDFILE, '>'.Echolot::Config::get()->{'pidfile'}) or
392 weasel 152 confess ("Cannot open pidfile '".Echolot::Config::get()->{'pidfile'}."': $!\n");
393 weasel 88 print PIDFILE "$PROCESS_ID ".Echolot::Globals::get()->{'hostname'}." ".time()."\n";
394     close PIDFILE;
395    
396 weasel 60 Echolot::Globals::initStorage();
397 weasel 54 setSigHandlers();
398 weasel 1
399 weasel 94 Echolot::Globals::get()->{'scheduler'} = new Echolot::Scheduler;
400     my $scheduler = Echolot::Globals::get()->{'scheduler'};
401 weasel 83 $scheduler->add('exit' , -1 , 0, 'exit' );
402     $scheduler->add('readcommands' , -1 , 0, \&Echolot::Commands::processCommands );
403 weasel 1
404 weasel 83 $scheduler->add('processmail' , Echolot::Config::get()->{'processmail'} , 0, \&Echolot::Mailin::process );
405     $scheduler->add('ping' , Echolot::Config::get()->{'pinger_interval'} , 0, \&Echolot::Pinger::send_pings );
406 weasel 107 $scheduler->add('buildstats' , Echolot::Config::get()->{'buildstats'} , 0, \&Echolot::Stats::build_stats );
407     $scheduler->add('buildkeys' , Echolot::Config::get()->{'buildkeys'} , 0, \&Echolot::Stats::build_keys );
408 weasel 172 $scheduler->add('buildthesaurus' , Echolot::Config::get()->{'buildthesaurus'} , 0, \&Echolot::Thesaurus::build_thesaurus );
409 weasel 54
410 weasel 83 $scheduler->add('commitprospectives' , Echolot::Config::get()->{'commitprospectives'} , 0, \&commit_prospective_address );
411     $scheduler->add('expire' , Echolot::Config::get()->{'expire'} , 0, \&expire );
412 weasel 206 $scheduler->add('getkeyconf' , Echolot::Config::get()->{'getkeyconf_interval'}, 0, \&Echolot::Conf::send_requests );
413 weasel 121 $scheduler->add('check_resurrection' , Echolot::Config::get()->{'check_resurrection'} , 0, \&Echolot::Conf::check_resurrection );
414 weasel 54
415 weasel 127 Echolot::Globals::get()->{'scheduler'}->schedule('readcommands', time() )
416 weasel 129 if ($process);
417 weasel 127
418 weasel 54 $scheduler->run();
419    
420     Echolot::Globals::get()->{'storage'}->commit();
421     Echolot::Globals::get()->{'storage'}->finish();
422 weasel 88
423     unlink (Echolot::Config::get()->{'pidfile'}) or
424     cluck ("Cannot unlink pidfile ".Echolot::Config::get()->{'pidfile'});
425     };
426    
427     sub send_sig($) {
428     my ($sig) = @_;
429    
430 weasel 107 die ("Pidfile '".Echolot::Config::get()->{'pidfile'}."' does not exist\n")
431 weasel 88 unless pid_exists();
432     open (PIDFILE, '<'.Echolot::Config::get()->{'pidfile'}) or
433 weasel 152 confess ("Cannot open pidfile '".Echolot::Config::get()->{'pidfile'}."': $!\n");
434 weasel 88 my $line = <PIDFILE>;
435     close PIDFILE;
436    
437     my ($pid, $host, $time) = $line =~ /^(\d+) \s+ (\S+) \s+ (\d+) \s* $/x or
438 weasel 152 confess ("Cannot parse pidfile '$line'\n");
439 weasel 88 my $sent = kill $sig, $pid;
440     ($sent == 1) or
441 weasel 152 confess ("Did not send signal $sig to exactly one process but $sent. (pidfile reads $line)\n");
442 weasel 88 };
443    
444     sub daemon_hup() {
445     send_sig(1);
446     };
447    
448     sub daemon_stop() {
449     send_sig(15);
450     };
451    
452 weasel 91 sub make_dirs() {
453     for my $dir (
454     Echolot::Config::get()->{'resultdir'},
455 weasel 106 Echolot::Config::get()->{'thesaurusdir'},
456 weasel 91 Echolot::Config::get()->{'private_resultdir'},
457     Echolot::Config::get()->{'gnupghome'},
458     Echolot::Config::get()->{'tmpdir'},
459     Echolot::Config::get()->{'storage'}->{'File'}->{'basedir'}
460     ) {
461     if ( ! -d $dir ) {
462 weasel 193 mkdir ($dir, 0755) or
463 weasel 152 confess ("Cannot create directory $dir: $!\n");
464 weasel 91 };
465     };
466     for my $dir (
467     Echolot::Config::get()->{'mailindir'},
468     Echolot::Config::get()->{'mailindir'}.'/cur',
469     Echolot::Config::get()->{'mailindir'}.'/tmp',
470     Echolot::Config::get()->{'mailindir'}.'/new',
471     Echolot::Config::get()->{'mailerrordir'},
472     Echolot::Config::get()->{'mailerrordir'}.'/cur',
473     Echolot::Config::get()->{'mailerrordir'}.'/tmp',
474     Echolot::Config::get()->{'mailerrordir'}.'/new'
475     ) {
476     if ( ! -d $dir ) {
477     mkdir ($dir, 0700) or
478 weasel 152 confess ("Cannot create directory $dir: $!\n");
479 weasel 91 };
480     };
481     };
482 weasel 88
483 weasel 172 sub hup_if_wanted($) {
484     my ($nohup) = @_;
485     if (!$nohup && pid_exists()) {
486     daemon_hup()
487     } else {
488     print "Don't forget to run $PROGRAM_NAME process.\n";
489     };
490     };
491 weasel 91
492    
493    
494    
495    
496    
497    
498    
499    
500    
501 weasel 88 my $params;
502     Getopt::Long::config('bundling');
503     if (!GetOptions (
504     'help' => \$params->{'help'},
505 weasel 117 'version' => \$params->{'version'},
506 weasel 88 'verbose' => \$params->{'verbose'},
507 weasel 94 'nohup' => \$params->{'nohup'},
508 weasel 88 'detach' => \$params->{'detach'},
509 weasel 127 'process' => \$params->{'process'},
510 weasel 88 )) {
511     die ("$PROGRAM_NAME: Usage: $PROGRAM_NAME [-fwhv]\n");
512     };
513     if ($params->{'help'}) {
514 weasel 115 print ("Usage: $PROGRAM_NAME [options] command\n");
515     print ("See man pingd or perldoc pingd for more info.\n");
516 weasel 117 print ("echolot $VERSION - (c) 2002 Peter Palfrader <peter\@palfrader.org>\n");
517     print ("http://savannah.gnu.org/projects/echolot/\n");
518 weasel 88 exit 0;
519     };
520 weasel 117 if ($params->{'version'}) {
521     print ("echolot $VERSION\n");
522     print ("(c) 2002 Peter Palfrader <peter\@palfrader.org>\n");
523     print ("http://savannah.gnu.org/projects/echolot/\n");
524     exit 0;
525     };
526 weasel 88
527     my $COMMAND = shift @ARGV;
528     die ("command required\n") unless defined $COMMAND;
529    
530    
531     Echolot::Config::init( $params );
532     chdir( Echolot::Config::get()->{'homedir'} );
533 weasel 187 Echolot::Globals::init( version => $VERSION);
534 weasel 88
535    
536 weasel 94 if ($COMMAND eq 'add' || $COMMAND eq 'delete') {
537     command_adddelete($COMMAND, @ARGV);
538 weasel 172 hup_if_wanted($params->{'nohup'});
539 weasel 88 } elsif ($COMMAND eq 'set') {
540     command_set(@ARGV);
541 weasel 172 hup_if_wanted($params->{'nohup'});
542     } elsif ($COMMAND eq 'deleteremailercaps') {
543     command_deleteremailercaps(@ARGV);
544     hup_if_wanted($params->{'nohup'});
545 weasel 103 } elsif ($COMMAND eq 'setremailercaps') {
546     command_setremailercaps(@ARGV);
547 weasel 172 hup_if_wanted($params->{'nohup'});
548 weasel 94 } elsif ($COMMAND eq 'getkeyconf') {
549 weasel 206 command_getkeyconf(@ARGV);
550 weasel 172 hup_if_wanted($params->{'nohup'});
551     } elsif ($COMMAND eq 'buildstats') {
552     Echolot::Commands::addCommand("buildstats");
553     hup_if_wanted($params->{'nohup'});
554     } elsif ($COMMAND eq 'buildkeys') {
555     Echolot::Commands::addCommand("buildkeys");
556     hup_if_wanted($params->{'nohup'});
557     } elsif ($COMMAND eq 'buildthesaurus') {
558     Echolot::Commands::addCommand("buildthesaurus");
559     hup_if_wanted($params->{'nohup'});
560 weasel 88 } elsif ($COMMAND eq 'process') {
561     daemon_hup();
562     } elsif ($COMMAND eq 'stop') {
563     daemon_stop();
564     } elsif ($COMMAND eq 'start') {
565 weasel 103 die ("Pidfile '".Echolot::Config::get()->{'pidfile'}."' exists\n")
566     if pid_exists();
567 weasel 91 make_dirs();
568 weasel 88 if ($params->{'detach'}) {
569     print "Detaching.\n";
570     unless (fork()) {
571     close STDOUT;
572     close STDERR;
573     open (STDOUT, ">>output") or die ("Cannot open 'output' as STDOUT\n");
574     open (STDERR, ">&STDOUT") or die ("Cannot dup STDOUT as STDERR\n");
575     close STDIN;
576     $redirected_stdio = 1;
577 weasel 105 print "Startup at ".scalar localtime().".\n";
578 weasel 129 daemon_run( $params->{'process'} );
579 weasel 105 print "done at ".scalar localtime().".\n";
580 weasel 88 };
581     } else {
582 weasel 129 daemon_run( $params->{'process'} );
583 weasel 88 };
584 weasel 81 } elsif ($COMMAND eq 'dumpconf') {
585     Echolot::Config::dump();
586 weasel 75 } elsif ($COMMAND eq 'convert') {
587     Echolot::Globals::initStorage();
588     setSigHandlers();
589    
590     Echolot::Globals::get()->{'storage'}->convert();
591    
592     Echolot::Globals::get()->{'storage'}->commit();
593     Echolot::Globals::get()->{'storage'}->finish();
594 weasel 54 } else {
595     die ("Command $COMMAND unknown");
596     };
597    
598 weasel 34 exit 0;
599 weasel 1
600     # vim: set ts=4 shiftwidth=4:

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC 1.1.5