eval 'exec perl -x $0 ${1+"$@"}' # -*-perl-*-
  if 0;
#!perl -w
#
# ======================================================================
# This file is Copyright 1998,1999 by the Purdue Research Foundation and
# may only be used under license.  For terms of the license, see the
# file named COPYRIGHT included with this software release.
# AAFID is a trademark of the Purdue Research Foundation.
# All rights reserved.
# ======================================================================
#
# Comm::Reactor package
#
# AAFID project, COAST Laboratory, CERIAS, 1998-1999.
#
# Frederic Dumont, 1998-1999
# Diego Zamboni, 1999
#
# $Id: Reactor.pm,v 1.6 1999/09/03 17:08:57 zamboni Exp $
#
# NOTE: This file is in Perl's POD format. For more information, see the 
#       manual page for perlpod(1).
#

=head1 NAME

Reactor - Poll a set of file handles plus a event queue

=head1 SYNOPSIS

=head2 Server code

     use Comm::Reactor;
     use IO::Handle;
     use IO::Socket;
     use Comm::Conn;

     my $fha=new IO::Socket::UNIX(Local=>"toto", Listen=>1);
     Comm::Conn::init;

     sub idle_func {
       print "Idle function ".time()."\n";
       Comm::Reactor::add_event(time()+5,\&idle_func);
     }

     sub login {
       my ($fh) = @_;
       my $nfh=$fh->accept();
       Comm::Reactor::add_handle($nfh,\&cb);
     }

     sub cb {
       my ($fh, $msg) = @_;
       if(!defined($msg)) {
	 print "Connection closed. Oh yeah\n";
	 Comm::Reactor::remove_handle($fh);
	 return;
       }
       if($msg eq "quit") {
	 print "Received request to quit\n";
	 unlink ("toto");
	 exit(0);
       }
       print "Msg : $msg\n";
     }

     Comm::Reactor::add_acceptor($fha,\&login);
     Comm::Reactor::add_event(time()+5,\&idle_func);

     print "Server ready.\n";
     Comm::Reactor::loop();
          
=head2 Client Code

     use Comm::Reactor;
     use IO::Socket;

     $fh=new IO::Socket::UNIX(Peer=>"toto");

     Comm::Reactor::add_acceptor(IO::Handle->new_from_fd(\*STDIN, "r"),
				 sub {
				   my $in=shift;
				   $msg=<$in>;
				   chop $msg;
				   if ($msg eq "") {
				     Comm::Reactor::flush();
				     exit;
				   } 
				   Comm::Reactor::send_message($fh, $msg);
				   if ($msg eq "quit") {
				     Comm::Reactor::flush();
				     exit;
				   }
				 }
				);

     print "Type message. Press RETURN on an empty line to exit.\n";
     Comm::Reactor::loop();

=head1 Interface

It has been assumed that an application would need only one Reactor,
therefore all methods are class methods.

=cut

package Comm::Reactor;

use strict;
use IO::Select;
use vars qw(%acceptor $timer %current %fh_callbacks %fh_queues %fh_files $readh $writeh $errorh $stdout_handle $stdin_handle %prev_pos);
use Comm::Timer;
use Data::Dumper;
use File::stat;
use IO::File;

# Private variables
my $timer=new Comm::Timer;      # the event queue
my %fh_callbacks;         # maps the file handles to the callback functions
my %fh_queues;            # maps the file handles to the send queue
my %current;		  # maps the file handles to the current read message
my %fh_files;             # contains the registered file handles.
my %prev_pos;            # size last seen for a file.
my $readh=new IO::Select;
my $writeh=new IO::Select;
my $errorh=new IO::Select;

sub _Log {
#  open LOG, ">>/homes/zamboni/systmp/aafid.log" or die "Blech\n";
#  print LOG "######## (PID $$) @_";
#  close LOG;
}

# Initialization
#      BEGIN {
#        my %c=AAFID::Config::configure;
#        Log_register("debug", $c{logfile});
#        foreach (@{$c{logcategories}}) {
# 	 Log_activate($_);
#        }
#      }

=over 4

=item add_handle ( HANDLE, FUNC )

Add the given handle to the reactor with the given function as a
callback. When a message comes through the handle, the callback will
be invoked with the file handle and the message as arguments. Returns
the previous callback for that handle, if any.

=cut

