2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 07:08:12 +00:00

Merge pull request #7933 from VersatusHPC/refactor/r26-capture-lifecycle

refactor(dhcp): share the packet capture lifecycle
This commit is contained in:
Daniel Hilst
2026-10-12 10:54:00 -03:00
committed by GitHub
8 changed files with 286 additions and 172 deletions
+1
View File
@@ -205,6 +205,7 @@ use constant XCAT_PROBE_HELPERS => qw(
CommandUtils.pm
GlobalDef.pm
NetworkUtils.pm
PacketCapture.pm
ServiceNodeUtils.pm
);
+105
View File
@@ -0,0 +1,105 @@
package xCAT::PacketCapture;
use strict;
use warnings;
use File::Temp qw(tempfile);
use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM);
sub start {
my ($class, $tcpdump, $interface, $on_reap) = @_;
my ($handle, $file) = tempfile('detect_dhcpd.XXXXXX', TMPDIR => 1, UNLINK => 1);
close($handle);
my $self = bless {
file => $file,
on_reap => $on_reap,
stop_signals => POSIX::SigSet->new(SIGINT, SIGTERM),
signal_mask => POSIX::SigSet->new(),
}, $class;
# Keep INT and TERM blocked until the parent can stop its child.
sigprocmask(SIG_BLOCK, $self->{stop_signals}, $self->{signal_mask});
my $pid = fork;
unless (defined $pid) {
sigprocmask(SIG_SETMASK, $self->{signal_mask});
return;
}
if ($pid == 0) {
sigprocmask(SIG_SETMASK, $self->{signal_mask});
open(STDOUT, '>', $file) or _exit(1);
open(STDERR, '>', '/dev/null');
exec($tcpdump, '-i', $interface, 'port', '68', '-n', '-vvvvvv') or _exit(1);
}
$self->{pid} = $pid;
$SIG{INT} = $SIG{TERM} = sub { $self->stop(); exit 1; };
sigprocmask(SIG_SETMASK, $self->{signal_mask});
return $self;
}
sub file {
return $_[0]->{file};
}
sub pid {
return $_[0]->{pid};
}
sub stop {
my ($self) = @_;
# Block repeated interrupts until the owned child is reaped.
sigprocmask(SIG_BLOCK, $self->{stop_signals});
my $child = delete $self->{pid};
unless ($child) {
sigprocmask(SIG_SETMASK, $self->{signal_mask});
return '';
}
my $reaped = waitpid($child, WNOHANG);
my $early = ($reaped == $child) ? 1 : 0;
if ($reaped == 0) {
kill 'TERM', $child;
foreach (1 .. 50) {
last if ($reaped = waitpid($child, WNOHANG)) != 0;
select(undef, undef, undef, 0.1);
}
if ($reaped == 0) {
kill 'KILL', $child;
$reaped = waitpid($child, 0);
}
}
sigprocmask(SIG_SETMASK, $self->{signal_mask});
return 'could not be reaped' if $reaped != $child;
my ($signal, $status) = ($? & 127, $? >> 8);
$self->{on_reap}->($child) if $self->{on_reap};
my $how = $signal ? "on signal $signal" : "with status $status";
return "ended before the capture window did, $how" if $early;
return '' if $signal == 15 || (!$signal && !$status);
return "left the capture $how";
}
1;
__END__
=head1 NAME
xCAT::PacketCapture - tcpdump lifecycle for standalone DHCP detection commands
=head1 METHODS
=head2 start
Start tcpdump with its resolved path and interface. Return the capture object,
or undef when fork fails. An optional callback receives the reaped child PID.
The caller owns the capture window and must call C<stop> before reading C<file>.
C<pid> returns the child PID until C<stop> takes ownership of its cleanup.
This owner installs process-wide INT and TERM handlers that stop the capture
and exit 1. They remain installed after C<stop>. Use one capture per standalone
command, not inside a daemon. The private file is removed at process exit.
=head2 stop
Stop and reap the child once. Return an empty string for a clean stop, or a
diagnostic for an early exit, failure, or forced kill after the TERM grace period.
=cut
+10 -60
View File
@@ -6,9 +6,8 @@ BEGIN { $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : -d '/opt/xcat' ? '/o
use lib "$::XCATROOT/probe/lib/perl";
use probe_utils;
use xCAT::CommandUtils;
use xCAT::PacketCapture;
use File::Basename;
use File::Temp qw(tempfile);
use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM);
use IO::Socket::INET;
use Time::HiRes qw(gettimeofday sleep);
use Getopt::Long;
@@ -154,28 +153,16 @@ my $package = packdhcppkg($MAC);
probe_utils->send_msg("$output", "i", "Start to detect DHCP, please wait $duration seconds");
$msg = "fork a process to capture the packet by tcpdump";
my ($dumpfh, $dumpfile) = tempfile("detect_dhcpd.XXXXXX", TMPDIR => 1, UNLINK => 1);
close($dumpfh);
# INT and TERM stay blocked from the fork until the handler that stops the capture is in place.
my $stop_signals = POSIX::SigSet->new(SIGINT, SIGTERM);
my $signal_mask = POSIX::SigSet->new();
sigprocmask(SIG_BLOCK, $stop_signals, $signal_mask);
my $pid = fork;
if (!defined $pid) {
sigprocmask(SIG_SETMASK, $signal_mask);
my $capture = xCAT::PacketCapture->start($tcpdump, $nic, sub {
my ($child) = @_;
probe_utils->send_msg("$output", "d", "Kill process $child used to capture the packet by 'tcpdump'") if ($::VERBOSE);
});
unless ($capture) {
probe_utils->send_msg("$output", "f", $msg);
exit 1;
} elsif ($pid == 0) {
# Child process: tcpdump itself, so the parent owns exactly this pid.
sigprocmask(SIG_SETMASK, $signal_mask);
open(STDOUT, '>', $dumpfile) or _exit(1);
open(STDERR, '>', '/dev/null');
exec($tcpdump, '-i', $nic, 'port', '68', '-n', '-vvvvvv');
_exit(1);
}
$SIG{INT} = $SIG{TERM} = sub { kill_child(); exit 1; };
sigprocmask(SIG_SETMASK, $signal_mask);
my $dumpfile = $capture->file();
my $pid = $capture->pid();
probe_utils->send_msg("$output", "d", "The id of process which is used to capture the packet by tcpdump is $pid") if ($::VERBOSE);
my $start = Time::HiRes::gettimeofday();
@@ -185,7 +172,7 @@ while ($end - $start <= $duration) {
unless ($sock->send($package)) {
probe_utils->send_msg("$output", "d", "Send DHCP discover error: $!") if ($::VERBOSE);
probe_utils->send_msg("$output", "f", "Send out DHCP discover");
kill_child();
$capture->stop();
exit 1;
}
sleep 2;
@@ -194,7 +181,7 @@ while ($end - $start <= $duration) {
}
$msg = "Capture the packets by tcpdump";
my $capture_problem = kill_child();
my $capture_problem = $capture->stop();
if ($capture_problem) {
probe_utils->send_msg("$output", "d", "tcpdump $capture_problem") if ($::VERBOSE);
probe_utils->send_msg("$output", "f", $msg);
@@ -391,40 +378,3 @@ sub packdhcppkg {
return $package;
}
# Stop the capture and reap it, once: INT and TERM are blocked while the pid is taken and until
# tcpdump is reaped, so an interrupt in that window cannot orphan it or reach a recycled pid. The
# handlers stay installed, so a later signal still leaves through exit and the END cleanup.
# Returns '' when tcpdump ran until this TERM and stopped cleanly, otherwise what went wrong: it
# ended on its own, ignored TERM and was killed, or left on another signal or with a non-zero
# status.
sub kill_child {
sigprocmask(SIG_BLOCK, $stop_signals);
my $child = $pid;
$pid = undef;
unless ($child) {
sigprocmask(SIG_SETMASK, $signal_mask);
return '';
}
my $reaped = waitpid($child, WNOHANG);
my $early = ($reaped == $child) ? 1 : 0;
if ($reaped == 0) {
kill 'TERM', $child;
foreach (1 .. 50) {
last if ($reaped = waitpid($child, WNOHANG)) != 0;
select(undef, undef, undef, 0.1);
}
if ($reaped == 0) {
kill 'KILL', $child;
$reaped = waitpid($child, 0);
}
}
sigprocmask(SIG_SETMASK, $signal_mask);
return "could not be reaped" if $reaped != $child;
my ($signal, $status) = ($? & 127, $? >> 8);
probe_utils->send_msg("$output", "d", "Kill process $child used to capture the packet by 'tcpdump'") if ($::VERBOSE);
my $how = $signal ? "on signal $signal" : "with status $status";
return "ended before the capture window did, $how" if $early;
return '' if $signal == 15 || (!$signal && !$status);
return "left the capture $how";
}
+7 -62
View File
@@ -4,9 +4,8 @@ BEGIN {
}
use lib "$::XCATROOT/lib/perl";
use xCAT::CommandUtils;
use xCAT::PacketCapture;
use IO::Socket::INET;
use File::Temp qw(tempfile);
use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM);
use Time::HiRes qw(gettimeofday sleep);
use Getopt::Long;
Getopt::Long::Configure("bundling");
@@ -93,26 +92,9 @@ if (-f "/etc/redhat-release") {
exit 1;
}
# fork a process to capture the packet by tcpdump
my ($dumpfh, $dumpfile) = tempfile("detect_dhcpd.XXXXXX", TMPDIR => 1, UNLINK => 1);
close($dumpfh);
# INT and TERM stay blocked from the fork until the handler that stops the capture is in place.
my $stop_signals = POSIX::SigSet->new(SIGINT, SIGTERM);
my $signal_mask = POSIX::SigSet->new();
sigprocmask(SIG_BLOCK, $stop_signals, $signal_mask);
my $pid = fork;
if (!defined $pid) { sigprocmask(SIG_SETMASK, $signal_mask); print "Fork failed.\n"; exit 1; }
if ($pid == 0) {
# Child process: tcpdump itself, so the parent owns exactly this pid.
sigprocmask(SIG_SETMASK, $signal_mask);
open(STDOUT, '>', $dumpfile) or _exit(1);
open(STDERR, '>', '/dev/null');
exec($tcpdump, '-i', $nic, 'port', '68', '-n', '-vvvvvv');
_exit(1);
}
$SIG{INT} = $SIG{TERM} = sub { kill_child(); exit 1; };
sigprocmask(SIG_SETMASK, $signal_mask);
my $capture = xCAT::PacketCapture->start($tcpdump, $nic);
unless ($capture) { print "Fork failed.\n"; exit 1; }
my $dumpfile = $capture->file();
# generate the discover package
my $package = packdhcppkg($MAC);
@@ -140,7 +122,7 @@ unless ($sock) {
unless ($sock) {
print "Create socket error: $@\n";
kill_child();
$capture->stop();
exit 1;
}
@@ -154,7 +136,7 @@ $end =~ s/(\d.*)\.(\d.*)/$1/;
while ($end - $start <= $timeout) {
unless ($sock->send($package)) {
print "Send discover error: $!\n";
kill_child();
$capture->stop();
exit 1;
}
sleep 2;
@@ -163,8 +145,7 @@ while ($end - $start <= $timeout) {
}
#kill the child process
my $capture_problem = kill_child();
my $capture_problem = $capture->stop();
if ($capture_problem) {
print "tcpdump $capture_problem.\n";
exit 1;
@@ -353,39 +334,3 @@ sub packdhcppkg {
return $package;
}
# Stop the capture and reap it, once: INT and TERM are blocked while the pid is taken and until
# tcpdump is reaped, so an interrupt in that window cannot orphan it or reach a recycled pid. The
# handlers stay installed, so a later signal still leaves through exit and the END cleanup.
# Returns '' when tcpdump ran until this TERM and stopped cleanly, otherwise what went wrong: it
# ended on its own, ignored TERM and was killed, or left on another signal or with a non-zero
# status.
sub kill_child {
sigprocmask(SIG_BLOCK, $stop_signals);
my $child = $pid;
$pid = undef;
unless ($child) {
sigprocmask(SIG_SETMASK, $signal_mask);
return '';
}
my $reaped = waitpid($child, WNOHANG);
my $early = ($reaped == $child) ? 1 : 0;
if ($reaped == 0) {
kill 'TERM', $child;
foreach (1 .. 50) {
last if ($reaped = waitpid($child, WNOHANG)) != 0;
select(undef, undef, undef, 0.1);
}
if ($reaped == 0) {
kill 'KILL', $child;
$reaped = waitpid($child, 0);
}
}
sigprocmask(SIG_SETMASK, $signal_mask);
return "could not be reaped" if $reaped != $child;
my ($signal, $status) = ($? & 127, $? >> 8);
my $how = $signal ? "on signal $signal" : "with status $status";
return "ended before the capture window did, $how" if $early;
return '' if $signal == 15 || (!$signal && !$status);
return "left the capture $how";
}
@@ -5,7 +5,7 @@ cmd:mkdir -p /tmp/xcatprobe_l
cmd:xcatprobe -l
check:rc==0
check:output=~Supported sub commands are:
cmd:for module in CommandUtils.pm GlobalDef.pm NetworkUtils.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done
cmd:for module in CommandUtils.pm GlobalDef.pm NetworkUtils.pm PacketCapture.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done
check:rc==0
cmd:xcatprobe -l|grep -v "Supported sub commands are" |awk '/^[[:graph:]]/ {print $1}'|sort > /tmp/xcatprobe_l/subcmd_from_xcatprobe_l
cmd:ls -l /opt/xcat/probe/subcmds/ |awk '/^-/ {print $9}'|sort > /tmp/xcatprobe_l/subcmd_under_subcmds_dir
+116 -49
View File
@@ -8,8 +8,13 @@ use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
use Time::HiRes qw(sleep);
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../build-utils/lib";
use lib "$FindBin::Bin/../lib";
use XCAT::BuildUtils qw(stage_probe_helpers);
use XCAT::Test::File qw(repo_path);
use xCAT::CommandUtils;
# Both detect_dhcpd copies refused to run unless /usr/sbin/tcpdump existed. Debian and Ubuntu
@@ -19,9 +24,9 @@ use xCAT::CommandUtils;
# records how it was started. Outside the namespace below, a host that also carries
# /usr/sbin/tcpdump lets the previous guard pass as well.
my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
my $tools = "$repo/xCAT-server/share/xcat/tools/detect_dhcpd";
my $probe = "$repo/xCAT-probe/subcmds/detect_dhcpd";
my $repo = repo_path('.');
my $tools = repo_path('xCAT-server/share/xcat/tools/detect_dhcpd');
my $probe = repo_path('xCAT-probe/subcmds/detect_dhcpd');
plan skip_all => 'detect_dhcpd not found' unless -f $tools && -f $probe;
my $perl = $Config{perlpath};
@@ -30,17 +35,20 @@ my $mac = '02:00:5e:00:53:01';
# The capture run masks /tmp, so every fixture lives outside it.
my @fixture_dir = ( -d '/var/tmp' && -w '/var/tmp' ) ? ( DIR => '/var/tmp' ) : ();
# An XCATROOT whose lib/perl and probe/lib/perl are this checkout, so the scripts load the
# libraries they would load on a management node.
# The standalone probe loads only the helpers staged by the package builders.
my $root = tempdir( @fixture_dir, CLEANUP => 1 );
make_path("$root/lib/perl", "$root/probe/lib");
make_path("$root/lib/perl", "$root/probe/lib/perl");
symlink( "$repo/perl-xCAT/xCAT", "$root/lib/perl/xCAT" ) or die "symlink: $!";
symlink( "$repo/xCAT-probe/lib/perl", "$root/probe/lib/perl" ) or die "symlink: $!";
symlink( "$repo/xCAT-probe/lib/perl/probe_utils.pm", "$root/probe/lib/perl/probe_utils.pm" ) or die "symlink: $!";
stage_probe_helpers("$repo/perl-xCAT/xCAT", "$root/probe/lib/perl/xCAT");
local @ENV{qw(PERL5LIB PERLLIB PERL5OPT)};
delete @ENV{qw(PERL5LIB PERLLIB PERL5OPT)};
sub write_script {
my ( $path, $body ) = @_;
my ( $path, $body, $interpreter ) = @_;
$interpreter ||= '/bin/sh';
open( my $fh, '>', $path ) or die "$path: $!";
print {$fh} "#!/bin/sh\n$body";
print {$fh} "#!$interpreter\n$body";
close($fh);
chmod 0755, $path;
}
@@ -67,7 +75,7 @@ sub fixture_bin {
sub script_command {
my ( $script, @args ) = @_;
return "$perl -I '$repo/perl-xCAT' -I '$repo/xCAT-probe/lib/perl' '$script' @args";
return "$perl '$script' @args";
}
sub run_with_path {
@@ -159,16 +167,25 @@ sub lifecycle_bin {
write_script( "$bindir/tcpdump", qq{printf 'started\\n' >> '$record'\nkill -9 \$\$\n} );
} elsif ( $with{stubborn} ) {
write_script( "$bindir/tcpdump", qq{trap '' TERM\nprintf 'started\\n' >> '$record'\nwhile :; do sleep 1; done\n} );
} elsif ( $with{exec_failure} ) {
write_script( "$bindir/tcpdump", '', '/missing-capture-interpreter' );
} else {
my $term_status = $with{term_status} || 0;
my $release = $with{release}
? qq{while [ ! -f "$with{release}" ]; do sleep 0.05 & wait \$!; done; }
: '';
write_script( "$bindir/tcpdump",
qq{parent=\$(cat /proc/\$PPID/comm 2>/dev/null)\n}
. qq{out=\$(readlink /proc/\$\$/fd/1 2>/dev/null)\n}
. qq{printf 'pid=%s ppid=%s parent=%s out=%s\\n' "\$\$" "\$PPID" "\$parent" "\$out" >> '$record'\n}
. qq{trap 'printf "TERM\\n" >> "$record"; exit 0' TERM\n}
. qq{while :; do sleep 1; done\n} );
. qq{mode=\$(stat -c %a "\$out")\n}
. qq{trap 'printf "TERM\\n" >> "$record"; ${release}exit $term_status' TERM\n}
. qq{printf 'pid=%s ppid=%s parent=%s mode=%s out=%s\\n' "\$\$" "\$PPID" "\$parent" "\$mode" "\$out" >> '$record'\n}
. qq{printf 'tcpdump-private-stderr\\n' >&2\n}
. qq{printf '%s\\n' '12:00:00.000000 IP 192.0.2.1.67 > 255.255.255.255.68:' ' Client-Ethernet-Address $mac' ' Your-IP 192.0.2.44' ' Server-IP 192.0.2.2' ' DHCP-Message Option 53: Offer' '12:00:01.000000 IP 192.0.2.1.67 > 255.255.255.255.68:'\n}
. qq{while :; do sleep 1 & wait \$!; done\n} );
}
write_script( "$bindir/ps", qq{printf 'ps %s\\n' "\$*" >> '$record.ps'\nexit 0\n} );
foreach my $tool (qw(awk head grep cat readlink sleep)) {
foreach my $tool (qw(awk head grep cat readlink sleep stat)) {
my $real = xCAT::CommandUtils::find_executable($tool) or next;
symlink( $real, "$bindir/$tool" ) or die "symlink $tool: $!";
}
@@ -193,10 +210,10 @@ sub slurp_lines {
}
SKIP: {
skip 'the private /tmp would hide this checkout or its fixtures', 44
skip 'the private /tmp would hide this checkout or its fixtures', 128
if index( $repo, '/tmp/' ) == 0 || !@fixture_dir;
my $ns = isolation();
skip 'no private network namespace on this host', 44 unless $ns;
skip 'no private network namespace on this host', 128 unless $ns;
foreach my $case ( [ $tools, 'the tool', '-t' ], [ $probe, 'the probe', '-d' ] ) {
my ( $script, $label, $window ) = @$case;
@@ -204,13 +221,25 @@ SKIP: {
my $record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' );
my $bindir = lifecycle_bin( record => $record, real_ip => $ns->{ip} );
my ( $out, $status ) = run_isolated_status( $ns, $bindir, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
my ( $out, $status ) = run_isolated_status( $ns, $bindir, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' );
is( $status, 0, "$label exits 0 after a capture window" );
my @rec = slurp_lines($record);
my ($start) = grep { /^pid=/ } @rec;
ok( defined $start, "$label started tcpdump" ) or diag($out);
like( $start // '', qr/ parent=perl /, '... directly from the script, with no shell in between' );
like( $start // '', qr{ out=\Q$tmpdir\E/detect_dhcpd\.\w+$}, '... writing a private capture file under TMPDIR' );
like( $start // '', qr/ mode=600 /, '... with owner-only access' );
like( $out, qr/There are 1 servers repl/, '... and reads the captured offer after stopping tcpdump' );
like( $out, qr/Server:192\.0\.2\.1 assign IP \[192\.0\.2\.44\].*next server is \[192\.0\.2\.2\]/,
'... preserving the captured server and address details' );
unlike( $out, qr/tcpdump-private-stderr/, '... without exposing tcpdump stderr' );
my ($captured_pid) = ($start // '') =~ /^pid=(\d+)/;
if ($script eq $probe) {
like( $out, qr/Kill process \Q$captured_pid\E used to capture the packet by 'tcpdump'/,
'the probe retains its verbose stop message' );
} else {
unlike( $out, qr/Kill process/, 'the tool does not add the probe stop message' );
}
ok( ( grep { $_ eq 'TERM' } @rec ), '... and stopped it with TERM when the window ended' );
ok( !-e "$record.ps", '... without searching the process table' );
my @left = glob("$tmpdir/detect_dhcpd.*");
@@ -224,54 +253,92 @@ SKIP: {
@left = glob("$tmpdir/detect_dhcpd.*");
is( scalar(@left), 0, '... and leaves no capture file behind' );
my $unexecutable = lifecycle_bin( record => $record, real_ip => $ns->{ip}, exec_failure => 1 );
( $out, $status ) = run_isolated_status( $ns, $unexecutable, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' );
is( $status, 1, "$label exits 1 when exec cannot load tcpdump" );
like( $out, qr/tcpdump ended before the capture window did, with status 1/, '... and reports the failed child' );
is( scalar(glob_files($tmpdir)), 0, '... and removes the failed capture file' );
my $failed_stop = lifecycle_bin( record => $record, real_ip => $ns->{ip}, term_status => 7 );
( $out, $status ) = run_isolated_status( $ns, $failed_stop, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' );
is( $status, 1, "$label rejects a nonzero exit after TERM" );
like( $out, qr/tcpdump left the capture with status 7/, '... and retains the exit status' );
unlike( $out, qr/servers repl/, '... without reporting an incomplete capture as success' );
is( scalar(glob_files($tmpdir)), 0, '... and removes the failed capture file' );
my $quitting = lifecycle_bin( record => $record, real_ip => $ns->{ip}, quit => 1 );
( $out, $status ) = run_isolated_status( $ns, $quitting, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
( $out, $status ) = run_isolated_status( $ns, $quitting, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' );
is( $status, 1, "$label exits 1 when tcpdump quits early with status 0" ) or diag($out);
unlike( $out, qr/0 servers repl/, '... instead of reporting zero servers' );
like( $out, qr/tcpdump ended before the capture window did, with status 0/, '... and identifies the early successful exit' );
is( scalar(glob_files($tmpdir)), 0, '... and removes the early capture file' );
my $dying = lifecycle_bin( record => $record, real_ip => $ns->{ip}, killed => 1 );
( $out, $status ) = run_isolated_status( $ns, $dying, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
( $out, $status ) = run_isolated_status( $ns, $dying, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' );
is( $status, 1, "$label exits 1 when tcpdump dies on another signal" ) or diag($out);
like( $out, qr/on signal 9|Capture the packets by tcpdump/, '... and names the signal or the failed step' );
like( $out, qr/tcpdump ended before the capture window did, on signal 9/, '... and identifies the early signal death' );
my $stubborn = lifecycle_bin( record => $record, real_ip => $ns->{ip}, stubborn => 1 );
my $began = time;
( $out, $status ) = run_isolated_status( $ns, $stubborn, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
( $out, $status ) = run_isolated_status( $ns, $stubborn, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' );
is( $status, 1, "$label exits 1 when tcpdump ignores TERM" ) or diag($out);
cmp_ok( time - $began, '<', 20, '... after a bounded grace period' );
like( $out, qr/on signal 9|Capture the packets by tcpdump/, '... having killed it' );
like( $out, qr/tcpdump left the capture on signal 9/, '... having killed it after the capture window' );
# An interrupt while the capture runs: the script is signalled once the fake tcpdump has
# reported the script's pid, and must stop tcpdump and remove the file on its way out.
my $int_record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' );
my $int_bin = lifecycle_bin( record => $int_record, real_ip => $ns->{ip} );
my $runner = fork;
die "fork: $!" unless defined $runner;
if ( $runner == 0 ) {
my ( undef, $st ) = run_isolated_status( $ns, $int_bin, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '30' );
exit $st;
foreach my $signals ([qw(INT INT)], [qw(TERM TERM)], [qw(INT TERM)], [qw(TERM INT)]) {
my ($interrupt, $second) = @$signals;
my $int_record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' );
my $release = "$int_record.release";
my $int_bin = lifecycle_bin( record => $int_record, real_ip => $ns->{ip}, release => $release );
my $runner = fork;
die "fork: $!" unless defined $runner;
if ( $runner == 0 ) {
my ( undef, $st ) = run_isolated_status( $ns, $int_bin, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '30' );
exit $st;
}
my $started;
foreach ( 1 .. 150 ) {
($started) = grep { /^pid=/ } slurp_lines($int_record);
last if $started;
sleep 0.2;
}
my ($tcpdump_pid) = ( $started // '' ) =~ /^pid=(\d+)/;
my ($script_pid) = ( $started // '' ) =~ / ppid=(\d+)/;
ok( $script_pid, "$label reports the pid to interrupt" ) or diag( $started // 'tcpdump never started' );
my $stopping;
if ($script_pid) {
kill $interrupt, $script_pid;
foreach (1 .. 100) {
($stopping) = grep { $_ eq 'TERM' } slurp_lines($int_record);
last if $stopping;
sleep 0.02;
}
if ($stopping) {
kill $second, $script_pid;
sleep 0.2;
}
}
ok( $stopping, '... waits for tcpdump to acknowledge TERM before the second interrupt' );
ok( $script_pid && kill(0, $script_pid), '... keeps the command alive while tcpdump cannot exit' );
ok( $tcpdump_pid && kill(0, $tcpdump_pid), '... keeps tcpdump alive until its release' );
open( my $release_fh, '>', $release ) or die "$release: $!";
close($release_fh) or die "$release: $!";
waitpid( $runner, 0 );
is( $? >> 8, 1, "... and exits 1 on $interrupt followed by $second during the capture" );
ok( ( grep { $_ eq 'TERM' } slurp_lines($int_record) ), '... after stopping tcpdump' );
ok( !( $tcpdump_pid && kill( 0, $tcpdump_pid ) ), '... which is gone' );
@left = glob("$tmpdir/detect_dhcpd.*");
is( scalar(@left), 0, '... and the capture file is removed' );
}
my $started;
foreach ( 1 .. 150 ) {
($started) = grep { /^pid=/ } slurp_lines($int_record);
last if $started;
select( undef, undef, undef, 0.2 );
}
my ($tcpdump_pid) = ( $started // '' ) =~ /^pid=(\d+)/;
my ($script_pid) = ( $started // '' ) =~ / ppid=(\d+)/;
ok( $script_pid, "$label reports the pid to interrupt" ) or diag( $started // 'tcpdump never started' );
if ($script_pid) {
kill 'INT', $script_pid;
select( undef, undef, undef, 0.3 );
kill 'INT', $script_pid;
}
waitpid( $runner, 0 );
is( $? >> 8, 1, '... and exits 1 on a double interrupt during the capture' );
ok( ( grep { $_ eq 'TERM' } slurp_lines($int_record) ), '... after stopping tcpdump' );
ok( !( $tcpdump_pid && kill( 0, $tcpdump_pid ) ), '... which is gone' );
@left = glob("$tmpdir/detect_dhcpd.*");
is( scalar(@left), 0, '... and the capture file is removed' );
}
}
sub glob_files {
my ($directory) = @_;
my @files = glob("$directory/detect_dhcpd.*");
return @files;
}
done_testing();
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Config;
use File::Slurper qw(read_text write_text);
use File::Temp qw(tempdir);
use FindBin;
use POSIX qw(WNOHANG);
use Test::More;
use Time::HiRes qw(sleep);
use lib "$FindBin::Bin/../../perl-xCAT";
use xCAT::PacketCapture;
my $dir = tempdir(CLEANUP => 1);
my $executable = "$dir/tcpdump";
write_text($executable, "#!$Config{perlpath}\n" . <<'CAPTURE');
$SIG{TERM} = 'DEFAULT';
$| = 1;
print "capture is ready\n";
sleep 60;
CAPTURE
chmod 0755, $executable or die "chmod $executable: $!";
local $ENV{TMPDIR} = $dir;
my $capture = xCAT::PacketCapture->start($executable, 'unused');
ok($capture, 'starts a capture without root or a network namespace')
or BAIL_OUT('capture could not start');
my $pid = $capture->pid;
my $ready;
foreach (1 .. 3000) {
$ready = read_text($capture->file) eq "capture is ready\n";
last if $ready;
sleep 0.01;
}
ok($ready, 'the real child has executed before stop sends TERM');
is($capture->stop, '', 'an untrapped TERM is a successful stop');
is(waitpid($pid, WNOHANG), -1, 'stop has already reaped its child');
is(read_text($capture->file), "capture is ready\n", 'captured output remains readable after stop');
ok(!defined $capture->pid, 'the reaped child is no longer owned');
is($capture->stop, '', 'stopping again succeeds without another child');
done_testing();
@@ -19,10 +19,12 @@ my @helpers = qw(
CommandUtils.pm
GlobalDef.pm
NetworkUtils.pm
PacketCapture.pm
ServiceNodeUtils.pm
);
my @affected_subcommands = qw(
code_template
detect_dhcpd
discovery
osdeploy
xcatmn