# IO::Poll event loop bridge for POE::Kernel.  The theory is that this
# will be faster for large scale applications.  This file is
# contributed by Matt Sergeant (baud).

# Empty package to appease perl.
package POE::Loop::IO_Poll;

use vars qw($VERSION);
$VERSION = '1.368'; # NOTE - Should be #.### (three decimal places)

# Include common signal handling.
use POE::Loop::PerlSignals;

# Everything plugs into POE::Kernel;
package POE::Kernel;

=for poe_tests

sub skip_tests {
  return "IO::Poll is not 100% compatible with $^O" if (
    $^O eq "MSWin32" and not $ENV{POE_DANTIC}
  );
  return "IO::Poll tests require the IO::Poll module" if (
    do { eval "use IO::Poll"; $@ }
  );
}

=cut

use strict;

# Be sure we're using a contemporary version of IO::Poll.  There were
# issues with certain versions of IO::Poll prior to 0.05.  The latest
# version is 0.01, however.
use IO::Poll 0.01;

# Hand off to POE::Loop::Select if we're running under ActivePerl.
BEGIN {
  if ($^O eq "MSWin32" and not $ENV{POE_DANTIC}) {
    warn "IO::Poll is defective on $^O.  Falling back to IO::Select.\n";
    require POE::Loop::Select;
    POE::Loop::Select->import();
    die "not really dying";
  }
}

use Errno qw(EINPROGRESS EWOULDBLOCK EINTR);

use IO::Poll qw(
  POLLRDNORM POLLWRNORM POLLRDBAND POLLERR POLLHUP POLLNVAL
);

# Many systems' IO::Poll don't define POLLRDNORM.
# Usually upgrading IO::Poll helps.
BEGIN {
  my $x = eval { POLLRDNORM };
  if ($@ or not defined $x) {
    warn(
      "Your IO::Poll doesn't define POLLRDNORM.  Falling back to IO::Select.\n"
    );
    require POE::Loop::Select;
    POE::Loop::Select->import();
    die "not really dying";
  }
}

my %poll_fd_masks;

# Allow $^T to change without affecting our internals.
my $start_time = monotime();

#------------------------------------------------------------------------------
# Loop construction and destruction.

sub loop_initialize {
  my $self = shift;
  %poll_fd_masks = ();
}

sub loop_finalize {
  my $self = shift;
  $self->loop_ignore_all_signals();
}

#------------------------------------------------------------------------------
# Signal handler maintenance functions.

sub loop_attach_uidestroy {
  # does nothing
}

#------------------------------------------------------------------------------
# Maintain time watchers.  For this loop, we simply save the next
# event time in a scalar.  loop_do_timeslice() will use the saved
# value.  A "paused" time watcher is just a timeout for some future
# time.

my $_next_event_time = monotime();

sub loop_resume_time_watcher {
  $_next_event_time = $_[1];
}

sub loop_reset_time_watcher {
  $_next_event_time = $_[1];
}

sub loop_pause_time_watcher {
  $_next_event_time = monotime() + 3600;
}

# A static function; not some object method.

sub mode_to_poll {
  return POLLRDNORM if $_[0] == MODE_RD;
  return POLLWRNORM if $_[0] == MODE_WR;
  return POLLRDBAND if $_[0] == MODE_EX;
  croak "unknown I/O mode $_[0]";
}

#------------------------------------------------------------------------------
# Maintain filehandle watchers.

sub loop_watch_filehandle {
  my ($self, $handle, $mode) = @_;
  my $fileno = fileno($handle);

  my $type = mode_to_poll($mode);
  my $current = $poll_fd_masks{$fileno} || 0;
  my $new = $current | $type;

  if (TRACE_FILES) {
    POE::Kernel::_warn(
      sprintf(
        "<fh> Watch $fileno: " .
        "Current mask: 0x%02X - including 0x%02X = 0x%02X\n",
        $current, $type, $new
      )
    );
  }

  $poll_fd_masks{$fileno} = $new;
}

