X-Original-To: jm@jmason.org
Delivered-To: jm@dogma.boxhost.net
Received: from localhost [127.0.0.1]
	by localhost with IMAP (fetchmail-6.2.5)
	for jm@localhost (single-drop); Thu, 09 Mar 2006 21:44:57 +0000 (GMT)
Received: from minotaur.apache.org (minotaur.apache.org [209.237.227.194])
	by dogma.boxhost.net (Postfix) with SMTP id 0D3463105BF
	for <jm@jmason.org>; Thu,  9 Mar 2006 19:52:50 +0000 (GMT)
Received: (qmail 30661 invoked by uid 1833); 9 Mar 2006 19:52:44 -0000
Delivered-To: jm@locus.apache.org
Received: (qmail 30451 invoked from network); 9 Mar 2006 19:52:38 -0000
Received: from hermes.apache.org (HELO mail.apache.org) (209.237.227.199)
  by minotaur.apache.org with SMTP; 9 Mar 2006 19:52:38 -0000
Received: (qmail 97860 invoked by uid 500); 9 Mar 2006 19:52:29 -0000
Delivered-To: apmail-jm@apache.org
Received: (qmail 97837 invoked by uid 500); 9 Mar 2006 19:52:28 -0000
Mailing-List: contact commits-help@spamassassin.apache.org; run by ezmlm
Precedence: bulk
list-help: <mailto:commits-help@spamassassin.apache.org>
list-unsubscribe: <mailto:commits-unsubscribe@spamassassin.apache.org>
List-Post: <mailto:commits@spamassassin.apache.org>
Reply-To: "SpamAssassin Dev" <dev@spamassassin.apache.org>
List-Id: <commits.spamassassin.apache.org>
Delivered-To: mailing list commits@spamassassin.apache.org
Received: (qmail 97826 invoked by uid 99); 9 Mar 2006 19:52:28 -0000
Received: from asf.osuosl.org (HELO asf.osuosl.org) (140.211.166.49)
    by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 09 Mar 2006 11:52:28 -0800
X-ASF-Spam-Status: No, hits=-9.4 required=10.0
	tests=ALL_TRUSTED,NO_REAL_NAME
Received: from [209.237.227.194] (HELO minotaur.apache.org) (209.237.227.194)
    by apache.org (qpsmtpd/0.29) with SMTP; Thu, 09 Mar 2006 11:52:26 -0800
Received: (qmail 29644 invoked by uid 65534); 9 Mar 2006 19:52:06 -0000
Message-ID: <20060309195206.29643.qmail@minotaur.apache.org>
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: svn commit: r384590 - in /spamassassin/branches/3.1: ./
 lib/Mail/SpamAssassin/ lib/Mail/SpamAssassin/Plugin/ spamd/
Date: Thu, 09 Mar 2006 19:52:02 -0000
To: commits@spamassassin.apache.org
From: sidney@apache.org
X-Mailer: svnmailer-1.0.7
X-Virus-Checked: Checked by ClamAV on apache.org
Status: O
X-UID: 60795
X-Keywords:                                                                                                    

Author: sidney
Date: Thu Mar  9 11:51:59 2006
New Revision: 384590

URL: http://svn.apache.org/viewcvs?rev=384590&view=rev
Log:
Bug 4696: consolidated fixes for timeout bugs

Added:
    spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm
Modified:
    spamassassin/branches/3.1/MANIFEST
    spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm
    spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm
    spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm
    spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm
    spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm
    spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm
    spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm
    spamassassin/branches/3.1/spamd/spamd.raw

Modified: spamassassin/branches/3.1/MANIFEST
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/MANIFEST?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/MANIFEST (original)
+++ spamassassin/branches/3.1/MANIFEST Thu Mar  9 11:51:59 2006
@@ -89,6 +89,7 @@
 lib/Mail/SpamAssassin/SQLBasedAddrList.pm
 lib/Mail/SpamAssassin/SpamdForkScaling.pm
 lib/Mail/SpamAssassin/SubProcBackChannel.pm
