~ubuntu-branches/ubuntu/wily/apparmor/wily

« back to all changes in this revision

Viewing changes to utils/aa-notify

  • Committer: Bazaar Package Importer
  • Author(s): Kees Cook
  • Date: 2011-04-27 10:38:07 UTC
  • mfrom: (5.1.118 natty)
  • Revision ID: james.westby@ubuntu.com-20110427103807-ym3rhwys6o84ith0
Tags: 2.6.1-2
debian/copyright: clarify for some full organization names.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/perl
 
2
# ------------------------------------------------------------------
 
3
#
 
4
#    Copyright (C) 2009-2010 Canonical Ltd.
 
5
#
 
6
#    This program is free software; you can redistribute it and/or
 
7
#    modify it under the terms of version 2 of the GNU General Public
 
8
#    License published by the Free Software Foundation.
 
9
#
 
10
# ------------------------------------------------------------------
 
11
#
 
12
# /etc/apparmor/notify.conf:
 
13
# # set to 'yes' to enable AppArmor DENIED notifications
 
14
# show_notifications="yes"
 
15
#
 
16
# # only people in use_group can run this script
 
17
# use_group="admin"
 
18
#
 
19
# $HOME/.apparmor/notify.conf can have:
 
20
# # set to 'yes' to enable AppArmor DENIED notifications
 
21
# show_notifications="yes"
 
22
#
 
23
 
 
24
use strict;
 
25
use warnings;
 
26
no warnings qw( once );
 
27
 
 
28
require LibAppArmor;
 
29
require POSIX;
 
30
require Time::Local;
 
31
require File::Basename;
 
32
 
 
33
use Getopt::Long;
 
34
 
 
35
my %prefs;
 
36
my $conf = "/etc/apparmor/notify.conf";
 
37
my $user_conf = "$ENV{HOME}/.apparmor/notify.conf";
 
38
my $notify_exe = "/usr/bin/notify-send";
 
39
my $last_exe = "/usr/bin/last";
 
40
my $ps_exe = "/bin/ps";
 
41
my $url = "https://wiki.ubuntu.com/DebuggingApparmor";
 
42
my $nobody_user = "nobody";
 
43
my $nobody_group = "nogroup";
 
44
 
 
45
sub readconf;
 
46
sub parse_message;
 
47
sub format_message;
 
48
sub format_stats;
 
49
sub kill_running_daemons;
 
50
sub do_notify;
 
51
sub show_since;
 
52
sub do_last;
 
53
sub do_show_messages;
 
54
sub _error;
 
55
sub _warn;
 
56
sub _debug;
 
57
sub exitscript;
 
58
sub usage;
 
59
 
 
60
#
 
61
# Main script
 
62
#
 
63
 
 
64
# Clean environment
 
65
$ENV{PATH} = "/bin:/usr/bin";
 
66
$ENV{SHELL} = "/bin/sh";
 
67
defined($ENV{IFS}) and $ENV{IFS} = ' \t\n';
 
68
 
 
69
my $prog = File::Basename::basename($0);
 
70
 
 
71
if ($prog !~ /^[a-zA-Z0-9_\-]+$/) {
 
72
    print STDERR "ERROR: bad programe name '$prog'\n";
 
73
    exitscript(1);
 
74
}
 
75
 
 
76
$> == $< or die "Cannot be suid\n";
 
