Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions lib/Net/Daemon.pm
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,8 @@ sub Bind ($) {
# whole group by killing the childs.
my $childpid;
$exit = 0;
$SIG{'TERM'} = sub { die };
$SIG{'INT'} = sub { die };
$SIG{'TERM'} = sub { $exit = 1 };
$SIG{'INT'} = sub { $exit = 1 };
eval {
do {
$childpid = wait;
Expand All @@ -705,6 +705,12 @@ sub Bind ($) {
}
}

# Install signal handlers for graceful shutdown. SIGTERM and SIGINT
# set the Done flag so the accept loop exits cleanly, allowing the
# "Server terminating" log message to fire and sockets to close.
$SIG{'TERM'} = sub { $self->Done(1) };
$SIG{'INT'} = sub { $self->Done(1) };

my $time = $self->{'loop-timeout'} ? ( time() + $self->{'loop-timeout'} ) : 0;

my $client;
Expand Down Expand Up @@ -1206,6 +1212,21 @@ true work. The connection is closed when B<Run> returns and the corresponding
thread or process exits.


=head2 Signal Handling

B<Bind> installs handlers for C<SIGTERM> and C<SIGINT> that trigger a
graceful shutdown. When either signal is received, the server sets the
B<Done> flag so the accept loop exits cleanly, logs a "Server
terminating" message, and closes the listening socket.

In preforking mode (B<--childs>), the parent process catches the signal,
terminates the wait loop, and sends C<SIGTERM> to all child processes
before exiting.

If your subclass needs custom signal handling, override B<Bind> or
install your own handlers after calling C<SUPER::Bind>.


=head2 Error Handling

All methods are supposed to throw Perl exceptions in case of errors.
Expand Down
149 changes: 149 additions & 0 deletions t/signal.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# -*- perl -*-
#
# Test that SIGTERM/SIGINT cause graceful shutdown
#

require 5.004;
use strict;
use warnings;
use Test::More;
use POSIX qw/WNOHANG WIFEXITED WEXITSTATUS/;

# Skip on systems without fork
my $can_fork;
eval {
if ( $^O ne "MSWin32" ) {
my $pid = fork();
if ( defined($pid) ) {
if ( !$pid ) { exit 0; } # Child
}
waitpid( $pid, 0 );
$can_fork = 1;
}
};
if ( !$can_fork ) {
plan skip_all => 'This test requires a system with working forks';
}

plan tests => 6;

use Net::Daemon ();

# Subclass that records whether it reached the "Server terminating" log.
{

package GracefulTestDaemon;
our @ISA = qw(Net::Daemon);

sub Log {
my ( $self, $level, $fmt, @args ) = @_;
my $msg = sprintf( $fmt, @args );
if ( $msg =~ /terminating/ ) {
$self->{'_terminated_cleanly'} = 1;
}
}

sub Fatal {
my ( $self, @msg ) = @_;
die join( ' ', @msg );
}
}

# Helper: spawn a child daemon, send $signal, verify graceful exit.
# Uses a pipe for synchronization: child writes "R" after socket is
# bound and immediately before calling Bind() (which installs signal
# handlers before entering the accept loop).
sub test_signal {
my ( $signal, $label ) = @_;

pipe( my $rd, my $wr ) or die "pipe: $!";

my $pid = fork();
die "Cannot fork: $!" unless defined $pid;

if ( !$pid ) {

# Child: create a daemon and enter the accept loop
close $rd;

my $daemon = bless {
'mode' => 'single',
'proto' => 'tcp',
'catchint' => 1,
'debug' => 0,
}, 'GracefulTestDaemon';

require IO::Socket::INET;
$daemon->{'socket'} = IO::Socket::INET->new(
'LocalAddr' => '127.0.0.1',
'LocalPort' => 0,
'Proto' => 'tcp',
'Listen' => 1,
'Reuse' => 1,
) or die "Cannot create socket: $!";

# Signal parent that we're about to enter Bind().
# Bind() installs SIGTERM/SIGINT handlers before the accept loop,
# so by the time the parent reads this and sends the signal,
# the handlers will be in place.
syswrite( $wr, "R", 1 );
close $wr;

eval { $daemon->Bind() };

if ( $daemon->{'_terminated_cleanly'} ) {
exit(42); # Magic exit code = graceful
}
exit(1);
}

# Parent: wait for child to be ready
close $wr;
my $buf = '';
sysread( $rd, $buf, 1 ); # Blocks until child signals
close $rd;

# Small delay to ensure Bind() has installed signal handlers.
# The child signals before calling Bind(), so we need a brief
# window for Bind() to reach the handler installation point.
select( undef, undef, undef, 0.2 );

# Send signal
kill $signal, $pid;

# Wait for child with timeout
my $reaped = 0;
for my $attempt ( 1 .. 100 ) {
my $ret = waitpid( $pid, WNOHANG );
if ( $ret > 0 ) {
$reaped = 1;
my $status = $?;
ok( WIFEXITED($status),
"$label: child exited normally (not killed by signal)" );
is( WEXITSTATUS($status), 42,
"$label: child reached graceful shutdown path" );
last;
}
select( undef, undef, undef, 0.1 );
}
if ( !$reaped ) {
kill 'KILL', $pid;
waitpid( $pid, 0 );
fail("$label: child exited normally (not killed by signal)");
fail("$label: child reached graceful shutdown path");
}
}

# Test 1-2: SIGTERM triggers graceful shutdown
test_signal( 'TERM', 'SIGTERM' );

# Test 3-4: SIGINT triggers graceful shutdown
test_signal( 'INT', 'SIGINT' );

# Test 5-6: Done() is the mechanism — verify it directly
{
my $daemon = bless { 'mode' => 'single', 'debug' => 0 }, 'Net::Daemon';
ok( !$daemon->Done(), 'Done() starts as false' );
$daemon->Done(1);
ok( $daemon->Done(), 'Done(1) sets the flag' );
}
Loading