Sunday, October 18, 2009

ddclient hangs while getting IP address under certain circumstances

DDClient is a great dynamic IP updating Perl script which supports many providers and has cool features like grabbing the IP from your modem instead some server in the Internet.

However with the latest version 3.8.0 I have had an issue which led to ddclient blocking while trying to read the response from the modem.

I am not an expert in Perl, however I can code. So I had a look at the source. I've managed to find the problematic calls around line 1800:
my $timeout = 0;
local $SIG{'ALRM'} = sub { $timeout = 1; msg(qq{Alarm!\n});};

$0 = sprintf("%s - reading from %s port %s", $program, $peer, $port);

alarm(opt('timeout')) if opt('timeout') > 0;

while (!$timeout && ($_ = <$sd>)) {
$0 = sprintf("%s - read from %s port %s", $program, $peer, $port);
verbose("RECEIVE:", "%s", define($_, ""));
$reply .= $_ if defined $_;
}
if (opt('timeout') > 0) {
alarm(0);
}

I remembered from my early days in learning Perl that the perldoc was clear: Signal Handling in Perl is different from other languages.

So although in C/C++ you would expect the read to return EINTR when the process was interrupted with a SIG_ALRM that does not happen in Perl (not on all platforms anyway). Yes the signal is caught and the handler is called but the read is not interrupted and therefore the sentinel timeout is never checked.

To make things worse the timeout option in the socket constructor does nothing.

The solution is to force the read to end using an eval/die pair:

$0 = sprintf("%s - reading from %s port %s", $program, $peer,
$port);
eval {
local $SIG{'ALRM'} = sub { die "timeout";};
alarm(opt('timeout')) if opt('timeout') > 0;
while (($_ = <$sd>)) {
$0 = sprintf("%s - read from %s port %s", $program,
$peer, $port);
verbose("RECEIVE:", "%s", define($_, ""));
$reply .= $_ if defined $_;
}
if (opt('timeout') > 0) {
alarm(0);
}
};

close($sd);

# if ($timeout) {
if ($@ and $@ =~ /timeout/) {
warning("TIMEOUT: %s after %s seconds", $to,
opt('timeout'));
$reply = '';
}

The commented line is the original code.
So the code in the eval block runs as if it was a perl program within our program. When the signal is caught we kill the little program with die and execution is returned to the main program after the eval block.

Variable $@ holds the last eval return value so that is how we test if a timeout has occurred.

And now this works! I can finally see the timeout messages in the logs. I suspect that some people may never have a problem with this limitation of ddclient. If ddclient will get a response from the http server even a trashy one then it will not hang. I guess that my modem is buggy and it simply does not return a response but holds the connection open which makes ddclient block.

I have filed a patch with the code changes for ddclient.
Post a Comment