sub add_handle {
  my ($fh, $cb_func)= @_;
  my ($pack, $file, $line)=caller;
  _Log("add_handle: called from $pack ($file:$line) with fh $fh\n");
  $readh->add($fh);
  $errorh->add($fh);
  my $prevcb=$fh_callbacks{$fh};
  $fh_callbacks{$fh}=$cb_func;
  return $prevcb;
}

=item add_acceptor ( HANDLE, FUNC )

Add the given handle as an acceptor to the reactor with the given
function as a callback. An acceptor bypass the message mecanism of the
Reactor and thus has to manage the messages by itself. The acceptor
callback will be called with only the file handle as an argument, and
has to read from it by itself.  C<Add_acceptor> returns the previous
acceptor for the handle, if any.

=cut

sub add_acceptor {
  my ($fh, $cb_func)=@_;
  my ($pack, $file, $line)=caller;
  _Log("add_acceptor: called from $pack ($file:$line) with fh $fh\n");
  $readh->add($fh);
  $errorh->add($fh);
  my $prevcb=$acceptor{$fh};
  $acceptor{$fh}=$cb_func;
  return $prevcb;
}

=item add_file ( FILENAME, FUNC_READ [, FUNC_TRUNC [, FUNC_RM [, FUNC_FAIL]]] )

Opens the file for reading, and adds callbacks for it. Files have to
be handled differently because when an "end of file" is detected, the
callback does not need to be called with C<undef> as it is usually
done, because we still want to wait for new data to appear at the end
of the file. A file handle callback is never called with C<undef>.

Files are also different in that they are read line by line. The callback
will be called with the file handle and the next line from the file,
including the newline at the end. The Reactor message format is not used.

If the first character of the filename is "+", the file pointer is moved
to the end of the file upon opening. Otherwise, reading starts at the
beginning of the file.

The callback FUNC_READ is called whenever there is new data on the
file. 

If while the file is being monitored it is truncated (its size
reduces), then the second callback is called with the file handle and
file name as arguments. If FUNC_TRUNC returns a true value, the
pointer is moved to the new end of file and operation continues. If
FUNC_TRUNC returns a false value, the callback for the file is
removed. If FUNC_TRUNC is not given or is C<undef>, then if the file
is truncated the pointer is moved to the new end of file and operation
continues unharmed.

If while the file is being monitored it is removed, then the third
callback FUNC_RM is called with the file name as argument. If FUNC_RM
returns a true value, the file is reopened and monitoring
continues. If FUNC_RM returns a false value, the callback for the file
is removed. If FUNC_RM is not given or is C<undef>, then the file is
reopened and monitoring continues.

If an error occurs at any time (while opening or reopening a file, for
example) then the FUNC_FAIL is called with the file name and the error
message as arumgnet. If FUNC_FAIL is not given, then the callbacks are
silently removed.

File handle callbacks are removed using the C<remove_file> method.

C<add_file> returns the previous callback for the given handle, if any.

=cut

sub add_file {
  my ($fname, $cb_read, $cb_trunc, $cb_rm, $cb_fail)= @_;
  # Since there seems to be no way of blocking on a file, we
  # implement this as a timed event that repeats every second
  # and calls a routine that checks the file.
  my $fh=_open_file($fname);
  # Remove the initial "+" if present
  $fname=~s/^\+//;
  if (!$fh) {
    if ($cb_fail) {
      &{$cb_fail}($fname, $!);
    }
    remove_file($fname);
    return -1;
  }
  my $prevcb=$fh_callbacks{$fname};
  $fh_callbacks{$fname}=$cb_read;
  # A single timed event is added for all the files that we
  # want to monitor, so as not to have a lot of them.
  # If %fh_files contains something, then we must have already
  # added the repeating event, so we do not add it again.
  if (!%fh_files) {
    add_repeating_event(1, \&_check_files);
  }
  $fh_files{$fname}=[$fh, $fname, $cb_read, $cb_trunc, $cb_rm];
  $prev_pos{$fname}=$fh->tell;
#  print "In init: prev_pos{$fname}=$prev_pos{$fname}\n";
  return $prevcb;
}

=item destroy_handle ( HANDLE )

Remove the given handle from the Reactor. This includes the queue of messages
to be sent. That means that once a handle is destroyed, any queued messages
are simply discarded.

This function must B<not> be used for removing file handles that have
been added using C<add_file>. See C<remove_file> instead.

