=head1 NAME
Mail::SpamAssassin::Timeout - safe, reliable timeouts in perl
=head1 SYNOPSIS
# non-timeout code...
my $t = Mail::SpamAssassin::Timeout->new({ secs => 5 });
$t->run(sub {
# code to run with a 5-second timeout...
});
if ($t->timed_out()) {
# do something...
}
# more non-timeout code...
=head1 DESCRIPTION
This module provides a safe, reliable and clean API to provide
C<alarm(2)>-based timeouts for perl code.
Note that C<$SIG{ALRM}> is used to provide the timeout, so this will not
interrupt out-of-control regular expression matches.
Nested timeouts are supported.
=head1 PUBLIC METHODS
=over 4
=cut
package Mail::SpamAssassin::Timeout;
use strict;
use warnings;
use bytes;
use vars qw{
@ISA
};
@ISA = qw();
=item my $t = Mail::SpamAssassin::Timeout->new({ ... options ... });
Constructor. Options include:
=over 4
=item secs => $seconds
timeout, in seconds. Optional; if not specified, no timeouts will be applied.
=back
=cut
sub new {
my ($class, $opts) = @_;
$class = ref($class) || $class;
my %selfval = $opts ? %{$opts} : ();
my $self = \%selfval;
bless ($self, $class);
$self;
}
=item $t->run($coderef)
Run a code reference within the currently-defined timeout.
The timeout is as defined by the B<secs> parameter to the constructor.
Returns whatever the subroutine returns, or C<undef> on timeout.
If the timer times out, C<$t-<gt>timed_out()> will return C<1>.
Time elapsed is not cumulative; multiple runs of C<run> will restart the
timeout from scratch.
=item $t->run_and_catch($coderef)
Run a code reference, as per C<$t-<gt>run()>, but also catching any
C<die()> calls within the code reference.
Returns C<undef> if no C<die()> call was executed and C<$@> was unset, or the
value of C<$@> if it was set. (The timeout event doesn't count as a C<die()>.)
=cut
sub run { $_[0]->_run($_[1], 0); }
sub run_and_catch { $_[0]->_run($_[1], 1); }
sub _run { my ($self, $sub, $and_catch) = @_;
delete $self->{timed_out};
if (!$self->{secs}) { return &$sub;
}
if ($self->{secs} < 0) {
die "Mail::SpamAssassin::Timeout: oops? neg value for 'secs': $self->{secs}";
}
my $oldalarm = 0;
my $ret;
my $timedout = 0;
eval {
local $SIG{ALRM} = sub { $timedout++; die "__alarm__ignore__\n" };
local $SIG{__DIE__};
$oldalarm = alarm($self->{secs});
$ret = &$sub;
alarm 0;
};
my $err = $@;
if (defined $oldalarm) {
alarm $oldalarm;
}
if ($err) {
if ($err =~ /__alarm__ignore__/) {
$self->{timed_out} = 1;
} else {
if ($and_catch) {
return $@;
} else {
die $@; }
}
} elsif ($timedout) {
warn "timeout with empty \$@";
$self->{timed_out} = 1;
}
if ($and_catch) {
return; } else {
return $ret;
}
}
=item $t->timed_out()
Returns C<1> if the most recent code executed in C<run()> timed out, or
C<undef> if it did not.
=cut
sub timed_out {
my ($self) = @_;
return $self->{timed_out};
}
=item $t->reset()
If called within a C<run()> code reference, causes the current alarm timer to
be reset to its starting value.
=cut
sub reset {
my ($self) = @_;
alarm($self->{secs});
}
1;