# Select loop bridge for POE::Kernel.

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

use strict;

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

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

=for poe_tests

sub skip_tests { return }

=cut

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

use strict;
use Errno qw(EINPROGRESS EWOULDBLOCK EINTR);

# select() vectors.  They're stored in an array so that the MODE_*
# offsets can refer to them.  This saves some code at the expense of
# clock cycles.
#
# [ $select_read_bit_vector,    (MODE_RD)
#   $select_write_bit_vector,   (MODE_WR)
#   $select_expedite_bit_vector (MODE_EX)
# ];
my @loop_vectors = ("", "", "");

# A record of the file descriptors we are actively watching.
my %loop_filenos;

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

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

sub loop_initialize {
  my $self = shift;

  # Initialize the vectors as vectors.
  @loop_vectors = ( '', '', '' );
  vec($loop_vectors[MODE_RD], 0, 1) = 0;
  vec($loop_vectors[MODE_WR], 0, 1) = 0;
  vec($loop_vectors[MODE_EX], 0, 1) = 0;
}

sub loop_finalize {
  my $self = shift;

  # This is "clever" in that it relies on each symbol on the left to
  # be stringified by the => operator.
  my %kernel_modes = (
    MODE_RD => MODE_RD,
    MODE_WR => MODE_WR,
    MODE_EX => MODE_EX,
  );

  while (my ($mode_name, $mode_offset) = each(%kernel_modes)) {
    my $bits = unpack('b*', $loop_vectors[$mode_offset]);
    if (index($bits, '1') >= 0) {
      POE::Kernel::_warn "<rc> LOOP VECTOR LEAK: $mode_name = $bits\a\n";
    }
  }

  $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;
}

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

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

  vec($loop_vectors[$mode], $fileno, 1) = 1;
  $loop_filenos{$fileno} |= (1<<$mode);
}

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

  vec($loop_vectors[$mode], $fileno, 1) = 0;
  delete $loop_filenos{$fileno} unless (
    $loop_filenos{$fileno} and $loop_filenos{$fileno} &= ~(1<<$mode)
  );
}

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

  vec($loop_vectors[$mode], $fileno, 1) = 0;
  delete $loop_filenos{$fileno} unless (
    $loop_filenos{$fileno} and $loop_filenos{$fileno} &= ~(1<<$mode)
  );
}

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

  vec($loop_vectors[$mode], $fileno, 1) = 1;
  $loop_filenos{$fileno} |= (1<<$mode);
}

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

sub loop_do_timeslice {
  my $self = shift;

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

  # Set the select timeout based on current queue conditions.  If
  # there are FIFO events, then the timeout is zero to poll select and
  # move on.  Otherwise set the select 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;

    # Very large timeouts can trigger EINVAL on Mac OSX.
    $timeout = 3600 if $timeout > 3600;
  }
  else {
    die "shouldn't happen" if ASSERT_DATA;
    $timeout = 3600;
  }

  # Tracing is relatively expensive, but it's not for live systems.
  # We can get away with it being after the timeout calculation.
  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) {
    POE::Kernel::_warn(
      "<fh> ,----- SELECT BITS IN -----\n",
      "<fh> | READ    : ", unpack('b*', $loop_vectors[MODE_RD]), "\n",
      "<fh> | WRITE   : ", unpack('b*', $loop_vectors[MODE_WR]), "\n",
      "<fh> | EXPEDITE: ", unpack('b*', $loop_vectors[MODE_EX]), "\n",
      "<fh> `--------------------------\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 %loop_filenos) {
    # There are filehandles to poll, so do so.

    # Check filehandles, or wait for a period of time to elapse.
    my $hits = CORE::select(
      my $rout = $loop_vectors[MODE_RD],
      my $wout = $loop_vectors[MODE_WR],
      my $eout = $loop_vectors[MODE_EX],
      $timeout,
    );

    if (ASSERT_FILES) {
      if (
        $hits < 0 and
        $! != EINPROGRESS and
        $! != EWOULDBLOCK and
        $! != EINTR and
        $! != 0                   # this is caused by SIGNAL_PIPE
      ) {
        POE::Kernel::_trap("<fh> select error: $! (hits=$hits)");
      }
    }

    if (TRACE_FILES) {
      if ($hits > 0) {
        POE::Kernel::_warn "<fh> select hits = $hits\n";
      }
      elsif ($hits == 0) {
        POE::Kernel::_warn "<fh> select timed out...\n";
      }
      POE::Kernel::_warn(
        "<fh> ,----- SELECT BITS OUT -----\n",
        "<fh> | READ    : ", unpack('b*', $rout), "\n",
        "<fh> | WRITE   : ", unpack('b*', $wout), "\n",
        "<fh> | EXPEDITE: ", unpack('b*', $eout), "\n",
        "<fh> `---------------------------\n"
      );
    }

    # If select 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.  It's a variant on a neat
      # hack Silmaril came up with.

      my (@rd_selects, @wr_selects, @ex_selects);
      foreach (keys %loop_filenos) {
        push(@rd_selects, $_) if vec($rout, $_, 1);
        push(@wr_selects, $_) if vec($wout, $_, 1);
        push(@ex_selects, $_) if vec($eout, $_, 1);
      }

      if (TRACE_FILES) {
        if (@rd_selects) {
          POE::Kernel::_warn(
            "<fh> found pending rd selects: ",
            join( ', ', sort { $a <=> $b } @rd_selects ),
            "\n"
          );
        }
        if (@wr_selects) {
          POE::Kernel::_warn(
            "<sl> found pending wr selects: ",
            join( ', ', sort { $a <=> $b } @wr_selects ),
            "\n"
          );
        }
        if (@ex_selects) {
          POE::Kernel::_warn(
            "<sl> found pending ex selects: ",
            join( ', ', sort { $a <=> $b } @ex_selects ),
            "\n"
          );
        }
      }

      if (ASSERT_FILES) {
        unless (@rd_selects or @wr_selects or @ex_selects) {
          POE::Kernel::_trap(
            "<fh> found no selects, with $hits hits from select???\n"
          );
        }
      }

      # Enqueue the gathered selects, and flag them as temporarily
      # paused.  They'll resume after dispatch.

      @rd_selects and $self->_data_handle_enqueue_ready(MODE_RD, @rd_selects);
      @wr_selects and $self->_data_handle_enqueue_ready(MODE_WR, @wr_selects);
      @ex_selects and $self->_data_handle_enqueue_ready(MODE_EX, @ex_selects);
    }
  }
  elsif ($timeout) {
    # No filehandles to select on.  Four-argument select() fails on
    # MSWin32 with all undef bitmasks.  Use sleep() there instead.

    # Not unconditionally the Time::HiRes microsleep because
    # Time::HiRes may not be installed.  This is only an issue until
    # we can require versions of Perl that include Time::HiRes.
    if ($^O eq 'MSWin32') {
      sleep($timeout);
    }
    else {
      CORE::select(undef, undef, undef, $timeout);
    }
  }

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

sub loop_run {
  my $self = shift;

  # Run for as long as there are sessions to service.
  while ($self->_data_ses_count()) {
    $self->loop_do_timeslice();
  }
}

sub loop_halt {
  # does nothing
}

1;

__END__

=head1 NAME

POE::Loop::Select - a bridge that allows POE to be driven by select(2)

=head1 SYNOPSIS

See L<POE::Loop>.

=head1 DESCRIPTION

POE::Loop::Select 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<select>, 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.