+lib/Mail/SpamAssassin/Timeout.pm
 lib/Mail/SpamAssassin/Util.pm
 lib/Mail/SpamAssassin/Util/DependencyInfo.pm
 lib/Mail/SpamAssassin/Util/Progress.pm

Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm (original)
+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm Thu Mar  9 11:51:59 2006
@@ -142,7 +142,7 @@
 
   if ($level eq "error") {
     # don't log alarm timeouts or broken pipes of various plugins' network checks
-    return if ($message[0] =~ /__(?:alarm|brokenpipe)__ignore__/);
+    return if ($message[0] =~ /__ignore__/);
 
     # dos: we can safely ignore any die's that we eval'd in our own modules so
     # don't log them -- this is caller 0, the use'ing package is 1, the eval is 2

Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm (original)
+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm Thu Mar  9 11:51:59 2006
@@ -44,6 +44,7 @@
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Timeout;
 use IO::Socket;
 use strict;
 use warnings;
@@ -375,15 +376,10 @@
 
   $permsgstatus->enter_helper_run_mode();
 
-  my $oldalarm = 0;
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
+  my $err = $timer->run_and_catch(sub {
 
-  eval {
-    # safe to use $SIG{ALRM} here instead of Util::trap_sigalrm_fully(),
-    # since there are no killer regexp hang dangers here
-    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
-
-    $oldalarm = alarm $timeout;
+    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
     my $sock = IO::Socket::UNIX->new(Type => SOCK_STREAM,
       Peer => $sockpath) || dbg("dcc: failed to open socket") && die;
@@ -419,28 +415,20 @@
     }
 
     dbg("dcc: dccifd got response: $response");
+  
+  });
 
-    if (defined $oldalarm) {
-      alarm $oldalarm; $oldalarm = undef;
-    }
-  };
+  $permsgstatus->leave_helper_run_mode();
 