=cut

sub destroy_handle {
  my ($fh)=@_;
  my ($pack, $file, $line)=caller;
  _Log("destroy_handle: called from $pack ($file:$line) with fh $fh\n");
  $readh->remove($fh);
  $errorh->remove($fh);
  $writeh->remove($fh);
  delete $fh_callbacks{$fh} if exists $fh_callbacks{$fh};
  delete $fh_queues{$fh} if exists $fh_queues{$fh};
  delete $current{$fh} if exists $current{$fh}; 
  delete $acceptor{$fh} if exists $acceptor{$fh};
}

=item remove_handle ( HANDLE )

Remove the given handle from the Reactor, but will empty the message queue
first. Once the handle is removed from the Reactor, it can't be listened on.

This subroutine is also used to remove file handles that have been added
using C<add_file>.

=cut
# '

sub remove_handle {
  my ($fh)=@_;
  my ($pack, $file, $line)=caller;
  _Log("remove_handle: called from $pack ($file:$line) with fh $fh\n");
  $readh->remove($fh);
  $errorh->remove($fh);
  delete $fh_callbacks{$fh} if exists $fh_callbacks{$fh};
  delete $current{$fh} if exists $current{$fh}; 
  delete $acceptor{$fh} if exists $acceptor{$fh};
}

=item remove_file ( FNAME )

Remove the callbacks for a file added with C<add_file>.

=cut

sub remove_file {
  my $fname=shift;
  # Remove the initial "+" if present.
  $fname=~s/^\+//;
  my $fdata=$fh_files{$fname};
  if ($fdata) {
    delete $fh_callbacks{$fname} if exists $fh_callbacks{$fname};
    my $fh=$fdata->[0];
    $fh->close;
    delete $fh_files{$fname};
    if (!%fh_files) {
      # If this was the last registered file handle, remove the timed
      # repeating event.
      remove_repeating_event(1, \&_check_files);
    }
  }
}

=item add_event ( TIME, FUNC )

See add_event in B<Comm::Timer>.

=cut 

sub add_event {
	$timer->add_event(@_);
}

=item add_repeating_event ( INTERVAL, FUNC )

See add_repeating_event in B<Comm::Timer>.

=cut

sub add_repeating_event {
  $timer->add_repeating_event(@_);
}

=item remove_event ( TIME, FUNC )

See remove_event in B<Comm::Timer>.

=cut 

sub remove_event {
	$timer->remove_event(@_);
}

=item remove_repeating_event ( INTERVAL, FUNC )

See remove_repeating_event in B<Comm::Timer>.

=cut

sub remove_repeating_event {
  $timer->remove_repeating_event(@_);
}

=item send_message ( HANDLE, MSG )

Put the given message (as a string) in the queue for the given handle and
return. The message will be sent whenever it is possible. Blocking is not
supported.

=cut

sub send_message {
	my ($fh, $mesg)=@_;
	$mesg=pack('N',length($mesg)).$mesg;
	push @{$fh_queues{$fh}}, $mesg;
	$writeh->add($fh);
}

=item loop

The main loop. It has to be called so that I/O and time operations can occur.
This routine never returns. All other actions have to be triggered either
through I/O events, timer events, or signals.

Notice that if there are no pending timed events, and if no signals occur,
loop will block until something happens in one of the handles that have
been registered. Be careful.

=cut

sub loop {
  while (1) {
    _loop();
  }
}

=item flush

Calls a single iteration of the event loop with a zero timeout to
catch anything that may be pending.

=cut

sub flush {
  _loop(0);
}

=item stdout

Returns a handle whose output goes to STDOUT. This should be used as
the argument to C<send_message> if you want to send a message to
STDOUT instead of creating your own handle to ensure proper ordering of
the messages.

=cut

sub stdout {
  $stdout_handle=IO::Handle->new_from_fd(fileno(STDOUT), "w")
    unless defined($stdout_handle);
  return $stdout_handle;
}

=item stdin

Returns a handle associated to STDIN. This should be used as the first
argument to C<add_handle> to add a handler for messages coming from
STDIN instead of creating your own handle to ensure proper ordering of
the messages.

=cut

sub stdin {
  $stdin_handle=IO::Handle->new_from_fd(fileno(STDIN), "r")
    unless defined($stdin_handle);
  return $stdin_handle;
}

=item reset

