The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w
# vim: ts=2 sw=2 expandtab

# Tests various signals using POE's stock signal handlers.  These are
# plain Perl signals, so mileage may vary.

use strict;
use lib qw(./mylib ../mylib);

use Test::More;

sub POE::Kernel::ASSERT_DEFAULT () { 1 }

BEGIN {
  package
  POE::Kernel;
  use constant TRACE_DEFAULT => exists($INC{'Devel/Cover.pm'});
}

# This is the number of processes to fork.  Increase this number if
# your system can handle the resource use.  Also try increasing it if
# you suspect a problem with POE's SIGCHLD handling.  Be warned
# though: setting this too high can cause timing problems and test
# failures on some systems.

use constant FORK_COUNT => 8;

BEGIN {
  # We can't "plan skip_all" because that calls exit().  And Tk will
  # croak if you call BEGIN { exit() }.  And that croak will cause
  # this test to FAIL instead of skip.

  my $error;
  if ($^O eq "MSWin32" and not $ENV{POE_DANTIC}) {
    $error = "$^O does not support signals";
  }
  elsif ($^O eq "MacOS" and not $ENV{POE_DANTIC}) {
    $error = "$^O does not support fork";
  }

  if ($error) {
    print "1..0 # Skip $error\n";
    CORE::exit();
  }

  plan tests => FORK_COUNT+ 7;
}

use IO::Pipely qw(pipely);
my ($pipe_read, $pipe_write) = pipely();

BEGIN { use_ok("POE") }

# Set up a second session that watches for child signals.  This is to
# test whether a session with only sig_child() stays alive because of
# the signals.

POE::Session->create(
  inline_states => {
    _start => sub { $_[KERNEL]->alias_set("catcher") },
    catch  => sub {
      my ($kernel, $heap, $pid) = @_[KERNEL, HEAP, ARG0];
			$kernel->sig(CHLD => "got_sigchld");
      $kernel->sig_child($pid, "got_chld");
      $heap->{children}{$pid} = 1;
      $heap->{watched}++;
    },
    remove_alias => sub { $_[KERNEL]->alias_remove("catcher") },
    got_chld => sub {
      my ($heap, $pid) = @_[HEAP, ARG1];
      ok(delete($heap->{children}{$pid}), "caught SIGCHLD for watched pid $pid");
      $heap->{caught}++;
    },
		got_sigchld => sub {
			$_[HEAP]->{caught_sigchld}++;
		},
    _stop => sub {
      my $heap = $_[HEAP];

      ok(
        $heap->{watched} == $heap->{caught},
        "expected $heap->{watched} reaped children, got $heap->{caught}"
      );

			ok(
				$heap->{watched} == $heap->{caught_sigchld},
        "expected $heap->{watched} sig(CHLD), got $heap->{caught_sigchld}"
			);

      ok(!keys(%{$heap->{children}}), "all reaped children were watched");
    },
  },
);

# Set up a signal catching session.  This test uses plain fork(2) and
# POE's $SIG{CHLD} handler.

POE::Session->create(
  inline_states => {
    _start => sub {
      my ($kernel, $heap) = @_[KERNEL, HEAP];

      # Clear the status counters, and catch SIGCHLD.

      $heap->{forked} = $heap->{reaped} = 0;

      # Fork some child processes, all to exit at the same time.

      my $fork_start_time = time();

      for (my $child = 0; $child < FORK_COUNT; $child++) {
        my $child_pid = fork;

        if (defined $child_pid) {
          if ($child_pid) {
            # Parent side keeps track of child IDs.
            $heap->{forked}++;
            $heap->{children}{$child_pid} = 1;
            $kernel->sig_child($child_pid, "catch_sigchld");
            $kernel->post(catcher => catch => $child_pid);
          }
          else {
            # A brief sleep so the parent has more opportunity to
            # finish forking.
            sleep 1;

            # Defensively make sure SIGINT will be fatal.
            $SIG{INT} = 'DEFAULT';

            # Tell the parent we're ready.
            print $pipe_write "$$\n";

            # Wait for SIGINT.
            sleep 3600;
            exit;
          }
        }
        else {
          die "fork error: $!";
        }
      }

      ok(
        $heap->{forked} == FORK_COUNT,
        "forked $heap->{forked} processes (out of " . FORK_COUNT . ")"
      );

      # NOTE: This is bad form.  We're going to block here until all
      # children check in, or die trying.

      my $ready_count = 0;
      while (<$pipe_read>) {
        last if ++$ready_count >= FORK_COUNT;
      }

      $kernel->yield( 'forking_time_is_up' );
    },

    _stop => sub {
      my $heap = $_[HEAP];

      # Everything is done.  See whether it succeeded.
      ok(
        $heap->{reaped} == $heap->{forked},
        "reaped $heap->{reaped} processes (out of $heap->{forked})"
      );
    },

    catch_sigchld => sub {
      my ($kernel, $heap) = @_[KERNEL, HEAP];

      # Count the child reap.  If that's all of them, wait just a
      # little longer to make sure there aren't extra ones.
      if (++$heap->{reaped} >= FORK_COUNT) {
        $kernel->delay( reaping_time_is_up => 0.500 );
      }
    },

    forking_time_is_up => sub {
      my ($kernel, $heap) = @_[KERNEL, HEAP];

      # Forking time is over.  We kill all the child processes as
      # immediately as possible.

      my $kill_count = kill INT => keys(%{$heap->{children}});
      ok(
        $kill_count == $heap->{forked},
        "killed $kill_count processes (out of $heap->{forked})"
      );

      # Start the reap timer.  This will tell us how long to wait
      # between CHLD signals.

      $heap->{reap_start} = time();

      # Cap the maximum time for failures.

      $kernel->delay( reaping_time_is_up => 10 );
    },

    # Do nothing here.  The timer exists just to keep the session
    # alive.  Once it's dispatched, the session can exit.
    reaping_time_is_up => sub { undef },
  },
);

# Run the tests.

POE::Kernel->run();

1;