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 ([email protected] and [email protected] =~ /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 [email protected] 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.

2 comments:

Anonymous said...

Great thanks for a patch. It works for me very weell. And it solved the problem with ddclient You are writing about and witch appeared for me after about two or three years of using ddclient. I dont't know what has changed, but it caused ddclient to hang every few days when trying to retrive new ip adress - now it works for a few days like it should. Once again thank You !

Roeften said...

Well as far as I remember the bug was affecting only the web method. It would surface only if the modem or web server would not respond so it could surface once every 2 or more months!

I believe that the patch is included in the current release of ddclient.

Glad it worked for you.