Returns the class to its just-loaded condition. Removes all handles,
acceptors, timed events, etc.

=cut

sub reset {
  $timer=new Comm::Timer;
  undef %fh_callbacks;
  undef %fh_queues;
  undef %current;
  undef %fh_files;
  undef %acceptor;
  undef %fh_files;
  undef %prev_pos;
  undef $stdout_handle;
  undef $stdin_handle;
  $readh=new IO::Select;
  $writeh=new IO::Select;
  $errorh=new IO::Select;
#  _Log("Comm::Reactor has been reset\n");
}

# Private methods

# _loop does the actual work of polling the registered handles and
# waiting for an event to happen. If no timed event is registered,
# it blocks forever on the registered handles, until one of them 
# has activity or a signal is received.
# It receives an optional argument to specify a timeout. If given, this
# timeout overrides any other timeout values (such as the time remaining
# until the next timer event), so it should normally not be used. It
# is useful when used from the flush() routine, which specifically
# requires an immediate return.
sub _loop {
	my $next_event;
	my $timeout=shift;
	my $fh;
	my ($rset, $wset, $eset);
	$next_event=$timer->get_when();
	if (!defined($timeout)) {
	  # The timeout is assigned only if none was given as argument.
	  if(defined $next_event) {
	    $timeout=$next_event-time();
	  } else {
	    # This was originally 0, changed to undef to make it block.
	    $timeout=undef;
	  }
	}
#	print "About to select..." . time ."\n";
	_Log("Read handles registered: ".join(" ", $readh->handles)."; callbacks=".join(" ",keys(%fh_callbacks))."; acceptors=".join(" ",keys(%acceptor))."\n");
	($rset,$wset,$eset)=IO::Select::select($readh,$writeh,$errorh,$timeout);
	foreach $fh (@$rset) {
		_read($fh);
	}
	foreach $fh (@$wset) {
	  _write($fh);
	}
	# TODO: Fix this. Why does $eset always get set on Linux?
	# The following three lines had to be commented out on Linux.
	foreach $fh (@$eset) {
		_error($fh);
	}
	if ($next_event) {
		if ($next_event-time()<=0) {
			my @tmp=@{$timer->get_next()};
			shift @tmp;
			foreach (@tmp) {
				&$_;
			}
		}
	}
}

# _read is called when a file handle is readable. When the message has been
# read, the callback is called. If an error occurs, it will simply call
# _errors.
# As the reading could be done in several steps, _read stores its state in the
# file handler.
# If anything can be read in a call, but that the connection is closed while
# we're reading, it will be detected at next call.
# The error handling is at best weak. It has almost no recovery of lost or
# partial messages at all.
sub _read {
	my ($fh)=@_;
	if (exists $acceptor{$fh}) {
	  &{$acceptor{$fh}}($fh);
	  return;
	}
	my $bytes_read;
	my $msg_len;
	my $mesg;
	my $read_stuff;			# Did we read anything in this call
	if(exists $current{$fh}) {
		$msg_len=$current{$fh}->{"msg_len"};
		$mesg=$current{$fh}->{"mesg"};
	} else {
		my $buff;
		$bytes_read=sysread($fh, $buff, 4, 0);
		if(defined $bytes_read) {
		  if($bytes_read==0) {
		    &{$fh_callbacks{$fh}}($fh,undef);
		    return;
		  } else {
		    $msg_len=unpack('N', $buff);
		    $mesg="";
		    $read_stuff=1;
		  }
		} else {
		  &{$fh_callbacks{$fh}}($fh,"ERROR UNKNOWN");
		  return;
		}
	}
	while($bytes_read=sysread($fh, $mesg, $msg_len,
				  length($mesg))) {
	  $read_stuff=1;
	  $msg_len-=$bytes_read;
	  last if $msg_len==0;
	}
	if(!defined $bytes_read) {
	  $current{$fh}->{"mesg"}=$mesg;
	  $current{$fh}->{"msg_len"}=$msg_len;
	  &{$fh_callbacks{$fh}}($fh,"ERROR UNKNOWN");
	  return;
	} elsif($msg_len==0) {
	  delete $current{$fh};
	  &{$fh_callbacks{$fh}}($fh,$mesg);
	  return;
	} elsif(!defined $read_stuff) {
	  # We did not read anything on this call, so the connection is
	  # closed.
	  &{$fh_callbacks{$fh}}($fh,undef);
	  return;
	} else {
	  $current{$fh}->{"mesg"}=$mesg;
	  $current{$fh}->{"msg_len"}=$msg_len;
	}
}

