Search code examples
perlsignalsalarmrace-condition

Use alarm to set a timeout for reading stdin


I have this code:

#!/usr/bin/perl
use strict;
use warnings;
my ($timeout, $size, $buffer) = (10, 10, undef);
eval {
    local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required
    alarm $timeout;
    my $nread = sysread STDIN, $buffer, $size;
    # !!!! race condition !!!!!
    alarm 0;
    print "$nread: $buffer";
};
if ($@) {    
    warn $@;
}

Is it correct? May be there is a race condition between 8 and 9 line?


Solution

  • Let's look, what's going on:

    my ($timeout, $size, $buffer) = (10, 10, undef);
    eval {
        #establish ALRM signal handler
        local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required
    
        #send alarm signal to program in 10 second
        alarm $timeout;
    
        #try yo read 10 bytes of data into $buffer
        my $nread = sysread STDIN, $buffer, $size;
    
        #cancel the previous timer without starting a new one 
        #if we returned from sysread. Yes, if 10 seconds pass
        #before the next function is executed, the script will
        #die even though the data was read
        alarm 0;
    
        #print number of bytes read (will be 10) and the string,
        #read from input
        print "$nread: $buffer";
    };
    

    $@ is set if the string to be eval-ed did not compile, or if Perl code executed during evaluation die()d. In these cases the value of $@ is the compile error, or the argument to die:

    if ($@) {    
        warn $@;
    }
    

    So, this will print die message "alarm\n" if we didn't return from sysread in 10 second.

    In the very unlikely case, when the input will be received just before 10 seconds elapse and we won't be able to run alarm 0;, I suggest to use the following code:

    my ($timeout, $size, $buffer) = (10, 10, undef);
    
    #I define $nread before the signal handler as undef, so if it's defined
    #it means, that sysread was executed and the value was assigned
    my $nread = undef;
    eval {
        local $SIG{ALRM} = sub {
    
            #if it's not defined - it means, that sysread wasn't executed
            unless(defined($nread))
            {
                die "alarm\n";
            }
        };
        alarm $timeout;
        $nread = sysread STDIN, $buffer, $size;
        alarm 0;
        print "$nread: $buffer";
    };
    

    Unfortunately, it doesn't save us from the case, when assignment operator wasn't executed.

    Links:

    http://perldoc.perl.org/functions/alarm.html

    http://perldoc.perl.org/perlvar.html

    http://perldoc.perl.org/functions/sysread.html