77
$) == $( or die "Cannot be sgid\n";
 
78
 
 
79
my $login;
 
80
our $orig_euid = $>;
 
81
 
 
82
my $opt_d = '';
 
83
my $opt_h = '';
 
84
my $opt_l = '';
 
85
my $opt_p = '';
 
86
my $opt_v = '';
 
87
my $opt_f = '';
 
88
my $opt_s = 0;
 
89
my $opt_u = '';
 
90
my $opt_w = 0;
 
91
GetOptions(
 
92
    'debug|d'        => \$opt_d,
 
93
    'help|h'         => \$opt_h,
 
94
    'since-last|l'   => \$opt_l,
 
95
    'poll|p'         => \$opt_p,
 
96
    'verbose|v'      => \$opt_v,
 
97
    'file|f=s'       => \$opt_f,
 
98
    'since-days|s=n' => \$opt_s,
 
99
    'user|u=s'       => \$opt_u,
 
100
    'wait|w=n'       => \$opt_w,
 
101
);
 
102
if ($opt_h) {
 
103
    usage;
 
104
    exitscript(0);
 
105
}
 
106
 
 
107
# monitor file specified with -f, else use audit.log if auditd is running,
 
108
# otherwise kern.log
 
109
our $logfile = "/var/log/kern.log";
 
110
if ($opt_f) {
 
111
    -f $opt_f or die "'$opt_f' does not exist. Aborting\n";
 
112
    $logfile = $opt_f;
 
113
} else {
 
114
    -e "/var/run/auditd.pid" and $logfile = "/var/log/audit/audit.log";
 
115
}
 
116
 
 
117
-r $logfile or die "Cannot read '$logfile'\n";
 
118
our $logfile_inode = get_logfile_inode($logfile);
 
119
our $logfile_size = get_logfile_size($logfile);
 
120
open (LOGFILE, "<$logfile") or die "Could not open '$logfile'\n";
 
121
# Drop priviliges, if running as root
 
122
if ($< == 0) {
 
123
    $login = "root";
 
124
    if (defined($ENV{SUDO_UID}) and defined($ENV{SUDO_GID})) {
 
125
        POSIX::setgid($ENV{SUDO_GID}) or _error("Could not change gid");
 
126
        $> = $ENV{SUDO_UID} or _error("Could not change euid");
 
127
        defined($ENV{SUDO_USER}) and $login = $ENV{SUDO_USER};
 
128
    } else {
 
129
        my $drop_to = $nobody_user;
 
130
        if ($opt_u) {
 
131
            $drop_to = $opt_u;
 
132
        }
 
133
        # nobody/nogroup
 
134
        POSIX::setgid(scalar(getgrnam($nobody_group))) or _error("Could not change gid to '$nobody_group'");
 
135
        $> = scalar(getpwnam($drop_to)) or _error("Could not change euid to '$drop_to'");
 
136
    }
 
137
} else {
 
138
    $login = getlogin();
 
139
    defined $login or $login = $ENV{'USER'};
 
140
}
 
141
 
 
142
if (-s $conf) {
 
143
    readconf($conf);
 
144
    if (defined($prefs{use_group})) {
 
145
        my ($name, $passwd, $gid, $members) = getgrnam($prefs{use_group});
 
146
        if (not defined($members) or not defined($login) or (not grep { $_ eq $login } split(/ /, $members) and $login ne "root")) {
 
147
            _error("'$login' must be in '$prefs{use_group}' group. Aborting");
 
148
        }
 
149
    }
 
150
}
 
151
 
 
152
if ($opt_p) {
 
153
    -x "$notify_exe" or _error("Could not find '$notify_exe'. Please install libnotify-bin. Aborting");
 
154
} elsif ($opt_l) {
 
155
    -x "$last_exe" or _error("Could not find '$last_exe'. Aborting");
 
156
}
 
157
if ($opt_s and not $opt_l) {
 
158
    $opt_s =~ /^[0-9]+$/ or _error("-s requires a number");
 
159
}
 
160
 
 
161
if ($opt_w) {
 
162
    $opt_w =~ /^[0-9]+$/ or _error("-w requires a number");
 
163
}
 
164
 
 
165
if ($opt_p or $opt_l) {
 
166
    if (-s $user_conf) {
 
167
        readconf($user_conf);
 
168
    }
 
169
 
 
170
    if (defined($prefs{show_notifications}) and $prefs{show_notifications} ne "yes") {
 
171
        _debug("'show_notifications' is disabled. Exiting");
 
172
        exitscript(0);
 
173
    }
 
174
}
 
175
 
 
176
my $now = time();
 
177
if ($opt_p) {
 
178
    do_notify();
 
179
} elsif ($opt_l) {
 
180
    do_last();
 
181
} elsif ($opt_s and not $opt_p) {
 
182
    do_show_messages($opt_s);
 
183
} else {
 
184
    usage;
 
185
    exitscript(1);
 
186
}
 
187
 
 
188
exitscript(0);
 
189
 
 
190
#
 
191
# Subroutines
 
192
#
 
193
sub readconf {
 
194
    my $cfg = $_[0];
 
195
    -r $cfg or die "'$cfg' does not exist\n";
 
196
 
 
197
    open (CFG, "<$cfg") or die "Could not open '$cfg'\n";
 
198
    while (<CFG>) {
 
199
        chomp;
 
200
        s/#.*//;                # no comments
 
201
        s/^\s+//;               # no leading white
 
202
        s/\s+$//;               # no trailing white
 
203
        next unless length;     # anything left?
 
204
        my ($var, $value) = split(/\s*=\s*/, $_, 2);
 
205
        if ($var eq "show_notifications" or $var eq "use_group") {
 
206
            $value =~ s/^"(.*)"$/$1/g;
 
207
            $prefs{$var} = $value;
 
208
        }
 
209
    }
 
210
    close(CFG);
 
211
}
 
212
 
 
213
sub parse_message {
 
214
    my @params = @_;
 
215
    my $msg = $params[0];
 
216
 
 
217
    chomp($msg);
 
218
    #_debug("processing: $msg");
 
219
 
 
220
    my ($test) = LibAppArmorc::parse_record($msg);
 
221
 
 
222
    # Don't show logs before certain date
 
223
    my $date = LibAppArmor::aa_log_record::swig_epoch_get($test);
 
224
    my $since = 0;
 
225
    if (defined($date) and $#params > 0 and $params[1] =~ /^[0-9]+$/) {
 
226
        $since = int($params[1]);
 
227
        int($date) >= $since or goto err;
 
228
    }
 
229
 
 
230
    # ignore all but status and denied messages
 
231
    my $type = LibAppArmor::aa_log_record::swig_event_get($test);
 
232
 
 
233
    $type == $LibAppArmor::AA_RECORD_DENIED or goto err;
 
234
 
 
235
    my $profile = LibAppArmor::aa_log_record::swig_profile_get($test);
 
236
    my $operation = LibAppArmor::aa_log_record::swig_operation_get($test);
 
237
    my $name = LibAppArmor::aa_log_record::swig_name_get($test);
 
238
    my $denied = LibAppArmor::aa_log_record::swig_denied_mask_get($test);
 
239
    my $family = LibAppArmor::aa_log_record::swig_net_family_get($test);
 
240
    my $sock_type = LibAppArmor::aa_log_record::swig_net_sock_type_get($test);
 
241
    LibAppArmorc::free_record($test);
 
242
 
 
243
    return ($profile, $operation, $name, $denied, $family, $sock_type, $date);
 
244
 
 
245
err:
 
246
    LibAppArmorc::free_record($test);
 
247
    return ();
 
248
}
 
249
 
 
250
sub format_message {
 
251
    my ($profile, $operation, $name, $denied, $family, $sock_type, $date) = @_;
 
252
 
 
253
    my $formatted = "";
 
254
    defined($profile) and $formatted .= "Profile: $profile\n";
 
255
    defined($operation) and $formatted .= "Operation: $operation\n";
 
256
    defined($name) and $formatted .= "Name: $name\n";
 
257
    defined($denied) and $formatted .= "Denied: $denied\n";
 
258
    defined($family) and defined ($sock_type) and $formatted .= "Family: $family\nSocket type: $sock_type\n";
 
259
    $formatted .= "Logfile: $logfile\n";
 
260
 
 
261
    return $formatted;
 
262
}
 
263
 
 
264
sub format_stats {
 
265
    my $num = $_[0];
 
266
    my $time = $_[1];
 
267
    if ($num > 0) {
 
268
        print "AppArmor denial";
 
269
        $num > 1 and print "s";
 
270
        print ": $num (since " . scalar(localtime($time)) . ")\n";
 
271
        $opt_v and print "For more information, please see: $url\n";
 
272
    }
 
273
}
 
274
 
 
275
sub kill_running_daemons {
 
276
    # Look for other daemon instances of this script and kill them. This
 
277
    # can happen on logout and back in (in which case $notify_exe fails
 
278
    # anyway). 'ps xw' should output something like:
 
279
    #  9987 ?        Ss     0:01 /usr/bin/perl ./bin/aa-notify -p
 
280
    # 10170 ?        Ss     0:00 /usr/bin/perl ./bin/aa-notify -p
 
281
    open(PS,"$ps_exe xw|") or die "Unable to run '$ps_exe':$!\n";
 
282
    while(<PS>) {
 
283
        chomp;
 
284
        /$prog -[ps]/ or next;
 
285
        s/^\s+//;
 
286
        my @line = split(/\s+/, $_);
 
287
        if ($line[5] =~ /$prog$/ and ($line[6] eq "-p" or $line[6] eq "-s")) {
 
288
            if ($line[0] != $$) {
 
289
                _warn("killing old daemon '$line[0]'");
 
290
                kill 15, ($line[0]);
 
291
            }
 
292
        }
 
293
    }
 
294
    close(PS);
 
295
}
 
296
 
 
297
sub send_message {
 
298
    my $msg = $_[0];
 
299
 
 
300
    my $pid = fork();
 
301
    if ($pid == 0) {    # child
 
302
        # notify-send needs $< to be the unprivileged user
 
303
        $< = $>;
 
304
 
 
305
        # 'system' uses execvp() so no shell metacharacters here.
 
306
        # $notify_exe is an absolute path so execvp won't search PATH.
 
307
        system "$notify_exe", "-i", "gtk-dialog-warning", "-u", "critical", "--", "AppArmor Message", "$msg";
 
308
        my $exit_code = $? >> 8;
 
309
        exit($exit_code);
 
310
    }
 
311
 
 
312
    # parent
 
313
    waitpid($pid, 0);
 
314
    return $?;
 
315
}
 
316
 
 
317
sub do_notify {
 
318
    my %seen;
 
319
    my $seconds = 5;
 
320
    our $time_to_die = 0;
 
321
 
 
322
    print "Starting aa-notify\n";
 
323
    kill_running_daemons();
 
324
 
 
325
    # Daemonize, but not if in debug mode
 
326
    if (not $opt_d) {
 
327
        chdir('/') or die "Can't chdir to /: $!";
 
328
        umask 0;
 
329
        open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
 
330
        open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
 
331
        #open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!";
 
332
        my $pid = fork();
 
333
        exit if $pid;
 
334
        die "Couldn't fork: $!" unless defined($pid);
 
335
        POSIX::setsid() or die "Can't start a new session: $!";
 
336
    }
 
337
 
 
338
    sub signal_handler {
 
339
        $time_to_die = 1;
 
340
    }
 
341
    $SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&signal_handler;
 
342
    $SIG{'PIPE'} = 'IGNORE';
 
343
 
 
344
    if ($opt_w) {
 
345
        sleep($opt_w);
 
346
    }
 
347
 
 
348
    my $count = 0;
 
349
    my $footer = "For more information, please see:\n$url";
 
350
    my $first_run = 1;
 
351
    my $since = $now;
 
352
    if ($opt_s and int($opt_s) > 0) {
 
353
       $since = $since - (int($opt_s) * 60 * 60 * 24);
 
354
    }
 
355
    for (my $i=0; $time_to_die == 0; $i++) {
 
356
        if ($logfile_inode != get_logfile_inode($logfile)) {
 
357
            _warn("$logfile changed inodes, reopening");
 
358
            reopen_logfile();
 
359
        } elsif (get_logfile_size($logfile) < $logfile_size) {
 
360
            _warn("$logfile is smaller, reopening");
 
361
            reopen_logfile();
 
362
        }
 
363
        while(my $msg = <LOGFILE>) {
 
364
            my @attrib;
 
365
            if ($first_run == 1) {
 
366
                if ($since != $now) {
 
367
                    @attrib = parse_message($msg, $since);
 
368
                }
 
369
            } else {
 
370
                @attrib = parse_message($msg);
 
371
            }
 
372
            $#attrib > 0 or next;
 
373
            if ($first_run == 1) {
 
374
               $count++;
 
375
               next;
 
376
            }
 
377
 
 
378
            my ($profile, $operation, $name, $denied, $family, $sock_type, $date) = @attrib;
 
379
 
 
380
            # Rate limit messages by creating a hash whose keys are:
 
381
            # - for files: $profile|$name|$denied|
 
382
            # - for everything else: $profile|$operation|$name|$denied|$family|$sock_type| (as available)
 
383
            # The value for the key is a timestamp (epoch) and we won't show
 
384
            # messages whose key has a timestamp from less than 5 seconds afo
 
385
            my $k = "";
 
386
            defined($profile) and $k .= "$profile|";
 
387
            if (defined($name) and defined($denied)) {
 
388
                $k .= "$name|$denied|";         # for file access, don't worry about operation
 
389
            } else {
 
390
                defined($operation) and $k .= "$operation|";
 
391
                defined($name) and $k .= "$name|";
 
392
                defined($denied) and $k .= "$denied|";
 
393
                defined($family) and defined ($sock_type) and $k .= "$family|$sock_type|";
 
394
            }
 
395
 
 
396
            # don't display same message if seen in last 5 seconds
 
397
            if (not defined($seen{$k})) {
 
398
                $seen{$k} = time();
 
399
            } else {
 
400
                my $now = time();
 
401
                $now - $seen{$k} < $seconds and next;
 
402
                $seen{$k} = $now;
 
403
            }
 
404
 
 
405
            my $m = format_message(@attrib);
 
406
            $m ne "" or next;
 
407
 
 
408
            $m .= $footer;
 
409
 
 
410
            my $rc = send_message($m);
 
411
            if ($rc != 0) {
 
412
                _warn("'$notify_exe' exited with error '$rc'");
 
413
                $time_to_die = 1;
 
414
                last;
 
415
            }
 
416
        }
 
417
        # from seek() in Programming Perl
 
418
        seek(LOGFILE, 0, 1);
 
419
        sleep(1);
 
420
 
 
421
        if ($first_run) {
 
422
            if ($count > 0) {
 
423
                my $m = "$logfile contains $count denied message";
 
424
                $count > 1 and $m .= "s";
 
425
                if ($opt_s) {
 
426
                    $m .= " in the last ";
 
427
                    if ($opt_s > 1) {
 
428
                        $m .= "$opt_s days";
 
429
                    } else {
 
430
                        $m .= "day";
 
431
                    }
 
432
                }
 
433
                $m .= ". ";
 
434
                $m .= $footer;
 
435
                send_message($m);
 
436
            }
 
437
            $first_run = 0;
 
438
        }
 
439
 
 
440
        # clean out the %seen database every 30 seconds
 
441
        if ($i > 30) {
 
442
            foreach my $k (keys %seen) {
 
443
                my $now = time();
 
444
                $now - $seen{$k} > $seconds and delete $seen{$k} and _debug("deleted $k");
 
445
            }
 
446
            $i = 0;
 
447
            _debug("done purging");
 
448
            foreach my $k (keys %seen) {
 
449
                _debug("remaining key: $k: $seen{$k}");
 
450
            }
 
451
        }
 
452
    }
 
453
    print STDERR "Stopping aa-notify\n";
 
454
}
 
455
 
 
456
sub show_since {
 
457
    my %msg_hash;
 
458
    my %last_date;
 
459
    my @msg_list;
 
460
    my $count = 0;
 
461
    while(my $msg = <LOGFILE>) {
 
462
        my @attrib = parse_message($msg, $_[0]);
 
463
        $#attrib > 0 or next;
 
464
 
 
465
        my $m = format_message(@attrib);
 
466
        $m ne "" or next;
 
467
        my $date = $attrib[6];
 
468
        if ($opt_v) {
 
469
            if (exists($msg_hash{$m})) {
 
470
                $msg_hash{$m}++;
 
471
                defined($date) and $last_date{$m} = scalar(localtime($date));
 
472
            } else {
 
473
                $msg_hash{$m} = 1;
 
474
                push(@msg_list, $m);
 
475
            }
 
476
        }
 
477
        $count++;
 
478
    }
 
479
    if ($opt_v) {
 
480
        foreach my $m (@msg_list) {
 
481
            print "$m";
 
482
            if ($msg_hash{$m} gt 1) {
 
483
                print "($msg_hash{$m} found";
 
484
                if (exists($last_date{$m})) {
 
485
                    print ", most recent from '$last_date{$m}'";
 
486
                }
 
487
                print ")\n";
 
488
            }
 
489
            print "\n";
 
490
        }
 
491
    }
 
492
    return $count;
 
493
}
 
494
 
 
495
sub do_last {
 
496
    open(LAST,"$last_exe -F -a $login|") or die "Unable to run $last_exe:$!\n";
 
497
    my $time = 0;
 
498
    while(my $line = <LAST>) {
 
499
        _debug("Checking '$line'");
 
500
        $line =~ /^$login/ or next;
 
501
        $line !~ /^$login\s+pts.*\s+:[0-9]+\.[0-9]+$/ or next; # ignore xterm and friends
 
502
        my @entry = split(/\s+/, $line);
 
503
        my ($hour, $min, $sec) = (split(/:/, $entry[5]))[0,1,2];
 
504
        $time = Time::Local::timelocal($sec, $min, $hour, $entry[4], $entry[3], $entry[6]);
 
505
        last;
 
506
    }
 
507
    close(LAST);
 
508
    $time > 0 or _error("Couldn't find last login");
 
509
 
 
510
    format_stats(show_since($time), $time);
 
511
}
 
512
 
 
513
sub do_show_messages {
 
514
    my $since = $now - (int($_[0]) * 60 * 60 * 24);
 
515
    format_stats(show_since($since), $since);
 
516
}
 
517
 
 
518
sub _warn {
 
519
    my $msg = $_[0];
 
520
    print STDERR "aa-notify: WARN: $msg\n";
 
521
}
 
522
sub _error {
 
523
    my $msg = $_[0];
 
524
    print STDERR "aa-notify: ERROR: $msg\n";
 
525
    exitscript(1);
 
526
}
 
527
 
 
528
sub _debug {
 
529
    $opt_d or return;
 
530
    my $msg = $_[0];
 
531
    print STDERR "aa-notify: DEBUG: $msg\n";
 
532
}
 
533
 
 
534
sub exitscript {
 
535
    my $rc = $_[0];
 
536
    close(LOGFILE);
 
537
    exit $rc;
 
538
}
 
539
 
 
540
sub usage {
 
541
    my $s = <<'EOF';
 
542
USAGE: aa-notify [OPTIONS]
 
543
 
 
544
Display AppArmor notifications or messages for DENIED entries.
 
545
 
 
546
OPTIONS:
 
547
  -p, --poll                    poll AppArmor logs and display notifications
 
548
  -f FILE, --file=FILE          search FILE for AppArmor messages
 
549
  -l, --since-last              display stats since last login
 
550
  -s NUM, --since-days=NUM      show stats for last NUM days (can be used alone
 
551
                                or with -p)
 
552
  -v, --verbose                 show messages with stats
 
553
  -h, --help                    display this help
 
554
  -u USER, --user=USER          user to drop privileges to when not using sudo
 
555
  -w NUM, --wait=NUM            wait NUM seconds before displaying
 
556
                                notifications (with -p)
 
557
EOF
 
558
    print $s;
 
559
}
 
560
 
 
561
sub reopen_logfile {
 
562
    # reopen the logfile, temporarily switching back to starting euid for
 
563
    # file permissions.
 
564
    close(LOGFILE);
 
565
 
 
566
    my $old_euid = $>;
 
567
    my $change_euid = 0;
 
568
    if ($> != $<) {
 
569
        _debug("raising privileges to '$orig_euid' in reopen_logfile()");
 
570
        $change_euid = 1;
 
571
        $> = $orig_euid;
 
572
        $> == $orig_euid or die "Could not raise privileges\n";
 
573
    }
 
574
 
 
575
    $logfile_inode = get_logfile_inode($logfile);
 
576
    $logfile_size = get_logfile_size($logfile);
 
577
    open (LOGFILE, "<$logfile") or die "Could not open '$logfile'\n";
 
578
 
 
579
    if ($change_euid) {
 
580
        _debug("dropping privileges to '$old_euid' in reopen_logfile()");
 
581
        $> = $old_euid;
 
582
        $> == $old_euid or die "Could not drop privileges\n";
 
583
    }
 
584
}
 
585
 
 
586
sub get_logfile_size {
 
587
    my $fn = $_[0];
 
588
    my $size;
 
589
    defined(($size = (stat($fn))[7])) or (sleep(10) and defined(($size = (stat($fn))[7])) or die "'$fn' disappeared. Aborting\n");
 
590
    return $size;
 
591
}
 
592
 
 
593
sub get_logfile_inode {
 
594
    my $fn = $_[0];
 
595
    my $inode;
 
596
    defined(($inode = (stat($fn))[1])) or (sleep(10) and defined(($inode = (stat($fn))[1])) or die "'$fn' disappeared. Aborting\n");
 
597
    return $inode;
 
598
}
 
599
 
 
600
#
 
601
# end Subroutines
 
602
#