App-karr
view release on metacpan or search on metacpan
t/148-foundation-runner-group-kill.t view on Meta::CPAN
# init and lived for 30s after the runner returned.
#
# The duration carries a marker unique to this test process ($$) so
# the post-mortem scan below can identify *our* sleep by exact argv
# instead of pattern-matching every process on the box. A bare
# "sleep 30" substring match also catches unrelated survivors like
# "sleep 300" from a concurrent session (#179), and would cross-match
# between two copies of this very test running at once on a shared
# machine -- the marker rules both out. The fractional suffix is a
# no-op for `sleep` (it just sleeps 30-point-something seconds
# instead of exactly 30), so the timeout/kill behaviour under test is
# unaffected.
my $marker = "30.$$";
my $cmd = "sleep $marker & exec sleep $marker";
# We need the agent's PID to look at its /proc/<pid>/stat before
# exec. Reading /proc/<pid>/stat AFTER exec still gives us the same
# pid's stat -- /proc is the process, not the executable. But the
# group identity is set right after fork by the runner, so we have
# to verify it once the run starts, before the timeout fires.
my $agent_pid_seen;
my $watcher = sub {
# Quick scan of /proc for the most recent sleep 30 we did not
# start. The runner's group is identifiable because the test
# process is the only other thing in the box doing sleep 30 in
# a tempdir, and any such process we see is the agent or its
# backgrounded child.
opendir my $d, '/proc' or return;
while ( my $entry = readdir($d) ) {
next unless $entry =~ /^\d+$/;
my $cmdline = safe_slurp("/proc/$entry/cmdline") // next;
$cmdline =~ s/\0/ /g;
if ( $cmdline =~ /sleep 30/ ) {
$agent_pid_seen //= $entry;
}
}
closedir $d;
};
# Spin a watcher for up to 6s while the runner is in max_runtime.
my $watcher_pid = fork // die "fork watcher: $!";
if ( $watcher_pid == 0 ) {
# child watcher: poll /proc every 100ms for 6s, then exit.
for my $i ( 1 .. 60 ) {
$watcher->();
select undef, undef, undef, 0.1;
}
exit 0;
}
my $f = App::karr::Foundation->new( _config_data => {} );
my $start = time;
my ( $code, $out ) = $f->_run_command(
$repo, { max_runtime => 1 }, $cmd,
);
my $elapsed = time - $start;
reap_wait($watcher_pid);
# The runner should report SIGTERM death and have killed the agent
# promptly -- not waited 30s for the foreground sleep to finish.
is $code, 143, 'timed-out run reports 128+SIGTERM';
ok $elapsed < 10, "killed promptly (took ${elapsed}s)"
or diag "the runner waited the full sleep -- group kill is broken";
# The bug-shaped check: nothing under /tmp/* should still be running
# a `sleep 30`. The watcher polled /proc while the run was live and
# afterwards; if any of those sleeps survived the runner, it is
# either our reaped watcher (impossible -- it does not run sleep 30)
# or an orphan that the group kill missed.
my @still_running;
opendir my $d, '/proc' or die "opendir /proc: $!";
while ( my $entry = readdir($d) ) {
next unless $entry =~ /^\d+$/;
next if $entry == $$; # the test itself
my $cmdline = safe_slurp("/proc/$entry/cmdline") // next;
$cmdline =~ s/\0/ /g;
$cmdline =~ s/\s+$//; # trailing NUL -> trailing space
# The watcher has exited; filter its defunct shell (a `sh -c`
# looping select) by parent. /proc/<pid>/status has PPid.
my $status = safe_slurp("/proc/$entry/status") // next;
my ($ppid) = $status =~ /^PPid:\s*(\d+)/m;
next if defined $ppid && $ppid == $test_pid;
# Exact argv match on our own marked duration, not a substring:
# "sleep 30" would also catch an unrelated "sleep 300" elsewhere
# on the box, or the sibling sleep of another copy of this test.
if ( $cmdline eq "sleep $marker" ) {
push @still_running, [ $entry, $cmdline, $ppid ];
}
}
closedir $d;
ok !@still_running, "no orphan sleep $marker left on the box"
or diag "survivors: @{[ map { join('/',@$_) } @still_running ]}";
};
# Group identity: the agent's pgid must equal its own pid, because the
# runner relies on `kill 'TERM', -$pgid` to signal the whole group. We
# spawn the runner, peek at /proc/<pid>/stat while it is alive, and
# confirm pgid == pid.
subtest 'runner puts the agent in a process group whose pgid is its own pid' => sub {
my $repo = path( tempdir( CLEANUP => 1 ) );
my $cmd = 'sleep 5';
# Fork a child that drives the runner; the parent watches /proc.
my $driver_pid = fork // die "fork driver: $!";
if ( $driver_pid == 0 ) {
# child driver
my $f = App::karr::Foundation->new( _config_data => {} );
$f->_run_command( $repo, { max_runtime => 8 }, $cmd );
exit 0;
}
# Poll for the agent up to 5s.
my $agent_pid;
my $end = time + 5;
while ( time < $end && !$agent_pid ) {
opendir my $d, '/proc' or die;
while ( my $entry = readdir($d) ) {
next unless $entry =~ /^\d+$/;
next if $entry == $$ || $entry == $driver_pid;
my $cmdline = safe_slurp("/proc/$entry/cmdline") // next;
$cmdline =~ s/\0/ /g;
( run in 0.469 second using v1.01-cache-2.11-cpan-aadc1410aed )