sub loop_ignore_filehandle {
  my ($self, $handle, $mode) = @_;
  my $fileno = fileno($handle);

  my $type = mode_to_poll($mode);
  my $current = $poll_fd_masks{$fileno} || 0;
  my $new = $current & ~$type;

  if (TRACE_FILES) {
    POE::Kernel::_warn(
      sprintf(
        "<fh> Ignore $fileno: " .
        ": Current mask: 0x%02X - removing 0x%02X = 0x%02X\n",
        $current, $type, $new
      )
    );
  }

  if ($new) {
    $poll_fd_masks{$fileno} = $new;
  }
  else {
    delete $poll_fd_masks{$fileno};
  }
}

sub loop_pause_filehandle {
  my ($self, $handle, $mode) = @_;
  my $fileno = fileno($handle);

  my $type = mode_to_poll($mode);
  my $current = $poll_fd_masks{$fileno} || 0;
  my $new = $current & ~$type;

  if (TRACE_FILES) {
    POE::Kernel::_warn(
      sprintf(
        "<fh> Pause $fileno: " .
        ": Current mask: 0x%02X - removing 0x%02X = 0x%02X\n",
        $current, $type, $new
      )
    );
  }

  if ($new) {
    $poll_fd_masks{$fileno} = $new;
  }
  else {
    delete $poll_fd_masks{$fileno};
  }
}

sub loop_resume_filehandle {
  my ($self, $handle, $mode) = @_;
  my $fileno = fileno($handle);

  my $type = mode_to_poll($mode);
  my $current = $poll_fd_masks{$fileno} || 0;
  my $new = $current | $type;

  if (TRACE_FILES) {
    POE::Kernel::_warn(
      sprintf(
        "<fh> Resume $fileno: " .
        "Current mask: 0x%02X - including 0x%02X = 0x%02X\n",
        $current, $type, $new
      )
    );
  }

  $poll_fd_masks{$fileno} = $new;
}

#------------------------------------------------------------------------------
# The event loop itself.