-  my $err = $@;
-  if (defined $oldalarm) {
-    alarm $oldalarm; $oldalarm = undef;
+  if ($timer->timed_out()) {
+    dbg("dcc: dccifd check timed out after $timeout secs.");
+    return 0;
   }
-  $permsgstatus->leave_helper_run_mode();
 
   if ($err) {
     chomp $err;
-    $response = undef;
-    if ($err eq "__alarm__ignore__") {
-      dbg("dcc: dccifd check timed out after $timeout secs.");
-      return 0;
-    } else {
-      warn("dcc: dccifd -> check skipped: $! $err");
-      return 0;
-    }
+    warn("dcc: dccifd -> check skipped: $! $err");
+    return 0;
   }
 
   if (!defined $response || $response !~ /^X-DCC/) {
@@ -494,17 +482,12 @@
 
   # use a temp file here -- open2() is unreliable, buffering-wise, under spamd
   my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);
-  my $oldalarm = 0;
-
   my $pid;
-  eval {
-    # safe to use $SIG{ALRM} here instead of Util::trap_sigalrm_fully(),
-    # since there are no killer regexp hang dangers here
-    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
 
-    $oldalarm = alarm $timeout;
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
+  my $err = $timer->run_and_catch(sub {
+
+    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
     # note: not really tainted, this came from system configuration file
     my $path = Mail::SpamAssassin::Util::untaint_file_path($self->{main}->{conf}->{dcc_path});
@@ -542,17 +525,7 @@
 
     dbg("dcc: got response: $response");
 
-    # note: this must be called BEFORE leave_helper_run_mode()
-    # $self->cleanup_kids($pid);
-    if (defined $oldalarm) {
-      alarm $oldalarm; $oldalarm = undef;
-    }
-  };
-
-  my $err = $@;
-  if (defined $oldalarm) {
-    alarm $oldalarm; $oldalarm = undef;
-  }
+  });
 
   if (defined(fileno(*DCC))) {  # still open
     if ($pid) {
@@ -564,11 +537,14 @@
   }
   $permsgstatus->leave_helper_run_mode();
 
+  if ($timer->timed_out()) {
+    dbg("dcc: check timed out after $timeout seconds");
+    return 0;
+  }
+
   if ($err) {
     chomp $err;
-    if ($err eq "__alarm__ignore__") {
-      dbg("dcc: check timed out after $timeout seconds");
-    } elsif ($err eq "__brokenpipe__ignore__") {
+    if ($err eq "__brokenpipe__ignore__") {
       dbg("dcc: check failed: broken pipe");
     } elsif ($err eq "no response") {
       dbg("dcc: check failed: no response");
@@ -645,47 +621,37 @@
   my ($self, $options, $tmpf) = @_;
   my $timeout = $options->{report}->{conf}->{dcc_timeout};
 
-  $options->{report}->enter_helper_run_mode();
+  # note: not really tainted, this came from system configuration file
+  my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{dcc_path});
 
-  my $oldalarm = 0;
+  my $opts = $options->{report}->{conf}->{dcc_options} || '';
 
-  eval {
-    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
 
-    $oldalarm = alarm $timeout;
-
-    # note: not really tainted, this came from system configuration file
-    my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{dcc_path});
+  $options->{report}->enter_helper_run_mode();
+  my $err = $timer->run_and_catch(sub {
 
-    my $opts = $options->{report}->{conf}->{dcc_options} || '';
+    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
     my $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
-	$tmpf, 1, $path, "-t", "many", split(' ', $opts));
+        $tmpf, 1, $path, "-t", "many", split(' ', $opts));
     $pid or die "$!\n";
 
     my @ignored = <DCC>;
     $options->{report}->close_pipe_fh(\*DCC);
-
     waitpid ($pid, 0);
-    if (defined $oldalarm) {
-      alarm $oldalarm; $oldalarm = undef;
-    }
-  };
+  
+  });
+  $options->{report}->leave_helper_run_mode();
 
-  my $err = $@;
-  if (defined $oldalarm) {
-    alarm $oldalarm; $oldalarm = undef;
+  if ($timer->timed_out()) {
+    dbg("reporter: DCC report timed out after $timeout seconds");
+    return 0;
   }
 
-  $options->{report}->leave_helper_run_mode();
-
   if ($err) {
     chomp $err;
-    if ($err eq "__alarm__ignore__") {
-      dbg("reporter: DCC report timed out after $timeout seconds");
-    } elsif ($err eq "__brokenpipe__ignore__") {
+    if ($err eq "__brokenpipe__ignore__") {
       dbg("reporter: DCC report failed: broken pipe");
     } else {
       warn("reporter: DCC report failed: $err\n");

Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm (original)
+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm Thu Mar  9 11:51:59 2006
@@ -34,6 +34,8 @@
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Timeout;
+
 use strict;
 use warnings;
 use bytes;
@@ -165,30 +167,22 @@
   }
 
   my $timeout = $scan->{conf}->{domainkeys_timeout};
-  my $oldalarm = 0;
 
-  eval {
-    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
-    $oldalarm = alarm($timeout);
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
+  my $err = $timer->run_and_catch(sub {
+
     $self->_dk_lookup_trapped($scan, $message, $domain);
-    if (defined $oldalarm) {
-      alarm $oldalarm; $oldalarm = undef;
-    }
-  };
-
-  my $err = $@;
-  if (defined $oldalarm) {
-    alarm $oldalarm; $oldalarm = undef;
+
+  });
+
+  if ($timer->timed_out()) {
+    dbg("dk: lookup timed out after $timeout seconds");
+    return 0;
   }
 
   if ($err) {
     chomp $err;
-    if ($err eq "__alarm__ignore__") {
-      dbg("dk: lookup timed out after $timeout seconds");
-    } else {
-      warn("dk: lookup failed: $err\n");
-    }
+    warn("dk: lookup failed: $err\n");
     return 0;
   }
 

Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm (original)
+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm Thu Mar  9 11:51:59 2006
@@ -35,6 +35,7 @@
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Timeout;
 use strict;
 use warnings;
 use bytes;
@@ -229,27 +230,22 @@
 
   $pyzor_count = 0;
   $pyzor_whitelisted = 0;
-
-  $permsgstatus->enter_helper_run_mode();
+  my $pid;
 
   # use a temp file here -- open2() is unreliable, buffering-wise, under spamd
   my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);
-  my $oldalarm = 0;
 
-  my $pid;
-  eval {
-    # safe to use $SIG{ALRM} here instead of Util::trap_sigalrm_fully(),
-    # since there are no killer regexp hang dangers here
-    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
+  # note: not really tainted, this came from system configuration file
+  my $path = Mail::SpamAssassin::Util::untaint_file_path($self->{main}->{conf}->{pyzor_path});
+
+  my $opts = $self->{main}->{conf}->{pyzor_options} || '';
 
-    $oldalarm = alarm $timeout;
+  $permsgstatus->enter_helper_run_mode();
 
-    # note: not really tainted, this came from system configuration file
-    my $path = Mail::SpamAssassin::Util::untaint_file_path($self->{main}->{conf}->{pyzor_path});
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
+  my $err = $timer->run_and_catch(sub {
 
-    my $opts = $self->{main}->{conf}->{pyzor_options} || '';
+    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
  
     dbg("pyzor: opening pipe: " . join(' ', $path, $opts, "check", "< $tmpf"));
 
@@ -273,21 +269,7 @@
       die("internal error\n");
     }
 
-    # note: this must be called BEFORE leave_helper_run_mode()
-    # $self->cleanup_kids($pid);
-
-    # attempt to call this inside the eval, as leaving this scope is
-    # a slow operation and timing *that* out is pointless
-    if (defined $oldalarm) { 
-      alarm $oldalarm; $oldalarm = undef;
-    }
-  };
-
-  # clear the alarm before doing lots of time-consuming hard work
-  my $err = $@;
-  if (defined $oldalarm) { 
-    alarm $oldalarm; $oldalarm = undef;
-  }
+  });
 
   if (defined(fileno(*PYZOR))) {  # still open
     if ($pid) {
@@ -299,11 +281,14 @@
   }
   $permsgstatus->leave_helper_run_mode();
 
+  if ($timer->timed_out()) {
+    dbg("pyzor: check timed out after $timeout seconds");
+    return 0;
+  }
+
   if ($err) {
     chomp $err;
-    if ($err eq "__alarm__ignore__") {
-      dbg("pyzor: check timed out after $timeout seconds");
-    } elsif ($err eq "__brokenpipe__ignore__") {
+    if ($err eq "__brokenpipe__ignore__") {
       dbg("pyzor: check failed: broken pipe");
     } elsif ($err eq "no response") {
       dbg("pyzor: check failed: no response");
@@ -364,23 +349,19 @@
 
 sub pyzor_report {
   my ($self, $options, $tmpf) = @_;
+
+  # note: not really tainted, this came from system configuration file
+  my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{pyzor_path});
+
+  my $opts = $options->{report}->{conf}->{pyzor_options} || '';
   my $timeout = $self->{main}->{conf}->{pyzor_timeout};
 
   $options->{report}->enter_helper_run_mode();
 
-  my $oldalarm = 0;
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
+  my $err = $timer->run_and_catch(sub {
 
-  eval {
-    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
     local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
-
-    $oldalarm = alarm $timeout;
-
-    # note: not really tainted, this came from system configuration file
-    my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{pyzor_path});
-
-    my $opts = $options->{report}->{conf}->{pyzor_options} || '';
 
     dbg("pyzor: opening pipe: " . join(' ', $path, $opts, "report", "< $tmpf"));
 
@@ -391,23 +372,19 @@
     my @ignored = <PYZOR>;
     $options->{report}->close_pipe_fh(\*PYZOR);
 
-    if (defined $oldalarm) { 
-      alarm $oldalarm; $oldalarm = undef;
-    }
     waitpid ($pid, 0);
-  };
+  });
 
-  my $err = $@;
-  if (defined $oldalarm) { 
-    alarm $oldalarm; $oldalarm = undef;
-  }
   $options->{report}->leave_helper_run_mode();
 
+  if ($timer->timed_out()) {
+    dbg("reporter: pyzor report timed out after $timeout seconds");
+    return 0;
+  }
+
   if ($err) {
     chomp $err;
-    if ($err eq '__alarm__ignore__') {
-      dbg("reporter: pyzor report timed out after $timeout seconds");
-    } elsif ($err eq '__brokenpipe__ignore__') {
+    if ($err eq '__brokenpipe__ignore__') {
       dbg("reporter: pyzor report failed: broken pipe");
     } else {
       warn("reporter: pyzor report failed: $err\n");

Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm (original)
+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm Thu Mar  9 11:51:59 2006
@@ -143,14 +143,11 @@
   }
 
   Mail::SpamAssassin::PerMsgStatus::enter_helper_run_mode($self);
-  my $oldalarm = 0;
 
-  eval {
-    local ($^W) = 0;    # argh, warnings in Razor
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
+  my $err = $timer->run_and_catch(sub {
 
-    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
-    $oldalarm = alarm $timeout;
+    local ($^W) = 0;    # argh, warnings in Razor
 
     # everything's in the module!
     my $rc = Razor2::Client::Agent->new("razor-$type");
@@ -184,7 +181,7 @@
       # let's reset the alarm since get_server_info() calls
       # nextserver() which calls discover() which very likely will
       # reset the alarm for us ... how polite.  :(
-      alarm $timeout;
+      $timer->reset();
 
       # no facility prefix on this die
       my $sigs = $rc->compute_sigs($objects)
@@ -219,100 +216,96 @@
 	  my $error = $rc->errprefix("$debug: spamassassin") || "$debug: razor2 had unknown error during disconnect";
 	  die $error;
 	}
+      }
 
-	# if we got here, we're done doing remote stuff, abort the alert
-        if (defined $oldalarm) {
-          alarm $oldalarm; $oldalarm = undef;
-        }
-
-	# Razor 2.14 says that if we get here, we did ok.
-	$return = 1;
+      # Razor 2.14 says that if we get here, we did ok.
+      $return = 1;
 
-	# figure out if we have a log file we need to close...
-	if (ref($rc->{logref}) && exists $rc->{logref}->{fd}) {
-	  # the fd can be stdout or stderr, so we need to find out if it is
-	  # so we don't close them by accident.  Note: we can't just
-	  # undef the fd here (like the IO::Handle manpage says we can)
-	  # because it won't actually close, unfortunately. :(
-	  my $untie = 1;
-	  foreach my $log (*STDOUT{IO}, *STDERR{IO}) {
-	    if ($log == $rc->{logref}->{fd}) {
-	      $untie = 0;
-	      last;
-	    }
-	  }
-	  close $rc->{logref}->{fd} if ($untie);
-	}
-
-	if ($type eq 'check') {
-	  # so $objects->[0] is the first (only) message, and ->{spam} is a general yes/no
-	  push(@results, { result => $objects->[0]->{spam} });
+      # figure out if we have a log file we need to close...
+      if (ref($rc->{logref}) && exists $rc->{logref}->{fd}) {
+        # the fd can be stdout or stderr, so we need to find out if it is
+        # so we don't close them by accident.  Note: we can't just
+        # undef the fd here (like the IO::Handle manpage says we can)
+        # because it won't actually close, unfortunately. :(
+        my $untie = 1;
+        foreach my $log (*STDOUT{IO}, *STDERR{IO}) {
+          if ($log == $rc->{logref}->{fd}) {
+            $untie = 0;
+            last;
+          }
+        }
+        close $rc->{logref}->{fd} if ($untie);
+      }
 
-	  # great for debugging, but leave this off!
-	  #use Data::Dumper;
-	  #print Dumper($objects),"\n";
-
-	  # ->{p} is for each part of the message
-	  # so go through each part, taking the highest cf we find
-	  # of any part that isn't contested (ct).  This helps avoid false
-	  # positives.  equals logic_method 4.
-	  #
-	  # razor-agents < 2.14 have a different object format, so we now support both.
-	  # $objects->[0]->{resp} vs $objects->[0]->{p}->[part #]->{resp}
-	  my $part = 0;
-	  my $arrayref = $objects->[0]->{p} || $objects;
-	  if (defined $arrayref) {
-	    foreach my $cf (@{$arrayref}) {
-	      if (exists $cf->{resp}) {
-		for (my $response=0; $response<@{$cf->{resp}}; $response++) {
-		  my $tmp = $cf->{resp}->[$response];
-		  my $tmpcf = $tmp->{cf}; # Part confidence
-		  my $tmpct = $tmp->{ct}; # Part contested?
-		  my $engine = $cf->{sent}->[$response]->{e};
-
-		  # These should always be set, but just in case ...
-		  $tmpcf = 0 unless defined $tmpcf;
-		  $tmpct = 0 unless defined $tmpct;
-		  $engine = 0 unless defined $engine;
-
-		  push(@results,
-		       { part => $part, engine => $engine, contested => $tmpct, confidence => $tmpcf });
-		}
-	      }
-	      else {
-		push(@results, { part => $part, noresponse => 1 });
-	      }
-	      $part++;
-	    }
-	  }
-	  else {
-	    # If we have some new $objects format that isn't close to
-	    # the current razor-agents 2.x version, we won't FP but we
-	    # should alert in debug.
-	    dbg("$debug: it looks like the internal Razor object has changed format!");
-	  }
-	}
+      if ($type eq 'check') {
+        # so $objects->[0] is the first (only) message, and ->{spam} is a general yes/no
+        push(@results, { result => $objects->[0]->{spam} });
+
+        # great for debugging, but leave this off!
+        #use Data::Dumper;
+        #print Dumper($objects),"\n";
+
+        # ->{p} is for each part of the message
+        # so go through each part, taking the highest cf we find
+        # of any part that isn't contested (ct).  This helps avoid false
+        # positives.  equals logic_method 4.
+        #
+        # razor-agents < 2.14 have a different object format, so we now support both.
+        # $objects->[0]->{resp} vs $objects->[0]->{p}->[part #]->{resp}
+        my $part = 0;
+        my $arrayref = $objects->[0]->{p} || $objects;
+        if (defined $arrayref) {
+          foreach my $cf (@{$arrayref}) {
+            if (exists $cf->{resp}) {
+              for (my $response=0; $response<@{$cf->{resp}}; $response++) {
+                my $tmp = $cf->{resp}->[$response];
+                my $tmpcf = $tmp->{cf}; # Part confidence
+                my $tmpct = $tmp->{ct}; # Part contested?
+                my $engine = $cf->{sent}->[$response]->{e};
+
+                # These should always be set, but just in case ...
+                $tmpcf = 0 unless defined $tmpcf;
+                $tmpct = 0 unless defined $tmpct;
+                $engine = 0 unless defined $engine;
+
+                push(@results,
+                      { part => $part, engine => $engine, contested => $tmpct, confidence => $tmpcf });
+              }
+            }
+            else {
+              push(@results, { part => $part, noresponse => 1 });
+            }
+            $part++;
+          }
+        }
+        else {
+          # If we have some new $objects format that isn't close to
+          # the current razor-agents 2.x version, we won't FP but we
+          # should alert in debug.
+          dbg("$debug: it looks like the internal Razor object has changed format!");
+        }
       }
     }
     else {
       warn "$debug: undefined Razor2::Client::Agent\n";
     }
   
-    if (defined $oldalarm) {
-      alarm $oldalarm; $oldalarm = undef;
-    }
-  };
+  });
+
+  # OK, that's enough Razor stuff. now, reset all that global
+  # state it futzes with :(
+  # work around serious brain damage in Razor2 (constant seed)
+  srand;
 
-  my $err = $@;
-  if (defined $oldalarm) {
-    alarm $oldalarm; $oldalarm = undef;
+  Mail::SpamAssassin::PerMsgStatus::leave_helper_run_mode($self);
+
+  if ($timer->timed_out()) {
+    dbg("$debug: razor2 $type timed out after $timeout seconds");
   }
 
   if ($err) {
     chomp $err;
-    if ($err eq "__alarm__ignore__") {
-      dbg("$debug: razor2 $type timed out after $timeout seconds");
-    } elsif ($err =~ /(?:could not connect|network is unreachable)/) {
+    if ($err =~ /(?:could not connect|network is unreachable)/) {
       # make this a dbg(); SpamAssassin will still continue,
       # but without Razor checking.  otherwise there may be
       # DSNs and errors in syslog etc., yuck
@@ -323,11 +316,6 @@
       warn("$debug: razor2 $type failed: $! $err");
     }
   }
-
-  # work around serious brain damage in Razor2 (constant seed)
-  srand;
-
-  Mail::SpamAssassin::PerMsgStatus::leave_helper_run_mode($self);
 
   # razor also debugs to stdout. argh. fix it to stderr...
   if (would_log('dbg', $debug)) {

Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm (original)
+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm Thu Mar  9 11:51:59 2006
@@ -34,6 +34,7 @@
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Timeout;
 use strict;
 use warnings;
 use bytes;
@@ -300,30 +301,17 @@
 
   my ($result, $comment);
   my $timeout = $scanner->{conf}->{spf_timeout};
-  my $oldalarm = 0;
 
-  eval {
-    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
-    $oldalarm = alarm($timeout);
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
+  my $err = $timer->run_and_catch(sub {
+
     ($result, $comment) = $query->result();
-    if (defined $oldalarm) {
-      alarm $oldalarm; $oldalarm = undef;
-    }
-  };
 
-  my $err = $@;
-  if (defined $oldalarm) {
-    alarm $oldalarm; $oldalarm = undef;
-  }
+  });
 
   if ($err) {
     chomp $err;
-    if ($err eq "__alarm__ignore__") {
-      dbg("spf: lookup timed out after $timeout seconds");
-    } else {
-      warn("spf: lookup failed: $err\n");
-    }
+    warn("spf: lookup failed: $err\n");
     return 0;
   }
 

Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm (original)
+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm Thu Mar  9 11:51:59 2006
@@ -25,6 +25,7 @@
 
 use Mail::SpamAssassin::Util;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Timeout;
 
 use vars qw {
   @PFSTATE_VARS %EXPORT_TAGS @EXPORT_OK
@@ -109,6 +110,9 @@
 
   delete $self->{kids}->{$pid};
 
+  # note this for the select()-caller's benefit
+  $self->{child_just_exited} = 1;
+
   # remove the child from the backchannel list, too
   $self->{backchannel}->delete_socket_for_child($pid);
 
@@ -188,24 +192,63 @@
     vec($rin, $self->{server_fileno}, 1) = 0;
   }
 
-  my ($rout, $eout, $nfound, $timeleft);
+  my ($rout, $eout, $nfound, $timeleft, $selerr);
+
+  # use alarm to back up select()'s built-in alarm, to debug Theo's bug.
+  # not that I can remember what Theo's bug was, but hey ;)    A good
+  # 60 seconds extra on the alarm() should make that quite rare...
+
+  my $timer = Mail::SpamAssassin::Timeout->new({ secs => ($tout*2) + 60 });
 
-  # use alarm to back up select()'s built-in alarm, to debug theo's bug
-  eval {
-    Mail::SpamAssassin::Util::trap_sigalrm_fully(sub { die "tcp timeout"; });
-    alarm ($tout*2) if ($tout);
+  $timer->run(sub {
+
+    $self->{child_just_exited} = 0;
     ($nfound, $timeleft) = select($rout=$rin, undef, $eout=$rin, $tout);
-  };
-  alarm 0;
+    $selerr = $!;
 
-  if ($@) {
-    warn "prefork: select timeout failed! recovering\n";
-    sleep 1;        # avoid overload
-    return;
-  }
+  });
+
+  # bug 4696: under load, the process can go for such a long time without
+  # being context-switched in, that when it does return the alarm() fires
+  # before the select() timeout does.   Treat this as a select() timeout
+  if ($timer->timed_out) {
+    dbg("prefork: select timed out (via alarm)");
+    $nfound = 0;
+    $timeleft = 0;
+  }
+
+  # errors; handle undef *or* -1 returned.  do this before "errors on
+  # the handle" below, since an error condition is signalled both via
+  # a -1 return and a $eout bit.
+  if (!defined $nfound || $nfound < 0)
+  {
+    if (exists &Errno::EINTR && $selerr == &Errno::EINTR)
+    {
+      # this happens if the process is signalled during the select(),
+      # for example if someone sends SIGHUP to reload the configuration.
+      # just return inmmediately
+      dbg("prefork: select returned err $selerr, probably signalled");
+      return;
+    }
+
+    # if a child exits during that select() call, it generates a spurious
+    # error, like this:
+    #
+    # Jan 29 12:53:17 dogma spamd[18518]: prefork: child states: BI
+    # Jan 29 12:53:17 dogma spamd[18518]: spamd: handled cleanup of child pid 13101 due to SIGCHLD
+    # Jan 29 12:53:17 dogma spamd[18518]: prefork: select returned -1! recovering:
+    #
+    # avoid by setting a boolean in the child_exited() callback and checking
+    # it here.  log $! just in case, though.
+    if ($self->{child_just_exited} && $nfound == -1) {
+      dbg("prefork: select returned -1 due to child exiting, ignored ($selerr)");
+      return;
+    }
+
+    warn "prefork: select returned ".
+            (defined $nfound ? $nfound : "undef").
+            "! recovering: $selerr\n";
 
-  if (!defined $nfound) {
-    warn "prefork: select returned undef! recovering\n";
     sleep 1;        # avoid overload
     return;
   }
@@ -213,7 +256,7 @@
   # errors on the handle?
   # return them immediately, they may be from a SIGHUP restart signal
   if (vec ($eout, $self->{server_fileno}, 1)) {
-    warn "prefork: select returned error on server filehandle: $!\n";
+    warn "prefork: select returned error on server filehandle: $selerr $!\n";
     return;
   }
 
@@ -282,7 +325,7 @@
 
   my ($sock, $kid);
   while (($kid, $sock) = each %{$self->{backchannel}->{kids}}) {
-    $self->syswrite_with_retry($sock, PF_PING_ORDER) and next;
+    $self->syswrite_with_retry($sock, PF_PING_ORDER, $kid, 3) and next;
 
     warn "prefork: write of ping failed to $kid fd=".$sock->fileno.": ".$!;
 
@@ -353,7 +396,7 @@
       return $self->order_idle_child_to_accept();
     }
 
-    if (!$self->syswrite_with_retry($sock, PF_ACCEPT_ORDER))
+    if (!$self->syswrite_with_retry($sock, PF_ACCEPT_ORDER, $kid))
     {
       # failure to write to the child; bad news.  call it dead
       warn "prefork: killing rogue child $kid, failed to write on fd ".$sock->fileno.": $!\n";
@@ -396,7 +439,7 @@
   my ($self, $kid) = @_;
   if ($self->{waiting_for_idle_child}) {
     my $sock = $self->{backchannel}->get_socket_for_child($kid);
-    $self->syswrite_with_retry($sock, PF_ACCEPT_ORDER)
+    $self->syswrite_with_retry($sock, PF_ACCEPT_ORDER, $kid)
         or die "prefork: $kid claimed it was ready, but write failed on fd ".
                             $sock->fileno.": ".$!;
     $self->{waiting_for_idle_child} = 0;
@@ -426,7 +469,7 @@
 sub report_backchannel_socket {
   my ($self, $str) = @_;
   my $sock = $self->{backchannel}->get_parent_socket();
-  $self->syswrite_with_retry($sock, $str)
+  $self->syswrite_with_retry($sock, $str, 'parent')
         or write "syswrite() to parent failed: $!";
 }
 
@@ -537,12 +580,31 @@
 }
 
 sub syswrite_with_retry {
-  my ($self, $sock, $buf) = @_;
+  my ($self, $sock, $buf, $targetname, $numretries) = @_;
+  $numretries ||= 10;       # default 10 retries
 
   my $written = 0;
+  my $try = 0;
 
 retry_write:
+
+  $try++;
+  if ($try > 1) {
+    warn "prefork: syswrite(".$sock->fileno.") to $targetname failed on try $try";
+    if ($try > $numretries) {
+      warn "prefork: giving up";
+      return undef;
+    }
+    else {
+      # give it 1 second to recover.  we retry indefinitely.
+      my $rout = '';
+      vec($rout, $sock->fileno, 1) = 1;
+      select(undef, $rout, undef, 1);
+    }
+  }
+
   my $nbytes = $sock->syswrite($buf);
+
   if (!defined $nbytes) {
     unless ((exists &Errno::EAGAIN && $! == &Errno::EAGAIN)
         || (exists &Errno::EWOULDBLOCK && $! == &Errno::EWOULDBLOCK))
@@ -551,13 +613,7 @@
       return undef;
     }
 
-    warn "prefork: syswrite(".$sock->fileno.") failed, retrying...";
-
-    # give it 5 seconds to recover.  we retry indefinitely.
-    my $rout = '';
-    vec($rout, $sock->fileno, 1) = 1;
-    select(undef, $rout, undef, 5);
-
+    warn "prefork: retrying syswrite(): $!";
     goto retry_write;
   }
   else {
@@ -568,7 +624,8 @@
       return $written;      # it's complete, we can return
     }
     else {
-      warn "prefork: partial write of $nbytes, towrite=".length($buf).
+      warn "prefork: partial write of $nbytes to ".
+            $targetname.", towrite=".length($buf).
             " sofar=".$written." fd=".$sock->fileno.", recovering";
       goto retry_write;
     }

Added: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm?rev=384590&view=auto
==============================================================================
--- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm (added)
+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm Thu Mar  9 11:51:59 2006
@@ -0,0 +1,215 @@
+# <@LICENSE>
+# Copyright 2004 Apache Software Foundation
+# 
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#     http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+=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 {      # private
+  my ($self, $sub, $and_catch) = @_;
+
+  delete $self->{timed_out};
+
+  if (!$self->{secs}) { # no timeout!  just call the sub and return.
+    return &$sub;
+  }
+
+  # assertion
+  if ($self->{secs} < 0) {
+    die "Mail::SpamAssassin::Timeout: oops? neg value for 'secs': $self->{secs}";
+  }
+
+  my $oldalarm = 0;
+  my $ret;
+
+  eval {
+    # note use of local to ensure closed scope here
+    local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
+    local $SIG{__DIE__};   # bug 4631
+
+    $oldalarm = alarm($self->{secs});
+
+    $ret = &$sub;
+
+    # Unset the alarm() before we leave eval{ } scope, as that stack-pop
+    # operation can take a second or two under load. Note: previous versions
+    # restored $oldalarm here; however, that is NOT what we want to do, since
+    # it creates a new race condition, namely that an old alarm could then fire
+    # while the stack-pop was underway, thereby appearing to be *this* timeout
+    # timing out. In terms of how we might possibly have nested timeouts in
+    # SpamAssassin, this is an academic issue with little impact, but it's
+    # still worth avoiding anyway.
+
+    alarm 0;
+  };
+
+  my $err = $@;
+
+  if (defined $oldalarm) {
+    # now, we could have died from a SIGALRM == timed out.  if so,
+    # restore the previously-active one, or zero all timeouts if none
+    # were previously active.
+    alarm $oldalarm;
+  }
+
+  if ($err) {
+    if ($err =~ /__alarm__ignore__/) {
+      $self->{timed_out} = 1;
+    } else {
+      if ($and_catch) {
+        return $@;
+      } else {
+        die $@;             # propagate any "real" errors
+      }
+    }
+  }
+
+  if ($and_catch) {
+    return;                 # undef
+  } 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;

Modified: spamassassin/branches/3.1/spamd/spamd.raw
URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/spamd/spamd.raw?rev=384590&r1=384589&r2=384590&view=diff
==============================================================================
--- spamassassin/branches/3.1/spamd/spamd.raw (original)
+++ spamassassin/branches/3.1/spamd/spamd.raw Thu Mar  9 11:51:59 2006
@@ -2049,6 +2049,9 @@
   foreach (keys %children) {
     kill 'INT' => $_;
     my $pid = waitpid($_, 0);
+    if ($scaling) {
+      $scaling->child_exited($pid);
+    }
     info("spamd: child $pid killed successfully");
   }
   %children = ();




 	