# _write is called when a file handle is writable. It will push as much as it
# can in the file handle and update the message queue. If an error occurs, it
# will simply call _error.
sub _write {
	my ($fh)=@_;
	my $bytes_written;
	my $mesg=$fh_queues{$fh}->[0];
	$fh->autoflush(1);
	while($bytes_written=syswrite($fh, $mesg, length($mesg))) {
	  $mesg=substr($mesg,$bytes_written,-1);
	  if($mesg eq "") {
	    shift @{$fh_queues{$fh}};
	    unless (@{$fh_queues{$fh}}) {
	      $writeh->remove($fh);
	    }
	    return;
	  }
	}
	$fh_queues{$fh}->[0]=$mesg;
	if(!defined $bytes_written) {
		&{$fh_callbacks{$fh}}($fh,"ERROR UNKNOWN");
		$writeh->remove($fh);
	}
}

# This goes through all the files that have been registered and sees
# if there's anything on any of them. If so, calls the appropriate
# callback.
sub _check_files {
  my $line;
  my $fname;
  foreach $fname (keys %fh_files) {
    my $fh=_check_file_status($fname);
    if ($fh) {
      $line=<$fh>;
      $prev_pos{$fname}=$fh->tell;
      if (defined($line)) {
	&{$fh_callbacks{$fname}}($fh, $line);
      }
      else {
	$fh->clearerr;
      }
    }
  }
}

# Check if a file has been removed or truncated, and act accordingly.
sub _check_file_status {
  my $arg=shift;
  my $fdata=$fh_files{$arg};
  my ($fh, $fname, $cb_read, $cb_trunc, $cb_rm)=@$fdata;
  my $st=stat($fname);
#  print "In check_file_status: prev_pos{$fname}=$prev_pos{$fname}, st->size=".$st->size."\n";
  # See if the file has been removed.
  if (!$st || $st->nlink==0) {
#    print "File was removed\n";
    if ($cb_rm) {
      # Call the RM callback
      if (&{$cb_rm}($fh)) {
	# If it returns true, try to reopen
	$fh->close;
	remove_file($fname);
	add_file($fname, $cb_read, $cb_trunc, $cb_rm);
      }
      else {
	# If cb_rm returns false, simply remove callbacks.
	remove_file($fname);
      }
    }
    else {
      # If no callback is given, try to reopen.
#      print "Closing and reopening\n";
      $fh->close;
      remove_file($fname);
      add_file($fname, $cb_read, $cb_trunc, $cb_rm);
    }
  }
  # See if it has been truncated
  elsif ($st->size<$prev_pos{$fname}) {
#    print "File shrank\n";
    if (!$cb_trunc || (&{$cb_trunc}($fname)) ) {
      # If no callback was given or the callback returns true, reopen
      # file and go to the end of the file.
#      printf("Closing and reopening\n");
      $fh->close;
      add_file("+$fname", $cb_read, $cb_trunc, $cb_rm);
    }
    else {
      remove_file($fname);
    }
  }
  # Return the file handle is everything is ok.
  if (exists($fh_files{$fname})) {
    return $fh_files{$fname}->[0];
  }
  else {
    return undef;
  }
}
  
# This opens a file and returns an IO::File object.
sub _open_file {
  my $fname=shift;
  my $tail=0;
  if ($fname =~ /^\+(.*)/) {
    # If the filename begins with a '+', go to the end upon opening.
    $fname=$1;
    $tail=1;
  }
  if ($fname) {
    my $file=IO::File->new($fname);
    if (!$file) {
      return undef;
    }
    if ($tail) {
      $file->seek(0,2);
    }
    $file->clearerr;
    return $file;
  }
  return undef;
}

# I hope no error will ever occur
# Something more sensible should be find...
sub _error {
	my ($fh)=@_;
	&{$fh_callbacks{$fh}}($fh,"ERROR UNKNOWN: $!");
}

1;

=back

=head1 BUGS

The system is not robust, and there's no recovery in case of problem. The
recovery can be managed in an upper level.

=head1 AUTHOR

Frederic Dumont <fdm@cs.purdue.edu>. Modifications and additions by
Diego Zamboni <zamboni@cs.purdue.edu>.