sub loop_do_timeslice {
  my $self = shift;

  # Check for a hung kernel.
  $self->_test_if_kernel_is_idle();

  # Set the poll timeout based on current queue conditions.  If there
  # are FIFO events, then the poll timeout is zero and move on.
  # Otherwise set the poll timeout until the next pending event, if
  # there are any.  If nothing is waiting, set the timeout for some
  # constant number of seconds.

  my $timeout = $_next_event_time;

  my $now = monotime();
  if (defined $timeout) {
    $timeout -= $now;
    $timeout = 0 if $timeout < 0;
  }
  else {
    die "shouldn't happen" if ASSERT_DATA;
    $timeout = 3600;
  }

  if (TRACE_EVENTS) {
    POE::Kernel::_warn(
      '<ev> Kernel::run() iterating.  ' .
      sprintf(
        "now(%.4f) timeout(%.4f) then(%.4f)\n",
        $now-$start_time, $timeout, ($now-$start_time)+$timeout
       )
    );
  }

  if (TRACE_FILES) {
    foreach (sort { $a<=>$b} keys %poll_fd_masks) {
      my @types;
      push @types, "plain-file"        if -f;
      push @types, "directory"         if -d;
      push @types, "symlink"           if -l;
      push @types, "pipe"              if -p;
      push @types, "socket"            if -S;
      push @types, "block-special"     if -b;
      push @types, "character-special" if -c;
      push @types, "tty"               if -t;
      my @modes;
      my $flags = $poll_fd_masks{$_};
      push @modes, 'r' if $flags & (POLLRDNORM | POLLHUP | POLLERR);
      push @modes, 'w' if $flags & (POLLWRNORM | POLLHUP | POLLERR);
      push @modes, 'x' if $flags & (POLLRDBAND | POLLHUP | POLLERR);
      POE::Kernel::_warn(
        "<fh> file descriptor $_ = modes(@modes) types(@types)\n"
      );
    }
  }

  # Avoid looking at filehandles if we don't need to.
  # TODO The added code to make this sleep is non-optimal.  There is a
  # way to do this in fewer tests.

  if (scalar keys %poll_fd_masks) {

    # There are filehandles to poll, so do so.

    # Check filehandles, or wait for a period of time to elapse.
    my $hits = IO::Poll::_poll($timeout * 1000, my @results = %poll_fd_masks);

    if (ASSERT_FILES) {
      if ($hits < 0) {
        POE::Kernel::_trap("<fh> poll returned $hits (error): $!")
          unless ( ($! == EINPROGRESS) or
                   ($! == EWOULDBLOCK) or
                   ($! == EINTR) or
                   ($! == 0)      # SIGNAL_PIPE strangeness
                 );
      }
    }

    if (TRACE_FILES) {
      if ($hits > 0) {
        POE::Kernel::_warn "<fh> poll hits = $hits\n";
      }
      elsif ($hits == 0) {
        POE::Kernel::_warn "<fh> poll timed out...\n";
      }
    }

    # If poll has seen filehandle activity, then gather up the
    # active filehandles and synchronously dispatch events to the
    # appropriate handlers.

    if ($hits > 0) {

      # This is where they're gathered.

      my (@rd_ready, @wr_ready, @ex_ready);
      my %poll_fd_results = @results;
      while (my ($fd, $got_mask) = each %poll_fd_results) {
        next unless $got_mask;

        my $watch_mask = $poll_fd_masks{$fd};
        if (
          $watch_mask & POLLRDNORM and
          $got_mask & (POLLRDNORM | POLLHUP | POLLERR | POLLNVAL)
        ) {
          if (TRACE_FILES) {
            POE::Kernel::_warn "<fh> enqueuing read for fileno $fd";
          }

          push @rd_ready, $fd;
        }

        if (
          $watch_mask & POLLWRNORM and
          $got_mask & (POLLWRNORM | POLLHUP | POLLERR | POLLNVAL)
        ) {
          if (TRACE_FILES) {
            POE::Kernel::_warn "<fh> enqueuing write for fileno $fd";
          }

          push @wr_ready, $fd;
        }

        if (
          $watch_mask & POLLRDBAND and
          $got_mask & (POLLRDBAND | POLLHUP | POLLERR | POLLNVAL)
        ) {
          if (TRACE_FILES) {
            POE::Kernel::_warn "<fh> enqueuing expedite for fileno $fd";
          }

          push @ex_ready, $fd;
        }
      }

      @rd_ready and $self->_data_handle_enqueue_ready(MODE_RD, @rd_ready);
      @wr_ready and $self->_data_handle_enqueue_ready(MODE_WR, @wr_ready);
      @ex_ready and $self->_data_handle_enqueue_ready(MODE_EX, @ex_ready);
    }
  }
  elsif ($timeout) {

    # No filehandles to poll on.  Try to sleep instead.  Use sleep()
    # itself on MSWin32.  Use a dummy four-argument select() everywhere
    # else.

    if ($^O eq 'MSWin32') {
      sleep($timeout);
    }
    else {
      CORE::select(undef, undef, undef, $timeout);
    }
  }

  # Dispatch whatever events are due.
  $self->_data_ev_dispatch_due();
}

### Run for as long as there are sessions to service.

sub loop_run {
  my $self = shift;
  while ($self->_data_ses_count()) {
    $self->loop_do_timeslice();
  }
}

sub loop_halt {
  # does nothing
}

1;

__END__

=head1 NAME

POE::Loop::IO_Poll - a bridge that allows POE to be driven by IO::Poll

=head1 SYNOPSIS

See L<POE::Loop>.

=head1 DESCRIPTION

POE::Loop::IO_Poll implements the interface documented in L<POE::Loop>.
Therefore it has no documentation of its own.  Please see L<POE::Loop>
for more details.

=head1 SEE ALSO

L<POE>, L<POE::Loop>, L<IO::Poll>, L<POE::Loop::PerlSignals>

=head1 AUTHORS & LICENSING

Please see L<POE> for more information about authors, contributors,
and POE's licensing.

=cut

# rocco // vim: ts=2 sw=2 expandtab
# TODO - Edit.