=cut

# $Log: Reactor.pm,v $
# Revision 1.6  1999/09/03 17:08:57  zamboni
# Changed the start line to something that is path-independent, and
# updated the copyright notice.
#
# Revision 1.5  1999/06/28 21:22:08  zamboni
# Merged with a07-port-to-linux
#
# Revision 1.4.2.1  1999/06/28 19:18:17  zamboni
# - Commented out the lines that check the handles that were flagged as
#   having error codes by select, because in Linux the handles
#   corresponding to pipes were being always flagged for some strange
#   reason.
#
# Revision 1.4  1999/06/08 05:02:04  zamboni
# Merged branch a06-raw-data-collection into main trunk
#
# Revision 1.3.2.1  1999/06/07 19:59:16  zamboni
# - Modified the syntax and semantics of add_file to take a file name instead
#   of a file handle. This is necessary to be able to automatically reopen
#   the file when it is truncated or moved, which was also added. add_file can
#   also take callbacks (additionally from the regular callback for data)
#   to code to execute when the file is truncated, when the file is removed,
#   and if the file does not exist.
#
#   The default functionality for when the file is truncated is to silently
#   reopen it and keep reading. The default functionality for when the file
#   is removed is to remove its callbacks.
#
# - Added a number of hooks for logging (_Log function and calls to it)
# - Added remove_file to remove files, instead of handling them through
#   remove_handle.
#
# Revision 1.3  1999/04/01 02:43:12  zamboni
# - Added a commented debug line.
#
# Revision 1.2  1999/03/29 22:33:32  zamboni
# Merged branch a05-new-comm-module, which updates it to make use of the new event-based communication mechanism.
#
# Revision 1.1.2.7  1999/03/29 20:42:49  zamboni
# - _write now sets the autoflush flag on the handle to which it is
#   writing.
#
# Revision 1.1.2.6  1999/03/29 17:25:08  zamboni
# - Added to add_handle and add_acceptor that they return the previous
#   value of the callback for that handle, if it existed. Otherwise they
#   return undef. This makes it easier to switch handlers back and forth,
#   as is necessary for getting code for a new module
#   (see Entity::command_NEWMODULE).
# - Added internal _Log subroutine, which prints directly to /tmp/aafid.log.
#   This is a ***HACK*** and should only be used for debugging when things
#   go wrong. Never leave any calls to this routine on a production version.
# - Added add_file, which allows to set a callback on a regular file. These
#   have to be handled specially because select never blocks on a regular
#   file, instead it returns EOF when it is at the end, so adding a handle
#   for a regular file using add_handle results in a busy waiting loop
#   because select unblocks immediately every time. Apparently there is
#   no way to block on a file handle, so what this does is set a repeating
#   event (every 1 second) which calls _check_files, that monitors the file
#   and calls the callback provided by the user if there is new data.
# - Made remove_handle recognize when the handle is a file handle (it was
#   added using add_file instead of add_handle) and act accordingly.
#   Among other things, if the removed file is the last one that is being
#   monitored, the repeating event that polls the file is removed to
#   prevent wasting unnecessary cycles.
# - Added stdin(), analog to stdout().
# - Made stdout() and stdin() create their handles as aliases of STDOUT
#   and STDIN instead of duplicates.
# - Added reset(), which returns everything to its original condition,
#   removing callbacks and such.
#
# Revision 1.1.2.5  1999/03/19 17:10:02  zamboni
# - Added stdout(), which returns an IO::Handle object associated to STDOUT.
#   This object is created only the first time the routine is called and it
#   is only returned afterwards. The result of this subroutine should be
#   used as the first argument to send_message for sending a message to STDOUT
#   (instead of manually creating an IO::Handle object associated to STDOUT)
#   to ensure proper ordering of the messages sent to STDOUT.
#
# Revision 1.1.2.4  1999/03/19 15:20:07  zamboni
# - Updated documentation.
# - Updated references of Timer to Comm::Timer.
# - Added add_repeating_event and remove_repeating_event.
#
# Revision 1.1.2.3  1999/03/18 22:08:22  zamboni
# - Made loop() never return. All activities will have to be handled through
#   events: I/O events, timer events, or signals.
# - Added flush(), which processes any previously pending events and
#   returns immediately.
# - Updated documentation and sample code.
# - Added standard AAFID header comment block.
#
